Skip to content

Move ModelSupportsReasoning calls to async bubbletea commands#1749

Merged
dgageot merged 2 commits intodocker:mainfrom
rumpl:async-tui
Feb 16, 2026
Merged

Move ModelSupportsReasoning calls to async bubbletea commands#1749
dgageot merged 2 commits intodocker:mainfrom
rumpl:async-tui

Conversation

@rumpl
Copy link
Member

@rumpl rumpl commented Feb 16, 2026

The modelsdev.ModelSupportsReasoning function can block for up to 30s on
a cold cache (HTTP fetch to models.dev). Calling it synchronously in
Update() froze the TUI.

  • sidebar.SetAgentInfo now returns tea.Cmd; the reasoning check runs in
    a goroutine and reports back via reasoningSupportResultMsg
  • handleToggleThinking fires an async command and handles the result in
    a separate handleToggleThinkingResult method
  • All call sites (runtime_events.go, tui.go) updated to propagate the
    returned commands

Also update AGENTS.md to steer agents into the right direction for the TUI.

The modelsdev.ModelSupportsReasoning function can block for up to 30s on
a cold cache (HTTP fetch to models.dev). Calling it synchronously in
Update() froze the TUI.

- sidebar.SetAgentInfo now returns tea.Cmd; the reasoning check runs in
  a goroutine and reports back via reasoningSupportResultMsg
- handleToggleThinking fires an async command and handles the result in
  a separate handleToggleThinkingResult method
- All call sites (runtime_events.go, tui.go) updated to propagate the
  returned commands

Assisted-By: cagent
@rumpl rumpl requested a review from a team as a code owner February 16, 2026 13:24
Copy link

@docker-agent docker-agent bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Found 2 medium-severity issues in the async command handling:

  1. Session state race condition - The thinking toggle may be applied to the wrong session if the user switches sessions while the async check is in progress
  2. Stale reasoning support status - When switching agents, the old agent's reasoning support status is displayed until the new async check completes

Both issues are related to state consistency when async operations complete after the user has changed context (session or agent).

return a, notification.InfoCmd("Thinking/reasoning is not supported for the current model")
}

sess := a.application.Session()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Session state race condition

The handleToggleThinkingResult function gets the current session without verifying it's the same session that initiated the thinking toggle. If a user:

  1. Toggles thinking on Session A
  2. Quickly switches to Session B before the async check completes
  3. The async result arrives and toggles thinking on Session B instead of Session A

This causes the thinking flag to be applied to the wrong session.

Suggested fix: Capture the session ID when starting the async check and include it in ToggleThinkingResultMsg. Then verify the session ID matches before applying the toggle:

func (a *appModel) handleToggleThinking() (tea.Model, tea.Cmd) {
    // ...
    sessionID := a.application.Session().ID
    return a, func() tea.Msg {
        supported := modelsdev.ModelSupportsReasoning(ctx, currentModel)
        return messages.ToggleThinkingResultMsg{
            Supported: supported,
            SessionID: sessionID,
        }
    }
}

func (a *appModel) handleToggleThinkingResult(msg messages.ToggleThinkingResultMsg) (tea.Model, tea.Cmd) {
    sess := a.application.Session()
    if sess.ID != msg.SessionID {
        return a, nil // Session changed, discard result
    }
    // ... rest of logic
}
``` <!-- cagent-review -->

case *runtime.AgentInfoEvent:
m.SetAgentInfo(msg.AgentName, msg.Model, msg.Description)
case reasoningSupportResultMsg:
if msg.modelID == m.agentModel {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale reasoning support status displayed when switching agents

The guard if msg.modelID == m.agentModel correctly prevents stale async results from updating the wrong agent's state. However, when SetAgentInfo is called with a new agent, the reasoningSupported flag retains the value from the previous agent until the new async check completes.

Scenario:

  1. Agent A does not support reasoning → reasoningSupported = false
  2. Switch to Agent B (which supports reasoning)
  3. Until the async check completes, the UI shows reasoning as not supported (stale value from Agent A)
  4. The initial default is true (fail-open), so we should reset to this default

Suggested fix: Reset reasoningSupported to the fail-open default when setting a new agent:

func (m *model) SetAgentInfo(agentName, modelID, description string) tea.Cmd {
    m.currentAgent = agentName
    m.agentModel = modelID
    m.agentDescription = description
    m.reasoningSupported = true // Reset to fail-open default

    if m.cancelReasoningCheck != nil {
        m.cancelReasoningCheck()
    }
    ctx, cancel := context.WithCancel(context.Background())
    m.cancelReasoningCheck = cancel
    // ... rest of function
}
``` <!-- cagent-review -->

@dgageot dgageot merged commit cf377fc into docker:main Feb 16, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants