Files
amc/dashboard.html
2026-02-25 09:21:59 -05:00

1471 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AMC</title>
<style>
:root {
--bg: #100F0F;
--surface: #1C1B1A;
--selection: #282726;
--fg: #CECDC3;
--bright: #FFFCF0;
--dim: #575653;
--active: #879A39;
--needs-attention: #D0A215;
--starting: #4385BE;
--done: #8B7EC8; /* Soft purple - ready/waiting, not dead */
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--fg);
font-family: "Berkeley Mono", "SF Mono", "Cascadia Code", "JetBrains Mono", monospace;
font-size: 13px;
line-height: 1.5;
min-height: 100vh;
}
.header {
background: var(--surface);
border-bottom: 1px solid var(--selection);
padding: 12px 20px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 10;
}
.header-title {
font-size: 14px;
font-weight: 600;
color: var(--bright);
letter-spacing: 0.5px;
}
.status-counts {
display: flex;
gap: 16px;
align-items: center;
}
.count-badge {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
color: var(--dim);
}
.count-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.count-num {
font-weight: 600;
color: var(--fg);
min-width: 12px;
text-align: center;
}
.main { padding: 16px 20px; }
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--dim);
}
.empty-state-title {
font-size: 16px;
color: var(--fg);
margin-bottom: 8px;
}
/* Status group sections */
.status-group { margin-bottom: 24px; }
.sessions-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 16px;
}
.status-group-header {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
padding: 4px 0 8px;
border-bottom: 1px solid var(--selection);
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.status-group-header .count-dot {
width: 6px;
height: 6px;
}
.status-group-header.needs-attention { color: var(--needs-attention); }
.status-group-header.done { color: var(--done); }
.status-group-header.active-header { color: var(--active); }
/* Session cards */
.session {
background: var(--surface);
border: 1px solid var(--selection);
border-radius: 8px;
padding: 0;
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s;
display: flex;
flex-direction: column;
height: 700px;
overflow: hidden;
}
.session:hover {
border-color: var(--dim);
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
}
.session.stale { opacity: 0.5; }
.session.done-session {
border-left: 3px solid var(--done);
}
/* Needs attention sessions get a left accent border */
.session.attention-session {
border-left: 3px solid var(--needs-attention);
}
.session-header {
padding: 12px 16px;
border-bottom: 1px solid var(--selection);
flex-shrink: 0;
}
.session-header .session-top {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
}
.session-header-left {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.session-header-right {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.session-header .dismiss-btn {
font-size: 16px;
padding: 2px 8px;
}
.session-chat {
flex: 1;
overflow-y: auto;
padding: 12px 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.session-chat::-webkit-scrollbar {
width: 6px;
}
.session-chat::-webkit-scrollbar-track {
background: transparent;
}
.session-chat::-webkit-scrollbar-thumb {
background: var(--selection);
border-radius: 3px;
}
.chat-msg {
max-width: 90%;
padding: 8px 12px;
border-radius: 8px;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
.chat-msg.user {
align-self: flex-end;
background: var(--selection);
color: var(--bright);
}
.chat-msg.assistant {
align-self: flex-start;
background: #2a2a2a;
color: var(--text);
}
.chat-loading {
color: var(--muted);
font-size: 12px;
font-style: italic;
padding: 8px;
}
.session-footer {
padding: 12px 16px;
border-top: 1px solid var(--selection);
flex-shrink: 0;
}
.session-top {
display: flex;
align-items: center;
gap: 10px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
/* NEEDS ATTENTION pulses — this is what needs your attention */
.status-dot.needs_attention {
background: var(--needs-attention);
animation: pulse-attention 2s ease-in-out infinite;
}
.status-dot.active { background: var(--active); }
.status-dot.starting { background: var(--starting); }
.status-dot.done { background: var(--done); }
.status-dot.unknown { background: var(--dim); }
@keyframes pulse-attention {
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(208, 162, 21, 0.4); }
50% { opacity: 0.7; box-shadow: 0 0 0 6px rgba(208, 162, 21, 0); }
}
.session-info { flex: 1; min-width: 0; }
.session-label {
font-weight: 600;
color: var(--bright);
font-size: 13px;
}
.session-status {
font-size: 11px;
color: var(--dim);
margin-left: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.session-status.attention-text { color: var(--needs-attention); }
.session-meta {
font-size: 11px;
color: var(--dim);
margin-top: 2px;
display: flex;
gap: 12px;
}
.session-duration { font-variant-numeric: tabular-nums; }
.session-preview {
font-size: 12px;
color: var(--dim);
margin-top: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
/* Question display for needs_attention sessions */
.question-block {
margin-top: 10px;
padding: 10px 12px;
background: #282726;
border: 1px solid rgba(208, 162, 21, 0.3);
border-radius: 4px;
}
.question-header {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--needs-attention);
margin-bottom: 6px;
}
.question-text {
font-size: 13px;
color: var(--bright);
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
}
.question-options {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 4px;
}
.question-option {
font-size: 12px;
color: var(--fg);
padding: 6px 10px;
background: var(--surface);
border: 1px solid var(--selection);
border-radius: 3px;
}
.question-option-num {
color: var(--needs-attention);
font-weight: 600;
}
.question-option-label {
font-weight: 600;
color: var(--bright);
}
.question-option-desc {
color: var(--dim);
margin-left: 4px;
}
.session-events {
display: none;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--selection);
}
.session.expanded .session-events { display: block; }
.event-line {
font-size: 11px;
color: var(--dim);
padding: 2px 0;
display: flex;
gap: 10px;
}
.event-time {
color: var(--dim);
flex-shrink: 0;
font-variant-numeric: tabular-nums;
}
.event-name { color: var(--fg); }
.badge {
font-size: 10px;
color: var(--dim);
background: var(--selection);
padding: 1px 6px;
border-radius: 3px;
flex-shrink: 0;
}
.stale-label { margin-left: auto; }
.dismiss-btn {
font-size: 11px;
color: var(--dim);
background: none;
border: 1px solid var(--selection);
border-radius: 4px;
padding: 2px 8px;
cursor: pointer;
margin-left: auto;
flex-shrink: 0;
transition: color 0.15s, border-color 0.15s;
}
.dismiss-btn:hover {
color: var(--fg);
border-color: var(--dim);
}
.loading-events {
font-size: 11px;
color: var(--dim);
font-style: italic;
padding: 4px 0;
}
/* Response UI */
.response-actions {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--selection);
}
.response-options {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
margin-bottom: 8px;
}
.question-block .response-options {
margin-bottom: 0;
}
.question-queue-hint {
margin-top: 8px;
font-size: 11px;
color: var(--muted);
font-style: italic;
}
.response-option-btn {
font-family: inherit;
color: var(--bright);
background: var(--surface);
border: 1px solid var(--selection);
border-radius: 6px;
padding: 10px 12px;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
text-align: left;
display: flex;
flex-direction: column;
gap: 4px;
}
.response-option-btn:hover {
background: var(--selection);
border-color: var(--needs-attention);
}
.response-option-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.opt-label-row {
display: flex;
align-items: baseline;
gap: 6px;
}
.opt-num {
color: var(--needs-attention);
font-weight: 600;
font-size: 12px;
}
.opt-label {
font-size: 13px;
font-weight: 500;
}
.opt-desc {
font-size: 12px;
color: var(--muted);
line-height: 1.4;
padding-left: 20px;
}
.response-freeform {
display: flex;
gap: 6px;
}
.response-input {
flex: 1;
font-size: 12px;
font-family: inherit;
color: var(--bright);
background: var(--bg);
border: 1px solid var(--selection);
border-radius: 4px;
padding: 6px 10px;
outline: none;
}
.response-input:focus {
border-color: var(--needs-attention);
}
.response-input::placeholder {
color: var(--dim);
}
.response-send-btn {
font-size: 12px;
font-family: inherit;
color: var(--bg);
background: var(--needs-attention);
border: none;
border-radius: 4px;
padding: 6px 14px;
cursor: pointer;
font-weight: 600;
transition: opacity 0.15s;
}
.response-send-btn:hover {
opacity: 0.9;
}
.response-send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.response-status {
font-size: 11px;
color: var(--dim);
margin-top: 6px;
}
.response-status.error {
color: #D14D41;
}
.response-status.success {
color: var(--active);
}
/* Modal */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 100;
overflow-y: auto;
}
.modal-overlay.open {
display: flex;
justify-content: center;
padding: 40px 20px;
}
.modal {
background: var(--surface);
border: 1px solid var(--selection);
border-radius: 8px;
width: 90%;
max-height: 70vh;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 16px 20px;
border-bottom: 1px solid var(--selection);
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.modal-title {
font-size: 14px;
font-weight: 600;
color: var(--bright);
}
.modal-meta {
font-size: 11px;
color: var(--dim);
margin-top: 2px;
}
.modal-close {
background: none;
border: none;
color: var(--dim);
font-size: 20px;
cursor: pointer;
padding: 4px 8px;
line-height: 1;
}
.modal-close:hover {
color: var(--fg);
}
.modal-body {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
}
.modal-footer {
padding: 16px 20px;
border-top: 1px solid var(--selection);
flex-shrink: 0;
}
/* Conversation messages */
.conversation {
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
padding: 12px 16px;
border-radius: 6px;
}
.message.user {
background: var(--selection);
margin-left: 40px;
}
.message.assistant {
background: var(--bg);
border: 1px solid var(--selection);
margin-right: 40px;
}
.message-role {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
.message.user .message-role {
color: var(--active);
}
.message.assistant .message-role {
color: var(--done);
}
.message-content {
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
}
.message-time {
font-size: 10px;
color: var(--dim);
margin-top: 6px;
}
.conversation-loading {
text-align: center;
padding: 40px;
color: var(--dim);
}
/* Modal prompt input */
.modal-prompt {
display: flex;
gap: 8px;
}
.modal-prompt-input {
flex: 1;
font-size: 13px;
font-family: inherit;
color: var(--bright);
background: var(--bg);
border: 1px solid var(--selection);
border-radius: 4px;
padding: 10px 14px;
outline: none;
resize: none;
min-height: 44px;
max-height: 120px;
}
.modal-prompt-input:focus {
border-color: var(--done);
}
.modal-prompt-input::placeholder {
color: var(--dim);
}
.modal-prompt-send {
font-size: 13px;
font-family: inherit;
color: var(--bg);
background: var(--done);
border: none;
border-radius: 4px;
padding: 10px 18px;
cursor: pointer;
font-weight: 600;
align-self: flex-end;
}
.modal-prompt-send:hover {
opacity: 0.9;
}
.modal-prompt-send:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="header">
<span class="header-title">AGENT MISSION CONTROL</span>
<div class="status-counts" id="counts"></div>
</div>
<div class="main" id="main">
<div class="empty-state">
<div class="empty-state-title">No active sessions</div>
<div>Start a Claude Code session to see it here</div>
</div>
</div>
<!-- Session Modal -->
<div class="modal-overlay" id="modal-overlay" onclick="closeModal(event)">
<div class="modal" onclick="event.stopPropagation()">
<div class="modal-header">
<div>
<div class="modal-title" id="modal-title">Session</div>
<div class="modal-meta" id="modal-meta"></div>
</div>
<button class="modal-close" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body" id="modal-body">
<div class="conversation-loading">Loading conversation...</div>
</div>
<div class="modal-footer">
<div class="modal-prompt">
<textarea class="modal-prompt-input" id="modal-prompt-input" placeholder="Send a message..." rows="1"></textarea>
<button class="modal-prompt-send" id="modal-prompt-send" onclick="sendModalPrompt()">Send</button>
</div>
</div>
</div>
</div>
<script>
var POLL_MS = 3000;
var TICK_MS = 1000;
var STALE_MINUTES = 60;
var API_STATE = '/api/state';
var API_EVENTS = '/api/events/';
var API_DISMISS = '/api/dismiss/';
var API_RESPOND = '/api/respond/';
var API_CONVERSATION = '/api/conversation/';
var state = { sessions: [], server_time: '' };
var modalSession = null; // Currently open modal session
var expandedSessions = new Set();
var eventCache = {};
var conversationCache = {}; // Cache conversations for cards: { sessionId: [messages] }
var inputState = {}; // Preserve input values across renders: { sessionId: inputValue }
var focusedInputSession = null; // Track which session's input has focus
var isTyping = false; // Flag to prevent render while typing
// -- Favicon --
var faviconCanvas = document.createElement('canvas');
faviconCanvas.width = 32;
faviconCanvas.height = 32;
var faviconCtx = faviconCanvas.getContext('2d');
var faviconLink = document.querySelector('link[rel="icon"]');
if (!faviconLink) {
faviconLink = document.createElement('link');
faviconLink.rel = 'icon';
document.head.appendChild(faviconLink);
}
function updateFavicon(attentionCount, doneCount) {
var ctx = faviconCtx;
ctx.clearRect(0, 0, 32, 32);
ctx.beginPath();
ctx.arc(16, 16, 14, 0, Math.PI * 2);
if (attentionCount > 0) {
ctx.fillStyle = '#D0A215';
} else if (doneCount > 0) {
ctx.fillStyle = '#879A39';
} else {
ctx.fillStyle = '#575653';
}
ctx.fill();
var total = attentionCount + doneCount;
if (total > 0) {
ctx.fillStyle = '#FFFCF0';
ctx.font = 'bold 18px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(total > 9 ? '9+' : String(total), 16, 17);
}
faviconLink.href = faviconCanvas.toDataURL('image/png');
}
// -- Helpers --
function formatDuration(isoStart) {
if (!isoStart) return '';
var ms = Date.now() - new Date(isoStart).getTime();
var s = Math.floor(ms / 1000);
if (s < 60) return s + 's';
var m = Math.floor(s / 60);
if (m < 60) return m + 'm ' + (s % 60) + 's';
var h = Math.floor(m / 60);
return h + 'h ' + (m % 60) + 'm';
}
function formatEventTime(iso) {
if (!iso) return '';
return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
function isStale(session) {
if (session.status !== 'active') return false;
if (!session.last_event_at) return false;
return (Date.now() - new Date(session.last_event_at).getTime()) > STALE_MINUTES * 60 * 1000;
}
// -- DOM building --
function el(tag, attrs, children) {
var e = document.createElement(tag);
if (attrs) {
for (var _i = 0, _a = Object.entries(attrs); _i < _a.length; _i++) {
var k = _a[_i][0], v = _a[_i][1];
if (k === 'className') e.className = v;
else if (k === 'textContent') e.textContent = v;
else if (k === 'title') e.title = v;
else if (k.startsWith('data-')) e.setAttribute(k, v);
else if (k === 'onclick') e.onclick = v;
else e.setAttribute(k, v);
}
}
if (children) {
for (var _j = 0; _j < children.length; _j++) {
var c = children[_j];
if (typeof c === 'string') e.appendChild(document.createTextNode(c));
else if (c) e.appendChild(c);
}
}
return e;
}
function buildCountsBadge(color, count, label) {
return el('div', { className: 'count-badge' }, [
el('span', { className: 'count-dot', style: 'background:' + color }),
el('span', { className: 'count-num', textContent: String(count) }),
' ' + label,
]);
}
function buildEventLine(evt) {
return el('div', { className: 'event-line' }, [
el('span', { className: 'event-time', textContent: formatEventTime(evt.at) }),
el('span', { className: 'event-name', textContent: evt.event }),
]);
}
function buildQuestionBlock(questions, sessionId) {
if (!questions || questions.length === 0) return null;
// Only show the FIRST question - they're sequential, not parallel
var q = questions[0];
var statusEl = el('div', { className: 'response-status' });
var optCount = q.options ? q.options.length : 0;
var qChildren = [];
// Header badge
if (q.header) {
qChildren.push(el('div', { className: 'question-header', textContent: q.header }));
}
// Question text
qChildren.push(el('div', { className: 'question-text', textContent: q.question }));
// This question's options
if (q.options && q.options.length > 0) {
var optBtns = [];
for (var j = 0; j < q.options.length; j++) {
var opt = q.options[j];
var num = j + 1;
(function(optNum, optData) {
var btnChildren = [
el('div', { className: 'opt-label-row' }, [
el('span', { className: 'opt-num', textContent: optNum + '.' }),
el('span', { className: 'opt-label', textContent: optData.label })
])
];
if (optData.description) {
btnChildren.push(el('div', { className: 'opt-desc', textContent: optData.description }));
}
var btn = el('button', {
className: 'response-option-btn',
onclick: function(e) {
e.stopPropagation();
respondToSession(sessionId, String(optNum), statusEl);
}
}, btnChildren);
optBtns.push(btn);
})(num, opt);
}
qChildren.push(el('div', { className: 'response-options' }, optBtns));
}
// Freeform input for this question
var input = el('input', {
className: 'response-input',
type: 'text',
placeholder: optCount > 0 ? 'Or type a custom response...' : 'Type your response...'
});
if (inputState[sessionId]) {
input.value = inputState[sessionId];
}
input.setAttribute('data-session', sessionId);
input.onclick = function(e) { e.stopPropagation(); };
input.onfocus = function() {
focusedInputSession = sessionId;
isTyping = true;
};
input.onblur = function() {
if (focusedInputSession === sessionId) {
focusedInputSession = null;
}
isTyping = false;
};
input.oninput = function() {
inputState[sessionId] = input.value;
isTyping = true;
};
input.onkeydown = function(e) {
if (e.key === 'Enter' && input.value.trim()) {
e.stopPropagation();
delete inputState[sessionId];
focusedInputSession = null;
isTyping = false;
respondToSession(sessionId, input.value, statusEl, true, optCount);
}
};
var sendBtn = el('button', {
className: 'response-send-btn',
textContent: 'Send',
onclick: function(e) {
e.stopPropagation();
if (input.value.trim()) {
delete inputState[sessionId];
focusedInputSession = null;
isTyping = false;
respondToSession(sessionId, input.value, statusEl, true, optCount);
}
}
});
qChildren.push(el('div', { className: 'response-freeform' }, [input, sendBtn]));
qChildren.push(statusEl);
// Show remaining question count if there are more
if (questions.length > 1) {
qChildren.push(el('div', {
className: 'question-queue-hint',
textContent: '+ ' + (questions.length - 1) + ' more question' + (questions.length > 2 ? 's' : '') + ' after this'
}));
}
return el('div', { className: 'question-block' }, qChildren);
}
function buildSessionCard(s) {
var stale = isStale(s);
var isDone = s.status === 'done';
var isAttention = s.status === 'needs_attention';
var cls = ['session'];
if (stale) cls.push('stale');
if (isDone) cls.push('done-session');
if (isAttention) cls.push('attention-session');
// -- HEADER --
var statusText = s.status;
if (isAttention) statusText = 'needs attention';
if (isDone) statusText = 'done';
if (s.status === 'active') statusText = 'active';
if (s.status === 'starting') statusText = 'starting';
var headerLeft = [
el('div', { className: 'status-dot ' + s.status }),
el('span', { className: 'session-label', textContent: s.project }),
el('span', { className: 'session-status' + (isAttention ? ' attention-text' : ''), textContent: statusText }),
];
var headerRight = [
el('span', {
className: 'session-duration',
'data-started': s.started_at,
textContent: formatDuration(s.started_at)
})
];
if (isDone) {
headerRight.push(el('button', {
className: 'dismiss-btn',
textContent: '×',
title: 'Dismiss session',
onclick: function(e) { e.stopPropagation(); dismissSession(s.session_id); }
}));
}
var header = el('div', { className: 'session-header' }, [
el('div', { className: 'session-top' }, [
el('div', { className: 'session-header-left' }, headerLeft),
el('div', { className: 'session-header-right' }, headerRight),
])
]);
// -- CHAT AREA --
var chatEl = el('div', { className: 'session-chat', id: 'chat-' + s.session_id });
// Use cached conversation if available, otherwise load async
if (conversationCache[s.session_id]) {
renderCardChat(chatEl, conversationCache[s.session_id]);
} else {
chatEl.appendChild(el('div', { className: 'chat-loading', textContent: 'Loading conversation...' }));
loadCardConversation(s.session_id, s.project_dir);
}
// -- FOOTER (question block for attention sessions) --
var footer = null;
if (isAttention && s.pending_questions) {
var qBlock = buildQuestionBlock(s.pending_questions, s.session_id);
if (qBlock) {
footer = el('div', { className: 'session-footer' }, [qBlock]);
}
}
var cardChildren = [header, chatEl];
if (footer) cardChildren.push(footer);
return el('div', {
className: cls.join(' '),
'data-sid': s.session_id,
onclick: function() { openModal(s); }
}, cardChildren);
}
// -- Render --
function render(force) {
// Skip render if user is typing (unless forced)
if (!force && isTyping) {
return;
}
var sessions = state.sessions || [];
var attention = sessions.filter(function(s) { return s.status === 'needs_attention'; });
var done = sessions.filter(function(s) { return s.status === 'done'; });
var active = sessions.filter(function(s) { return s.status === 'active' || s.status === 'starting'; });
// Clean up inputState for sessions no longer needing attention
var attentionIds = new Set(attention.map(function(s) { return s.session_id; }));
Object.keys(inputState).forEach(function(sid) {
if (!attentionIds.has(sid)) {
delete inputState[sid];
if (focusedInputSession === sid) focusedInputSession = null;
}
});
// Tab title — attention first (needs response), then done, then active
var parts = [];
if (attention.length > 0) parts.push(attention.length + ' attention');
if (done.length > 0) parts.push(done.length + ' done');
if (active.length > 0) parts.push(active.length + ' active');
document.title = parts.length ? parts.join(', ') + ' - AMC' : 'AMC';
updateFavicon(attention.length, done.length);
// Header counts
var countsEl = document.getElementById('counts');
countsEl.replaceChildren(
buildCountsBadge('var(--needs-attention)', attention.length, 'attention'),
buildCountsBadge('var(--done)', done.length, 'done'),
buildCountsBadge('var(--active)', active.length, 'active')
);
var mainEl = document.getElementById('main');
if (sessions.length === 0) {
mainEl.replaceChildren(
el('div', { className: 'empty-state' }, [
el('div', { className: 'empty-state-title', textContent: 'No active sessions' }),
el('div', { textContent: 'Start a Claude Code session to see it here' }),
])
);
return;
}
var fragment = document.createDocumentFragment();
function byRecent(a, b) {
return (b.last_event_at || '').localeCompare(a.last_event_at || '');
}
// Section 1: Needs Attention
if (attention.length > 0) {
attention.sort(byRecent);
var header = el('div', { className: 'status-group-header needs-attention' }, [
el('span', { className: 'count-dot', style: 'background:var(--needs-attention)' }),
'NEEDS ATTENTION (' + attention.length + ')',
]);
var grid = el('div', { className: 'sessions-grid' });
for (var i = 0; i < attention.length; i++) grid.appendChild(buildSessionCard(attention[i]));
var group = el('div', { className: 'status-group' }, [header, grid]);
fragment.appendChild(group);
}
// Section 2: Done
if (done.length > 0) {
done.sort(byRecent);
var header2 = el('div', { className: 'status-group-header done' }, [
el('span', { className: 'count-dot', style: 'background:var(--done)' }),
'DONE (' + done.length + ')',
]);
var grid2 = el('div', { className: 'sessions-grid' });
for (var j = 0; j < done.length; j++) grid2.appendChild(buildSessionCard(done[j]));
var group2 = el('div', { className: 'status-group' }, [header2, grid2]);
fragment.appendChild(group2);
}
// Section 3: Active
if (active.length > 0) {
active.sort(byRecent);
var header3 = el('div', { className: 'status-group-header active-header' }, [
el('span', { className: 'count-dot', style: 'background:var(--active)' }),
'ACTIVE (' + active.length + ')',
]);
var grid3 = el('div', { className: 'sessions-grid' });
for (var k = 0; k < active.length; k++) grid3.appendChild(buildSessionCard(active[k]));
var group3 = el('div', { className: 'status-group' }, [header3, grid3]);
fragment.appendChild(group3);
}
mainEl.replaceChildren(fragment);
// Restore focus to input if it was focused before render
if (focusedInputSession) {
var inputToFocus = document.querySelector('.response-input[data-session="' + focusedInputSession + '"]');
if (inputToFocus) {
inputToFocus.focus();
// Move cursor to end
var len = inputToFocus.value.length;
inputToFocus.setSelectionRange(len, len);
}
}
}
// -- Actions --
function toggleSession(sid) {
if (expandedSessions.has(sid)) {
expandedSessions.delete(sid);
} else {
expandedSessions.add(sid);
if (!eventCache[sid]) fetchEvents(sid);
}
render();
}
async function dismissSession(sid) {
try {
await fetch(API_DISMISS + encodeURIComponent(sid), { method: 'POST' });
state.sessions = (state.sessions || []).filter(function(s) { return s.session_id !== sid; });
expandedSessions.delete(sid);
delete eventCache[sid];
render();
} catch (e) {}
}
async function respondToSession(sid, text, statusEl, freeform, optionCount) {
if (!text.trim()) return;
// Show sending state
if (statusEl) {
statusEl.textContent = 'Sending...';
statusEl.className = 'response-status';
}
// Disable buttons during send
var card = document.querySelector('[data-sid="' + sid + '"]');
if (card) {
card.querySelectorAll('.response-option-btn, .response-send-btn').forEach(function(btn) {
btn.disabled = true;
});
}
try {
var payload = { text: text };
if (freeform) {
payload.freeform = true;
payload.optionCount = optionCount || 0;
}
var res = await fetch(API_RESPOND + encodeURIComponent(sid), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
var data = await res.json();
if (data.ok) {
if (statusEl) {
statusEl.textContent = 'Sent!';
statusEl.className = 'response-status success';
}
// Trigger a poll to get updated state
setTimeout(poll, 500);
} else {
throw new Error(data.error || 'Unknown error');
}
} catch (e) {
if (statusEl) {
statusEl.textContent = 'Error: ' + e.message;
statusEl.className = 'response-status error';
}
// Re-enable buttons on error
if (card) {
card.querySelectorAll('.response-option-btn, .response-send-btn').forEach(function(btn) {
btn.disabled = false;
});
}
}
}
async function fetchEvents(sid) {
try {
var res = await fetch(API_EVENTS + encodeURIComponent(sid));
if (res.ok) {
var data = await res.json();
eventCache[sid] = data.events || [];
render();
}
} catch (e) {}
}
async function loadCardConversation(sessionId, projectDir) {
if (conversationCache[sessionId]) return; // Already loaded
try {
var url = API_CONVERSATION + encodeURIComponent(sessionId);
if (projectDir) {
url += '?project_dir=' + encodeURIComponent(projectDir);
}
var res = await fetch(url);
if (res.ok) {
var data = await res.json();
conversationCache[sessionId] = data.messages || [];
// Update the chat area in the card
var chatEl = document.getElementById('chat-' + sessionId);
if (chatEl) {
renderCardChat(chatEl, conversationCache[sessionId]);
}
}
} catch (e) {}
}
function renderCardChat(chatEl, messages) {
chatEl.innerHTML = '';
if (messages.length === 0) {
chatEl.appendChild(el('div', { className: 'chat-loading', textContent: 'No messages yet' }));
return;
}
// Show last N messages (most recent at bottom)
var recent = messages.slice(-20);
for (var i = 0; i < recent.length; i++) {
var msg = recent[i];
var msgEl = el('div', { className: 'chat-msg ' + msg.role });
// Truncate long messages
var text = msg.content;
if (text.length > 500) {
text = text.substring(0, 500) + '...';
}
msgEl.textContent = text;
chatEl.appendChild(msgEl);
}
// Scroll to bottom
chatEl.scrollTop = chatEl.scrollHeight;
}
// -- Modal --
function openModal(session) {
modalSession = session;
var overlay = document.getElementById('modal-overlay');
var title = document.getElementById('modal-title');
var meta = document.getElementById('modal-meta');
var body = document.getElementById('modal-body');
var input = document.getElementById('modal-prompt-input');
title.textContent = session.project;
meta.textContent = session.status + ' • ' + formatDuration(session.started_at);
body.replaceChildren(el('div', { className: 'conversation-loading', textContent: 'Loading conversation...' }));
input.value = '';
overlay.classList.add('open');
document.body.style.overflow = 'hidden';
// Fetch conversation
fetchConversation(session.session_id, session.project_dir);
}
function closeModal(event) {
if (event && event.target !== event.currentTarget) return;
var overlay = document.getElementById('modal-overlay');
overlay.classList.remove('open');
document.body.style.overflow = '';
modalSession = null;
}
async function fetchConversation(sessionId, projectDir) {
var body = document.getElementById('modal-body');
try {
var res = await fetch(API_CONVERSATION + encodeURIComponent(sessionId) + '?project_dir=' + encodeURIComponent(projectDir));
if (!res.ok) throw new Error('Failed to load');
var data = await res.json();
renderConversation(data.messages || []);
} catch (e) {
body.replaceChildren(el('div', { className: 'conversation-loading', textContent: 'Could not load conversation history' }));
}
}
function renderConversation(messages) {
var body = document.getElementById('modal-body');
if (messages.length === 0) {
body.replaceChildren(el('div', { className: 'conversation-loading', textContent: 'No messages yet' }));
return;
}
var conv = el('div', { className: 'conversation' });
for (var i = 0; i < messages.length; i++) {
var msg = messages[i];
var msgEl = el('div', { className: 'message ' + msg.role }, [
el('div', { className: 'message-role', textContent: msg.role === 'user' ? 'You' : 'Claude' }),
el('div', { className: 'message-content', textContent: msg.content }),
el('div', { className: 'message-time', textContent: formatEventTime(msg.timestamp) })
]);
conv.appendChild(msgEl);
}
body.replaceChildren(conv);
body.scrollTop = body.scrollHeight;
}
async function sendModalPrompt() {
if (!modalSession) return;
var input = document.getElementById('modal-prompt-input');
var btn = document.getElementById('modal-prompt-send');
var text = input.value.trim();
if (!text) return;
btn.disabled = true;
btn.textContent = 'Sending...';
try {
var res = await fetch(API_RESPOND + encodeURIComponent(modalSession.session_id), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text })
});
var data = await res.json();
if (data.ok) {
input.value = '';
btn.textContent = 'Sent!';
setTimeout(function() {
btn.textContent = 'Send';
btn.disabled = false;
}, 1000);
} else {
throw new Error(data.error || 'Send failed');
}
} catch (e) {
btn.textContent = 'Error';
setTimeout(function() {
btn.textContent = 'Send';
btn.disabled = false;
}, 2000);
}
}
// Handle Escape key to close modal
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && modalSession) {
closeModal();
}
});
// Handle Enter in modal prompt
document.getElementById('modal-prompt-input').addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendModalPrompt();
}
});
// -- Polling --
async function poll() {
try {
var res = await fetch(API_STATE);
if (res.ok) {
state = await res.json();
expandedSessions.forEach(function(sid) { fetchEvents(sid); });
render();
}
} catch (e) {}
}
// -- Duration ticker --
function tickTimers() {
document.querySelectorAll('.session-duration[data-started]').forEach(function(node) {
node.textContent = formatDuration(node.getAttribute('data-started'));
});
}
// -- Init --
poll();
setInterval(poll, POLL_MS);
setInterval(tickTimers, TICK_MS);
</script>
</body>
</html>