Skip to content

Commit a132558

Browse files
committed
Disable /think in the TUI when using non-reasoning models
Signed-off-by: Christopher Petito <chrisjpetito@gmail.com>
1 parent da741b0 commit a132558

File tree

6 files changed

+135
-46
lines changed

6 files changed

+135
-46
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/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: 52 additions & 44 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"
@@ -62,33 +64,34 @@ type ragIndexingState struct {
6264

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

9497
// Option is a functional option for configuring the sidebar.
@@ -101,18 +104,19 @@ func WithLayoutConfig(cfg LayoutConfig) Option {
101104

102105
func New(sessionState *service.SessionState, opts ...Option) Model {
103106
m := &model{
104-
width: 20,
105-
layoutCfg: DefaultLayoutConfig(),
106-
height: 24,
107-
sessionUsage: make(map[string]*runtime.Usage),
108-
sessionAgent: make(map[string]string),
109-
todoComp: todotool.NewSidebarComponent(),
110-
spinner: spinner.New(spinner.ModeSpinnerOnly, styles.SpinnerDotsHighlightStyle),
111-
sessionTitle: "New session",
112-
ragIndexing: make(map[string]*ragIndexingState),
113-
sessionState: sessionState,
114-
scrollbar: scrollbar.New(),
115-
workingDirectory: getCurrentWorkingDirectory(),
107+
width: 20,
108+
layoutCfg: DefaultLayoutConfig(),
109+
height: 24,
110+
sessionUsage: make(map[string]*runtime.Usage),
111+
sessionAgent: make(map[string]string),
112+
todoComp: todotool.NewSidebarComponent(),
113+
spinner: spinner.New(spinner.ModeSpinnerOnly, styles.SpinnerDotsHighlightStyle),
114+
sessionTitle: "New session",
115+
ragIndexing: make(map[string]*ragIndexingState),
116+
sessionState: sessionState,
117+
scrollbar: scrollbar.New(),
118+
workingDirectory: getCurrentWorkingDirectory(),
119+
reasoningSupported: true, // Default to true (fail-open)
116120
}
117121
for _, opt := range opts {
118122
opt(m)
@@ -143,16 +147,19 @@ func (m *model) SetTodos(result *tools.ToolCallResult) error {
143147
}
144148

145149
// SetAgentInfo sets the current agent information and updates the model in availableAgents
146-
func (m *model) SetAgentInfo(agentName, model, description string) {
150+
func (m *model) SetAgentInfo(agentName, modelID, description string) {
147151
m.currentAgent = agentName
148-
m.agentModel = model
152+
m.agentModel = modelID
149153
m.agentDescription = description
150154

155+
// Check if the model supports reasoning (fail-open on errors)
156+
m.reasoningSupported = modelsdev.ModelSupportsReasoning(context.Background(), modelID)
157+
151158
// Update the model in availableAgents for the current agent
152159
// This is important when model routing selects a different model than configured
153160
for i := range m.availableAgents {
154-
if m.availableAgents[i].Name == agentName && model != "" {
155-
m.availableAgents[i].Model = model
161+
if m.availableAgents[i].Name == agentName && modelID != "" {
162+
m.availableAgents[i].Model = modelID
156163
break
157164
}
158165
}
@@ -749,13 +756,14 @@ func (m *model) toolsetInfo(contentWidth int) string {
749756
lines = append(lines, m.renderToolsStatus())
750757

751758
// Toggle indicators with shortcuts
759+
// Only show "Thinking enabled" if the model supports reasoning
752760
toggles := []struct {
753761
enabled bool
754762
label string
755763
shortcut string
756764
}{
757765
{m.sessionState.YoloMode, "YOLO mode enabled", "^y"},
758-
{m.sessionState.Thinking, "Thinking enabled", "/think"},
766+
{m.sessionState.Thinking && m.reasoningSupported, "Thinking enabled", "/think"},
759767
{m.sessionState.HideToolResults, "Tool output hidden", "^o"},
760768
{m.sessionState.SplitDiffView, "Split Diff View enabled", "^t"},
761769
}

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)