diff --git a/packages/app/public/assets/Inter.ttf b/packages/app/public/assets/Inter.ttf new file mode 100644 index 0000000000..e31b51e3e9 Binary files /dev/null and b/packages/app/public/assets/Inter.ttf differ diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 339cda8edf..c4eadbd5fb 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -46,6 +46,12 @@ import DirectoryLayout from "@/pages/directory-layout" import Layout from "@/pages/layout" import { ErrorPage } from "./pages/error" import { useCheckServerHealth } from "./utils/server-health" +import { ServersProvider } from "./context/servers" + +if (import.meta.env.VITE_OPENCODE_CHANNEL !== "prod") { + document.body.classList.remove("text-12-regular") + document.body.classList.add("font-(family-name:--font-family-text)", "text-[13px]", "font-[440]") +} const HomeRoute = lazy(() => import("@/pages/home")) const Session = lazy(() => import("@/pages/session")) @@ -296,31 +302,29 @@ export function AppInterface(props: { disableHealthCheck?: boolean }) { return ( - - - - - - - {routerProps.children}} - > - - - } /> - - - - - - - - + + + + + + + + {routerProps.children}} + > + + + } /> + + + + + + + + + ) } diff --git a/packages/app/src/components/status-popover-body.tsx b/packages/app/src/components/status-popover-body.tsx index 1febaf2a2b..438ca82f00 100644 --- a/packages/app/src/components/status-popover-body.tsx +++ b/packages/app/src/components/status-popover-body.tsx @@ -7,16 +7,17 @@ import { useMutation, useQueryClient } 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 { createStore } 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" +import { type ServerHealth } from "@/utils/server-health" import { useQueryOptions } from "@/context/server-sync" import { pathKey } from "@/utils/path-key" +import { useServers } from "@/context/servers" const pollMs = 10_000 @@ -54,40 +55,6 @@ const listServersByHealth = ( }) } -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, ) => { @@ -168,6 +135,7 @@ const useMcpToggleMutation = () => { export function StatusPopoverBody(props: { shown: Accessor }) { const sync = useSync() + const servers = useServers() const server = useServer() const platform = usePlatform() const dialog = useDialog() @@ -192,15 +160,7 @@ export function StatusPopoverBody(props: { shown: Accessor }) { 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 sortedServers = createMemo(() => listServersByHealth(servers.list(), server.key, servers.health)) const toggleMcp = useMcpToggleMutation() const defaultServer = useDefaultServerKey(platform.getDefaultServer) const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b))) @@ -226,7 +186,7 @@ export function StatusPopoverBody(props: { shown: Accessor }) { > - {sortedServers().length > 0 ? `${sortedServers().length} ` : ""} + {servers.list().length > 0 ? `${servers.list().length} ` : ""} {language.t("status.popover.tab.servers")} @@ -249,7 +209,7 @@ export function StatusPopoverBody(props: { shown: Accessor }) { {(s) => { const key = ServerConnection.key(s) - const blocked = () => health[key]?.healthy === false + const blocked = () => servers.health[key]?.healthy === false return ( - } - > - - {(project) => ( - - )} - - - + + } + > + {(server) => { + const key = ServerConnection.key(server) + const healthy = () => !!servers.health[key]?.healthy + const [open, setOpen] = createSignal(true) + + return ( +
+
+ + } + /> +
+ +
+ + +
+ ) + }} +
) } + +function ProjectList(props: { + projects: LocalProject[] + selectedProject?: string + onSelectedProjectChange?(project: string): void + onChooseProject?(): void +}) { + const language = useLanguage() + + return ( + 0} + fallback={ + + } + > +
+ + {(project) => ( + + )} + +
+
+ ) +} diff --git a/packages/app/src/utils/server-health.ts b/packages/app/src/utils/server-health.ts index a13fd34ef7..0081f3d60c 100644 --- a/packages/app/src/utils/server-health.ts +++ b/packages/app/src/utils/server-health.ts @@ -1,6 +1,8 @@ import { usePlatform } from "@/context/platform" -import type { ServerConnection } from "@/context/server" +import { ServerConnection } from "@/context/server" import { createSdkForServer } from "./server" +import { Accessor, createEffect, onCleanup } from "solid-js" +import { createStore, reconcile } from "solid-js/store" export type ServerHealth = { healthy: boolean; version?: string } @@ -92,6 +94,8 @@ export async function checkServerHealth( return attempt(0).finally(() => timeout?.clear?.()) } +const pollMs = 10_000 + export function useCheckServerHealth() { const platform = usePlatform() const fetcher = platform.fetch ?? globalThis.fetch @@ -111,3 +115,37 @@ export function useCheckServerHealth() { return promise } } + +export 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 +} diff --git a/packages/ui/src/v2/components/icon-button-v2.css b/packages/ui/src/v2/components/icon-button-v2.css index f5ea604e92..e75e4c7ade 100644 --- a/packages/ui/src/v2/components/icon-button-v2.css +++ b/packages/ui/src/v2/components/icon-button-v2.css @@ -5,7 +5,6 @@ } [data-component="icon-button-v2"] { - position: relative; display: inline-flex; align-items: center; justify-content: center; diff --git a/packages/ui/src/v2/components/icon.tsx b/packages/ui/src/v2/components/icon.tsx index 89190869ae..0e2b92b09b 100644 --- a/packages/ui/src/v2/components/icon.tsx +++ b/packages/ui/src/v2/components/icon.tsx @@ -37,6 +37,14 @@ const icons = { viewBox: "0 0 16 16", body: ``, }, + "outline-chevron-down": { + viewBox: "0 0 16 16", + body: ``, + }, + "outline-dots": { + viewBox: "0 0 16 16", + body: ``, + }, } const spriteID = "opencode-v2-icon-sprite" diff --git a/packages/ui/src/v2/styles/theme.css b/packages/ui/src/v2/styles/theme.css index 2dc795d438..91e90d678d 100644 --- a/packages/ui/src/v2/styles/theme.css +++ b/packages/ui/src/v2/styles/theme.css @@ -92,6 +92,8 @@ --v2-illustration-illustration-layer-01: var(--v2-grey-300); --v2-illustration-illustration-layer-02: var(--v2-grey-400); --v2-illustration-illustration-layer-03: var(--v2-grey-500); + + --font-family-text: "Inter", sans-serif; } /* OS preference fallback (no JS needed) */