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"
41import { Switch } from "@opencode-ai/ui/switch"
52import { Tabs } from "@opencode-ai/ui/tabs"
63import { useMutation , useQueryClient } from "@tanstack/solid-query"
74import { 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"
126import { useLanguage } from "@/context/language"
13- import { usePlatform } from "@/context/platform"
147import { useSDK } from "@/context/sdk"
15- import { normalizeServerUrl , ServerConnection , useServer } from "@/context/server"
168import { useSync } from "@/context/sync"
17- import { useCheckServerHealth , type ServerHealth } from "@/utils/server-health"
189import { mcpQueryKey } from "@/context/global-sync"
1910
20- const pollMs = 10_000
21-
2211const 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-
13723const 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