function genId(){return Date.now().toString(36)+Math.random().toString(36).substr(2,5)}
-function startNewSession(){sessionId=genId();chatHist=[];pendingFiles=[];document.getElementById('fileChips').innerHTML='';document.getElementById('messages').innerHTML='
Deep Chat \u2014 250K context window Drop files, images, or just talk.
Deep Chat \u2014 250K context window Drop files, images, or just talk.
'}
+
+// PR #39: persist sessionId across page reloads. If a previous session
+// exists, load it on boot so the user lands back on the same chat (with
+// any project plan cards rehydrated). Falls back to a fresh session if
+// the previous one's GET returns empty.
+(function bootSession(){
+ var saved=localStorage.getItem('codec-chat-session');
+ if(!saved){startNewSession();return}
+ // Optimistically load \u2014 if the session is empty/missing, we'll start fresh
+ fetch('/api/qchat/session/'+saved).then(function(r){
+ return r.ok?r.json():[];
+ }).then(function(data){
+ if(data&&data.length>0){
+ // Use the saved id and replay history through the same code path
+ // sidebar uses (so the plan-card rehydration runs once the messages
+ // are in the DOM).
+ sessionId=saved;
+ var es=document.getElementById('emptyState');if(es)es.remove();
+ document.getElementById('messages').innerHTML='';
+ chatHist=[];
+ for(var i=0;iCODEC
}
async function loadSession(sid){
- closeSidebar();sessionId=sid;chatHist=[];pendingFiles=[];
+ closeSidebar();sessionId=sid;localStorage.setItem('codec-chat-session',sid);chatHist=[];pendingFiles=[];
document.getElementById('fileChips').innerHTML='';
var es=document.getElementById('emptyState');if(es)es.remove();
document.getElementById('messages').innerHTML='';
try{
var r=await fetch('/api/qchat/session/'+sid);var data=await r.json();
for(var i=0;i] marker
+ // gets replaced by the live plan card (fetched from /api/agents/).
+ await rehydrateAgentPlanCards();
scrollBottom()
}catch(e){}
}
@@ -743,6 +776,119 @@
}
}
var addMsg=addMessage; // alias for webcam code
+
+// ───────────────────────────────────────────────────────────────────────────
+// PR #39 — Agent plan persistence (Project mode)
+// ───────────────────────────────────────────────────────────────────────────
+// Why this exists: Phase 3.5 saved only the bare string "Project drafted:
+// agent_xxx" to chat history; the inline card with approve/reject/view-plan
+// buttons lived only in the DOM. Reloading the chat lost the card.
+//
+// Approach: persist a marker token `[CODEC_AGENT_PLAN:]` inside the
+// assistant message content. On chat session load, scan rendered messages,
+// detect the marker, fetch the agent state, and replace the text bubble
+// with the same card the live flow renders.
+//
+// The card's button callbacks (approveAgentInChat / rejectAgentInChat /
+// viewAgentPlan) only need agent_id, so reload-time rendering matches the
+// live render exactly. Status-aware buttons (e.g. "Resume" on
+// blocked_on_qwen) come for free via the agent state object.
+var AGENT_PLAN_MARKER_RE=/\[CODEC_AGENT_PLAN:(agent_[a-z0-9]+)\]/;
+
+function extractAgentIdFromMessage(content){
+ if(!content||typeof content!=='string')return null;
+ var m=content.match(AGENT_PLAN_MARKER_RE);
+ return m?m[1]:null;
+}
+
+function renderAgentPlanCard(agentId,agentInfo){
+ // agentInfo accepts two shapes:
+ // - POST /api/agents result: {agent_id, status, project_dir} (flat)
+ // - GET /api/agents/: {manifest: {agent_id, status, ...}, plan, state, grants}
+ // Normalize both into the same view-model.
+ agentInfo=agentInfo||{};
+ var manifest=agentInfo.manifest||agentInfo; // GET wraps in .manifest, POST is flat
+ var status=manifest.status||'';
+ var reason=manifest.status_reason||'';
+ var projectDir=manifest.project_dir||'';
+ var folderHtml='';
+ if(projectDir){
+ folderHtml='
'+
+ '
Project folder
'+
+ ''+escHtml(projectDir)+''+
+ '
Open this folder in your IDE to see the agent\'s files as they\'re created. (cmd+click to open in Finder)
';
+ }
+ var subline=isPendingApproval
+ ?'A plan with a permission manifest has been written. Review and approve to let the agent run autonomously.'
+ :(isTerminal?'Agent finished. View plan to see what ran.':'Agent state shown above; click View plan for live progress.');
+ return'
Project drafted
'+
+ '
agent_id: '+escHtml(agentId)+'
'+
+ statusLine+
+ folderHtml+
+ '
'+subline+'
'+
+ '
'+buttonsHtml+'
';
+}
+
+async function rehydrateAgentPlanCards(){
+ // Called after loadSession finishes rendering text messages. Scan
+ // assistant bubbles for the marker, fetch each agent in parallel,
+ // replace the bubble with the rendered card. Failures fall back
+ // to leaving the marker text visible (so the user knows something
+ // is missing rather than seeing nothing).
+ var msgs=document.querySelectorAll('#messages .msg.assistant .msg-bubble');
+ var jobs=[];
+ for(var i=0;i returns {manifest, plan, state, grants}.
+ // Treat presence of manifest.agent_id as "agent exists".
+ var ok=info&&info.manifest&&info.manifest.agent_id;
+ if(ok){
+ b.innerHTML=renderAgentPlanCard(id,info);
+ }else{
+ // Agent missing (e.g. manifest deleted); show inline notice
+ // but keep the agent_id visible so the user knows what's gone.
+ b.innerHTML='
Project: '+escHtml(id)+'
'+
+ '
Agent state not found — manifest may have been removed.
';
+ }
+ }).catch(function(){/* keep raw text on error */});
+ })(bubble,agentId));
+ }
+ if(jobs.length){await Promise.all(jobs)}
+}
+
function scrollBottom(){var m=document.getElementById('messages');m.scrollTop=m.scrollHeight}
function showTyping(){var div=document.createElement('div');div.className='typing';div.id='typing';div.innerHTML='\u25CF\u25CF\u25CF CODEC is thinking...';document.getElementById('messages').appendChild(div);scrollBottom()}
function hideTyping(){var t=document.getElementById('typing');if(t)t.remove()}
@@ -798,25 +944,14 @@