From 72243ba375bde61ce97368e2dd86ccb75b61a2a7 Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Sat, 9 May 2026 02:39:55 -0700 Subject: [PATCH] feat(web): show sub-agent name, type, and stats in sidechain headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sidechain headers now display the agent type name (Explore, linus-rants, xmn, etc.) from the Agent tool_use subagent_type field, plus the description. When toolUseResult is available, the header shows token count, tool call count, duration, and lines added — replacing the generic "N messages, N tools" summary. Outline nav entries also show agent type + description instead of generic "Sub-agent N" labels. Export SubAgentResultData type so the web package can read it. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/parser/session.go | 4 +- internal/parser/types.go | 4 +- internal/web/templates.go | 118 +++++++++++++++++++++++++++---------- 3 files changed, 92 insertions(+), 34 deletions(-) diff --git a/internal/parser/session.go b/internal/parser/session.go index 70a8be5..80cd167 100644 --- a/internal/parser/session.go +++ b/internal/parser/session.go @@ -443,11 +443,11 @@ func extractSummary(messages []*Message) string { return "(no summary)" } -func parseToolUseResult(raw json.RawMessage) *toolUseResultData { +func parseToolUseResult(raw json.RawMessage) *SubAgentResultData { if len(raw) == 0 || raw[0] != '{' { return nil } - var result toolUseResultData + var result SubAgentResultData if err := json.Unmarshal(raw, &result); err != nil { return nil } diff --git a/internal/parser/types.go b/internal/parser/types.go index 0855478..deb1ead 100644 --- a/internal/parser/types.go +++ b/internal/parser/types.go @@ -129,7 +129,7 @@ type Message struct { Subtype string // For system messages: compact_boundary, local_command Usage *MessageUsage // Per-message API token usage + cost. nil if absent. - SubAgentResult *toolUseResultData // Sub-agent summary from toolUseResult. nil for non-agent tool results. + SubAgentResult *SubAgentResultData // Sub-agent summary from toolUseResult. nil for non-agent tool results. RawJSON string // Original JSONL line verbatim raw rawMessage @@ -181,7 +181,7 @@ type rawMessage struct { ToolUseResult json.RawMessage `json:"toolUseResult"` // Sub-agent result metadata (string or object) } -type toolUseResultData struct { +type SubAgentResultData struct { AgentID string `json:"agentId"` AgentType string `json:"agentType"` Status string `json:"status"` diff --git a/internal/web/templates.go b/internal/web/templates.go index f7ac732..fea7268 100644 --- a/internal/web/templates.go +++ b/internal/web/templates.go @@ -705,8 +705,11 @@ func buildToolResultsMap(messages []*parser.Message) map[string]parser.ContentBl } type sidechainGroup struct { - AgentID string - Messages []*parser.Message + AgentID string + AgentType string + Description string + Messages []*parser.Message + Result *parser.SubAgentResultData // from toolUseResult on the tool_result message } func groupSidechainsByAgent(allMsgs []*parser.Message) []sidechainGroup { @@ -732,45 +735,93 @@ var subagentToolSet = map[string]bool{ "Task": true, "TaskCreate": true, "Agent": true, } +type agentDispatch struct { + ToolID string + AgentType string + Description string +} + func matchSidechainsToToolUse(mainMsgs []*parser.Message, groups []sidechainGroup) map[string]sidechainGroup { if len(groups) == 0 { return nil } - var dispatchIDs []string + + var dispatches []agentDispatch + resultByToolID := make(map[string]*parser.SubAgentResultData) + for _, msg := range mainMsgs { for _, block := range msg.Content { if block.Type == "tool_use" && subagentToolSet[block.ToolName] { - dispatchIDs = append(dispatchIDs, block.ToolID) + d := agentDispatch{ToolID: block.ToolID} + if m, ok := block.ToolInput.(map[string]any); ok { + d.AgentType, _ = m["subagent_type"].(string) + d.Description, _ = m["description"].(string) + } + dispatches = append(dispatches, d) + } + } + if msg.SubAgentResult != nil { + for _, block := range msg.Content { + if block.Type == "tool_result" { + resultByToolID[block.ToolID] = msg.SubAgentResult + } } } } - result := make(map[string]sidechainGroup, len(groups)) - for i, id := range dispatchIDs { + + out := make(map[string]sidechainGroup, len(groups)) + for i, d := range dispatches { if i < len(groups) { - result[id] = groups[i] + g := groups[i] + g.AgentType = d.AgentType + g.Description = d.Description + g.Result = resultByToolID[d.ToolID] + out[d.ToolID] = g } } - return result + return out } func renderInlineSidechain(b *strings.Builder, g sidechainGroup, showThinking, showTools bool) { - snippet := sidechainSnippet(g.Messages) - label := "Sub-agent" - if snippet != "" { - label += ": " + snippet - } - msgCount := len(g.Messages) - toolCount := 0 - for _, m := range g.Messages { - for _, block := range m.Content { - if block.Type == "tool_use" { - toolCount++ + label := "Agent" + if g.AgentType != "" { + label = g.AgentType + } + if g.Description != "" { + label += ": " + g.Description + } + + var meta strings.Builder + if r := g.Result; r != nil { + if r.TotalTokens > 0 { + fmt.Fprintf(&meta, "%s tokens", formatTokens(r.TotalTokens)) + } + if r.TotalToolUseCount > 0 { + if meta.Len() > 0 { + meta.WriteString(", ") + } + fmt.Fprintf(&meta, "%d tools", r.TotalToolUseCount) + } + if r.TotalDurationMs > 0 { + if meta.Len() > 0 { + meta.WriteString(", ") + } + fmt.Fprintf(&meta, "%s", formatDuration(float64(r.TotalDurationMs)/1000)) + } + if r.ToolStats != nil && r.ToolStats.LinesAdded > 0 { + if meta.Len() > 0 { + meta.WriteString(", ") } + fmt.Fprintf(&meta, "+%d lines", r.ToolStats.LinesAdded) } } + if meta.Len() == 0 { + fmt.Fprintf(&meta, "%d messages", len(g.Messages)) + } + b.WriteString(fmt.Sprintf(`
`, html.EscapeString(g.AgentID))) - b.WriteString(fmt.Sprintf(` %s %d messages, %d tools`, - html.EscapeString(label), msgCount, toolCount)) + b.WriteString(fmt.Sprintf(` %s %s`, + html.EscapeString(label), html.EscapeString(meta.String()))) b.WriteString(`
`) toolResults := buildToolResultsMap(g.Messages) @@ -1925,7 +1976,12 @@ func renderSpendSection(turns []*parser.Exchange, sessionTotal float64) string { func renderConversationNav(b *strings.Builder, messages []*parser.Message) { flat := flattenMessages(messages) allMsgs := filterMainConversation(flat) - sidechains := groupSidechainsByAgent(flat) + scGroups := groupSidechainsByAgent(flat) + scMap := matchSidechainsToToolUse(allMsgs, scGroups) + var sidechains []sidechainGroup + for _, g := range scMap { + sidechains = append(sidechains, g) + } type navGroup struct { user *parser.Message @@ -2036,14 +2092,16 @@ func renderConversationNav(b *strings.Builder, messages []*parser.Message) { } } - for i, g := range sidechains { - snippet := sidechainSnippet(g.Messages) - label := fmt.Sprintf("Sub-agent %d", i+1) - if snippet != "" { - label = snippet - if len(label) > 30 { - label = label[:27] + "..." - } + for _, g := range sidechains { + label := g.AgentType + if label == "" { + label = "Agent" + } + if g.Description != "" { + label += ": " + g.Description + } + if len(label) > 35 { + label = label[:32] + "..." } b.WriteString(fmt.Sprintf(``, html.EscapeString(g.AgentID), html.EscapeString(g.AgentID)))