feat: Sprint 23 — agentic transparency + polish

Track A: Token/cost display
- Read agent usage attrs (session_prompt_tokens, session_completion_tokens,
  session_estimated_cost_usd) after run_conversation in streaming.py
- Add input_tokens, output_tokens, estimated_cost fields to Session model
- Include usage in done SSE event payload
- Store usage on S.lastUsage in messages.js done handler
- Render usage badge below last assistant message (input/output/cost)

Track B: Subagent delegation cards
- Add subagent_progress to toolIcon map with shuffle emoji
- Special-case subagent_progress in buildToolCard: "Subagent" label,
  strip double emoji from preview, add tool-card-subagent CSS class
- Indented border-left styling for subagent cards
- Clean delegate_task display name

Track C: Skill picker in cron create form
- Add skill search input + tag chips to cron create form HTML
- Skill picker JS in panels.js: search/filter, click-to-add tags,
  remove tag chips, pre-fetch skill list on form open
- submitCronCreate sends skills array in POST body
- Skill picker dropdown + tag CSS

Track D: Skill linked files viewer
- Add file query param to /api/skills/content endpoint
- Serve linked files from skill directory with path traversal protection
- Ensure linked_files key always present in skill content response
- Render linked files section below SKILL.md content in preview panel
- openSkillFile function for viewing individual linked files

Track E: Bug fixes and code quality
- Expand Session.__init__ and compact() to readable multi-line format
- Remove inline import json as _j2 inside loop in streaming.py
- Fix tool_calls: capture args from assistant messages, skip unresolved names
- Store args snapshot in persisted tool_calls for reload display

6 new tests. Total: 421 (409 passing).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nathan Esquenazi
2026-04-03 18:33:49 -07:00
parent 2c0f6e80b6
commit df06c1cdca
9 changed files with 371 additions and 21 deletions

View File

@@ -79,6 +79,9 @@ async function loadCrons() {
} catch(e) { box.innerHTML = `<div style="padding:12px;color:var(--accent);font-size:12px">Error: ${esc(e.message)}</div>`; }
}
let _cronSelectedSkills=[];
let _cronSkillsCache=null;
function toggleCronForm(){
const form=$('cronCreateForm');
if(!form)return;
@@ -90,10 +93,65 @@ function toggleCronForm(){
$('cronFormPrompt').value='';
$('cronFormDeliver').value='local';
$('cronFormError').style.display='none';
_cronSelectedSkills=[];
_renderCronSkillTags();
const search=$('cronFormSkillSearch');
if(search)search.value='';
// Pre-fetch skills for the picker
if(!_cronSkillsCache){
api('/api/skills').then(d=>{_cronSkillsCache=d.skills||[];}).catch(()=>{});
}
$('cronFormName').focus();
}
}
function _renderCronSkillTags(){
const wrap=$('cronFormSkillTags');
if(!wrap)return;
wrap.innerHTML='';
for(const name of _cronSelectedSkills){
const tag=document.createElement('span');
tag.className='skill-tag';
tag.innerHTML=esc(name)+'<span class="remove-tag" onclick="this.parentElement.remove();_cronSelectedSkills=_cronSelectedSkills.filter(s=>s!==\''+esc(name)+'\')">×</span>';
wrap.appendChild(tag);
}
}
// Skill search input handler
(function(){
const setup=()=>{
const search=$('cronFormSkillSearch');
const dropdown=$('cronFormSkillDropdown');
if(!search||!dropdown)return;
search.oninput=()=>{
const q=search.value.trim().toLowerCase();
if(!q||!_cronSkillsCache){dropdown.style.display='none';return;}
const matches=_cronSkillsCache.filter(s=>
!_cronSelectedSkills.includes(s.name)&&
(s.name.toLowerCase().includes(q)||(s.category||'').toLowerCase().includes(q))
).slice(0,8);
if(!matches.length){dropdown.style.display='none';return;}
dropdown.innerHTML='';
for(const s of matches){
const opt=document.createElement('div');
opt.className='skill-opt';
opt.textContent=s.name+(s.category?' ('+s.category+')':'');
opt.onclick=()=>{
_cronSelectedSkills.push(s.name);
_renderCronSkillTags();
search.value='';
dropdown.style.display='none';
};
dropdown.appendChild(opt);
}
dropdown.style.display='';
};
search.onblur=()=>setTimeout(()=>{dropdown.style.display='none';},150);
};
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',setup);
else setTimeout(setup,0);
})();
async function submitCronCreate(){
const name=$('cronFormName').value.trim();
const schedule=$('cronFormSchedule').value.trim();
@@ -104,7 +162,10 @@ async function submitCronCreate(){
if(!schedule){errEl.textContent='Schedule is required (e.g. "0 9 * * *" or "every 1h")';errEl.style.display='';return;}
if(!prompt){errEl.textContent='Prompt is required';errEl.style.display='';return;}
try{
await api('/api/crons/create',{method:'POST',body:JSON.stringify({name:name||undefined,schedule,prompt,deliver})});
const body={schedule,prompt,deliver};
if(name)body.name=name;
if(_cronSelectedSkills.length)body.skills=_cronSelectedSkills;
await api('/api/crons/create',{method:'POST',body:JSON.stringify(body)});
toggleCronForm();
showToast('Job created ✓');
await loadCrons();
@@ -344,12 +405,45 @@ async function openSkill(name, el) {
$('previewBadge').textContent = 'skill';
$('previewBadge').className = 'preview-badge md';
showPreview('md');
$('previewMd').innerHTML = renderMd(data.content || '(no content)');
let html = renderMd(data.content || '(no content)');
// Render linked files section if present
const lf = data.linked_files || {};
const categories = Object.entries(lf).filter(([,files]) => files && files.length > 0);
if (categories.length) {
html += '<div class="skill-linked-files"><div style="font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px">Linked Files</div>';
for (const [cat, files] of categories) {
html += `<div class="skill-linked-section"><h4>${esc(cat)}</h4>`;
for (const f of files) {
html += `<a class="skill-linked-file" onclick="openSkillFile('${esc(name)}','${esc(f)}');return false" href="#">${esc(f)}</a>`;
}
html += '</div>';
}
html += '</div>';
}
$('previewMd').innerHTML = html;
$('previewArea').classList.add('visible');
$('fileTree').style.display = 'none';
} catch(e) { setStatus('Could not load skill: ' + e.message); }
}
async function openSkillFile(skillName, filePath) {
try {
const data = await api(`/api/skills/content?name=${encodeURIComponent(skillName)}&file=${encodeURIComponent(filePath)}`);
$('previewPathText').textContent = skillName + ' / ' + filePath;
$('previewBadge').textContent = filePath.split('.').pop() || 'file';
$('previewBadge').className = 'preview-badge code';
const ext = filePath.split('.').pop() || '';
if (['md','markdown'].includes(ext)) {
showPreview('md');
$('previewMd').innerHTML = renderMd(data.content || '');
} else {
showPreview('code');
$('previewCode').textContent = data.content || '';
requestAnimationFrame(() => highlightCode());
}
} catch(e) { setStatus('Could not load file: ' + e.message); }
}
// ── Skill create/edit form ──
let _editingSkillName = null;