Jelajahi Sumber

fix(tui): simplify MCP status rows (#40916)

Kit Langton 1 Minggu lalu
induk
melakukan
65c6a71903

+ 41 - 15
packages/tui/src/component/dialog-mcp.tsx

@@ -19,13 +19,20 @@ function statusError(status: McpServer["status"]) {
   return undefined
 }
 
-function Status(props: { enabled: boolean; loading: boolean }) {
-  const theme = useTheme("elevated")
-  if (props.loading) return <span style={{ fg: theme.text.subdued }}>⋯ Loading</span>
-  if (props.enabled) {
-    return <span style={{ fg: theme.text.feedback.success.default, attributes: TextAttributes.BOLD }}>✓ Enabled</span>
+function Status(props: { status: McpServer["status"]; loading: boolean }) {
+  if (props.loading || props.status.status === "pending") {
+    return <>Connecting …</>
+  }
+  if (props.status.status === "connected") {
+    return <span style={{ attributes: TextAttributes.BOLD }}>Connected ✓</span>
+  }
+  if (props.status.status === "failed") {
+    return <>Failed !</>
   }
-  return <span style={{ fg: theme.text.subdued }}>○ Disabled</span>
+  if (props.status.status === "needs_auth") {
+    return <>Sign in required →</>
+  }
+  return <>Disabled ○</>
 }
 
 export function DialogMcp() {
@@ -38,6 +45,13 @@ export function DialogMcp() {
   const [detail, setDetail] = createSignal<McpServer>()
   const [loading, setLoading] = createSignal<string | null>(null)
 
+  const statusColor = (status: McpServer["status"]) => {
+    if (status.status === "connected") return theme.text.feedback.success.default
+    if (status.status === "failed") return theme.text.feedback.error.default
+    if (status.status === "needs_auth") return theme.text.feedback.warning.default
+    return theme.text.subdued
+  }
+
   const servers = createMemo(() =>
     pipe(
       data.location.mcp.server.list() ?? [],
@@ -53,17 +67,29 @@ export function DialogMcp() {
 
   const options = createMemo(() => {
     const loadingMcp = loading()
-    return servers().map((server) => ({
-      value: server.name,
-      title: server.name,
-      description: server.status.status,
-      footer: <Status enabled={server.status.status === "connected"} loading={loadingMcp === server.name} />,
-    }))
+    return servers().map((server) => {
+      const pending = loadingMcp === server.name || server.status.status === "pending"
+      return {
+        value: server.name,
+        title: server.name,
+        footer: <Status status={server.status} loading={pending} />,
+        footerColor: pending ? theme.text.subdued : statusColor(server.status),
+      }
+    })
+  })
+
+  const focusedServer = createMemo(() => servers().find((server) => server.name === focused()))
+
+  const toggleTitle = createMemo(() => {
+    const status = focusedServer()?.status.status
+    if (status === "connected") return "disconnect"
+    if (status === "failed") return "retry"
+    if (status === "needs_auth") return "sign in"
+    return "connect"
   })
 
   const focusedError = createMemo(() => {
-    const name = focused()
-    const server = servers().find((entry) => entry.name === name)
+    const server = focusedServer()
     return server ? statusError(server.status) : undefined
   })
 
@@ -100,7 +126,7 @@ export function DialogMcp() {
             onSelect={(option) => open(option.value as string)}
             actions={[
               {
-                title: "toggle",
+                title: toggleTitle(),
                 command: "dialog.mcp.toggle",
                 onTrigger: (option) => {
                   setFocused(option.value as string)

+ 14 - 1
packages/tui/src/ui/dialog-select.tsx

@@ -71,6 +71,7 @@ export interface DialogSelectOption<T = any> {
   detailsColor?: RGBA
   detailsWrap?: boolean
   footer?: JSX.Element | string
+  footerColor?: RGBA
   titleWidth?: number
   truncateTitle?: boolean | "left"
   category?: string
@@ -727,6 +728,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
                               footer={
                                 flatten() ? (option.searchFooter ?? option.category ?? option.footer) : option.footer
                               }
+                              footerColor={option.footerColor}
                               titleWidth={option.titleWidth}
                               truncateTitle={option.truncateTitle}
                               description={option.description !== category ? option.description : undefined}
@@ -784,6 +786,7 @@ function Option(props: {
   current?: boolean
   muted?: boolean
   footer?: JSX.Element | string
+  footerColor?: RGBA
   titleWidth?: number
   truncateTitle?: boolean | "left"
   gutter?: () => JSX.Element
@@ -832,7 +835,17 @@ function Option(props: {
       </text>
       <Show when={props.footer}>
         <box flexShrink={0}>
-          <text fg={props.active && !props.muted ? text() : theme.text.subdued}>{props.footer}</text>
+          <text
+            fg={
+              props.active && !props.muted
+                ? text()
+                : props.muted && (props.active || props.current)
+                  ? theme.text.subdued
+                  : (props.footerColor ?? theme.text.subdued)
+            }
+          >
+            {props.footer}
+          </text>
         </box>
       </Show>
     </>