Files
2026-07-13 13:00:08 +08:00

228 lines
6.0 KiB
Go

package cli
import (
"fmt"
"sort"
"strings"
"reasonix/internal/plugin"
)
const mcpMaxItemsPerSection = 6
func renderMCPStatus(width int, servers []plugin.ServerStatus, prompts []plugin.Prompt, resources []plugin.Resource, failures []plugin.Failure) string {
var b strings.Builder
fmt.Fprintf(&b, "%s\n", viewHeader("MCP servers (%d)", len(servers)))
promptsByServer := map[string][]plugin.Prompt{}
for _, p := range prompts {
promptsByServer[p.Server] = append(promptsByServer[p.Server], p)
}
resourcesByServer := map[string][]plugin.Resource{}
for _, r := range resources {
resourcesByServer[r.Server] = append(resourcesByServer[r.Server], r)
}
seen := map[string]bool{}
for _, s := range servers {
seen[s.Name] = true
writeMCPServer(&b, width, s, promptsByServer[s.Name], resourcesByServer[s.Name])
}
for _, name := range extraMCPServers(seen, promptsByServer, resourcesByServer) {
writeMCPServer(&b, width, plugin.ServerStatus{Name: name}, promptsByServer[name], resourcesByServer[name])
}
for _, f := range failures {
writeMCPFailure(&b, width, f)
}
return strings.TrimRight(b.String(), "\n")
}
func extraMCPServers(seen map[string]bool, prompts map[string][]plugin.Prompt, resources map[string][]plugin.Resource) []string {
set := map[string]bool{}
for name := range prompts {
if !seen[name] {
set[name] = true
}
}
for name := range resources {
if !seen[name] {
set[name] = true
}
}
var names []string
for name := range set {
names = append(names, name)
}
sort.Strings(names)
return names
}
func writeMCPServer(b *strings.Builder, width int, s plugin.ServerStatus, prompts []plugin.Prompt, resources []plugin.Resource) {
transport := s.Transport
if transport == "" {
transport = "unknown"
}
meta := fmt.Sprintf("(%s) %s · %s · %s", transport, countText(s.Tools, "tool"), countText(len(prompts), "prompt"), countText(len(resources), "resource"))
name := viewCompactText(s.Name, viewBudget(width, 4+2+1+visibleWidth(meta)))
fmt.Fprintf(b, " %s %s %s\n", accent("✓"), bold(name), viewMeta(meta))
if len(prompts) > 0 {
writeMCPPromptList(b, width, prompts)
}
if len(resources) > 0 {
writeMCPResourceList(b, width, resources)
}
}
func writeMCPFailure(b *strings.Builder, width int, f plugin.Failure) {
transport := f.Transport
if transport == "" {
transport = "unknown"
}
meta := fmt.Sprintf("(%s) %s", transport, oneLineText(f.Error))
name := viewCompactText(f.Name, viewBudget(width, 4+2+1+visibleWidth(meta)))
fmt.Fprintf(b, " %s %s %s\n", yellow("!"), bold(name), viewMeta(viewCompactText(meta, viewBudget(width, 10+visibleWidth(name)))))
}
func writeMCPPromptList(b *strings.Builder, width int, prompts []plugin.Prompt) {
b.WriteString(viewSubhead(" prompts") + "\n")
limit := len(prompts)
if limit > mcpMaxItemsPerSection {
limit = mcpMaxItemsPerSection
}
for _, p := range prompts[:limit] {
writeMCPItem(b, width, " ", "/"+p.Name, p.Description)
}
if extra := len(prompts) - limit; extra > 0 {
fmt.Fprintf(b, " %s\n", viewMore(extra, "prompts"))
}
}
func writeMCPResourceList(b *strings.Builder, width int, resources []plugin.Resource) {
b.WriteString(viewSubhead(" resources") + "\n")
limit := len(resources)
if limit > mcpMaxItemsPerSection {
limit = mcpMaxItemsPerSection
}
for _, r := range resources[:limit] {
label := strings.TrimSpace(r.Name)
if label == "" {
label = strings.TrimSpace(r.Description)
}
if r.MimeType != "" {
if label == "" {
label = r.MimeType
} else {
label += " [" + r.MimeType + "]"
}
}
writeMCPItem(b, width, " ", "@"+r.Server+":"+r.URI, label)
}
if extra := len(resources) - limit; extra > 0 {
fmt.Fprintf(b, " %s\n", viewMore(extra, "resources"))
}
}
func writeMCPItem(b *strings.Builder, width int, indent, ref, desc string) {
desc = oneLineText(desc)
ref = oneLineText(ref)
available := viewBudget(width, visibleWidth(indent))
if desc == "" || available < 30 {
b.WriteString(indent + compactMiddle(ref, available))
b.WriteByte('\n')
return
}
descBudget := min(40, max(12, available/2))
refBudget := available - 2 - descBudget
if refBudget < 16 {
refBudget = min(16, available)
descBudget = available - refBudget - 2
}
line := indent + compactMiddle(ref, refBudget)
if descBudget >= 12 {
line += " " + viewMeta(viewCompactText(desc, descBudget))
}
b.WriteString(line)
b.WriteByte('\n')
}
func oneLineText(s string) string {
return strings.Join(strings.Fields(s), " ")
}
func countText(n int, noun string) string {
if n == 1 {
return "1 " + noun
}
return fmt.Sprintf("%d %ss", n, noun)
}
func compactEnd(s string, maxWidth int) string {
if maxWidth <= 0 || visibleWidth(s) <= maxWidth {
return s
}
if maxWidth <= 1 {
return "…"
}
var out strings.Builder
for _, r := range s {
next := out.String() + string(r)
if visibleWidth(next)+1 > maxWidth {
break
}
out.WriteRune(r)
}
return out.String() + "…"
}
func compactMiddle(s string, maxWidth int) string {
if maxWidth <= 0 || visibleWidth(s) <= maxWidth {
return s
}
if maxWidth <= 3 {
return compactEnd(s, maxWidth)
}
keep := maxWidth - 1
leftWidth := keep / 2
rightWidth := keep - leftWidth
left := takeLeftWidth(s, leftWidth)
right := takeRightWidth(s, rightWidth)
return left + "…" + right
}
func takeLeftWidth(s string, maxWidth int) string {
var out strings.Builder
for _, r := range s {
next := out.String() + string(r)
if visibleWidth(next) > maxWidth {
break
}
out.WriteRune(r)
}
return out.String()
}
func takeRightWidth(s string, maxWidth int) string {
var out []rune
width := 0
for _, r := range reverseRunes([]rune(s)) {
w := visibleWidth(string(r))
if width+w > maxWidth {
break
}
out = append(out, r)
width += w
}
for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
out[i], out[j] = out[j], out[i]
}
return string(out)
}
func reverseRunes(in []rune) []rune {
out := append([]rune(nil), in...)
for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
out[i], out[j] = out[j], out[i]
}
return out
}