From b47908e251a766f058aa3fddb23fa036653f91ac Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:27:34 +1000 Subject: [PATCH] fix(app): clarify status indicator severity --- .../status-popover-indicator.test.ts | 36 ++++++++++ .../components/status-popover-indicator.ts | 17 +++++ .../app/src/components/status-popover.tsx | 68 +++++++------------ 3 files changed, 78 insertions(+), 43 deletions(-) create mode 100644 packages/app/src/components/status-popover-indicator.test.ts create mode 100644 packages/app/src/components/status-popover-indicator.ts diff --git a/packages/app/src/components/status-popover-indicator.test.ts b/packages/app/src/components/status-popover-indicator.test.ts new file mode 100644 index 0000000000..e3c62d2a95 --- /dev/null +++ b/packages/app/src/components/status-popover-indicator.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from "bun:test" +import { hasNonBlockingServiceIssue, 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 warning token for non-blocking issues while the server is online", () => { + expect(serverStatusDotClass({ ready: true, serverHealth: true, issue: true })).toBe("bg-icon-warning-base") + }) + + test("uses the critical token only after the server connection drops", () => { + expect(serverStatusDotClass({ ready: true, serverHealth: false, issue: false })).toBe("bg-icon-critical-base") + expect(serverStatusDotClass({ ready: true, serverHealth: false, issue: true })).toBe("bg-icon-critical-base") + }) + + test("stays neutral before status is ready", () => { + expect(serverStatusDotClass({ ready: false, serverHealth: true, issue: false })).toBe("bg-border-weak-base") + expect(serverStatusDotClass({ ready: false, serverHealth: undefined, issue: false })).toBe("bg-border-weak-base") + }) +}) + +describe("hasNonBlockingServiceIssue", () => { + test("detects MCP failures that do not block chatting", () => { + expect(hasNonBlockingServiceIssue({ mcp: ["failed"], lsp: [] })).toBe(true) + expect(hasNonBlockingServiceIssue({ mcp: ["needs_auth"], lsp: [] })).toBe(true) + expect(hasNonBlockingServiceIssue({ mcp: ["needs_client_registration"], lsp: [] })).toBe(true) + expect(hasNonBlockingServiceIssue({ mcp: ["connected", "disabled"], lsp: [] })).toBe(false) + }) + + test("detects LSP failures that do not block chatting", () => { + expect(hasNonBlockingServiceIssue({ mcp: [], lsp: ["error"] })).toBe(true) + expect(hasNonBlockingServiceIssue({ mcp: [], lsp: ["connected"] })).toBe(false) + }) +}) diff --git a/packages/app/src/components/status-popover-indicator.ts b/packages/app/src/components/status-popover-indicator.ts new file mode 100644 index 0000000000..b454826f48 --- /dev/null +++ b/packages/app/src/components/status-popover-indicator.ts @@ -0,0 +1,17 @@ +type McpStatus = "connected" | "disabled" | "failed" | "needs_auth" | "needs_client_registration" +type LspStatus = "connected" | "error" + +export function hasNonBlockingServiceIssue(input: { mcp: McpStatus[]; lsp: LspStatus[] }) { + return ( + input.mcp.some((status) => status !== "connected" && status !== "disabled") || + input.lsp.some((status) => status === "error") + ) +} + +export function serverStatusDotClass(input: { ready: boolean; serverHealth: boolean | undefined; issue: boolean }) { + if (input.serverHealth === false) return "bg-icon-critical-base" + if (!input.ready || input.serverHealth === undefined) return "bg-border-weak-base" + 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 c75dd5b655..2aed45702a 100644 --- a/packages/app/src/components/status-popover.tsx +++ b/packages/app/src/components/status-popover.tsx @@ -9,6 +9,7 @@ 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" const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody }))) const ServerBody = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverServerBody }))) @@ -19,16 +20,14 @@ export function StatusPopover() { const global = useGlobal() const sync = useSync() const [shown, setShown] = createSignal(false) - const ready = createMemo(() => global.servers.health[server.key]?.healthy === false || sync().data.mcp_ready) - const mcpIssue = createMemo(() => { - const mcp = Object.values(sync().data.mcp ?? {}) - const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration") - const warn = mcp.some((item) => item.status === "needs_auth") - if (failed) return "critical" as const - if (warn) return "warning" as const - }) - const serverHealthy = () => global.servers.health[server.key]?.healthy === true - const healthy = createMemo(() => global.servers.health[server.key]?.healthy === true && !mcpIssue()) + const serverHealth = () => global.servers.health[server.key]?.healthy + const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready)) + const issue = createMemo(() => + hasNonBlockingServiceIssue({ + mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status), + lsp: (sync().data.lsp ?? []).map((item) => item.status), + }), + ) return (
} @@ -87,21 +84,18 @@ function DirectoryStatusPopover() { const sync = useSync() const [shown, setShown] = createSignal(false) const serverHealth = () => global.servers.health[ServerConnection.key(server().server)]?.healthy - const ready = createMemo(() => serverHealth() === false || sync().data.mcp_ready) - const mcpIssue = createMemo(() => { - const mcp = Object.values(sync().data.mcp ?? {}) - const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration") - const warn = mcp.some((item) => item.status === "needs_auth") - if (failed) return "critical" as const - if (warn) return "warning" as const - }) - const healthy = createMemo(() => serverHealth() === true && !mcpIssue()) + const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready)) + const issue = createMemo(() => + hasNonBlockingServiceIssue({ + mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status), + lsp: (sync().data.lsp ?? []).map((item) => item.status), + }), + ) const state = createMemo(() => ({ shown: shown(), ready: ready(), - healthy: healthy(), serverHealth: serverHealth(), - issue: mcpIssue(), + issue: issue(), label: language.t("status.popover.trigger"), onOpenChange: setShown, body: () => ( @@ -123,8 +117,8 @@ function ServerStatusPopover() { const state = createMemo(() => ({ shown: shown(), ready: serverHealth() !== undefined, - healthy: serverHealth() === true, serverHealth: serverHealth(), + issue: false, label: language.t("status.popover.trigger"), onOpenChange: setShown, body: () => ( @@ -140,9 +134,8 @@ function ServerStatusPopover() { type StatusPopoverState = { shown: boolean ready: boolean - healthy: boolean serverHealth: boolean | undefined - issue?: "critical" | "warning" + issue: boolean label: string onOpenChange: (value: boolean) => void body: () => JSX.Element @@ -161,16 +154,6 @@ function StatusPopoverBody(props: { shown: boolean; children: JSX.Element }) { } function StatusPopoverView(props: { state: StatusPopoverState }) { - const statusDotClass = () => ({ - "absolute rounded-full": true, - "bg-icon-success-base": props.state.ready && props.state.healthy, - "bg-icon-warning-base": props.state.ready && props.state.serverHealth === true && props.state.issue === "warning", - "bg-icon-critical-base": - props.state.serverHealth === false || - (props.state.ready && props.state.serverHealth === true && props.state.issue === "critical"), - "bg-border-weak-base": props.state.serverHealth === undefined || !props.state.ready, - }) - const popoverProps = { class: "[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl", @@ -195,8 +178,7 @@ function StatusPopoverView(props: { state: StatusPopoverState }) {
}