From 5a52259fd777c0cb9618e66162df0052b2813194 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Mon, 6 Apr 2026 14:23:26 -0700 Subject: [PATCH] fix: tool cards actually render on page reload from session data (#140) (#153) Co-authored-by: Nathan Esquenazi --- api/streaming.py | 17 +++++++++++++++++ static/ui.js | 31 +++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/api/streaming.py b/api/streaming.py index a7044a0..b8bd051 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -293,6 +293,7 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta for msg_idx, m in enumerate(s.messages): if m.get('role') == 'assistant': c = m.get('content', '') + # Anthropic format: content is a list with type=tool_use blocks if isinstance(c, list): for p in c: if isinstance(p, dict) and p.get('type') == 'tool_use': @@ -300,6 +301,22 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta pending_names[tid] = p.get('name', '') pending_args[tid] = p.get('input', {}) pending_asst_idx[tid] = msg_idx + # OpenAI format: tool_calls as top-level field on the message + for tc in m.get('tool_calls', []): + if not isinstance(tc, dict): + continue + tid = tc.get('id', '') or tc.get('call_id', '') + fn = tc.get('function', {}) + name = fn.get('name', '') + try: + import json as _j + args = _j.loads(fn.get('arguments', '{}') or '{}') + except Exception: + args = {} + if tid and name: + pending_names[tid] = name + pending_args[tid] = args + pending_asst_idx[tid] = msg_idx elif m.get('role') == 'tool': tid = m.get('tool_call_id') or m.get('tool_use_id', '') name = pending_names.get(tid, '') diff --git a/static/ui.js b/static/ui.js index a6c73c5..e0f717c 100644 --- a/static/ui.js +++ b/static/ui.js @@ -477,12 +477,17 @@ function renderMessages(){ }); $('emptyState').style.display=vis.length?'none':''; inner.innerHTML=''; - // Track original indices (in S.messages) so truncate knows the cut point + // Track original indices (in S.messages) so truncate knows the cut point. + // Also include assistant messages that have tool_calls (OpenAI format) or + // tool_use content (Anthropic format) even when their text is empty — these + // rows serve as DOM anchors for tool card insertion on page reload. const visWithIdx=[]; let rawIdx=0; for(const m of S.messages){ if(!m||!m.role||m.role==='tool'){rawIdx++;continue;} - if(msgContent(m)||m.attachments?.length) visWithIdx.push({m,rawIdx}); + const hasTc=Array.isArray(m.tool_calls)&&m.tool_calls.length>0; + const hasTu=Array.isArray(m.content)&&m.content.some(p=>p&&p.type==='tool_use'); + if(msgContent(m)||m.attachments?.length||(m.role==='assistant'&&(hasTc||hasTu))) visWithIdx.push({m,rawIdx}); rawIdx++; } for(let vi=0;vi{ + if(m.role!=='assistant') return; + (m.tool_calls||[]).forEach(tc=>{ + if(!tc||typeof tc!=='object') return; + const fn=tc.function||{}; + const name=fn.name||tc.name||'tool'; + let args={}; + try{ args=JSON.parse(fn.arguments||'{}'); }catch(e){} + let argsSnap={}; + Object.keys(args).slice(0,4).forEach(k=>{ const v=String(args[k]); argsSnap[k]=v.slice(0,120)+(v.length>120?'...':''); }); + derived.push({name,snippet:'',tid:tc.id||tc.call_id||'',assistant_msg_idx:rawIdx,args:argsSnap,done:true}); + }); + }); + if(derived.length) S.toolCalls=derived; + } if(!S.busy && S.toolCalls && S.toolCalls.length){ inner.querySelectorAll('.tool-card-row').forEach(el=>el.remove()); const byAssistant = {};