diff --git a/internal/fsext/ls.go b/internal/fsext/ls.go index 80d25a57f..c22b960ad 100644 --- a/internal/fsext/ls.go +++ b/internal/fsext/ls.go @@ -210,7 +210,7 @@ func ListDirectory(initialPath string, ignorePatterns []string, depth, limit int found := csync.NewSlice[string]() dl := NewDirectoryLister(initialPath) - slog.Warn("listing directory", "path", initialPath, "depth", depth, "limit", limit, "ignorePatterns", ignorePatterns) + slog.Debug("listing directory", "path", initialPath, "depth", depth, "limit", limit, "ignorePatterns", ignorePatterns) conf := fastwalk.Config{ Follow: true, diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index 9493ad327..a0ed18a85 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -176,9 +176,9 @@ func NewAgent( } baseToolsFn := func() map[string]tools.BaseTool { - slog.Info("Initializing agent base tools", "agent", agentCfg.ID) + slog.Debug("Initializing agent base tools", "agent", agentCfg.ID) defer func() { - slog.Info("Initialized agent base tools", "agent", agentCfg.ID) + slog.Debug("Initialized agent base tools", "agent", agentCfg.ID) }() // Base tools available to all agents @@ -202,9 +202,9 @@ func NewAgent( return result } mcpToolsFn := func() map[string]tools.BaseTool { - slog.Info("Initializing agent mcp tools", "agent", agentCfg.ID) + slog.Debug("Initializing agent mcp tools", "agent", agentCfg.ID) defer func() { - slog.Info("Initialized agent mcp tools", "agent", agentCfg.ID) + slog.Debug("Initialized agent mcp tools", "agent", agentCfg.ID) }() mcpToolsOnce.Do(func() { @@ -346,9 +346,6 @@ func (a *agent) err(err error) AgentEvent { } func (a *agent) Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error) { - if !a.Model().SupportsImages && attachments != nil { - attachments = nil - } events := make(chan AgentEvent, 1) if a.IsSessionBusy(sessionID) { existing, ok := a.promptQueue.Get(sessionID) @@ -371,7 +368,15 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac }) var attachmentParts []message.ContentPart for _, attachment := range attachments { - attachmentParts = append(attachmentParts, message.BinaryContent{Path: attachment.FilePath, MIMEType: attachment.MimeType, Data: attachment.Content}) + if !a.Model().SupportsImages && strings.HasPrefix(attachment.MimeType, "image/") { + slog.Warn("Model does not support images, skipping attachment", "mimeType", attachment.MimeType, "fileName", attachment.FileName) + continue + } + attachmentParts = append(attachmentParts, message.BinaryContent{ + Path: attachment.FilePath, + MIMEType: attachment.MimeType, + Data: attachment.Content, + }) } result := a.processGeneration(genCtx, sessionID, content, attachmentParts) if result.Error != nil { diff --git a/internal/llm/agent/mcp-tools.go b/internal/llm/agent/mcp-tools.go index e263152bd..243b3bc14 100644 --- a/internal/llm/agent/mcp-tools.go +++ b/internal/llm/agent/mcp-tools.go @@ -69,8 +69,9 @@ type MCPEvent struct { // MCPCounts number of available tools, prompts, etc. type MCPCounts struct { - Tools int - Prompts int + Tools int + Prompts int + Resources int } // MCPClientInfo holds information about an MCP client's state @@ -84,14 +85,16 @@ type MCPClientInfo struct { } var ( - mcpToolsOnce sync.Once - mcpTools = csync.NewMap[string, tools.BaseTool]() - mcpClient2Tools = csync.NewMap[string, []tools.BaseTool]() - mcpClients = csync.NewMap[string, *mcp.ClientSession]() - mcpStates = csync.NewMap[string, MCPClientInfo]() - mcpBroker = pubsub.NewBroker[MCPEvent]() - mcpPrompts = csync.NewMap[string, *mcp.Prompt]() - mcpClient2Prompts = csync.NewMap[string, []*mcp.Prompt]() + mcpToolsOnce sync.Once + mcpTools = csync.NewMap[string, tools.BaseTool]() + mcpClient2Tools = csync.NewMap[string, []tools.BaseTool]() + mcpClients = csync.NewMap[string, *mcp.ClientSession]() + mcpStates = csync.NewMap[string, MCPClientInfo]() + mcpBroker = pubsub.NewBroker[MCPEvent]() + mcpPrompts = csync.NewMap[string, *mcp.Prompt]() + mcpClient2Prompts = csync.NewMap[string, []*mcp.Prompt]() + mcpResources = csync.NewMap[string, *mcp.Resource]() + mcpClient2Resources = csync.NewMap[string, []*mcp.Resource]() ) type McpTool struct { @@ -327,12 +330,22 @@ func doGetMCPTools(ctx context.Context, permissions permission.Service, cfg *con return } + resources, err := getResources(ctx, c) + if err != nil { + slog.Error("error listing resources", "error", err) + updateMCPState(name, MCPStateError, err, nil, MCPCounts{}) + c.Close() + return + } + updateMcpTools(name, tools) updateMcpPrompts(name, prompts) + updateMcpResources(name, resources) mcpClients.Set(name, c) counts := MCPCounts{ - Tools: len(tools), - Prompts: len(prompts), + Tools: len(tools), + Prompts: len(prompts), + Resources: len(resources), } updateMCPState(name, MCPStateConnected, nil, c, counts) }(name, m) @@ -496,6 +509,32 @@ func updateMcpPrompts(mcpName string, prompts []*mcp.Prompt) { } } +func getResources(ctx context.Context, c *mcp.ClientSession) ([]*mcp.Resource, error) { + if c.InitializeResult().Capabilities.Resources == nil { + return nil, nil + } + result, err := c.ListResources(ctx, &mcp.ListResourcesParams{}) + if err != nil { + return nil, err + } + return result.Resources, nil +} + +// updateMcpResources updates the global mcpResources and mcpClient2Resources maps. +func updateMcpResources(mcpName string, resources []*mcp.Resource) { + if len(resources) == 0 { + mcpClient2Resources.Del(mcpName) + } else { + mcpClient2Resources.Set(mcpName, resources) + } + for clientName, resources := range mcpClient2Resources.Seq2() { + for _, p := range resources { + key := clientName + ":" + p.Name + mcpResources.Set(key, p) + } + } +} + // GetMCPPrompts returns all available MCP prompts. func GetMCPPrompts() map[string]*mcp.Prompt { return maps.Collect(mcpPrompts.Seq2()) @@ -523,3 +562,30 @@ func GetMCPPromptContent(ctx context.Context, clientName, promptName string, arg Arguments: args, }) } + +// GetMCPResources returns all available MCP resources. +func GetMCPResources() map[string]*mcp.Resource { + return maps.Collect(mcpResources.Seq2()) +} + +// GetMCPResource returns a specific MCP resource by name. +func GetMCPResource(name string) (*mcp.Resource, bool) { + return mcpResources.Get(name) +} + +// GetMCPResourcesByClient returns all resources for a specific MCP client. +func GetMCPResourcesByClient(clientName string) ([]*mcp.Resource, bool) { + return mcpClient2Resources.Get(clientName) +} + +// GetMCPResourceContent retrieves the content of an MCP resource. +func GetMCPResourceContent(ctx context.Context, clientName, uri string) (*mcp.ReadResourceResult, error) { + c, err := getOrRenewClient(ctx, clientName) + if err != nil { + return nil, err + } + + return c.ReadResource(ctx, &mcp.ReadResourceParams{ + URI: uri, + }) +} diff --git a/internal/llm/provider/anthropic.go b/internal/llm/provider/anthropic.go index 981ff4590..be9ec8454 100644 --- a/internal/llm/provider/anthropic.go +++ b/internal/llm/provider/anthropic.go @@ -126,9 +126,16 @@ func (a *anthropicClient) convertMessages(messages []message.Message) (anthropic var contentBlocks []anthropic.ContentBlockParamUnion contentBlocks = append(contentBlocks, content) for _, binaryContent := range msg.BinaryContent() { - base64Image := binaryContent.String(catwalk.InferenceProviderAnthropic) - imageBlock := anthropic.NewImageBlockBase64(binaryContent.MIMEType, base64Image) - contentBlocks = append(contentBlocks, imageBlock) + if strings.HasPrefix(binaryContent.MIMEType, "image/") { + base64Image := binaryContent.String(catwalk.InferenceProviderAnthropic) + imageBlock := anthropic.NewImageBlockBase64(binaryContent.MIMEType, base64Image) + contentBlocks = append(contentBlocks, imageBlock) + continue + } + blk := anthropic.NewDocumentBlock(anthropic.PlainTextSourceParam{ + Data: string(binaryContent.Data), + }) + contentBlocks = append(contentBlocks, blk) } anthropicMessages = append(anthropicMessages, anthropic.NewUserMessage(contentBlocks...)) diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index f70a0a3db..51277929d 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -26,6 +26,7 @@ import ( "github.com/charmbracelet/crush/internal/tui/components/dialogs" "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands" "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker" + "github.com/charmbracelet/crush/internal/tui/components/dialogs/mcp" "github.com/charmbracelet/crush/internal/tui/components/dialogs/quit" "github.com/charmbracelet/crush/internal/tui/styles" "github.com/charmbracelet/crush/internal/tui/util" @@ -179,10 +180,13 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.repositionCompletions case filepicker.FilePickedMsg: if len(m.attachments) >= maxAttachments { - return m, util.ReportError(fmt.Errorf("cannot add more than %d images", maxAttachments)) + // TODO: check if this still needed + return m, util.ReportError(fmt.Errorf("cannot add more than %d attachments", maxAttachments)) } m.attachments = append(m.attachments, msg.Attachment) return m, nil + case mcp.ResourcePickedMsg: + m.attachments = append(m.attachments, msg.Attachment) case completions.CompletionsOpenedMsg: m.isCompletionsOpen = true case completions.CompletionsClosedMsg: @@ -458,16 +462,21 @@ func (m *editorCmp) attachmentsContent() string { Background(t.FgMuted). Foreground(t.FgBase) for i, attachment := range m.attachments { - var filename string + icon := styles.DocumentIcon + if strings.HasPrefix(attachment.MimeType, "image/") { + icon = styles.ImageIcon + } + + var item string if len(attachment.FileName) > 10 { - filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, attachment.FileName[0:7]) + item = fmt.Sprintf(" %s %s...", icon, attachment.FileName[0:7]) } else { - filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, attachment.FileName) + item = fmt.Sprintf(" %s %s", icon, attachment.FileName) } if m.deleteMode { - filename = fmt.Sprintf("%d%s", i, filename) + item = fmt.Sprintf("%d%s", i, item) } - styledAttachments = append(styledAttachments, attachmentStyles.Render(filename)) + styledAttachments = append(styledAttachments, attachmentStyles.Render(item)) } content := lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...) return content diff --git a/internal/tui/components/dialogs/commands/commands.go b/internal/tui/components/dialogs/commands/commands.go index dc8289dc5..98e95eaa7 100644 --- a/internal/tui/components/dialogs/commands/commands.go +++ b/internal/tui/components/dialogs/commands/commands.go @@ -79,6 +79,7 @@ type ( SwitchModelMsg struct{} QuitMsg struct{} OpenFilePickerMsg struct{} + OpenResourcePickerMsg struct{} ToggleHelpMsg struct{} ToggleCompactModeMsg struct{} ToggleThinkingMsg struct{} diff --git a/internal/tui/components/dialogs/mcp/keys.go b/internal/tui/components/dialogs/mcp/keys.go new file mode 100644 index 000000000..88021af9f --- /dev/null +++ b/internal/tui/components/dialogs/mcp/keys.go @@ -0,0 +1,33 @@ +package mcp + +import ( + "github.com/charmbracelet/bubbles/v2/key" +) + +type KeyMap struct { + Close key.Binding + Select key.Binding +} + +func DefaultKeyMap() KeyMap { + return KeyMap{ + Close: key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "close"), + ), + Select: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "select"), + ), + } +} + +func (k KeyMap) ShortHelp() []key.Binding { + return []key.Binding{k.Select, k.Close} +} + +func (k KeyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + {k.Select, k.Close}, + } +} diff --git a/internal/tui/components/dialogs/mcp/resourcepicker.go b/internal/tui/components/dialogs/mcp/resourcepicker.go new file mode 100644 index 000000000..2200d7ae9 --- /dev/null +++ b/internal/tui/components/dialogs/mcp/resourcepicker.go @@ -0,0 +1,231 @@ +package mcp + +import ( + "cmp" + "context" + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/v2/help" + "github.com/charmbracelet/bubbles/v2/key" + "github.com/charmbracelet/bubbles/v2/list" + tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/crush/internal/llm/agent" + "github.com/charmbracelet/crush/internal/message" + "github.com/charmbracelet/crush/internal/tui/components/core" + "github.com/charmbracelet/crush/internal/tui/components/dialogs" + "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" +) + +const ( + ResourcePickerID = "resourcepicker" + listHeight = 15 +) + +type ResourcePickedMsg struct { + Attachment message.Attachment +} + +type ResourcePicker interface { + dialogs.DialogModel +} + +type resourceItem struct { + clientName string + resource *mcp.Resource +} + +func (i resourceItem) Title() string { + return i.resource.URI +} + +func (i resourceItem) Description() string { + return cmp.Or(i.resource.Description, i.resource.Title, i.resource.Name, "(no description)") +} + +func (i resourceItem) FilterValue() string { + return i.Title() + " " + i.Description() +} + +type model struct { + wWidth int + wHeight int + width int + list list.Model + keyMap KeyMap + help help.Model + loading bool +} + +func NewResourcePickerCmp() ResourcePicker { + t := styles.CurrentTheme() + + delegate := list.NewDefaultDelegate() + delegate.Styles.SelectedTitle = delegate.Styles.SelectedTitle.Foreground(t.Secondary) + delegate.Styles.SelectedDesc = delegate.Styles.SelectedDesc.Foreground(t.FgMuted) + + l := list.New([]list.Item{}, delegate, 0, listHeight) + l.SetShowStatusBar(false) + l.SetShowTitle(false) + l.SetFilteringEnabled(true) + l.DisableQuitKeybindings() + + help := help.New() + help.Styles = t.S().Help + + return &model{ + list: l, + keyMap: DefaultKeyMap(), + help: help, + loading: true, + } +} + +func (m *model) Init() tea.Cmd { + return m.loadResources +} + +func (m *model) loadResources() tea.Msg { + resources := agent.GetMCPResources() + items := make([]list.Item, 0, len(resources)) + + for key, resource := range resources { + parts := strings.SplitN(key, ":", 2) + if len(parts) != 2 { + continue + } + clientName := parts[0] + items = append(items, resourceItem{ + clientName: clientName, + resource: resource, + }) + } + + return resourcesLoadedMsg{items: items} +} + +type resourcesLoadedMsg struct { + items []list.Item +} + +func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.wWidth = msg.Width + m.wHeight = msg.Height + m.width = min(80, m.wWidth-4) + h := min(listHeight+4, m.wHeight-4) + m.list.SetSize(m.width-4, h) + return m, nil + + case resourcesLoadedMsg: + m.loading = false + cmd := m.list.SetItems(msg.items) + if len(msg.items) == 0 { + return m, tea.Batch( + cmd, + util.ReportWarn("No MCP resources available"), + util.CmdHandler(dialogs.CloseDialogMsg{}), + ) + } + return m, cmd + + case tea.KeyPressMsg: + if key.Matches(msg, m.keyMap.Close) { + return m, util.CmdHandler(dialogs.CloseDialogMsg{}) + } + if key.Matches(msg, m.keyMap.Select) { + if item, ok := m.list.SelectedItem().(resourceItem); ok { + return m, tea.Sequence( + util.CmdHandler(dialogs.CloseDialogMsg{}), + m.fetchResource(item), + ) + } + } + } + + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd +} + +func (m *model) fetchResource(item resourceItem) tea.Cmd { + return func() tea.Msg { + ctx := context.Background() + content, err := agent.GetMCPResourceContent(ctx, item.clientName, item.resource.URI) + if err != nil { + return util.ReportError(fmt.Errorf("failed to fetch resource: %w", err)) + } + + var textContent strings.Builder + for _, c := range content.Contents { + if c.Text != "" { + textContent.WriteString(c.Text) + } else if len(c.Blob) > 0 { + textContent.WriteString(string(c.Blob)) + } + } + + fileName := item.resource.Name + if item.resource.Title != "" { + fileName = item.resource.Title + } + + mimeType := item.resource.MIMEType + if mimeType == "" { + mimeType = "text/plain" + } + + attachment := message.Attachment{ + FileName: fileName, + FilePath: fileName, + MimeType: mimeType, + Content: []byte(textContent.String()), + } + + return ResourcePickedMsg{Attachment: attachment} + } +} + +func (m *model) View() string { + t := styles.CurrentTheme() + + if m.loading { + return m.style().Render( + lipgloss.JoinVertical( + lipgloss.Left, + t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Loading MCP Resources...", m.width-4)), + ), + ) + } + + content := lipgloss.JoinVertical( + lipgloss.Left, + t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Add MCP Resource", m.width-4)), + m.list.View(), + t.S().Base.Width(m.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(m.help.View(m.keyMap)), + ) + + return m.style().Render(content) +} + +func (m *model) style() lipgloss.Style { + t := styles.CurrentTheme() + return t.S().Base. + Width(m.width). + Border(lipgloss.RoundedBorder()). + BorderForeground(t.BorderFocus) +} + +func (m *model) Position() (int, int) { + x := (m.wWidth - m.width) / 2 + y := (m.wHeight - listHeight - 6) / 2 + return y, x +} + +func (m *model) ID() dialogs.DialogID { + return ResourcePickerID +} diff --git a/internal/tui/components/mcp/mcp.go b/internal/tui/components/mcp/mcp.go index 91afa66c1..5147c4cd1 100644 --- a/internal/tui/components/mcp/mcp.go +++ b/internal/tui/components/mcp/mcp.go @@ -74,6 +74,9 @@ func RenderMCPList(opts RenderOptions) []string { if count := state.Counts.Prompts; count > 0 { extraContent = append(extraContent, t.S().Subtle.Render(fmt.Sprintf("%d prompts", count))) } + if count := state.Counts.Resources; count > 0 { + extraContent = append(extraContent, t.S().Subtle.Render(fmt.Sprintf("%d resources", count))) + } case agent.MCPStateError: icon = t.ItemErrorIcon if state.Error != nil { diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index 291892506..7c3e3d32d 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -3,6 +3,7 @@ package chat import ( "context" "fmt" + "log/slog" "time" "github.com/charmbracelet/bubbles/v2/help" @@ -30,6 +31,7 @@ import ( "github.com/charmbracelet/crush/internal/tui/components/dialogs" "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands" "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker" + "github.com/charmbracelet/crush/internal/tui/components/dialogs/mcp" "github.com/charmbracelet/crush/internal/tui/components/dialogs/models" "github.com/charmbracelet/crush/internal/tui/components/dialogs/reasoning" "github.com/charmbracelet/crush/internal/tui/page" @@ -286,6 +288,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, cmd) return p, tea.Batch(cmds...) case filepicker.FilePickedMsg, + mcp.ResourcePickedMsg, completions.CompletionsClosedMsg, completions.SelectCompletionMsg: u, cmd := p.editor.Update(msg) @@ -378,6 +381,9 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } else { return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Name) } + case key.Matches(msg, p.keyMap.AddMCPResource): + slog.Warn("AQUI") + return p, util.CmdHandler(commands.OpenResourcePickerMsg{}) case key.Matches(msg, p.keyMap.Tab): if p.session.ID == "" { u, cmd := p.splash.Update(msg) @@ -761,6 +767,7 @@ func (p *chatPage) Bindings() []key.Binding { bindings := []key.Binding{ p.keyMap.NewSession, p.keyMap.AddAttachment, + p.keyMap.AddMCPResource, } if p.app.CoderAgent != nil && p.app.CoderAgent.IsBusy() { cancelBinding := p.keyMap.Cancel @@ -1021,6 +1028,10 @@ func (p *chatPage) Help() help.KeyMap { key.WithKeys("ctrl+f"), key.WithHelp("ctrl+f", "add image"), ), + key.NewBinding( + key.WithKeys("ctrl+m"), + key.WithHelp("ctrl+m", "add mcp resource"), + ), key.NewBinding( key.WithKeys("/"), key.WithHelp("/", "add file"), diff --git a/internal/tui/page/chat/keys.go b/internal/tui/page/chat/keys.go index 679a97c69..74f065646 100644 --- a/internal/tui/page/chat/keys.go +++ b/internal/tui/page/chat/keys.go @@ -5,11 +5,12 @@ import ( ) type KeyMap struct { - NewSession key.Binding - AddAttachment key.Binding - Cancel key.Binding - Tab key.Binding - Details key.Binding + NewSession key.Binding + AddAttachment key.Binding + AddMCPResource key.Binding + Cancel key.Binding + Tab key.Binding + Details key.Binding } func DefaultKeyMap() KeyMap { @@ -22,6 +23,10 @@ func DefaultKeyMap() KeyMap { key.WithKeys("ctrl+f"), key.WithHelp("ctrl+f", "add attachment"), ), + AddMCPResource: key.NewBinding( + key.WithKeys("ctrl+r"), + key.WithHelp("ctrl+r", "add mcp resource"), + ), Cancel: key.NewBinding( key.WithKeys("esc", "alt+esc"), key.WithHelp("esc", "cancel"), diff --git a/internal/tui/styles/icons.go b/internal/tui/styles/icons.go index d9d1ab06f..a53ee09bc 100644 --- a/internal/tui/styles/icons.go +++ b/internal/tui/styles/icons.go @@ -8,7 +8,8 @@ const ( HintIcon string = "∵" SpinnerIcon string = "..." LoadingIcon string = "⟳" - DocumentIcon string = "🖼" + DocumentIcon string = "📄 " + ImageIcon string = "🖼" ModelIcon string = "◇" // Tool call icons diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 8cd754fc1..0d3b0ecfe 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -25,6 +25,7 @@ import ( "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands" "github.com/charmbracelet/crush/internal/tui/components/dialogs/compact" "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker" + "github.com/charmbracelet/crush/internal/tui/components/dialogs/mcp" "github.com/charmbracelet/crush/internal/tui/components/dialogs/models" "github.com/charmbracelet/crush/internal/tui/components/dialogs/permissions" "github.com/charmbracelet/crush/internal/tui/components/dialogs/quit" @@ -254,6 +255,14 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return a, util.CmdHandler(dialogs.OpenDialogMsg{ Model: filepicker.NewFilePickerCmp(a.app.Config().WorkingDir()), }) + // Resource Picker + case commands.OpenResourcePickerMsg: + if a.dialog.ActiveDialogID() == mcp.ResourcePickerID { + return a, util.CmdHandler(dialogs.CloseDialogMsg{}) + } + return a, util.CmdHandler(dialogs.OpenDialogMsg{ + Model: mcp.NewResourcePickerCmp(), + }) // Permissions case pubsub.Event[permission.PermissionNotification]: item, ok := a.pages[a.currentPage]