Skip to content

Commit 91b9370

Browse files
authored
Merge pull request #1403 from krissetto/disable-think-cmd-on-dumb-models
Disable /think in the TUI when using non-reasoning models
2 parents f0724d0 + 434ea92 commit 91b9370

File tree

8 files changed

+148
-50
lines changed

8 files changed

+148
-50
lines changed

pkg/app/app.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type App struct {
2929
events chan tea.Msg
3030
throttleDuration time.Duration
3131
cancel context.CancelFunc
32+
currentAgentModel string // Tracks the current agent's model ID from AgentInfoEvent
3233
}
3334

3435
// Opt is an option for creating a new App.
@@ -105,6 +106,29 @@ func (a *App) CurrentAgentCommands(ctx context.Context) types.Commands {
105106
return a.runtime.CurrentAgentInfo(ctx).Commands
106107
}
107108

109+
// CurrentAgentModel returns the model ID for the current agent.
110+
// Returns the tracked model from AgentInfoEvent, or falls back to session overrides.
111+
// Returns empty string if no model information is available (fail-open scenario).
112+
func (a *App) CurrentAgentModel() string {
113+
if a.currentAgentModel != "" {
114+
return a.currentAgentModel
115+
}
116+
// Fallback to session overrides
117+
if a.session != nil && a.session.AgentModelOverrides != nil {
118+
agentName := a.runtime.CurrentAgentName()
119+
if modelRef, ok := a.session.AgentModelOverrides[agentName]; ok {
120+
return modelRef
121+
}
122+
}
123+
return ""
124+
}
125+
126+
// TrackCurrentAgentModel updates the tracked model ID for the current agent.
127+
// This is called when AgentInfoEvent is received from the runtime.
128+
func (a *App) TrackCurrentAgentModel(model string) {
129+
a.currentAgentModel = model
130+
}
131+
108132
// CurrentMCPPrompts returns the available MCP prompts for the active agent
109133
func (a *App) CurrentMCPPrompts(ctx context.Context) map[string]mcptools.PromptInfo {
110134
if localRuntime, ok := a.runtime.(*runtime.LocalRuntime); ok {

pkg/modelsdev/store.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,3 +349,36 @@ var bedrockRegionPrefixes = map[string]bool{
349349
func isBedrockRegionPrefix(prefix string) bool {
350350
return bedrockRegionPrefixes[prefix]
351351
}
352+
353+
// ModelSupportsReasoning checks if the given model ID supports reasoning/thinking.
354+
//
355+
// This function implements fail-open semantics:
356+
// - If modelID is empty or not in "provider/model" format, returns true (fail-open)
357+
// - If models.dev lookup fails for any reason, returns true (fail-open)
358+
// - If lookup succeeds, returns the model's Reasoning field value
359+
func ModelSupportsReasoning(ctx context.Context, modelID string) bool {
360+
// Fail-open for empty model ID
361+
if modelID == "" {
362+
return true
363+
}
364+
365+
// Fail-open if not in provider/model format
366+
if !strings.Contains(modelID, "/") {
367+
slog.Debug("Model ID not in provider/model format, assuming reasoning supported to allow user choice", "model_id", modelID)
368+
return true
369+
}
370+
371+
store, err := NewStore()
372+
if err != nil {
373+
slog.Debug("Failed to create modelsdev store, assuming reasoning supported to allow user choice", "error", err)
374+
return true
375+
}
376+
377+
model, err := store.GetModel(ctx, modelID)
378+
if err != nil {
379+
slog.Debug("Failed to lookup model in models.dev, assuming reasoning supported to allow user choice", "model_id", modelID, "error", err)
380+
return true
381+
}
382+
383+
return model.Reasoning
384+
}

pkg/runtime/event.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ func MCPInitFinished(agentName string) Event {
373373
type AgentInfoEvent struct {
374374
Type string `json:"type"`
375375
AgentName string `json:"agent_name"`
376-
Model string `json:"model"`
376+
Model string `json:"model"` // this is in provider/model format (e.g., "openai/gpt-4o")
377377
Description string `json:"description"`
378378
WelcomeMessage string `json:"welcome_message,omitempty"`
379379
AgentContext

pkg/runtime/runtime.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,8 +1086,14 @@ func (r *LocalRuntime) handleStream(ctx context.Context, stream chat.MessageStre
10861086
if actualModel == "" && response.Model != "" {
10871087
actualModel = response.Model
10881088
if !actualModelEventEmitted && actualModel != modelID {
1089-
slog.Debug("Detected actual model differs from configured model (streaming)", "configured", modelID, "actual", actualModel)
1090-
events <- AgentInfo(a.Name(), actualModel, a.Description(), a.WelcomeMessage())
1089+
// NOTE(krissetto):Prepend the provider from the configured modelID to maintain consistent format
1090+
// every other invocation in the code uses the provider/model format
1091+
formattedModel := actualModel
1092+
if idx := strings.Index(modelID, "/"); idx != -1 {
1093+
formattedModel = modelID[:idx+1] + actualModel
1094+
}
1095+
slog.Debug("Detected actual model differs from configured model (streaming)", "configured", modelID, "actual", formattedModel)
1096+
events <- AgentInfo(a.Name(), formattedModel, a.Description(), a.WelcomeMessage())
10911097
actualModelEventEmitted = true
10921098
}
10931099
}

pkg/tui/commands/commands.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/docker/cagent/pkg/app"
1111
"github.com/docker/cagent/pkg/feedback"
12+
"github.com/docker/cagent/pkg/modelsdev"
1213
"github.com/docker/cagent/pkg/tui/components/toolcommon"
1314
"github.com/docker/cagent/pkg/tui/core"
1415
"github.com/docker/cagent/pkg/tui/messages"
@@ -220,10 +221,25 @@ func builtInFeedbackCommands() []Item {
220221

221222
// BuildCommandCategories builds the list of command categories for the command palette
222223
func BuildCommandCategories(ctx context.Context, application *app.App) []Category {
224+
// Get session commands and filter based on model capabilities
225+
sessionCommands := builtInSessionCommands()
226+
227+
// Check if the current model supports reasoning; hide /think if not
228+
currentModel := application.CurrentAgentModel()
229+
if !modelsdev.ModelSupportsReasoning(ctx, currentModel) {
230+
filtered := make([]Item, 0, len(sessionCommands))
231+
for _, cmd := range sessionCommands {
232+
if cmd.ID != "session.think" {
233+
filtered = append(filtered, cmd)
234+
}
235+
}
236+
sessionCommands = filtered
237+
}
238+
223239
categories := []Category{
224240
{
225241
Name: "Session",
226-
Commands: builtInSessionCommands(),
242+
Commands: sessionCommands,
227243
},
228244
{
229245
Name: "Feedback",

pkg/tui/components/sidebar/sidebar.go

Lines changed: 56 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package sidebar
22

33
import (
4+
"context"
45
"fmt"
56
"log/slog"
67
"maps"
@@ -11,6 +12,7 @@ import (
1112
tea "charm.land/bubbletea/v2"
1213
"charm.land/lipgloss/v2"
1314

15+
"github.com/docker/cagent/pkg/modelsdev"
1416
"github.com/docker/cagent/pkg/paths"
1517
"github.com/docker/cagent/pkg/runtime"
1618
"github.com/docker/cagent/pkg/session"
@@ -63,34 +65,35 @@ type ragIndexingState struct {
6365

6466
// model implements Model
6567
type model struct {
66-
width int
67-
height int
68-
xPos int // absolute x position on screen
69-
yPos int // absolute y position on screen
70-
layoutCfg LayoutConfig // layout configuration for spacing
71-
sessionUsage map[string]*runtime.Usage // sessionID -> latest usage snapshot
72-
sessionAgent map[string]string // sessionID -> agent name
73-
todoComp *todotool.SidebarComponent
74-
mcpInit bool
75-
ragIndexing map[string]*ragIndexingState // strategy name -> indexing state
76-
spinner spinner.Spinner
77-
mode Mode
78-
sessionTitle string
79-
sessionStarred bool
80-
sessionHasContent bool // true when session has been used (has messages)
81-
currentAgent string
82-
agentModel string
83-
agentDescription string
84-
availableAgents []runtime.AgentDetails
85-
agentSwitching bool
86-
availableTools int
87-
toolsLoading bool // true when more tools may still be loading
88-
sessionState *service.SessionState
89-
workingAgent string // Name of the agent currently working (empty if none)
90-
scrollbar *scrollbar.Model
91-
workingDirectory string
92-
queuedMessages []string // Truncated preview of queued messages
93-
streamCancelled bool // true after ESC cancel until next StreamStartedEvent
68+
width int
69+
height int
70+
xPos int // absolute x position on screen
71+
yPos int // absolute y position on screen
72+
layoutCfg LayoutConfig // layout configuration for spacing
73+
sessionUsage map[string]*runtime.Usage // sessionID -> latest usage snapshot
74+
sessionAgent map[string]string // sessionID -> agent name
75+
todoComp *todotool.SidebarComponent
76+
mcpInit bool
77+
ragIndexing map[string]*ragIndexingState // strategy name -> indexing state
78+
spinner spinner.Spinner
79+
mode Mode
80+
sessionTitle string
81+
sessionStarred bool
82+
sessionHasContent bool // true when session has been used (has messages)
83+
currentAgent string
84+
agentModel string
85+
agentDescription string
86+
availableAgents []runtime.AgentDetails
87+
agentSwitching bool
88+
availableTools int
89+
toolsLoading bool // true when more tools may still be loading
90+
sessionState *service.SessionState
91+
workingAgent string // Name of the agent currently working (empty if none)
92+
scrollbar *scrollbar.Model
93+
workingDirectory string
94+
queuedMessages []string // Truncated preview of queued messages
95+
streamCancelled bool // true after ESC cancel until next StreamStartedEvent
96+
reasoningSupported bool // true if current model supports reasoning (default: true / fail-open)
9497
}
9598

9699
// Option is a functional option for configuring the sidebar.
@@ -103,18 +106,19 @@ func WithLayoutConfig(cfg LayoutConfig) Option {
103106

104107
func New(sessionState *service.SessionState, opts ...Option) Model {
105108
m := &model{
106-
width: 20,
107-
layoutCfg: DefaultLayoutConfig(),
108-
height: 24,
109-
sessionUsage: make(map[string]*runtime.Usage),
110-
sessionAgent: make(map[string]string),
111-
todoComp: todotool.NewSidebarComponent(),
112-
spinner: spinner.New(spinner.ModeSpinnerOnly, styles.SpinnerDotsHighlightStyle),
113-
sessionTitle: "New session",
114-
ragIndexing: make(map[string]*ragIndexingState),
115-
sessionState: sessionState,
116-
scrollbar: scrollbar.New(),
117-
workingDirectory: getCurrentWorkingDirectory(),
109+
width: 20,
110+
layoutCfg: DefaultLayoutConfig(),
111+
height: 24,
112+
sessionUsage: make(map[string]*runtime.Usage),
113+
sessionAgent: make(map[string]string),
114+
todoComp: todotool.NewSidebarComponent(),
115+
spinner: spinner.New(spinner.ModeSpinnerOnly, styles.SpinnerDotsHighlightStyle),
116+
sessionTitle: "New session",
117+
ragIndexing: make(map[string]*ragIndexingState),
118+
sessionState: sessionState,
119+
scrollbar: scrollbar.New(),
120+
workingDirectory: getCurrentWorkingDirectory(),
121+
reasoningSupported: true, // Default to true (fail-open)
118122
}
119123
for _, opt := range opts {
120124
opt(m)
@@ -145,16 +149,22 @@ func (m *model) SetTodos(result *tools.ToolCallResult) error {
145149
}
146150

147151
// SetAgentInfo sets the current agent information and updates the model in availableAgents
148-
func (m *model) SetAgentInfo(agentName, model, description string) {
152+
func (m *model) SetAgentInfo(agentName, modelID, description string) {
149153
m.currentAgent = agentName
150-
m.agentModel = model
154+
m.agentModel = modelID
151155
m.agentDescription = description
156+
m.reasoningSupported = modelsdev.ModelSupportsReasoning(context.Background(), modelID)
152157

153158
// Update the model in availableAgents for the current agent
154159
// This is important when model routing selects a different model than configured
160+
// Extract just the model name from "provider/model" format to match TeamInfoEvent format
155161
for i := range m.availableAgents {
156-
if m.availableAgents[i].Name == agentName && model != "" {
157-
m.availableAgents[i].Model = model
162+
if m.availableAgents[i].Name == agentName && modelID != "" {
163+
modelName := modelID
164+
if idx := strings.LastIndex(modelName, "/"); idx != -1 {
165+
modelName = modelName[idx+1:]
166+
}
167+
m.availableAgents[i].Model = modelName
158168
break
159169
}
160170
}
@@ -776,13 +786,14 @@ func (m *model) toolsetInfo(contentWidth int) string {
776786
lines = append(lines, m.renderToolsStatus())
777787

778788
// Toggle indicators with shortcuts
789+
// Only show "Thinking enabled" if the model supports reasoning
779790
toggles := []struct {
780791
enabled bool
781792
label string
782793
shortcut string
783794
}{
784795
{m.sessionState.YoloMode, "YOLO mode enabled", "^y"},
785-
{m.sessionState.Thinking, "Thinking enabled", "/think"},
796+
{m.sessionState.Thinking && m.reasoningSupported, "Thinking enabled", "/think"},
786797
{m.sessionState.HideToolResults, "Tool output hidden", "^o"},
787798
{m.sessionState.SplitDiffView, "Split Diff View enabled", "^t"},
788799
}

pkg/tui/handlers.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/docker/cagent/pkg/browser"
1313
"github.com/docker/cagent/pkg/evaluation"
14+
"github.com/docker/cagent/pkg/modelsdev"
1415
mcptools "github.com/docker/cagent/pkg/tools/mcp"
1516
"github.com/docker/cagent/pkg/tui/components/editor"
1617
"github.com/docker/cagent/pkg/tui/components/notification"
@@ -191,6 +192,12 @@ func (a *appModel) handleToggleYolo() (tea.Model, tea.Cmd) {
191192
}
192193

193194
func (a *appModel) handleToggleThinking() (tea.Model, tea.Cmd) {
195+
// Check if the current model supports reasoning
196+
currentModel := a.application.CurrentAgentModel()
197+
if !modelsdev.ModelSupportsReasoning(context.Background(), currentModel) {
198+
return a, notification.InfoCmd("Thinking/reasoning is not supported for the current model")
199+
}
200+
194201
sess := a.application.Session()
195202
sess.Thinking = !sess.Thinking
196203
a.sessionState.SetThinking(sess.Thinking)

pkg/tui/tui.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,10 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
203203
return a, cmd
204204

205205
case *runtime.AgentInfoEvent:
206-
// Track current agent
206+
// Track current agent and model
207207
a.currentAgent = msg.AgentName
208208
a.sessionState.SetCurrentAgent(msg.AgentName)
209+
a.application.TrackCurrentAgentModel(msg.Model)
209210
// Forward to chat page
210211
updated, cmd := a.chatPage.Update(msg)
211212
a.chatPage = updated.(chat.Page)

0 commit comments

Comments
 (0)