Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions internal/parser/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions internal/parser/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"`
Expand Down
118 changes: 88 additions & 30 deletions internal/web/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -705,8 +705,11 @@
}

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 {
Expand All @@ -732,45 +735,93 @@
"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(`<details class="sidechain-group" id="sidechain-%s">`, html.EscapeString(g.AgentID)))
b.WriteString(fmt.Sprintf(`<summary class="sidechain-header"><span class="sidechain-icon">◆</span> %s <span class="sidechain-meta">%d messages, %d tools</span></summary>`,
html.EscapeString(label), msgCount, toolCount))
b.WriteString(fmt.Sprintf(`<summary class="sidechain-header"><span class="sidechain-icon">◆</span> %s <span class="sidechain-meta">%s</span></summary>`,
html.EscapeString(label), html.EscapeString(meta.String())))
b.WriteString(`<div class="sidechain-body">`)

toolResults := buildToolResultsMap(g.Messages)
Expand All @@ -784,7 +835,7 @@
b.WriteString(`</div></details>`)
}

func sidechainSnippet(msgs []*parser.Message) string {

Check failure on line 838 in internal/web/templates.go

View workflow job for this annotation

GitHub Actions / lint

func `sidechainSnippet` is unused (unused)
for _, m := range msgs {
if m.Kind != parser.KindUserPrompt {
continue
Expand Down Expand Up @@ -1925,7 +1976,12 @@
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
Expand Down Expand Up @@ -2036,14 +2092,16 @@
}
}

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(`<a href="#sidechain-%s" class="nav-item nav-sidechain" data-msg="sidechain-%s">`,
html.EscapeString(g.AgentID), html.EscapeString(g.AgentID)))
Expand Down
Loading