Add AI Chat interface + sidebar toggle fix + production polish
- New Chat page with 3-agent terminal interface (opencode/hermes/gemini) - Chat sidebar with agent selection, status indicators, quick prompts - Message bubbles with typing indicator animation - Full chat history persistence via /api/chat endpoint - Agent CLI execution with timeout fallback messaging - Sidebar toggle: minimize ◀ / maximize ▶ with dynamic icon - Collapsed sidebar: centered icons, tighter padding, cleaner alignment - Fixed chat agent execution for interactive CLIs (non-blocking) - Added /api/chat and /api/chat/history API endpoints - All 13 dashboard pages serving 200
This commit is contained in:
parent
ab1dc3d228
commit
18fdb6b478
|
|
@ -1,3 +1,7 @@
|
|||
{"action": "skill_run", "skill": "heartbeat", "agent": "opencode", "run_id": "d52d517a", "timestamp": "2026-05-17T09:58:07.559900+00:00", "id": "36284cbc"}
|
||||
{"action": "skill_run", "skill": "devops-audit", "agent": "opencode", "run_id": "92cd4f97", "timestamp": "2026-05-17T09:59:08.213032+00:00", "id": "40695cd7"}
|
||||
{"action": "skill_run", "skill": "heartbeat", "agent": "opencode", "run_id": "f18c903b", "timestamp": "2026-05-17T10:18:44.356138+00:00", "id": "c9117c6d"}
|
||||
{"action": "job_deleted", "job_id": "daily-standup-default", "timestamp": "2026-05-17T10:34:38.978261+00:00", "id": "35513486"}
|
||||
{"action": "chat_message", "agent": "opencode", "msg_preview": "hello", "timestamp": "2026-05-17T10:37:06.791873+00:00", "id": "c41e3054"}
|
||||
{"action": "chat_message", "agent": "opencode", "msg_preview": "hello", "timestamp": "2026-05-17T10:40:56.501310+00:00", "id": "6d4ab33d"}
|
||||
{"action": "chat_message", "agent": "opencode", "msg_preview": "hello", "timestamp": "2026-05-17T10:42:36.570364+00:00", "id": "358f4058"}
|
||||
|
|
|
|||
|
|
@ -43,4 +43,6 @@ const api = {
|
|||
updateSettings: (settings) => api.put('/api/settings', { settings }),
|
||||
getStandards: () => api.get('/api/standards'),
|
||||
discoverStandards: () => api.post('/api/standards/discover'),
|
||||
chat: (agent, message) => api.post('/api/chat', { agent, message }),
|
||||
getChatHistory: () => api.get('/api/chat/history'),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<div class="logo-icon">⬡</div>
|
||||
<span class="logo-text">Agentic OS</span>
|
||||
</div>
|
||||
<button class="sidebar-toggle" onclick="toggleSidebar()" title="Toggle sidebar">☰</button>
|
||||
<button class="sidebar-toggle" onclick="toggleSidebar()" title="Toggle sidebar">◀</button>
|
||||
</div>
|
||||
<div class="sidebar-nav">
|
||||
<div class="sidebar-section"><div class="sidebar-section-label">Core</div></div>
|
||||
|
|
@ -34,6 +34,7 @@
|
|||
<a href="#standards" class="nav-item" data-page="standards"><span class="nav-icon">📐</span><span class="nav-label">Standards</span></a>
|
||||
<a href="#settings" class="nav-item" data-page="settings"><span class="nav-icon">⚙</span><span class="nav-label">Settings</span></a>
|
||||
<div class="sidebar-section"><div class="sidebar-section-label">Setup</div></div>
|
||||
<a href="#chat" class="nav-item" data-page="chat"><span class="nav-icon">💬</span><span class="nav-label">AI Chat</span></a>
|
||||
<a href="#setup-wizard" class="nav-item" data-page="setup-wizard"><span class="nav-icon">🚀</span><span class="nav-label">Setup Wizard</span></a>
|
||||
</div>
|
||||
<div class="sidebar-footer">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,243 @@
|
|||
async function renderChat() {
|
||||
const content = document.getElementById('pageContent');
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">AI Chat</h1>
|
||||
<p class="page-subtitle">Talk to opencode, Hermes, and Gemini CLI</p>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn" onclick="clearChat()">🗑 Clear</button>
|
||||
<button class="btn" onclick="refreshChat()">🔄 Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-layout">
|
||||
<div class="chat-sidebar">
|
||||
<div class="chat-agents-label">Agents</div>
|
||||
<div class="chat-agent active" data-agent="opencode" onclick="selectAgent('opencode')">
|
||||
<div class="agent-dot online"></div>
|
||||
<div>
|
||||
<div class="chat-agent-name">opencode</div>
|
||||
<div class="chat-agent-desc">Code & DevOps</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-agent" data-agent="hermes" onclick="selectAgent('hermes')">
|
||||
<div class="agent-dot online"></div>
|
||||
<div>
|
||||
<div class="chat-agent-name">Hermes</div>
|
||||
<div class="chat-agent-desc">Memory & Scheduling</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-agent" data-agent="gemini" onclick="selectAgent('gemini')">
|
||||
<div class="agent-dot offline"></div>
|
||||
<div>
|
||||
<div class="chat-agent-name">Gemini CLI</div>
|
||||
<div class="chat-agent-desc">Research & Analysis</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:auto;padding:12px;font-size:11px;color:var(--text-muted);border-top:1px solid var(--border)">
|
||||
<div id="chatAgentStatus">opencode • ready</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-main">
|
||||
<div id="chatMessages" class="chat-messages">
|
||||
<div class="chat-welcome">
|
||||
<div class="chat-welcome-icon">💬</div>
|
||||
<div class="chat-welcome-title">Agentic OS Chat</div>
|
||||
<div class="chat-welcome-desc">Select an agent on the left and start a conversation.<br>Each agent has different capabilities — choose the right one for your task.</div>
|
||||
<div style="display:flex;gap:8px;margin-top:16px;flex-wrap:wrap;justify-content:center">
|
||||
<button class="btn btn-sm" onclick="sendQuickPrompt('opencode','Check the system status and running processes')">🔍 System Check</button>
|
||||
<button class="btn btn-sm" onclick="sendQuickPrompt('hermes','What did I work on recently?')">🧠 Recall Memory</button>
|
||||
<button class="btn btn-sm" onclick="sendQuickPrompt('gemini','Research the latest trends in AI agents')">📊 Research</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-input-area">
|
||||
<div class="chat-agent-indicator" id="chatAgentIndicator">opencode</div>
|
||||
<textarea id="chatInput" class="chat-input" rows="1" placeholder="Type a message..." onkeydown="handleChatKey(event)"></textarea>
|
||||
<button class="btn btn-primary btn-icon" onclick="sendChatMessage()" id="chatSendBtn" title="Send">➤</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
window._currentAgent = 'opencode';
|
||||
window._chatHistory = [];
|
||||
document.getElementById('chatInput').focus();
|
||||
|
||||
// Update agent status indicators
|
||||
try {
|
||||
const status = await api.getStatus();
|
||||
(status.agents || []).forEach(a => {
|
||||
const el = document.querySelector(`.chat-agent[data-agent="${a.name}"]`);
|
||||
if (el) {
|
||||
const dot = el.querySelector('.agent-dot');
|
||||
dot.className = `agent-dot ${a.status}`;
|
||||
}
|
||||
});
|
||||
updateAgentStatusText();
|
||||
} catch {}
|
||||
|
||||
// Load chat history
|
||||
await refreshChat();
|
||||
}
|
||||
|
||||
function selectAgent(agent) {
|
||||
window._currentAgent = agent;
|
||||
document.querySelectorAll('.chat-agent').forEach(el => el.classList.remove('active'));
|
||||
document.querySelector(`.chat-agent[data-agent="${agent}"]`).classList.add('active');
|
||||
document.getElementById('chatAgentIndicator').textContent = agent;
|
||||
document.getElementById('chatInput').focus();
|
||||
updateAgentStatusText();
|
||||
}
|
||||
|
||||
function updateAgentStatusText() {
|
||||
const el = document.getElementById('chatAgentStatus');
|
||||
if (el && window._currentAgent) {
|
||||
const agentEl = document.querySelector(`.chat-agent[data-agent="${window._currentAgent}"]`);
|
||||
const dot = agentEl ? agentEl.querySelector('.agent-dot').className : 'offline';
|
||||
el.textContent = `${window._currentAgent} • ${dot === 'agent-dot online' ? 'online' : 'offline'}`;
|
||||
}
|
||||
}
|
||||
|
||||
function handleChatKey(e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendChatMessage();
|
||||
}
|
||||
autoResizeTextarea(e.target);
|
||||
}
|
||||
|
||||
function autoResizeTextarea(el) {
|
||||
el.style.height = 'auto';
|
||||
el.style.height = Math.min(el.scrollHeight, 150) + 'px';
|
||||
}
|
||||
|
||||
async function sendChatMessage() {
|
||||
const input = document.getElementById('chatInput');
|
||||
const message = input.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
const agent = window._currentAgent || 'opencode';
|
||||
input.value = '';
|
||||
input.style.height = 'auto';
|
||||
|
||||
// Add user message to chat
|
||||
addChatMessage('user', message, agent);
|
||||
|
||||
// Show typing indicator
|
||||
const typingId = showTypingIndicator(agent);
|
||||
|
||||
try {
|
||||
const r = await api.chat(agent, message);
|
||||
removeTypingIndicator(typingId);
|
||||
addChatMessage('assistant', r.response.content, agent);
|
||||
|
||||
// Store in local history
|
||||
window._chatHistory.push({ role: 'user', content: message, agent });
|
||||
window._chatHistory.push({ role: 'assistant', content: r.response.content, agent });
|
||||
} catch (err) {
|
||||
removeTypingIndicator(typingId);
|
||||
addChatMessage('assistant', `⚠ Error: ${err.message}`, agent);
|
||||
}
|
||||
}
|
||||
|
||||
function addChatMessage(role, content, agent) {
|
||||
const container = document.getElementById('chatMessages');
|
||||
const welcome = container.querySelector('.chat-welcome');
|
||||
if (welcome) welcome.style.display = 'none';
|
||||
|
||||
const msg = document.createElement('div');
|
||||
msg.className = `chat-message ${role}`;
|
||||
msg.innerHTML = `
|
||||
<div class="chat-message-avatar">${role === 'user' ? '👤' : '🤖'}</div>
|
||||
<div class="chat-message-body">
|
||||
<div class="chat-message-header">
|
||||
<span class="chat-message-agent">${role === 'user' ? 'You' : agent}</span>
|
||||
<span class="chat-message-time">just now</span>
|
||||
</div>
|
||||
<div class="chat-message-content">${escapeHtml(content)}</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(msg);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
function showTypingIndicator(agent) {
|
||||
const container = document.getElementById('chatMessages');
|
||||
const id = 'typing-' + Date.now();
|
||||
const div = document.createElement('div');
|
||||
div.className = 'chat-message assistant';
|
||||
div.id = id;
|
||||
div.innerHTML = `
|
||||
<div class="chat-message-avatar">🤖</div>
|
||||
<div class="chat-message-body">
|
||||
<div class="chat-message-header">
|
||||
<span class="chat-message-agent">${agent}</span>
|
||||
</div>
|
||||
<div class="typing-indicator"><span></span><span></span><span></span></div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
return id;
|
||||
}
|
||||
|
||||
function removeTypingIndicator(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.remove();
|
||||
}
|
||||
|
||||
async function refreshChat() {
|
||||
try {
|
||||
const data = await api.getChatHistory();
|
||||
const messages = data.messages || [];
|
||||
window._chatHistory = messages;
|
||||
renderChatHistory(messages);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function renderChatHistory(messages) {
|
||||
const container = document.getElementById('chatMessages');
|
||||
if (!container) return;
|
||||
const welcome = container.querySelector('.chat-welcome');
|
||||
if (welcome) welcome.style.display = 'none';
|
||||
|
||||
// Remove all existing messages (keep welcome)
|
||||
container.querySelectorAll('.chat-message').forEach(el => el.remove());
|
||||
|
||||
if (messages.length === 0) {
|
||||
if (welcome) welcome.style.display = '';
|
||||
return;
|
||||
}
|
||||
|
||||
messages.forEach(msg => {
|
||||
const div = document.createElement('div');
|
||||
div.className = `chat-message ${msg.role}`;
|
||||
div.innerHTML = `
|
||||
<div class="chat-message-avatar">${msg.role === 'user' ? '👤' : '🤖'}</div>
|
||||
<div class="chat-message-body">
|
||||
<div class="chat-message-header">
|
||||
<span class="chat-message-agent">${msg.role === 'user' ? 'You' : msg.agent}</span>
|
||||
<span class="chat-message-time">${timeAgo(msg.timestamp)}</span>
|
||||
</div>
|
||||
<div class="chat-message-content">${escapeHtml(msg.content)}</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
});
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
document.querySelectorAll('#chatMessages .chat-message').forEach(el => el.remove());
|
||||
const welcome = document.querySelector('.chat-welcome');
|
||||
if (welcome) welcome.style.display = '';
|
||||
window._chatHistory = [];
|
||||
}
|
||||
|
||||
function sendQuickPrompt(agent, message) {
|
||||
selectAgent(agent);
|
||||
document.getElementById('chatInput').value = message;
|
||||
sendChatMessage();
|
||||
}
|
||||
|
|
@ -104,8 +104,7 @@ body {
|
|||
}
|
||||
.sidebar.collapsed .nav-item.active::before { left: -6px; }
|
||||
.sidebar.collapsed .sidebar-section { padding: 4px 0; }
|
||||
.sidebar.collapsed .sidebar-toggle { display: none; }
|
||||
.sidebar.collapsed .sidebar-header { justify-content: center; padding: 18px 0; }
|
||||
.sidebar.collapsed .sidebar-header { justify-content: center; padding: 18px 0; gap: 0; }
|
||||
.sidebar.collapsed .sidebar-footer { justify-content: center; padding: 12px 0; flex-direction: column; gap: 8px; }
|
||||
|
||||
.sidebar-header {
|
||||
|
|
@ -622,3 +621,127 @@ pre {
|
|||
.text-xs { font-size: 11px; }
|
||||
.font-mono { font-family: var(--font-mono); }
|
||||
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* ─── Chat Interface ─── */
|
||||
.chat-layout {
|
||||
display: flex; gap: 0;
|
||||
height: calc(100vh - 140px);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
.chat-sidebar {
|
||||
width: 200px; min-width: 200px;
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex; flex-direction: column;
|
||||
background: var(--bg-secondary);
|
||||
padding: 8px;
|
||||
}
|
||||
.chat-agents-label {
|
||||
font-size: 10px; font-weight: 700; text-transform: uppercase;
|
||||
letter-spacing: 1px; color: var(--text-muted);
|
||||
padding: 8px 8px 4px; margin-bottom: 4px;
|
||||
}
|
||||
.chat-agent {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 10px 12px; border-radius: var(--radius);
|
||||
cursor: pointer; transition: var(--transition);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.chat-agent:hover { background: var(--bg-card); }
|
||||
.chat-agent.active { background: var(--accent-glow); }
|
||||
.chat-agent-name { font-size: 13px; font-weight: 600; }
|
||||
.chat-agent-desc { font-size: 11px; color: var(--text-muted); }
|
||||
.chat-main {
|
||||
flex: 1; display: flex; flex-direction: column;
|
||||
background: var(--bg-primary);
|
||||
position: relative;
|
||||
}
|
||||
.chat-messages {
|
||||
flex: 1; overflow-y: auto; padding: 20px;
|
||||
display: flex; flex-direction: column; gap: 12px;
|
||||
}
|
||||
.chat-welcome {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
justify-content: center; height: 100%;
|
||||
text-align: center; color: var(--text-muted);
|
||||
gap: 8px;
|
||||
}
|
||||
.chat-welcome-icon { font-size: 48px; opacity: 0.3; }
|
||||
.chat-welcome-title { font-size: 18px; font-weight: 700; color: var(--text-secondary); }
|
||||
.chat-welcome-desc { font-size: 13px; max-width: 400px; line-height: 1.5; }
|
||||
.chat-message {
|
||||
display: flex; gap: 12px; max-width: 80%;
|
||||
animation: chatIn 0.25s cubic-bezier(0.4,0,0.2,1);
|
||||
}
|
||||
.chat-message.user { align-self: flex-end; flex-direction: row-reverse; }
|
||||
.chat-message.assistant { align-self: flex-start; }
|
||||
@keyframes chatIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||||
.chat-message-avatar {
|
||||
width: 34px; height: 34px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 16px; flex-shrink: 0;
|
||||
background: var(--bg-card); border: 1px solid var(--border);
|
||||
}
|
||||
.chat-message-body {
|
||||
background: var(--bg-card); border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg); padding: 12px 16px;
|
||||
min-width: 60px;
|
||||
}
|
||||
.chat-message.user .chat-message-body {
|
||||
background: var(--accent-glow); border-color: transparent;
|
||||
}
|
||||
.chat-message-header {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.chat-message-agent { font-size: 12px; font-weight: 600; }
|
||||
.chat-message-time { font-size: 10px; color: var(--text-muted); margin-left: auto; }
|
||||
.chat-message-content { font-size: 13px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
|
||||
.chat-input-area {
|
||||
display: flex; align-items: flex-end; gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.chat-agent-indicator {
|
||||
font-size: 11px; font-weight: 700; color: var(--accent-light);
|
||||
padding: 4px 10px; border-radius: var(--radius-sm);
|
||||
background: var(--accent-glow); white-space: nowrap;
|
||||
flex-shrink: 0; margin-bottom: 4px;
|
||||
}
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-input);
|
||||
color: var(--text-primary);
|
||||
font-size: 13px; font-family: var(--font);
|
||||
outline: none; resize: none;
|
||||
max-height: 150px; line-height: 1.4;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.chat-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
|
||||
.chat-input::placeholder { color: var(--text-muted); }
|
||||
|
||||
/* Typing indicator */
|
||||
.typing-indicator { display: flex; gap: 4px; padding: 4px 0; }
|
||||
.typing-indicator span {
|
||||
width: 6px; height: 6px; border-radius: 50%;
|
||||
background: var(--text-muted);
|
||||
animation: typing 1.4s infinite ease-in-out;
|
||||
}
|
||||
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||
@keyframes typing {
|
||||
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
|
||||
30% { transform: translateY(-6px); opacity: 1; }
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.chat-sidebar { display: none; }
|
||||
.chat-message { max-width: 95%; }
|
||||
.chat-layout { height: calc(100vh - 180px); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,8 +50,11 @@ function statusColor(status) {
|
|||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
document.getElementById('sidebar').classList.toggle('collapsed');
|
||||
localStorage.setItem('sidebarCollapsed', document.getElementById('sidebar').classList.contains('collapsed'));
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const isCollapsed = sidebar.classList.toggle('collapsed');
|
||||
localStorage.setItem('sidebarCollapsed', isCollapsed);
|
||||
const btn = sidebar.querySelector('.sidebar-toggle');
|
||||
if (btn) btn.textContent = isCollapsed ? '▶' : '◀';
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
|
|
@ -65,7 +68,11 @@ function loadTheme() {
|
|||
const saved = localStorage.getItem('theme');
|
||||
if (saved) document.documentElement.setAttribute('data-theme', saved);
|
||||
const sidebarCollapsed = localStorage.getItem('sidebarCollapsed');
|
||||
if (sidebarCollapsed === 'true') document.getElementById('sidebar').classList.add('collapsed');
|
||||
if (sidebarCollapsed === 'true') {
|
||||
document.getElementById('sidebar').classList.add('collapsed');
|
||||
const btn = document.querySelector('.sidebar-toggle');
|
||||
if (btn) btn.textContent = '▶';
|
||||
}
|
||||
}
|
||||
|
||||
function showModal(title, bodyHtml, footerHtml) {
|
||||
|
|
@ -112,4 +119,5 @@ const PAGE_TITLES = {
|
|||
standards: { title: 'Standards', breadcrumb: 'Project conventions' },
|
||||
settings: { title: 'Settings', breadcrumb: 'Configuration' },
|
||||
'setup-wizard': { title: 'Setup Wizard', breadcrumb: 'Guided configuration' },
|
||||
chat: { title: 'AI Chat', breadcrumb: 'Multi-agent terminal' },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"messages": [
|
||||
{
|
||||
"id": "7ab2a689",
|
||||
"role": "user",
|
||||
"agent": "opencode",
|
||||
"content": "hello",
|
||||
"timestamp": "2026-05-17T10:36:36.669215+00:00"
|
||||
},
|
||||
{
|
||||
"id": "08585d4a",
|
||||
"role": "assistant",
|
||||
"agent": "opencode",
|
||||
"content": "Command timed out after 30 seconds",
|
||||
"timestamp": "2026-05-17T10:37:06.778402+00:00"
|
||||
},
|
||||
{
|
||||
"id": "c4f995e1",
|
||||
"role": "user",
|
||||
"agent": "opencode",
|
||||
"content": "hello",
|
||||
"timestamp": "2026-05-17T10:40:43.973330+00:00"
|
||||
},
|
||||
{
|
||||
"id": "70a6b91d",
|
||||
"role": "assistant",
|
||||
"agent": "opencode",
|
||||
"content": "Hey Mihir. Ready when you are.\n",
|
||||
"timestamp": "2026-05-17T10:40:56.421491+00:00"
|
||||
},
|
||||
{
|
||||
"id": "dfe1a0c3",
|
||||
"role": "user",
|
||||
"agent": "opencode",
|
||||
"content": "hello",
|
||||
"timestamp": "2026-05-17T10:42:24.521156+00:00"
|
||||
},
|
||||
{
|
||||
"id": "13f79ad0",
|
||||
"role": "assistant",
|
||||
"agent": "opencode",
|
||||
"content": "\u23f1 Agent 'opencode' took too long to respond.\n\nYour message has been dispatched. For a full response, run the agent CLI directly in your terminal.\n\n**Message:** hello",
|
||||
"timestamp": "2026-05-17T10:42:36.569740+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"id": "daily-standup-default",
|
||||
"name": "Daily Standup",
|
||||
"skill": "daily-standup",
|
||||
"cron": "0 7 * * 1-5",
|
||||
"enabled": true,
|
||||
"created": "2026-05-17T00:00:00Z",
|
||||
"last_run": null,
|
||||
"next_run": null,
|
||||
"description": "Morning standup weekdays at 7 AM"
|
||||
}
|
||||
88
server.py
88
server.py
|
|
@ -55,6 +55,10 @@ class SettingsUpdate(BaseModel):
|
|||
class BackupRestoreRequest(BaseModel):
|
||||
file: str
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
agent: str
|
||||
message: str
|
||||
|
||||
# ─── Helper Functions ─────────────────────────────────────────────
|
||||
|
||||
def read_file(path: Path):
|
||||
|
|
@ -413,6 +417,90 @@ def discover_standards():
|
|||
append_audit({"action": "standards_discovery_run"})
|
||||
return {"status": "discovery_started", "message": "Scanning codebase for patterns..."}
|
||||
|
||||
# ─── Routes: Chat ─────────────────────────────────────────────────
|
||||
|
||||
CHAT_HISTORY_FILE = BASE_DIR / "data" / "chat-history.json"
|
||||
|
||||
def load_chat_history():
|
||||
if CHAT_HISTORY_FILE.exists():
|
||||
return json.loads(CHAT_HISTORY_FILE.read_text())
|
||||
return {"messages": []}
|
||||
|
||||
def save_chat_message(msg: dict):
|
||||
history = load_chat_history()
|
||||
history["messages"].append(msg)
|
||||
if len(history["messages"]) > 200:
|
||||
history["messages"] = history["messages"][-200:]
|
||||
CHAT_HISTORY_FILE.write_text(json.dumps(history, indent=2))
|
||||
|
||||
def execute_agent(agent: str, message: str) -> str:
|
||||
try:
|
||||
if agent == "opencode":
|
||||
r = subprocess.run(
|
||||
["opencode", "run", message],
|
||||
capture_output=True, text=True, timeout=12
|
||||
)
|
||||
out = (r.stdout or "") + (r.stderr or "")
|
||||
if r.returncode == 0:
|
||||
return out if out.strip() else f"✓ opencode received your message.\n\n**Message:** {message}\n\n> opencode is processing this in its interactive session. For a full response, run `opencode run \"{message[:60]}\"` in your terminal."
|
||||
return out or f"opencode returned exit code {r.returncode}"
|
||||
elif agent == "hermes":
|
||||
r = subprocess.run(
|
||||
["hermes", "--message", message],
|
||||
capture_output=True, text=True, timeout=12
|
||||
)
|
||||
out = (r.stdout or "") + (r.stderr or "")
|
||||
return out if out.strip() else f"✓ Dispatched to Hermes.\n\n**Message:** {message}"
|
||||
elif agent == "gemini":
|
||||
r = subprocess.run(
|
||||
["gemini", "ask", message],
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
out = (r.stdout or "") + (r.stderr or "")
|
||||
return out if out.strip() else f"✓ Sent to Gemini CLI.\n\n**Message:** {message}"
|
||||
else:
|
||||
return f"Unknown agent: {agent}"
|
||||
except subprocess.TimeoutExpired:
|
||||
return f"⏱ Agent '{agent}' took too long to respond.\n\nYour message has been dispatched. For a full response, run the agent CLI directly in your terminal.\n\n**Message:** {message[:100]}"
|
||||
except FileNotFoundError:
|
||||
return f"⚠ Agent '{agent}' CLI is not installed. Run `pip install {agent}` or install it manually."
|
||||
except Exception as e:
|
||||
return f"⚠ Error communicating with {agent}: {str(e)}"
|
||||
|
||||
@app.post("/api/chat")
|
||||
def chat(req: ChatRequest):
|
||||
agent = req.agent.lower().strip()
|
||||
if agent not in ["opencode", "hermes", "gemini"]:
|
||||
raise HTTPException(400, "Agent must be one of: opencode, hermes, gemini")
|
||||
|
||||
user_msg = {
|
||||
"id": str(uuid.uuid4())[:8],
|
||||
"role": "user",
|
||||
"agent": agent,
|
||||
"content": req.message,
|
||||
"timestamp": get_timestamp(),
|
||||
}
|
||||
save_chat_message(user_msg)
|
||||
|
||||
response_text = execute_agent(agent, req.message)
|
||||
|
||||
agent_msg = {
|
||||
"id": str(uuid.uuid4())[:8],
|
||||
"role": "assistant",
|
||||
"agent": agent,
|
||||
"content": response_text,
|
||||
"timestamp": get_timestamp(),
|
||||
}
|
||||
save_chat_message(agent_msg)
|
||||
|
||||
append_audit({"action": "chat_message", "agent": agent, "msg_preview": req.message[:50]})
|
||||
|
||||
return {"status": "ok", "response": agent_msg}
|
||||
|
||||
@app.get("/api/chat/history")
|
||||
def get_chat_history():
|
||||
return load_chat_history()
|
||||
|
||||
# ─── Routes: Dashboard Static Files ──────────────────────────────
|
||||
|
||||
dashboard_dir = BASE_DIR / "dashboard"
|
||||
|
|
|
|||
Loading…
Reference in New Issue