diff --git a/opax-web/backend/agentClient.js b/opax-web/backend/agentClient.js
index 1902a82..67d3d5c 100644
--- a/opax-web/backend/agentClient.js
+++ b/opax-web/backend/agentClient.js
@@ -10,6 +10,7 @@ const READ_ONLY_TOOLS = new Set([
'get_build_status',
'get_state',
'get_telemetry',
+ 'run_emma',
]);
export class AgentNotConfiguredError extends Error {
diff --git a/opax-web/backend/capabilityRegistry.js b/opax-web/backend/capabilityRegistry.js
new file mode 100644
index 0000000..db3470f
--- /dev/null
+++ b/opax-web/backend/capabilityRegistry.js
@@ -0,0 +1,27 @@
+export const capabilities = {
+ get_health: { type: 'read' },
+ get_build_status: { type: 'read' },
+ get_state: { type: 'read' },
+ get_telemetry: { type: 'read' },
+ list_commits: { type: 'read' },
+ get_file: { type: 'read' },
+ list_customers: { type: 'read' },
+ get_billing_summary: { type: 'read' },
+ get_billing_forecast: { type: 'read' },
+ get_billing_anomalies: { type: 'read' },
+ get_billing_budget: { type: 'read' },
+ list_workspace_users: { type: 'read' },
+ list_user_aliases: { type: 'read' },
+ list_calendar_events: { type: 'read' },
+ run_emma: { type: 'read' },
+ trigger_build: { type: 'proposal' },
+ commit_and_push_files: { type: 'proposal' },
+ create_a2h2a_ticket: { type: 'proposal' },
+ send_email: { type: 'proposal' },
+ send_sms: { type: 'proposal' },
+ send_webhook: { type: 'proposal' },
+ create_invite: { type: 'proposal' },
+ create_email_alias: { type: 'proposal' },
+ delete_email_alias: { type: 'proposal' },
+ set_billing_budget: { type: 'proposal' },
+};
diff --git a/opax-web/backend/routes/consoleApi.js b/opax-web/backend/routes/consoleApi.js
index 9416c18..ca9129a 100644
--- a/opax-web/backend/routes/consoleApi.js
+++ b/opax-web/backend/routes/consoleApi.js
@@ -67,4 +67,38 @@ router.get('/approvals', (req, res) => {
});
});
+router.post('/emma/chat', async (req, res) => {
+ const { message, session_id, ticket_number } = req.body;
+
+ if (!message || typeof message !== 'string' || message.length > 4096) {
+ return res.status(400).json({ status: 'error', message: 'Invalid message.'});
+ }
+
+ if (ticket_number && (typeof ticket_number !== 'number' || !Number.isInteger(ticket_number) || ticket_number <= 0)) {
+ return res.status(400).json({ status: 'error', message: 'Invalid ticket number.'});
+ }
+
+ try {
+ const emmaData = await callAgent('run_emma', { prompt: message, history: [] });
+
+ res.json({
+ status: 'success',
+ reply: emmaData.response,
+ session_id,
+ ticket_number,
+ model_label: 'Emma Local',
+ message: 'Message processed by Emma.',
+ });
+
+ } catch (e) {
+ res.status(500).json({ status: 'error', message: 'Could not connect to Emma.' });
+ }
+});
+
+import { capabilities } from '../capabilityRegistry.js';
+
+router.get('/capabilities', (req, res) => {
+ res.json(capabilities);
+});
+
export default router;
diff --git a/opax-web/backend/server.js b/opax-web/backend/server.js
index cf6556e..951be97 100644
--- a/opax-web/backend/server.js
+++ b/opax-web/backend/server.js
@@ -43,6 +43,7 @@ const sm = session({
const io = new Server(http, { cors: { origin: front, credentials: true } });
app.use(cors({ origin: front, credentials: true }));
+app.use(express.json());
app.use(sm);
app.use(express.static(frontendDistPath));
io.use((s, n) => sm(s.request, {}, n));
diff --git a/opax-web/frontend/src/components/AgentConsoleView.tsx b/opax-web/frontend/src/components/AgentConsoleView.tsx
index b66c9c5..a38a591 100644
--- a/opax-web/frontend/src/components/AgentConsoleView.tsx
+++ b/opax-web/frontend/src/components/AgentConsoleView.tsx
@@ -1,15 +1,75 @@
-import React from 'react';
+import React, { useState } from 'react';
const AgentConsoleView = () => {
- return (
-
-
Agent Console
-
- Draft → Plan → Scope Approval → Implementation → Diff Approval → Validation → Deployment Approval → Deploy
-
-
Ticket-bound agent execution is not configured yet.
-
- );
+ const [message, setMessage] = useState('');
+ const [transcript, setTranscript] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [ticketNumber, setTicketNumber] = useState(null);
+
+ const handleSend = async () => {
+ if (!message.trim()) return;
+
+ const newMessage = { role: 'user', content: message };
+ setTranscript(prev => [...prev, newMessage]);
+ setMessage('');
+ setLoading(true);
+ setError(null);
+
+ try {
+ const res = await fetch('/api/console/emma/chat', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({ message, ticket_number: ticketNumber }),
+ });
+
+ if (!res.ok) {
+ throw new Error('Emma is unavailable.');
+ }
+
+ const data = await res.json();
+ const emmaMessage = { role: 'assistant', content: data.reply };
+ setTranscript(prev => [...prev, emmaMessage]);
+
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
Emma Local
+
+ Local model · server-side memory enabled
+ {ticketNumber && Working context: Gitea #{ticketNumber}}
+
+
+ {transcript.map((msg, index) => (
+
+ {msg.role}: {msg.content}
+
+ ))}
+ {loading &&
...
}
+ {error &&
{error}
}
+
+
+ setMessage(e.target.value)}
+ onKeyPress={(e) => e.key === 'Enter' && handleSend()}
+ placeholder="Chat with Emma..."
+ disabled={loading}
+ />
+
+
+
+ );
};
export default AgentConsoleView;
diff --git a/opax-web/frontend/src/components/CapabilityPanel.tsx b/opax-web/frontend/src/components/CapabilityPanel.tsx
new file mode 100644
index 0000000..3d6d30a
--- /dev/null
+++ b/opax-web/frontend/src/components/CapabilityPanel.tsx
@@ -0,0 +1,49 @@
+import React, { useState, useEffect } from 'react';
+
+const CapabilityPanel = () => {
+ const [capabilities, setCapabilities] = useState({});
+
+ useEffect(() => {
+ const fetchCapabilities = async () => {
+ const res = await fetch('/api/console/capabilities');
+ const data = await res.json();
+ setCapabilities(data);
+ };
+ fetchCapabilities();
+ }, []);
+
+ const capabilityGroups = {
+ 'Platform Status': ['get_health', 'get_build_status', 'get_state', 'get_telemetry'],
+ 'Git and Gitea': ['list_commits', 'get_file'],
+ 'Tickets and Work Queue': [],
+ 'Code Agents': ['run_emma'],
+ 'Tests and Validation': [],
+ 'Builds and Deployments': ['trigger_build', 'commit_and_push_files'],
+ 'Cloud Run and Infrastructure': [],
+ 'Logs and Telemetry': [],
+ 'Billing and CostGuard': ['get_billing_summary', 'get_billing_forecast', 'get_billing_anomalies', 'get_billing_budget', 'set_billing_budget'],
+ 'Customers and Workspace': ['list_customers', 'list_workspace_users', 'create_invite'],
+ 'Notifications': ['send_email', 'send_sms', 'send_webhook'],
+ 'Memory and Knowledge': [],
+ };
+
+ return (
+
+
Capabilities
+ {Object.entries(capabilityGroups).map(([group, capabilityKeys]) => (
+
+
{group}
+
+ {capabilityKeys.map(key => {
+ const capability = capabilities[key];
+ if (!capability) return null;
+ return - {key}
+ })}
+
+
+ ))}
+
+ );
+};
+
+export default CapabilityPanel;
diff --git a/opax-web/frontend/src/pages/OperationsConsole.tsx b/opax-web/frontend/src/pages/OperationsConsole.tsx
index 9983aef..6ccae6b 100644
--- a/opax-web/frontend/src/pages/OperationsConsole.tsx
+++ b/opax-web/frontend/src/pages/OperationsConsole.tsx
@@ -4,6 +4,7 @@ import ProjectRegistryView from '../components/ProjectRegistryView';
import WorkQueueView from '../components/WorkQueueView';
import ApprovalsView from '../components/ApprovalsView';
import AgentConsoleView from '../components/AgentConsoleView';
+import CapabilityPanel from '../components/CapabilityPanel';
// --- Types for Safe Data Handling ---
type ConsoleStatus = 'success' | 'unavailable';
@@ -130,6 +131,7 @@ const OperationsConsole = () => {
+
);