From c7760b433b1bdbcaed7e7cd55d53b5b331f0f0fa Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:41:22 -0500 Subject: fix(app): more startup perf (#19288) --- .../app/src/components/dialog-connect-provider.tsx | 19 +- packages/app/src/components/dialog-select-mcp.tsx | 46 ++- .../src/components/dialog-select-model-unpaid.tsx | 20 +- .../app/src/components/dialog-select-model.tsx | 35 +- packages/app/src/components/prompt-input.tsx | 7 +- .../app/src/components/session-context-usage.tsx | 4 +- .../src/components/session/session-context-tab.tsx | 4 +- .../app/src/components/status-popover-body.tsx | 443 +++++++++++++++++++++ packages/app/src/components/status-popover.tsx | 410 +------------------ 9 files changed, 569 insertions(+), 419 deletions(-) create mode 100644 packages/app/src/components/status-popover-body.tsx (limited to 'packages/app/src/components') diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index e7eaa1fb2..41225d02a 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -15,13 +15,20 @@ import { Link } from "@/components/link" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" -import { DialogSelectProvider } from "./dialog-select-provider" +import { useProviders } from "@/hooks/use-providers" export function DialogConnectProvider(props: { provider: string }) { const dialog = useDialog() const globalSync = useGlobalSync() const globalSDK = useGlobalSDK() const language = useLanguage() + const providers = useProviders() + + const all = () => { + void import("./dialog-select-provider").then((x) => { + dialog.show(() => ) + }) + } const alive = { value: true } const timer = { current: undefined as ReturnType | undefined } @@ -33,7 +40,11 @@ export function DialogConnectProvider(props: { provider: string }) { timer.current = undefined }) - const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!) + const provider = createMemo( + () => + providers.all().find((x) => x.id === props.provider) ?? + globalSync.data.provider.all.find((x) => x.id === props.provider)!, + ) const fallback = createMemo(() => [ { type: "api" as const, @@ -333,7 +344,7 @@ export function DialogConnectProvider(props: { provider: string }) { function goBack() { if (methods().length === 1) { - dialog.show(() => ) + all() return } if (store.authorization) { @@ -344,7 +355,7 @@ export function DialogConnectProvider(props: { provider: string }) { dispatch({ type: "method.reset" }) return } - dialog.show(() => ) + all() } function MethodSelection() { diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx index fafba6168..98f262ce5 100644 --- a/packages/app/src/components/dialog-select-mcp.tsx +++ b/packages/app/src/components/dialog-select-mcp.tsx @@ -1,10 +1,12 @@ import { useMutation } from "@tanstack/solid-query" -import { Component, createMemo, Show } from "solid-js" +import { Component, createEffect, createMemo, on, Show } from "solid-js" +import { createStore } from "solid-js/store" import { useSync } from "@/context/sync" import { useSDK } from "@/context/sdk" import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" import { Switch } from "@opencode-ai/ui/switch" +import { showToast } from "@opencode-ai/ui/toast" import { useLanguage } from "@/context/language" const statusLabels = { @@ -18,6 +20,48 @@ export const DialogSelectMcp: Component = () => { const sync = useSync() const sdk = useSDK() const language = useLanguage() + const [state, setState] = createStore({ + done: false, + loading: false, + }) + + createEffect( + on( + () => sync.data.mcp_ready, + (ready, prev) => { + if (!ready && prev) setState("done", false) + }, + { defer: true }, + ), + ) + + createEffect(() => { + if (state.done || state.loading) return + if (sync.data.mcp_ready) { + setState("done", true) + return + } + + setState("loading", true) + void sdk.client.mcp + .status() + .then((result) => { + sync.set("mcp", result.data ?? {}) + sync.set("mcp_ready", true) + setState("done", true) + }) + .catch((err) => { + setState("done", true) + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: err instanceof Error ? err.message : String(err), + }) + }) + .finally(() => { + setState("loading", false) + }) + }) const items = createMemo(() => Object.entries(sync.data.mcp ?? {}) diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx index 2106b3a01..e25e8f0c1 100644 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid.tsx @@ -8,8 +8,6 @@ import { Tooltip } from "@opencode-ai/ui/tooltip" import { type Component, Show } from "solid-js" import { useLocal } from "@/context/local" import { popularProviders, useProviders } from "@/hooks/use-providers" -import { DialogConnectProvider } from "./dialog-connect-provider" -import { DialogSelectProvider } from "./dialog-select-provider" import { ModelTooltip } from "./model-tooltip" import { useLanguage } from "@/context/language" @@ -21,6 +19,18 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props const providers = useProviders() const language = useLanguage() + const connect = (provider: string) => { + void import("./dialog-connect-provider").then((x) => { + dialog.show(() => ) + }) + } + + const all = () => { + void import("./dialog-select-provider").then((x) => { + dialog.show(() => ) + }) + } + let listRef: ListRef | undefined const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") return @@ -91,7 +101,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props }} onSelect={(x) => { if (!x) return - dialog.show(() => ) + connect(x.id) }} > {(i) => ( @@ -122,9 +132,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props variant="ghost" class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium" icon="dot-grid" - onClick={() => { - dialog.show(() => ) - }} + onClick={all} > {language.t("dialog.provider.viewAll")} diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index 3654aab85..cb688c30a 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -10,8 +10,6 @@ import { Tag } from "@opencode-ai/ui/tag" import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" import { Tooltip } from "@opencode-ai/ui/tooltip" -import { DialogSelectProvider } from "./dialog-select-provider" -import { DialogManageModels } from "./dialog-manage-models" import { ModelTooltip } from "./model-tooltip" import { useLanguage } from "@/context/language" @@ -107,12 +105,16 @@ export function ModelSelectorPopover(props: { const handleManage = () => { setStore("open", false) - dialog.show(() => ) + void import("./dialog-manage-models").then((x) => { + dialog.show(() => ) + }) } const handleConnectProvider = () => { setStore("open", false) - dialog.show(() => ) + void import("./dialog-select-provider").then((x) => { + dialog.show(() => ) + }) } const language = useLanguage() @@ -193,26 +195,29 @@ export const DialogSelectModel: Component<{ provider?: string; model?: ModelStat const dialog = useDialog() const language = useLanguage() + const provider = () => { + void import("./dialog-select-provider").then((x) => { + dialog.show(() => ) + }) + } + + const manage = () => { + void import("./dialog-manage-models").then((x) => { + dialog.show(() => ) + }) + } + return ( dialog.show(() => )} - > + } > dialog.close()} /> - diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index ee98e68cd..1cc7c578d 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -27,7 +27,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Select } from "@opencode-ai/ui/select" import { useDialog } from "@opencode-ai/ui/context/dialog" import { ModelSelectorPopover } from "@/components/dialog-select-model" -import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid" import { useProviders } from "@/hooks/use-providers" import { useCommand } from "@/context/command" import { Persist, persisted } from "@/utils/persist" @@ -1494,7 +1493,11 @@ export const PromptInput: Component = (props) => { size="normal" class="min-w-0 max-w-[320px] text-13-regular text-text-base group" style={control()} - onClick={() => dialog.show(() => )} + onClick={() => { + void import("@/components/dialog-select-model-unpaid").then((x) => { + dialog.show(() => ) + }) + }} > props.variant ?? "button") @@ -50,7 +52,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) { }), ) - const metrics = createMemo(() => getSessionContextMetrics(messages(), sync.data.provider.all)) + const metrics = createMemo(() => getSessionContextMetrics(messages(), providers.all())) const context = createMemo(() => metrics().context) const cost = createMemo(() => { return usd().format(metrics().totalCost) diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index 4d90930a0..4e7dc8e78 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -12,6 +12,7 @@ import { Markdown } from "@opencode-ai/ui/markdown" import { ScrollView } from "@opencode-ai/ui/scroll-view" import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client" import { useLanguage } from "@/context/language" +import { useProviders } from "@/hooks/use-providers" import { useSessionLayout } from "@/pages/session/session-layout" import { getSessionContextMetrics } from "./session-context-metrics" import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown" @@ -92,6 +93,7 @@ const emptyUserMessages: UserMessage[] = [] export function SessionContextTab() { const sync = useSync() const language = useLanguage() + const providers = useProviders() const { params, view } = useSessionLayout() const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) @@ -130,7 +132,7 @@ export function SessionContextTab() { }), ) - const metrics = createMemo(() => getSessionContextMetrics(messages(), sync.data.provider.all)) + const metrics = createMemo(() => getSessionContextMetrics(messages(), providers.all())) const ctx = createMemo(() => metrics().context) const formatter = createMemo(() => createSessionContextFormatter(language.intl())) diff --git a/packages/app/src/components/status-popover-body.tsx b/packages/app/src/components/status-popover-body.tsx new file mode 100644 index 000000000..aaf9f58d6 --- /dev/null +++ b/packages/app/src/components/status-popover-body.tsx @@ -0,0 +1,443 @@ +import { Button } from "@opencode-ai/ui/button" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Icon } from "@opencode-ai/ui/icon" +import { Switch } from "@opencode-ai/ui/switch" +import { Tabs } from "@opencode-ai/ui/tabs" +import { useMutation } from "@tanstack/solid-query" +import { showToast } from "@opencode-ai/ui/toast" +import { useNavigate } from "@solidjs/router" +import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js" +import { createStore, reconcile } from "solid-js/store" +import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row" +import { useLanguage } from "@/context/language" +import { usePlatform } from "@/context/platform" +import { useSDK } from "@/context/sdk" +import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" +import { useSync } from "@/context/sync" +import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health" + +const pollMs = 10_000 + +const pluginEmptyMessage = (value: string, file: string): JSXElement => { + const parts = value.split(file) + if (parts.length === 1) return value + return ( + <> + {parts[0]} + {file} + {parts.slice(1).join(file)} + + ) +} + +const listServersByHealth = ( + list: ServerConnection.Any[], + active: ServerConnection.Key | undefined, + status: Record, +) => { + if (!list.length) return list + const order = new Map(list.map((url, index) => [url, index] as const)) + const rank = (value?: ServerHealth) => { + if (value?.healthy === true) return 0 + if (value?.healthy === false) return 2 + return 1 + } + + return list.slice().sort((a, b) => { + if (ServerConnection.key(a) === active) return -1 + if (ServerConnection.key(b) === active) return 1 + const diff = rank(status[ServerConnection.key(a)]) - rank(status[ServerConnection.key(b)]) + if (diff !== 0) return diff + return (order.get(a) ?? 0) - (order.get(b) ?? 0) + }) +} + +const useServerHealth = (servers: Accessor, enabled: Accessor) => { + const checkServerHealth = useCheckServerHealth() + const [status, setStatus] = createStore({} as Record) + + createEffect(() => { + if (!enabled()) { + setStatus(reconcile({})) + return + } + const list = servers() + let dead = false + + const refresh = async () => { + const results: Record = {} + await Promise.all( + list.map(async (conn) => { + results[ServerConnection.key(conn)] = await checkServerHealth(conn.http) + }), + ) + if (dead) return + setStatus(reconcile(results)) + } + + void refresh() + const id = setInterval(() => void refresh(), pollMs) + onCleanup(() => { + dead = true + clearInterval(id) + }) + }) + + return status +} + +const useDefaultServerKey = ( + get: (() => string | Promise | null | undefined) | undefined, +) => { + const [state, setState] = createStore({ + url: undefined as string | undefined, + tick: 0, + }) + + createEffect(() => { + state.tick + let dead = false + const result = get?.() + if (!result) { + setState("url", undefined) + onCleanup(() => { + dead = true + }) + return + } + + if (result instanceof Promise) { + void result.then((next) => { + if (dead) return + setState("url", next ? normalizeServerUrl(next) : undefined) + }) + onCleanup(() => { + dead = true + }) + return + } + + setState("url", normalizeServerUrl(result)) + onCleanup(() => { + dead = true + }) + }) + + return { + key: () => { + const u = state.url + if (!u) return + return ServerConnection.key({ type: "http", http: { url: u } }) + }, + refresh: () => setState("tick", (value) => value + 1), + } +} + +const useMcpToggleMutation = () => { + const sync = useSync() + const sdk = useSDK() + const language = useLanguage() + + return useMutation(() => ({ + mutationFn: async (name: string) => { + const status = sync.data.mcp[name] + await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name })) + const result = await sdk.client.mcp.status() + if (result.data) sync.set("mcp", result.data) + }, + onError: (err) => { + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: err instanceof Error ? err.message : String(err), + }) + }, + })) +} + +export function StatusPopoverBody(props: { shown: Accessor }) { + const sync = useSync() + const server = useServer() + const platform = usePlatform() + const dialog = useDialog() + const language = useLanguage() + const navigate = useNavigate() + const sdk = useSDK() + + const [load, setLoad] = createStore({ + lspDone: false, + lspLoading: false, + mcpDone: false, + mcpLoading: false, + }) + + const fail = (err: unknown) => { + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: err instanceof Error ? err.message : String(err), + }) + } + + createEffect(() => { + if (!props.shown()) return + + if (!sync.data.mcp_ready && !load.mcpDone && !load.mcpLoading) { + setLoad("mcpLoading", true) + void sdk.client.mcp + .status() + .then((result) => { + sync.set("mcp", result.data ?? {}) + sync.set("mcp_ready", true) + }) + .catch((err) => { + setLoad("mcpDone", true) + fail(err) + }) + .finally(() => { + setLoad("mcpLoading", false) + }) + } + + if (!sync.data.lsp_ready && !load.lspDone && !load.lspLoading) { + setLoad("lspLoading", true) + void sdk.client.lsp + .status() + .then((result) => { + sync.set("lsp", result.data ?? []) + sync.set("lsp_ready", true) + }) + .catch((err) => { + setLoad("lspDone", true) + fail(err) + }) + .finally(() => { + setLoad("lspLoading", false) + }) + } + }) + + let dialogRun = 0 + let dialogDead = false + onCleanup(() => { + dialogDead = true + dialogRun += 1 + }) + const servers = createMemo(() => { + const current = server.current + const list = server.list + if (!current) return list + if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list] + return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))] + }) + const health = useServerHealth(servers, props.shown) + const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health)) + const toggleMcp = useMcpToggleMutation() + const defaultServer = useDefaultServerKey(platform.getDefaultServer) + const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b))) + const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status + const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length) + const lspItems = createMemo(() => sync.data.lsp ?? []) + const lspCount = createMemo(() => lspItems().length) + const plugins = createMemo(() => sync.data.config.plugin ?? []) + const pluginCount = createMemo(() => plugins().length) + const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json")) + + return ( +
+ + + + {sortedServers().length > 0 ? `${sortedServers().length} ` : ""} + {language.t("status.popover.tab.servers")} + + + {mcpConnected() > 0 ? `${mcpConnected()} ` : ""} + {language.t("status.popover.tab.mcp")} + + + {lspCount() > 0 ? `${lspCount()} ` : ""} + {language.t("status.popover.tab.lsp")} + + + {pluginCount() > 0 ? `${pluginCount()} ` : ""} + {language.t("status.popover.tab.plugins")} + + + + +
+
+ + {(s) => { + const key = ServerConnection.key(s) + const blocked = () => health[key]?.healthy === false + return ( + + ) + }} + + + +
+
+
+ + +
+
+ 0} + fallback={ +
{language.t("dialog.mcp.empty")}
+ } + > + + {(name) => { + const status = () => mcpStatus(name) + const enabled = () => status() === "connected" + return ( + + ) + }} + +
+
+
+
+ + +
+
+ 0} + fallback={ +
{language.t("dialog.lsp.empty")}
+ } + > + + {(item) => ( +
+
+ {item.name || item.id} +
+ )} + + +
+
+ + + +
+
+ 0} + fallback={
{pluginEmpty()}
} + > + + {(plugin) => ( +
+
+ {plugin} +
+ )} + + +
+
+ + +
+ ) +} diff --git a/packages/app/src/components/status-popover.tsx b/packages/app/src/components/status-popover.tsx index 8d5ecac39..6820a940b 100644 --- a/packages/app/src/components/status-popover.tsx +++ b/packages/app/src/components/status-popover.tsx @@ -1,202 +1,24 @@ import { Button } from "@opencode-ai/ui/button" -import { useDialog } from "@opencode-ai/ui/context/dialog" import { Icon } from "@opencode-ai/ui/icon" import { Popover } from "@opencode-ai/ui/popover" -import { Switch } from "@opencode-ai/ui/switch" -import { Tabs } from "@opencode-ai/ui/tabs" -import { useMutation } from "@tanstack/solid-query" -import { showToast } from "@opencode-ai/ui/toast" -import { useNavigate } from "@solidjs/router" -import { type Accessor, createEffect, createMemo, createSignal, For, type JSXElement, onCleanup, Show } from "solid-js" -import { createStore, reconcile } from "solid-js/store" -import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row" +import { Suspense, createMemo, createSignal, lazy, Show } from "solid-js" import { useLanguage } from "@/context/language" -import { usePlatform } from "@/context/platform" -import { useSDK } from "@/context/sdk" -import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" +import { useServer } from "@/context/server" import { useSync } from "@/context/sync" -import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health" -const pollMs = 10_000 - -const pluginEmptyMessage = (value: string, file: string): JSXElement => { - const parts = value.split(file) - if (parts.length === 1) return value - return ( - <> - {parts[0]} - {file} - {parts.slice(1).join(file)} - - ) -} - -const listServersByHealth = ( - list: ServerConnection.Any[], - active: ServerConnection.Key | undefined, - status: Record, -) => { - if (!list.length) return list - const order = new Map(list.map((url, index) => [url, index] as const)) - const rank = (value?: ServerHealth) => { - if (value?.healthy === true) return 0 - if (value?.healthy === false) return 2 - return 1 - } - - return list.slice().sort((a, b) => { - if (ServerConnection.key(a) === active) return -1 - if (ServerConnection.key(b) === active) return 1 - const diff = rank(status[ServerConnection.key(a)]) - rank(status[ServerConnection.key(b)]) - if (diff !== 0) return diff - return (order.get(a) ?? 0) - (order.get(b) ?? 0) - }) -} - -const useServerHealth = (servers: Accessor, enabled: Accessor) => { - const checkServerHealth = useCheckServerHealth() - const [status, setStatus] = createStore({} as Record) - - createEffect(() => { - if (!enabled()) { - setStatus(reconcile({})) - return - } - const list = servers() - let dead = false - - const refresh = async () => { - const results: Record = {} - await Promise.all( - list.map(async (conn) => { - results[ServerConnection.key(conn)] = await checkServerHealth(conn.http) - }), - ) - if (dead) return - setStatus(reconcile(results)) - } - - void refresh() - const id = setInterval(() => void refresh(), pollMs) - onCleanup(() => { - dead = true - clearInterval(id) - }) - }) - - return status -} - -const useDefaultServerKey = ( - get: (() => string | Promise | null | undefined) | undefined, -) => { - const [state, setState] = createStore({ - url: undefined as string | undefined, - tick: 0, - }) - - createEffect(() => { - state.tick - let dead = false - const result = get?.() - if (!result) { - setState("url", undefined) - onCleanup(() => { - dead = true - }) - return - } - - if (result instanceof Promise) { - void result.then((next) => { - if (dead) return - setState("url", next ? normalizeServerUrl(next) : undefined) - }) - onCleanup(() => { - dead = true - }) - return - } - - setState("url", normalizeServerUrl(result)) - onCleanup(() => { - dead = true - }) - }) - - return { - key: () => { - const u = state.url - if (!u) return - return ServerConnection.key({ type: "http", http: { url: u } }) - }, - refresh: () => setState("tick", (value) => value + 1), - } -} - -const useMcpToggleMutation = () => { - const sync = useSync() - const sdk = useSDK() - const language = useLanguage() - - return useMutation(() => ({ - mutationFn: async (name: string) => { - const status = sync.data.mcp[name] - await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name })) - const result = await sdk.client.mcp.status() - if (result.data) sync.set("mcp", result.data) - }, - onError: (err) => { - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: err instanceof Error ? err.message : String(err), - }) - }, - })) -} +const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody }))) export function StatusPopover() { - const sync = useSync() - const server = useServer() - const platform = usePlatform() - const dialog = useDialog() const language = useLanguage() - const navigate = useNavigate() - + const server = useServer() + const sync = useSync() const [shown, setShown] = createSignal(false) - let dialogRun = 0 - let dialogDead = false - onCleanup(() => { - dialogDead = true - dialogRun += 1 - }) - const servers = createMemo(() => { - const current = server.current - const list = server.list - if (!current) return list - if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list] - return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))] - }) - const health = useServerHealth(servers, shown) - const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health)) - const toggleMcp = useMcpToggleMutation() - const defaultServer = useDefaultServerKey(platform.getDefaultServer) - const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b))) - const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status - const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length) - const lspItems = createMemo(() => sync.data.lsp ?? []) - const lspCount = createMemo(() => lspItems().length) - const plugins = createMemo(() => sync.data.config.plugin ?? []) - const pluginCount = createMemo(() => plugins().length) - const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json")) - const overallHealthy = createMemo(() => { + const ready = createMemo(() => server.healthy() === false || sync.data.mcp_ready) + const healthy = createMemo(() => { const serverHealthy = server.healthy() === true - const anyMcpIssue = mcpNames().some((name) => { - const status = mcpStatus(name) - return status !== "connected" && status !== "disabled" - }) - return serverHealthy && !anyMcpIssue + const mcp = Object.values(sync.data.mcp ?? {}) + const issue = mcp.some((item) => item.status !== "connected" && item.status !== "disabled") + return serverHealthy && !issue }) return ( @@ -218,9 +40,9 @@ export function StatusPopover() {
@@ -230,205 +52,15 @@ export function StatusPopover() { placement="bottom-end" shift={-168} > -
- + + } > - - - {sortedServers().length > 0 ? `${sortedServers().length} ` : ""} - {language.t("status.popover.tab.servers")} - - - {mcpConnected() > 0 ? `${mcpConnected()} ` : ""} - {language.t("status.popover.tab.mcp")} - - - {lspCount() > 0 ? `${lspCount()} ` : ""} - {language.t("status.popover.tab.lsp")} - - - {pluginCount() > 0 ? `${pluginCount()} ` : ""} - {language.t("status.popover.tab.plugins")} - - - - -
-
- - {(s) => { - const key = ServerConnection.key(s) - const isBlocked = () => health[key]?.healthy === false - return ( - - ) - }} - - - -
-
-
- - -
-
- 0} - fallback={ -
- {language.t("dialog.mcp.empty")} -
- } - > - - {(name) => { - const status = () => mcpStatus(name) - const enabled = () => status() === "connected" - return ( - - ) - }} - -
-
-
-
- - -
-
- 0} - fallback={ -
- {language.t("dialog.lsp.empty")} -
- } - > - - {(item) => ( -
-
- {item.name || item.id} -
- )} - - -
-
- - - -
-
- 0} - fallback={
{pluginEmpty()}
} - > - - {(plugin) => ( -
-
- {plugin} -
- )} - - -
-
- - -
+ + + ) } -- cgit v1.2.3