1package commands
2
3import (
4 "cmp"
5 "context"
6 "fmt"
7 "log/slog"
8 "strings"
9
10 "github.com/charmbracelet/bubbles/v2/help"
11 "github.com/charmbracelet/bubbles/v2/key"
12 "github.com/charmbracelet/bubbles/v2/textinput"
13 tea "github.com/charmbracelet/bubbletea/v2"
14 "github.com/charmbracelet/lipgloss/v2"
15 "github.com/modelcontextprotocol/go-sdk/mcp"
16
17 "github.com/charmbracelet/crush/internal/llm/agent"
18 "github.com/charmbracelet/crush/internal/tui/components/chat"
19 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
20 "github.com/charmbracelet/crush/internal/tui/styles"
21 "github.com/charmbracelet/crush/internal/tui/util"
22)
23
24const mcpArgumentsDialogID dialogs.DialogID = "mcp_arguments"
25
26type MCPPromptArgumentsDialog interface {
27 dialogs.DialogModel
28}
29
30type mcpPromptArgumentsDialogCmp struct {
31 wWidth, wHeight int
32 width, height int
33 selected int
34 inputs []textinput.Model
35 keys ArgumentsDialogKeyMap
36 id string
37 prompt *mcp.Prompt
38 help help.Model
39}
40
41func NewMCPPromptArgumentsDialog(id, name string) MCPPromptArgumentsDialog {
42 id = strings.TrimPrefix(id, MCPPromptPrefix)
43 prompt, ok := agent.GetMCPPrompt(id)
44 if !ok {
45 return nil
46 }
47
48 t := styles.CurrentTheme()
49 inputs := make([]textinput.Model, len(prompt.Arguments))
50
51 for i, arg := range prompt.Arguments {
52 ti := textinput.New()
53 placeholder := fmt.Sprintf("Enter value for %s...", arg.Name)
54 if arg.Description != "" {
55 placeholder = arg.Description
56 }
57 ti.Placeholder = placeholder
58 ti.SetWidth(40)
59 ti.SetVirtualCursor(false)
60 ti.Prompt = ""
61 ti.SetStyles(t.S().TextInput)
62
63 if i == 0 {
64 ti.Focus()
65 } else {
66 ti.Blur()
67 }
68
69 inputs[i] = ti
70 }
71
72 return &mcpPromptArgumentsDialogCmp{
73 inputs: inputs,
74 keys: DefaultArgumentsDialogKeyMap(),
75 id: id,
76 prompt: prompt,
77 help: help.New(),
78 }
79}
80
81func (c *mcpPromptArgumentsDialogCmp) Init() tea.Cmd {
82 return nil
83}
84
85func (c *mcpPromptArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
86 switch msg := msg.(type) {
87 case tea.WindowSizeMsg:
88 c.wWidth = msg.Width
89 c.wHeight = msg.Height
90 cmd := c.SetSize()
91 return c, cmd
92 case tea.KeyPressMsg:
93 switch {
94 case key.Matches(msg, c.keys.Cancel):
95 return c, util.CmdHandler(dialogs.CloseDialogMsg{})
96 case key.Matches(msg, c.keys.Confirm):
97 if c.selected == len(c.inputs)-1 {
98 args := make(map[string]string)
99 for i, arg := range c.prompt.Arguments {
100 value := c.inputs[i].Value()
101 args[arg.Name] = value
102 }
103 return c, tea.Sequence(
104 util.CmdHandler(dialogs.CloseDialogMsg{}),
105 c.executeMCPPrompt(args),
106 )
107 }
108 c.inputs[c.selected].Blur()
109 c.selected++
110 c.inputs[c.selected].Focus()
111 case key.Matches(msg, c.keys.Next):
112 c.inputs[c.selected].Blur()
113 c.selected = (c.selected + 1) % len(c.inputs)
114 c.inputs[c.selected].Focus()
115 case key.Matches(msg, c.keys.Previous):
116 c.inputs[c.selected].Blur()
117 c.selected = (c.selected - 1 + len(c.inputs)) % len(c.inputs)
118 c.inputs[c.selected].Focus()
119 default:
120 var cmd tea.Cmd
121 c.inputs[c.selected], cmd = c.inputs[c.selected].Update(msg)
122 return c, cmd
123 }
124 }
125 return c, nil
126}
127
128func (c *mcpPromptArgumentsDialogCmp) executeMCPPrompt(args map[string]string) tea.Cmd {
129 return func() tea.Msg {
130 parts := strings.SplitN(c.id, ":", 2)
131 if len(parts) != 2 {
132 return util.ReportError(fmt.Errorf("invalid prompt ID: %s", c.id))
133 }
134 clientName := parts[0]
135
136 ctx := context.Background()
137 slog.Warn("AQUI", "name", c.prompt.Name, "id", c.id)
138 result, err := agent.GetMCPPromptContent(ctx, clientName, c.prompt.Name, args)
139 if err != nil {
140 return util.ReportError(err)
141 }
142
143 var content strings.Builder
144 for _, msg := range result.Messages {
145 if msg.Role == "user" {
146 if textContent, ok := msg.Content.(*mcp.TextContent); ok {
147 content.WriteString(textContent.Text)
148 content.WriteString("\n")
149 }
150 }
151 }
152
153 return chat.SendMsg{
154 Text: content.String(),
155 }
156 }
157}
158
159func (c *mcpPromptArgumentsDialogCmp) View() string {
160 t := styles.CurrentTheme()
161 baseStyle := t.S().Base
162
163 title := lipgloss.NewStyle().
164 Foreground(t.Primary).
165 Bold(true).
166 Padding(0, 1).
167 Render(cmp.Or(c.prompt.Title, c.prompt.Name))
168
169 promptName := t.S().Text.
170 Padding(0, 1).
171 Render(c.prompt.Description)
172
173 if c.prompt == nil {
174 return baseStyle.Padding(1, 1, 0, 1).
175 Border(lipgloss.RoundedBorder()).
176 BorderForeground(t.BorderFocus).
177 Width(c.width).
178 Render(lipgloss.JoinVertical(lipgloss.Left, title, promptName, "", "Prompt not found"))
179 }
180
181 inputFields := make([]string, len(c.inputs))
182 for i, input := range c.inputs {
183 labelStyle := baseStyle.Padding(1, 1, 0, 1)
184
185 if i == c.selected {
186 labelStyle = labelStyle.Foreground(t.FgBase).Bold(true)
187 } else {
188 labelStyle = labelStyle.Foreground(t.FgMuted)
189 }
190
191 argName := c.prompt.Arguments[i].Name
192 if c.prompt.Arguments[i].Required {
193 argName += " *"
194 }
195 label := labelStyle.Render(argName + ":")
196
197 field := t.S().Text.
198 Padding(0, 1).
199 Render(input.View())
200
201 inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
202 }
203
204 elements := []string{title, promptName}
205 elements = append(elements, inputFields...)
206
207 c.help.ShowAll = false
208 helpText := baseStyle.Padding(0, 1).Render(c.help.View(c.keys))
209 elements = append(elements, "", helpText)
210
211 content := lipgloss.JoinVertical(lipgloss.Left, elements...)
212
213 return baseStyle.Padding(1, 1, 0, 1).
214 Border(lipgloss.RoundedBorder()).
215 BorderForeground(t.BorderFocus).
216 Width(c.width).
217 Render(content)
218}
219
220func (c *mcpPromptArgumentsDialogCmp) Cursor() *tea.Cursor {
221 if len(c.inputs) == 0 {
222 return nil
223 }
224 cursor := c.inputs[c.selected].Cursor()
225 if cursor != nil {
226 cursor = c.moveCursor(cursor)
227 }
228 return cursor
229}
230
231const (
232 headerHeight = 3
233 itemHeight = 3
234 paddingHorizontal = 3
235)
236
237func (c *mcpPromptArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
238 row, col := c.Position()
239 offset := row + headerHeight + (1+c.selected)*itemHeight
240 cursor.Y += offset
241 cursor.X = cursor.X + col + paddingHorizontal
242 return cursor
243}
244
245func (c *mcpPromptArgumentsDialogCmp) SetSize() tea.Cmd {
246 c.width = min(90, c.wWidth)
247 c.height = min(15, c.wHeight)
248 for i := range c.inputs {
249 c.inputs[i].SetWidth(c.width - (paddingHorizontal * 2))
250 }
251 return nil
252}
253
254func (c *mcpPromptArgumentsDialogCmp) Position() (int, int) {
255 row := (c.wHeight / 2) - (c.height / 2)
256 col := (c.wWidth / 2) - (c.width / 2)
257 return row, col
258}
259
260func (c *mcpPromptArgumentsDialogCmp) ID() dialogs.DialogID {
261 return mcpArgumentsDialogID
262}