Skip to content

Commit f0724d0

Browse files
authored
Merge pull request #1412 from krissetto/collapsible-reasoning
Collapsible reasoning in TUI
2 parents b6242d1 + 02f95f5 commit f0724d0

File tree

8 files changed

+1366
-133
lines changed

8 files changed

+1366
-133
lines changed

pkg/runtime/runtime.go

Lines changed: 29 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,8 +1033,13 @@ func (r *LocalRuntime) handleStream(ctx context.Context, stream chat.MessageStre
10331033
var actualModelEventEmitted bool
10341034
var messageUsage *chat.Usage
10351035
modelID := getAgentModelID(a)
1036-
// Track which tool call indices we've already emitted partial events for
1037-
emittedPartialEvents := make(map[string]bool)
1036+
1037+
toolCallIndex := make(map[string]int) // toolCallID -> index in toolCalls slice
1038+
emittedPartial := make(map[string]bool) // toolCallID -> whether we've emitted a partial event
1039+
toolDefMap := make(map[string]tools.Tool, len(agentTools))
1040+
for _, t := range agentTools {
1041+
toolDefMap[t.Name] = t
1042+
}
10381043

10391044
for {
10401045
response, err := stream.Recv()
@@ -1103,63 +1108,39 @@ func (r *LocalRuntime) handleStream(ctx context.Context, stream chat.MessageStre
11031108
// Handle tool calls
11041109
if len(choice.Delta.ToolCalls) > 0 {
11051110
// Process each tool call delta
1106-
for _, deltaToolCall := range choice.Delta.ToolCalls {
1107-
// Find existing tool call by ID, or create a new one
1108-
idx := -1
1109-
for i, toolCall := range toolCalls {
1110-
if toolCall.ID == deltaToolCall.ID {
1111-
idx = i
1112-
break
1113-
}
1114-
}
1115-
1116-
// If tool call doesn't exist yet, append it
1117-
if idx == -1 {
1111+
for _, delta := range choice.Delta.ToolCalls {
1112+
idx, exists := toolCallIndex[delta.ID]
1113+
if !exists {
11181114
idx = len(toolCalls)
1115+
toolCallIndex[delta.ID] = idx
11191116
toolCalls = append(toolCalls, tools.ToolCall{
1120-
ID: deltaToolCall.ID,
1121-
Type: deltaToolCall.Type,
1117+
ID: delta.ID,
1118+
Type: delta.Type,
11221119
})
11231120
}
11241121

1125-
// Check if we should emit a partial event for this tool call
1126-
// We want to emit when we first get the function name
1127-
shouldEmitPartial := !emittedPartialEvents[deltaToolCall.ID] &&
1128-
deltaToolCall.Function.Name != "" &&
1129-
toolCalls[idx].Function.Name == "" // Don't emit if we already have the name
1122+
tc := &toolCalls[idx]
11301123

1131-
// Update fields based on what's in the delta
1132-
if deltaToolCall.ID != "" {
1133-
toolCalls[idx].ID = deltaToolCall.ID
1134-
}
1135-
if deltaToolCall.Type != "" {
1136-
toolCalls[idx].Type = deltaToolCall.Type
1124+
// Track if we're learning the name for the first time
1125+
learningName := delta.Function.Name != "" && tc.Function.Name == ""
1126+
1127+
// Update fields from delta
1128+
if delta.Type != "" {
1129+
tc.Type = delta.Type
11371130
}
1138-
if deltaToolCall.Function.Name != "" {
1139-
toolCalls[idx].Function.Name = deltaToolCall.Function.Name
1131+
if delta.Function.Name != "" {
1132+
tc.Function.Name = delta.Function.Name
11401133
}
1141-
if deltaToolCall.Function.Arguments != "" {
1142-
if toolCalls[idx].Function.Arguments == "" {
1143-
toolCalls[idx].Function.Arguments = deltaToolCall.Function.Arguments
1144-
} else {
1145-
toolCalls[idx].Function.Arguments += deltaToolCall.Function.Arguments
1146-
}
1147-
// Emit if we get more arguments
1148-
shouldEmitPartial = true
1134+
if delta.Function.Arguments != "" {
1135+
tc.Function.Arguments += delta.Function.Arguments
11491136
}
11501137

1151-
// Emit PartialToolCallEvent when we first get the function name
1152-
if shouldEmitPartial {
1153-
// TODO: clean this up, it's gross
1154-
tool := tools.Tool{}
1155-
for _, t := range agentTools {
1156-
if t.Name == toolCalls[idx].Function.Name {
1157-
tool = t
1158-
break
1159-
}
1138+
// Emit PartialToolCall once we have a name, and on subsequent argument deltas
1139+
if tc.Function.Name != "" && (learningName || delta.Function.Arguments != "") {
1140+
if !emittedPartial[delta.ID] || delta.Function.Arguments != "" {
1141+
events <- PartialToolCall(*tc, toolDefMap[tc.Function.Name], a.Name())
1142+
emittedPartial[delta.ID] = true
11601143
}
1161-
events <- PartialToolCall(toolCalls[idx], tool, a.Name())
1162-
emittedPartialEvents[deltaToolCall.ID] = true
11631144
}
11641145
}
11651146
continue

pkg/tui/components/message/message.go

Lines changed: 1 addition & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -108,40 +108,6 @@ func (mv *messageModel) Render(width int) string {
108108
}
109109

110110
return mv.senderPrefix(msg.Sender) + messageStyle.Render(rendered)
111-
case types.MessageTypeAssistantReasoning:
112-
if msg.Content == "" {
113-
return mv.spinner.View()
114-
}
115-
116-
messageStyle := styles.AssistantMessageStyle
117-
thinkingStyle := styles.MutedStyle.Italic(true)
118-
119-
rendered, err := markdown.NewRenderer(width - messageStyle.GetHorizontalFrameSize()).Render(msg.Content)
120-
if err != nil {
121-
rendered = msg.Content
122-
}
123-
124-
// Strip ANSI so muted style applies uniformly, and trim trailing whitespace.
125-
// Unlike regular content where markdown ANSI output goes directly to messageStyle,
126-
// here we strip ANSI which exposes raw trailing newlines from markdown that would
127-
// otherwise be handled differently by lipgloss when embedded in ANSI sequences.
128-
clean := strings.TrimRight(stripANSI(rendered), "\n\r\t ")
129-
130-
// Show "Thinking:" badge only when starting a new thinking block after content
131-
var text string
132-
if mv.continuingThinking(msg) {
133-
text = thinkingStyle.Render(clean)
134-
} else {
135-
text = styles.ThinkingBadgeStyle.Render("Thinking:") + "\n\n" + thinkingStyle.Render(clean)
136-
}
137-
138-
styledContent := messageStyle.Render(text)
139-
140-
if mv.sameAgentAsPrevious(msg) {
141-
return styledContent
142-
}
143-
144-
return mv.senderPrefix(msg.Sender) + styledContent
145111
case types.MessageTypeShellOutput:
146112
if rendered, err := markdown.NewRenderer(width).Render(fmt.Sprintf("```console\n%s\n```", msg.Content)); err == nil {
147113
return rendered
@@ -187,23 +153,7 @@ func (mv *messageModel) sameAgentAsPrevious(msg *types.Message) bool {
187153
}
188154
switch mv.previous.Type {
189155
case types.MessageTypeAssistant,
190-
types.MessageTypeAssistantReasoning,
191-
types.MessageTypeToolCall,
192-
types.MessageTypeToolResult:
193-
return true
194-
default:
195-
return false
196-
}
197-
}
198-
199-
// continuingThinking returns true if we're continuing a thinking flow
200-
// (previous was thinking or tool call, not content)
201-
func (mv *messageModel) continuingThinking(msg *types.Message) bool {
202-
if mv.previous == nil || mv.previous.Sender != msg.Sender {
203-
return false
204-
}
205-
switch mv.previous.Type {
206-
case types.MessageTypeAssistantReasoning,
156+
types.MessageTypeAssistantReasoningBlock,
207157
types.MessageTypeToolCall,
208158
types.MessageTypeToolResult:
209159
return true

0 commit comments

Comments
 (0)