diff --git a/packages/app/src/components/status-popover-indicator.test.ts b/packages/app/src/components/status-popover-indicator.test.ts index c1c57b9d708..d6d6d07a1d8 100644 --- a/packages/app/src/components/status-popover-indicator.test.ts +++ b/packages/app/src/components/status-popover-indicator.test.ts @@ -1,11 +1,21 @@ import { describe, expect, test } from "bun:test" -import { hasNonBlockingServiceIssue, serverStatusDotClass } from "./status-popover-indicator" +import { + hasNonBlockingServiceIssue, + hasServiceNeedingAttention, + serverStatusDotClass, +} from "./status-popover-indicator" describe("serverStatusDotClass", () => { test("uses the success token while the server and services are healthy", () => { expect(serverStatusDotClass({ ready: true, serverHealth: true, issue: false })).toBe("bg-icon-success-base") }) + test("uses the session attention token when a service needs attention", () => { + expect(serverStatusDotClass({ ready: true, serverHealth: true, attention: true, issue: true })).toBe( + "bg-v2-background-bg-accent", + ) + }) + test("uses the warning token for non-blocking issues while the server is online", () => { expect(serverStatusDotClass({ ready: true, serverHealth: true, issue: true })).toBe("bg-icon-warning-base") }) @@ -34,3 +44,15 @@ describe("hasNonBlockingServiceIssue", () => { expect(hasNonBlockingServiceIssue({ mcp: [], lsp: ["connected"] })).toBe(false) }) }) + +describe("hasServiceNeedingAttention", () => { + test("detects MCP states that need user attention", () => { + expect(hasServiceNeedingAttention({ mcp: ["needs_auth"] })).toBe(true) + expect(hasServiceNeedingAttention({ mcp: ["needs_client_registration"] })).toBe(true) + }) + + test("ignores states that do not need user attention", () => { + expect(hasServiceNeedingAttention({ mcp: ["failed"] })).toBe(false) + expect(hasServiceNeedingAttention({ mcp: ["connected", "pending", "disabled"] })).toBe(false) + }) +}) diff --git a/packages/app/src/components/status-popover-indicator.ts b/packages/app/src/components/status-popover-indicator.ts index d89f90febbf..62fc3bedbff 100644 --- a/packages/app/src/components/status-popover-indicator.ts +++ b/packages/app/src/components/status-popover-indicator.ts @@ -1,6 +1,10 @@ import type { LspStatus } from "@opencode-ai/sdk/v2/client" import type { McpServer } from "@opencode-ai/client/promise" +export function hasServiceNeedingAttention(input: { mcp: Array }) { + return input.mcp.some((status) => status === "needs_auth" || status === "needs_client_registration") +} + export function hasNonBlockingServiceIssue(input: { mcp: Array lsp: Array @@ -11,9 +15,15 @@ export function hasNonBlockingServiceIssue(input: { ) } -export function serverStatusDotClass(input: { ready: boolean; serverHealth: boolean | undefined; issue: boolean }) { +export function serverStatusDotClass(input: { + ready: boolean + serverHealth: boolean | undefined + attention?: boolean + issue: boolean +}) { if (input.serverHealth === false) return "bg-icon-critical-base" if (!input.ready || input.serverHealth === undefined) return "bg-border-weak-base" + if (input.attention) return "bg-v2-background-bg-accent" if (input.issue) return "bg-icon-warning-base" if (input.serverHealth === true) return "bg-icon-success-base" return "bg-border-weak-base" diff --git a/packages/app/src/components/status-popover.tsx b/packages/app/src/components/status-popover.tsx index 2aed45702a1..767e7e62c6f 100644 --- a/packages/app/src/components/status-popover.tsx +++ b/packages/app/src/components/status-popover.tsx @@ -9,7 +9,11 @@ import { ServerConnection, useServer } from "@/context/server" import { useServerSDK } from "@/context/server-sdk" import { useSync } from "@/context/sync" import { useGlobal } from "@/context/global" -import { hasNonBlockingServiceIssue, serverStatusDotClass } from "./status-popover-indicator" +import { + hasNonBlockingServiceIssue, + hasServiceNeedingAttention, + serverStatusDotClass, +} from "./status-popover-indicator" const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody }))) const ServerBody = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverServerBody }))) @@ -22,6 +26,11 @@ export function StatusPopover() { const [shown, setShown] = createSignal(false) const serverHealth = () => global.servers.health[server.key]?.healthy const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready)) + const attention = createMemo(() => + hasServiceNeedingAttention({ + mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status), + }), + ) const issue = createMemo(() => hasNonBlockingServiceIssue({ mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status), @@ -49,6 +58,7 @@ export function StatusPopover() { class={`absolute -top-px -right-px size-1.5 rounded-full ${serverStatusDotClass({ ready: ready(), serverHealth: serverHealth(), + attention: attention(), issue: issue(), })}`} /> @@ -85,6 +95,11 @@ function DirectoryStatusPopover() { const [shown, setShown] = createSignal(false) const serverHealth = () => global.servers.health[ServerConnection.key(server().server)]?.healthy const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready)) + const attention = createMemo(() => + hasServiceNeedingAttention({ + mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status), + }), + ) const issue = createMemo(() => hasNonBlockingServiceIssue({ mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status), @@ -95,6 +110,7 @@ function DirectoryStatusPopover() { shown: shown(), ready: ready(), serverHealth: serverHealth(), + attention: attention(), issue: issue(), label: language.t("status.popover.trigger"), onOpenChange: setShown, @@ -118,6 +134,7 @@ function ServerStatusPopover() { shown: shown(), ready: serverHealth() !== undefined, serverHealth: serverHealth(), + attention: false, issue: false, label: language.t("status.popover.trigger"), onOpenChange: setShown, @@ -135,6 +152,7 @@ type StatusPopoverState = { shown: boolean ready: boolean serverHealth: boolean | undefined + attention: boolean issue: boolean label: string onOpenChange: (value: boolean) => void