Skip to content

Commit 0ca631c

Browse files
committed
Filter out <think> reasoning blocks from streaming output
Strips <think>...</think> tags (used by MiniMax, DeepSeek, etc.) from streamed text deltas and shows a "Thinking..." notification while inside a reasoning block. Made-with: Cursor
1 parent 455438e commit 0ca631c

1 file changed

Lines changed: 51 additions & 1 deletion

File tree

chat.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ type chatModel struct {
6464
toolsPending int // tools currently running
6565
toolsDone int // tools completed this turn
6666
lastStreamRebuild time.Time // debounce streaming viewport rebuilds
67+
inThink bool // inside <think> tags (MiniMax reasoning)
6768

6869
// Glue + notifications
6970
glue *GlueClient
@@ -761,7 +762,35 @@ func (m *chatModel) handleAgentEvent(evt AgentEvent) tea.Cmd {
761762

762763
switch evt.Type {
763764
case EventTextDelta:
764-
m.streaming.WriteString(evt.Text)
765+
// Filter out <think>...</think> reasoning blocks (MiniMax, DeepSeek, etc.)
766+
text := evt.Text
767+
for text != "" {
768+
if m.inThink {
769+
if end := strings.Index(text, "</think>"); end >= 0 {
770+
m.inThink = false
771+
text = text[end+len("</think>"):]
772+
continue
773+
}
774+
// Still inside think block — discard everything
775+
text = ""
776+
} else {
777+
if start := strings.Index(text, "<think>"); start >= 0 {
778+
// Write text before the tag
779+
if start > 0 {
780+
m.streaming.WriteString(text[:start])
781+
}
782+
m.inThink = true
783+
if !m.notify.HasActive() {
784+
m.notify.Push(Notification{Type: NotifyProgress, Text: "Thinking..."})
785+
}
786+
text = text[start+len("<think>"):]
787+
continue
788+
}
789+
// No think tags — write as-is
790+
m.streaming.WriteString(text)
791+
text = ""
792+
}
793+
}
765794
// Debounce: only rebuild viewport at most every 50ms during streaming
766795
if time.Since(m.lastStreamRebuild) > 50*time.Millisecond {
767796
m.rebuildViewport()
@@ -909,7 +938,10 @@ func (m *chatModel) handleAgentEvent(evt AgentEvent) tea.Cmd {
909938
}
910939

911940
func (m *chatModel) flushStreamingText() {
941+
m.inThink = false
912942
text := m.streaming.String()
943+
// Strip any residual think tags
944+
text = stripThinkTags(text)
913945
if text != "" {
914946
m.segments = append(m.segments, segment{
915947
kind: "text",
@@ -1050,6 +1082,24 @@ func (m chatModel) View() string {
10501082
return framedBody + "\n" + suggestBar + inputRow
10511083
}
10521084

1085+
// stripThinkTags removes <think>...</think> blocks and stray tags from text.
1086+
func stripThinkTags(s string) string {
1087+
for {
1088+
start := strings.Index(s, "<think>")
1089+
if start < 0 {
1090+
break
1091+
}
1092+
end := strings.Index(s[start:], "</think>")
1093+
if end < 0 {
1094+
// Unclosed tag — strip from <think> to end
1095+
s = s[:start]
1096+
break
1097+
}
1098+
s = s[:start] + s[start+end+len("</think>"):]
1099+
}
1100+
return strings.TrimSpace(s)
1101+
}
1102+
10531103
// parseSuggestionIndex checks if input is a suggestion number (1-based).
10541104
// Returns 0-based index or -1 if not a valid suggestion number.
10551105
func parseSuggestionIndex(input string, count int) int {

0 commit comments

Comments
 (0)