@@ -354,18 +354,16 @@ func (m *UI) loadCustomCommands() tea.Cmd {
354354}
355355
356356// loadMCPrompts loads the MCP prompts asynchronously.
357−func (m *UI) loadMCPrompts() tea.Cmd {
358− return func() tea.Msg {
359− prompts, err := commands.LoadMCPPrompts()
360− if err != nil {
361− slog.Error("Failed to load MCP prompts", "error", err)
362− }
363− if prompts == nil {
364− // flag them as loaded even if there is none or an error
365− prompts = []commands.MCPPrompt{}
366− }
367− return mcpPromptsLoadedMsg{Prompts: prompts}
357+func (m *UI) loadMCPrompts() tea.Msg {
358+ prompts, err := commands.LoadMCPPrompts()
359+ if err != nil {
360+ slog.Error("Failed to load MCP prompts", "error", err)
368361 }
362+ if prompts == nil {
363+ // flag them as loaded even if there is none or an error
364+ prompts = []commands.MCPPrompt{}
365+ }
366+ return mcpPromptsLoadedMsg{Prompts: prompts}
369367}
370368
371369// Update handles updates to the UI model.
@@ -505,17 +503,18 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
505503 case pubsub.Event[app.LSPEvent]:
506504 m.lspStates = app.GetLSPStates()
507505 case pubsub.Event[mcp.Event]:
508− m.mcpStates = mcp.GetStates()
509− // check if all mcps are initialized
510− initialized := true
511− for _, state := range m.mcpStates {
512− if state.State == mcp.StateStarting {
513− initialized = false
514− break
515− }
516− }
517− if initialized && m.mcpPrompts == nil {
518− cmds = append(cmds, m.loadMCPrompts())
506+ switch msg.Payload.Type {
507+ case mcp.EventStateChanged:
508+ return m, tea.Batch(
509+ m.handleStateChanged(),
510+ m.loadMCPrompts,
511+ )
512+ case mcp.EventPromptsListChanged:
513+ return m, handleMCPPromptsEvent(msg.Payload.Name)
514+ case mcp.EventToolsListChanged:
515+ return m, handleMCPToolsEvent(m.com.Config(), msg.Payload.Name)
516+ case mcp.EventResourcesListChanged:
517+ return m, handleMCPResourcesEvent(msg.Payload.Name)
519518 }
520519 case pubsub.Event[permission.PermissionRequest]:
521520 if cmd := m.openPermissionsDialog(msg.Payload); cmd != nil {
@@ -713,10 +712,9 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
713712 cmds = append(cmds, clearInfoMsgCmd(ttl))
714713 case util.ClearStatusMsg:
715714 m.status.ClearInfoMsg()
716− case completions.FilesLoadedMsg:
717− // Handle async file loading for completions.
715+ case completions.CompletionItemsLoadedMsg:
718716 if m.completionsOpen {
719− m.completions.SetFiles(msg.Files)
717+ m.completions.SetItems(msg.Files, msg.Resources)
720718 }
721719 case uv.KittyGraphicsEvent:
722720 if !bytes.HasPrefix(msg.Payload, []byte("OK")) {
@@ -1517,12 +1515,14 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
15171515 if m.completionsOpen {
15181516 if msg, ok := m.completions.Update(msg); ok {
15191517 switch msg := msg.(type) {
1520− case completions.SelectionMsg:
1521− // Handle file completion selection.
1522− if item, ok := msg.Value.(completions.FileCompletionValue); ok {
1523− cmds = append(cmds, m.insertFileCompletion(item.Path))
1518+ case completions.SelectionMsg[completions.FileCompletionValue]:
1519+ cmds = append(cmds, m.insertFileCompletion(msg.Value.Path))
1520+ if !msg.KeepOpen {
1521+ m.closeCompletions()
15241522 }
1525− if !msg.Insert {
1523+ case completions.SelectionMsg[completions.ResourceCompletionValue]:
1524+ cmds = append(cmds, m.insertMCPResourceCompletion(msg.Value))
1525+ if !msg.KeepOpen {
15261526 m.closeCompletions()
15271527 }
15281528 case completions.ClosedMsg:
@@ -1636,7 +1636,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
16361636 m.completionsStartIndex = curIdx
16371637 m.completionsPositionStart = m.completionsPosition()
16381638 depth, limit := m.com.Config().Options.TUI.Completions.Limits()
1639− cmds = append(cmds, m.completions.OpenWithFiles(depth, limit))
1639+ cmds = append(cmds, m.completions.Open(depth, limit))
16401640 }
16411641 }
16421642
@@ -2475,24 +2475,29 @@ func (m *UI) closeCompletions() {
24752475 m.completions.Close()
24762476}
24772477
2478−// insertFileCompletion inserts the selected file path into the textarea,
2479−// replacing the @query, and adds the file as an attachment.
2480−func (m *UI) insertFileCompletion(path string) tea.Cmd {
2478+// insertCompletionText replaces the @query in the textarea with the given text.
2479+// Returns false if the replacement cannot be performed.
2480+func (m *UI) insertCompletionText(text string) bool {
24812481 value := m.textarea.Value()
2482− word := m.textareaWord()
2483−
2484− // Find the @ and query to replace.
24852482 if m.completionsStartIndex > len(value) {
2486− return nil
2483+ return false
24872484 }
24882485
2489− // Build the new value: everything before @, the path, everything after query.
2486+ word := m.textareaWord()
24902487 endIdx := min(m.completionsStartIndex+len(word), len(value))
2491−
2492− newValue := value[:m.completionsStartIndex] + path + value[endIdx:]
2488+ newValue := value[:m.completionsStartIndex] + text + value[endIdx:]
24932489 m.textarea.SetValue(newValue)
24942490 m.textarea.MoveToEnd()
24952491 m.textarea.InsertRune(' ')
2492+ return true
2493+}
2494+
2495+// insertFileCompletion inserts the selected file path into the textarea,
2496+// replacing the @query, and adds the file as an attachment.
2497+func (m *UI) insertFileCompletion(path string) tea.Cmd {
2498+ if !m.insertCompletionText(path) {
2499+ return nil
2500+ }
24962501
24972502 return func() tea.Msg {
24982503 absPath, _ := filepath.Abs(path)
@@ -2527,6 +2532,61 @@ func (m *UI) insertFileCompletion(path string) tea.Cmd {
25272532 }
25282533}
25292534
2535+// insertMCPResourceCompletion inserts the selected resource into the textarea,
2536+// replacing the @query, and adds the resource as an attachment.
2537+func (m *UI) insertMCPResourceCompletion(item completions.ResourceCompletionValue) tea.Cmd {
2538+ displayText := item.Title
2539+ if displayText == "" {
2540+ displayText = item.URI
2541+ }
2542+
2543+ if !m.insertCompletionText(displayText) {
2544+ return nil
2545+ }
2546+
2547+ return func() tea.Msg {
2548+ contents, err := mcp.ReadResource(
2549+ context.Background(),
2550+ m.com.Config(),
2551+ item.MCPName,
2552+ item.URI,
2553+ )
2554+ if err != nil {
2555+ slog.Warn("Failed to read MCP resource", "uri", item.URI, "error", err)
2556+ return nil
2557+ }
2558+ if len(contents) == 0 {
2559+ return nil
2560+ }
2561+
2562+ content := contents[0]
2563+ var data []byte
2564+ if content.Text != "" {
2565+ data = []byte(content.Text)
2566+ } else if len(content.Blob) > 0 {
2567+ data = content.Blob
2568+ }
2569+ if len(data) == 0 {
2570+ return nil
2571+ }
2572+
2573+ mimeType := item.MIMEType
2574+ if mimeType == "" && content.MIMEType != "" {
2575+ mimeType = content.MIMEType
2576+ }
2577+ if mimeType == "" {
2578+ mimeType = "text/plain"
2579+ }
2580+
2581+ return message.Attachment{
2582+ FilePath: item.URI,
2583+ FileName: displayText,
2584+ MimeType: mimeType,
2585+ Content: data,
2586+ }
2587+ }
2588+}
2589+
25302590// completionsPosition returns the X and Y position for the completions popup.
25312591func (m *UI) completionsPosition() image.Point {
25322592 cur := m.textarea.Cursor()
@@ -3088,6 +3148,43 @@ func (m *UI) runMCPPrompt(clientID, promptID string, arguments map[string]string
30883148 return tea.Sequence(cmds...)
30893149}
30903150
3151+func (m *UI) handleStateChanged() tea.Cmd {
3152+ slog.Warn("handleStateChanged")
3153+ return func() tea.Msg {
3154+ m.com.App.UpdateAgentModel(context.Background())
3155+ m.mcpStates = mcp.GetStates()
3156+ return nil
3157+ }
3158+}
3159+
3160+func handleMCPPromptsEvent(name string) tea.Cmd {
3161+ slog.Warn("handleMCPPromptsEvent")
3162+ return func() tea.Msg {
3163+ mcp.RefreshPrompts(context.Background(), name)
3164+ return nil
3165+ }
3166+}
3167+
3168+func handleMCPToolsEvent(cfg *config.Config, name string) tea.Cmd {
3169+ slog.Warn("handleMCPToolsEvent")
3170+ return func() tea.Msg {
3171+ mcp.RefreshTools(
3172+ context.Background(),
3173+ cfg,
3174+ name,
3175+ )
3176+ return nil
3177+ }
3178+}
3179+
3180+func handleMCPResourcesEvent(name string) tea.Cmd {
3181+ slog.Warn("handleMCPResourcesEvent")
3182+ return func() tea.Msg {
3183+ mcp.RefreshResources(context.Background(), name)
3184+ return nil
3185+ }
3186+}
3187+
30913188func (m *UI) copyChatHighlight() tea.Cmd {
30923189 text := m.chat.HighlightContent()
30933190 return common.CopyToClipboardWithCallback(