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:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user