Skip to content

Commit f7482cc

Browse files
committed
feat: configure desktop local server access
1 parent 03de7f8 commit f7482cc

19 files changed

Lines changed: 652 additions & 319 deletions

File tree

packages/app/src/components/dialog-select-server.tsx

Lines changed: 339 additions & 48 deletions
Large diffs are not rendered by default.

packages/app/src/components/server/server-row.tsx

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,20 @@ interface ServerRowProps extends ParentProps {
2323
dimmed?: boolean
2424
badge?: JSXElement
2525
showCredentials?: boolean
26+
name?: string
27+
details?: string[]
28+
credentials?: {
29+
username?: string
30+
password?: string
31+
}
2632
}
2733

2834
export function ServerRow(props: ServerRowProps) {
2935
const language = useLanguage()
3036
const [truncated, setTruncated] = createSignal(false)
3137
let nameRef: HTMLSpanElement | undefined
3238
let versionRef: HTMLSpanElement | undefined
33-
const name = createMemo(() => serverName(props.conn))
39+
const name = createMemo(() => props.name ?? serverName(props.conn))
3440

3541
const check = () => {
3642
const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false
@@ -53,22 +59,31 @@ export function ServerRow(props: ServerRowProps) {
5359

5460
const tooltipValue = () => (
5561
<span class="flex items-center gap-2">
56-
<span>{serverName(props.conn, true)}</span>
62+
<span>{name()}</span>
5763
<Show when={props.status?.version}>
5864
<span class="text-text-invert-weak">v{props.status?.version}</span>
5965
</Show>
6066
</span>
6167
)
6268

6369
const badge = children(() => props.badge)
70+
const details = createMemo(() => props.details?.filter(Boolean) ?? [])
71+
const credentials = createMemo(() => {
72+
if (props.credentials) return props.credentials
73+
if (props.conn.type !== "http") return
74+
return {
75+
username: props.conn.http.username,
76+
password: props.conn.http.password,
77+
}
78+
})
6479

6580
return (
6681
<Tooltip
6782
class="flex-1 min-w-0"
6883
value={tooltipValue()}
6984
contentStyle={{ "max-width": "none", "white-space": "nowrap" }}
7085
placement="top-start"
71-
inactive={!truncated() && !props.conn.displayName}
86+
inactive={!truncated() && !props.conn.displayName && !props.name}
7287
>
7388
<div class={props.class} classList={{ "opacity-50": props.dimmed }}>
7489
<div class="flex flex-col items-start min-w-0 w-full">
@@ -92,19 +107,21 @@ export function ServerRow(props: ServerRowProps) {
92107
{(badge) => badge()}
93108
</Show>
94109
</div>
95-
<Show when={props.showCredentials && props.conn.type === "http" && props.conn}>
96-
{(conn) => (
97-
<div class="flex flex-row gap-3">
98-
<span>
99-
{conn().http.username ? (
100-
<span class="text-text-weak">{conn().http.username}</span>
101-
) : (
102-
<span class="text-text-weaker">{language.t("server.row.noUsername")}</span>
103-
)}
104-
</span>
105-
{conn().http.password && <span class="text-text-weak">••••••••</span>}
106-
</div>
107-
)}
110+
<Show when={props.showCredentials && (details().length || credentials())}>
111+
<div class="flex flex-row gap-3">
112+
<Show when={details().length}>
113+
<span class="text-text-weak">{details().join(" · ")}</span>
114+
</Show>
115+
<Show when={credentials()?.username}>
116+
<span class="text-text-weak">{credentials()?.username}</span>
117+
</Show>
118+
<Show when={!details().length && credentials() && !credentials()?.username}>
119+
<span class="text-text-weaker">{language.t("server.row.noUsername")}</span>
120+
</Show>
121+
<Show when={credentials()?.password}>
122+
<span class="text-text-weak">••••••••</span>
123+
</Show>
124+
</div>
108125
</Show>
109126
</div>
110127
{props.children}

packages/app/src/components/settings-general.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,6 @@ export const SettingsGeneral: Component = () => {
119119

120120
permission.disableAutoAccept(params.id, value)
121121
}
122-
const desktop = createMemo(() => platform.platform === "desktop")
123-
124122
const check = () => {
125123
if (!platform.checkUpdate) return
126124
setStore("checking", true)
@@ -729,7 +727,6 @@ export const SettingsGeneral: Component = () => {
729727
</div>
730728
)
731729

732-
console.log(import.meta.env)
733730
return (
734731
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
735732
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
@@ -775,7 +772,7 @@ export const SettingsGeneral: Component = () => {
775772
</div>
776773
</Show>
777774

778-
<Show when={desktop() && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"}>
775+
<Show when={platform.platform === "desktop" && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"}>
779776
<AdvancedSection />
780777
</Show>
781778
</div>

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

Lines changed: 4 additions & 220 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,13 @@
1-
import { Button } from "@opencode-ai/ui/button"
2-
import { useDialog } from "@opencode-ai/ui/context/dialog"
3-
import { Icon } from "@opencode-ai/ui/icon"
41
import { Switch } from "@opencode-ai/ui/switch"
52
import { Tabs } from "@opencode-ai/ui/tabs"
63
import { useMutation, useQueryClient } from "@tanstack/solid-query"
74
import { showToast } from "@opencode-ai/ui/toast"
8-
import { useNavigate } from "@solidjs/router"
9-
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
10-
import { createStore, reconcile } from "solid-js/store"
11-
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
5+
import { type Accessor, createMemo, For, type JSXElement, Show } from "solid-js"
126
import { useLanguage } from "@/context/language"
13-
import { usePlatform } from "@/context/platform"
147
import { useSDK } from "@/context/sdk"
15-
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
168
import { useSync } from "@/context/sync"
17-
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
189
import { mcpQueryKey } from "@/context/global-sync"
1910

20-
const pollMs = 10_000
21-
2211
const pluginEmptyMessage = (value: string, file: string): JSXElement => {
2312
const parts = value.split(file)
2413
if (parts.length === 1) return value
@@ -31,109 +20,6 @@ const pluginEmptyMessage = (value: string, file: string): JSXElement => {
3120
)
3221
}
3322

34-
const listServersByHealth = (
35-
list: ServerConnection.Any[],
36-
active: ServerConnection.Key | undefined,
37-
status: Record<ServerConnection.Key, ServerHealth | undefined>,
38-
) => {
39-
if (!list.length) return list
40-
const order = new Map(list.map((url, index) => [url, index] as const))
41-
const rank = (value?: ServerHealth) => {
42-
if (value?.healthy === true) return 0
43-
if (value?.healthy === false) return 2
44-
return 1
45-
}
46-
47-
return list.slice().sort((a, b) => {
48-
if (ServerConnection.key(a) === active) return -1
49-
if (ServerConnection.key(b) === active) return 1
50-
const diff = rank(status[ServerConnection.key(a)]) - rank(status[ServerConnection.key(b)])
51-
if (diff !== 0) return diff
52-
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
53-
})
54-
}
55-
56-
const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, enabled: Accessor<boolean>) => {
57-
const checkServerHealth = useCheckServerHealth()
58-
const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
59-
60-
createEffect(() => {
61-
if (!enabled()) {
62-
setStatus(reconcile({}))
63-
return
64-
}
65-
const list = servers()
66-
let dead = false
67-
68-
const refresh = async () => {
69-
const results: Record<string, ServerHealth> = {}
70-
await Promise.all(
71-
list.map(async (conn) => {
72-
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http)
73-
}),
74-
)
75-
if (dead) return
76-
setStatus(reconcile(results))
77-
}
78-
79-
void refresh()
80-
const id = setInterval(() => void refresh(), pollMs)
81-
onCleanup(() => {
82-
dead = true
83-
clearInterval(id)
84-
})
85-
})
86-
87-
return status
88-
}
89-
90-
const useDefaultServerKey = (
91-
get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined,
92-
) => {
93-
const [state, setState] = createStore({
94-
url: undefined as string | undefined,
95-
tick: 0,
96-
})
97-
98-
createEffect(() => {
99-
state.tick
100-
let dead = false
101-
const result = get?.()
102-
if (!result) {
103-
setState("url", undefined)
104-
onCleanup(() => {
105-
dead = true
106-
})
107-
return
108-
}
109-
110-
if (result instanceof Promise) {
111-
void result.then((next) => {
112-
if (dead) return
113-
setState("url", next ? normalizeServerUrl(next) : undefined)
114-
})
115-
onCleanup(() => {
116-
dead = true
117-
})
118-
return
119-
}
120-
121-
setState("url", normalizeServerUrl(result))
122-
onCleanup(() => {
123-
dead = true
124-
})
125-
})
126-
127-
return {
128-
key: () => {
129-
const u = state.url
130-
if (!u) return
131-
return ServerConnection.key({ type: "http", http: { url: u } })
132-
},
133-
refresh: () => setState("tick", (value) => value + 1),
134-
}
135-
}
136-
13723
const useMcpToggleMutation = () => {
13824
const sync = useSync()
13925
const sdk = useSDK()
@@ -156,43 +42,10 @@ const useMcpToggleMutation = () => {
15642
}))
15743
}
15844

159-
export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
45+
export function StatusPopoverBody(_props: { shown: Accessor<boolean> }) {
16046
const sync = useSync()
161-
const server = useServer()
162-
const platform = usePlatform()
163-
const dialog = useDialog()
16447
const language = useLanguage()
165-
const navigate = useNavigate()
166-
167-
const fail = (err: unknown) => {
168-
showToast({
169-
variant: "error",
170-
title: language.t("common.requestFailed"),
171-
description: err instanceof Error ? err.message : String(err),
172-
})
173-
}
174-
175-
createEffect(() => {
176-
if (!props.shown()) return
177-
})
178-
179-
let dialogRun = 0
180-
let dialogDead = false
181-
onCleanup(() => {
182-
dialogDead = true
183-
dialogRun += 1
184-
})
185-
const servers = createMemo(() => {
186-
const current = server.current
187-
const list = server.list
188-
if (!current) return list
189-
if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list]
190-
return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))]
191-
})
192-
const health = useServerHealth(servers, props.shown)
193-
const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health))
19448
const toggleMcp = useMcpToggleMutation()
195-
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
19649
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
19750
const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status
19851
const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length)
@@ -210,15 +63,11 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
21063
aria-label={language.t("status.popover.ariaLabel")}
21164
class="tabs bg-background-strong rounded-xl overflow-hidden"
21265
data-component="tabs"
213-
data-active="servers"
214-
defaultValue="servers"
66+
data-active="mcp"
67+
defaultValue="mcp"
21568
variant="alt"
21669
>
21770
<Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
218-
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
219-
{sortedServers().length > 0 ? `${sortedServers().length} ` : ""}
220-
{language.t("status.popover.tab.servers")}
221-
</Tabs.Trigger>
22271
<Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
22372
{mcpConnected() > 0 ? `${mcpConnected()} ` : ""}
22473
{language.t("status.popover.tab.mcp")}
@@ -233,71 +82,6 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
23382
</Tabs.Trigger>
23483
</Tabs.List>
23584

236-
<Tabs.Content value="servers">
237-
<div class="flex flex-col px-2 pb-2">
238-
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
239-
<For each={sortedServers()}>
240-
{(s) => {
241-
const key = ServerConnection.key(s)
242-
const blocked = () => health[key]?.healthy === false
243-
return (
244-
<button
245-
type="button"
246-
class="flex items-center gap-2 w-full h-8 pl-3 pr-1.5 py-1.5 rounded-md transition-colors text-left"
247-
classList={{
248-
"hover:bg-surface-raised-base-hover": !blocked(),
249-
"cursor-not-allowed": blocked(),
250-
}}
251-
aria-disabled={blocked()}
252-
onClick={() => {
253-
if (blocked()) return
254-
navigate("/")
255-
queueMicrotask(() => server.setActive(key))
256-
}}
257-
>
258-
<ServerHealthIndicator health={health[key]} />
259-
<ServerRow
260-
conn={s}
261-
dimmed={blocked()}
262-
status={health[key]}
263-
class="flex items-center gap-2 w-full min-w-0"
264-
nameClass="text-14-regular text-text-base truncate"
265-
versionClass="text-12-regular text-text-weak truncate"
266-
badge={
267-
<Show when={key === defaultServer.key()}>
268-
<span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
269-
{language.t("common.default")}
270-
</span>
271-
</Show>
272-
}
273-
>
274-
<div class="flex-1" />
275-
<Show when={server.current && key === ServerConnection.key(server.current)}>
276-
<Icon name="check" size="small" class="text-icon-weak shrink-0" />
277-
</Show>
278-
</ServerRow>
279-
</button>
280-
)
281-
}}
282-
</For>
283-
284-
<Button
285-
variant="secondary"
286-
class="mt-3 self-start h-8 px-3 py-1.5"
287-
onClick={() => {
288-
const run = ++dialogRun
289-
void import("./dialog-select-server").then((x) => {
290-
if (dialogDead || dialogRun !== run) return
291-
dialog.show(() => <x.DialogSelectServer />, defaultServer.refresh)
292-
})
293-
}}
294-
>
295-
{language.t("status.popover.action.manageServers")}
296-
</Button>
297-
</div>
298-
</div>
299-
</Tabs.Content>
300-
30185
<Tabs.Content value="mcp">
30286
<div class="flex flex-col px-2 pb-2">
30387
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">

0 commit comments

Comments
 (0)