Skip to content
Merged
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
72 changes: 72 additions & 0 deletions tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,31 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(listenForEvents(m.eventCh), tickCmd())
}
}
// No matching start entry by callID — try matching by tool name
// as a fallback for the most recent running entry of the same type.
toolName := getToolNameFromEventContent(msg.event.Content)
if toolName != "" {
if matchedID, matchedEntry := findRunningEntryByName(m.toolCallEntries, toolName); matchedEntry != nil {
resultContent := getResultFromEventContent(msg.event.Content)
isError := strings.HasPrefix(resultContent, "Error:")
matchedEntry.SetResult(tui.ToolResultInfo{
ToolCallID: matchedID,
Name: matchedEntry.Call.Name,
Content: resultContent,
IsError: isError,
})
if idx := findLogEntryByToolCallID(m.logEntries, matchedID); idx >= 0 {
le := &m.logEntries[idx]
le.content = resultContent
le.isToolRunning = false
le.rendered = ""
}
delete(m.toolCallEntries, matchedID)
m.updateActiveAnim()
m.buildViewportContent()
return m, tea.Batch(listenForEvents(m.eventCh), tickCmd())
}
}
// No matching start entry — add as standalone
}

Expand Down Expand Up @@ -1205,6 +1230,30 @@ func formatEventAsEntry(event *messaging.MessageEvent) logEntry {
if entry.content == "" {
entry.content = fmt.Sprintf("%v", event.Content)
}
// Create a ToolEntry for standalone results so they use new-style
// rendering (✓ read_file · /path/to/file) instead of the legacy
// emoji path (✅ read_file).
isErr := strings.HasPrefix(entry.content, "Error:")
entry.toolEntry = tui.NewToolEntry(tui.ToolCallInfo{
ID: entry.toolCallID,
Name: entry.toolName,
Summary: extractToolSummary(entry.toolName, ""),
})
if isErr {
Comment on lines +1236 to +1242
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The ToolResultInfo construction is duplicated and can be simplified using the existing isErr variable.

Since isErr is already computed, you can avoid duplicating the ToolResultInfo literal in both branches by constructing it once, setting IsError: isErr, and calling SetResult a single time. This removes repetition and makes future changes to the struct safer.

entry.toolEntry.SetResult(tui.ToolResultInfo{
ToolCallID: entry.toolCallID,
Name: entry.toolName,
Content: entry.content,
IsError: true,
})
} else {
entry.toolEntry.SetResult(tui.ToolResultInfo{
ToolCallID: entry.toolCallID,
Name: entry.toolName,
Content: entry.content,
IsError: false,
})
}
case "user_help_needed":
if s, ok := event.Content.(string); ok {
entry.content = "HELP: " + s
Expand Down Expand Up @@ -1799,6 +1848,29 @@ func getToolCallIDFromEventContent(content interface{}) string {
return ""
}

// getToolNameFromEventContent extracts tool_name from event content.
func getToolNameFromEventContent(content interface{}) string {
if m, ok := content.(map[string]interface{}); ok {
if name, ok := m["tool_name"]; ok {
if nameStr, ok := name.(string); ok {
return nameStr
}
}
}
return ""
}

// findRunningEntryByName finds the most recently-added running entry with the
// given tool name in the toolCallEntries map. Returns the call ID and the entry.
func findRunningEntryByName(entries map[string]*tui.ToolEntry, toolName string) (string, *tui.ToolEntry) {
for id, entry := range entries {
Comment on lines +1863 to +1866
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Iterating a map does not guarantee returning the most recently-added running entry, conflicting with the function’s documented behavior.

Because Go map iteration order is random, findRunningEntryByName may return any matching running entry when multiple calls with the same tool name are active. If true recency is required, consider tracking order explicitly (e.g., an ordered slice of IDs or a timestamp on ToolEntry) so you can deterministically select the newest entry, or adjust the contract/callers if any matching running entry is acceptable.

if entry.Call.Name == toolName && entry.Status == tui.ToolStatusRunning {
return id, entry
}
}
return "", nil
}

// getResultFromEventContent extracts the result string from event content.
func getResultFromEventContent(content interface{}) string {
if m, ok := content.(map[string]interface{}); ok {
Expand Down