on main help sign in
fix(ui/docker): don't block ui when checking for docker desktop
commit600310508e2b00a933454e3afcea33cd5eda3054 authorChristian Rocha <christian@rocha.is> dateThu 19 Mar 2026, 11:51 (7 months ago) parent351e562
3 files changed+73−7
internal/config/docker_mcp.go+33 internal/ui/dialog/commands.go+34−2 internal/ui/model/ui.go+6−5
internal/config/docker_mcp.go+33
@@ -4,6 +4,7 @@ import (
44 "context"
55 "fmt"
66 "os/exec"
7+ "sync"
78 "time"
89)
910
@@ -12,6 +13,15 @@ var dockerMCPVersionRunner = func(ctx context.Context) error {
1213 return cmd.Run()
1314}
1415
16+const dockerMCPAvailabilityTTL = 10 * time.Second
17+
18+var dockerMCPAvailabilityCache struct {
19+ mu sync.Mutex
20+ available bool
21+ checkedAt time.Time
22+ known bool
23+}
24+
1525// DockerMCPName is the name of the Docker MCP configuration.
1626const DockerMCPName = "docker"
1727
@@ -25,6 +35,29 @@ func IsDockerMCPAvailable() bool {
2535 return err == nil
2636}
2737
38+func DockerMCPAvailabilityCached() (available bool, known bool) {
39+ dockerMCPAvailabilityCache.mu.Lock()
40+ defer dockerMCPAvailabilityCache.mu.Unlock()
41+
42+ if !dockerMCPAvailabilityCache.known {
43+ return false, false
44+ }
45+ if time.Since(dockerMCPAvailabilityCache.checkedAt) > dockerMCPAvailabilityTTL {
46+ return dockerMCPAvailabilityCache.available, false
47+ }
48+ return dockerMCPAvailabilityCache.available, true
49+}
50+
51+func RefreshDockerMCPAvailability() bool {
52+ available := IsDockerMCPAvailable()
53+ dockerMCPAvailabilityCache.mu.Lock()
54+ dockerMCPAvailabilityCache.available = available
55+ dockerMCPAvailabilityCache.checkedAt = time.Now()
56+ dockerMCPAvailabilityCache.known = true
57+ dockerMCPAvailabilityCache.mu.Unlock()
58+ return available
59+}
60+
2861// IsDockerMCPEnabled checks if Docker MCP is already configured.
2962func (c *Config) IsDockerMCPEnabled() bool {
3063 if c.MCP == nil {
internal/ui/dialog/commands.go+34−2
@@ -37,6 +37,10 @@ const (
3737)
3838
3939// Commands represents a dialog that shows available commands.
40+type dockerMCPAvailabilityCheckedMsg struct {
41+ available bool
42+}
43+
4044type Commands struct {
4145 com *common.Common
4246 keyMap struct {
@@ -66,6 +70,9 @@ type Commands struct {
6670
6771 customCommands []commands.CustomCommand
6872 mcpPrompts []commands.MCPPrompt
73+
74+ dockerMCPAvailable *bool
75+ dockerMCPCheckInFlight bool
6976}
7077
7178var _ Dialog = (*Commands)(nil)
@@ -126,6 +133,10 @@ func NewCommands(com *common.Common, sessionID string, hasSession, hasTodos, has
126133 closeKey.SetHelp("esc", "cancel")
127134 c.keyMap.Close = closeKey
128135
136+ if available, known := config.DockerMCPAvailabilityCached(); known {
137+ c.dockerMCPAvailable = &available
138+ }
139+
129140 // Set initial commands
130141 c.setCommandItems(c.selected)
131142
@@ -145,6 +156,13 @@ func (c *Commands) ID() string {
145156// HandleMsg implements [Dialog].
146157func (c *Commands) HandleMsg(msg tea.Msg) Action {
147158 switch msg := msg.(type) {
159+ case dockerMCPAvailabilityCheckedMsg:
160+ c.dockerMCPAvailable = &msg.available
161+ c.dockerMCPCheckInFlight = false
162+ if c.selected == SystemCommands {
163+ c.setCommandItems(c.selected)
164+ }
165+ return nil
148166 case spinner.TickMsg:
149167 if c.loading {
150168 var cmd tea.Cmd
@@ -207,6 +225,20 @@ func (c *Commands) HandleMsg(msg tea.Msg) Action {
207225 return nil
208226}
209227
228+func checkDockerMCPAvailabilityCmd() tea.Cmd {
229+ return func() tea.Msg {
230+ return dockerMCPAvailabilityCheckedMsg{available: config.RefreshDockerMCPAvailability()}
231+ }
232+}
233+
234+func (c *Commands) InitialCmd() tea.Cmd {
235+ if c.dockerMCPAvailable != nil || c.dockerMCPCheckInFlight {
236+ return nil
237+ }
238+ c.dockerMCPCheckInFlight = true
239+ return checkDockerMCPAvailabilityCmd()
240+}
241+
210242// Cursor returns the cursor position relative to the dialog.
211243func (c *Commands) Cursor() *tea.Cursor {
212244 return InputCursor(c.com.Styles, c.input.Cursor())
@@ -446,8 +478,8 @@ func (c *Commands) defaultCommands() []*CommandItem {
446478 commands = append(commands, NewCommandItem(c.com.Styles, "open_external_editor", "Open External Editor", "ctrl+o", ActionExternalEditor{}))
447479 }
448480
449− // Add Docker MCP command if available and not already enabled
450− if config.IsDockerMCPAvailable() && !cfg.IsDockerMCPEnabled() {
481+ // Add Docker MCP command if available and not already enabled.
482+ if !cfg.IsDockerMCPEnabled() && c.dockerMCPAvailable != nil && *c.dockerMCPAvailable {
451483 commands = append(commands, NewCommandItem(c.com.Styles, "enable_docker_mcp", "Enable Docker MCP Catalog", "", ActionEnableDockerMCP{}))
452484 }
453485
internal/ui/model/ui.go+6−5
@@ -3017,7 +3017,7 @@ func (m *UI) openCommandsDialog() tea.Cmd {
30173017
30183018 m.dialog.OpenDialog(commands)
30193019
3020− return nil
3020+ return commands.InitialCmd()
30213021}
30223022
30233023// openReasoningDialog opens the reasoning effort dialog.
@@ -3473,16 +3473,17 @@ func (m *UI) enableDockerMCP() tea.Msg {
34733473
34743474 ctx := context.Background()
34753475 if err := mcp.InitializeSingle(ctx, config.DockerMCPName, store); err != nil {
3476− // Roll back in-memory state when startup fails.
3476+ // Roll back runtime and in-memory state when startup fails.
3477+ disableErr := mcp.DisableSingle(store, config.DockerMCPName)
34773478 delete(store.Config().MCP, config.DockerMCPName)
3478− return util.ReportError(fmt.Errorf("docker MCP enabled but failed to start: %w", err))()
3479+ return util.ReportError(fmt.Errorf("failed to start docker MCP: %w", errors.Join(err, disableErr)))()
34793480 }
34803481
34813482 if err := store.PersistDockerMCPConfig(mcpConfig); err != nil {
34823483 // Roll back runtime and in-memory state if persistence fails.
3483− _ = mcp.DisableSingle(store, config.DockerMCPName)
3484+ disableErr := mcp.DisableSingle(store, config.DockerMCPName)
34843485 delete(store.Config().MCP, config.DockerMCPName)
3485− return util.ReportError(fmt.Errorf("docker MCP started but failed to persist configuration: %w", err))()
3486+ return util.ReportError(fmt.Errorf("docker MCP started but failed to persist configuration: %w", errors.Join(err, disableErr)))()
34863487 }
34873488
34883489 return util.NewInfoMsg("Docker MCP enabled and started successfully")