// Session list and detail rendering module. // Extracted from render.js -- all session/subagent DOM generation. import { state } from './state.js'; import { loadSessionDetail, loadSubagentDetail, isTextTruncated, loadFullText } from './data.js'; import { escapeHTML, highlightPlain, fmtListTime, fmtRelative, fmtClockTime, renderMarkdown, formatProjectLabel, $ } from './utils.js'; import { FOLDER_SVG } from './state.js'; import registry from './registry.js'; // --- Session list --- export function renderSessionList() { const items = visibleSessions(); const list = $('#session-list'); if (!list) return; if (!items.length) { list.innerHTML = `
No sessions here.${state.query ? 'Try a different search term.' : 'Press / to search.'}
`; return; } list.innerHTML = items.map(s => renderSessionRow(s)).join(''); } function renderSessionRow(s) { const q = state.query.trim(); const showProjectPrefix = state.projectFilter === 'all'; const startedTs = new Date(s.started_at || 0).getTime(); return `
${highlightPlain(s.title || '(untitled)', q)}
${showProjectPrefix ? `${escapeHTML(formatProjectLabel(s.project))}` : ''} ${s.message_count || 0} msg
${fmtListTime(startedTs)}
`; } // --- Session detail --- export async function renderSessionDetail() { // If viewing a subagent, render that instead if (state.subagentId) { return renderSubagentDetail(); } const s = state.sessions.find(x => x.id === state.detailId); if (!s) return; const detail = $('#session-detail'); if (!detail) return; const wrap = $('#session-detail-wrap'); // If DOM was already built for this session, skip rebuild if (detail.dataset.renderedSession === state.detailId) { return; } // Load messages on demand if (!s.messages || s.messages.length === 0) { const loaded = await loadSessionDetail(s.id); if (loaded) Object.assign(s, loaded); } const startedTs = new Date(s.started_at || 0).getTime(); const headerHTML = `
${FOLDER_SVG} ${escapeHTML(formatProjectLabel(s.project))} · ${escapeHTML(s.project_path || '')}
${escapeHTML(s.title || '(untitled)')}
${fmtRelative(startedTs)} ${s.message_count || 0} messages ${s.git_branch ? `${escapeHTML(s.git_branch)}` : ''}
`; const messagesHTML = (s.messages || []).map((msg, idx) => renderMessage(msg, idx)).join(''); detail.innerHTML = `
${headerHTML}
${messagesHTML}
`; detail.dataset.renderedSession = state.detailId; // Progress bar: track scroll position relative to messages const progressFill = detail.querySelector('#session-progress-fill'); if (wrap && progressFill) { const updateProgress = () => { const msgs = detail.querySelectorAll('.msg, .wf-card'); if (!msgs.length) return; const wrapTop = wrap.getBoundingClientRect().top; let topMsgIdx = 0; for (let i = 0; i < msgs.length; i++) { if (msgs[i].getBoundingClientRect().top <= wrapTop + 50) topMsgIdx = i; else break; } const pct = msgs.length <= 1 ? 100 : Math.round((topMsgIdx / (msgs.length - 1)) * 100); progressFill.style.width = pct + '%'; // Show/hide back-to-top button const topBtn = detail.querySelector('#back-to-top'); if (topBtn) topBtn.classList.toggle('show', wrap.scrollTop > 300); }; wrap.addEventListener('scroll', updateProgress); updateProgress(); } // Back to top button const topBtn = document.createElement('button'); topBtn.id = 'back-to-top'; topBtn.className = 'back-to-top'; topBtn.innerHTML = ``; topBtn.addEventListener('click', () => { if (wrap) wrap.scrollTo({ top: 0, behavior: 'smooth' }); }); detail.appendChild(topBtn); // Wire up tool call toggles detail.querySelectorAll('.toolcall-toggle').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); btn.closest('.msg-tool').classList.toggle('open'); }); }); detail.querySelectorAll('.summary-toggle').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); btn.closest('.msg-summary').classList.toggle('open'); }); }); detail.querySelectorAll('.thinking-toggle').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); btn.closest('.msg-thinking').classList.toggle('open'); }); }); detail.querySelectorAll('.meta-toggle').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); btn.closest('.msg-meta-collapsed').classList.toggle('open'); }); }); detail.querySelectorAll('.truncated-btn').forEach(btn => { btn.addEventListener('click', async e => { e.stopPropagation(); const uuid = btn.dataset.uuid; btn.textContent = 'Loading…'; const fullText = await loadFullText(uuid); if (fullText) { const msgEl = btn.closest('.msg'); const bodyEl = msgEl.querySelector('.markdown-msg') || msgEl.querySelector('.markdown-compact'); const variant = bodyEl?.classList.contains('markdown-compact') ? 'compact' : 'msg'; const rendered = renderMarkdown(fullText, { variant, query: state.query }); if (bodyEl) bodyEl.outerHTML = rendered; btn.remove(); } else { btn.textContent = 'Failed to load full text'; } }); }); // Subagent navigation detail.querySelectorAll('[data-action="open-subagent"]').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); registry.navigateToSubagent(btn.dataset.agentId, btn.dataset.agentDesc); }); }); // Focus on pending message if (state.pendingFocusUuid) { const targetUuid = state.pendingFocusUuid; requestAnimationFrame(() => { const target = detail.querySelector(`.msg[data-uuid="${targetUuid}"]`); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); requestAnimationFrame(() => { target.classList.add('is-focused'); setTimeout(() => target.classList.remove('is-focused'), 1200); }); } }); state.pendingFocusUuid = null; } } async function renderSubagentDetail() { const detail = $('#subagent-detail'); if (!detail) return; const wrap = $('#subagent-detail-wrap'); if (wrap) wrap.scrollTop = 0; const messages = await loadSubagentDetail(state.subagentId); const headerHTML = `
SUBAGENT
${escapeHTML(state.subagentDescription || state.subagentId)}
${messages.length} messages
`; const messagesHTML = messages.map((msg, idx) => { return renderMessage(msg, idx, { isSubagent: true }); }).join(''); detail.innerHTML = `
${headerHTML}
${messagesHTML}
`; // Wire up toggles detail.querySelectorAll('.toolcall-toggle').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); btn.closest('.msg-tool').classList.toggle('open'); }); }); detail.querySelectorAll('.summary-toggle').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); btn.closest('.msg-summary').classList.toggle('open'); }); }); detail.querySelectorAll('.thinking-toggle').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); btn.closest('.msg-thinking').classList.toggle('open'); }); }); detail.querySelectorAll('.meta-toggle').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); btn.closest('.msg-meta-collapsed').classList.toggle('open'); }); }); detail.querySelectorAll('.truncated-btn').forEach(btn => { btn.addEventListener('click', async e => { e.stopPropagation(); const uuid = btn.dataset.uuid; btn.textContent = 'Loading…'; const fullText = await loadFullText(uuid); if (fullText) { const msgEl = btn.closest('.msg'); const bodyEl = msgEl.querySelector('.markdown-msg') || msgEl.querySelector('.markdown-compact'); const variant = bodyEl?.classList.contains('markdown-compact') ? 'compact' : 'msg'; const rendered = renderMarkdown(fullText, { variant, query: state.query }); if (bodyEl) bodyEl.outerHTML = rendered; btn.remove(); } else { btn.textContent = 'Failed to load full text'; } }); }); // Nested subagent navigation detail.querySelectorAll('[data-action="open-subagent"]').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); registry.navigateToSubagent(btn.dataset.agentId, btn.dataset.agentDesc); }); }); // Progress bar const progressFill = detail.querySelector('#session-progress-fill'); if (wrap && progressFill) { const updateProgress = () => { const msgs = detail.querySelectorAll('.msg'); if (!msgs.length) return; const wrapTop = wrap.getBoundingClientRect().top; let topMsgIdx = 0; for (let i = 0; i < msgs.length; i++) { if (msgs[i].getBoundingClientRect().top <= wrapTop + 50) topMsgIdx = i; else break; } const pct = msgs.length <= 1 ? 100 : Math.round((topMsgIdx / (msgs.length - 1)) * 100); progressFill.style.width = pct + '%'; }; wrap.addEventListener('scroll', updateProgress); updateProgress(); } } function renderMessage(msg, idx, opts = {}) { const isUser = msg.type === 'user'; const isThinking = msg.content_type === 'thinking'; const isMeta = msg.is_meta === 1; const tools = (msg.tool_calls || []).map(renderToolCall).join(''); // In subagent context, all user text messages are prompts (from main agent or human) let roleLabel = isUser ? 'You' : 'Assistant'; if (opts.isSubagent && isUser) { roleLabel = 'Prompt'; } // Meta messages: collapsed by default, shown as a small system indicator if (isMeta) { const preview = (msg.text || '').replace(/<[^>]+>/g, '').slice(0, 80); const truncated = isTextTruncated(msg.text); return `
${renderMarkdown(msg.text, { variant: 'compact', query: state.query })} ${truncated ? `` : ''}
`; } // Workflow as standalone card (not inside assistant bubble) const workflowCall = (msg.tool_calls || []).find(tc => tc.name === 'Workflow' && tc.workflow); if (workflowCall && !isUser) { const wf = workflowCall.workflow; const wfName = wf.workflow_name || 'Workflow'; const agents = wf.agents || []; const phases = {}; for (const a of agents) { const phase = a.phase || 'Other'; if (!phases[phase]) phases[phase] = []; phases[phase].push(a); } const phasesHTML = Object.entries(phases).map(([phase, agentList]) => `
${escapeHTML(phase)}
${agentList.map(a => ` `).join('')}
`).join(''); // Render other tool calls (non-workflow) if any const otherTools = (msg.tool_calls || []).filter(tc => tc !== workflowCall).map(renderToolCall).join(''); return `
${escapeHTML(wfName)} ${agents.length} agents ${wf.status ? `${escapeHTML(wf.status)}` : ''}
${phasesHTML}
${otherTools ? `
${otherTools}
` : ''} `; } // Standalone thinking message (no following assistant to attach to) if (isThinking) { return `
${renderMarkdown(msg.text, { variant: 'msg', query: state.query })}
`; } // Thinking block attached to this message (merged from preceding thinking messages) let thinkingHTML = ''; if (msg._thinking) { thinkingHTML = `
${renderMarkdown(msg._thinking, { variant: 'msg', query: state.query })}
`; } const truncated = isTextTruncated(msg.text); let textHTML = msg.text ? renderMarkdown(msg.text, { variant: 'msg', query: state.query }) : (tools ? '' : '
(no text content)
'); if (truncated) { textHTML += ``; } let summaryHTML = ''; if (msg.summary) { summaryHTML = `
${renderMarkdown(msg.summary.content, { variant: 'compact' })}
`; } return `
${roleLabel} ${msg.timestamp ? fmtClockTime(msg.timestamp) : ''}
${thinkingHTML} ${textHTML} ${tools ? `
${tools}
` : ''} ${summaryHTML}
`; } function renderToolCall(tc) { const isError = tc.result && tc.result.is_error; // Special rendering for Agent/Task tool calls (subagents) if (tc.name === 'Agent' || tc.name === 'Task') { let parsed = {}; try { parsed = JSON.parse(tc.input_json || '{}'); } catch {} const agentType = parsed.subagent_type || parsed.agentType || 'Agent'; const description = parsed.description || parsed.prompt?.slice(0, 80) || ''; const resultContent = tc.result?.content || ''; const subagentId = tc.subagent?.agent_id || null; return `
` : ''}
${parsed.prompt ? `
Prompt
${escapeHTML(parsed.prompt.slice(0, 500))}${parsed.prompt.length > 500 ? '…' : ''}
` : ''} ${resultContent ? `
Result
${renderMarkdown(resultContent, { variant: 'compact' })}
` : ''}
`; } // Special rendering for Workflow tool calls if (tc.name === 'Workflow') { let parsed = {}; try { parsed = JSON.parse(tc.input_json || '{}'); } catch {} const wf = tc.workflow; const wfName = wf?.workflow_name || parsed.name || 'Workflow'; const wfStatus = wf?.status || ''; const agents = wf?.agents || []; // Group agents by phase const phases = {}; for (const a of agents) { const phase = a.phase || 'Other'; if (!phases[phase]) phases[phase] = []; phases[phase].push(a); } const phasesHTML = Object.entries(phases).map(([phase, agentList]) => `
${escapeHTML(phase)}
${agentList.map(a => ` `).join('')}
`).join(''); const agentListHTML = agents.length ? `
Agents · ${agents.length}
${phasesHTML}
` : ''; return `
${agentListHTML}
`; } let argPreview = ''; try { const j = JSON.parse(tc.input_json || '{}'); if (j.file_path) argPreview = j.file_path; else if (j.command) argPreview = j.command; else if (j.path) argPreview = j.path; else if (j.description) argPreview = j.description; else argPreview = JSON.stringify(j).slice(0, 100); } catch { argPreview = (tc.input_json || '').slice(0, 100); } return `
Input
${escapeHTML(tc.input_json || '')}
${tc.result ? `
${isError ? 'Error' : 'Output'}
${escapeHTML(tc.result.content || '(empty)')}
` : ''}
`; } // --- Private helper: visibleSessions (same logic as render.js) --- function visibleSessions() { const q = state.query.trim().toLowerCase(); return state.sessions .filter(s => state.projectFilter === 'all' || s.project === state.projectFilter) .map(s => { if (!q) return { ...s, messageHit: null }; const topMatch = (s.title || '').toLowerCase().includes(q) || (s.project || '').toLowerCase().includes(q) || (s.git_branch || '').toLowerCase().includes(q); if (topMatch) return { ...s, messageHit: null }; return null; }) .filter(Boolean) .sort((a, b) => { const ta = new Date(a.started_at || 0).getTime(); const tb = new Date(b.started_at || 0).getTime(); return state.sortDesc ? tb - ta : ta - tb; }); }