Skip to content

feat: improve UI and add agent-to-agent chat#2

Open
narutoZed wants to merge 1 commit intomainfrom
feature/openclaw-bot-chat
Open

feat: improve UI and add agent-to-agent chat#2
narutoZed wants to merge 1 commit intomainfrom
feature/openclaw-bot-chat

Conversation

@narutoZed
Copy link
Copy Markdown
Owner

Summary

  • 改进聊天界面 UI,新增现代 CSS 样式
  • 修复发送消息不显示的问题
  • 添加 Agent 选择器,可以触发特定 agent 回复
  • 通过 WebSocket 广播 agent 响应

Test plan

  • 刷新前端页面查看新 UI
  • 发送消息确认自己消息可见
  • 选择一个 agent 发送消息,确认收到回复

🤖 Generated with Claude Code

- Add CSS styles for modern chat interface
- Fix sent messages not being displayed locally
- Add agent selector to trigger specific agent responses
- Broadcast agent responses via WebSocket
- Distinguish sent/received/system messages visually

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 7, 2026 01:28
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the chat app UI to a modern styled layout, adds agent selection for directing messages to specific agents, and introduces backend WebSocket broadcasting of agent responses for real-time updates.

Changes:

  • Replaced inline styles with a new CSS-based layout and improved chat UX (message types, auto-scroll, loading state).
  • Added agent selection in the UI and a /api/chat flow to request an agent response.
  • Backend now derives agent lists via the OpenCLAW CLI and broadcasts agent replies over WebSocket.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 12 comments.

File Description
frontend/src/App.tsx New UI structure, WebSocket handling, agent selection, and combined WS + REST chat sending logic
frontend/src/App.css Adds a full modern styling system for sidebar/chat/input components
backend/websocket_manager.py Adds send_to() and keeps broadcast-based delivery for chat events
backend/main.py Adds CLI-based agent discovery and broadcasts agent responses via WebSocket from /api/chat

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/main.py
agents = []
for line in output.split("\n"):
# Match "- agentName" or "- agentName (default)" format
match = re.match(r"^- (\w+)", line)
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parsing regex r"^- (\w+)" only matches word characters (letters/digits/underscore). If OpenCLAW agent IDs include hyphens, dots, or other valid characters, those agents will be silently dropped from the returned list. Consider matching up to whitespace/parenthesis instead (e.g., until ( or end-of-line).

Suggested change
match = re.match(r"^- (\w+)", line)
match = re.match(r"^- ([^(\s]+)", line)

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/App.tsx
Comment on lines 45 to +46
const socket = new WebSocket(`ws://${window.location.host}/ws/user`)


Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client always connects to /ws/user, so every browser instance shares the same client_id. On the server, active_connections is keyed by client_id, so new connections will overwrite older ones and broadcasts/exclusions won’t behave correctly for multiple clients. Generate a stable unique client id per browser (e.g., crypto.randomUUID() stored in localStorage) and use it in the WebSocket URL.

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/App.tsx
Comment on lines +242 to +249
<div
key={agent.id}
className={`agent-chip ${selectedAgent === agent.id ? 'selected' : ''}`}
onClick={() => setSelectedAgent(selectedAgent === agent.id ? null : agent.id)}
>
<span className="agent-chip-emoji">{agent.emoji || '🤖'}</span>
{agent.name}
</div>
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agent-chip is a clickable div without keyboard interaction/semantics, which is not accessible. Consider rendering these chips as button elements (or add role/tabIndex/key handlers) so they can be used via keyboard and announced correctly by assistive tech.

Suggested change
<div
key={agent.id}
className={`agent-chip ${selectedAgent === agent.id ? 'selected' : ''}`}
onClick={() => setSelectedAgent(selectedAgent === agent.id ? null : agent.id)}
>
<span className="agent-chip-emoji">{agent.emoji || '🤖'}</span>
{agent.name}
</div>
<button
key={agent.id}
type="button"
className={`agent-chip ${selectedAgent === agent.id ? 'selected' : ''}`}
onClick={() => setSelectedAgent(selectedAgent === agent.id ? null : agent.id)}
aria-pressed={selectedAgent === agent.id}
>
<span className="agent-chip-emoji">{agent.emoji || '🤖'}</span>
{agent.name}
</button>

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/App.tsx
Comment on lines +92 to +129
id: Date.now().toString(),
sender: 'user',
senderName: 'You',
content: messageText,
timestamp: new Date(),
type: 'sent'
}
setMessages(prev => [...prev, userMessage])
setInput('')

// Also broadcast via WebSocket for real-time sync
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(messageText)
}

// If an agent is selected, send to that agent via API
if (currentAgent) {
setIsLoading(true)
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: messageText,
agent_id: currentAgent
})
})

if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}

const result = await response.json()

// Add agent's response to the chat
const agentResponse: Message = {
id: (Date.now() + 1).toString(),
sender: currentAgent,
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Message IDs are generated from Date.now() (and Date.now() + 1), which can collide when multiple messages are created within the same millisecond (leading to duplicate React keys and rendering issues). Use a monotonic counter or crypto.randomUUID() for stable unique IDs.

Copilot uses AI. Check for mistakes.
Comment thread backend/main.py
Comment on lines 53 to 66
@app.get("/api/agents")
async def get_agents():
"""Get list of agents from OpenCLAW Gateway."""
try:
agents = await openclaw_client.get_agents()
agents = get_agents_from_openclaw()
if not agents:
# Fallback to default agents
agents = [
{"id": "main", "name": "Main Agent", "emoji": "🤖"},
{"id": "default", "name": "Default Agent", "emoji": "🦊"},
]
return {"agents": agents}
except Exception as e:
raise HTTPException(status_code=503, detail=str(e))
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/api/agents now calls get_agents_from_openclaw() which catches all exceptions and returns [], causing this endpoint to return 200 with fallback agents even when OpenCLAW is unavailable. The existing test suite expects a 503 on upstream failure (see backend/tests/test_routes.py::test_get_agents_gateway_unavailable). Either update the endpoint contract/tests, or let failures propagate so the route can return a 503 consistently.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +39
async def send_to(self, client_id: str, message: dict):
"""Send a message to a specific client."""
if client_id in self.active_connections:
try:
await self.active_connections[client_id].send_json(message)
except Exception:
self.disconnect(client_id)
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new send_to() method was added but there’s no corresponding test coverage, even though this repo already has a backend/tests/test_websocket.py suite for WebSocketManager. Add tests that verify send_to() sends JSON to the right connection and disconnects on send failure.

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/App.tsx
@@ -30,76 +43,240 @@ function App() {

// Connect WebSocket
const socket = new WebSocket(`ws://${window.location.host}/ws/user`)
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WebSocket URL is hard-coded to ws://, which will be blocked as mixed-content when the app is served over HTTPS. Build the URL using window.location.protocol (wss for https, ws otherwise) to make the client work in secure deployments.

Suggested change
const socket = new WebSocket(`ws://${window.location.host}/ws/user`)
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
const socket = new WebSocket(`${wsProtocol}://${window.location.host}/ws/user`)

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/App.tsx
Comment on lines +125 to +135

// Add agent's response to the chat
const agentResponse: Message = {
id: (Date.now() + 1).toString(),
sender: currentAgent,
senderName: currentAgent,
content: result.response || result.message || JSON.stringify(result),
timestamp: new Date(),
type: 'received'
}
setMessages(prev => [...prev, agentResponse])
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an agent is selected, the agent reply will be added twice: once locally after the /api/chat fetch resolves, and again when the backend broadcasts the same agent response over WebSocket. To avoid duplicate messages, either rely solely on the WebSocket broadcast for agent replies or stop broadcasting to the initiating client (e.g., include a client_id in the request and exclude it server-side).

Suggested change
// Add agent's response to the chat
const agentResponse: Message = {
id: (Date.now() + 1).toString(),
sender: currentAgent,
senderName: currentAgent,
content: result.response || result.message || JSON.stringify(result),
timestamp: new Date(),
type: 'received'
}
setMessages(prev => [...prev, agentResponse])
// Rely on WebSocket broadcasts to add the agent's response to the chat.

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/App.tsx
Comment on lines +175 to +179
<div
key={agent.id}
className={`agent-item ${selectedAgent === agent.id ? 'active' : ''}`}
onClick={() => setSelectedAgent(selectedAgent === agent.id ? null : agent.id)}
>
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agent-item is a clickable div without keyboard interaction or semantics, which makes the agent list inaccessible to keyboard/screen-reader users. Use a button (preferred) or add role="button", tabIndex={0}, and an onKeyDown handler for Enter/Space.

Copilot uses AI. Check for mistakes.
Comment thread backend/main.py
Comment on lines +27 to +32
def get_agents_from_openclaw() -> list[dict]:
"""Get agents from OpenCLAW CLI."""
try:
output = subprocess.check_output(
["openclaw", "agents", "list"], timeout=10, text=True
)
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subprocess.check_output(...) is called from within an async request path (/api/agents). Because it’s synchronous, it will block the event loop while the CLI runs (up to the 10s timeout), reducing throughput for all requests. Run the CLI call in a thread (asyncio.to_thread) or use an async subprocess API, and consider caching results to avoid running the CLI on every request.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants