Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 188 additions & 3 deletions codec_chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ <h1><a href="/" style="color:inherit;text-decoration:none">CODEC</a></h1>
<svg class="ico ico-sm" viewBox="0 0 24 24"><path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>Think
</button>
<button class="mode-btn" onclick="setMode('research')" id="modeResearchBtn"><svg class="ico" viewBox="0 0 24 24" style="width:14px;height:14px"><rect x="4" y="4" width="16" height="16" rx="2"/><circle cx="9" cy="10" r="1.5" fill="currentColor" stroke="none"/><circle cx="15" cy="10" r="1.5" fill="currentColor" stroke="none"/><path d="M9 15h6"/></svg>Agents</button>
<button class="mode-btn" onclick="setMode('project')" id="modeProjectBtn" title="Drop a project — autonomous plan-and-build mode"><svg class="ico" viewBox="0 0 24 24" style="width:14px;height:14px"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="5"/><line x1="12" y1="2" x2="12" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/></svg>Project</button>
<select id="crewSelect">
<option value="deep_research">🔍 Deep Research</option>
<option value="daily_briefing">📰 Daily Briefing</option>
Expand Down Expand Up @@ -516,6 +517,8 @@ <h1><a href="/" style="color:inherit;text-decoration:none">CODEC</a></h1>
chatMode=mode;
document.getElementById('modeChatBtn').classList.toggle('active',mode==='chat');
document.getElementById('modeResearchBtn').classList.toggle('active',mode==='research');
var pBtn=document.getElementById('modeProjectBtn');
if(pBtn)pBtn.classList.toggle('active',mode==='project');
var cs=document.getElementById('crewSelect');
cs.style.display=mode==='research'?'inline-block':'none';
var crew=mode==='research'?cs.value:'deep_research';
Expand All @@ -527,12 +530,33 @@ <h1><a href="/" style="color:inherit;text-decoration:none">CODEC</a></h1>
// that explicitly in the placeholder so the user knows they can ask
// follow-up questions.
var hasAgentReport=mode==='chat'&&chatHist.some(function(m){return m.role==='assistant'&&typeof m.content==='string'&&m.content.indexOf('Complete!')!==-1});
document.getElementById('chatInput').placeholder=mode==='research'
?(hints[crew]||'Enter crew task...')
:(hasAgentReport?'Ask follow-up about the report...':'Message CODEC...');
var ph;
if(mode==='research')ph=hints[crew]||'Enter crew task...';
else if(mode==='project')ph='Drop your project here — describe what you want CODEC to build...';
else ph=hasAgentReport?'Ask follow-up about the report...':'Message CODEC...';
document.getElementById('chatInput').placeholder=ph;
document.getElementById('scheduleBtn').style.display=mode==='research'?'':'none';
var builder=document.getElementById('agentBuilder');
if(mode==='research'&&crew==='custom'){builder.classList.add('show');if(!_toolsLoaded)loadAgentTools();loadSavedAgentList()}else{builder.classList.remove('show')}
// Phase 3.5 Step 11: Project-mode on-screen instructions panel
var pPanel=document.getElementById('projectModePanel');
if(mode==='project'){
if(!pPanel){
pPanel=document.createElement('div');
pPanel.id='projectModePanel';
pPanel.style.cssText='margin:12px 16px 8px;padding:14px 16px;background:rgba(167,139,250,0.08);border:1px solid var(--accent,#a78bfa);border-radius:10px;font-size:13px;line-height:1.5';
pPanel.innerHTML=
'<div style="font-weight:600;color:var(--accent,#a78bfa);margin-bottom:6px">Project mode — autonomous plan-and-build</div>'+
'<div style="color:var(--text-dim);margin-bottom:8px">Drop a project below. CODEC drafts a plan + permission manifest, you approve once, then codec-agent-runner works through it autonomously (Qwen-3.6 + skills). Updates post back to this thread.</div>'+
'<div style="color:var(--text-dim);font-size:12px">Examples: <em>"Build a Telegram bot that monitors Marbella property listings under €500k"</em> · <em>"Watch EUR/USD intraday vol and ping me when 30-min realized vol crosses 0.4%"</em> · <em>"Plan the launch of [product] over 6 weeks with weekly milestones"</em></div>';
}
var msgsEl=document.getElementById('messages');
if(msgsEl&&!document.getElementById('projectModePanel')){
msgsEl.parentNode.insertBefore(pPanel,msgsEl);
}
}else if(pPanel){
pPanel.remove();
}
// Live-switch behaviour: only wipe the session when the chat is EMPTY.
// If there's already a conversation (e.g. an agent just returned a deep-
// research report), keep chatHist intact so the user can flip into chat
Expand Down Expand Up @@ -749,6 +773,54 @@ <h1><a href="/" style="color:inherit;text-decoration:none">CODEC</a></h1>
if(!text&&!pendingFiles.length)return;
input.value='';input.style.height='auto';

// Phase 3.5 Step 11: Project mode → POST /api/agents (Step 8 plan dispatch)
if(chatMode==='project'){
if(!text)return;
addMessage('user',text);
chatHist.push({role:'user',content:text});
saveMessages([{role:'user',content:text}]);
var pendingDiv=document.createElement('div');
pendingDiv.className='msg assistant';
pendingDiv.innerHTML='<div class="msg-bubble">Drafting plan via Qwen-3.6...</div>';
document.getElementById('messages').appendChild(pendingDiv);
scrollBottom();
isProcessing=true;document.getElementById('sendBtn').disabled=true;
try{
var pr=await fetch('/api/agents',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({title:text.slice(0,80),description:text})});
if(pr.status===401||pr.status===403){
pendingDiv.querySelector('.msg-bubble').innerHTML='<span style="color:var(--danger,#f87171)">Session expired</span> — refreshing...';
setTimeout(function(){window.location.href='/auth'},1500);
return;
}
var pd=await pr.json();
if(pd.detail){
pendingDiv.querySelector('.msg-bubble').innerHTML='<span style="color:var(--danger,#f87171)">Plan failed:</span> '+escHtml(pd.detail);
chatHist.push({role:'assistant',content:'Plan failed: '+pd.detail});
}else if(pd.agent_id){
var html='<div style="font-weight:600;margin-bottom:6px">Project drafted</div>'+
'<div style="margin-bottom:6px">agent_id: <code>'+escHtml(pd.agent_id)+'</code></div>'+
'<div style="color:var(--text-dim);font-size:12px;margin-bottom:8px">A plan with a permission manifest has been written to <code>~/.codec/agents/'+escHtml(pd.agent_id)+'/plan.json</code>. Review and approve to start.</div>'+
'<div style="display:flex;gap:8px;flex-wrap:wrap">'+
'<button onclick="approveAgentInChat(\''+pd.agent_id+'\',event)" style="padding:6px 14px;background:var(--accent,#a78bfa);color:#000;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600">Approve plan</button>'+
'<button onclick="rejectAgentInChat(\''+pd.agent_id+'\',event)" style="padding:6px 14px;background:transparent;color:var(--text);border:1px solid var(--border,#2a2a30);border-radius:6px;cursor:pointer;font-size:12px">Reject</button>'+
'<button onclick="viewAgentPlan(\''+pd.agent_id+'\',event)" style="padding:6px 14px;background:transparent;color:var(--text);border:1px solid var(--border,#2a2a30);border-radius:6px;cursor:pointer;font-size:12px">View plan</button>'+
'</div>';
pendingDiv.querySelector('.msg-bubble').innerHTML=html;
chatHist.push({role:'assistant',content:'Project drafted: '+pd.agent_id});
refreshAgentStatusPills();
}else{
pendingDiv.querySelector('.msg-bubble').innerHTML='<span style="color:var(--danger,#f87171)">Unknown response</span>';
}
}catch(e){
pendingDiv.querySelector('.msg-bubble').innerHTML='<span style="color:var(--danger,#f87171)">Failed:</span> '+escHtml(e.message);
}finally{
isProcessing=false;document.getElementById('sendBtn').disabled=false;
_showSend();
}
return;
}

if(chatMode==='research'){
var researchText=text||'';
var crew=document.getElementById('crewSelect').value||'deep_research';
Expand Down Expand Up @@ -1206,5 +1278,118 @@ <h1><a href="/" style="color:inherit;text-decoration:none">CODEC</a></h1>
poll();setInterval(poll,3000);
})();
</script>

<!-- Phase 3.5 Step 11 — Project mode agent action handlers + status pills -->
<script>
async function approveAgentInChat(agentId,e){
if(e)e.preventDefault();
if(!confirm('Approve plan for '+agentId+'?\n\nThe agent will run autonomously with the manifest permissions.'))return;
try{
var r=await fetch('/api/agents/'+encodeURIComponent(agentId)+'/approve',{method:'POST'});
if(r.ok){
var div=document.createElement('div');div.className='msg assistant';
div.innerHTML='<div class="msg-bubble"><span style="color:var(--success,#10b981)">Plan approved.</span> codec-agent-runner picks it up within 5s. Watch <code>~/.codec/audit.log</code> for <code>agent_started</code> + checkpoint events.</div>';
document.getElementById('messages').appendChild(div);scrollBottom();
}else{
var d=await r.json().catch(function(){return{}});
showToast('Approval failed: '+(d.detail||r.status));
}
}catch(err){showToast('Approval error: '+err.message)}
refreshAgentStatusPills();
}
async function rejectAgentInChat(agentId,e){
if(e)e.preventDefault();
var reason=prompt('Reason for rejection (optional):','');
if(reason===null)return;
try{
var r=await fetch('/api/agents/'+encodeURIComponent(agentId)+'/reject',{method:'POST',
headers:{'Content-Type':'application/json'},body:JSON.stringify({reason:reason||''})});
if(r.ok){showToast('Plan rejected.')}else{showToast('Reject failed')}
}catch(err){showToast('Error: '+err.message)}
refreshAgentStatusPills();
}
async function viewAgentPlan(agentId,e){
if(e)e.preventDefault();
try{
var r=await fetch('/api/agents/'+encodeURIComponent(agentId));
if(!r.ok){showToast('Failed to load plan: '+r.status);return}
var d=await r.json();
var plan=d.plan||{};
var manifest=plan.permission_manifest||{};
var html='<div class="msg-bubble"><div style="font-weight:600;margin-bottom:8px">Plan for '+escHtml(agentId)+'</div>';
if(plan.goals&&plan.goals.length){
html+='<div style="font-weight:500;margin:8px 0 4px;font-size:12px;text-transform:uppercase;color:var(--text-dim)">Goals</div>';
html+='<ul style="margin:0 0 8px;padding-left:18px;font-size:13px">';
plan.goals.forEach(function(g){html+='<li>'+escHtml(g)+'</li>'});
html+='</ul>';
}
if(plan.checkpoints&&plan.checkpoints.length){
html+='<div style="font-weight:500;margin:8px 0 4px;font-size:12px;text-transform:uppercase;color:var(--text-dim)">Checkpoints ('+plan.checkpoints.length+')</div>';
html+='<ol style="margin:0 0 8px;padding-left:18px;font-size:13px">';
plan.checkpoints.forEach(function(cp){html+='<li><b>'+escHtml(cp.title||'')+'</b>: '+escHtml((cp.description||'').slice(0,200))+'</li>'});
html+='</ol>';
}
html+='<div style="font-weight:500;margin:8px 0 4px;font-size:12px;text-transform:uppercase;color:var(--text-dim)">Permission manifest</div>';
html+='<div style="font-size:12px;background:rgba(0,0,0,0.2);padding:8px;border-radius:6px;font-family:var(--mono,monospace)">';
html+='skills: '+escHtml((manifest.skills||[]).join(', ')||'(none)')+'<br>';
html+='network_domains: '+escHtml((manifest.network_domains||[]).join(', ')||'(none)')+'<br>';
html+='write_paths: '+escHtml((manifest.write_paths||[]).join(', ')||'(none)')+'<br>';
html+='destructive_ops: '+escHtml((manifest.destructive_ops||[]).join(', ')||'(none)');
html+='</div></div>';
var div=document.createElement('div');div.className='msg assistant';div.innerHTML=html;
document.getElementById('messages').appendChild(div);scrollBottom();
}catch(err){showToast('Error: '+err.message)}
}

// Status pills (above input) — polls /api/agents every 5s
async function refreshAgentStatusPills(){
var container=document.getElementById('agentStatusPills');
if(!container){
// Inject the container right above the input area
var inputArea=document.querySelector('.input-area');
if(!inputArea)return;
container=document.createElement('div');
container.id='agentStatusPills';
container.style.cssText='display:none;flex-wrap:wrap;gap:6px;margin:0 16px 6px;font-size:11px';
inputArea.parentNode.insertBefore(container,inputArea);
}
try{
var r=await fetch('/api/agents');
if(r.status===401||r.status===403)return;
if(!r.ok)return;
var data=await r.json();
var pills=(data.agents||[]).filter(function(a){
return ['running','paused','blocked_on_permission','blocked_on_destructive',
'awaiting_approval','draft_pending','crashed_resumed'].indexOf(a.status)>=0;
}).slice(0,5);
if(pills.length===0){container.style.display='none';container.innerHTML='';return}
container.style.display='flex';
container.innerHTML=pills.map(function(p){
var color='#888';
if(p.status==='running')color='#10b981';
else if(p.status==='paused')color='#f59e0b';
else if(p.status==='awaiting_approval')color='#eab308';
else if(p.status==='draft_pending')color='#6b7280';
else if(p.status==='blocked_on_permission'||p.status==='blocked_on_destructive')color='#ef4444';
else if(p.status==='crashed_resumed')color='#f59e0b';
var actions='';
if(p.status==='awaiting_approval'){
actions=' <a href="#" onclick="approveAgentInChat(\''+p.agent_id+'\',event)" style="color:#fff;text-decoration:underline;margin-left:4px">approve</a>';
}else if(p.status==='paused'){
actions=' <a href="#" onclick="resumeAgentInChat(\''+p.agent_id+'\',event)" style="color:#fff;text-decoration:underline;margin-left:4px">resume</a>';
}else if(p.status==='running'){
actions=' <a href="#" onclick="pauseAgentInChat(\''+p.agent_id+'\',event)" style="color:#fff;text-decoration:underline;margin-left:4px">pause</a>';
}
actions+=' <a href="#" onclick="abortAgentInChat(\''+p.agent_id+'\',event)" style="color:#fff;text-decoration:underline;margin-left:4px">abort</a>';
return '<span style="display:inline-flex;align-items:center;padding:3px 10px;background:'+color+';color:#fff;border-radius:10px">'+
escHtml((p.title||p.agent_id).slice(0,40))+' · '+p.status+actions+'</span>';
}).join('');
}catch(e){/* silent */}
}
async function pauseAgentInChat(agentId,e){if(e)e.preventDefault();await fetch('/api/agents/'+encodeURIComponent(agentId)+'/pause',{method:'POST'});refreshAgentStatusPills()}
async function resumeAgentInChat(agentId,e){if(e)e.preventDefault();await fetch('/api/agents/'+encodeURIComponent(agentId)+'/resume',{method:'POST'});refreshAgentStatusPills()}
async function abortAgentInChat(agentId,e){if(e)e.preventDefault();if(!confirm('Abort agent '+agentId+'?'))return;await fetch('/api/agents/'+encodeURIComponent(agentId)+'/abort',{method:'POST'});refreshAgentStatusPills()}
setTimeout(refreshAgentStatusPills,500);setInterval(refreshAgentStatusPills,5000);
</script>
</body>
</html>
Loading
Loading