feat(app): add servers tab to settings dialog (#29675)
This commit is contained in:
parent
c36a433a1f
commit
c6f684366a
30 changed files with 920 additions and 412 deletions
|
|
@ -277,6 +277,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||
<div class="text-14-regular text-text-base">{select()?.message}</div>
|
||||
<div>
|
||||
<List
|
||||
class="px-3"
|
||||
items={select()?.options ?? []}
|
||||
key={(x) => x.value}
|
||||
current={select()?.options.find((x) => x.value === formStore.value[select()!.key])}
|
||||
|
|
@ -364,6 +365,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||
</div>
|
||||
<div>
|
||||
<List
|
||||
class="px-3"
|
||||
ref={(ref) => {
|
||||
listRef = ref
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ export const DialogFork: Component = () => {
|
|||
return (
|
||||
<Dialog title={language.t("command.session.fork")}>
|
||||
<List
|
||||
class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
|
||||
class="flex-1 px-3 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
|
||||
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
|
||||
emptyMessage={language.t("dialog.fork.empty")}
|
||||
key={(x) => x.id}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export const DialogManageModels: Component = () => {
|
|||
}
|
||||
>
|
||||
<List
|
||||
class="px-3"
|
||||
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
|
||||
emptyMessage={language.t("dialog.model.empty")}
|
||||
key={(x) => `${x?.provider?.id}:${x?.id}`}
|
||||
|
|
|
|||
|
|
@ -6,15 +6,16 @@ import type { ListRef } from "@opencode-ai/ui/list"
|
|||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { createMemo, createResource, createSignal } from "solid-js"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { ServerSDK } from "@/context/server-sdk"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { ServerConnection } from "@/context/server"
|
||||
import { useGlobal } from "@/context/global"
|
||||
|
||||
interface DialogSelectDirectoryProps {
|
||||
title?: string
|
||||
multiple?: boolean
|
||||
onSelect: (result: string | string[] | null) => void
|
||||
server: ServerConnection.Any
|
||||
}
|
||||
|
||||
type Row = {
|
||||
|
|
@ -127,11 +128,7 @@ function uniqueRows(rows: Row[]) {
|
|||
})
|
||||
}
|
||||
|
||||
function useDirectorySearch(args: {
|
||||
sdk: ReturnType<typeof useServerSDK>
|
||||
start: () => string | undefined
|
||||
home: () => string
|
||||
}) {
|
||||
function useDirectorySearch(args: { sdk: ServerSDK; start: () => string | undefined; home: () => string }) {
|
||||
const cache = new Map<string, Promise<Array<{ name: string; absolute: string }>>>()
|
||||
let current = 0
|
||||
|
||||
|
|
@ -246,9 +243,8 @@ function useDirectorySearch(args: {
|
|||
}
|
||||
|
||||
export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
const sync = useServerSync()
|
||||
const sdk = useServerSDK()
|
||||
const layout = useLayout()
|
||||
const global = useGlobal()
|
||||
const { sync, sdk, ...serverCtx } = global.createServerCtx(props.server)
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
|
||||
|
|
@ -279,7 +275,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
|||
})
|
||||
|
||||
const recentProjects = createMemo(() => {
|
||||
const projects = layout.projects.list()
|
||||
const projects = serverCtx.projects.list()
|
||||
const byProject = new Map<string, number>()
|
||||
|
||||
for (const project of projects) {
|
||||
|
|
@ -324,6 +320,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
|||
return (
|
||||
<Dialog title={props.title ?? language.t("command.project.open")}>
|
||||
<List
|
||||
class="px-3"
|
||||
search={{ placeholder: language.t("dialog.directory.search.placeholder"), autofocus: true }}
|
||||
emptyMessage={language.t("dialog.directory.empty")}
|
||||
loadingMessage={language.t("common.loading")}
|
||||
|
|
|
|||
|
|
@ -386,6 +386,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||
return (
|
||||
<Dialog class="pt-3 pb-0 !max-h-[480px]" transition>
|
||||
<List
|
||||
class="px-3"
|
||||
search={{
|
||||
placeholder: filesOnly()
|
||||
? language.t("session.header.searchFiles")
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export const DialogSelectMcp: Component = () => {
|
|||
description={language.t("dialog.mcp.description", { enabled: enabledCount(), total: totalCount() })}
|
||||
>
|
||||
<List
|
||||
class="px-3"
|
||||
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
|
||||
emptyMessage={language.t("dialog.mcp.empty")}
|
||||
key={(x) => x?.name ?? ""}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
|
|||
<div class="flex flex-col gap-3 px-2.5" onKeyDown={handleKeyDown}>
|
||||
<div class="text-14-medium text-text-base px-2.5">{language.t("dialog.model.unpaid.freeModels.title")}</div>
|
||||
<List
|
||||
class="[&_[data-slot=list-scroll]]:overflow-visible"
|
||||
class="px-3 [&_[data-slot=list-scroll]]:overflow-visible"
|
||||
ref={(ref) => (listRef = ref)}
|
||||
items={model.list}
|
||||
current={model.current()}
|
||||
|
|
@ -90,7 +90,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
|
|||
<div class="px-2 text-14-medium text-text-base">{language.t("dialog.model.unpaid.addMore.title")}</div>
|
||||
<div class="w-full">
|
||||
<List
|
||||
class="w-full px-0"
|
||||
class="w-full px-3"
|
||||
key={(p) => p.id}
|
||||
items={providers.popular}
|
||||
activeIcon="plus-small"
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ const ModelList: Component<{
|
|||
|
||||
return (
|
||||
<List
|
||||
class={`flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 ${props.class ?? ""}`}
|
||||
class={`flex-1 px-3 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 ${props.class ?? ""}`}
|
||||
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true, action: props.action }}
|
||||
emptyMessage={language.t("dialog.model.empty")}
|
||||
key={(x) => `${x.provider.id}:${x.id}`}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export const DialogSelectProvider: Component = () => {
|
|||
return (
|
||||
<Dialog title={language.t("command.provider.connect")} transition>
|
||||
<List
|
||||
class="px-3"
|
||||
search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }}
|
||||
emptyMessage={language.t("dialog.provider.empty")}
|
||||
activeIcon="plus-small"
|
||||
|
|
|
|||
|
|
@ -9,13 +9,15 @@ import { TextField } from "@opencode-ai/ui/text-field"
|
|||
import { useMutation } from "@tanstack/solid-query"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { createEffect, createMemo, createResource, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
||||
import { useGlobal } from "@/context/global"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
||||
import { type ServerHealth, useCheckServerHealth } from "@/utils/server-health"
|
||||
import { useSettings } from "@/context/settings"
|
||||
|
||||
const DEFAULT_USERNAME = "opencode"
|
||||
|
||||
|
|
@ -71,7 +73,7 @@ function useDefaultServer() {
|
|||
}
|
||||
}
|
||||
|
||||
return { defaultKey, canDefault, setDefault }
|
||||
return { defaultKey: () => defaultKey.latest, canDefault, setDefault }
|
||||
}
|
||||
|
||||
function useServerPreview() {
|
||||
|
|
@ -121,7 +123,7 @@ function ServerForm(props: ServerFormProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div class="px-5">
|
||||
<div>
|
||||
<div class="bg-surface-base rounded-md p-5 flex flex-col gap-3">
|
||||
<div class="flex-1 min-w-0 [&_[data-slot=input-wrapper]]:relative">
|
||||
<TextField
|
||||
|
|
@ -172,16 +174,30 @@ function ServerForm(props: ServerFormProps) {
|
|||
}
|
||||
|
||||
export function DialogSelectServer() {
|
||||
const navigate = useNavigate()
|
||||
const dialog = useDialog()
|
||||
const controller = useServerManagementController({ onSelect: dialog.close })
|
||||
|
||||
return (
|
||||
<Dialog title={controller.formTitle()}>
|
||||
<div class="flex flex-1 min-h-0 flex-col px-5">
|
||||
<Show when={controller.isFormMode()} fallback={<ServerConnectionList controller={controller} />}>
|
||||
<ServerConnectionForm controller={controller} />
|
||||
</Show>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export function useServerManagementController(options: { onSelect?: () => void } = {}) {
|
||||
const navigate = useNavigate()
|
||||
const server = useServer()
|
||||
const global = useGlobal()
|
||||
const platform = usePlatform()
|
||||
const language = useLanguage()
|
||||
const { defaultKey, canDefault, setDefault } = useDefaultServer()
|
||||
const { previewStatus } = useServerPreview()
|
||||
const checkServerHealth = useCheckServerHealth()
|
||||
const [store, setStore] = createStore({
|
||||
status: {} as Record<ServerConnection.Key, ServerHealth | undefined>,
|
||||
addServer: {
|
||||
url: "",
|
||||
name: "",
|
||||
|
|
@ -311,7 +327,12 @@ export function DialogSelectServer() {
|
|||
return [current, ...list.filter((x) => x !== current)]
|
||||
})
|
||||
|
||||
const current = createMemo(() => items().find((x) => ServerConnection.key(x) === server.key) ?? items()[0])
|
||||
const settings = useSettings()
|
||||
const current = createMemo<ServerConnection.Any | undefined>(() =>
|
||||
settings.general.newLayoutDesigns()
|
||||
? undefined
|
||||
: (items().find((x) => ServerConnection.key(x) === server.key) ?? items()[0]),
|
||||
)
|
||||
|
||||
const sortedItems = createMemo(() => {
|
||||
const list = items()
|
||||
|
|
@ -326,32 +347,16 @@ export function DialogSelectServer() {
|
|||
return list.slice().sort((a, b) => {
|
||||
if (a === active) return -1
|
||||
if (b === active) return 1
|
||||
const diff = rank(store.status[ServerConnection.key(a)]) - rank(store.status[ServerConnection.key(b)])
|
||||
const diff =
|
||||
rank(global.servers.health[ServerConnection.key(a)]) - rank(global.servers.health[ServerConnection.key(b)])
|
||||
if (diff !== 0) return diff
|
||||
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
|
||||
})
|
||||
})
|
||||
|
||||
async function refreshHealth() {
|
||||
const results: Record<ServerConnection.Key, ServerHealth> = {}
|
||||
await Promise.all(
|
||||
items().map(async (conn) => {
|
||||
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http)
|
||||
}),
|
||||
)
|
||||
setStore("status", reconcile(results))
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
items()
|
||||
void refreshHealth()
|
||||
const interval = setInterval(refreshHealth, 10_000)
|
||||
onCleanup(() => clearInterval(interval))
|
||||
})
|
||||
|
||||
async function select(conn: ServerConnection.Any, persist?: boolean) {
|
||||
if (!persist && store.status[ServerConnection.key(conn)]?.healthy === false) return
|
||||
dialog.close()
|
||||
if (!persist && global.servers.health[ServerConnection.key(conn)]?.healthy === false) return
|
||||
options.onSelect?.()
|
||||
if (persist && conn.type === "http") {
|
||||
server.add(conn)
|
||||
navigate("/")
|
||||
|
|
@ -457,7 +462,7 @@ export function DialogSelectServer() {
|
|||
username: conn.http.username ?? "",
|
||||
password: conn.http.password ?? "",
|
||||
error: "",
|
||||
status: store.status[ServerConnection.key(conn)]?.healthy,
|
||||
status: global.servers.health[ServerConnection.key(conn)]?.healthy,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -502,148 +507,183 @@ export function DialogSelectServer() {
|
|||
}
|
||||
}
|
||||
|
||||
return {
|
||||
defaultKey,
|
||||
canDefault,
|
||||
current,
|
||||
sortedItems,
|
||||
status: () => global.servers.health,
|
||||
isFormMode,
|
||||
isAddMode,
|
||||
formTitle,
|
||||
formBusy,
|
||||
formValue: () => (isAddMode() ? store.addServer.url : store.editServer.value),
|
||||
formName: () => (isAddMode() ? store.addServer.name : store.editServer.name),
|
||||
formUsername: () => (isAddMode() ? store.addServer.username : store.editServer.username),
|
||||
formPassword: () => (isAddMode() ? store.addServer.password : store.editServer.password),
|
||||
formError: () => (isAddMode() ? store.addServer.error : store.editServer.error),
|
||||
formStatus: () => (isAddMode() ? store.addServer.status : store.editServer.status),
|
||||
select,
|
||||
setDefault,
|
||||
startAdd,
|
||||
startEdit,
|
||||
resetForm,
|
||||
submitForm,
|
||||
handleRemove,
|
||||
handleFormChange: () => (isAddMode() ? handleAddChange : handleEditChange),
|
||||
handleFormNameChange: () => (isAddMode() ? handleAddNameChange : handleEditNameChange),
|
||||
handleFormUsernameChange: () => (isAddMode() ? handleAddUsernameChange : handleEditUsernameChange),
|
||||
handleFormPasswordChange: () => (isAddMode() ? handleAddPasswordChange : handleEditPasswordChange),
|
||||
}
|
||||
}
|
||||
|
||||
export function ServerConnectionList(props: { controller: ReturnType<typeof useServerManagementController> }) {
|
||||
const language = useLanguage()
|
||||
const settings = useSettings()
|
||||
|
||||
return (
|
||||
<Dialog title={formTitle()}>
|
||||
<div class="flex flex-1 min-h-0 flex-col gap-2">
|
||||
<Show
|
||||
when={!isFormMode()}
|
||||
fallback={
|
||||
<ServerForm
|
||||
value={isAddMode() ? store.addServer.url : store.editServer.value}
|
||||
name={isAddMode() ? store.addServer.name : store.editServer.name}
|
||||
username={isAddMode() ? store.addServer.username : store.editServer.username}
|
||||
password={isAddMode() ? store.addServer.password : store.editServer.password}
|
||||
placeholder={language.t("dialog.server.add.placeholder")}
|
||||
busy={formBusy()}
|
||||
error={isAddMode() ? store.addServer.error : store.editServer.error}
|
||||
status={isAddMode() ? store.addServer.status : store.editServer.status}
|
||||
onChange={isAddMode() ? handleAddChange : handleEditChange}
|
||||
onNameChange={isAddMode() ? handleAddNameChange : handleEditNameChange}
|
||||
onUsernameChange={isAddMode() ? handleAddUsernameChange : handleEditUsernameChange}
|
||||
onPasswordChange={isAddMode() ? handleAddPasswordChange : handleEditPasswordChange}
|
||||
onSubmit={submitForm}
|
||||
onBack={resetForm}
|
||||
/>
|
||||
}
|
||||
<div class="flex flex-1 min-h-0 flex-col gap-4">
|
||||
<List
|
||||
class="flex-1 min-h-0 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
||||
search={{
|
||||
placeholder: language.t("dialog.server.search.placeholder"),
|
||||
autofocus: false,
|
||||
}}
|
||||
noInitialSelection
|
||||
emptyMessage={language.t("dialog.server.empty")}
|
||||
items={props.controller.sortedItems}
|
||||
key={(x) => x.http.url}
|
||||
onSelect={(x) => {
|
||||
if (x && !settings.general.newLayoutDesigns()) void props.controller.select(x)
|
||||
}}
|
||||
divider={true}
|
||||
>
|
||||
{(i) => {
|
||||
const key = ServerConnection.key(i)
|
||||
return (
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1 w-full group/item">
|
||||
<div class="flex flex-col h-full items-center w-5">
|
||||
<ServerHealthIndicator health={props.controller.status()[key]} />
|
||||
</div>
|
||||
<ServerRow
|
||||
conn={i}
|
||||
dimmed={props.controller.status()[key]?.healthy === false}
|
||||
status={props.controller.status()[key]}
|
||||
class="flex items-center gap-3 min-w-0 flex-1"
|
||||
badge={
|
||||
<Show when={props.controller.defaultKey() === ServerConnection.key(i)}>
|
||||
<span class="text-text-base bg-surface-base text-14-regular px-1.5 rounded-xs">
|
||||
{language.t("dialog.server.status.default")}
|
||||
</span>
|
||||
</Show>
|
||||
}
|
||||
showCredentials
|
||||
/>
|
||||
<div class="flex items-center justify-center gap-4 pl-4">
|
||||
<Show when={props.controller.current() && ServerConnection.key(props.controller.current()!) === key}>
|
||||
<Icon name="check" class="h-6" />
|
||||
</Show>
|
||||
|
||||
<Show when={i.type === "http"}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
|
||||
onClick={(e: MouseEvent) => e.stopPropagation()}
|
||||
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="mt-1">
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
if (i.type !== "http") return
|
||||
props.controller.startEdit(i)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<Show when={props.controller.canDefault() && props.controller.defaultKey() !== key}>
|
||||
<DropdownMenu.Item onSelect={() => props.controller.setDefault(key)}>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.default")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<Show when={props.controller.canDefault() && props.controller.defaultKey() === key}>
|
||||
<DropdownMenu.Item onSelect={() => props.controller.setDefault(null)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.defaultRemove")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => props.controller.handleRemove(ServerConnection.key(i))}
|
||||
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</List>
|
||||
|
||||
<div class="shrink-0 pb-5">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="plus-small"
|
||||
size="large"
|
||||
onClick={props.controller.startAdd}
|
||||
class="py-1.5 pl-1.5 pr-3 flex items-center gap-1.5"
|
||||
>
|
||||
<List
|
||||
search={{
|
||||
placeholder: language.t("dialog.server.search.placeholder"),
|
||||
autofocus: false,
|
||||
}}
|
||||
noInitialSelection
|
||||
emptyMessage={language.t("dialog.server.empty")}
|
||||
items={sortedItems}
|
||||
key={(x) => x.http.url}
|
||||
onSelect={(x) => {
|
||||
if (x) void select(x)
|
||||
}}
|
||||
divider={true}
|
||||
class="flex-1 min-h-0 px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
||||
>
|
||||
{(i) => {
|
||||
const key = ServerConnection.key(i)
|
||||
return (
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1 w-full group/item">
|
||||
<div class="flex flex-col h-full items-start w-5">
|
||||
<ServerHealthIndicator health={store.status[key]} />
|
||||
</div>
|
||||
<ServerRow
|
||||
conn={i}
|
||||
dimmed={store.status[key]?.healthy === false}
|
||||
status={store.status[key]}
|
||||
class="flex items-center gap-3 min-w-0 flex-1"
|
||||
badge={
|
||||
<Show when={defaultKey() === ServerConnection.key(i)}>
|
||||
<span class="text-text-base bg-surface-base text-14-regular px-1.5 rounded-xs">
|
||||
{language.t("dialog.server.status.default")}
|
||||
</span>
|
||||
</Show>
|
||||
}
|
||||
showCredentials
|
||||
/>
|
||||
<div class="flex items-center justify-center gap-4 pl-4">
|
||||
<Show when={ServerConnection.key(current()) === key}>
|
||||
<Icon name="check" class="h-6" />
|
||||
</Show>
|
||||
|
||||
<Show when={i.type === "http"}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
|
||||
onClick={(e: MouseEvent) => e.stopPropagation()}
|
||||
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="mt-1">
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
if (i.type !== "http") return
|
||||
startEdit(i)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<Show when={canDefault() && defaultKey() !== key}>
|
||||
<DropdownMenu.Item onSelect={() => setDefault(key)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.default")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<Show when={canDefault() && defaultKey() === key}>
|
||||
<DropdownMenu.Item onSelect={() => setDefault(null)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.defaultRemove")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => handleRemove(ServerConnection.key(i))}
|
||||
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</List>
|
||||
</Show>
|
||||
|
||||
<div class="shrink-0 px-5 pb-5">
|
||||
<Show
|
||||
when={isFormMode()}
|
||||
fallback={
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="plus-small"
|
||||
size="large"
|
||||
onClick={startAdd}
|
||||
class="py-1.5 pl-1.5 pr-3 flex items-center gap-1.5"
|
||||
>
|
||||
{language.t("dialog.server.add.button")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Button variant="primary" size="large" onClick={submitForm} disabled={formBusy()} class="px-3 py-1.5">
|
||||
{formBusy()
|
||||
? language.t("dialog.server.add.checking")
|
||||
: isAddMode()
|
||||
? language.t("dialog.server.add.button")
|
||||
: language.t("common.save")}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
{language.t("dialog.server.add.button")}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ServerConnectionForm(props: { controller: ReturnType<typeof useServerManagementController> }) {
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<div class="flex flex-1 min-h-0 flex-col gap-4">
|
||||
<ServerForm
|
||||
value={props.controller.formValue()}
|
||||
name={props.controller.formName()}
|
||||
username={props.controller.formUsername()}
|
||||
password={props.controller.formPassword()}
|
||||
placeholder={language.t("dialog.server.add.placeholder")}
|
||||
busy={props.controller.formBusy()}
|
||||
error={props.controller.formError()}
|
||||
status={props.controller.formStatus()}
|
||||
onChange={props.controller.handleFormChange()}
|
||||
onNameChange={props.controller.handleFormNameChange()}
|
||||
onUsernameChange={props.controller.handleFormUsernameChange()}
|
||||
onPasswordChange={props.controller.handleFormPasswordChange()}
|
||||
onSubmit={props.controller.submitForm}
|
||||
onBack={props.controller.resetForm}
|
||||
/>
|
||||
<div class="shrink-0 pb-5">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
onClick={props.controller.submitForm}
|
||||
disabled={props.controller.formBusy()}
|
||||
class="px-3 py-1.5"
|
||||
>
|
||||
{props.controller.formBusy()
|
||||
? language.t("dialog.server.add.checking")
|
||||
: props.controller.isAddMode()
|
||||
? language.t("dialog.server.add.button")
|
||||
: language.t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { SettingsGeneral } from "./settings-general"
|
|||
import { SettingsKeybinds } from "./settings-keybinds"
|
||||
import { SettingsProviders } from "./settings-providers"
|
||||
import { SettingsModels } from "./settings-models"
|
||||
import { SettingsServers } from "./settings-servers"
|
||||
|
||||
export const DialogSettings: Component = () => {
|
||||
const language = useLanguage()
|
||||
|
|
@ -17,7 +18,7 @@ export const DialogSettings: Component = () => {
|
|||
<Dialog size="x-large" transition>
|
||||
<Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
|
||||
<Tabs.List>
|
||||
<div class="flex flex-col justify-between h-full w-full">
|
||||
<div class="flex flex-col justify-between h-full w-full gap-4">
|
||||
<div class="flex flex-col gap-3 w-full pt-3">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
|
|
@ -31,6 +32,10 @@ export const DialogSettings: Component = () => {
|
|||
<Icon name="keyboard" />
|
||||
{language.t("settings.tab.shortcuts")}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="servers">
|
||||
<Icon name="server" />
|
||||
{language.t("status.popover.tab.servers")}
|
||||
</Tabs.Trigger>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -61,6 +66,9 @@ export const DialogSettings: Component = () => {
|
|||
<Tabs.Content value="shortcuts" class="no-scrollbar">
|
||||
<SettingsKeybinds />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="servers" class="no-scrollbar">
|
||||
<SettingsServers />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="providers" class="no-scrollbar">
|
||||
<SettingsProviders />
|
||||
</Tabs.Content>
|
||||
|
|
|
|||
|
|
@ -1363,6 +1363,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
navigate(`/${base64Encode(worktree)}/session`)
|
||||
}
|
||||
const addProject = async () => {
|
||||
const conn = server.current
|
||||
if (!conn) return
|
||||
const select = (result: string | string[] | null) => {
|
||||
const directory = Array.isArray(result) ? result[0] : result
|
||||
if (!directory) return
|
||||
|
|
@ -1374,7 +1376,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
}
|
||||
void import("@/components/dialog-select-directory").then((x) => {
|
||||
dialog.show(
|
||||
() => <x.DialogSelectDirectory onSelect={select} />,
|
||||
() => <x.DialogSelectDirectory onSelect={select} server={conn} />,
|
||||
() => select(null),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout"
|
|||
|
||||
export function NewSessionDesignView(props: { children: JSX.Element }) {
|
||||
return (
|
||||
<div data-component="session-new-design" class="relative size-full overflow-hidden bg-v2-background-bg-deep">
|
||||
<div data-component="session-new-design" class="relative size-full overflow-hidden bg-v2-background-bg-deep ">
|
||||
<div class="absolute inset-x-0 top-[25.375%] flex justify-center px-6">
|
||||
<div class={NEW_SESSION_CONTENT_WIDTH}>
|
||||
<WordmarkV2 class="h-auto w-full text-v2-icon-icon-base" />
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useLanguage } from "@/context/language"
|
|||
import { useModels } from "@/context/models"
|
||||
import { popularProviders } from "@/hooks/use-providers"
|
||||
import { SettingsList } from "./settings-list"
|
||||
import { SettingsServerPicker, SettingsServerScope } from "./settings-server-picker"
|
||||
|
||||
type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
|
||||
|
||||
|
|
@ -32,6 +33,14 @@ const ListEmptyState: Component<{ message: string; filter: string }> = (props) =
|
|||
}
|
||||
|
||||
export const SettingsModels: Component = () => {
|
||||
return (
|
||||
<SettingsServerScope>
|
||||
<SettingsModelsContent />
|
||||
</SettingsServerScope>
|
||||
)
|
||||
}
|
||||
|
||||
const SettingsModelsContent: Component = () => {
|
||||
const language = useLanguage()
|
||||
const models = useModels()
|
||||
|
||||
|
|
@ -61,7 +70,10 @@ export const SettingsModels: Component = () => {
|
|||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
|
||||
<SettingsServerPicker />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
|
||||
<Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
|
||||
<TextField
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { DialogConnectProvider } from "./dialog-connect-provider"
|
|||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
import { DialogCustomProvider } from "./dialog-custom-provider"
|
||||
import { SettingsList } from "./settings-list"
|
||||
import { SettingsServerPicker, SettingsServerScope } from "./settings-server-picker"
|
||||
|
||||
type ProviderSource = "env" | "api" | "config" | "custom"
|
||||
type ProviderItem = ReturnType<ReturnType<typeof useProviders>["connected"]>[number]
|
||||
|
|
@ -28,6 +29,14 @@ const PROVIDER_NOTES = [
|
|||
] as const
|
||||
|
||||
export const SettingsProviders: Component = () => {
|
||||
return (
|
||||
<SettingsServerScope>
|
||||
<SettingsProvidersContent />
|
||||
</SettingsServerScope>
|
||||
)
|
||||
}
|
||||
|
||||
const SettingsProvidersContent: Component = () => {
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const serverSDK = useServerSDK()
|
||||
|
|
@ -129,8 +138,9 @@ export const SettingsProviders: Component = () => {
|
|||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||
<div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
|
||||
<div class="flex items-center justify-between gap-4 pt-6 pb-8 max-w-[720px]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
|
||||
<SettingsServerPicker />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
106
packages/app/src/components/settings-server-picker.tsx
Normal file
106
packages/app/src/components/settings-server-picker.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { QueryClientProvider } from "@tanstack/solid-query"
|
||||
import { createMemo, For, type ParentProps, Show } from "solid-js"
|
||||
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
||||
import { ModelsProvider } from "@/context/models"
|
||||
import { ServerConnection } from "@/context/server"
|
||||
import { ServerSDKProvider } from "@/context/server-sdk"
|
||||
import { ServerSyncProvider } from "@/context/server-sync"
|
||||
import { useGlobal } from "@/context/global"
|
||||
import { useSettings } from "@/context/settings"
|
||||
|
||||
export function SettingsServerScope(props: ParentProps) {
|
||||
const global = useGlobal()
|
||||
const settings = useSettings()
|
||||
|
||||
return (
|
||||
<Show when={settings.general.newLayoutDesigns()} fallback={props.children}>
|
||||
<Show when={global.settings.server.selected()}>
|
||||
{(server) => <SettingsServerDataProviders server={server()}>{props.children}</SettingsServerDataProviders>}
|
||||
</Show>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsServerDataProviders(props: ParentProps<{ server: ServerConnection.Any }>) {
|
||||
const global = useGlobal()
|
||||
const serverCtx = () => global.createServerCtx(props.server)
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={serverCtx().queryClient}>
|
||||
<ServerSDKProvider server={props.server}>
|
||||
<ServerSyncProvider>
|
||||
<ModelsProvider>{props.children}</ModelsProvider>
|
||||
</ServerSyncProvider>
|
||||
</ServerSDKProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export function SettingsServerPicker() {
|
||||
const global = useGlobal()
|
||||
const settings = useSettings()
|
||||
const selected = createMemo(() =>
|
||||
settings.general.newLayoutDesigns() ? global.settings.server.selected() : undefined,
|
||||
)
|
||||
|
||||
return (
|
||||
<Show when={selected()}>
|
||||
{(conn) => (
|
||||
<DropdownMenu gutter={4} placement="bottom-end">
|
||||
<DropdownMenu.Trigger
|
||||
as={Button}
|
||||
variant="secondary"
|
||||
size="large"
|
||||
class="h-8 max-w-[260px] gap-2 px-2 py-1.5 data-[expanded]:bg-surface-base-active"
|
||||
>
|
||||
<ServerHealthIndicator health={global.servers.health[ServerConnection.key(conn())]} />
|
||||
<ServerRow
|
||||
conn={conn()}
|
||||
status={global.servers.health[ServerConnection.key(conn())]}
|
||||
class="flex items-center gap-2 min-w-0 flex-1"
|
||||
nameClass="text-14-regular text-text-base truncate"
|
||||
versionClass="hidden"
|
||||
/>
|
||||
<Icon name="chevron-down" size="small" class="text-icon-weak shrink-0" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="w-[320px] mt-1 [&_[data-slot=dropdown-menu-radio-item]]:pl-2 [&_[data-slot=dropdown-menu-radio-item]]:pr-2">
|
||||
<DropdownMenu.RadioGroup
|
||||
value={global.settings.server.key}
|
||||
onChange={(key) => {
|
||||
if (typeof key === "string") global.settings.server.set(ServerConnection.Key.make(key))
|
||||
}}
|
||||
>
|
||||
<For each={global.servers.list()}>
|
||||
{(item) => {
|
||||
const key = ServerConnection.key(item)
|
||||
const blocked = () => global.servers.health[key]?.healthy === false
|
||||
return (
|
||||
<DropdownMenu.RadioItem value={key} disabled={blocked()}>
|
||||
<ServerHealthIndicator health={global.servers.health[key]} />
|
||||
<ServerRow
|
||||
conn={item}
|
||||
dimmed={blocked()}
|
||||
status={global.servers.health[key]}
|
||||
class="flex items-center gap-2 min-w-0 flex-1"
|
||||
nameClass="text-14-regular text-text-base truncate"
|
||||
versionClass="text-12-regular text-text-weak truncate"
|
||||
/>
|
||||
<DropdownMenu.ItemIndicator>
|
||||
<Icon name="check-small" size="small" class="text-icon-weak" />
|
||||
</DropdownMenu.ItemIndicator>
|
||||
</DropdownMenu.RadioItem>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</DropdownMenu.RadioGroup>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
33
packages/app/src/components/settings-servers.tsx
Normal file
33
packages/app/src/components/settings-servers.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { Show, type Component } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { ServerConnectionForm, ServerConnectionList, useServerManagementController } from "./dialog-select-server"
|
||||
|
||||
export const SettingsServers: Component = () => {
|
||||
const language = useLanguage()
|
||||
const controller = useServerManagementController()
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||
<div class="flex flex-col flex-1 min-h-0 max-w-[720px]">
|
||||
<Show
|
||||
when={controller.isFormMode()}
|
||||
fallback={
|
||||
<>
|
||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||
<div class="flex flex-col gap-1 pt-6 pb-8">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("status.popover.tab.servers")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<ServerConnectionList controller={controller} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div class="flex flex-1 min-h-0 flex-col gap-4 pt-6">
|
||||
<div class="text-16-medium text-text-strong">{controller.formTitle()}</div>
|
||||
<ServerConnectionForm controller={controller} />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import { SettingsKeybinds } from "../settings-keybinds"
|
|||
import { SettingsProvidersV2 } from "./providers"
|
||||
import { SettingsModelsV2 } from "./models"
|
||||
import "./settings-v2.css"
|
||||
import { SettingsServers } from "../settings-servers"
|
||||
|
||||
export const DialogSettings: Component = () => {
|
||||
const language = useLanguage()
|
||||
|
|
@ -38,6 +39,10 @@ export const DialogSettings: Component = () => {
|
|||
<Icon name="keyboard" />
|
||||
{language.t("settings.tab.shortcuts")}
|
||||
</TabsV2.Trigger>
|
||||
<TabsV2.Trigger value="servers">
|
||||
<Icon name="server" />
|
||||
{language.t("status.popover.tab.servers")}
|
||||
</TabsV2.Trigger>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -68,6 +73,9 @@ export const DialogSettings: Component = () => {
|
|||
<TabsV2.Content value="shortcuts" class="settings-v2-panel">
|
||||
<SettingsKeybinds v2 />
|
||||
</TabsV2.Content>
|
||||
<TabsV2.Content value="servers" class="settings-v2-panel">
|
||||
<SettingsServers />
|
||||
</TabsV2.Content>
|
||||
<TabsV2.Content value="providers" class="settings-v2-panel">
|
||||
<SettingsProvidersV2 />
|
||||
</TabsV2.Content>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ import { useSync } from "@/context/sync"
|
|||
import { type ServerHealth } from "@/utils/server-health"
|
||||
import { useQueryOptions } from "@/context/server-sync"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
import { useServers } from "@/context/servers"
|
||||
import { useGlobal } from "@/context/global"
|
||||
import { useSettings } from "@/context/settings"
|
||||
|
||||
const pollMs = 10_000
|
||||
|
||||
|
|
@ -153,7 +154,7 @@ type ServerStatusItem = {
|
|||
}
|
||||
|
||||
export function StatusPopoverServerBody() {
|
||||
const servers = useServers()
|
||||
const global = useGlobal()
|
||||
const server = useServer()
|
||||
const platform = usePlatform()
|
||||
const dialog = useDialog()
|
||||
|
|
@ -167,7 +168,7 @@ export function StatusPopoverServerBody() {
|
|||
dialogRun += 1
|
||||
})
|
||||
|
||||
const sortedServers = createMemo(() => listServersByHealth(servers.list(), server.key, servers.health))
|
||||
const sortedServers = createMemo(() => listServersByHealth(global.servers.list(), server.key, global.servers.health))
|
||||
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
|
||||
const serverItems = createMemo(() =>
|
||||
sortedServers().map((conn) => {
|
||||
|
|
@ -175,8 +176,8 @@ export function StatusPopoverServerBody() {
|
|||
return {
|
||||
key,
|
||||
conn,
|
||||
health: servers.health[key],
|
||||
blocked: servers.health[key]?.healthy === false,
|
||||
health: global.servers.health[key],
|
||||
blocked: global.servers.health[key]?.healthy === false,
|
||||
active: !!server.current && key === ServerConnection.key(server.current),
|
||||
onSelect: () => {
|
||||
navigate("/")
|
||||
|
|
@ -288,12 +289,13 @@ function ServerStatusList(props: { state: ServerStatusState }) {
|
|||
|
||||
export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||
const sync = useSync()
|
||||
const servers = useServers()
|
||||
const global = useGlobal()
|
||||
const server = useServer()
|
||||
const platform = usePlatform()
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const navigate = useNavigate()
|
||||
const settings = useSettings()
|
||||
|
||||
const fail = (err: unknown) => {
|
||||
showToast({
|
||||
|
|
@ -313,7 +315,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
|||
dialogDead = true
|
||||
dialogRun += 1
|
||||
})
|
||||
const sortedServers = createMemo(() => listServersByHealth(servers.list(), server.key, servers.health))
|
||||
const sortedServers = createMemo(() => listServersByHealth(global.servers.list(), server.key, global.servers.health))
|
||||
const toggleMcp = useMcpToggleMutation()
|
||||
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
|
||||
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
|
||||
|
|
@ -333,15 +335,17 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
|||
aria-label={language.t("status.popover.ariaLabel")}
|
||||
class="tabs bg-background-strong rounded-xl overflow-hidden"
|
||||
data-component="tabs"
|
||||
data-active="servers"
|
||||
defaultValue="servers"
|
||||
data-active={settings.general.newLayoutDesigns() ? "mcp" : "servers"}
|
||||
defaultValue={settings.general.newLayoutDesigns() ? "mcp" : "servers"}
|
||||
variant="alt"
|
||||
>
|
||||
<Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
|
||||
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
|
||||
{servers.list().length > 0 ? `${servers.list().length} ` : ""}
|
||||
{language.t("status.popover.tab.servers")}
|
||||
</Tabs.Trigger>
|
||||
{!settings.general.newLayoutDesigns() && (
|
||||
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
|
||||
{global.servers.list().length > 0 ? `${global.servers.list().length} ` : ""}
|
||||
{language.t("status.popover.tab.servers")}
|
||||
</Tabs.Trigger>
|
||||
)}
|
||||
<Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
|
||||
{mcpConnected() > 0 ? `${mcpConnected()} ` : ""}
|
||||
{language.t("status.popover.tab.mcp")}
|
||||
|
|
@ -356,70 +360,72 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
|||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content value="servers">
|
||||
<div class="flex flex-col px-2 pb-2">
|
||||
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
|
||||
<For each={sortedServers()}>
|
||||
{(s) => {
|
||||
const key = ServerConnection.key(s)
|
||||
const blocked = () => servers.health[key]?.healthy === false
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 w-full h-8 pl-3 pr-1.5 py-1.5 rounded-md transition-colors text-left"
|
||||
classList={{
|
||||
"hover:bg-surface-raised-base-hover": !blocked(),
|
||||
"cursor-not-allowed": blocked(),
|
||||
}}
|
||||
aria-disabled={blocked()}
|
||||
onClick={() => {
|
||||
if (blocked()) return
|
||||
navigate("/")
|
||||
queueMicrotask(() => server.setActive(key))
|
||||
}}
|
||||
>
|
||||
<ServerHealthIndicator health={servers.health[key]} />
|
||||
<ServerRow
|
||||
conn={s}
|
||||
dimmed={blocked()}
|
||||
status={servers.health[key]}
|
||||
class="flex items-center gap-2 w-full min-w-0"
|
||||
nameClass="text-14-regular text-text-base truncate"
|
||||
versionClass="text-12-regular text-text-weak truncate"
|
||||
badge={
|
||||
<Show when={key === defaultServer.key()}>
|
||||
<span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
|
||||
{language.t("common.default")}
|
||||
</span>
|
||||
</Show>
|
||||
}
|
||||
{!settings.general.newLayoutDesigns() && (
|
||||
<Tabs.Content value="servers">
|
||||
<div class="flex flex-col px-2 pb-2">
|
||||
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
|
||||
<For each={sortedServers()}>
|
||||
{(s) => {
|
||||
const key = ServerConnection.key(s)
|
||||
const blocked = () => global.servers.health[key]?.healthy === false
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 w-full h-8 pl-3 pr-1.5 py-1.5 rounded-md transition-colors text-left"
|
||||
classList={{
|
||||
"hover:bg-surface-raised-base-hover": !blocked(),
|
||||
"cursor-not-allowed": blocked(),
|
||||
}}
|
||||
aria-disabled={blocked()}
|
||||
onClick={() => {
|
||||
if (blocked()) return
|
||||
navigate("/")
|
||||
queueMicrotask(() => server.setActive(key))
|
||||
}}
|
||||
>
|
||||
<div class="flex-1" />
|
||||
<Show when={server.current && key === ServerConnection.key(server.current)}>
|
||||
<Icon name="check" size="small" class="text-icon-weak shrink-0" />
|
||||
</Show>
|
||||
</ServerRow>
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<ServerHealthIndicator health={global.servers.health[key]} />
|
||||
<ServerRow
|
||||
conn={s}
|
||||
dimmed={blocked()}
|
||||
status={global.servers.health[key]}
|
||||
class="flex items-center gap-2 w-full min-w-0"
|
||||
nameClass="text-14-regular text-text-base truncate"
|
||||
versionClass="text-12-regular text-text-weak truncate"
|
||||
badge={
|
||||
<Show when={key === defaultServer.key()}>
|
||||
<span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
|
||||
{language.t("common.default")}
|
||||
</span>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<div class="flex-1" />
|
||||
<Show when={server.current && key === ServerConnection.key(server.current)}>
|
||||
<Icon name="check" size="small" class="text-icon-weak shrink-0" />
|
||||
</Show>
|
||||
</ServerRow>
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="mt-3 self-start h-8 px-3 py-1.5"
|
||||
onClick={() => {
|
||||
const run = ++dialogRun
|
||||
void import("./dialog-select-server").then((x) => {
|
||||
if (dialogDead || dialogRun !== run) return
|
||||
dialog.show(() => <x.DialogSelectServer />, defaultServer.refresh)
|
||||
})
|
||||
}}
|
||||
>
|
||||
{language.t("status.popover.action.manageServers")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="mt-3 self-start h-8 px-3 py-1.5"
|
||||
onClick={() => {
|
||||
const run = ++dialogRun
|
||||
void import("./dialog-select-server").then((x) => {
|
||||
if (dialogDead || dialogRun !== run) return
|
||||
dialog.show(() => <x.DialogSelectServer />, defaultServer.refresh)
|
||||
})
|
||||
}}
|
||||
>
|
||||
{language.t("status.popover.action.manageServers")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Content>
|
||||
)}
|
||||
|
||||
<Tabs.Content value="mcp">
|
||||
<div class="flex flex-col px-2 pb-2">
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Suspense, createMemo, createSignal, lazy, Show, type JSX } from "solid-
|
|||
import { useLanguage } from "@/context/language"
|
||||
import { useServer } from "@/context/server"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useServers } from "@/context/servers"
|
||||
import { useGlobal } from "@/context/global"
|
||||
|
||||
const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody })))
|
||||
const ServerBody = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverServerBody })))
|
||||
|
|
@ -15,10 +15,10 @@ const ServerBody = lazy(() => import("./status-popover-body").then((x) => ({ def
|
|||
export function StatusPopover() {
|
||||
const language = useLanguage()
|
||||
const server = useServer()
|
||||
const servers = useServers()
|
||||
const global = useGlobal()
|
||||
const sync = useSync()
|
||||
const [shown, setShown] = createSignal(false)
|
||||
const ready = createMemo(() => servers.health[server.key]?.healthy === false || sync.data.mcp_ready)
|
||||
const ready = createMemo(() => global.servers.health[server.key]?.healthy === false || sync.data.mcp_ready)
|
||||
const mcpIssue = createMemo(() => {
|
||||
const mcp = Object.values(sync.data.mcp ?? {})
|
||||
const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
|
||||
|
|
@ -26,8 +26,8 @@ export function StatusPopover() {
|
|||
if (failed) return "critical" as const
|
||||
if (warn) return "warning" as const
|
||||
})
|
||||
const serverHealthy = () => servers.health[server.key]?.healthy === true
|
||||
const healthy = createMemo(() => servers.health[server.key]?.healthy === true && !mcpIssue())
|
||||
const serverHealthy = () => global.servers.health[server.key]?.healthy === true
|
||||
const healthy = createMemo(() => global.servers.health[server.key]?.healthy === true && !mcpIssue())
|
||||
|
||||
return (
|
||||
<Popover
|
||||
|
|
@ -82,10 +82,10 @@ export function StatusPopoverV2(props: { scope?: "server" }) {
|
|||
function DirectoryStatusPopover() {
|
||||
const language = useLanguage()
|
||||
const server = useServer()
|
||||
const servers = useServers()
|
||||
const global = useGlobal()
|
||||
const sync = useSync()
|
||||
const [shown, setShown] = createSignal(false)
|
||||
const serverHealth = () => servers.health[server.key]?.healthy
|
||||
const serverHealth = () => global.servers.health[server.key]?.healthy
|
||||
const ready = createMemo(() => serverHealth() === false || sync.data.mcp_ready)
|
||||
const mcpIssue = createMemo(() => {
|
||||
const mcp = Object.values(sync.data.mcp ?? {})
|
||||
|
|
@ -116,9 +116,9 @@ function DirectoryStatusPopover() {
|
|||
function ServerStatusPopover() {
|
||||
const language = useLanguage()
|
||||
const server = useServer()
|
||||
const servers = useServers()
|
||||
const global = useGlobal()
|
||||
const [shown, setShown] = createSignal(false)
|
||||
const serverHealth = () => servers.health[server.key]?.healthy
|
||||
const serverHealth = () => global.servers.health[server.key]?.healthy
|
||||
const state = createMemo<StatusPopoverState>(() => ({
|
||||
shown: shown(),
|
||||
ready: serverHealth() !== undefined,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue