Files
KimiFleet/Dashboard.html
T

113 lines
25 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>KimiFleet</title>
<style>
:root{color-scheme:dark;--bg:#090d16;--surface:#101622;--surface2:#151d2c;--surface3:#1b2537;--line:#263247;--line2:#34435d;--text:#edf2fb;--muted:#8b9ab2;--subtle:#66748b;--blue:#6ea8fe;--cyan:#65d6d0;--green:#52d39a;--yellow:#f4bf64;--red:#ff737d;--violet:#a78bfa;--shadow:0 18px 55px #0005}
*{box-sizing:border-box}html,body{height:100%}body{margin:0;background:radial-gradient(900px 500px at 50% -200px,#213353 0,#0d1320 50%,var(--bg) 100%);color:var(--text);font:14px Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;overflow-x:hidden}button,input,textarea{font:inherit}button{color:inherit}.hidden{display:none!important}
.topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:18px;min-height:72px;padding:14px 26px;background:#0b101bd9;border-bottom:1px solid var(--line);backdrop-filter:blur(18px)}.brand{display:flex;align-items:center;gap:11px;min-width:210px}.brand-mark{display:grid;place-items:center;width:34px;height:34px;border:1px solid #41618f;border-radius:10px;background:linear-gradient(145deg,#223c63,#111a2a);color:#bcd6ff;font-weight:900;box-shadow:inset 0 1px #ffffff14,0 7px 22px #0005}.brand h1{margin:0;font-size:17px;letter-spacing:-.3px}.brand small{display:block;margin-top:2px;color:var(--muted);font-size:11px}.fleet-stats{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:12px}.stat{display:flex;align-items:center;gap:6px;padding:6px 9px;border:1px solid var(--line);border-radius:999px;background:#101725}.stat-dot{width:6px;height:6px;border-radius:50%;background:var(--green);box-shadow:0 0 9px var(--green)}.toolbar{display:flex;align-items:center;gap:8px;margin-left:auto}.search-wrap{position:relative}.search-wrap svg{position:absolute;left:11px;top:10px;color:var(--subtle)}.search{width:min(28vw,320px);height:36px;padding:0 12px 0 34px;border:1px solid var(--line);border-radius:9px;outline:none;background:#0e1522;color:var(--text)}.search:focus{border-color:#4c6f9f;box-shadow:0 0 0 3px #4e7fbd1d}.icon-btn,.primary-btn,.quiet-btn{height:36px;border:1px solid var(--line);border-radius:9px;background:#131b29;cursor:pointer;transition:.16s ease}.icon-btn{display:grid;place-items:center;width:36px;padding:0}.icon-btn:hover,.quiet-btn:hover{border-color:var(--line2);background:#1a2434}.primary-btn{display:flex;align-items:center;gap:7px;padding:0 13px;border-color:#456fa8;background:#28598f;color:white;font-weight:700}.primary-btn:hover{background:#326ba7}.quiet-btn{padding:0 11px;color:#c6d2e5}
.fleet{padding:20px 26px 32px}.agent-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(520px,100%),1fr));gap:16px;align-items:start}.agent-grid.dense{grid-template-columns:repeat(auto-fit,minmax(min(400px,100%),1fr))}.agent-grid.focused{display:block;max-width:1180px;margin:0 auto}.empty-fleet{display:grid;place-items:center;min-height:55vh;border:1px dashed var(--line2);border-radius:18px;color:var(--muted);text-align:center}.empty-fleet strong{display:block;margin-bottom:6px;color:var(--text);font-size:17px}
.agent-card{display:flex;flex-direction:column;min-height:650px;max-height:calc(100vh - 112px);overflow:hidden;border:1px solid var(--line);border-radius:15px;background:linear-gradient(180deg,#121a28,#0e1521);box-shadow:var(--shadow)}.dense .agent-card{min-height:520px}.focused .agent-card{height:calc(100vh - 112px);max-height:none}.agent-head{padding:13px 14px 11px;border-bottom:1px solid var(--line);background:#141c2a}.head-row{display:flex;align-items:center;gap:9px}.agent-avatar{display:grid;place-items:center;width:31px;height:31px;border:1px solid #36527a;border-radius:9px;background:#172943;color:#9ec8ff;font-weight:900}.agent-identity{min-width:0}.agent-name{font-weight:800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.agent-meta{margin-top:2px;color:var(--muted);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.agent-actions{display:flex;gap:5px;margin-left:auto}.mini-btn{display:grid;place-items:center;width:29px;height:29px;padding:0;border:1px solid transparent;border-radius:7px;background:transparent;color:var(--muted);cursor:pointer}.mini-btn:hover{border-color:var(--line);background:var(--surface3);color:var(--text)}.state-pill{display:flex;align-items:center;gap:6px;margin-left:5px;padding:4px 8px;border:1px solid var(--line);border-radius:999px;color:var(--muted);font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.06em}.state-pill i{width:6px;height:6px;border-radius:50%;background:var(--muted)}.state-pill.working{color:var(--yellow)}.state-pill.working i{background:var(--yellow);box-shadow:0 0 8px var(--yellow)}.state-pill.ready{color:var(--green)}.state-pill.ready i{background:var(--green)}.state-pill.error{color:var(--red)}.state-pill.error i{background:var(--red)}
.activity-line{display:flex;align-items:center;gap:8px;margin-top:10px;color:#cbd8ea;font-size:12px}.activity-line .pulse{width:6px;height:6px;border-radius:50%;background:var(--yellow);box-shadow:0 0 9px var(--yellow)}.activity-line.ready .pulse{background:var(--green);box-shadow:0 0 9px var(--green)}.activity-line.error .pulse,.activity-line.stopped .pulse{background:var(--red);box-shadow:0 0 9px var(--red)}.activity-text{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.queue{margin-left:auto;color:var(--violet);font-size:10px}.context-row{display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center;margin-top:10px}.context-track{height:3px;overflow:hidden;border-radius:4px;background:#253044}.context-fill{height:100%;border-radius:4px;background:var(--cyan);transition:width .3s}.context-fill.warn{background:var(--yellow)}.context-fill.danger{background:var(--red)}.context-label{color:var(--muted);font:10px ui-monospace,SFMono-Regular,Consolas,monospace}
.chat{flex:1;min-height:0;overflow:auto;padding:15px 15px 20px;scrollbar-color:#35445d transparent;scrollbar-width:thin}.chat-empty{display:grid;place-items:center;height:100%;color:var(--subtle);text-align:center}.chat-empty .orb{display:grid;place-items:center;width:42px;height:42px;margin:0 auto 10px;border:1px solid #315078;border-radius:13px;background:#16243a;color:#8ab9f4}.message{margin:0 0 14px}.message-label{margin:0 0 5px;color:var(--muted);font-size:10px;font-weight:750;text-transform:uppercase;letter-spacing:.06em}.message-body{max-width:92%;padding:10px 12px;border:1px solid var(--line);border-radius:10px;background:#151e2d;color:#dce5f3;line-height:1.48;white-space:pre-wrap;overflow-wrap:anywhere}.message.user{display:flex;flex-direction:column;align-items:flex-end}.message.user .message-label{text-align:right}.message.user .message-body{border-color:#315989;background:#173254;color:#eef6ff}.message.assistant .message-body{border-color:#29384e;background:#131b28}.message.error .message-body{border-color:#6a3540;background:#321b24;color:#ffc6cb}.system-message{margin:11px auto;padding:5px 9px;width:max-content;max-width:90%;border:1px solid var(--line);border-radius:999px;background:#121a27;color:var(--muted);font-size:10px;text-align:center}.thought{margin:0 0 12px;border-left:2px solid #556987;padding-left:10px;color:var(--muted)}.thought summary{cursor:pointer;font-size:11px;font-weight:700;user-select:none}.thought-text{margin-top:7px;max-height:180px;overflow:auto;color:#8fa0b8;font-size:11px;line-height:1.45;white-space:pre-wrap}.tool-call{margin:0 0 9px;padding:8px 10px;border:1px solid var(--line);border-radius:9px;background:#101824}.tool-main{display:flex;align-items:center;gap:8px}.tool-icon{display:grid;place-items:center;width:22px;height:22px;border-radius:6px;background:#1d2a3e;color:#9bbbe8}.tool-title{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#c8d5e7;font:11px ui-monospace,SFMono-Regular,Consolas,monospace}.tool-status{color:var(--muted);font-size:10px}.tool-status.in_progress{color:var(--yellow)}.tool-status.completed{color:var(--green)}.tool-status.failed{color:var(--red)}.tool-call details{margin-top:7px}.tool-call summary{color:var(--subtle);cursor:pointer;font-size:10px}.tool-detail{max-height:180px;margin:7px 0 0;padding:8px;overflow:auto;border-radius:6px;background:#0a101a;color:#8fa4c2;font:10px/1.45 ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap}
.composer{padding:11px;border-top:1px solid var(--line);background:#111925}.composer-box{display:flex;align-items:flex-end;gap:8px;padding:8px 8px 8px 11px;border:1px solid var(--line2);border-radius:11px;background:#0c121d;transition:.15s}.composer-box:focus-within{border-color:#4d709e;box-shadow:0 0 0 3px #416caa18}.composer textarea{min-height:24px;max-height:110px;flex:1;resize:none;border:0;outline:0;background:transparent;color:var(--text);line-height:1.45}.composer textarea::placeholder{color:#65748a}.send-btn{display:grid;place-items:center;width:30px;height:30px;flex:none;border:0;border-radius:8px;background:#2c66a4;color:white;cursor:pointer}.send-btn:hover{background:#3979bd}.send-btn:disabled{cursor:not-allowed;opacity:.4}.composer-foot{display:flex;align-items:center;gap:6px;margin-top:7px;color:var(--subtle);font-size:9px}.scope-chip{padding:2px 5px;border-radius:4px;background:#192438;color:#8fa4c1;font:9px ui-monospace,SFMono-Regular,Consolas,monospace}.composer-foot .spacer{flex:1}
dialog{width:min(520px,calc(100vw - 30px));padding:0;border:1px solid var(--line2);border-radius:15px;background:#111826;color:var(--text);box-shadow:0 35px 100px #000b}dialog::backdrop{background:#050812b8;backdrop-filter:blur(5px)}.dialog-head{display:flex;align-items:center;padding:17px 18px;border-bottom:1px solid var(--line)}.dialog-head h2{margin:0;font-size:16px}.dialog-body{display:grid;gap:13px;padding:18px}.field label{display:block;margin-bottom:6px;color:var(--muted);font-size:11px;font-weight:700}.field input,.field textarea{width:100%;padding:9px 10px;border:1px solid var(--line);border-radius:8px;outline:none;background:#0b111c;color:var(--text)}.field textarea{min-height:76px;resize:vertical}.field input:focus,.field textarea:focus{border-color:#5278a8}.field-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}.dialog-foot{display:flex;justify-content:flex-end;gap:8px;padding:13px 18px;border-top:1px solid var(--line)}.toast-stack{position:fixed;right:18px;bottom:18px;z-index:50;display:grid;gap:8px}.toast{max-width:380px;padding:10px 12px;border:1px solid var(--line2);border-radius:9px;background:#172131;box-shadow:0 12px 35px #0008;color:#dce5f2;font-size:12px}.toast.error{border-color:#7b3b48;color:#ffc6cc}
@media(max-width:800px){.topbar{flex-wrap:wrap;padding:12px 14px}.brand{min-width:auto}.fleet-stats{order:3;width:100%}.toolbar{gap:6px}.search{width:180px}.fleet{padding:13px}.agent-card{max-height:none;height:calc(100vh - 150px);min-height:560px}.field-row{grid-template-columns:1fr}}
</style>
</head>
<body>
<header class="topbar">
<div class="brand"><div class="brand-mark">K</div><div><h1>KimiFleet</h1><small>K3 context cockpit</small></div></div>
<div id="fleetStats" class="fleet-stats"></div>
<div class="toolbar">
<label class="search-wrap"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg><input id="search" class="search" placeholder="Search agents"></label>
<button id="densityBtn" class="icon-btn" title="Toggle compact grid">▦</button>
<button id="newAgentBtn" class="primary-btn"><span>+</span> New agent</button>
</div>
</header>
<main class="fleet"><section id="agentGrid" class="agent-grid"></section></main>
<dialog id="agentDialog">
<form id="agentForm" method="dialog">
<div class="dialog-head"><h2>Start Kimi K3 instance</h2><button type="button" class="mini-btn" data-close-dialog style="margin-left:auto">✕</button></div>
<div class="dialog-body">
<div class="field-row"><div class="field"><label for="agentName">Name</label><input id="agentName" name="name" required pattern="[A-Za-z0-9_-]+" placeholder="renderer-2"></div><div class="field"><label for="agentRole">Role</label><input id="agentRole" name="role" placeholder="reviewer"></div></div>
<div class="field"><label for="agentWorkspace">Workspace</label><input id="agentWorkspace" name="workspace" required placeholder="/path/to/project"></div>
<div class="field"><label for="agentScopes">Exclusive scopes · one per line</label><textarea id="agentScopes" name="scopes" placeholder="src/Engine.AI&#10;tests/Engine.Tests"></textarea></div>
</div>
<div class="dialog-foot"><button type="button" class="quiet-btn" data-close-dialog>Cancel</button><button type="submit" class="primary-btn">Start instance</button></div>
</form>
</dialog>
<div id="toasts" class="toast-stack"></div>
<script>
const grid=document.querySelector('#agentGrid'),stats=document.querySelector('#fleetStats'),search=document.querySelector('#search'),dialog=document.querySelector('#agentDialog'),form=document.querySelector('#agentForm'),toasts=document.querySelector('#toasts');
const state={agents:[],chats:new Map(),drafts:new Map(),focused:null,dense:localStorage.getItem('kimiFleetDensity')==='dense',refreshing:false,pendingAgents:new Set(),refreshTimer:null};
const esc=value=>String(value??'').replace(/[&<>"']/g,char=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]));
const fmtTokens=value=>value>=1000?`${(value/1000).toFixed(value>=100000?0:1)}k`:String(value||0);
const icon=name=>({read:'◇',execute:'›_',edit:'✎',search:'⌕',delete:'−',fetch:'↓',think:'◌',move:'↝'}[name]||'·');
function toast(message,error=false){const node=document.createElement('div');node.className=`toast${error?' error':''}`;node.textContent=message;toasts.append(node);setTimeout(()=>node.remove(),4200)}
async function api(path,options={}){const response=await fetch(path,{headers:{'Content-Type':'application/json',...(options.headers||{})},...options});let body=null;try{body=await response.json()}catch{}if(!response.ok)throw new Error(body?.error||`${response.status} ${response.statusText}`);return body}
function rememberEditor(){const active=document.activeElement;if(!active?.dataset?.composer)return null;return {name:active.dataset.composer,start:active.selectionStart,end:active.selectionEnd}}
function restoreEditor(saved){if(!saved)return;const editor=document.querySelector(`[data-composer="${CSS.escape(saved.name)}"]`);if(editor){editor.focus();editor.setSelectionRange(saved.start,saved.end)}}
function nearBottom(element){return element.scrollHeight-element.scrollTop-element.clientHeight<90}
function render(){
const editor=rememberEditor(),scrolls=new Map([...document.querySelectorAll('.chat')].map(node=>[node.dataset.chat,{top:node.scrollTop,bottom:nearBottom(node)}])),openEntries=new Set([...document.querySelectorAll('details[open][data-entry]')].map(node=>node.dataset.entry));
const query=search.value.trim().toLowerCase();let agents=state.agents.filter(agent=>!query||`${agent.name} ${agent.role} ${agent.workspace} ${(agent.scopes||[]).join(' ')}`.toLowerCase().includes(query));
agents.sort((a,b)=>Number(b.state==='working')-Number(a.state==='working')||a.name.localeCompare(b.name));if(state.focused)agents=agents.filter(agent=>agent.name===state.focused);
grid.className=`agent-grid${state.dense?' dense':''}${state.focused?' focused':''}`;
stats.innerHTML=`<span class="stat"><span class="stat-dot"></span>${state.agents.length} instance${state.agents.length===1?'':'s'}</span><span class="stat">${state.agents.filter(a=>a.state==='working').length} working</span>`;
grid.innerHTML=agents.length?agents.map(card).join(''):`<div class="empty-fleet"><div><strong>${state.agents.length?'No matching agents':'No Kimi instances yet'}</strong>${state.agents.length?'Try another search.':'Start a scoped K3 agent to begin.'}</div></div>`;
bindAutoResize();restoreEditor(editor);document.querySelectorAll('details[data-entry]').forEach(node=>node.open=openEntries.has(node.dataset.entry));
document.querySelectorAll('.chat').forEach(node=>{const saved=scrolls.get(node.dataset.chat);if(saved?.bottom||!saved)node.scrollTop=node.scrollHeight;else node.scrollTop=saved.top});
}
function card(agent){
const chat=state.chats.get(agent.name)||[],percent=Math.min(100,Number(agent.contextPercent)||0),contextClass=percent>=90?'danger':percent>=70?'warn':'',draft=state.drafts.get(agent.name)||'';
return `<article class="agent-card" data-agent="${esc(agent.name)}">
<header class="agent-head"><div class="head-row"><div class="agent-avatar">K</div><div class="agent-identity"><div class="agent-name">${esc(agent.name)}</div><div class="agent-meta">${esc(agent.role)} · ${esc(agent.workspace)}</div></div><span class="state-pill ${esc(agent.state)}"><i></i>${esc(agent.state)}</span><div class="agent-actions">${agent.state==='working'?`<button class="mini-btn" data-action="cancel" data-agent="${esc(agent.name)}" title="Cancel current task">■</button>`:''}<button class="mini-btn" data-action="compact" data-agent="${esc(agent.name)}" title="Compact context">↯</button><button class="mini-btn" data-action="reset" data-agent="${esc(agent.name)}" title="Fresh context">↺</button><button class="mini-btn" data-action="focus" data-agent="${esc(agent.name)}" title="${state.focused===agent.name?'Exit focus':'Focus'}">${state.focused===agent.name?'⊙':'⛶'}</button><button class="mini-btn" data-action="stop" data-agent="${esc(agent.name)}" title="Stop agent">×</button></div></div>
<div class="activity-line ${esc(agent.state)}"><span class="pulse"></span><span class="activity-text" title="${esc(agent.activity)}">${esc(agent.activity||'Waiting')}</span>${agent.queueDepth?`<span class="queue">${agent.queueDepth} queued</span>`:''}</div>
<div class="context-row" title="Estimated context usage"><div class="context-track"><div class="context-fill ${contextClass}" style="width:${percent}%"></div></div><span class="context-label">~${fmtTokens(agent.contextTokens)} / ${fmtTokens(agent.maxContextTokens)} · ${percent.toFixed(1)}%</span></div></header>
<section class="chat" data-chat="${esc(agent.name)}">${chat.length?chat.map(message).join(''):`<div class="chat-empty"><div><div class="orb">K</div>No messages in this context yet</div></div>`}</section>
<footer class="composer"><div class="composer-box"><textarea rows="1" data-composer="${esc(agent.name)}" placeholder="Message ${esc(agent.name)}…">${esc(draft)}</textarea><button class="send-btn" data-action="send" data-agent="${esc(agent.name)}" ${draft.trim()?'':'disabled'} title="Send task">↑</button></div><div class="composer-foot"><span>Enter to send · Shift+Enter newline</span><span class="spacer"></span>${(agent.scopes||[]).slice(0,2).map(scope=>`<span class="scope-chip">${esc(scope)}</span>`).join('')}</div></footer>
</article>`;
}
function message(item){
if(item.type==='system')return `<div class="system-message">${esc(item.text)}</div>`;
if(item.type==='thought')return `<details class="thought" data-entry="${esc(item.id)}"><summary>Thinking</summary><div class="thought-text">${esc(item.text)}</div></details>`;
if(item.type==='tool')return `<div class="tool-call"><div class="tool-main"><span class="tool-icon">${icon(item.toolKind)}</span><span class="tool-title" title="${esc(item.title)}">${esc(item.title||item.toolKind||'Tool call')}</span><span class="tool-status ${esc(item.status)}">${esc((item.status||'pending').replace('_',' '))}</span></div>${item.text?`<details data-entry="${esc(item.id)}"><summary>Details</summary><pre class="tool-detail">${esc(item.text)}</pre></details>`:''}</div>`;
const role=item.type==='user'?'You':item.type==='error'?'Error':'Kimi';return `<div class="message ${esc(item.type)}"><div class="message-label">${role}</div><div class="message-body">${esc(item.text)}</div></div>`;
}
async function refresh(requestedAgents=null){
if(state.refreshing){(requestedAgents||[]).forEach(name=>state.pendingAgents.add(name));return}
state.refreshing=true;
try{
const agents=await api('/agents'),activeNames=new Set(agents.map(agent=>agent.name)),knownNames=new Set(state.agents.map(agent=>agent.name));
const targets=requestedAgents===null?agents.map(agent=>agent.name):[...new Set([...(requestedAgents||[]),...agents.filter(agent=>!knownNames.has(agent.name)).map(agent=>agent.name)])];
const chats=new Map(state.chats);for(const name of [...chats.keys()])if(!activeNames.has(name))chats.delete(name);
await Promise.all(targets.filter(name=>activeNames.has(name)).map(async name=>chats.set(name,await api(`/agents/${encodeURIComponent(name)}/chat`))));
state.agents=agents;state.chats=chats;render();
}catch(error){toast(error.message,true)}finally{state.refreshing=false;if(state.pendingAgents.size)scheduleRefresh()}
}
function scheduleRefresh(agent){if(agent&&agent!=='fleet')state.pendingAgents.add(agent);if(state.refreshTimer)return;state.refreshTimer=setTimeout(()=>{const agents=[...state.pendingAgents];state.pendingAgents.clear();state.refreshTimer=null;refresh(agents)},450)}
function bindAutoResize(){document.querySelectorAll('.composer textarea').forEach(area=>{area.style.height='auto';area.style.height=`${Math.min(110,area.scrollHeight)}px`})}
grid.addEventListener('input',event=>{if(!event.target.matches('[data-composer]'))return;state.drafts.set(event.target.dataset.composer,event.target.value);event.target.style.height='auto';event.target.style.height=`${Math.min(110,event.target.scrollHeight)}px`;const button=event.target.closest('.composer').querySelector('[data-action="send"]');button.disabled=!event.target.value.trim()});
grid.addEventListener('keydown',event=>{if(event.target.matches('[data-composer]')&&event.key==='Enter'&&!event.shiftKey){event.preventDefault();send(event.target.dataset.composer)}});
grid.addEventListener('click',async event=>{const button=event.target.closest('[data-action]');if(!button)return;const {action,agent}=button.dataset;try{if(action==='send')await send(agent);else if(action==='focus'){state.focused=state.focused===agent?null:agent;render()}else if(action==='cancel'){await api(`/agents/${encodeURIComponent(agent)}/cancel`,{method:'POST',body:'{}'});toast(`${agent}: cancellation requested`)}else if(action==='compact'){await api(`/agents/${encodeURIComponent(agent)}/context/compact`,{method:'POST',body:JSON.stringify({instructions:'Preserve decisions, constraints, current task, changed files, and pending verification.'})});toast(`${agent}: context compaction queued`)}else if(action==='reset'){if(confirm(`Create a fresh context for ${agent}? Files are not changed.`)){await api(`/agents/${encodeURIComponent(agent)}/context/reset`,{method:'POST',body:'{}'});toast(`${agent}: fresh context created`);scheduleRefresh()}}else if(action==='stop'){if(confirm(`Stop ${agent} and release its scopes?`)){await api(`/agents/${encodeURIComponent(agent)}/stop`,{method:'POST',body:'{}'});if(state.focused===agent)state.focused=null;toast(`${agent} stopped`);scheduleRefresh()}}}catch(error){toast(error.message,true)}});
async function send(agent){const prompt=(state.drafts.get(agent)||'').trim();if(!prompt)return;state.drafts.set(agent,'');render();try{await api(`/agents/${encodeURIComponent(agent)}/prompt`,{method:'POST',body:JSON.stringify({prompt})});scheduleRefresh()}catch(error){state.drafts.set(agent,prompt);render();toast(error.message,true)}}
document.querySelector('#newAgentBtn').addEventListener('click',()=>{document.querySelector('#agentWorkspace').value=localStorage.getItem('kimiFleetWorkspace')||state.agents[0]?.workspace||'';dialog.showModal()});document.querySelectorAll('[data-close-dialog]').forEach(button=>button.addEventListener('click',()=>dialog.close()));
form.addEventListener('submit',async event=>{event.preventDefault();const data=new FormData(form),payload={name:data.get('name').trim(),workspace:data.get('workspace').trim(),role:data.get('role').trim()||'worker',scopes:data.get('scopes').split(/[,\n]/).map(value=>value.trim()).filter(Boolean)};try{await api('/agents',{method:'POST',body:JSON.stringify(payload)});localStorage.setItem('kimiFleetWorkspace',payload.workspace);dialog.close();form.reset();toast(`${payload.name} started`);scheduleRefresh()}catch(error){toast(error.message,true)}});
document.querySelector('#densityBtn').addEventListener('click',()=>{state.dense=!state.dense;localStorage.setItem('kimiFleetDensity',state.dense?'dense':'comfortable');render()});search.addEventListener('input',render);
const source=new EventSource('/events');source.onmessage=event=>{try{scheduleRefresh(JSON.parse(event.data).agent)}catch{scheduleRefresh()}};source.onerror=()=>setTimeout(()=>scheduleRefresh(),1000);refresh();setInterval(()=>refresh([]),4000);
</script>
</body>
</html>