@@ -354,18 +354,16 @@ func (m *UI) loadCustomCommands() tea.Cmd {
356// loadMCPrompts loads the MCP prompts asynchronously.
356// loadMCPrompts loads the MCP prompts asynchronously.
357−func (m *UI) loadMCPrompts() tea.Cmd {
357+func (m *UI) loadMCPrompts() tea.Msg {
358− return func() tea.Msg {
359− prompts, err := commands.LoadMCPPrompts()
358+ prompts, err := commands.LoadMCPPrompts()
360− if err != nil {
359+ if err != nil {
361− slog.Error("Failed to load MCP prompts", "error", err)
360+ slog.Error("Failed to load MCP prompts", "error", err)
364− // flag them as loaded even if there is none or an error
365− prompts = []commands.MCPPrompt{}
367− return mcpPromptsLoadedMsg{Prompts: prompts}
363+ // flag them as loaded even if there is none or an error
364+ prompts = []commands.MCPPrompt{}
366+ return mcpPromptsLoadedMsg{Prompts: prompts}
371// Update handles updates to the UI model.
369// Update handles updates to the UI model.
@@ -505,17 +503,18 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
505 case pubsub.Event[app.LSPEvent]:
503 case pubsub.Event[app.LSPEvent]:
506 m.lspStates = app.GetLSPStates()
504 m.lspStates = app.GetLSPStates()
507 case pubsub.Event[mcp.Event]:
505 case pubsub.Event[mcp.Event]:
508− m.mcpStates = mcp.GetStates()
506+ switch msg.Payload.Type {
509− // check if all mcps are initialized
507+ case mcp.EventStateChanged:
510− initialized := true
508+ return m, tea.Batch(
511− for _, state := range m.mcpStates {
509+ m.handleStateChanged(),
512− if state.State == mcp.StateStarting {
517− if initialized && m.mcpPrompts == nil {
518− cmds = append(cmds, m.loadMCPrompts())
510+ m.loadMCPrompts,
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)
520 case pubsub.Event[permission.PermissionRequest]:
519 case pubsub.Event[permission.PermissionRequest]:
521 if cmd := m.openPermissionsDialog(msg.Payload); cmd != nil {
520 if cmd := m.openPermissionsDialog(msg.Payload); cmd != nil {
@@ -713,10 +712,9 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
713 cmds = append(cmds, clearInfoMsgCmd(ttl))
712 cmds = append(cmds, clearInfoMsgCmd(ttl))
714 case util.ClearStatusMsg:
713 case util.ClearStatusMsg:
715 m.status.ClearInfoMsg()
714 m.status.ClearInfoMsg()
716− case completions.FilesLoadedMsg:
715+ case completions.CompletionItemsLoadedMsg:
717− // Handle async file loading for completions.
718 if m.completionsOpen {
716 if m.completionsOpen {
719− m.completions.SetFiles(msg.Files)
717+ m.completions.SetItems(msg.Files, msg.Resources)
721 case uv.KittyGraphicsEvent:
719 case uv.KittyGraphicsEvent:
722 if !bytes.HasPrefix(msg.Payload, []byte("OK")) {
720 if !bytes.HasPrefix(msg.Payload, []byte("OK")) {
@@ -1517,12 +1515,14 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
1517 if m.completionsOpen {
1515 if m.completionsOpen {
1518 if msg, ok := m.completions.Update(msg); ok {
1516 if msg, ok := m.completions.Update(msg); ok {
1519 switch msg := msg.(type) {
1517 switch msg := msg.(type) {
1520− case completions.SelectionMsg:
1518+ case completions.SelectionMsg[completions.FileCompletionValue]:
1521− // Handle file completion selection.
1522− if item, ok := msg.Value.(completions.FileCompletionValue); ok {
1523− cmds = append(cmds, m.insertFileCompletion(item.Path))
1519+ cmds = append(cmds, m.insertFileCompletion(msg.Value.Path))
1521+ m.closeCompletions()
1523+ case completions.SelectionMsg[completions.ResourceCompletionValue]:
1524+ cmds = append(cmds, m.insertMCPResourceCompletion(msg.Value))
1525− if !msg.Insert {
1525+ if !msg.KeepOpen {
1526 m.closeCompletions()
1526 m.closeCompletions()
1528 case completions.ClosedMsg:
1528 case completions.ClosedMsg:
@@ -1636,7 +1636,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
1636 m.completionsStartIndex = curIdx
1636 m.completionsStartIndex = curIdx
1637 m.completionsPositionStart = m.completionsPosition()
1637 m.completionsPositionStart = m.completionsPosition()
1638 depth, limit := m.com.Config().Options.TUI.Completions.Limits()
1638 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))
@@ -2475,24 +2475,29 @@ func (m *UI) closeCompletions() {
2475 m.completions.Close()
2475 m.completions.Close()
2478−// insertFileCompletion inserts the selected file path into the textarea,
2478+// insertCompletionText replaces the @query in the textarea with the given text.
2479−// replacing the @query, and adds the file as an attachment.
2479+// Returns false if the replacement cannot be performed.
2480−func (m *UI) insertFileCompletion(path string) tea.Cmd {
2480+func (m *UI) insertCompletionText(text string) bool {
2481 value := m.textarea.Value()
2481 value := m.textarea.Value()
2482− word := m.textareaWord()
2484− // Find the @ and query to replace.
2485 if m.completionsStartIndex > len(value) {
2482 if m.completionsStartIndex > len(value) {
2486− return nil
2483+ return false
2489− // Build the new value: everything before @, the path, everything after query.
2486+ word := m.textareaWord()
2490 endIdx := min(m.completionsStartIndex+len(word), len(value))
2487 endIdx := min(m.completionsStartIndex+len(word), len(value))
2492− newValue := value[:m.completionsStartIndex] + path + value[endIdx:]
2488+ newValue := value[:m.completionsStartIndex] + text + value[endIdx:]
2493 m.textarea.SetValue(newValue)
2489 m.textarea.SetValue(newValue)
2494 m.textarea.MoveToEnd()
2490 m.textarea.MoveToEnd()
2495 m.textarea.InsertRune(' ')
2491 m.textarea.InsertRune(' ')
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) {
2497 return func() tea.Msg {
2502 return func() tea.Msg {
2498 absPath, _ := filepath.Abs(path)
2503 absPath, _ := filepath.Abs(path)
@@ -2527,6 +2532,61 @@ func (m *UI) insertFileCompletion(path string) tea.Cmd {
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
2543+ if !m.insertCompletionText(displayText) {
2547+ return func() tea.Msg {
2548+ contents, err := mcp.ReadResource(
2549+ context.Background(),
2555+ slog.Warn("Failed to read MCP resource", "uri", item.URI, "error", err)
2558+ if len(contents) == 0 {
2562+ content := contents[0]
2564+ if content.Text != "" {
2565+ data = []byte(content.Text)
2566+ } else if len(content.Blob) > 0 {
2567+ data = content.Blob
2569+ if len(data) == 0 {
2573+ mimeType := item.MIMEType
2574+ if mimeType == "" && content.MIMEType != "" {
2575+ mimeType = content.MIMEType
2577+ if mimeType == "" {
2578+ mimeType = "text/plain"
2581+ return message.Attachment{
2582+ FilePath: item.URI,
2583+ FileName: displayText,
2584+ MimeType: mimeType,
2530// completionsPosition returns the X and Y position for the completions popup.
2590// completionsPosition returns the X and Y position for the completions popup.
2531func (m *UI) completionsPosition() image.Point {
2591func (m *UI) completionsPosition() image.Point {
2532 cur := m.textarea.Cursor()
2592 cur := m.textarea.Cursor()
@@ -3088,6 +3148,43 @@ func (m *UI) runMCPPrompt(clientID, promptID string, arguments map[string]string
3088 return tea.Sequence(cmds...)
3148 return tea.Sequence(cmds...)
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()
3160+func handleMCPPromptsEvent(name string) tea.Cmd {
3161+ slog.Warn("handleMCPPromptsEvent")
3162+ return func() tea.Msg {
3163+ mcp.RefreshPrompts(context.Background(), name)
3168+func handleMCPToolsEvent(cfg *config.Config, name string) tea.Cmd {
3169+ slog.Warn("handleMCPToolsEvent")
3170+ return func() tea.Msg {
3172+ context.Background(),
3180+func handleMCPResourcesEvent(name string) tea.Cmd {
3181+ slog.Warn("handleMCPResourcesEvent")
3182+ return func() tea.Msg {
3183+ mcp.RefreshResources(context.Background(), name)
3091func (m *UI) copyChatHighlight() tea.Cmd {
3188func (m *UI) copyChatHighlight() tea.Cmd {
3092 text := m.chat.HighlightContent()
3189 text := m.chat.HighlightContent()
3093 return common.CopyToClipboardWithCallback(
3190 return common.CopyToClipboardWithCallback(