on main help sign in
fix(ui/docker): don't block ui when checking for docker desktop
6003105Christian Rocha
internal/config/docker_mcp.go+33
@@ -4,6 +4,7 @@ import (
4 "context"
4 "context"
5 "fmt"
5 "fmt"
6 "os/exec"
6 "os/exec"
7+ "sync"
7 "time"
8 "time"
8)
9)
9
10
@@ -12,6 +13,15 @@ var dockerMCPVersionRunner = func(ctx context.Context) error {
12 return cmd.Run()
13 return cmd.Run()
13}
14}
14
15
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+
15// DockerMCPName is the name of the Docker MCP configuration.
25// DockerMCPName is the name of the Docker MCP configuration.
16const DockerMCPName = "docker"
26const DockerMCPName = "docker"
17
27
@@ -25,6 +35,29 @@ func IsDockerMCPAvailable() bool {
25 return err == nil
35 return err == nil
26}
36}
27
37
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+
28// IsDockerMCPEnabled checks if Docker MCP is already configured.
61// IsDockerMCPEnabled checks if Docker MCP is already configured.
29func (c *Config) IsDockerMCPEnabled() bool {
62func (c *Config) IsDockerMCPEnabled() bool {
30 if c.MCP == nil {
63 if c.MCP == nil {
internal/ui/dialog/commands.go+34−2
@@ -37,6 +37,10 @@ const (
37)
37)
38
38
39// Commands represents a dialog that shows available commands.
39// Commands represents a dialog that shows available commands.
40+type dockerMCPAvailabilityCheckedMsg struct {
41+ available bool
42+}
43+
40type Commands struct {
44type Commands struct {
41 com *common.Common
45 com *common.Common
42 keyMap struct {
46 keyMap struct {
@@ -66,6 +70,9 @@ type Commands struct {
66
70
67 customCommands []commands.CustomCommand
71 customCommands []commands.CustomCommand
68 mcpPrompts []commands.MCPPrompt
72 mcpPrompts []commands.MCPPrompt
73+
74+ dockerMCPAvailable *bool
75+ dockerMCPCheckInFlight bool
69}
76}
70
77
71var _ Dialog = (*Commands)(nil)
78var _ Dialog = (*Commands)(nil)
@@ -126,6 +133,10 @@ func NewCommands(com *common.Common, sessionID string, hasSession, hasTodos, has
126 closeKey.SetHelp("esc", "cancel")
133 closeKey.SetHelp("esc", "cancel")
127 c.keyMap.Close = closeKey
134 c.keyMap.Close = closeKey
128
135
136+ if available, known := config.DockerMCPAvailabilityCached(); known {
137+ c.dockerMCPAvailable = &available
138+ }
139+
129 // Set initial commands
140 // Set initial commands
130 c.setCommandItems(c.selected)
141 c.setCommandItems(c.selected)
131
142
@@ -145,6 +156,13 @@ func (c *Commands) ID() string {
145// HandleMsg implements [Dialog].
156// HandleMsg implements [Dialog].
146func (c *Commands) HandleMsg(msg tea.Msg) Action {
157func (c *Commands) HandleMsg(msg tea.Msg) Action {
147 switch msg := msg.(type) {
158 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
148 case spinner.TickMsg:
166 case spinner.TickMsg:
149 if c.loading {
167 if c.loading {
150 var cmd tea.Cmd
168 var cmd tea.Cmd
@@ -207,6 +225,20 @@ func (c *Commands) HandleMsg(msg tea.Msg) Action {
207 return nil
225 return nil
208}
226}
209
227
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+
210// Cursor returns the cursor position relative to the dialog.
242// Cursor returns the cursor position relative to the dialog.
211func (c *Commands) Cursor() *tea.Cursor {
243func (c *Commands) Cursor() *tea.Cursor {
212 return InputCursor(c.com.Styles, c.input.Cursor())
244 return InputCursor(c.com.Styles, c.input.Cursor())
@@ -446,8 +478,8 @@ func (c *Commands) defaultCommands() []*CommandItem {
446 commands = append(commands, NewCommandItem(c.com.Styles, "open_external_editor", "Open External Editor", "ctrl+o", ActionExternalEditor{}))
478 commands = append(commands, NewCommandItem(c.com.Styles, "open_external_editor", "Open External Editor", "ctrl+o", ActionExternalEditor{}))
447 }
479 }
448
480
449− // Add Docker MCP command if available and not already enabled
481+ // Add Docker MCP command if available and not already enabled.
450− if config.IsDockerMCPAvailable() && !cfg.IsDockerMCPEnabled() {
482+ if !cfg.IsDockerMCPEnabled() && c.dockerMCPAvailable != nil && *c.dockerMCPAvailable {
451 commands = append(commands, NewCommandItem(c.com.Styles, "enable_docker_mcp", "Enable Docker MCP Catalog", "", ActionEnableDockerMCP{}))
483 commands = append(commands, NewCommandItem(c.com.Styles, "enable_docker_mcp", "Enable Docker MCP Catalog", "", ActionEnableDockerMCP{}))
452 }
484 }
453
485
internal/ui/model/ui.go+6−5
@@ -3017,7 +3017,7 @@ func (m *UI) openCommandsDialog() tea.Cmd {
3017
3017
3018 m.dialog.OpenDialog(commands)
3018 m.dialog.OpenDialog(commands)
3019
3019
3020− return nil
3020+ return commands.InitialCmd()
3021}
3021}
3022
3022
3023// openReasoningDialog opens the reasoning effort dialog.
3023// openReasoningDialog opens the reasoning effort dialog.
@@ -3473,16 +3473,17 @@ func (m *UI) enableDockerMCP() tea.Msg {
3473
3473
3474 ctx := context.Background()
3474 ctx := context.Background()
3475 if err := mcp.InitializeSingle(ctx, config.DockerMCPName, store); err != nil {
3475 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)
3477 delete(store.Config().MCP, config.DockerMCPName)
3478 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)))()
3479 }
3480 }
3480
3481
3481 if err := store.PersistDockerMCPConfig(mcpConfig); err != nil {
3482 if err := store.PersistDockerMCPConfig(mcpConfig); err != nil {
3482 // Roll back runtime and in-memory state if persistence fails.
3483 // Roll back runtime and in-memory state if persistence fails.
3483− _ = mcp.DisableSingle(store, config.DockerMCPName)
3484+ disableErr := mcp.DisableSingle(store, config.DockerMCPName)
3484 delete(store.Config().MCP, config.DockerMCPName)
3485 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)))()
3486 }
3487 }
3487
3488
3488 return util.NewInfoMsg("Docker MCP enabled and started successfully")
3489 return util.NewInfoMsg("Docker MCP enabled and started successfully")