on main help sign in
feat: implement thinking mode for anthropic models
6c15eafKujtim Hoxha
internal/llm/agent/agent.go+7−1
@@ -600,12 +600,17 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg
600
600
601 switch event.Type {
601 switch event.Type {
602 case provider.EventThinkingDelta:
602 case provider.EventThinkingDelta:
603− assistantMsg.AppendReasoningContent(event.Content)
603+ assistantMsg.AppendReasoningContent(event.Thinking)
604+ return a.messages.Update(ctx, *assistantMsg)
605+ case provider.EventSignatureDelta:
606+ assistantMsg.AppendReasoningSignature(event.Signature)
604 return a.messages.Update(ctx, *assistantMsg)
607 return a.messages.Update(ctx, *assistantMsg)
605 case provider.EventContentDelta:
608 case provider.EventContentDelta:
609+ assistantMsg.FinishThinking()
606 assistantMsg.AppendContent(event.Content)
610 assistantMsg.AppendContent(event.Content)
607 return a.messages.Update(ctx, *assistantMsg)
611 return a.messages.Update(ctx, *assistantMsg)
608 case provider.EventToolUseStart:
612 case provider.EventToolUseStart:
613+ assistantMsg.FinishThinking()
609 slog.Info("Tool call started", "toolCall", event.ToolCall)
614 slog.Info("Tool call started", "toolCall", event.ToolCall)
610 assistantMsg.AddToolCall(*event.ToolCall)
615 assistantMsg.AddToolCall(*event.ToolCall)
611 return a.messages.Update(ctx, *assistantMsg)
616 return a.messages.Update(ctx, *assistantMsg)
@@ -619,6 +624,7 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg
619 case provider.EventError:
624 case provider.EventError:
620 return event.Error
625 return event.Error
621 case provider.EventComplete:
626 case provider.EventComplete:
627+ assistantMsg.FinishThinking()
622 assistantMsg.SetToolCalls(event.Response.ToolCalls)
628 assistantMsg.SetToolCalls(event.Response.ToolCalls)
623 assistantMsg.AddFinish(event.Response.FinishReason, "", "")
629 assistantMsg.AddFinish(event.Response.FinishReason, "", "")
624 if err := a.messages.Update(ctx, *assistantMsg); err != nil {
630 if err := a.messages.Update(ctx, *assistantMsg); err != nil {
internal/llm/prompt/coder.go+5−2
@@ -74,7 +74,7 @@ When making changes to files, first understand the file's code conventions. Mimi
74- Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to the repository.
74- Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to the repository.
75
75
76# Code style
76# Code style
77−- Do not add comments to the code you write, unless the user asks you to, or the code is complex and requires additional context.
77+- IMPORTANT: DO NOT ADD ***ANY*** COMMENTS unless asked
78
78
79- If completing the user's task requires writing or modifying files:
79- If completing the user's task requires writing or modifying files:
80 - Your code and final answer should follow these _CODING GUIDELINES_:
80 - Your code and final answer should follow these _CODING GUIDELINES_:
@@ -204,7 +204,7 @@ When making changes to files, first understand the file's code conventions. Mimi
204- Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to the repository.
204- Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to the repository.
205
205
206# Code style
206# Code style
207−- Do not add comments to the code you write, unless the user asks you to, or the code is complex and requires additional context.
207+- IMPORTANT: DO NOT ADD ***ANY*** COMMENTS unless asked
208
208
209# Doing tasks
209# Doing tasks
210The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
210The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
@@ -249,6 +249,9 @@ When you spend time searching for commands to typecheck, lint, build, or test, y
249- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
249- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
250- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
250- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
251
251
252+# Code style
253+- IMPORTANT: DO NOT ADD ***ANY*** COMMENTS unless asked
254+
252# Primary Workflows
255# Primary Workflows
253
256
254## Software Engineering Tasks
257## Software Engineering Tasks
internal/llm/provider/anthropic.go+16−6
@@ -72,6 +72,13 @@ func (a *anthropicClient) convertMessages(messages []message.Message) (anthropic
72
72
73 case message.Assistant:
73 case message.Assistant:
74 blocks := []anthropic.ContentBlockParamUnion{}
74 blocks := []anthropic.ContentBlockParamUnion{}
75+
76+ // Add thinking blocks first if present (required when thinking is enabled with tool use)
77+ if reasoningContent := msg.ReasoningContent(); reasoningContent.Thinking != "" {
78+ thinkingBlock := anthropic.NewThinkingBlock(reasoningContent.Signature, reasoningContent.Thinking)
79+ blocks = append(blocks, thinkingBlock)
80+ }
81+
75 if msg.Content().String() != "" {
82 if msg.Content().String() != "" {
76 content := anthropic.NewTextBlock(msg.Content().String())
83 content := anthropic.NewTextBlock(msg.Content().String())
77 if cache && !a.providerOptions.disableCache {
84 if cache && !a.providerOptions.disableCache {
@@ -159,16 +166,14 @@ func (a *anthropicClient) preparedMessages(messages []anthropic.MessageParam, to
159 }
166 }
160 temperature := anthropic.Float(0)
167 temperature := anthropic.Float(0)
161
168
162− if a.Model().CanReason && modelConfig.Think {
163− thinkingParam = anthropic.ThinkingConfigParamOfEnabled(int64(float64(a.providerOptions.maxTokens) * 0.8))
164− temperature = anthropic.Float(1)
165− }
166−
167 maxTokens := model.DefaultMaxTokens
169 maxTokens := model.DefaultMaxTokens
168 if modelConfig.MaxTokens > 0 {
170 if modelConfig.MaxTokens > 0 {
169 maxTokens = modelConfig.MaxTokens
171 maxTokens = modelConfig.MaxTokens
170 }
172 }
171−
173+ if a.Model().CanReason && modelConfig.Think {
174+ thinkingParam = anthropic.ThinkingConfigParamOfEnabled(int64(float64(maxTokens) * 0.8))
175+ temperature = anthropic.Float(1)
176+ }
172 // Override max tokens if set in provider options
177 // Override max tokens if set in provider options
173 if a.providerOptions.maxTokens > 0 {
178 if a.providerOptions.maxTokens > 0 {
174 maxTokens = a.providerOptions.maxTokens
179 maxTokens = a.providerOptions.maxTokens
@@ -300,6 +305,11 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
300 Type: EventThinkingDelta,
305 Type: EventThinkingDelta,
301 Thinking: event.Delta.Thinking,
306 Thinking: event.Delta.Thinking,
302 }
307 }
308+ } else if event.Delta.Type == "signature_delta" && event.Delta.Signature != "" {
309+ eventChan <- ProviderEvent{
310+ Type: EventSignatureDelta,
311+ Signature: event.Delta.Signature,
312+ }
303 } else if event.Delta.Type == "text_delta" && event.Delta.Text != "" {
313 } else if event.Delta.Type == "text_delta" && event.Delta.Text != "" {
304 eventChan <- ProviderEvent{
314 eventChan <- ProviderEvent{
305 Type: EventContentDelta,
315 Type: EventContentDelta,
internal/llm/provider/provider.go+17−15
@@ -15,16 +15,17 @@ type EventType string
15const maxRetries = 8
15const maxRetries = 8
16
16
17const (
17const (
18− EventContentStart EventType = "content_start"
18+ EventContentStart EventType = "content_start"
19− EventToolUseStart EventType = "tool_use_start"
19+ EventToolUseStart EventType = "tool_use_start"
20− EventToolUseDelta EventType = "tool_use_delta"
20+ EventToolUseDelta EventType = "tool_use_delta"
21− EventToolUseStop EventType = "tool_use_stop"
21+ EventToolUseStop EventType = "tool_use_stop"
22− EventContentDelta EventType = "content_delta"
22+ EventContentDelta EventType = "content_delta"
23− EventThinkingDelta EventType = "thinking_delta"
23+ EventThinkingDelta EventType = "thinking_delta"
24+ EventSignatureDelta EventType = "signature_delta"
24− EventContentStop EventType = "content_stop"
25+ EventContentStop EventType = "content_stop"
25− EventComplete EventType = "complete"
26+ EventComplete EventType = "complete"
26− EventError EventType = "error"
27+ EventError EventType = "error"
27− EventWarning EventType = "warning"
28+ EventWarning EventType = "warning"
28)
29)
29
30
30type TokenUsage struct {
31type TokenUsage struct {
@@ -44,11 +45,12 @@ type ProviderResponse struct {
44type ProviderEvent struct {
45type ProviderEvent struct {
45 Type EventType
46 Type EventType
46
47
47− Content string
48+ Content string
48− Thinking string
49+ Thinking string
50+ Signature string
49− Response *ProviderResponse
51+ Response *ProviderResponse
50− ToolCall *message.ToolCall
52+ ToolCall *message.ToolCall
51− Error error
53+ Error error
52}
54}
53type Provider interface {
55type Provider interface {
54 SendMessages(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error)
56 SendMessages(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error)
internal/message/content.go+59−3
@@ -36,7 +36,10 @@ type ContentPart interface {
36}
36}
37
37
38type ReasoningContent struct {
38type ReasoningContent struct {
39− Thinking string `json:"thinking"`
39+ Thinking string `json:"thinking"`
40+ Signature string `json:"signature"`
41+ StartedAt int64 `json:"started_at,omitempty"`
42+ FinishedAt int64 `json:"finished_at,omitempty"`
40}
43}
41
44
42func (tc ReasoningContent) String() string {
45func (tc ReasoningContent) String() string {
@@ -230,15 +233,68 @@ func (m *Message) AppendReasoningContent(delta string) {
230 found := false
233 found := false
231 for i, part := range m.Parts {
234 for i, part := range m.Parts {
232 if c, ok := part.(ReasoningContent); ok {
235 if c, ok := part.(ReasoningContent); ok {
233− m.Parts[i] = ReasoningContent{Thinking: c.Thinking + delta}
236+ m.Parts[i] = ReasoningContent{
237+ Thinking: c.Thinking + delta,
238+ Signature: c.Signature,
239+ StartedAt: c.StartedAt,
240+ FinishedAt: c.FinishedAt,
241+ }
234 found = true
242 found = true
235 }
243 }
236 }
244 }
237 if !found {
245 if !found {
238− m.Parts = append(m.Parts, ReasoningContent{Thinking: delta})
246+ m.Parts = append(m.Parts, ReasoningContent{
247+ Thinking: delta,
248+ StartedAt: time.Now().Unix(),
249+ })
250+ }
251+}
252+
253+func (m *Message) AppendReasoningSignature(signature string) {
254+ for i, part := range m.Parts {
255+ if c, ok := part.(ReasoningContent); ok {
256+ m.Parts[i] = ReasoningContent{
257+ Thinking: c.Thinking,
258+ Signature: c.Signature + signature,
259+ StartedAt: c.StartedAt,
260+ FinishedAt: c.FinishedAt,
261+ }
262+ return
263+ }
264+ }
265+ m.Parts = append(m.Parts, ReasoningContent{Signature: signature})
266+}
267+
268+func (m *Message) FinishThinking() {
269+ for i, part := range m.Parts {
270+ if c, ok := part.(ReasoningContent); ok {
271+ if c.FinishedAt == 0 {
272+ m.Parts[i] = ReasoningContent{
273+ Thinking: c.Thinking,
274+ Signature: c.Signature,
275+ StartedAt: c.StartedAt,
276+ FinishedAt: time.Now().Unix(),
277+ }
278+ }
279+ return
280+ }
239 }
281 }
240}
282}
241
283
284+func (m *Message) ThinkingDuration() time.Duration {
285+ reasoning := m.ReasoningContent()
286+ if reasoning.StartedAt == 0 {
287+ return 0
288+ }
289+
290+ endTime := reasoning.FinishedAt
291+ if endTime == 0 {
292+ endTime = time.Now().Unix()
293+ }
294+
295+ return time.Duration(endTime-reasoning.StartedAt) * time.Second
296+}
297+
242func (m *Message) FinishToolCall(toolCallID string) {
298func (m *Message) FinishToolCall(toolCallID string) {
243 for i, part := range m.Parts {
299 for i, part := range m.Parts {
244 if c, ok := part.(ToolCall); ok {
300 if c, ok := part.(ToolCall); ok {
internal/tui/components/chat/chat.go+1−1
@@ -329,7 +329,7 @@ func (m *messageListCmp) updateAssistantMessageContent(msg message.Message, assi
329
329
330// shouldShowAssistantMessage determines if an assistant message should be displayed.
330// shouldShowAssistantMessage determines if an assistant message should be displayed.
331func (m *messageListCmp) shouldShowAssistantMessage(msg message.Message) bool {
331func (m *messageListCmp) shouldShowAssistantMessage(msg message.Message) bool {
332− return len(msg.ToolCalls()) == 0 || msg.Content().Text != "" || msg.IsThinking()
332+ return len(msg.ToolCalls()) == 0 || msg.Content().Text != "" || msg.ReasoningContent().Thinking != "" || msg.IsThinking()
333}
333}
334
334
335// updateToolCalls handles updates to tool calls, updating existing ones and adding new ones.
335// updateToolCalls handles updates to tool calls, updating existing ones and adding new ones.
internal/tui/components/chat/messages/messages.go+77−30
@@ -6,6 +6,7 @@ import (
6 "strings"
6 "strings"
7 "time"
7 "time"
8
8
9+ "github.com/charmbracelet/bubbles/v2/viewport"
9 tea "github.com/charmbracelet/bubbletea/v2"
10 tea "github.com/charmbracelet/bubbletea/v2"
10 "github.com/charmbracelet/lipgloss/v2"
11 "github.com/charmbracelet/lipgloss/v2"
11 "github.com/charmbracelet/x/ansi"
12 "github.com/charmbracelet/x/ansi"
@@ -42,6 +43,9 @@ type messageCmp struct {
42 message message.Message // The underlying message content
43 message message.Message // The underlying message content
43 spinning bool // Whether to show loading animation
44 spinning bool // Whether to show loading animation
44 anim util.Model // Animation component for loading states
45 anim util.Model // Animation component for loading states
46+
47+ // Thinking viewport for displaying reasoning content
48+ thinkingViewport viewport.Model
45}
49}
46
50
47var focusedMessageBorder = lipgloss.Border{
51var focusedMessageBorder = lipgloss.Border{
@@ -51,6 +55,11 @@ var focusedMessageBorder = lipgloss.Border{
51// NewMessageCmp creates a new message component with the given message and options
55// NewMessageCmp creates a new message component with the given message and options
52func NewMessageCmp(msg message.Message) MessageCmp {
56func NewMessageCmp(msg message.Message) MessageCmp {
53 t := styles.CurrentTheme()
57 t := styles.CurrentTheme()
58+
59+ thinkingViewport := viewport.New()
60+ thinkingViewport.SetHeight(1)
61+ thinkingViewport.KeyMap = viewport.KeyMap{}
62+
54 m := &messageCmp{
63 m := &messageCmp{
55 message: msg,
64 message: msg,
56 anim: anim.New(anim.Settings{
65 anim: anim.New(anim.Settings{
@@ -59,6 +68,7 @@ func NewMessageCmp(msg message.Message) MessageCmp {
59 GradColorB: t.Secondary,
68 GradColorB: t.Secondary,
60 CycleColors: true,
69 CycleColors: true,
61 }),
70 }),
71+ thinkingViewport: thinkingViewport,
62 }
72 }
63 return m
73 return m
64}
74}
@@ -139,8 +149,38 @@ func (msg *messageCmp) style() lipgloss.Style {
139// renderAssistantMessage renders assistant messages with optional footer information.
149// renderAssistantMessage renders assistant messages with optional footer information.
140// Shows model name, response time, and finish reason when the message is complete.
150// Shows model name, response time, and finish reason when the message is complete.
141func (m *messageCmp) renderAssistantMessage() string {
151func (m *messageCmp) renderAssistantMessage() string {
152+ t := styles.CurrentTheme()
142− parts := []string{
153+ parts := []string{}
143− m.markdownContent(),
154+ content := m.message.Content().String()
155+ thinking := m.message.IsThinking()
156+ finished := m.message.IsFinished()
157+ finishedData := m.message.FinishPart()
158+ thinkingContent := ""
159+
160+ if thinking || m.message.ReasoningContent().Thinking != "" {
161+ thinkingContent = m.renderThinkingContent()
162+ } else if finished && content == "" && finishedData.Reason == message.FinishReasonEndTurn {
163+ content = ""
164+ } else if finished && content == "" && finishedData.Reason == message.FinishReasonCanceled {
165+ content = "*Canceled*"
166+ } else if finished && content == "" && finishedData.Reason == message.FinishReasonError {
167+ errTag := t.S().Base.Padding(0, 1).Background(t.Red).Foreground(t.White).Render("ERROR")
168+ truncated := ansi.Truncate(finishedData.Message, m.textWidth()-2-lipgloss.Width(errTag), "...")
169+ title := fmt.Sprintf("%s %s", errTag, t.S().Base.Foreground(t.FgHalfMuted).Render(truncated))
170+ details := t.S().Base.Foreground(t.FgSubtle).Width(m.textWidth() - 2).Render(finishedData.Details)
171+ // Handle error messages differently
172+ return fmt.Sprintf("%s\n\n%s", title, details)
173+ }
174+
175+ if thinkingContent != "" {
176+ parts = append(parts, thinkingContent)
177+ }
178+
179+ if content != "" {
180+ if thinkingContent != "" {
181+ parts = append(parts, "")
182+ }
183+ parts = append(parts, m.toMarkdown(content))
144 }
184 }
145
185
146 joined := lipgloss.JoinVertical(lipgloss.Left, parts...)
186 joined := lipgloss.JoinVertical(lipgloss.Left, parts...)
@@ -152,7 +192,7 @@ func (m *messageCmp) renderAssistantMessage() string {
152func (m *messageCmp) renderUserMessage() string {
192func (m *messageCmp) renderUserMessage() string {
153 t := styles.CurrentTheme()
193 t := styles.CurrentTheme()
154 parts := []string{
194 parts := []string{
155− m.markdownContent(),
195+ m.toMarkdown(m.message.Content().String()),
156 }
196 }
157 attachmentStyles := t.S().Text.
197 attachmentStyles := t.S().Text.
158 MarginLeft(1).
198 MarginLeft(1).
@@ -182,34 +222,41 @@ func (m *messageCmp) toMarkdown(content string) string {
182 return strings.TrimSuffix(rendered, "\n")
222 return strings.TrimSuffix(rendered, "\n")
183}
223}
184
224
185−// markdownContent processes the message content and handles special states.
186−// Returns appropriate content for thinking, finished, and error states.
187−func (m *messageCmp) markdownContent() string {
225+func (m *messageCmp) renderThinkingContent() string {
188 t := styles.CurrentTheme()
226 t := styles.CurrentTheme()
189− content := m.message.Content().String()
227+ reasoningContent := m.message.ReasoningContent()
190− if m.message.Role == message.Assistant {
228+ if reasoningContent.Thinking == "" {
191− thinking := m.message.IsThinking()
229+ return ""
192− finished := m.message.IsFinished()
230+ }
193− finishedData := m.message.FinishPart()
231+ lines := strings.Split(reasoningContent.Thinking, "\n")
194− if thinking {
232+ var content strings.Builder
195− // Handle the thinking state
233+ lineStyle := t.S().Muted.Background(t.BgBaseLighter)
196− // TODO: maybe add the thinking content if available later.
234+ for _, line := range lines {
197− content = fmt.Sprintf("**%s %s**", styles.LoadingIcon, "Thinking...")
235+ if line == "" {
198− } else if finished && content == "" && finishedData.Reason == message.FinishReasonEndTurn {
236+ continue
199− // Sometimes the LLMs respond with no content when they think the previous tool result
237+ }
200− // provides the requested question
238+ content.WriteString(lineStyle.Width(m.textWidth()-2).Render(line) + "\n")
201− content = ""
239+ }
202− } else if finished && content == "" && finishedData.Reason == message.FinishReasonCanceled {
240+ fullContent := content.String()
203− content = "*Canceled*"
241+ height := util.Clamp(lipgloss.Height(fullContent), 1, 10)
204− } else if finished && content == "" && finishedData.Reason == message.FinishReasonError {
242+ m.thinkingViewport.SetHeight(height)
205− errTag := t.S().Base.Padding(0, 1).Background(t.Red).Foreground(t.White).Render("ERROR")
243+ m.thinkingViewport.SetWidth(m.textWidth())
206− truncated := ansi.Truncate(finishedData.Message, m.textWidth()-2-lipgloss.Width(errTag), "...")
244+ m.thinkingViewport.SetContent(fullContent)
207− title := fmt.Sprintf("%s %s", errTag, t.S().Base.Foreground(t.FgHalfMuted).Render(truncated))
245+ m.thinkingViewport.GotoBottom()
208− details := t.S().Base.Foreground(t.FgSubtle).Width(m.textWidth() - 2).Render(finishedData.Details)
246+ var footer string
209− return fmt.Sprintf("%s\n\n%s", title, details)
247+ if reasoningContent.StartedAt > 0 {
248+ duration := m.message.ThinkingDuration()
249+ opts := core.StatusOpts{
250+ Title: "Thinking...",
251+ Description: duration.String(),
252+ }
253+ if reasoningContent.FinishedAt > 0 {
254+ opts.NoIcon = true
255+ opts.Title = "Thought for"
210 }
256 }
257+ footer = t.S().Base.PaddingLeft(1).Render(core.Status(opts, m.textWidth()-1))
211 }
258 }
212− return m.toMarkdown(content)
259+ return lineStyle.Width(m.textWidth()).Padding(0, 1).Render(m.thinkingViewport.View()) + "\n\n" + footer
213}
260}
214
261
215// shouldSpin determines whether the message should show a loading animation.
262// shouldSpin determines whether the message should show a loading animation.
@@ -257,8 +304,8 @@ func (m *messageCmp) GetSize() (int, int) {
257
304
258// SetSize updates the width of the message component for text wrapping
305// SetSize updates the width of the message component for text wrapping
259func (m *messageCmp) SetSize(width int, height int) tea.Cmd {
306func (m *messageCmp) SetSize(width int, height int) tea.Cmd {
260− // For better readability, we limit the width to a maximum of 120 characters
261− m.width = min(width, 120)
307+ m.width = util.Clamp(width, 1, 120)
308+ m.thinkingViewport.SetWidth(m.width - 4)
262 return nil
309 return nil
263}
310}
264
311
internal/tui/components/dialogs/commands/commands.go+27
@@ -6,6 +6,8 @@ import (
6 tea "github.com/charmbracelet/bubbletea/v2"
6 tea "github.com/charmbracelet/bubbletea/v2"
7 "github.com/charmbracelet/lipgloss/v2"
7 "github.com/charmbracelet/lipgloss/v2"
8
8
9+ "github.com/charmbracelet/crush/internal/config"
10+ "github.com/charmbracelet/crush/internal/fur/provider"
9 "github.com/charmbracelet/crush/internal/llm/prompt"
11 "github.com/charmbracelet/crush/internal/llm/prompt"
10 "github.com/charmbracelet/crush/internal/tui/components/chat"
12 "github.com/charmbracelet/crush/internal/tui/components/chat"
11 "github.com/charmbracelet/crush/internal/tui/components/completions"
13 "github.com/charmbracelet/crush/internal/tui/components/completions"
@@ -58,6 +60,7 @@ type (
58 SwitchSessionsMsg struct{}
60 SwitchSessionsMsg struct{}
59 SwitchModelMsg struct{}
61 SwitchModelMsg struct{}
60 ToggleCompactModeMsg struct{}
62 ToggleCompactModeMsg struct{}
63+ ToggleThinkingMsg struct{}
61 CompactMsg struct {
64 CompactMsg struct {
62 SessionID string
65 SessionID string
63 }
66 }
@@ -260,6 +263,30 @@ func (c *commandDialogCmp) defaultCommands() []Command {
260 },
263 },
261 })
264 })
262 }
265 }
266+
267+ // Only show thinking toggle for Anthropic models that can reason
268+ cfg := config.Get()
269+ if agentCfg, ok := cfg.Agents["coder"]; ok {
270+ providerCfg := cfg.GetProviderForModel(agentCfg.Model)
271+ model := cfg.GetModelByType(agentCfg.Model)
272+ if providerCfg != nil && model != nil &&
273+ providerCfg.Type == provider.TypeAnthropic && model.CanReason {
274+ selectedModel := cfg.Models[agentCfg.Model]
275+ status := "Enable"
276+ if selectedModel.Think {
277+ status = "Disable"
278+ }
279+ commands = append(commands, Command{
280+ ID: "toggle_thinking",
281+ Title: status + " Thinking Mode",
282+ Description: "Toggle model thinking for reasoning-capable models",
283+ Handler: func(cmd Command) tea.Cmd {
284+ return util.CmdHandler(ToggleThinkingMsg{})
285+ },
286+ })
287+ }
288+ }
289+
263 // Only show toggle compact mode command if window width is larger than compact breakpoint (90)
290 // Only show toggle compact mode command if window width is larger than compact breakpoint (90)
264 if c.wWidth > 120 && c.sessionID != "" {
291 if c.wWidth > 120 && c.sessionID != "" {
265 commands = append(commands, Command{
292 commands = append(commands, Command{
internal/tui/page/chat/chat.go+31
@@ -183,6 +183,8 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
183 cmd = p.updateCompactConfig(false)
183 cmd = p.updateCompactConfig(false)
184 }
184 }
185 return p, tea.Batch(p.SetSize(p.width, p.height), cmd)
185 return p, tea.Batch(p.SetSize(p.width, p.height), cmd)
186+ case commands.ToggleThinkingMsg:
187+ return p, p.toggleThinking()
186 case pubsub.Event[session.Session]:
188 case pubsub.Event[session.Session]:
187 u, cmd := p.header.Update(msg)
189 u, cmd := p.header.Update(msg)
188 p.header = u.(header.Header)
190 p.header = u.(header.Header)
@@ -409,6 +411,35 @@ func (p *chatPage) updateCompactConfig(compact bool) tea.Cmd {
409 }
411 }
410}
412}
411
413
414+func (p *chatPage) toggleThinking() tea.Cmd {
415+ return func() tea.Msg {
416+ cfg := config.Get()
417+ agentCfg := cfg.Agents["coder"]
418+ currentModel := cfg.Models[agentCfg.Model]
419+
420+ // Toggle the thinking mode
421+ currentModel.Think = !currentModel.Think
422+ cfg.Models[agentCfg.Model] = currentModel
423+
424+ // Update the agent with the new configuration
425+ if err := p.app.UpdateAgentModel(); err != nil {
426+ return util.InfoMsg{
427+ Type: util.InfoTypeError,
428+ Msg: "Failed to update thinking mode: " + err.Error(),
429+ }
430+ }
431+
432+ status := "disabled"
433+ if currentModel.Think {
434+ status = "enabled"
435+ }
436+ return util.InfoMsg{
437+ Type: util.InfoTypeInfo,
438+ Msg: "Thinking mode " + status,
439+ }
440+ }
441+}
442+
412func (p *chatPage) setCompactMode(compact bool) {
443func (p *chatPage) setCompactMode(compact bool) {
413 if p.compact == compact {
444 if p.compact == compact {
414 return
445 return