@@ -665,6 +665,25 @@ function addToolCall(name, data) {
665665 scrollBottom ( ) ;
666666}
667667
668+ // appendToolResultContent renders a tool result into a block, truncating
669+ // long output behind a "show all" expander. Shared by the live path
670+ // (addToolResult) and session-history rendering.
671+ function appendToolResultContent ( block , output ) {
672+ const MAX_RESULT = 600 ;
673+ const truncated = output && output . length > MAX_RESULT ;
674+ const resultEl = document . createElement ( 'div' ) ;
675+ resultEl . className = 'tb-result' ;
676+ block . appendChild ( resultEl ) ;
677+ if ( truncated ) {
678+ resultEl . innerHTML =
679+ escapeHtml ( output . slice ( 0 , MAX_RESULT ) ) +
680+ '<span class="tb-result-more" onclick="expandToolResult(this)" data-full="' +
681+ escapeAttr ( output ) + '"> …show all (' + output . length + ' chars)</span>' ;
682+ } else {
683+ resultEl . textContent = output || '' ;
684+ }
685+ }
686+
668687function addToolResult ( name , output ) {
669688 // Route to the matching pending block via FIFO queue.
670689 const queue = toolBlockQueues . get ( name ) ;
@@ -682,23 +701,7 @@ function addToolResult(name, output) {
682701 if ( latEl ) latEl . textContent = ms < 1000 ? Math . round ( ms ) + 'ms' : ( ms / 1000 ) . toFixed ( 1 ) + 's' ;
683702 }
684703
685- // Show result, truncated if long.
686- const MAX_RESULT = 600 ;
687- const truncated = output && output . length > MAX_RESULT ;
688- let resultEl = block . querySelector ( '.tb-result' ) ;
689- if ( ! resultEl ) {
690- resultEl = document . createElement ( 'div' ) ;
691- resultEl . className = 'tb-result' ;
692- block . appendChild ( resultEl ) ;
693- }
694- if ( truncated ) {
695- resultEl . innerHTML =
696- escapeHtml ( output . slice ( 0 , MAX_RESULT ) ) +
697- '<span class="tb-result-more" onclick="expandToolResult(this)" data-full="' +
698- escapeAttr ( output ) + '"> …show all (' + output . length + ' chars)</span>' ;
699- } else {
700- resultEl . textContent = output || '' ;
701- }
704+ appendToolResultContent ( block , output || '' ) ;
702705 scrollBottom ( ) ;
703706}
704707
@@ -759,13 +762,11 @@ function addSubagentGroup(command) {
759762 scrollBottom ( ) ;
760763}
761764
762- function completeSubagents ( output ) {
763- if ( ! subagentGroup ) return ;
764-
765- // Parse sub-agent results from the output text
766- // The delegate_tasks tool returns formatted text like:
767- // "📋 Sub-agent results:\n\n─── Task 1: goal ───\n{json}\n\n─── Task 2: ..."
768- const lines = output . split ( '\n' ) ;
765+ // parseSubagentResults parses delegate_tasks output text of the form
766+ // "📋 Sub-agent results:\n\n─── Task 1: goal ───\n{json}\n\n─── Task 2: ..."
767+ // into a map of task index → parsed result object.
768+ function parseSubagentResults ( output ) {
769+ const lines = ( output || '' ) . split ( '\n' ) ;
769770 let currentTaskIdx = - 1 ;
770771 const taskResults = { } ;
771772
@@ -790,49 +791,60 @@ function completeSubagents(output) {
790791 }
791792 }
792793 }
794+ return taskResults ;
795+ }
793796
794- const cards = subagentGroup . querySelectorAll ( '.subagent-card' ) ;
795- cards . forEach ( ( card , i ) => {
796- const result = taskResults [ i ] ;
797- card . querySelector ( '.sa-icon' ) . textContent = '✓' ;
798- card . classList . remove ( 'running' ) ;
799- card . querySelector ( '.sa-status' ) . textContent = 'done' ;
800-
801- if ( result ) {
802- const status = result . status || 'success' ;
803- if ( status === 'error' ) {
804- card . classList . add ( 'error' ) ;
805- card . querySelector ( '.sa-icon' ) . textContent = '✗' ;
806- card . querySelector ( '.sa-status' ) . textContent = 'error' ;
807- } else {
808- card . classList . add ( 'completed' ) ;
809- }
797+ // finalizeSubagentCard marks a card done/error and fills its details from
798+ // the parsed result. Shared by the live path (completeSubagents) and
799+ // session-history rendering.
800+ function finalizeSubagentCard ( card , result ) {
801+ card . querySelector ( '.sa-icon' ) . textContent = '✓' ;
802+ card . classList . remove ( 'running' ) ;
803+ card . querySelector ( '.sa-status' ) . textContent = 'done' ;
810804
811- // Auto-open details for error or when there's a summary
812- const details = card . querySelector ( '.sa-details' ) ;
813- const summary = result . summary || '' ;
814- const files = result . files_changed || [ ] ;
815- const tokens = result . tokens_used || 0 ;
816- const iters = result . iterations || 0 ;
805+ if ( ! result ) {
806+ card . classList . add ( 'completed' ) ;
807+ return ;
808+ }
817809
818- if ( summary || files . length > 0 ) {
819- const meta = details . querySelector ( '.sa-meta' ) ;
820- if ( tokens ) meta . textContent = tokens + ' tokens' + ( iters ? ' · ' + iters + ' iters' : '' ) ;
810+ const status = result . status || 'success' ;
811+ if ( status === 'error' ) {
812+ card . classList . add ( 'error' ) ;
813+ card . querySelector ( '.sa-icon' ) . textContent = '✗' ;
814+ card . querySelector ( '.sa-status' ) . textContent = 'error' ;
815+ } else {
816+ card . classList . add ( 'completed' ) ;
817+ }
821818
822- const summaryEl = details . querySelector ( '.sa-summary' ) ;
823- summaryEl . textContent = typeof summary === 'string' ? summary : '' ;
819+ // Auto-open details for error or when there's a summary
820+ const details = card . querySelector ( '.sa-details' ) ;
821+ const summary = result . summary || '' ;
822+ const files = result . files_changed || [ ] ;
823+ const tokens = result . tokens_used || 0 ;
824+ const iters = result . iterations || 0 ;
824825
825- if ( files . length > 0 ) {
826- const filesEl = details . querySelector ( '.sa-files' ) ;
827- filesEl . innerHTML = files . map ( f => '<span class="file-chip"><span class="icon">📄</span>' + escapeHtml ( f ) + '</span>' ) . join ( '' ) ;
828- }
826+ if ( summary || files . length > 0 ) {
827+ const meta = details . querySelector ( '.sa-meta' ) ;
828+ if ( tokens ) meta . textContent = tokens + ' tokens' + ( iters ? ' · ' + iters + ' iters' : '' ) ;
829829
830- details . classList . add ( 'open' ) ;
831- }
832- } else {
833- card . classList . add ( 'completed' ) ;
830+ const summaryEl = details . querySelector ( '.sa-summary' ) ;
831+ summaryEl . textContent = typeof summary === 'string' ? summary : '' ;
832+
833+ if ( files . length > 0 ) {
834+ const filesEl = details . querySelector ( '.sa-files' ) ;
835+ filesEl . innerHTML = files . map ( f => '<span class="file-chip"><span class="icon">📄</span>' + escapeHtml ( f ) + '</span>' ) . join ( '' ) ;
834836 }
835- } ) ;
837+
838+ details . classList . add ( 'open' ) ;
839+ }
840+ }
841+
842+ function completeSubagents ( output ) {
843+ if ( ! subagentGroup ) return ;
844+
845+ const taskResults = parseSubagentResults ( output ) ;
846+ const cards = subagentGroup . querySelectorAll ( '.subagent-card' ) ;
847+ cards . forEach ( ( card , i ) => finalizeSubagentCard ( card , taskResults [ i ] ) ) ;
836848
837849 pruneMessages ( ) ;
838850 scrollBottom ( ) ;
@@ -1885,6 +1897,117 @@ sessionListEl.addEventListener('click', (e) => {
18851897 loadAndRenderSession ( sid ) ;
18861898} ) ;
18871899
1900+ // ── Session history rendering ──
1901+ // Renders the full persisted transcript on session load: user/assistant
1902+ // text, thinking (reasoning_content), tool calls with their results, and
1903+ // delegate_tasks sub-agent groups. Previously only user/assistant text was
1904+ // re-rendered, so a reloaded session silently dropped most of what happened.
1905+ function renderSessionHistory ( messages ) {
1906+ // Index tool results by call id for matching against assistant tool_calls.
1907+ const resultsById = new Map ( ) ;
1908+ messages . forEach ( m => {
1909+ if ( m . role === 'tool' && m . tool_call_id ) resultsById . set ( m . tool_call_id , m . content || '' ) ;
1910+ } ) ;
1911+
1912+ let toolDividerShown = false ;
1913+ messages . forEach ( msg => {
1914+ if ( msg . role === 'user' ) {
1915+ addMessage ( 'user' , stripAttachmentBodies ( msg . content || '' ) ) ;
1916+ toolDividerShown = false ;
1917+ return ;
1918+ }
1919+ if ( msg . role !== 'assistant' ) return ; // skip system/tool internals
1920+
1921+ if ( msg . reasoning_content ) renderHistoricalThinking ( msg . reasoning_content ) ;
1922+
1923+ const toolCalls = Array . isArray ( msg . tool_calls ) ? msg . tool_calls : [ ] ;
1924+ if ( msg . content ) {
1925+ renderAssistantMessage ( msg . content ) ;
1926+ }
1927+ if ( toolCalls . length > 0 ) {
1928+ if ( ! toolDividerShown ) {
1929+ addDivider ( 'tool calls' ) ;
1930+ toolDividerShown = true ;
1931+ }
1932+ toolCalls . forEach ( tc => {
1933+ const name = ( tc . function && tc . function . name ) || 'tool' ;
1934+ const args = ( tc . function && tc . function . arguments ) || '' ;
1935+ const result = resultsById . get ( tc . id ) || '' ;
1936+ if ( name === 'delegate_tasks' ) {
1937+ renderHistoricalSubagents ( args , result ) ;
1938+ } else {
1939+ renderHistoricalToolBlock ( name , args , result ) ;
1940+ }
1941+ } ) ;
1942+ }
1943+ } ) ;
1944+ }
1945+
1946+ // renderHistoricalThinking renders a completed reasoning block (collapsed).
1947+ function renderHistoricalThinking ( content ) {
1948+ const block = document . createElement ( 'div' ) ;
1949+ block . className = 'thinking-block' ;
1950+ block . innerHTML =
1951+ '<div class="thinking-toggle" onclick="toggleThinking(this)">' +
1952+ '<span class="arrow">▶</span> reasoning' +
1953+ '</div>' +
1954+ '<div class="thinking-content">' + escapeHtml ( content ) + '</div>' ;
1955+ messagesEl . appendChild ( block ) ;
1956+ }
1957+
1958+ // renderHistoricalToolBlock renders a completed tool call with its result
1959+ // (no spinner, no latency — those are live-turn concerns).
1960+ function renderHistoricalToolBlock ( name , args , result ) {
1961+ const preview = buildToolPreview ( name , args ) ;
1962+ const el = document . createElement ( 'div' ) ;
1963+ el . className = 'tool-block' ;
1964+ el . innerHTML =
1965+ '<div class="tb-header" onclick="toggleToolBody(this)">' +
1966+ '<span class="arrow">▶</span>' +
1967+ ' <span class="tb-emoji">' + toolEmoji ( name ) + '</span>' +
1968+ ' <span class="tb-name">' + escapeHtml ( name ) + '</span>' +
1969+ ( preview ? ' <span class="tb-preview">' + escapeHtml ( preview ) + '</span>' : '' ) +
1970+ '</div>' +
1971+ '<div class="tb-body">' + escapeHtml ( formatToolArgs ( args ) ) + '</div>' ;
1972+ messagesEl . appendChild ( el ) ;
1973+ if ( result ) appendToolResultContent ( el , result ) ;
1974+ }
1975+
1976+ // renderHistoricalSubagents renders a completed delegate_tasks group with
1977+ // per-task final states parsed from the tool result.
1978+ function renderHistoricalSubagents ( args , output ) {
1979+ addDivider ( 'delegated tasks' ) ;
1980+
1981+ let tasks = [ ] ;
1982+ try { tasks = JSON . parse ( args ) . tasks || [ ] ; } catch { tasks = [ ] ; }
1983+ const taskResults = parseSubagentResults ( output ) ;
1984+
1985+ const group = document . createElement ( 'div' ) ;
1986+ group . className = 'subagent-group' ;
1987+ group . innerHTML = '<div class="sg-header">Sub-agents</div><div class="subagent-grid"></div>' ;
1988+ messagesEl . appendChild ( group ) ;
1989+ const grid = group . querySelector ( '.subagent-grid' ) ;
1990+
1991+ tasks . forEach ( ( task , i ) => {
1992+ const card = document . createElement ( 'div' ) ;
1993+ card . className = 'subagent-card running' ;
1994+ card . dataset . index = i ;
1995+ card . innerHTML =
1996+ '<div class="sa-top">' +
1997+ '<div class="sa-icon">⟳</div>' +
1998+ '<div class="sa-goal" title="' + escapeAttr ( task . goal || 'Task ' + ( i + 1 ) ) + '">' + escapeHtml ( task . goal || 'Task ' + ( i + 1 ) ) + '</div>' +
1999+ '<div class="sa-status">running</div>' +
2000+ '</div>' +
2001+ '<div class="sa-details" onclick="toggleSaDetails(this)">' +
2002+ '<div class="sa-meta"></div>' +
2003+ '<div class="sa-summary"></div>' +
2004+ '<div class="sa-files"></div>' +
2005+ '</div>' ;
2006+ grid . appendChild ( card ) ;
2007+ finalizeSubagentCard ( card , taskResults [ i ] ) ;
2008+ } ) ;
2009+ }
2010+
18882011async function loadAndRenderSession ( sid ) {
18892012 try {
18902013 let token = getSessionToken ( sid ) ;
@@ -1916,7 +2039,8 @@ async function loadAndRenderSession(sid) {
19162039 if ( savedScrollBtnNode ) messagesEl . appendChild ( savedScrollBtnNode ) ;
19172040
19182041 const messages = sess . messages || [ ] ;
1919- // Only render user and assistant messages; skip system/tool internals.
2042+ // The full transcript renders now (tool calls, thinking, sub-agents);
2043+ // the empty check still keys on conversational messages only.
19202044 const visible = messages . filter ( m => m . role === 'user' || m . role === 'assistant' ) ;
19212045
19222046 if ( visible . length === 0 ) {
@@ -1925,13 +2049,7 @@ async function loadAndRenderSession(sid) {
19252049 return ;
19262050 }
19272051
1928- visible . forEach ( msg => {
1929- if ( msg . role === 'user' ) {
1930- addMessage ( 'user' , stripAttachmentBodies ( msg . content || '' ) ) ;
1931- } else if ( msg . role === 'assistant' && msg . content ) {
1932- renderAssistantMessage ( msg . content ) ;
1933- }
1934- } ) ;
2052+ renderSessionHistory ( messages ) ;
19352053
19362054 forceScrollBottom ( ) ;
19372055 showToast ( 'Session loaded' ) ;
0 commit comments