<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Containerized Strands Agents</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
height: 100vh;
display: flex;
}
.sidebar {
width: 300px;
background: white;
border-right: 1px solid #e0e0e0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid #e0e0e0;
background: #fafafa;
}
.sidebar-header h1 {
font-size: 18px;
color: #333;
margin-bottom: 10px;
}
.header-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.refresh-btn {
background: #007bff;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.refresh-btn:hover {
background: #0056b3;
}
.filter-controls {
margin-top: 10px;
}
.filter-checkbox {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #6c757d;
}
.filter-checkbox input[type="checkbox"] {
margin: 0;
}
.new-agent-btn {
background: #28a745;
color: white;
border: none;
padding: 12px;
margin: 10px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: background-color 0.2s;
}
.new-agent-btn:hover {
background: #218838;
}
.new-agent-btn::before {
content: '+';
font-size: 18px;
font-weight: bold;
}
.inbox-btn {
background: #6c757d;
color: white;
border: none;
padding: 10px 12px;
margin: 0 10px 10px 10px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: background-color 0.2s;
}
.inbox-btn:hover {
background: #545b62;
}
.inbox-btn.active {
background: #007bff;
}
.inbox-btn .unread-count {
display: inline-block;
background: #dc3545;
color: white;
font-size: 10px;
padding: 1px 6px;
border-radius: 10px;
}
.inbox-view {
display: flex;
flex-direction: column;
height: 100%;
background: #fafafa;
}
.inbox-header {
padding: 20px;
border-bottom: 1px solid #e0e0e0;
background: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.inbox-header h2 {
margin: 0;
font-size: 18px;
color: #333;
}
.inbox-content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.agents-list {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.agent-item {
padding: 12px;
margin-bottom: 8px;
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.agent-item:hover {
background: #e9ecef;
}
.agent-item.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.agent-item.active .agent-status,
.agent-item.active .agent-meta {
color: rgba(255, 255, 255, 0.9);
}
.agent-info {
flex: 1;
}
.agent-id {
font-weight: 500;
font-size: 14px;
}
.agent-description {
font-size: 12px;
color: #6c757d;
margin-top: 4px;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.agent-item.active .agent-description {
color: rgba(255, 255, 255, 0.85);
}
.agent-status {
font-size: 12px;
opacity: 0.7;
margin-top: 2px;
}
.unread-dot {
display: inline-block;
width: 8px;
height: 8px;
background: #007bff;
border-radius: 50%;
margin-right: 6px;
flex-shrink: 0;
}
.agent-item.active .unread-dot {
background: white;
}
.inbox-item {
padding: 12px;
margin-bottom: 8px;
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.inbox-item:hover {
background: #e9ecef;
}
.inbox-item.unread {
background: #fff;
border-color: #007bff;
border-left: 3px solid #007bff;
}
.inbox-item-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.inbox-item-agent {
display: flex;
align-items: center;
font-weight: 500;
font-size: 14px;
}
.inbox-item.unread .inbox-item-agent {
font-weight: 600;
}
.inbox-item-time {
font-size: 11px;
color: #6c757d;
}
.inbox-item-preview {
font-size: 13px;
color: #495057;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.inbox-item.unread .inbox-item-preview {
color: #212529;
}
.inbox-empty {
text-align: center;
padding: 40px 20px;
color: #6c757d;
}
.inbox-empty-icon {
font-size: 48px;
margin-bottom: 10px;
}
.agent-meta {
font-size: 11px;
color: #6c757d;
margin-top: 4px;
display: flex;
flex-direction: column;
gap: 2px;
}
.agent-meta-row {
display: flex;
align-items: center;
gap: 4px;
}
.copy-btn {
background: transparent;
border: 1px solid #dee2e6;
color: #6c757d;
padding: 2px 6px;
border-radius: 3px;
cursor: pointer;
font-size: 10px;
margin-left: 4px;
transition: all 0.2s;
}
.copy-btn:hover {
background: #e9ecef;
border-color: #adb5bd;
}
.agent-item.active .copy-btn {
border-color: rgba(255, 255, 255, 0.5);
color: rgba(255, 255, 255, 0.9);
}
.agent-item.active .copy-btn:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.8);
}
.copy-btn.copied {
background: #28a745;
color: white;
border-color: #28a745;
}
.agent-item.active .copy-btn.copied {
background: rgba(40, 167, 69, 0.9);
border-color: rgba(40, 167, 69, 0.9);
}
.status-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 10px;
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
}
.status-running {
background: #28a745;
color: white;
}
.status-stopped {
background: #6c757d;
color: white;
}
.status-error {
background: #dc3545;
color: white;
}
.status-processing {
background: #ffc107;
color: black;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.message.tool_use,
.message.tool_result {
margin-bottom: 4px;
}
.message.tool_use .message-content,
.message.tool_result .message-content {
background: transparent;
border: none;
padding: 2px 0;
display: inline-flex;
align-items: center;
gap: 6px;
}
.message.tool_use .message-header,
.message.tool_result .message-header {
display: none;
}
.tool-icon {
font-size: 12px;
color: #6c757d;
}
.tool-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.tool-badge.tool-name {
background: #e9ecef;
color: #495057;
}
.tool-badge.status-success {
background: #d4edda;
color: #155724;
}
.tool-badge.status-error {
background: #f8d7da;
color: #721c24;
}
.tool-badge.status-unknown {
background: #e9ecef;
color: #6c757d;
}
.tool-params {
font-size: 11px;
color: #6c757d;
font-family: monospace;
max-width: 400px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
background: white;
}
.chat-header {
padding: 20px;
border-bottom: 1px solid #e0e0e0;
background: #fafafa;
}
.no-agent {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #6c757d;
font-size: 18px;
}
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
}
.messages-area {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #fafafa;
}
.message {
margin-bottom: 20px;
display: flex;
flex-direction: column;
}
.message-header {
font-size: 12px;
color: #6c757d;
margin-bottom: 5px;
font-weight: 500;
}
.message-content {
background: white;
padding: 15px;
border-radius: 8px;
border: 1px solid #e0e0e0;
white-space: pre-wrap;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.5;
}
.message.user .message-content {
background: #007bff;
color: white;
border-color: #007bff;
}
.input-area {
padding: 20px;
border-top: 1px solid #e0e0e0;
background: white;
}
.input-row {
display: flex;
gap: 10px;
}
.message-input {
flex: 1;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-family: inherit;
font-size: 14px;
resize: none;
min-height: 60px;
}
.message-input:focus {
outline: none;
border-color: #007bff;
}
.send-btn {
background: #007bff;
color: white;
border: none;
padding: 12px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
height: fit-content;
align-self: flex-end;
}
.send-btn:hover {
background: #0056b3;
}
.send-btn:disabled {
background: #6c757d;
cursor: not-allowed;
}
.options-row {
display: flex;
gap: 10px;
margin-top: 10px;
}
.option-input {
flex: 1;
padding: 8px;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 12px;
}
.loading {
text-align: center;
padding: 20px;
color: #6c757d;
font-style: italic;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 10px;
border-radius: 4px;
margin: 10px;
border: 1px solid #f5c6cb;
}
.new-agent-form {
padding: 20px;
display: flex;
flex-direction: column;
height: 100%;
background: white;
}
.form-header {
padding: 20px 0;
border-bottom: 1px solid #e0e0e0;
margin-bottom: 20px;
}
.form-header h2 {
font-size: 18px;
color: #333;
margin-bottom: 10px;
}
.form-header p {
color: #6c757d;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
font-weight: 500;
margin-bottom: 5px;
color: #333;
}
.form-input {
width: 100%;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-family: inherit;
font-size: 14px;
}
.form-input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
.form-textarea {
resize: vertical;
min-height: 80px;
}
.form-textarea.large {
min-height: 120px;
}
.form-buttons {
display: flex;
gap: 10px;
margin-top: auto;
padding-top: 20px;
}
.btn-primary {
background: #007bff;
color: white;
border: none;
padding: 12px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.btn-primary:hover {
background: #0056b3;
}
.btn-primary:disabled {
background: #6c757d;
cursor: not-allowed;
}
.btn-secondary {
background: #6c757d;
color: white;
border: none;
padding: 12px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.btn-secondary:hover {
background: #545b62;
}
.form-help {
font-size: 12px;
color: #6c757d;
margin-top: 5px;
}
@media (max-width: 768px) {
body {
flex-direction: column;
}
.sidebar {
width: 100%;
height: 200px;
}
.main-content {
height: calc(100vh - 200px);
}
}
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-header">
<h1>Agents</h1>
<div class="header-controls">
<button class="refresh-btn" onclick="loadAgents()">Refresh</button>
</div>
<div class="filter-controls">
<label class="filter-checkbox">
<input type="checkbox" id="show-all-agents" onchange="toggleShowAllAgents()">
Show all agents (including old)
</label>
</div>
</div>
<button class="new-agent-btn" onclick="showNewAgentForm()">New Agent</button>
<button class="inbox-btn" id="inbox-btn" onclick="showInbox()">
📥 Inbox <span class="unread-count" id="unread-count" style="display: none;">0</span>
</button>
<div class="agents-list" id="agents-list">
<div class="loading">Loading agents...</div>
</div>
</div>
<div class="main-content">
<div id="inbox-view" class="inbox-view">
<div class="inbox-header">
<h2>📥 Inbox</h2>
<button class="refresh-btn" onclick="loadInbox()">Refresh</button>
</div>
<div class="inbox-content" id="inbox-content">
<div class="loading">Loading inbox...</div>
</div>
</div>
<div id="no-agent" class="no-agent" style="display: none;">
Select an agent to start chatting
</div>
<div id="chat-container" class="chat-container" style="display: none;">
<div class="chat-header">
<h2 id="chat-title">Agent Chat</h2>
</div>
<div class="messages-area" id="messages-area">
<div class="loading">Loading messages...</div>
</div>
<div class="input-area">
<div class="input-row">
<textarea
id="message-input"
class="message-input"
placeholder="Type your message..."
onkeydown="handleKeyDown(event)"
></textarea>
<button class="send-btn" onclick="sendMessage()" id="send-btn">Send</button>
</div>
<div class="options-row">
<input
type="text"
id="aws-profile"
class="option-input"
placeholder="AWS Profile (optional)"
/>
<input
type="text"
id="aws-region"
class="option-input"
placeholder="AWS Region (optional)"
/>
<input
type="text"
id="system-prompt"
class="option-input"
placeholder="System Prompt (new agents only)"
/>
</div>
</div>
</div>
</div>
<script>
let currentAgentId = null;
let refreshInterval = null;
let messagesInterval = null;
let allAgents = []; // Store all agents for filtering
let inboxItems = []; // Store inbox items
// Show inbox view
function showInbox() {
currentAgentId = null;
// Remove new agent form if open
const newAgentForm = document.getElementById('new-agent-form');
if (newAgentForm) newAgentForm.remove();
// Update UI
document.getElementById('inbox-view').style.display = 'flex';
document.getElementById('no-agent').style.display = 'none';
document.getElementById('chat-container').style.display = 'none';
// Update inbox button state
document.getElementById('inbox-btn').classList.add('active');
// Clear agent selection in sidebar
renderAgents(getFilteredAndSortedAgents());
// Load inbox
loadInbox();
}
// Auto-refresh every 3 seconds
function startAutoRefresh() {
refreshInterval = setInterval(() => {
loadAgents();
loadInboxData(); // Just update data, not full render
if (currentAgentId) {
loadMessages(currentAgentId);
}
}, 3000);
}
function stopAutoRefresh() {
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
}
if (messagesInterval) {
clearInterval(messagesInterval);
messagesInterval = null;
}
}
// Load inbox data (for badge updates)
async function loadInboxData() {
try {
const response = await fetch('/inbox');
const data = await response.json();
if (data.status === 'success') {
inboxItems = data.items;
updateUnreadCount();
}
} catch (error) {
console.error('Error loading inbox data:', error);
}
}
// Load and render inbox
async function loadInbox() {
try {
const response = await fetch('/inbox');
const data = await response.json();
if (data.status === 'success') {
inboxItems = data.items;
renderInbox(inboxItems);
updateUnreadCount();
} else {
showError('Failed to load inbox');
}
} catch (error) {
showError('Error loading inbox: ' + error.message);
}
}
// Render inbox items in main content
function renderInbox(items) {
const inboxContent = document.getElementById('inbox-content');
if (items.length === 0) {
inboxContent.innerHTML = `
<div class="inbox-empty">
<div class="inbox-empty-icon">📭</div>
<div>No agent responses yet</div>
<div style="font-size: 12px; margin-top: 5px;">Create an agent to get started</div>
</div>
`;
return;
}
// Filter to only show items with responses
const itemsWithResponses = items.filter(item => item.last_response_preview);
if (itemsWithResponses.length === 0) {
inboxContent.innerHTML = `
<div class="inbox-empty">
<div class="inbox-empty-icon">📭</div>
<div>No agent responses yet</div>
<div style="font-size: 12px; margin-top: 5px;">Send a message to an agent to see responses here</div>
</div>
`;
return;
}
inboxContent.innerHTML = itemsWithResponses.map(item => {
const timeText = formatLastActivity(item.last_activity);
const isUnread = item.has_unread;
const statusBadge = item.processing ?
'<span class="status-badge status-processing">Processing</span>' : '';
return `
<div class="inbox-item ${isUnread ? 'unread' : ''}" onclick="selectAgent('${item.agent_id}')">
<div class="inbox-item-header">
<div class="inbox-item-agent">
${isUnread ? '<span class="unread-dot"></span>' : ''}
${escapeHtml(item.agent_id)}
${statusBadge}
</div>
<div class="inbox-item-time">${timeText}</div>
</div>
<div class="inbox-item-preview">${escapeHtml(item.last_response_preview || 'No response yet')}</div>
</div>
`;
}).join('');
}
// Update unread count badge
function updateUnreadCount() {
const unreadCount = inboxItems.filter(item => item.has_unread && item.last_response_preview).length;
const badge = document.getElementById('unread-count');
if (unreadCount > 0) {
badge.textContent = unreadCount;
badge.style.display = 'inline-block';
} else {
badge.style.display = 'none';
}
}
// Load agents from API
async function loadAgents() {
try {
const response = await fetch('/agents');
const data = await response.json();
if (data.status === 'success') {
allAgents = data.agents; // Store all agents
renderAgents(getFilteredAndSortedAgents());
} else {
showError('Failed to load agents');
}
} catch (error) {
showError('Error loading agents: ' + error.message);
}
}
// Get filtered and sorted agents based on current settings
function getFilteredAndSortedAgents() {
const showAll = document.getElementById('show-all-agents').checked;
let agents = [...allAgents];
// Filter out old agents (older than 7 days) if not showing all
if (!showAll) {
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
agents = agents.filter(agent => {
if (!agent.last_activity) {
// If no last_activity, include only if created recently
if (!agent.created_at) return true;
const createdAt = new Date(agent.created_at);
return createdAt > sevenDaysAgo;
}
const lastActivity = new Date(agent.last_activity);
return lastActivity > sevenDaysAgo;
});
}
// Sort by last_activity (newest first)
agents.sort((a, b) => {
const aTime = a.last_activity ? new Date(a.last_activity) :
a.created_at ? new Date(a.created_at) : new Date(0);
const bTime = b.last_activity ? new Date(b.last_activity) :
b.created_at ? new Date(b.created_at) : new Date(0);
return bTime - aTime; // Descending order (newest first)
});
return agents;
}
// Toggle show all agents
function toggleShowAllAgents() {
renderAgents(getFilteredAndSortedAgents());
}
// Render agents in sidebar
function renderAgents(agents) {
const agentsList = document.getElementById('agents-list');
if (agents.length === 0) {
const showAll = document.getElementById('show-all-agents').checked;
const message = showAll ? 'No agents found' : 'No recent agents found (toggle "Show all agents" to see older ones)';
agentsList.innerHTML = `<div class="loading">${message}</div>`;
return;
}
agentsList.innerHTML = agents.map(agent => {
const lastActivityText = formatLastActivity(agent.last_activity || agent.created_at);
const dataDir = agent.data_dir || 'N/A';
const port = agent.port || 'N/A';
const description = agent.description || '';
return `
<div class="agent-item ${agent.agent_id === currentAgentId ? 'active' : ''}"
onclick="selectAgent('${agent.agent_id}')">
<div class="agent-info">
<div class="agent-id">${agent.agent_id}</div>
${description ? `<div class="agent-description">${escapeHtml(description)}</div>` : ''}
<div class="agent-status">
<span class="status-badge status-${agent.status}">${agent.status}</span>
${agent.processing ? '<span class="status-badge status-processing">Processing</span>' : ''}
</div>
<div class="agent-status" style="margin-top: 4px; font-size: 11px;">
Last active: ${lastActivityText}
</div>
<div class="agent-meta">
<div class="agent-meta-row">
<span>Port: ${port}</span>
</div>
<div class="agent-meta-row">
<span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${escapeHtml(dataDir)}">
Path: ${escapeHtml(dataDir)}
</span>
</div>
<div class="agent-meta-row">
<button class="copy-btn" onclick="copyKiroCommand(event, '${escapeHtml(dataDir)}')" title="Copy 'kiro ${escapeHtml(dataDir)}' to clipboard">
📋 Copy kiro command
</button>
</div>
</div>
</div>
</div>
`;
}).join('');
}
// Format last activity time for display
function formatLastActivity(dateString) {
if (!dateString) return 'Unknown';
const date = new Date(dateString);
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffMins < 1) {
return 'Just now';
} else if (diffMins < 60) {
return `${diffMins}m ago`;
} else if (diffHours < 24) {
return `${diffHours}h ago`;
} else if (diffDays < 7) {
return `${diffDays}d ago`;
} else {
return date.toLocaleDateString();
}
}
// Select an agent
async function selectAgent(agentId) {
currentAgentId = agentId;
// Remove new agent form if open
const newAgentForm = document.getElementById('new-agent-form');
if (newAgentForm) newAgentForm.remove();
// Update UI - hide inbox, show chat
document.getElementById('inbox-view').style.display = 'none';
document.getElementById('no-agent').style.display = 'none';
document.getElementById('chat-container').style.display = 'flex';
document.getElementById('chat-title').textContent = `Chat with ${agentId}`;
// Update inbox button state
document.getElementById('inbox-btn').classList.remove('active');
// Update active agent in sidebar (re-render with current filter)
renderAgents(getFilteredAndSortedAgents());
// Load messages
await loadMessages(agentId);
}
// Load messages for current agent
async function loadMessages(agentId) {
try {
const response = await fetch(`/agents/${agentId}/messages?count=20`);
const data = await response.json();
if (data.status === 'success') {
renderMessages(data.messages);
} else if (response.status === 404) {
// Agent doesn't exist yet, show empty chat
renderMessages([]);
} else {
showError('Failed to load messages');
}
} catch (error) {
showError('Error loading messages: ' + error.message);
}
}
// Render messages - handles raw Strands message format
function renderMessages(messages) {
const messagesArea = document.getElementById('messages-area');
// Check if user is near bottom before re-rendering (within 100px)
const wasNearBottom = messagesArea.scrollHeight - messagesArea.scrollTop - messagesArea.clientHeight < 100;
if (messages.length === 0) {
messagesArea.innerHTML = '<div class="loading">No messages yet. Send a message to start the conversation.</div>';
return;
}
// Process raw Strands messages into display format
const displayMessages = [];
for (const msg of messages) {
const content = msg.content || [];
if (msg.role === 'user') {
// Check for tool_result in content
const toolResult = Array.isArray(content) && content.find(c => c.type === 'tool_result' || c.toolResult);
if (toolResult) {
const result = toolResult.toolResult || toolResult;
const status = result.status || (result.is_error ? 'error' : 'success');
displayMessages.push({ type: 'tool_result', status });
} else {
// Regular user message
const text = extractText(content);
if (text) displayMessages.push({ type: 'user', content: text });
}
} else if (msg.role === 'assistant') {
// Extract text and tool_use from content
if (Array.isArray(content)) {
let text = '';
for (const item of content) {
if (item.type === 'text' || item.text) {
text += (item.text || '') + '\n';
} else if (item.type === 'tool_use' || item.toolUse) {
// Add any accumulated text first
if (text.trim()) {
displayMessages.push({ type: 'assistant', content: text.trim() });
text = '';
}
const toolData = item.toolUse || item;
displayMessages.push({
type: 'tool_use',
tool: toolData.name || 'unknown',
input: toolData.input || null
});
}
}
// Add remaining text
if (text.trim()) {
displayMessages.push({ type: 'assistant', content: text.trim() });
}
} else if (typeof content === 'string') {
displayMessages.push({ type: 'assistant', content });
}
}
}
messagesArea.innerHTML = displayMessages.map(msg => {
if (msg.type === 'tool_use') {
const inputStr = msg.input ? formatToolInput(msg.input) : '';
return `
<div class="message tool_use">
<div class="message-content">
<span class="tool-icon">⚙️</span>
<span class="tool-badge tool-name">${escapeHtml(msg.tool)}</span>
${inputStr ? `<span class="tool-params">${escapeHtml(inputStr)}</span>` : ''}
</div>
</div>
`;
} else if (msg.type === 'tool_result') {
const statusClass = msg.status === 'success' ? 'status-success' :
msg.status === 'error' ? 'status-error' : 'status-unknown';
const icon = msg.status === 'success' ? '✓' : msg.status === 'error' ? '✗' : '?';
return `
<div class="message tool_result">
<div class="message-content">
<span class="tool-icon" style="margin-left: 22px;">↳</span>
<span class="tool-badge ${statusClass}">${icon}</span>
</div>
</div>
`;
} else {
return `
<div class="message ${msg.type}">
<div class="message-header">${msg.type.toUpperCase()}</div>
<div class="message-content">${escapeHtml(msg.content || '')}</div>
</div>
`;
}
}).join('');
// Only auto-scroll if user was already near the bottom
if (wasNearBottom) {
messagesArea.scrollTop = messagesArea.scrollHeight;
}
}
// Format tool input for display
function formatToolInput(input) {
if (!input) return '';
if (typeof input === 'string') return input;
// For objects, show key=value pairs, truncating long values
const parts = [];
for (const [key, value] of Object.entries(input)) {
let valStr = typeof value === 'string' ? value : JSON.stringify(value);
// Truncate long values
if (valStr.length > 50) {
valStr = valStr.substring(0, 47) + '...';
}
parts.push(`${key}=${valStr}`);
}
return parts.join(', ');
}
// Extract text from Strands content array
function extractText(content) {
if (typeof content === 'string') return content;
if (!Array.isArray(content)) return '';
return content
.filter(c => c.type === 'text' || c.text)
.map(c => c.text || '')
.join('\n');
}
// Send message
async function sendMessage() {
if (!currentAgentId) return;
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const message = messageInput.value.trim();
if (!message) return;
// Get optional parameters
const awsProfile = document.getElementById('aws-profile').value.trim() || null;
const awsRegion = document.getElementById('aws-region').value.trim() || null;
const systemPrompt = document.getElementById('system-prompt').value.trim() || null;
// Disable input
messageInput.disabled = true;
sendBtn.disabled = true;
sendBtn.textContent = 'Sending...';
try {
const response = await fetch(`/agents/${currentAgentId}/message`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: message,
aws_profile: awsProfile,
aws_region: awsRegion,
system_prompt: systemPrompt
})
});
const data = await response.json();
if (data.status === 'dispatched' || data.status === 'queued') {
// Clear input
messageInput.value = '';
// Immediately add user message to UI
addMessageToUI('user', message);
// Start polling for response
pollForResponse();
} else {
showError(data.error || 'Failed to send message');
}
} catch (error) {
showError('Error sending message: ' + error.message);
} finally {
// Re-enable input
messageInput.disabled = false;
sendBtn.disabled = false;
sendBtn.textContent = 'Send';
messageInput.focus();
}
}
// Add message to UI immediately
function addMessageToUI(role, content) {
const messagesArea = document.getElementById('messages-area');
// If no messages, clear loading text
if (messagesArea.innerHTML.includes('No messages yet')) {
messagesArea.innerHTML = '';
}
const messageDiv = document.createElement('div');
messageDiv.className = `message ${role}`;
messageDiv.innerHTML = `
<div class="message-header">${role.toUpperCase()}</div>
<div class="message-content">${escapeHtml(content)}</div>
`;
messagesArea.appendChild(messageDiv);
messagesArea.scrollTop = messagesArea.scrollHeight;
}
// Poll for response after sending message
function pollForResponse() {
let attempts = 0;
const maxAttempts = 40; // 2 minutes at 3 second intervals
const poll = async () => {
attempts++;
try {
const response = await fetch(`/agents/${currentAgentId}/messages?count=1`);
const data = await response.json();
if (data.status === 'success' && data.messages.length > 0) {
const lastMessage = data.messages[data.messages.length - 1];
// Check if this is a new assistant message
const messagesArea = document.getElementById('messages-area');
const lastDisplayed = messagesArea.querySelector('.message:last-child .message-header');
if (lastMessage.role === 'assistant' &&
(!lastDisplayed || !lastDisplayed.textContent.includes('ASSISTANT'))) {
// Load all messages to get complete conversation
loadMessages(currentAgentId);
return; // Stop polling
}
}
// Continue polling if not processing and within attempt limit
if (!data.processing && attempts < maxAttempts) {
setTimeout(poll, 3000);
} else if (attempts >= maxAttempts) {
showError('Response timeout. The agent may be taking longer than expected.');
} else {
// Still processing, continue polling
setTimeout(poll, 3000);
}
} catch (error) {
console.error('Error polling for response:', error);
if (attempts < maxAttempts) {
setTimeout(poll, 3000);
}
}
};
// Start polling after a short delay
setTimeout(poll, 1000);
}
// Handle Enter key in message input
function handleKeyDown(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
// Show error message
function showError(message) {
const error = document.createElement('div');
error.className = 'error';
error.textContent = message;
document.body.appendChild(error);
setTimeout(() => {
error.remove();
}, 5000);
}
// Show new agent form
function showNewAgentForm() {
// Hide inbox, chat, show form
document.getElementById('inbox-view').style.display = 'none';
document.getElementById('no-agent').style.display = 'none';
document.getElementById('chat-container').style.display = 'none';
// Update inbox button state
document.getElementById('inbox-btn').classList.remove('active');
// Remove existing form if any
const existingForm = document.getElementById('new-agent-form');
if (existingForm) existingForm.remove();
// Create form HTML
const formHtml = `
<div id="new-agent-form" class="new-agent-form">
<div class="form-header">
<h2>Create New Agent</h2>
<p>Configure and start a new AI agent in an isolated Docker container.</p>
</div>
<div class="form-group">
<label class="form-label">Agent ID *</label>
<input type="text" id="new-agent-id" class="form-input"
placeholder="e.g., code-reviewer, data-analyst, my-helper"
pattern="[a-z0-9-]+" />
<div class="form-help">Lowercase letters, numbers, and hyphens only. This will be the agent's unique identifier.</div>
</div>
<div class="form-group">
<label class="form-label">Description (optional)</label>
<input type="text" id="new-agent-description" class="form-input"
placeholder="e.g., Reviews Python code for best practices" />
<div class="form-help">Brief description to help identify this agent later.</div>
</div>
<div class="form-group">
<label class="form-label">System Prompt (optional)</label>
<textarea id="new-agent-system-prompt" class="form-input form-textarea large"
placeholder="You are a helpful assistant specialized in..."></textarea>
<div class="form-help">Define the agent's personality, expertise, and behavior. Leave empty for default.</div>
</div>
<div class="form-group">
<label class="form-label">Initial Message *</label>
<textarea id="new-agent-message" class="form-input form-textarea"
placeholder="Hello! Please help me with..."></textarea>
<div class="form-help">The first message to send to the agent to start the conversation.</div>
</div>
<div style="display: flex; gap: 15px;">
<div class="form-group" style="flex: 1;">
<label class="form-label">AWS Profile (optional)</label>
<input type="text" id="new-agent-aws-profile" class="form-input"
placeholder="default" />
<div class="form-help">AWS credentials profile for Bedrock access.</div>
</div>
<div class="form-group" style="flex: 1;">
<label class="form-label">AWS Region (optional)</label>
<input type="text" id="new-agent-aws-region" class="form-input"
placeholder="us-east-1" />
<div class="form-help">AWS region for Bedrock API calls.</div>
</div>
</div>
<div class="form-buttons">
<button class="btn-secondary" onclick="cancelNewAgent()">Cancel</button>
<button class="btn-primary" id="create-agent-btn" onclick="createNewAgent()">Create Agent</button>
</div>
</div>
`;
document.querySelector('.main-content').insertAdjacentHTML('beforeend', formHtml);
// Focus the agent ID input
setTimeout(() => document.getElementById('new-agent-id').focus(), 100);
// Clear current agent selection
currentAgentId = null;
renderAgents(getFilteredAndSortedAgents());
}
// Cancel new agent form
function cancelNewAgent() {
const form = document.getElementById('new-agent-form');
if (form) form.remove();
// Show inbox view
showInbox();
}
// Create new agent
async function createNewAgent() {
const agentId = document.getElementById('new-agent-id').value.trim();
const description = document.getElementById('new-agent-description').value.trim() || null;
const systemPrompt = document.getElementById('new-agent-system-prompt').value.trim() || null;
const message = document.getElementById('new-agent-message').value.trim();
const awsProfile = document.getElementById('new-agent-aws-profile').value.trim() || null;
const awsRegion = document.getElementById('new-agent-aws-region').value.trim() || null;
// Validation
if (!agentId) {
showError('Agent ID is required');
document.getElementById('new-agent-id').focus();
return;
}
if (!/^[a-z0-9-]+$/.test(agentId)) {
showError('Agent ID can only contain lowercase letters, numbers, and hyphens');
document.getElementById('new-agent-id').focus();
return;
}
if (!message) {
showError('Initial message is required');
document.getElementById('new-agent-message').focus();
return;
}
// Check if agent already exists
if (allAgents.some(a => a.agent_id === agentId)) {
showError(`Agent "${agentId}" already exists. Choose a different ID or select it from the list.`);
document.getElementById('new-agent-id').focus();
return;
}
// Disable button
const btn = document.getElementById('create-agent-btn');
btn.disabled = true;
btn.textContent = 'Creating...';
try {
const response = await fetch(`/agents/${agentId}/message`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: message,
aws_profile: awsProfile,
aws_region: awsRegion,
system_prompt: systemPrompt,
description: description
})
});
const data = await response.json();
if (data.status === 'dispatched' || data.status === 'queued') {
// Remove form
const form = document.getElementById('new-agent-form');
if (form) form.remove();
// Refresh agents list and select the new agent
await loadAgents();
await selectAgent(agentId);
// Add the user message to UI immediately
addMessageToUI('user', message);
// Start polling for response
pollForResponse();
} else {
showError(data.error || 'Failed to create agent');
btn.disabled = false;
btn.textContent = 'Create Agent';
}
} catch (error) {
showError('Error creating agent: ' + error.message);
btn.disabled = false;
btn.textContent = 'Create Agent';
}
}
// Escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Copy kiro command to clipboard
function copyKiroCommand(event, dataDir) {
// Prevent event bubbling to selectAgent
event.stopPropagation();
const command = `kiro ${dataDir}`;
// Copy to clipboard
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(command)
.then(() => {
// Visual feedback
const btn = event.target.closest('.copy-btn');
const originalText = btn.innerHTML;
btn.classList.add('copied');
btn.innerHTML = '✓ Copied!';
setTimeout(() => {
btn.classList.remove('copied');
btn.innerHTML = originalText;
}, 2000);
})
.catch(err => {
console.error('Failed to copy:', err);
showError('Failed to copy command to clipboard');
});
} else {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = command;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
const btn = event.target.closest('.copy-btn');
const originalText = btn.innerHTML;
btn.classList.add('copied');
btn.innerHTML = '✓ Copied!';
setTimeout(() => {
btn.classList.remove('copied');
btn.innerHTML = originalText;
}, 2000);
} catch (err) {
console.error('Failed to copy:', err);
showError('Failed to copy command to clipboard');
} finally {
document.body.removeChild(textArea);
}
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadAgents();
loadInbox();
document.getElementById('inbox-btn').classList.add('active');
startAutoRefresh();
});
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
stopAutoRefresh();
});
</script>
</body>
</html>