Skip to content

Commit b0fcba5

Browse files
authored
feat(app): make server sdk + sync state global (anomalyco#29285)
1 parent 2b3ddf9 commit b0fcba5

13 files changed

Lines changed: 248 additions & 182 deletions

File tree

854 KB
Binary file not shown.

packages/app/src/app.tsx

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ import DirectoryLayout from "@/pages/directory-layout"
4646
import Layout from "@/pages/layout"
4747
import { ErrorPage } from "./pages/error"
4848
import { useCheckServerHealth } from "./utils/server-health"
49+
import { ServersProvider } from "./context/servers"
50+
51+
if (import.meta.env.VITE_OPENCODE_CHANNEL !== "prod") {
52+
document.body.classList.remove("text-12-regular")
53+
document.body.classList.add("font-(family-name:--font-family-text)", "text-[13px]", "font-[440]")
54+
}
4955

5056
const HomeRoute = lazy(() => import("@/pages/home"))
5157
const Session = lazy(() => import("@/pages/session"))
@@ -296,31 +302,29 @@ export function AppInterface(props: {
296302
disableHealthCheck?: boolean
297303
}) {
298304
return (
299-
<ServerProvider
300-
defaultServer={props.defaultServer}
301-
disableHealthCheck={props.disableHealthCheck}
302-
servers={props.servers}
303-
>
304-
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
305-
<ServerKey>
306-
<QueryProvider>
307-
<ServerSDKProvider>
308-
<ServerSyncProvider>
309-
<Dynamic
310-
component={props.router ?? Router}
311-
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
312-
>
313-
<Route path="/" component={HomeRoute} />
314-
<Route path="/:dir" component={DirectoryLayout}>
315-
<Route path="/" component={() => <Navigate href="session" />} />
316-
<Route path="/session/:id?" component={SessionRoute} />
317-
</Route>
318-
</Dynamic>
319-
</ServerSyncProvider>
320-
</ServerSDKProvider>
321-
</QueryProvider>
322-
</ServerKey>
323-
</ConnectionGate>
305+
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
306+
<ServersProvider>
307+
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
308+
<ServerKey>
309+
<QueryProvider>
310+
<ServerSDKProvider>
311+
<ServerSyncProvider>
312+
<Dynamic
313+
component={props.router ?? Router}
314+
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
315+
>
316+
<Route path="/" component={HomeRoute} />
317+
<Route path="/:dir" component={DirectoryLayout}>
318+
<Route path="/" component={() => <Navigate href="session" />} />
319+
<Route path="/session/:id?" component={SessionRoute} />
320+
</Route>
321+
</Dynamic>
322+
</ServerSyncProvider>
323+
</ServerSDKProvider>
324+
</QueryProvider>
325+
</ServerKey>
326+
</ConnectionGate>
327+
</ServersProvider>
324328
</ServerProvider>
325329
)
326330
}

packages/app/src/components/status-popover-body.tsx

Lines changed: 9 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,17 @@ import { useMutation, useQueryClient } from "@tanstack/solid-query"
77
import { showToast } from "@opencode-ai/ui/toast"
88
import { useNavigate } from "@solidjs/router"
99
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
10-
import { createStore, reconcile } from "solid-js/store"
10+
import { createStore } from "solid-js/store"
1111
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
1212
import { useLanguage } from "@/context/language"
1313
import { usePlatform } from "@/context/platform"
1414
import { useSDK } from "@/context/sdk"
1515
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
1616
import { useSync } from "@/context/sync"
17-
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
17+
import { type ServerHealth } from "@/utils/server-health"
1818
import { useQueryOptions } from "@/context/server-sync"
1919
import { pathKey } from "@/utils/path-key"
20+
import { useServers } from "@/context/servers"
2021

2122
const pollMs = 10_000
2223

@@ -54,40 +55,6 @@ const listServersByHealth = (
5455
})
5556
}
5657

57-
const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, enabled: Accessor<boolean>) => {
58-
const checkServerHealth = useCheckServerHealth()
59-
const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
60-
61-
createEffect(() => {
62-
if (!enabled()) {
63-
setStatus(reconcile({}))
64-
return
65-
}
66-
const list = servers()
67-
let dead = false
68-
69-
const refresh = async () => {
70-
const results: Record<string, ServerHealth> = {}
71-
await Promise.all(
72-
list.map(async (conn) => {
73-
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http)
74-
}),
75-
)
76-
if (dead) return
77-
setStatus(reconcile(results))
78-
}
79-
80-
void refresh()
81-
const id = setInterval(() => void refresh(), pollMs)
82-
onCleanup(() => {
83-
dead = true
84-
clearInterval(id)
85-
})
86-
})
87-
88-
return status
89-
}
90-
9158
const useDefaultServerKey = (
9259
get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined,
9360
) => {
@@ -168,6 +135,7 @@ const useMcpToggleMutation = () => {
168135

169136
export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
170137
const sync = useSync()
138+
const servers = useServers()
171139
const server = useServer()
172140
const platform = usePlatform()
173141
const dialog = useDialog()
@@ -192,15 +160,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
192160
dialogDead = true
193161
dialogRun += 1
194162
})
195-
const servers = createMemo(() => {
196-
const current = server.current
197-
const list = server.list
198-
if (!current) return list
199-
if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list]
200-
return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))]
201-
})
202-
const health = useServerHealth(servers, props.shown)
203-
const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health))
163+
const sortedServers = createMemo(() => listServersByHealth(servers.list(), server.key, servers.health))
204164
const toggleMcp = useMcpToggleMutation()
205165
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
206166
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
@@ -226,7 +186,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
226186
>
227187
<Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
228188
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
229-
{sortedServers().length > 0 ? `${sortedServers().length} ` : ""}
189+
{servers.list().length > 0 ? `${servers.list().length} ` : ""}
230190
{language.t("status.popover.tab.servers")}
231191
</Tabs.Trigger>
232192
<Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
@@ -249,7 +209,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
249209
<For each={sortedServers()}>
250210
{(s) => {
251211
const key = ServerConnection.key(s)
252-
const blocked = () => health[key]?.healthy === false
212+
const blocked = () => servers.health[key]?.healthy === false
253213
return (
254214
<button
255215
type="button"
@@ -265,11 +225,11 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
265225
queueMicrotask(() => server.setActive(key))
266226
}}
267227
>
268-
<ServerHealthIndicator health={health[key]} />
228+
<ServerHealthIndicator health={servers.health[key]} />
269229
<ServerRow
270230
conn={s}
271231
dimmed={blocked()}
272-
status={health[key]}
232+
status={servers.health[key]}
273233
class="flex items-center gap-2 w-full min-w-0"
274234
nameClass="text-14-regular text-text-base truncate"
275235
versionClass="text-12-regular text-text-weak truncate"

packages/app/src/components/status-popover.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,26 @@ import { Suspense, createMemo, createSignal, lazy, Show } from "solid-js"
55
import { useLanguage } from "@/context/language"
66
import { useServer } from "@/context/server"
77
import { useSync } from "@/context/sync"
8+
import { useServers } from "@/context/servers"
89

910
const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody })))
1011

1112
export function StatusPopover() {
1213
const language = useLanguage()
1314
const server = useServer()
15+
const servers = useServers()
1416
const sync = useSync()
1517
const [shown, setShown] = createSignal(false)
16-
const ready = createMemo(() => server.healthy() === false || sync.data.mcp_ready)
18+
const ready = createMemo(() => servers.health[server.key]?.healthy === false || sync.data.mcp_ready)
1719
const mcpIssue = createMemo(() => {
1820
const mcp = Object.values(sync.data.mcp ?? {})
1921
const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
2022
const warn = mcp.some((item) => item.status === "needs_auth")
2123
if (failed) return "critical" as const
2224
if (warn) return "warning" as const
2325
})
24-
const healthy = createMemo(() => server.healthy() === true && !mcpIssue())
26+
const serverHealthy = () => servers.health[server.key]?.healthy === true
27+
const healthy = createMemo(() => servers.health[server.key]?.healthy === true && !mcpIssue())
2528

2629
return (
2730
<Popover
@@ -43,10 +46,9 @@ export function StatusPopover() {
4346
classList={{
4447
"absolute -top-px -right-px size-1.5 rounded-full": true,
4548
"bg-icon-success-base": ready() && healthy(),
46-
"bg-icon-warning-base": ready() && server.healthy() === true && mcpIssue() === "warning",
47-
"bg-icon-critical-base":
48-
server.healthy() === false || (ready() && server.healthy() === true && mcpIssue() === "critical"),
49-
"bg-border-weak-base": server.healthy() === undefined || !ready(),
49+
"bg-icon-warning-base": ready() && serverHealthy() && mcpIssue() === "warning",
50+
"bg-icon-critical-base": serverHealthy() || (ready() && serverHealthy() && mcpIssue() === "critical"),
51+
"bg-border-weak-base": serverHealthy() || !ready(),
5052
}}
5153
/>
5254
</div>

packages/app/src/context/server.tsx

Lines changed: 3 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { createSimpleContext } from "@opencode-ai/ui/context"
2-
import { type Accessor, batch, createEffect, createMemo, onCleanup } from "solid-js"
2+
import { type Accessor, batch, createMemo } from "solid-js"
33
import { createStore } from "solid-js/store"
44
import { Persist, persisted } from "@/utils/persist"
5-
import { useCheckServerHealth } from "@/utils/server-health"
65

76
type StoredProject = { worktree: string; expanded: boolean }
87
type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http
@@ -38,6 +37,7 @@ export function resolveServerList(input: {
3837
stored: StoredServer[]
3938
}): Array<ServerConnection.Any> {
4039
const servers = [
40+
...(input.props ?? []),
4141
...input.stored.map((value) =>
4242
typeof value === "string"
4343
? {
@@ -46,7 +46,6 @@ export function resolveServerList(input: {
4646
}
4747
: value,
4848
),
49-
...(input.props ?? []),
5049
]
5150

5251
const deduped = new Map<ServerConnection.Key, ServerConnection.Any>()
@@ -122,13 +121,7 @@ export namespace ServerConnection {
122121

123122
export const { use: useServer, provider: ServerProvider } = createSimpleContext({
124123
name: "Server",
125-
init: (props: {
126-
defaultServer: ServerConnection.Key
127-
disableHealthCheck?: boolean
128-
servers?: Array<ServerConnection.Any>
129-
}) => {
130-
const checkServerHealth = useCheckServerHealth()
131-
124+
init: (props: { defaultServer: ServerConnection.Key; servers?: Array<ServerConnection.Any> }) => {
132125
const [store, setStore, _, ready] = persisted(
133126
Persist.global("server", ["server.v3"]),
134127
createStore({
@@ -146,36 +139,8 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
146139

147140
const [state, setState] = createStore({
148141
active: props.defaultServer,
149-
healthy: undefined as boolean | undefined,
150142
})
151143

152-
const healthy = () => state.healthy
153-
154-
function startHealthPolling(conn: ServerConnection.Any) {
155-
let alive = true
156-
let busy = false
157-
158-
const run = () => {
159-
if (busy) return
160-
busy = true
161-
void check(conn)
162-
.then((next) => {
163-
if (!alive) return
164-
setState("healthy", next)
165-
})
166-
.finally(() => {
167-
busy = false
168-
})
169-
}
170-
171-
run()
172-
const interval = setInterval(run, HEALTH_POLL_INTERVAL_MS)
173-
return () => {
174-
alive = false
175-
clearInterval(interval)
176-
}
177-
}
178-
179144
function setActive(input: ServerConnection.Key) {
180145
if (state.active !== input) setState("active", input)
181146
}
@@ -209,20 +174,6 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
209174

210175
const isReady = createMemo(() => ready() && !!state.active)
211176

212-
const check = (conn: ServerConnection.Any) => checkServerHealth(conn.http).then((x) => x.healthy)
213-
214-
createEffect(() => {
215-
const current_ = current()
216-
if (!current_) return
217-
218-
if (props.disableHealthCheck) {
219-
setState("healthy", true)
220-
return
221-
}
222-
setState("healthy", undefined)
223-
onCleanup(startHealthPolling(current_))
224-
})
225-
226177
const origin = createMemo(() => projectsKey(state.active))
227178
const projectsList = createMemo(() => store.projects[origin()] ?? [])
228179
const current: Accessor<ServerConnection.Any | undefined> = createMemo(
@@ -235,7 +186,6 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
235186

236187
return {
237188
ready: isReady,
238-
healthy,
239189
isLocal,
240190
get key() {
241191
return state.active
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { createSimpleContext } from "@opencode-ai/ui/context"
2+
import { useServer } from "./server"
3+
import { useServerHealth } from "@/utils/server-health"
4+
5+
export const { use: useServers, provider: ServersProvider } = createSimpleContext({
6+
name: "Servers",
7+
init: () => {
8+
const server = useServer()
9+
10+
const health = useServerHealth(
11+
() => server.list,
12+
() => true,
13+
)
14+
15+
return {
16+
list: () => server.list,
17+
health,
18+
}
19+
},
20+
})

packages/app/src/entry.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,7 @@ if (root instanceof HTMLElement) {
168168
() => (
169169
<PlatformProvider value={platform}>
170170
<AppBaseProviders>
171-
<AppInterface
172-
defaultServer={ServerConnection.Key.make(getDefaultUrl())}
173-
servers={[server]}
174-
disableHealthCheck
175-
/>
171+
<AppInterface defaultServer={ServerConnection.Key.make(getDefaultUrl())} servers={[server]} />
176172
</AppBaseProviders>
177173
</PlatformProvider>
178174
),

packages/app/src/index.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
font-style: normal;
99
}
1010

11+
@font-face {
12+
font-family: "Inter";
13+
src: url("/assets/Inter.ttf") format("truetype");
14+
font-weight: normal;
15+
font-style: normal;
16+
}
17+
1118
@layer components {
1219
@keyframes session-progress-whip {
1320
0% {

0 commit comments

Comments
 (0)