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 (