refactor: reuse same dialog

Carlos Alexandro Becker created

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

Change summary

internal/tui/components/dialogs/commands/arguments.go     | 159 +++--
internal/tui/components/dialogs/commands/loader.go        |  35 
internal/tui/components/dialogs/commands/mcp_arguments.go | 262 ---------
internal/tui/tui.go                                       |  69 ++
4 files changed, 172 insertions(+), 353 deletions(-)

Detailed changes

internal/tui/components/dialogs/commands/arguments.go 🔗

@@ -1,8 +1,7 @@
 package commands
 
 import (
-	"fmt"
-	"strings"
+	"cmp"
 
 	"github.com/charmbracelet/bubbles/v2/help"
 	"github.com/charmbracelet/bubbles/v2/key"
@@ -20,9 +19,10 @@ const (
 
 // ShowArgumentsDialogMsg is a message that is sent to show the arguments dialog.
 type ShowArgumentsDialogMsg struct {
-	CommandID string
-	Content   string
-	ArgNames  []string
+	CommandID   string
+	Description string
+	Content     string
+	ArgNames    []string
 }
 
 // CloseArgumentsDialogMsg is a message that is sent when the arguments dialog is closed.
@@ -39,26 +39,39 @@ type CommandArgumentsDialog interface {
 }
 
 type commandArgumentsDialogCmp struct {
-	width   int
-	wWidth  int // Width of the terminal window
-	wHeight int // Height of the terminal window
-
-	inputs     []textinput.Model
-	focusIndex int
-	keys       ArgumentsDialogKeyMap
-	commandID  string
-	content    string
-	argNames   []string
-	help       help.Model
+	wWidth, wHeight int
+	width, height   int
+
+	inputs    []textinput.Model
+	focused   int
+	keys      ArgumentsDialogKeyMap
+	arguments []Argument
+	help      help.Model
+
+	id          string
+	title       string
+	name        string
+	description string
+
+	onSubmit func(args map[string]string) tea.Cmd
+}
+
+type Argument struct {
+	Name, Title, Description string
+	Required                 bool
 }
 
-func NewCommandArgumentsDialog(commandID, content string, argNames []string) CommandArgumentsDialog {
+func NewCommandArgumentsDialog(
+	id, title, name, description string,
+	arguments []Argument,
+	onSubmit func(args map[string]string) tea.Cmd,
+) CommandArgumentsDialog {
 	t := styles.CurrentTheme()
-	inputs := make([]textinput.Model, len(argNames))
+	inputs := make([]textinput.Model, len(arguments))
 
-	for i, name := range argNames {
+	for i, arg := range arguments {
 		ti := textinput.New()
-		ti.Placeholder = fmt.Sprintf("Enter value for %s...", name)
+		ti.Placeholder = cmp.Or(arg.Description, "Enter value for "+arg.Name)
 		ti.SetWidth(40)
 		ti.SetVirtualCursor(false)
 		ti.Prompt = ""
@@ -75,14 +88,16 @@ func NewCommandArgumentsDialog(commandID, content string, argNames []string) Com
 	}
 
 	return &commandArgumentsDialogCmp{
-		inputs:     inputs,
-		keys:       DefaultArgumentsDialogKeyMap(),
-		commandID:  commandID,
-		content:    content,
-		argNames:   argNames,
-		focusIndex: 0,
-		width:      60,
-		help:       help.New(),
+		inputs:      inputs,
+		keys:        DefaultArgumentsDialogKeyMap(),
+		id:          id,
+		name:        name,
+		title:       title,
+		description: description,
+		arguments:   arguments,
+		width:       60,
+		help:        help.New(),
+		onSubmit:    onSubmit,
 	}
 }
 
@@ -97,41 +112,45 @@ func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case tea.WindowSizeMsg:
 		c.wWidth = msg.Width
 		c.wHeight = msg.Height
+		c.width = min(90, c.wWidth)
+		c.height = min(15, c.wHeight)
+		for i := range c.inputs {
+			c.inputs[i].SetWidth(c.width - (paddingHorizontal * 2))
+		}
 	case tea.KeyPressMsg:
 		switch {
+		case key.Matches(msg, c.keys.Cancel):
+			return c, util.CmdHandler(dialogs.CloseDialogMsg{})
 		case key.Matches(msg, c.keys.Confirm):
-			if c.focusIndex == len(c.inputs)-1 {
-				content := c.content
-				for i, name := range c.argNames {
+			if c.focused == len(c.inputs)-1 {
+				args := make(map[string]string)
+				for i, arg := range c.arguments {
 					value := c.inputs[i].Value()
-					placeholder := "$" + name
-					content = strings.ReplaceAll(content, placeholder, value)
+					args[arg.Name] = value
 				}
 				return c, tea.Sequence(
 					util.CmdHandler(dialogs.CloseDialogMsg{}),
-					util.CmdHandler(CommandRunCustomMsg{
-						Content: content,
-					}),
+					c.onSubmit(args),
 				)
 			}
 			// Otherwise, move to the next input
-			c.inputs[c.focusIndex].Blur()
-			c.focusIndex++
-			c.inputs[c.focusIndex].Focus()
+			c.inputs[c.focused].Blur()
+			c.focused++
+			c.inputs[c.focused].Focus()
 		case key.Matches(msg, c.keys.Next):
 			// Move to the next input
-			c.inputs[c.focusIndex].Blur()
-			c.focusIndex = (c.focusIndex + 1) % len(c.inputs)
-			c.inputs[c.focusIndex].Focus()
+			c.inputs[c.focused].Blur()
+			c.focused = (c.focused + 1) % len(c.inputs)
+			c.inputs[c.focused].Focus()
 		case key.Matches(msg, c.keys.Previous):
 			// Move to the previous input
-			c.inputs[c.focusIndex].Blur()
-			c.focusIndex = (c.focusIndex - 1 + len(c.inputs)) % len(c.inputs)
-			c.inputs[c.focusIndex].Focus()
+			c.inputs[c.focused].Blur()
+			c.focused = (c.focused - 1 + len(c.inputs)) % len(c.inputs)
+			c.inputs[c.focused].Focus()
 
 		default:
 			var cmd tea.Cmd
-			c.inputs[c.focusIndex], cmd = c.inputs[c.focusIndex].Update(msg)
+			c.inputs[c.focused], cmd = c.inputs[c.focused].Update(msg)
 			return c, cmd
 		}
 	}
@@ -147,26 +166,27 @@ func (c *commandArgumentsDialogCmp) View() string {
 		Foreground(t.Primary).
 		Bold(true).
 		Padding(0, 1).
-		Render("Command Arguments")
+		Render(cmp.Or(c.title, c.name))
 
-	explanation := t.S().Text.
+	promptName := t.S().Text.
 		Padding(0, 1).
-		Render("This command requires arguments.")
+		Render(c.description)
 
-	// Create input fields for each argument
 	inputFields := make([]string, len(c.inputs))
 	for i, input := range c.inputs {
-		// Highlight the label of the focused input
-		labelStyle := baseStyle.
-			Padding(1, 1, 0, 1)
+		labelStyle := baseStyle.Padding(1, 1, 0, 1)
 
-		if i == c.focusIndex {
+		if i == c.focused {
 			labelStyle = labelStyle.Foreground(t.FgBase).Bold(true)
 		} else {
 			labelStyle = labelStyle.Foreground(t.FgMuted)
 		}
 
-		label := labelStyle.Render(c.argNames[i] + ":")
+		argName := c.arguments[i].Name
+		if c.arguments[i].Required {
+			argName += " *"
+		}
+		label := labelStyle.Render(argName + ":")
 
 		field := t.S().Text.
 			Padding(0, 1).
@@ -175,18 +195,14 @@ func (c *commandArgumentsDialogCmp) View() string {
 		inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
 	}
 
-	// Join all elements vertically
-	elements := []string{title, explanation}
+	elements := []string{title, promptName}
 	elements = append(elements, inputFields...)
 
 	c.help.ShowAll = false
 	helpText := baseStyle.Padding(0, 1).Render(c.help.View(c.keys))
 	elements = append(elements, "", helpText)
 
-	content := lipgloss.JoinVertical(
-		lipgloss.Left,
-		elements...,
-	)
+	content := lipgloss.JoinVertical(lipgloss.Left, elements...)
 
 	return baseStyle.Padding(1, 1, 0, 1).
 		Border(lipgloss.RoundedBorder()).
@@ -196,26 +212,33 @@ func (c *commandArgumentsDialogCmp) View() string {
 }
 
 func (c *commandArgumentsDialogCmp) Cursor() *tea.Cursor {
-	cursor := c.inputs[c.focusIndex].Cursor()
+	if len(c.inputs) == 0 {
+		return nil
+	}
+	cursor := c.inputs[c.focused].Cursor()
 	if cursor != nil {
 		cursor = c.moveCursor(cursor)
 	}
 	return cursor
 }
 
+const (
+	headerHeight      = 3
+	itemHeight        = 3
+	paddingHorizontal = 3
+)
+
 func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
 	row, col := c.Position()
-	offset := row + 3 + (1+c.focusIndex)*3
+	offset := row + headerHeight + (1+c.focused)*itemHeight
 	cursor.Y += offset
-	cursor.X = cursor.X + col + 3
+	cursor.X = cursor.X + col + paddingHorizontal
 	return cursor
 }
 
 func (c *commandArgumentsDialogCmp) Position() (int, int) {
-	row := c.wHeight / 2
-	row -= c.wHeight / 2
-	col := c.wWidth / 2
-	col -= c.width / 2
+	row := (c.wHeight / 2) - (c.height / 2)
+	col := (c.wWidth / 2) - (c.width / 2)
 	return row, col
 }
 

internal/tui/components/dialogs/commands/loader.go 🔗

@@ -1,6 +1,7 @@
 package commands
 
 import (
+	"cmp"
 	"context"
 	"fmt"
 	"io/fs"
@@ -20,9 +21,8 @@ import (
 )
 
 const (
-	UserCommandPrefix    = "user:"
-	ProjectCommandPrefix = "project:"
-	MCPPromptPrefix      = "mcp:"
+	userCommandPrefix    = "user:"
+	projectCommandPrefix = "project:"
 )
 
 var namedArgPattern = regexp.MustCompile(`\$([A-Z][A-Z0-9_]*)`)
@@ -56,7 +56,7 @@ func buildCommandSources(cfg *config.Config) []commandSource {
 	if dir := getXDGCommandsDir(); dir != "" {
 		sources = append(sources, commandSource{
 			path:   dir,
-			prefix: UserCommandPrefix,
+			prefix: userCommandPrefix,
 		})
 	}
 
@@ -64,14 +64,14 @@ func buildCommandSources(cfg *config.Config) []commandSource {
 	if home := home.Dir(); home != "" {
 		sources = append(sources, commandSource{
 			path:   filepath.Join(home, ".crush", "commands"),
-			prefix: UserCommandPrefix,
+			prefix: userCommandPrefix,
 		})
 	}
 
 	// Project directory
 	sources = append(sources, commandSource{
 		path:   filepath.Join(cfg.Options.DataDirectory, "commands"),
-		prefix: ProjectCommandPrefix,
+		prefix: projectCommandPrefix,
 	})
 
 	return sources
@@ -133,12 +133,13 @@ func (l *commandLoader) loadCommand(path, baseDir, prefix string) (Command, erro
 	}
 
 	id := buildCommandID(path, baseDir, prefix)
+	desc := fmt.Sprintf("Custom command from %s", filepath.Base(path))
 
 	return Command{
 		ID:          id,
 		Title:       id,
-		Description: fmt.Sprintf("Custom command from %s", filepath.Base(path)),
-		Handler:     createCommandHandler(id, string(content)),
+		Description: desc,
+		Handler:     createCommandHandler(id, desc, string(content)),
 	}, nil
 }
 
@@ -155,15 +156,16 @@ func buildCommandID(path, baseDir, prefix string) string {
 	return prefix + strings.Join(parts, ":")
 }
 
-func createCommandHandler(id string, content string) func(Command) tea.Cmd {
+func createCommandHandler(id, desc, content string) func(Command) tea.Cmd {
 	return func(cmd Command) tea.Cmd {
 		args := extractArgNames(content)
 
 		if len(args) > 0 {
 			return util.CmdHandler(ShowArgumentsDialogMsg{
-				CommandID: id,
-				Content:   content,
-				ArgNames:  args,
+				CommandID:   id,
+				Description: desc,
+				Content:     content,
+				ArgNames:    args,
 			})
 		}
 
@@ -220,14 +222,9 @@ func LoadMCPPrompts() []Command {
 			continue
 		}
 		clientName, promptName := parts[0], parts[1]
-
-		displayName := promptName
-		if p.Title != "" {
-			displayName = p.Title
-		}
-
+		displayName := clientName + " " + cmp.Or(p.Title, promptName)
 		commands = append(commands, Command{
-			ID:          MCPPromptPrefix + key,
+			ID:          key,
 			Title:       displayName,
 			Description: fmt.Sprintf("[%s] %s", clientName, p.Description),
 			Handler:     createMCPPromptHandler(key, promptName, p),

internal/tui/components/dialogs/commands/mcp_arguments.go 🔗

@@ -1,262 +0,0 @@
-package commands
-
-import (
-	"cmp"
-	"context"
-	"fmt"
-	"log/slog"
-	"strings"
-
-	"github.com/charmbracelet/bubbles/v2/help"
-	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/bubbles/v2/textinput"
-	tea "github.com/charmbracelet/bubbletea/v2"
-	"github.com/charmbracelet/lipgloss/v2"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-
-	"github.com/charmbracelet/crush/internal/llm/agent"
-	"github.com/charmbracelet/crush/internal/tui/components/chat"
-	"github.com/charmbracelet/crush/internal/tui/components/dialogs"
-	"github.com/charmbracelet/crush/internal/tui/styles"
-	"github.com/charmbracelet/crush/internal/tui/util"
-)
-
-const mcpArgumentsDialogID dialogs.DialogID = "mcp_arguments"
-
-type MCPPromptArgumentsDialog interface {
-	dialogs.DialogModel
-}
-
-type mcpPromptArgumentsDialogCmp struct {
-	wWidth, wHeight int
-	width, height   int
-	selected        int
-	inputs          []textinput.Model
-	keys            ArgumentsDialogKeyMap
-	id              string
-	prompt          *mcp.Prompt
-	help            help.Model
-}
-
-func NewMCPPromptArgumentsDialog(id, name string) MCPPromptArgumentsDialog {
-	id = strings.TrimPrefix(id, MCPPromptPrefix)
-	prompt, ok := agent.GetMCPPrompt(id)
-	if !ok {
-		return nil
-	}
-
-	t := styles.CurrentTheme()
-	inputs := make([]textinput.Model, len(prompt.Arguments))
-
-	for i, arg := range prompt.Arguments {
-		ti := textinput.New()
-		placeholder := fmt.Sprintf("Enter value for %s...", arg.Name)
-		if arg.Description != "" {
-			placeholder = arg.Description
-		}
-		ti.Placeholder = placeholder
-		ti.SetWidth(40)
-		ti.SetVirtualCursor(false)
-		ti.Prompt = ""
-		ti.SetStyles(t.S().TextInput)
-
-		if i == 0 {
-			ti.Focus()
-		} else {
-			ti.Blur()
-		}
-
-		inputs[i] = ti
-	}
-
-	return &mcpPromptArgumentsDialogCmp{
-		inputs: inputs,
-		keys:   DefaultArgumentsDialogKeyMap(),
-		id:     id,
-		prompt: prompt,
-		help:   help.New(),
-	}
-}
-
-func (c *mcpPromptArgumentsDialogCmp) Init() tea.Cmd {
-	return nil
-}
-
-func (c *mcpPromptArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
-	switch msg := msg.(type) {
-	case tea.WindowSizeMsg:
-		c.wWidth = msg.Width
-		c.wHeight = msg.Height
-		cmd := c.SetSize()
-		return c, cmd
-	case tea.KeyPressMsg:
-		switch {
-		case key.Matches(msg, c.keys.Cancel):
-			return c, util.CmdHandler(dialogs.CloseDialogMsg{})
-		case key.Matches(msg, c.keys.Confirm):
-			if c.selected == len(c.inputs)-1 {
-				args := make(map[string]string)
-				for i, arg := range c.prompt.Arguments {
-					value := c.inputs[i].Value()
-					args[arg.Name] = value
-				}
-				return c, tea.Sequence(
-					util.CmdHandler(dialogs.CloseDialogMsg{}),
-					c.executeMCPPrompt(args),
-				)
-			}
-			c.inputs[c.selected].Blur()
-			c.selected++
-			c.inputs[c.selected].Focus()
-		case key.Matches(msg, c.keys.Next):
-			c.inputs[c.selected].Blur()
-			c.selected = (c.selected + 1) % len(c.inputs)
-			c.inputs[c.selected].Focus()
-		case key.Matches(msg, c.keys.Previous):
-			c.inputs[c.selected].Blur()
-			c.selected = (c.selected - 1 + len(c.inputs)) % len(c.inputs)
-			c.inputs[c.selected].Focus()
-		default:
-			var cmd tea.Cmd
-			c.inputs[c.selected], cmd = c.inputs[c.selected].Update(msg)
-			return c, cmd
-		}
-	}
-	return c, nil
-}
-
-func (c *mcpPromptArgumentsDialogCmp) executeMCPPrompt(args map[string]string) tea.Cmd {
-	return func() tea.Msg {
-		parts := strings.SplitN(c.id, ":", 2)
-		if len(parts) != 2 {
-			return util.ReportError(fmt.Errorf("invalid prompt ID: %s", c.id))
-		}
-		clientName := parts[0]
-
-		ctx := context.Background()
-		slog.Warn("AQUI", "name", c.prompt.Name, "id", c.id)
-		result, err := agent.GetMCPPromptContent(ctx, clientName, c.prompt.Name, args)
-		if err != nil {
-			return util.ReportError(err)
-		}
-
-		var content strings.Builder
-		for _, msg := range result.Messages {
-			if msg.Role == "user" {
-				if textContent, ok := msg.Content.(*mcp.TextContent); ok {
-					content.WriteString(textContent.Text)
-					content.WriteString("\n")
-				}
-			}
-		}
-
-		return chat.SendMsg{
-			Text: content.String(),
-		}
-	}
-}
-
-func (c *mcpPromptArgumentsDialogCmp) View() string {
-	t := styles.CurrentTheme()
-	baseStyle := t.S().Base
-
-	title := lipgloss.NewStyle().
-		Foreground(t.Primary).
-		Bold(true).
-		Padding(0, 1).
-		Render(cmp.Or(c.prompt.Title, c.prompt.Name))
-
-	promptName := t.S().Text.
-		Padding(0, 1).
-		Render(c.prompt.Description)
-
-	if c.prompt == nil {
-		return baseStyle.Padding(1, 1, 0, 1).
-			Border(lipgloss.RoundedBorder()).
-			BorderForeground(t.BorderFocus).
-			Width(c.width).
-			Render(lipgloss.JoinVertical(lipgloss.Left, title, promptName, "", "Prompt not found"))
-	}
-
-	inputFields := make([]string, len(c.inputs))
-	for i, input := range c.inputs {
-		labelStyle := baseStyle.Padding(1, 1, 0, 1)
-
-		if i == c.selected {
-			labelStyle = labelStyle.Foreground(t.FgBase).Bold(true)
-		} else {
-			labelStyle = labelStyle.Foreground(t.FgMuted)
-		}
-
-		argName := c.prompt.Arguments[i].Name
-		if c.prompt.Arguments[i].Required {
-			argName += " *"
-		}
-		label := labelStyle.Render(argName + ":")
-
-		field := t.S().Text.
-			Padding(0, 1).
-			Render(input.View())
-
-		inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
-	}
-
-	elements := []string{title, promptName}
-	elements = append(elements, inputFields...)
-
-	c.help.ShowAll = false
-	helpText := baseStyle.Padding(0, 1).Render(c.help.View(c.keys))
-	elements = append(elements, "", helpText)
-
-	content := lipgloss.JoinVertical(lipgloss.Left, elements...)
-
-	return baseStyle.Padding(1, 1, 0, 1).
-		Border(lipgloss.RoundedBorder()).
-		BorderForeground(t.BorderFocus).
-		Width(c.width).
-		Render(content)
-}
-
-func (c *mcpPromptArgumentsDialogCmp) Cursor() *tea.Cursor {
-	if len(c.inputs) == 0 {
-		return nil
-	}
-	cursor := c.inputs[c.selected].Cursor()
-	if cursor != nil {
-		cursor = c.moveCursor(cursor)
-	}
-	return cursor
-}
-
-const (
-	headerHeight      = 3
-	itemHeight        = 3
-	paddingHorizontal = 3
-)
-
-func (c *mcpPromptArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
-	row, col := c.Position()
-	offset := row + headerHeight + (1+c.selected)*itemHeight
-	cursor.Y += offset
-	cursor.X = cursor.X + col + paddingHorizontal
-	return cursor
-}
-
-func (c *mcpPromptArgumentsDialogCmp) SetSize() tea.Cmd {
-	c.width = min(90, c.wWidth)
-	c.height = min(15, c.wHeight)
-	for i := range c.inputs {
-		c.inputs[i].SetWidth(c.width - (paddingHorizontal * 2))
-	}
-	return nil
-}
-
-func (c *mcpPromptArgumentsDialogCmp) Position() (int, int) {
-	row := (c.wHeight / 2) - (c.height / 2)
-	col := (c.wWidth / 2) - (c.width / 2)
-	return row, col
-}
-
-func (c *mcpPromptArgumentsDialogCmp) ID() dialogs.DialogID {
-	return mcpArgumentsDialogID
-}

internal/tui/tui.go 🔗

@@ -3,6 +3,7 @@ package tui
 import (
 	"context"
 	"fmt"
+	"log/slog"
 	"math/rand"
 	"strings"
 	"time"
@@ -34,6 +35,7 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
 	"github.com/charmbracelet/lipgloss/v2"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 )
 
 var lastMouseEvent time.Time
@@ -138,21 +140,80 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		a.dialog = u.(dialogs.DialogCmp)
 		return a, tea.Batch(completionCmd, dialogCmd)
 	case commands.ShowArgumentsDialogMsg:
+		var args []commands.Argument
+		for _, arg := range msg.ArgNames {
+			args = append(args, commands.Argument{Name: arg})
+		}
 		return a, util.CmdHandler(
 			dialogs.OpenDialogMsg{
 				Model: commands.NewCommandArgumentsDialog(
 					msg.CommandID,
-					msg.Content,
-					msg.ArgNames,
+					msg.CommandID,
+					msg.CommandID,
+					msg.Description,
+					args,
+					func(args map[string]string) tea.Cmd {
+						return func() tea.Msg {
+							content := msg.Content
+							for _, name := range msg.ArgNames {
+								value := args[name]
+								placeholder := "$" + name
+								content = strings.ReplaceAll(content, placeholder, value)
+							}
+							return commands.CommandRunCustomMsg{
+								Content: content,
+							}
+						}
+					},
 				),
 			},
 		)
 	case commands.ShowMCPPromptArgumentsDialogMsg:
-		dialog := commands.NewMCPPromptArgumentsDialog(msg.PromptID, msg.PromptName)
-		if dialog == nil {
+		prompt, ok := agent.GetMCPPrompt(msg.PromptID)
+		if !ok {
+			slog.Warn("prompt not found", "prompt_id", msg.PromptID, "prompt_name", msg.PromptName)
 			util.ReportWarn(fmt.Sprintf("Prompt %s not found", msg.PromptName))
 			return a, nil
 		}
+		args := make([]commands.Argument, 0, len(prompt.Arguments))
+		for _, arg := range prompt.Arguments {
+			args = append(args, commands.Argument(*arg))
+		}
+		dialog := commands.NewCommandArgumentsDialog(
+			msg.PromptID,
+			prompt.Title,
+			prompt.Name,
+			prompt.Description,
+			args,
+			func(args map[string]string) tea.Cmd {
+				return func() tea.Msg {
+					parts := strings.SplitN(msg.PromptID, ":", 2)
+					if len(parts) != 2 {
+						return util.ReportError(fmt.Errorf("invalid prompt ID: %s", msg.PromptID))
+					}
+					clientName := parts[0]
+
+					ctx := context.Background()
+					result, err := agent.GetMCPPromptContent(ctx, clientName, prompt.Name, args)
+					if err != nil {
+						return util.ReportError(err)
+					}
+
+					var content strings.Builder
+					for _, msg := range result.Messages {
+						if msg.Role == "user" {
+							if textContent, ok := msg.Content.(*mcp.TextContent); ok {
+								content.WriteString(textContent.Text)
+								content.WriteString("\n")
+							}
+						}
+					}
+					return cmpChat.SendMsg{
+						Text: content.String(),
+					}
+				}
+			},
+		)
 		return a, util.CmdHandler(
 			dialogs.OpenDialogMsg{
 				Model: dialog,