<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Oxios — Agent OS</title>
<style>
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #1c2128;
--bg-hover: #21262d;
--border: #30363d;
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--text-muted: #6e7681;
--accent: #58a6ff;
--accent-dim: #1f6feb;
--green: #3fb950;
--red: #f85149;
--yellow: #d29922;
--purple: #bc8cff;
--font-mono: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
--radius: 6px;
--sidebar-width: 220px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
background: var(--bg-primary);
color: var(--text-primary);
font-family: var(--font-sans);
font-size: 14px;
line-height: 1.5;
overflow: hidden;
}
.app {
display: flex;
height: 100vh;
}
.sidebar {
width: var(--sidebar-width);
min-width: var(--sidebar-width);
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow-y: auto;
transition: width 0.2s ease, min-width 0.2s ease;
position: relative;
}
.sidebar.collapsed {
width: 60px;
min-width: 60px;
}
.sidebar.collapsed .sidebar-header .subtitle,
.sidebar.collapsed .nav-item span:not(.icon),
.sidebar.collapsed .sidebar-footer span:not(.status-dot) {
display: none;
}
.sidebar.collapsed .nav-item { justify-content: center; padding: 10px; }
.sidebar-toggle {
position: absolute;
top: 50%;
right: -14px;
transform: translateY(-50%);
width: 24px;
height: 24px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 50%;
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
z-index: 10;
transition: transform 0.2s ease;
}
.sidebar-toggle:hover {
color: var(--accent);
background: var(--bg-tertiary);
}
.sidebar.collapsed .sidebar-toggle {
transform: translateY(-50%) rotate(180deg);
}
.sidebar-header {
padding: 16px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 10px;
}
.sidebar-header .logo {
font-family: var(--font-mono);
font-size: 18px;
font-weight: 700;
color: var(--accent);
}
.sidebar-header .subtitle {
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1px;
}
.sidebar-nav {
padding: 8px 0;
flex: 1;
}
.nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 16px;
color: var(--text-secondary);
cursor: pointer;
border-left: 3px solid transparent;
transition: all 0.15s ease;
font-size: 13px;
user-select: none;
}
.nav-item:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.nav-item.active {
background: var(--bg-tertiary);
color: var(--accent);
border-left-color: var(--accent);
}
.nav-item .icon {
width: 18px;
text-align: center;
font-size: 15px;
}
.sidebar-footer {
padding: 12px 16px;
border-top: 1px solid var(--border);
font-size: 11px;
color: var(--text-muted);
font-family: var(--font-mono);
}
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--green);
margin-right: 6px;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
}
.panel {
display: none;
flex-direction: column;
flex: 1;
overflow: hidden;
}
.panel.active {
display: flex;
}
.panel-header {
padding: 12px 20px;
border-bottom: 1px solid var(--border);
background: var(--bg-secondary);
display: flex;
align-items: center;
justify-content: space-between;
}
.panel-header h2 {
font-size: 15px;
font-weight: 600;
font-family: var(--font-mono);
}
.panel-body {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.chat-msg {
max-width: 80%;
padding: 10px 14px;
border-radius: var(--radius);
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
.chat-msg.user {
align-self: flex-end;
background: var(--accent-dim);
color: var(--text-primary);
border-bottom-right-radius: 2px;
}
.chat-msg.system {
align-self: flex-start;
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border);
border-bottom-left-radius: 2px;
}
.chat-msg .meta {
font-size: 11px;
color: var(--text-muted);
margin-top: 4px;
font-family: var(--font-mono);
}
.chat-input-bar {
padding: 12px 20px;
border-top: 1px solid var(--border);
background: var(--bg-secondary);
display: flex;
gap: 8px;
}
.chat-input-bar input {
flex: 1;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 10px 14px;
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 13px;
outline: none;
transition: border-color 0.15s;
}
.chat-input-bar input:focus {
border-color: var(--accent);
}
.chat-input-bar input::placeholder {
color: var(--text-muted);
}
.btn {
background: var(--accent-dim);
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 8px 16px;
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
font-family: var(--font-sans);
}
.btn:hover {
background: var(--accent);
color: #fff;
}
.btn-danger {
background: transparent;
color: var(--red);
border-color: var(--red);
}
.btn-danger:hover {
background: var(--red);
color: #fff;
}
.btn-sm {
padding: 4px 10px;
font-size: 12px;
}
.agent-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
.agent-card .agent-info {
flex: 1;
}
.agent-card .agent-name {
font-family: var(--font-mono);
font-size: 13px;
font-weight: 600;
}
.agent-card .agent-id {
font-size: 11px;
color: var(--text-muted);
font-family: var(--font-mono);
}
.agent-card .agent-status {
padding: 3px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-running { background: rgba(63,185,80,0.15); color: var(--green); }
.status-idle { background: rgba(210,153,34,0.15); color: var(--yellow); }
.status-stopped { background: rgba(139,148,158,0.15); color: var(--text-secondary); }
.status-failed { background: rgba(248,81,73,0.15); color: var(--red); }
.status-starting { background: rgba(88,166,255,0.15); color: var(--accent); }
.workspace-split {
display: flex;
flex: 1;
overflow: hidden;
}
.workspace-tree {
width: 260px;
min-width: 200px;
border-right: 1px solid var(--border);
overflow-y: auto;
padding: 12px;
background: var(--bg-secondary);
}
.tree-item {
padding: 5px 8px;
cursor: pointer;
border-radius: 4px;
font-size: 13px;
font-family: var(--font-mono);
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 6px;
user-select: none;
}
.tree-item:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.tree-item.dir .icon { color: var(--accent); }
.tree-item.file .icon { color: var(--text-muted); }
.workspace-viewer {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.workspace-viewer pre {
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
color: var(--text-primary);
}
.workspace-viewer textarea {
width: 100%;
height: 100%;
background: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px;
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.6;
resize: none;
outline: none;
}
.workspace-viewer textarea:focus {
border-color: var(--accent);
}
.workspace-toolbar {
padding: 8px 16px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 8px;
}
.workspace-toolbar .breadcrumb {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-muted);
flex: 1;
}
.item-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.item-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px;
cursor: pointer;
transition: border-color 0.15s;
}
.item-card:hover {
border-color: var(--accent);
}
.item-card .item-title {
font-family: var(--font-mono);
font-size: 13px;
font-weight: 600;
margin-bottom: 4px;
}
.item-card .item-subtitle {
font-size: 12px;
color: var(--text-muted);
}
.detail-view {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
margin-top: 16px;
}
.detail-view h3 {
font-family: var(--font-mono);
font-size: 14px;
margin-bottom: 12px;
color: var(--accent);
}
.detail-view pre {
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
color: var(--text-primary);
}
.config-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.config-section {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px;
}
.config-section h3 {
font-family: var(--font-mono);
font-size: 13px;
color: var(--accent);
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.config-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid var(--border);
font-size: 13px;
}
.config-row:last-child { border-bottom: none; }
.config-row .key {
font-family: var(--font-mono);
color: var(--text-secondary);
}
.config-row .val {
font-family: var(--font-mono);
color: var(--text-primary);
}
word-break: break-word;
max-height: 200px;
overflow-y: auto;
color: var(--text-secondary);
}
.toast-container {
position: fixed;
top: 16px;
right: 16px;
z-index: 200;
display: flex;
flex-direction: column;
gap: 8px;
}
.toast {
padding: 12px 16px;
border-radius: var(--radius);
font-size: 13px;
font-family: var(--font-mono);
min-width: 250px;
max-width: 400px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
animation: toast-in 0.3s ease;
}
.toast.success {
background: rgba(63,185,80,0.95);
color: #000;
}
.toast.error {
background: rgba(248,81,73,0.95);
color: #fff;
}
.toast.info {
background: rgba(88,166,255,0.95);
color: #000;
}
@keyframes toast-in {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes toast-out {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
.host-tools-section {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px;
margin-bottom: 16px;
}
.host-tools-section h3 {
font-family: var(--font-mono);
font-size: 13px;
color: var(--accent);
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.host-tool-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 0;
font-size: 13px;
}
.host-tool-item .tool-icon {
font-size: 14px;
}
.host-tool-item .tool-available {
color: var(--green);
}
.host-tool-item .tool-missing {
color: var(--red);
}
.host-tool-item .tool-name {
font-family: var(--font-mono);
color: var(--text-primary);
}
.host-tools-summary {
display: flex;
gap: 16px;
padding: 12px;
background: var(--bg-tertiary);
border-radius: var(--radius);
margin-bottom: 16px;
font-size: 13px;
}
.host-tools-summary .stat {
font-family: var(--font-mono);
}
.host-tools-summary .stat.green { color: var(--green); }
.host-tools-summary .stat.red { color: var(--red); }
.program-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px;
margin-bottom: 10px;
cursor: pointer;
transition: border-color 0.15s;
}
.program-card:hover {
border-color: var(--accent);
}
.program-card .program-name {
font-family: var(--font-mono);
font-size: 13px;
font-weight: 600;
}
.program-card .program-version {
font-size: 11px;
color: var(--accent);
font-family: var(--font-mono);
}
.program-card .program-desc {
font-size: 12px;
color: var(--text-muted);
margin-top: 4px;
}
.program-card .program-enabled {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
margin-left: 8px;
}
.program-enabled.yes {
background: rgba(63,185,80,0.15);
color: var(--green);
}
.program-enabled.no {
background: rgba(139,148,158,0.15);
color: var(--text-secondary);
}
.program-install-form {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px;
margin-bottom: 16px;
}
.program-install-form h3 {
font-family: var(--font-mono);
font-size: 13px;
color: var(--accent);
margin-bottom: 12px;
}
.program-install-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.program-install-row input {
flex: 1;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 8px 12px;
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 13px;
outline: none;
}
.program-install-row input:focus {
border-color: var(--accent);
}
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
padding: 24px;
min-width: 360px;
max-width: 480px;
}
.modal h3 {
font-family: var(--font-mono);
font-size: 15px;
margin-bottom: 16px;
color: var(--accent);
}
.modal label {
display: block;
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 4px;
font-family: var(--font-mono);
}
.modal input {
width: 100%;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 10px 12px;
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 13px;
outline: none;
margin-bottom: 16px;
}
.modal input:focus {
border-color: var(--accent);
}
.modal-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-muted);
}
.empty-state .icon {
font-size: 40px;
margin-bottom: 12px;
}
.empty-state p {
font-size: 13px;
max-width: 300px;
margin: 0 auto;
}
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
.loading {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.event-log {
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.8;
color: var(--text-secondary);
}
.event-log .event-line {
padding: 2px 0;
border-bottom: 1px solid var(--bg-tertiary);
}
.event-log .event-time {
color: var(--text-muted);
margin-right: 8px;
}
.event-log .event-type {
color: var(--accent);
font-weight: 600;
}
.ouroboros-panel {
display: flex;
flex-direction: column;
gap: 16px;
}
.phase-indicator {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
}
.phase-current {
font-family: var(--font-mono);
font-size: 20px;
font-weight: 700;
color: var(--accent);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 10px;
}
.phase-progress-bar {
height: 8px;
background: var(--bg-tertiary);
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.phase-progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent-dim), var(--accent));
transition: width 0.4s ease;
border-radius: 4px;
}
.phase-meta {
display: flex;
justify-content: space-between;
font-size: 12px;
color: var(--text-muted);
font-family: var(--font-mono);
}
.phase-steps {
display: flex;
justify-content: space-between;
margin-top: 16px;
}
.phase-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
flex: 1;
cursor: pointer;
opacity: 0.5;
transition: opacity 0.2s;
}
.phase-step.active, .phase-step.completed {
opacity: 1;
}
.phase-step-icon {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--bg-tertiary);
border: 2px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: all 0.2s;
}
.phase-step.active .phase-step-icon {
border-color: var(--accent);
background: var(--accent-dim);
color: var(--text-primary);
}
.phase-step.completed .phase-step-icon {
border-color: var(--green);
background: rgba(63,185,80,0.2);
color: var(--green);
}
.phase-step-label {
font-size: 11px;
font-family: var(--font-mono);
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.phase-step.active .phase-step-label {
color: var(--accent);
}
.phase-step.completed .phase-step-label {
color: var(--green);
}
.protocol-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.protocol-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px;
}
.protocol-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.protocol-card-title {
font-family: var(--font-mono);
font-size: 13px;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.protocol-status-badge {
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.protocol-connected { background: rgba(63,185,80,0.15); color: var(--green); }
.protocol-disconnected { background: rgba(139,148,158,0.15); color: var(--text-secondary); }
.protocol-error { background: rgba(248,81,73,0.15); color: var(--red); }
.protocol-stat {
display: flex;
justify-content: space-between;
padding: 6px 0;
font-size: 13px;
border-bottom: 1px solid var(--bg-tertiary);
}
.protocol-stat:last-child { border-bottom: none; }
.protocol-stat .key { color: var(--text-secondary); font-family: var(--font-mono); }
.protocol-stat .val { color: var(--text-primary); font-family: var(--font-mono); }
.protocol-permissions {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.protocol-permission-tag {
padding: 2px 8px;
background: var(--bg-tertiary);
border-radius: 10px;
font-size: 11px;
font-family: var(--font-mono);
color: var(--text-secondary);
}
.mcp-server-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid var(--bg-tertiary);
}
.mcp-server-item:last-child { border-bottom: none; }
.mcp-server-name {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-primary);
}
.mcp-server-status {
font-size: 11px;
font-family: var(--font-mono);
}
.agent-monitor-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.agent-monitor-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px;
}
.agent-monitor-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.agent-monitor-title {
font-family: var(--font-mono);
font-size: 13px;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.agent-monitor-count {
padding: 4px 10px;
background: rgba(88,166,255,0.15);
border-radius: 12px;
font-size: 11px;
font-weight: 600;
color: var(--accent);
}
.agent-registry-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 300px;
overflow-y: auto;
}
.agent-registry-item {
padding: 10px 12px;
background: var(--bg-tertiary);
border-radius: var(--radius);
cursor: pointer;
transition: border-color 0.15s;
border: 1px solid transparent;
}
.agent-registry-item:hover {
border-color: var(--accent);
}
.agent-registry-item.selected {
border-color: var(--accent);
background: var(--bg-hover);
}
.agent-registry-name {
font-family: var(--font-mono);
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
}
.agent-registry-capabilities {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 6px;
}
.agent-cap-tag {
padding: 2px 6px;
background: var(--bg-primary);
border-radius: 6px;
font-size: 10px;
font-family: var(--font-mono);
color: var(--accent);
}
.agent-registry-id {
font-size: 11px;
color: var(--text-muted);
font-family: var(--font-mono);
margin-top: 4px;
}
.agent-detail-panel {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px;
margin-top: 16px;
}
.agent-detail-panel h4 {
font-family: var(--font-mono);
font-size: 14px;
color: var(--accent);
margin-bottom: 12px;
}
@media (max-width: 768px) {
.sidebar { width: 60px; min-width: 60px; }
.sidebar-header .subtitle,
.nav-item span:not(.icon),
.sidebar-footer span { display: none; }
.nav-item { justify-content: center; padding: 10px; }
.config-grid { grid-template-columns: 1fr; }
.workspace-tree { width: 180px; }
}
</style>
</head>
<body>
<div class="app">
<aside class="sidebar">
<div class="sidebar-header">
<div>
<div class="logo">λ oxios</div>
<div class="subtitle">Agent OS</div>
</div>
</div>
<nav class="sidebar-nav">
<div class="nav-item active" data-panel="chat">
<span class="icon">💬</span><span>Chat</span>
</div>
<div class="nav-item" data-panel="dashboard">
<span class="icon">📊</span><span>Dashboard</span>
</div>
<div class="nav-item" data-panel="agents">
<span class="icon">⚡</span><span>Agents</span>
</div>
<div class="nav-item" data-panel="protocol">
<span class="icon">🔌</span><span>Protocol</span>
</div>
<div class="nav-item" data-panel="workspace">
<span class="icon">📁</span><span>Workspace</span>
</div>
<div class="nav-item" data-panel="seeds">
<span class="icon">🌱</span><span>Seeds</span>
</div>
<div class="nav-item" data-panel="memory">
<span class="icon">🧠</span><span>Memory</span>
</div>
<div class="nav-item" data-panel="skills">
<span class="icon">📖</span><span>Skills</span>
</div>
<div class="nav-item" data-panel="programs">
<span class="icon">📦</span><span>Programs</span>
</div>
<div class="nav-item" data-panel="personas">
<span class="icon">🎭</span><span>Personas</span>
</div>
<div class="nav-item" data-panel="host-tools">
<span class="icon">🔧</span><span>Host Tools</span>
</div>
<div class="nav-item" data-panel="events">
<span class="icon">📡</span><span>Events</span>
</div>
<div class="nav-item" data-panel="scheduler">
<span class="icon">📋</span><span>Scheduler</span>
</div>
<div class="nav-item" data-panel="security">
<span class="icon">🔒</span><span>Security</span>
</div>
<div class="nav-item" data-panel="approvals">
<span class="icon">🛡️</span><span>Approvals</span>
</div>
<div class="nav-item" data-panel="config">
<span class="icon">⚙️</span><span>Config</span>
</div>
<div class="nav-item" style="cursor:pointer" onclick="window.open('/dioxus/','_blank')">
<span class="icon">⬢</span><span>Dioxus UI</span>
</div>
</nav>
<div class="sidebar-footer">
<span class="status-dot"></span><span id="conn-status">connected</span>
</div>
<button class="sidebar-toggle" id="sidebar-toggle" title="Toggle Sidebar (Ctrl+B)">◀</button>
</aside>
<main class="main">
<div class="panel active" id="panel-chat">
<div class="panel-header">
<h2>💬 Chat</h2>
<span style="font-size:12px;color:var(--text-muted)" id="ws-status">WebSocket: connecting…</span>
</div>
<div class="chat-messages" id="chat-messages">
<div class="chat-msg system">
Welcome to Oxios. Type a message to start interacting with your agents.
<div class="meta">system · just now</div>
</div>
</div>
<div class="chat-input-bar">
<input type="text" id="chat-input" placeholder="Send a message… (Enter to send)" autocomplete="off" />
<button class="btn" id="chat-send">Send</button>
</div>
</div>
<div class="panel" id="panel-dashboard">
<div class="panel-header">
<h2>📊 Dashboard</h2>
<button class="btn btn-sm" id="dashboard-refresh">Refresh</button>
</div>
<div class="panel-body" id="dashboard-body">
<div class="empty-state">
<div class="icon">📊</div>
<p>Loading Agent OS status...</p>
</div>
</div>
</div>
<div class="panel" id="panel-protocol">
<div class="panel-header">
<h2>🔌 Protocol Status</h2>
<button class="btn btn-sm" id="protocol-refresh">Refresh</button>
</div>
<div class="panel-body" id="protocol-body">
<div class="empty-state">
<div class="icon">🔌</div>
<p>Loading protocol status...</p>
</div>
</div>
</div>
<div class="panel" id="panel-agents">
<div class="panel-header">
<h2>⚡ Agent Monitor</h2>
<button class="btn btn-sm" id="agents-refresh">Refresh</button>
</div>
<div class="panel-body" id="agents-body">
<div class="empty-state">
<div class="icon">⚡</div>
<p>No agents running. Start a conversation to spawn agents.</p>
</div>
</div>
</div>
<div class="panel" id="panel-phase">
<div class="panel-header">
<h2>🌀 Ouroboros Phase</h2>
<button class="btn btn-sm" id="phase-refresh">Refresh</button>
</div>
<div class="panel-body">
<div class="ouroboros-panel" id="phase-body">
<div class="empty-state">
<div class="icon">🌀</div>
<p>Loading Ouroboros status...</p>
</div>
</div>
</div>
</div>
<div class="panel" id="panel-workspace">
<div class="panel-header">
<h2>📁 Workspace</h2>
<button class="btn btn-sm" id="workspace-refresh">Refresh</button>
</div>
<div class="workspace-split">
<div class="workspace-tree" id="workspace-tree"></div>
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
<div class="workspace-toolbar">
<span class="breadcrumb" id="workspace-breadcrumb">~</span>
<button class="btn btn-sm" id="workspace-save" style="display:none">Save</button>
</div>
<div class="workspace-viewer" id="workspace-viewer">
<div class="empty-state">
<div class="icon">📄</div>
<p>Select a file from the tree to view its contents.</p>
</div>
</div>
</div>
</div>
</div>
<div class="panel" id="panel-seeds">
<div class="panel-header">
<h2>🌱 Seeds</h2>
<button class="btn btn-sm" id="seeds-refresh">Refresh</button>
</div>
<div class="panel-body" id="seeds-body">
<div class="empty-state">
<div class="icon">🌱</div>
<p>No seeds yet. Seeds are created through the Ouroboros interview process.</p>
</div>
</div>
</div>
<div class="panel" id="panel-memory">
<div class="panel-header">
<h2>🧠 Memory</h2>
<button class="btn btn-sm" id="memory-refresh">Refresh</button>
</div>
<div class="panel-body" id="memory-body">
<div class="empty-state">
<div class="icon">🧠</div>
<p>No memory entries yet. Agent conversations generate daily memory summaries.</p>
</div>
</div>
</div>
<div class="panel" id="panel-skills">
<div class="panel-header">
<h2>📖 Skills</h2>
<button class="btn btn-sm" id="skills-refresh">Refresh</button>
</div>
<div class="panel-body" id="skills-body">
<div class="empty-state">
<div class="icon">📖</div>
<p>Loading skills...</p>
</div>
</div>
</div>
<div class="panel" id="panel-programs">
<div class="panel-header">
<h2>📦 Programs</h2>
<button class="btn btn-sm" id="programs-refresh">Refresh</button>
</div>
<div class="panel-body" id="programs-body">
<div class="program-install-form" id="program-install-form">
<h3>Install Program</h3>
<div class="program-install-row">
<input type="text" id="program-install-name" placeholder="Program name (e.g. my-program)" />
<input type="text" id="program-install-path" placeholder="Path to program directory" />
<button class="btn btn-sm" id="program-install-btn">Install</button>
</div>
</div>
<div class="empty-state" id="programs-empty">
<div class="icon">📦</div>
<p>No programs installed. Install a program to extend agent capabilities.</p>
</div>
</div>
</div>
<div class="panel" id="panel-personas">
<div class="panel-header">
<h2>🎭 Personas</h2>
<button class="btn btn-sm" id="personas-refresh">Refresh</button>
</div>
<div class="panel-body" id="personas-body">
<p style="color: var(--text-muted); font-size: 12px;">
Personas API available via REST. Access at <code>/api/personas</code>.
</p>
</div>
</div>
<div class="panel" id="panel-host-tools">
<div class="panel-header">
<h2>🔧 Host Tools</h2>
<button class="btn btn-sm" id="host-tools-refresh">Refresh</button>
</div>
<div class="panel-body" id="host-tools-body">
<div class="empty-state">
<div class="icon">🔧</div>
<p>Loading host tools...</p>
</div>
</div>
</div>
<div class="panel" id="panel-events">
<div class="panel-header">
<h2>📡 Events</h2>
<button class="btn btn-sm" id="events-clear">Clear</button>
</div>
<div class="panel-body">
<div class="event-log" id="event-log"></div>
</div>
</div>
<div class="panel" id="panel-scheduler">
<div class="panel-header">
<h2>📋 Scheduler</h2>
<button class="btn btn-sm" id="scheduler-refresh">Refresh</button>
</div>
<div class="panel-body" id="scheduler-body">
<div class="empty-state">
<div class="icon">📋</div>
<p>Loading scheduler stats...</p>
</div>
</div>
</div>
<div class="panel" id="panel-security">
<div class="panel-header">
<h2>🔒 Security</h2>
<div style="display:flex;gap:8px">
<button class="btn btn-sm" id="security-refresh">Refresh</button>
</div>
</div>
<div class="panel-body" id="security-body">
<div class="empty-state">
<div class="icon">🔒</div>
<p>Loading security info...</p>
</div>
</div>
</div>
<div class="panel" id="panel-approvals">
<div class="panel-header">
<h2>🛡️ Approvals</h2>
<div style="display:flex;gap:8px">
<button class="btn btn-sm" id="approvals-refresh">Refresh</button>
</div>
</div>
<div class="panel-body" id="approvals-body">
<div class="empty-state">
<div class="icon">🛡️</div>
<p>Loading pending approvals...</p>
</div>
</div>
</div>
<div class="panel" id="panel-config">
<div class="panel-header">
<h2>⚙️ Config</h2>
<button class="btn btn-sm" id="config-refresh">Refresh</button>
</div>
<div class="panel-body" id="config-body">
<div class="config-grid" id="config-grid"></div>
</div>
</div>
</main>
</div>
<div class="toast-container" id="toast-container"></div>
<script>
const API = '';
let currentPanel = 'chat';
let ws = null;
let eventSource = null;
let selectedFilePath = null;
let fileContent = '';
let currentSessionId = null;
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', () => {
const panel = item.dataset.panel;
switchPanel(panel);
});
});
function switchPanel(panel) {
currentPanel = panel;
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.querySelector(`.nav-item[data-panel="${panel}"]`).classList.add('active');
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
document.getElementById(`panel-${panel}`).classList.add('active');
switch (panel) {
case 'dashboard': loadDashboard(); break;
case 'protocol': loadProtocol(); break;
case 'phase': loadPhase(); break;
case 'agents': loadAgents(); break;
case 'workspace': loadWorkspaceTree(); break;
case 'seeds': loadSeeds(); break;
case 'memory': loadMemory(); break;
case 'skills': loadSkills(); break;
case 'programs': loadPrograms(); break;
case 'host-tools': loadHostTools(); break;
case 'scheduler': loadScheduler(); break;
case 'security': loadSecurity(); break;
case 'approvals': loadApprovals(); break;
case 'config': loadConfig(); break;
}
}
const chatInput = document.getElementById('chat-input');
const chatSend = document.getElementById('chat-send');
const chatMessages = document.getElementById('chat-messages');
function addChatMessage(text, type, meta) {
const div = document.createElement('div');
div.className = `chat-msg ${type}`;
div.textContent = text;
if (meta) {
const metaDiv = document.createElement('div');
metaDiv.className = 'meta';
metaDiv.textContent = meta;
div.appendChild(metaDiv);
}
chatMessages.appendChild(div);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
async function sendChatMessage() {
const text = chatInput.value.trim();
if (!text) return;
chatInput.value = '';
addChatMessage(text, 'user', 'you · just now');
const processingDiv = document.createElement('div');
processingDiv.className = 'chat-msg system';
processingDiv.id = 'processing-indicator';
processingDiv.textContent = 'Processing…';
chatMessages.appendChild(processingDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
try {
const body = { content: text };
if (currentSessionId) {
body.session_id = currentSessionId;
}
const res = await fetch(`${API}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await res.json();
const indicator = document.getElementById('processing-indicator');
if (indicator) indicator.remove();
if (data.session_id) {
currentSessionId = data.session_id;
}
const meta = data.session_id
? `oxios · ${data.id?.slice(0, 8) || ''} · ${data.phase || ''}`
: `oxios · ${data.id?.slice(0, 8) || ''}`;
addChatMessage(data.reply, 'system', meta);
} catch (err) {
const indicator = document.getElementById('processing-indicator');
if (indicator) indicator.remove();
addChatMessage(`Error: ${err.message}`, 'system', 'error');
}
}
chatSend.addEventListener('click', sendChatMessage);
chatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendChatMessage();
}
});
function connectWebSocket() {
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${location.host}/api/chat/stream`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
document.getElementById('ws-status').textContent = 'WebSocket: connected';
};
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
addChatMessage(msg.content, 'system', `${msg.channel} · ${msg.user_id}`);
} catch {
addChatMessage(event.data, 'system', 'stream');
}
};
ws.onclose = () => {
document.getElementById('ws-status').textContent = 'WebSocket: disconnected';
setTimeout(connectWebSocket, 3000);
};
ws.onerror = () => {
document.getElementById('ws-status').textContent = 'WebSocket: error';
};
}
connectWebSocket();
function connectSSE() {
eventSource = new EventSource(`${API}/api/events`);
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
addEvent(data);
} catch {
}
};
eventSource.onerror = () => {
};
}
connectSSE();
function addEvent(data) {
const log = document.getElementById('event-log');
const line = document.createElement('div');
line.className = 'event-line';
let displayText = JSON.stringify(data);
if (data.PhaseStarted) {
const { session_id, phase } = data.PhaseStarted;
const phaseIcon = getPhaseIcon(phase);
displayText = `${phaseIcon} Phase started: ${phase}`;
} else if (data.PhaseCompleted) {
const { session_id, phase, result_summary } = data.PhaseCompleted;
const phaseIcon = getPhaseIcon(phase);
displayText = `${phaseIcon} Phase completed: ${phase} — ${result_summary}`;
} else if (data.AgentStarted) {
displayText = `⚙️ Agent started: ${data.AgentStarted.id}`;
} else if (data.AgentStopped) {
displayText = `✅ Agent stopped: ${data.AgentStopped.id}`;
} else if (data.SeedCreated) {
displayText = `🌱 Seed created: ${data.SeedCreated.seed_id}`;
} else if (data.ApprovalRequested) {
displayText = `🛡️ Approval requested: ${data.ApprovalRequested.action} → ${data.ApprovalRequested.resource}`;
if (currentPanel === 'approvals') {
loadApprovals();
} else {
showToast(`Approval: ${data.ApprovalRequested.action}`, 'info');
}
} else if (data.ApprovalResolved) {
const icon = data.ApprovalResolved.approved ? '✅' : '❌';
displayText = `${icon} Approval ${data.ApprovalResolved.approved ? 'approved' : 'rejected'}`;
if (currentPanel === 'approvals') {
loadApprovals();
}
}
const type = Object.keys(data)[0] || 'unknown';
const time = new Date().toLocaleTimeString();
line.innerHTML = `<span class="event-time">${time}</span><span class="event-type">${type}</span> ${esc(displayText)}`;
log.appendChild(line);
while (log.children.length > 200) {
log.removeChild(log.firstChild);
}
if (currentPanel === 'events') {
log.parentElement.scrollTop = log.parentElement.scrollHeight;
}
updateProcessingIndicator(data);
}
function getPhaseIcon(phase) {
switch (phase) {
case 'interview': return '🤔';
case 'seed': return '📝';
case 'execute': return '⚙️';
case 'evaluate': return '✅';
case 'evolve': return '🔄';
default: return '🔹';
}
}
function updateProcessingIndicator(data) {
const indicator = document.getElementById('processing-indicator');
if (!indicator) return;
if (data.PhaseStarted) {
const icon = getPhaseIcon(data.PhaseStarted.phase);
indicator.textContent = `${icon} ${capitalize(data.PhaseStarted.phase)}…`;
} else if (data.PhaseCompleted) {
const icon = getPhaseIcon(data.PhaseCompleted.phase);
indicator.textContent = `${icon} ${capitalize(data.PhaseCompleted.phase)} done`;
}
}
function capitalize(s) {
return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
}
document.getElementById('events-clear').addEventListener('click', () => {
document.getElementById('event-log').innerHTML = '';
});
async function loadScheduler() {
const body = document.getElementById('scheduler-body');
try {
const res = await fetch(`${API}/api/scheduler/stats`);
const stats = await res.json();
const tasksRes = await fetch(`${API}/api/scheduler/tasks`);
const tasks = await tasksRes.json();
body.innerHTML = `
<div class="config-grid">
<div class="config-section">
<h3>Queue Status</h3>
<div class="config-row"><span class="key">Queued</span><span class="val">${stats.queued}</span></div>
<div class="config-row"><span class="key">Running</span><span class="val">${stats.running}</span></div>
<div class="config-row"><span class="key">Max Concurrent</span><span class="val">${stats.max_concurrent}</span></div>
<div class="config-row"><span class="key">Rate Limit/min</span><span class="val">${stats.rate_limit_per_minute}</span></div>
<div class="config-row"><span class="key">Rate Remaining</span><span class="val">${stats.rate_remaining}</span></div>
</div>
</div>
<h3 style="font-family:var(--font-mono);margin-top:24px;color:var(--accent)">Queued Tasks</h3>
<div class="item-list" id="scheduler-queued-list">
${tasks.queued.length === 0 ? '<div class="empty-state"><p>No queued tasks.</p></div>' :
tasks.queued.map(t => `
<div class="item-card">
<div class="item-title">${esc(t.description)} <span style="color:var(--yellow);font-size:11px">${t.priority}</span></div>
<div class="item-subtitle">ID: ${t.id.slice(0, 8)} · Status: ${t.status} · Created: ${new Date(t.created_at).toLocaleTimeString()}</div>
</div>
`).join('')
}
</div>
<h3 style="font-family:var(--font-mono);margin-top:24px;color:var(--green)">Running Tasks</h3>
<div class="item-list" id="scheduler-running-list">
${tasks.running.length === 0 ? '<div class="empty-state"><p>No running tasks.</p></div>' :
tasks.running.map(t => `
<div class="item-card" style="border-color:var(--green)">
<div class="item-title">${esc(t.description)} <span style="color:var(--green);font-size:11px">${t.priority}</span></div>
<div class="item-subtitle">ID: ${t.id.slice(0, 8)} · Status: ${t.status} · Created: ${new Date(t.created_at).toLocaleTimeString()}</div>
</div>
`).join('')
}
</div>
`;
} catch (err) {
body.innerHTML = `<div class="empty-state"><p>Error: ${esc(err.message)}</p></div>`;
}
}
document.getElementById('scheduler-refresh').addEventListener('click', loadScheduler);
async function loadSecurity() {
const body = document.getElementById('security-body');
try {
const auditRes = await fetch(`${API}/api/audit`);
const auditLog = await auditRes.json();
const denied = auditLog.filter(e => !e.allowed);
const recent = auditLog.slice(-20);
body.innerHTML = `
<div class="config-grid">
<div class="config-section">
<h3>Audit Log (${auditLog.length} entries)</h3>
${recent.length === 0 ? '<p style="color:var(--text-muted)">No access events recorded yet.</p>' :
recent.map(e => `
<div class="config-row" style="font-size:12px">
<span class="key">${new Date(e.timestamp).toLocaleTimeString()}</span>
<span style="color:${e.allowed ? 'var(--green)' : 'var(--red)'}">${e.allowed ? '✅' : '🚫'}</span>
<span class="val" style="flex:1">${esc(e.agent_name)} · ${esc(e.action)} · ${esc(e.resource)}</span>
</div>
`).join('')
}
</div>
</div>
${denied.length > 0 ? `
<h3 style="font-family:var(--font-mono);margin-top:24px;color:var(--red)">Recent Denied Actions (${denied.length})</h3>
<div class="item-list">
${denied.slice(-10).map(e => `
<div class="item-card" style="border-color:var(--red)">
<div class="item-title" style="color:var(--red)">🚫 ${esc(e.agent_name)} · ${esc(e.action)}</div>
<div class="item-subtitle">Resource: ${esc(e.resource)} · ${esc(e.reason || 'no reason')}</div>
</div>
`).join('')}
</div>
` : '<p style="color:var(--text-muted);margin-top:24px">No denied actions recorded.</p>'}
`;
} catch (err) {
body.innerHTML = `<div class="empty-state"><p>Error: ${esc(err.message)}</p></div>`;
}
}
document.getElementById('security-refresh').addEventListener('click', loadSecurity);
document.getElementById('approvals-refresh').addEventListener('click', loadApprovals);
let workspaceDir = '';
async function loadWorkspaceTree(dir) {
const tree = document.getElementById('workspace-tree');
workspaceDir = dir || '';
try {
const params = dir ? `?dir=${encodeURIComponent(dir)}` : '';
const res = await fetch(`${API}/api/workspace/tree${params}`);
const entries = await res.json();
if (!entries.length) {
tree.innerHTML = `<div class="tree-item" style="color:var(--text-muted)">Empty directory</div>`;
return;
}
let html = '';
if (dir) {
const parent = dir.includes('/') ? dir.split('/').slice(0, -1).join('/') : '';
html += `<div class="tree-item dir" onclick="loadWorkspaceTree('${escAttr(parent)}')"><span class="icon">📂</span> ..</div>`;
}
for (const e of entries) {
const icon = e.is_dir ? '📂' : '📄';
const cls = e.is_dir ? 'dir' : 'file';
const fullPath = dir ? `${dir}/${e.name}` : e.name;
if (e.is_dir) {
html += `<div class="tree-item ${cls}" onclick="loadWorkspaceTree('${escAttr(fullPath)}')"><span class="icon">${icon}</span> ${esc(e.name)}</div>`;
} else {
html += `<div class="tree-item ${cls}" onclick="loadFile('${escAttr(fullPath)}')"><span class="icon">${icon}</span> ${esc(e.name)}</div>`;
}
}
tree.innerHTML = html;
} catch (err) {
tree.innerHTML = `<div class="tree-item" style="color:var(--red)">Error: ${esc(err.message)}</div>`;
}
}
async function loadFile(path) {
selectedFilePath = path;
const viewer = document.getElementById('workspace-viewer');
const breadcrumb = document.getElementById('workspace-breadcrumb');
const saveBtn = document.getElementById('workspace-save');
breadcrumb.textContent = `~/${path}`;
try {
const res = await fetch(`${API}/api/workspace/file/${path}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
fileContent = await res.text();
const ext = path.split('.').pop().toLowerCase();
if (['md', 'txt', 'toml', 'yaml', 'yml', 'json', 'rs', 'js', 'html', 'css', 'sh'].includes(ext)) {
viewer.innerHTML = `<textarea id="file-editor">${esc(fileContent)}</textarea>`;
saveBtn.style.display = 'inline-flex';
} else {
viewer.innerHTML = `<pre>${esc(fileContent)}</pre>`;
saveBtn.style.display = 'none';
}
} catch (err) {
viewer.innerHTML = `<div class="empty-state"><p>Error: ${esc(err.message)}</p></div>`;
saveBtn.style.display = 'none';
}
}
document.getElementById('workspace-save').addEventListener('click', async () => {
if (!selectedFilePath) return;
const editor = document.getElementById('file-editor');
if (!editor) return;
try {
const res = await fetch(`${API}/api/workspace/file/${selectedFilePath}`, {
method: 'PUT',
body: editor.value,
});
if (res.ok) {
document.getElementById('workspace-save').textContent = 'Saved!';
setTimeout(() => { document.getElementById('workspace-save').textContent = 'Save'; }, 1500);
}
} catch (err) {
alert(`Save failed: ${err.message}`);
}
});
document.getElementById('workspace-refresh').addEventListener('click', () => loadWorkspaceTree(workspaceDir));
let selectedSeed = null;
async function loadSeeds() {
const body = document.getElementById('seeds-body');
try {
const res = await fetch(`${API}/api/seeds`);
const seeds = await res.json();
if (!seeds.length) {
body.innerHTML = `<div class="empty-state"><div class="icon">🌱</div><p>No seeds yet.</p></div>`;
return;
}
let html = '<div class="item-list">';
for (const s of seeds) {
html += `
<div class="item-card" onclick="loadSeedDetail('${escAttr(s.id)}')">
<div class="item-title">${esc(s.goal || s.id)}</div>
<div class="item-subtitle">${s.id.slice(0, 8)} · ${s.constraints_count} constraints · ${s.created_at || 'unknown date'}</div>
</div>
`;
}
html += '</div>';
if (selectedSeed) {
html += `<div class="detail-view" id="seed-detail"></div>`;
}
body.innerHTML = html;
if (selectedSeed) {
loadSeedDetail(selectedSeed);
}
} catch (err) {
body.innerHTML = `<div class="empty-state"><p>Error: ${esc(err.message)}</p></div>`;
}
}
async function loadSeedDetail(id) {
selectedSeed = id;
let detail = document.getElementById('seed-detail');
if (!detail) {
await loadSeeds();
detail = document.getElementById('seed-detail');
}
if (!detail) return;
try {
const res = await fetch(`${API}/api/seeds/${id}`);
const data = await res.json();
detail.innerHTML = `<h3>Seed: ${id.slice(0, 8)}</h3><pre>${esc(JSON.stringify(data, null, 2))}</pre>`;
} catch (err) {
detail.innerHTML = `<p style="color:var(--red)">Error: ${esc(err.message)}</p>`;
}
}
document.getElementById('seeds-refresh').addEventListener('click', loadSeeds);
let selectedMemory = null;
async function loadMemory() {
const body = document.getElementById('memory-body');
try {
const res = await fetch(`${API}/api/memory`);
const entries = await res.json();
if (!entries.length) {
body.innerHTML = `<div class="empty-state"><div class="icon">🧠</div><p>No memory entries yet.</p></div>`;
return;
}
let html = '<div class="item-list">';
for (const m of entries) {
html += `
<div class="item-card" onclick="loadMemoryDetail('${escAttr(m.name)}')">
<div class="item-title">${esc(m.name)}</div>
<div class="item-subtitle">${m.category}</div>
</div>
`;
}
html += '</div>';
if (selectedMemory) {
html += `<div class="detail-view" id="memory-detail"></div>`;
}
body.innerHTML = html;
if (selectedMemory) {
loadMemoryDetail(selectedMemory);
}
} catch (err) {
body.innerHTML = `<div class="empty-state"><p>Error: ${esc(err.message)}</p></div>`;
}
}
async function loadMemoryDetail(name) {
selectedMemory = name;
let detail = document.getElementById('memory-detail');
if (!detail) {
await loadMemory();
detail = document.getElementById('memory-detail');
}
if (!detail) return;
try {
const res = await fetch(`${API}/api/memory/${name}`);
const data = await res.json();
detail.innerHTML = `<h3>${esc(name)}</h3><pre>${esc(data.content || JSON.stringify(data, null, 2))}</pre>`;
} catch (err) {
detail.innerHTML = `<p style="color:var(--red)">Error: ${esc(err.message)}</p>`;
}
}
document.getElementById('memory-refresh').addEventListener('click', loadMemory);
let selectedSkill = null;
async function loadSkills() {
const body = document.getElementById('skills-body');
try {
const res = await fetch(`${API}/api/skills`);
const skills = await res.json();
if (!skills.length) {
body.innerHTML = `<div class="empty-state"><div class="icon">📖</div><p>No skills yet. Skills are instruction templates for agents.</p></div>`;
return;
}
let html = '<div class="item-list">';
for (const s of skills) {
html += `
<div class="item-card" onclick="loadSkillDetail('${escAttr(s.name)}')">
<div class="item-title">${esc(s.name)}</div>
<div class="item-subtitle">${esc(s.description || 'No description')}</div>
</div>
`;
}
html += '</div>';
html += `
<button class="btn btn-sm" id="skills-create-btn" style="margin-top:16px;background:var(--green);color:#000">+ New Skill</button>
`;
if (selectedSkill) {
html += `<div class="detail-view" id="skill-detail"></div>`;
}
body.innerHTML = html;
const createBtn = document.getElementById('skills-create-btn');
if (createBtn) {
createBtn.addEventListener('click', showSkillCreate);
}
if (selectedSkill) {
loadSkillDetail(selectedSkill);
}
} catch (err) {
body.innerHTML = `<div class="empty-state"><p>Error: ${esc(err.message)}</p></div>`;
}
}
async function loadSkillDetail(name) {
selectedSkill = name;
let detail = document.getElementById('skill-detail');
if (!detail) {
await loadSkills();
detail = document.getElementById('skill-detail');
}
if (!detail) return;
try {
const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}`);
const data = await res.json();
detail.innerHTML = `
<h3>${esc(data.name || name)}</h3>
<p style="color:var(--text-secondary);margin-bottom:12px">${esc(data.description || '')}</p>
<pre style="white-space:pre-wrap">${esc(data.content || '')}</pre>
<div style="margin-top:12px;display:flex;gap:8px">
<button class="btn btn-danger btn-sm" onclick="deleteSkill('${escAttr(name)}')">Delete</button>
</div>
`;
} catch (err) {
detail.innerHTML = `<p style="color:var(--red)">Error: ${esc(err.message)}</p>`;
}
}
function showSkillCreate() {
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.innerHTML = `
<div class="modal">
<h3>Create Skill</h3>
<label>Skill Name</label>
<input type="text" id="skill-create-name" placeholder="e.g. code-review" />
<label>Description</label>
<input type="text" id="skill-create-desc" placeholder="Brief description of this skill" />
<label>Content (Markdown)</label>
<textarea id="skill-create-content" rows="10" style="width:100%;background:var(--bg-primary);color:var(--text-primary);border:1px solid var(--border);border-radius:var(--radius);padding:10px;font-family:var(--font-mono);font-size:13px" placeholder="# Skill Title Skill instructions..."></textarea>
<div class="modal-actions">
<button class="btn btn-sm" id="skill-create-cancel">Cancel</button>
<button class="btn btn-sm" style="background:var(--green);color:#000" id="skill-create-submit">Create</button>
</div>
</div>
`;
document.body.appendChild(overlay);
document.getElementById('skill-create-cancel').addEventListener('click', () => overlay.remove());
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
document.getElementById('skill-create-submit').addEventListener('click', async () => {
const name = document.getElementById('skill-create-name').value.trim();
const desc = document.getElementById('skill-create-desc').value.trim();
const content = document.getElementById('skill-create-content').value.trim();
if (!name) return;
try {
const res = await fetch(`${API}/api/skills`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description: desc, content }),
});
if (!res.ok) {
const err = await res.json();
alert(`Error: ${err.error || 'Failed to create skill'}`);
return;
}
overlay.remove();
loadSkills();
} catch (err) {
alert(`Failed to create skill: ${err.message}`);
}
});
}
async function deleteSkill(name) {
if (!confirm(`Delete skill "${name}"? This cannot be undone.`)) return;
try {
await fetch(`${API}/api/skills/${encodeURIComponent(name)}`, { method: 'DELETE' });
selectedSkill = null;
loadSkills();
} catch (err) {
alert(`Failed to delete skill: ${err.message}`);
}
}
document.getElementById('skills-refresh').addEventListener('click', loadSkills);
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'toast-out 0.3s ease forwards';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
const sidebarToggle = document.getElementById('sidebar-toggle');
const sidebar = document.querySelector('.sidebar');
sidebarToggle.addEventListener('click', () => {
sidebar.classList.toggle('collapsed');
sidebarToggle.textContent = sidebar.classList.contains('collapsed') ? '▶' : '◀';
});
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'Enter') {
const chatInput = document.getElementById('chat-input');
if (chatInput && document.activeElement === chatInput) {
e.preventDefault();
sendChatMessage();
}
}
if (e.key === 'Escape') {
const modals = document.querySelectorAll('.modal-overlay');
modals.forEach(modal => modal.remove());
}
if (e.ctrlKey && e.key === 'b') {
e.preventDefault();
sidebarToggle.click();
}
});
let selectedProgram = null;
async function loadPrograms() {
const body = document.getElementById('programs-body');
try {
const res = await fetch(`${API}/api/programs`);
const programs = await res.json();
const installForm = document.getElementById('program-install-form');
if (!programs.length) {
const existingCards = body.querySelectorAll('.program-card');
existingCards.forEach(c => c.remove());
if (!document.getElementById('programs-empty')) {
const empty = document.createElement('div');
empty.id = 'programs-empty';
empty.className = 'empty-state';
empty.innerHTML = '<div class="icon">📦</div><p>No programs installed. Install a program to extend agent capabilities.</p>';
body.appendChild(empty);
}
return;
}
const emptyState = document.getElementById('programs-empty');
if (emptyState) emptyState.remove();
let html = '<div class="item-list" id="programs-list">';
for (const p of programs) {
const enabledCls = p.enabled ? 'yes' : 'no';
const enabledText = p.enabled ? 'ENABLED' : 'DISABLED';
html += `
<div class="program-card" onclick="loadProgramDetail('${escAttr(p.name)}')">
<div class="program-name">
📦 ${esc(p.name)}
<span class="program-enabled ${enabledCls}">${enabledText}</span>
</div>
<div class="program-version">v${esc(p.version || '0.0.0')}</div>
<div class="program-desc">${esc(p.description || 'No description')}</div>
<div style="margin-top:8px">
<button class="btn btn-danger btn-sm" onclick="event.stopPropagation(); deleteProgram('${escAttr(p.name)}')">Delete</button>
</div>
</div>
`;
}
html += '</div>';
if (selectedProgram) {
html += `<div class="detail-view" id="program-detail"></div>`;
}
const existingList = document.getElementById('programs-list');
if (existingList) {
existingList.remove();
}
body.insertAdjacentHTML('beforeend', html);
if (selectedProgram) {
loadProgramDetail(selectedProgram);
}
} catch (err) {
showToast(`Error loading programs: ${err.message}`, 'error');
}
}
async function loadProgramDetail(name) {
selectedProgram = name;
let detail = document.getElementById('program-detail');
if (!detail) {
await loadPrograms();
detail = document.getElementById('program-detail');
}
if (!detail) return;
try {
const res = await fetch(`${API}/api/programs/${encodeURIComponent(name)}`);
const data = await res.json();
detail.innerHTML = `
<h3>${esc(data.name || name)}</h3>
<p style="color:var(--text-secondary);margin-bottom:12px">${esc(data.description || 'No description')}</p>
<div class="config-row"><span class="key">Version</span><span class="val">${esc(data.version || 'unknown')}</span></div>
<div class="config-row"><span class="key">Enabled</span><span class="val">${data.enabled ? 'Yes' : 'No'}</span></div>
${data.tools ? `<div class="config-row"><span class="key">Tools</span><span class="val">${esc(JSON.stringify(data.tools))}</span></div>` : ''}
<pre style="white-space:pre-wrap;margin-top:12px">${esc(data.content || '')}</pre>
`;
} catch (err) {
detail.innerHTML = `<p style="color:var(--red)">Error: ${esc(err.message)}</p>`;
}
}
async function deleteProgram(name) {
if (!confirm(`Delete program "${name}"? This cannot be undone.`)) return;
try {
await fetch(`${API}/api/programs/${encodeURIComponent(name)}`, { method: 'DELETE' });
selectedProgram = null;
showToast(`Program "${name}" deleted`, 'success');
loadPrograms();
} catch (err) {
showToast(`Failed to delete program: ${err.message}`, 'error');
}
}
document.getElementById('program-install-btn').addEventListener('click', async () => {
const name = document.getElementById('program-install-name').value.trim();
const path = document.getElementById('program-install-path').value.trim();
if (!name || !path) {
showToast('Please provide both name and path', 'error');
return;
}
try {
const res = await fetch(`${API}/api/programs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, path }),
});
if (!res.ok) {
const err = await res.json();
showToast(`Error: ${err.error || 'Failed to install program'}`, 'error');
return;
}
document.getElementById('program-install-name').value = '';
document.getElementById('program-install-path').value = '';
showToast(`Program "${name}" installed`, 'success');
loadPrograms();
} catch (err) {
showToast(`Failed to install program: ${err.message}`, 'error');
}
});
document.getElementById('programs-refresh').addEventListener('click', loadPrograms);
async function loadHostTools() {
const body = document.getElementById('host-tools-body');
try {
const res = await fetch(`${API}/api/host-tools`);
const data = await res.json();
const allRequired = data.required || [];
const allOptional = data.optional || [];
const requiredAvailable = data.required_available || [];
const optionalAvailable = data.optional_available || [];
const missingRequired = data.missing_required || [];
let html = `
<div class="host-tools-summary">
<div>All Required Present: <span class="stat ${missingRequired.length === 0 ? 'green' : 'red'}">${missingRequired.length === 0 ? '✓' : '✗'}</span></div>
<div>Required: <span class="stat green">${requiredAvailable.length}/${allRequired.length}</span></div>
<div>Optional: <span class="stat">${optionalAvailable.length}/${allOptional.length}</span></div>
</div>
`;
html += `
<div class="host-tools-section">
<h3>Required Tools</h3>
`;
if (allRequired.length === 0) {
html += '<p style="color:var(--text-muted)">No required tools specified.</p>';
} else {
for (const tool of allRequired) {
const available = requiredAvailable.includes(tool);
const icon = available ? '<span class="tool-icon">✅</span>' : '<span class="tool-icon">❌</span>';
const cls = available ? 'tool-available' : 'tool-missing';
html += `
<div class="host-tool-item">
${icon}
<span class="tool-name ${cls}">${esc(tool)}</span>
<span style="color:var(--text-muted);font-size:11px">${available ? 'available' : 'MISSING'}</span>
</div>
`;
}
}
html += '</div>';
html += `
<div class="host-tools-section">
<h3>Optional Tools</h3>
`;
if (allOptional.length === 0) {
html += '<p style="color:var(--text-muted)">No optional tools specified.</p>';
} else {
for (const tool of allOptional) {
const available = optionalAvailable.includes(tool);
const icon = available ? '<span class="tool-icon">✅</span>' : '<span class="tool-icon">⚪</span>';
const cls = available ? 'tool-available' : 'tool-missing';
html += `
<div class="host-tool-item">
${icon}
<span class="tool-name ${cls}">${esc(tool)}</span>
<span style="color:var(--text-muted);font-size:11px">${available ? 'available' : 'not installed'}</span>
</div>
`;
}
}
html += '</div>';
body.innerHTML = html;
} catch (err) {
body.innerHTML = `<div class="empty-state"><p>Error: ${esc(err.message)}</p></div>`;
}
}
document.getElementById('host-tools-refresh').addEventListener('click', loadHostTools);
async function loadConfig() {
const body = document.getElementById('config-body');
try {
const res = await fetch(`${API}/api/config`);
const config = await res.json();
let html = '<div class="config-grid">';
for (const [section, values] of Object.entries(config)) {
html += `<div class="config-section"><h3>${esc(section)}</h3>`;
if (typeof values === 'object' && values !== null) {
for (const [key, val] of Object.entries(values)) {
html += `<div class="config-row"><span class="key">${esc(key)}</span><span class="val">${esc(String(val))}</span></div>`;
}
} else {
html += `<div class="config-row"><span class="val">${esc(String(values))}</span></div>`;
}
html += '</div>';
}
html += '</div>';
body.innerHTML = html;
} catch (err) {
body.innerHTML = `<div class="empty-state"><p>Error: ${esc(err.message)}</p></div>`;
}
}
document.getElementById('config-refresh').addEventListener('click', loadConfig);
async function loadApprovals() {
const body = document.getElementById('approvals-body');
try {
const res = await fetch(`${API}/api/approvals`);
const approvals = await res.json();
if (!approvals.length) {
body.innerHTML = `
<div class="empty-state">
<div class="icon">🛡️</div>
<p>No approval requests. HitL is active when agents request high-risk actions.</p>
</div>
`;
return;
}
const pending = approvals.filter(a => a.status === 'pending');
const resolved = approvals.filter(a => a.status !== 'pending');
let html = '';
if (pending.length) {
html += `<h3 style="font-family:var(--font-mono);color:var(--yellow);margin-bottom:12px">⏳ Pending (${pending.length})</h3>`;
html += '<div class="item-list">';
for (const a of pending) {
const actionColor = a.action.includes('SystemConfig') || a.action.includes('ManageRBAC')
? 'var(--red)' : 'var(--yellow)';
html += `
<div class="item-card" style="border-color:var(--yellow)">
<div class="item-title" style="color:${actionColor}">🛡️ ${esc(a.action)}</div>
<div class="item-subtitle">${esc(a.subject)} → ${esc(a.resource)}</div>
<div class="item-subtitle" style="color:var(--text-muted)">${esc(a.reason)}</div>
<div style="margin-top:10px;display:flex;gap:8px">
<button class="btn btn-sm" style="background:var(--green);color:#000" onclick="approveRequest('${escAttr(a.id)}')">✓ Approve</button>
<button class="btn btn-sm btn-danger" onclick="rejectRequest('${escAttr(a.id)}')">✗ Reject</button>
</div>
</div>
`;
}
html += '</div>';
}
if (resolved.length) {
html += `<h3 style="font-family:var(--font-mono);color:var(--text-muted);margin-top:24px;margin-bottom:12px">📜 History (${resolved.length})</h3>`;
html += '<div class="item-list">';
for (const a of resolved.slice().reverse()) {
const statusColor = a.status === 'approved' ? 'var(--green)' : 'var(--red)';
const statusIcon = a.status === 'approved' ? '✓' : '✗';
html += `
<div class="item-card">
<div style="display:flex;align-items:center;gap:8px">
<span style="color:${statusColor};font-weight:bold">${statusIcon}</span>
<span style="color:${statusColor}">${esc(a.action)}</span>
</div>
<div class="item-subtitle">${esc(a.subject)} → ${esc(a.resource)}</div>
<div class="item-subtitle" style="font-size:11px">${new Date(a.created_at).toLocaleString()}</div>
</div>
`;
}
html += '</div>';
}
body.innerHTML = html;
} catch (err) {
body.innerHTML = `<div class="empty-state"><p>Error: ${esc(err.message)}</p></div>`;
}
}
async function approveRequest(id) {
try {
const res = await fetch(`${API}/api/approvals/${id}/approve`, { method: 'POST' });
if (res.ok) {
showToast(`Request approved`, 'success');
loadApprovals();
} else {
showToast('Failed to approve request', 'error');
}
} catch (err) {
showToast(`Error: ${err.message}`, 'error');
}
}
async function rejectRequest(id) {
try {
const res = await fetch(`${API}/api/approvals/${id}/reject`, { method: 'POST' });
if (res.ok) {
showToast(`Request rejected`, 'error');
loadApprovals();
} else {
showToast('Failed to reject request', 'error');
}
} catch (err) {
showToast(`Error: ${err.message}`, 'error');
}
}
async function loadDashboard() {
const body = document.getElementById('dashboard-body');
try {
const [agentRes, protocolRes, phaseRes] = await Promise.all([
fetch(`${API}/api/agents`),
fetch(`${API}/api/protocol/status`),
fetch(`${API}/api/ouroboros/phase`).catch(() => null),
]);
const agents = agentRes.ok ? await agentRes.json() : [];
const protocol = protocolRes.ok ? await protocolRes.json() : {};
const phaseData = phaseRes ? await phaseRes.json() : null;
const runningAgents = agents.filter(a => a.status === 'running');
const forkedAgents = agents.filter(a => a.status === 'forked');
const completedAgents = agents.filter(a => a.status === 'completed');
const failedAgents = agents.filter(a => a.status === 'failed');
body.innerHTML = `
<div class="config-grid">
<!-- Agent Status Summary -->
<div class="config-section">
<h3>Agent Status</h3>
<div class="config-row"><span class="key">Total</span><span class="val">${agents.length}</span></div>
<div class="config-row"><span class="key">Running</span><span class="val" style="color:var(--green)">${runningAgents.length}</span></div>
<div class="config-row"><span class="key">Forked</span><span class="val" style="color:var(--yellow)">${forkedAgents.length}</span></div>
<div class="config-row"><span class="key">Completed</span><span class="val">${completedAgents.length}</span></div>
<div class="config-row"><span class="key">Failed</span><span class="val" style="color:var(--red)">${failedAgents.length}</span></div>
</div>
<!-- Protocol Status Summary -->
<div class="config-section">
<h3>Protocol</h3>
<div class="config-row"><span class="key">MCP Servers</span><span class="val">${protocol.mcp_servers ? protocol.mcp_servers.length : 0}</span></div>
<div class="config-row"><span class="key">A2A Agents</span><span class="val">${protocol.a2a_agents || protocol.a2a_registry || 0}</span></div>
<div class="config-row"><span class="key">RBAC Role</span><span class="val">${protocol.rbac_role || 'unknown'}</span></div>
</div>
<!-- Ouroboros Phase Summary -->
<div class="config-section">
<h3>Ouroboros</h3>
<div class="config-row"><span class="key">Phase</span><span class="val" style="color:var(--accent)">${phaseData ? phaseData.phase : 'idle'}</span></div>
<div class="config-row"><span class="key">Iteration</span><span class="val">${phaseData ? phaseData.iteration : 0}</span></div>
<div class="config-row"><span class="key">Progress</span><span class="val">${phaseData ? Math.round(phaseData.progress * 100) + '%' : '0%'}</span></div>
</div>
</div>
<!-- Agent Cards -->
${agents.length > 0 ? `
<h3 style="font-family:var(--font-mono);margin-top:24px;color:var(--accent)">Active Agents</h3>
<div class="agent-monitor-grid">
${agents.map(a => `
<div class="agent-monitor-card" onclick="viewAgentDetail('${escAttr(a.id)}')">
<div class="agent-monitor-header">
<span class="agent-monitor-title">${esc(a.name || 'Agent')}</span>
<span class="agent-status status-${a.status}">${a.status}</span>
</div>
<div class="agent-registry-id">${a.id.slice(0, 16)}</div>
${a.capabilities ? `
<div class="agent-registry-capabilities">
${a.capabilities.map(c => `<span class="agent-cap-tag">${esc(c)}</span>`).join('')}
</div>
` : ''}
</div>
`).join('')}
</div>
` : ''}
`;
} catch (err) {
body.innerHTML = `<div class="empty-state"><p>Error: ${esc(err.message)}</p></div>`;
}
}
async function loadProtocol() {
const body = document.getElementById('protocol-body');
try {
const [mcpRes, a2aRes, rbacRes] = await Promise.all([
fetch(`${API}/api/mcp/servers`).catch(() => null),
fetch(`${API}/api/a2a/agents`).catch(() => null),
fetch(`${API}/api/rbac/status`).catch(() => null),
]);
const mcpServers = mcpRes?.ok ? await mcpRes.json() : [];
const a2aAgents = a2aRes?.ok ? await a2aRes.json() : [];
const rbac = rbacRes?.ok ? await rbacRes.json() : {};
body.innerHTML = `
<div class="protocol-grid">
<!-- MCP Servers -->
<div class="protocol-card">
<div class="protocol-card-header">
<span class="protocol-card-title">MCP Servers</span>
<span class="protocol-status-badge ${mcpServers.length > 0 ? 'protocol-connected' : 'protocol-disconnected'}">
${mcpServers.length > 0 ? 'Connected' : 'No Servers'}
</span>
</div>
${mcpServers.length > 0 ? `
${mcpServers.map(s => `
<div class="mcp-server-item">
<span class="mcp-server-name">${esc(s.name || s.id)}</span>
<span class="mcp-server-status" style="color:${getMcpStatusColor(s.status)}">${s.status || 'unknown'}</span>
</div>
`).join('')}
` : '<p style="color:var(--text-muted);font-size:12px">No MCP servers configured.</p>'}
</div>
<!-- A2A Registry -->
<div class="protocol-card">
<div class="protocol-card-header">
<span class="protocol-card-title">A2A Registry</span>
<span class="protocol-status-badge ${a2aAgents.length > 0 ? 'protocol-connected' : 'protocol-disconnected'}">
${a2aAgents.length} registered
</span>
</div>
${a2aAgents.length > 0 ? `
<div class="agent-registry-list">
${a2aAgents.map(a => `
<div class="agent-registry-item" onclick="viewAgentDetail('${escAttr(a.id)}')">
<div class="agent-registry-name">${esc(a.name || a.id)}</div>
<div class="agent-registry-id">${a.id.slice(0, 16)}</div>
${a.capabilities ? `
<div class="agent-registry-capabilities">
${a.capabilities.slice(0, 5).map(c => `<span class="agent-cap-tag">${esc(c)}</span>`).join('')}
${a.capabilities.length > 5 ? `<span class="agent-cap-tag">+${a.capabilities.length - 5}</span>` : ''}
</div>
` : ''}
</div>
`).join('')}
</div>
` : '<p style="color:var(--text-muted);font-size:12px">No agents registered in A2A registry.</p>'}
</div>
<!-- RBAC Status -->
<div class="protocol-card" style="grid-column: span 2;">
<div class="protocol-card-header">
<span class="protocol-card-title">RBAC</span>
<span class="protocol-status-badge ${rbac.current_role ? 'protocol-connected' : 'protocol-disconnected'}">
Role: ${rbac.current_role || 'unknown'}
</span>
</div>
<div class="protocol-stat">
<span class="key">Current Role</span>
<span class="val">${rbac.current_role || 'unknown'}</span>
</div>
<div class="protocol-stat">
<span class="key">Permissions</span>
<span class="val">${rbac.permissions?.length || 0}</span>
</div>
${rbac.permissions && rbac.permissions.length > 0 ? `
<div class="protocol-permissions">
${rbac.permissions.map(p => `<span class="protocol-permission-tag">${esc(p)}</span>`).join('')}
</div>
` : ''}
</div>
</div>
`;
} catch (err) {
body.innerHTML = `<div class="empty-state"><p>Error: ${esc(err.message)}</p></div>`;
}
}
function getMcpStatusColor(status) {
switch (status) {
case 'connected': return 'var(--green)';
case 'disconnected': return 'var(--text-secondary)';
case 'error': return 'var(--red)';
default: return 'var(--text-muted)';
}
}
async function loadPhase() {
const body = document.getElementById('phase-body');
try {
const res = await fetch(`${API}/api/ouroboros/phase`);
const data = await res.json();
const phases = ['interview', 'seed', 'execute', 'evaluate', 'evolve'];
const phaseIcons = { interview: '🤔', seed: '📝', execute: '⚙️', evaluate: '✅', evolve: '🔄' };
const currentPhase = data.phase || 'idle';
const currentIndex = phases.indexOf(currentPhase);
const progress = data.progress || 0;
const iteration = data.iteration || 0;
body.innerHTML = `
<div class="phase-indicator">
<div class="phase-current">
${phaseIcons[currentPhase] || '🌀'} ${capitalize(currentPhase)} Phase
${currentPhase !== 'idle' ? `<span style="font-size:14px;color:var(--text-muted)">· Iteration ${iteration}</span>` : ''}
</div>
<div class="phase-progress-bar">
<div class="phase-progress-fill" style="width: ${Math.round(progress * 100)}%"></div>
</div>
<div class="phase-meta">
<span>${Math.round(progress * 100)}% complete</span>
<span>Iteration ${iteration}</span>
</div>
<div class="phase-steps">
${phases.map((phase, idx) => {
let stepState = 'default';
if (idx < currentIndex) stepState = 'completed';
else if (phase === currentPhase) stepState = 'active';
return `
<div class="phase-step ${stepState}">
<div class="phase-step-icon">${phaseIcons[phase]}</div>
<span class="phase-step-label">${phase}</span>
</div>
`;
}).join('')}
</div>
</div>
${data.session_id ? `
<div class="config-section" style="margin-top:16px">
<h3>Session</h3>
<div class="config-row"><span class="key">Session ID</span><span class="val">${data.session_id.slice(0, 16)}</span></div>
<div class="config-row"><span class="key">Seed ID</span><span class="val">${data.seed_id ? data.seed_id.slice(0, 16) : 'none'}</span></div>
<div class="config-row"><span class="key">Started</span><span class="val">${data.started_at || 'unknown'}</span></div>
</div>
` : ''}
`;
} catch (err) {
body.innerHTML = `
<div class="phase-indicator">
<div class="phase-current">🌀 Idle</div>
<div class="phase-progress-bar">
<div class="phase-progress-fill" style="width: 0%"></div>
</div>
<div class="phase-meta">
<span>No active session</span>
<span>Iteration 0</span>
</div>
<div class="phase-steps">
${['interview', 'seed', 'execute', 'evaluate', 'evolve'].map(phase => `
<div class="phase-step">
<div class="phase-step-icon">${phase === 'interview' ? '🤔' : phase === 'seed' ? '📝' : phase === 'execute' ? '⚙️' : phase === 'evaluate' ? '✅' : '🔄'}</div>
<span class="phase-step-label">${phase}</span>
</div>
`).join('')}
</div>
</div>
<p style="color:var(--text-muted);font-size:12px;margin-top:16px">
Ouroboros phase information unavailable. Start a conversation to initiate the interview phase.
</p>
`;
}
}
function viewAgentDetail(agentId) {
switchPanel('agents');
loadAgents();
}
document.getElementById('dashboard-refresh').addEventListener('click', loadDashboard);
document.getElementById('protocol-refresh').addEventListener('click', loadProtocol);
document.getElementById('phase-refresh').addEventListener('click', loadPhase);
async function loadAgents() {
const body = document.getElementById('agents-body');
try {
const res = await fetch(`${API}/api/agents`);
const agents = await res.json();
if (!agents.length) {
body.innerHTML = `<div class="empty-state"><div class="icon">⚡</div><p>No agents running.</p></div>`;
return;
}
body.innerHTML = `
<div class="agent-monitor-grid">
<div class="agent-monitor-card">
<div class="agent-monitor-header">
<span class="agent-monitor-title">Active Agents</span>
<span class="agent-monitor-count">${agents.length}</span>
</div>
<div class="agent-registry-list">
${agents.map(a => `
<div class="agent-registry-item selected" onclick="showAgentDetail('${escAttr(a.id)}')">
<div style="display:flex;justify-content:space-between;align-items:center">
<div class="agent-registry-name">${esc(a.name || 'Agent')}</div>
<span class="agent-status status-${a.status}">${a.status}</span>
</div>
<div class="agent-registry-id">${a.id.slice(0, 16)}</div>
${a.capabilities ? `
<div class="agent-registry-capabilities">
${a.capabilities.map(c => `<span class="agent-cap-tag">${esc(c)}</span>`).join('')}
</div>
` : ''}
</div>
`).join('')}
</div>
</div>
<div class="agent-monitor-card">
<div class="agent-monitor-header">
<span class="agent-monitor-title">Agent Capabilities</span>
</div>
<p style="color:var(--text-muted);font-size:12px">Select an agent to view its capabilities from the A2A registry.</p>
</div>
</div>
`;
} catch (err) {
body.innerHTML = `<div class="empty-state"><p>Error loading agents: ${esc(err.message)}</p></div>`;
}
}
async function showAgentDetail(agentId) {
try {
const res = await fetch(`${API}/api/agents/${agentId}`);
if (!res.ok) return;
const agent = await res.json();
const detail = document.createElement('div');
detail.className = 'agent-detail-panel';
detail.id = 'agent-detail-' + agentId.slice(0, 8);
detail.innerHTML = `
<h4>Agent: ${esc(agent.name || agentId.slice(0, 8))}</h4>
<div class="config-row"><span class="key">ID</span><span class="val">${agent.id}</span></div>
<div class="config-row"><span class="key">Status</span><span class="val">${agent.status}</span></div>
${agent.capabilities ? `
<div class="config-row"><span class="key">Capabilities</span></div>
<div class="protocol-permissions" style="margin-top:8px">
${agent.capabilities.map(c => `<span class="protocol-permission-tag">${esc(c)}</span>`).join('')}
</div>
` : ''}
${agent.created_at ? `<div class="config-row"><span class="key">Created</span><span class="val">${agent.created_at}</span></div>` : ''}
`;
const existing = document.getElementById('agent-detail-' + agentId.slice(0, 8));
if (existing) existing.remove();
document.getElementById('agents-body').appendChild(detail);
} catch (err) {
}
}
async function killAgent(id) {
try {
await fetch(`${API}/api/agents/${id}/kill`, { method: 'POST' });
loadAgents();
} catch (err) {
alert(`Failed to kill agent: ${err.message}`);
}
}
document.getElementById('agents-refresh').addEventListener('click', loadAgents);
function esc(s) {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
function escAttr(s) {
return s.replace(/'/g, "\\'").replace(/"/g, '"');
}
(async () => {
try {
const res = await fetch(`${API}/api/status`);
const data = await res.json();
document.querySelector('.sidebar-footer span:last-child').textContent =
`${data.status} · v${data.version}`;
} catch {
document.getElementById('conn-status').textContent = 'disconnected';
}
})();
</script>
</body>
</html>