|
|
@@ -11,7 +11,8 @@ import { usePlatform } from "@/context/platform"
|
|
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
|
|
import { useNavigate } from "@solidjs/router"
|
|
|
import { useLanguage } from "@/context/language"
|
|
|
-import { Popover } from "@opencode-ai/ui/popover"
|
|
|
+import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
|
|
+import { Tooltip } from "@opencode-ai/ui/tooltip"
|
|
|
import { useGlobalSDK } from "@/context/global-sdk"
|
|
|
|
|
|
type ServerStatus = { healthy: boolean; version?: string }
|
|
|
@@ -52,16 +53,27 @@ async function checkHealth(url: string, platform: ReturnType<typeof usePlatform>
|
|
|
|
|
|
function AddRow(props: AddRowProps) {
|
|
|
return (
|
|
|
- <div class="flex items-center gap-3 px-4 min-w-0 flex-1">
|
|
|
- <div
|
|
|
- classList={{
|
|
|
- "size-1.5 rounded-full shrink-0": true,
|
|
|
- "bg-icon-success-base": props.status === true,
|
|
|
- "bg-icon-critical-base": props.status === false,
|
|
|
- "bg-border-weak-base": props.status === undefined,
|
|
|
- }}
|
|
|
- />
|
|
|
- <div class="flex-1 min-w-0">
|
|
|
+ <div class="flex items-center px-4 min-h-14 py-3 min-w-0 flex-1">
|
|
|
+ <div class="flex-1 min-w-0 [&_[data-slot=input-wrapper]]:relative">
|
|
|
+ <div
|
|
|
+ classList={{
|
|
|
+ "size-1.5 rounded-full absolute left-3 z-10 pointer-events-none": true,
|
|
|
+ "bg-icon-success-base": props.status === true,
|
|
|
+ "bg-icon-critical-base": props.status === false,
|
|
|
+ "bg-border-weak-base": props.status === undefined,
|
|
|
+ }}
|
|
|
+ style={{ top: "50%", transform: "translateY(-50%)" }}
|
|
|
+ ref={(el) => {
|
|
|
+ // Position relative to input-wrapper
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ const wrapper = el.parentElement?.querySelector('[data-slot="input-wrapper"]')
|
|
|
+ if (wrapper instanceof HTMLElement) {
|
|
|
+ wrapper.style.position = "relative"
|
|
|
+ wrapper.appendChild(el)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }}
|
|
|
+ />
|
|
|
<TextField
|
|
|
type="text"
|
|
|
hideLabel
|
|
|
@@ -74,6 +86,7 @@ function AddRow(props: AddRowProps) {
|
|
|
onChange={props.onChange}
|
|
|
onKeyDown={props.onKeyDown}
|
|
|
onBlur={props.onBlur}
|
|
|
+ class="pl-7"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -134,7 +147,14 @@ export function DialogSelectServer() {
|
|
|
status: undefined as boolean | undefined,
|
|
|
},
|
|
|
})
|
|
|
- const [defaultUrl, defaultUrlActions] = createResource(() => platform.getDefaultServerUrl?.())
|
|
|
+ const [defaultUrl, defaultUrlActions] = createResource(
|
|
|
+ async () => {
|
|
|
+ const url = await platform.getDefaultServerUrl?.()
|
|
|
+ if (!url) return null
|
|
|
+ return normalizeServerUrl(url) ?? null
|
|
|
+ },
|
|
|
+ { initialValue: null },
|
|
|
+ )
|
|
|
const isDesktop = platform.platform === "desktop"
|
|
|
|
|
|
const looksComplete = (value: string) => {
|
|
|
@@ -344,17 +364,23 @@ export function DialogSelectServer() {
|
|
|
|
|
|
return (
|
|
|
<Dialog title={language.t("dialog.server.title")}>
|
|
|
- <div class="flex flex-col gap-2 pb-4">
|
|
|
+ <div class="flex flex-col gap-2">
|
|
|
<List
|
|
|
- search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: true }}
|
|
|
+ search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: false }}
|
|
|
+ noInitialSelection
|
|
|
emptyMessage={language.t("dialog.server.empty")}
|
|
|
items={sortedItems}
|
|
|
key={(x) => x}
|
|
|
onSelect={(x) => {
|
|
|
if (x) select(x)
|
|
|
}}
|
|
|
+ onFilter={(value) => {
|
|
|
+ if (value && store.addServer.showForm && !store.addServer.adding) {
|
|
|
+ resetAdd()
|
|
|
+ }
|
|
|
+ }}
|
|
|
divider={true}
|
|
|
- class="[&_[data-slot=list-scroll]]:max-h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-raised-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:py-3"
|
|
|
+ class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:max-h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-raised-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent [&_[data-slot=list-item-add]]:px-0"
|
|
|
add={
|
|
|
store.addServer.showForm
|
|
|
? {
|
|
|
@@ -375,7 +401,35 @@ export function DialogSelectServer() {
|
|
|
}
|
|
|
>
|
|
|
{(i) => {
|
|
|
- const [popoverOpen, setPopoverOpen] = createSignal(false)
|
|
|
+ const [truncated, setTruncated] = createSignal(false)
|
|
|
+ let nameRef: HTMLSpanElement | undefined
|
|
|
+ let versionRef: HTMLSpanElement | undefined
|
|
|
+
|
|
|
+ const check = () => {
|
|
|
+ const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false
|
|
|
+ const versionTruncated = versionRef ? versionRef.scrollWidth > versionRef.clientWidth : false
|
|
|
+ setTruncated(nameTruncated || versionTruncated)
|
|
|
+ }
|
|
|
+
|
|
|
+ createEffect(() => {
|
|
|
+ check()
|
|
|
+ window.addEventListener("resize", check)
|
|
|
+ onCleanup(() => window.removeEventListener("resize", check))
|
|
|
+ })
|
|
|
+
|
|
|
+ const tooltipValue = () => {
|
|
|
+ const name = serverDisplayName(i)
|
|
|
+ const version = store.status[i]?.version
|
|
|
+ return (
|
|
|
+ <span class="flex items-center gap-2">
|
|
|
+ <span>{name}</span>
|
|
|
+ <Show when={version}>
|
|
|
+ <span class="text-text-invert-base">{version}</span>
|
|
|
+ </Show>
|
|
|
+ </span>
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
return (
|
|
|
<div class="flex items-center gap-3 min-w-0 flex-1 group/item">
|
|
|
<Show
|
|
|
@@ -393,58 +447,54 @@ export function DialogSelectServer() {
|
|
|
/>
|
|
|
}
|
|
|
>
|
|
|
- <div
|
|
|
- class="flex items-center gap-3 px-4 min-w-0 flex-1"
|
|
|
- classList={{ "opacity-50": store.status[i]?.healthy === false }}
|
|
|
- >
|
|
|
+ <Tooltip value={tooltipValue()} placement="top" inactive={!truncated()}>
|
|
|
<div
|
|
|
- classList={{
|
|
|
- "size-1.5 rounded-full shrink-0": true,
|
|
|
- "bg-icon-success-base": store.status[i]?.healthy === true,
|
|
|
- "bg-icon-critical-base": store.status[i]?.healthy === false,
|
|
|
- "bg-border-weak-base": store.status[i] === undefined,
|
|
|
- }}
|
|
|
- />
|
|
|
- <span class="truncate">{serverDisplayName(i)}</span>
|
|
|
- <Show when={store.status[i]?.version}>
|
|
|
- <span class="text-text-weak text-14-regular">{store.status[i]?.version}</span>
|
|
|
- </Show>
|
|
|
- <Show when={defaultUrl() === i}>
|
|
|
- <span class="text-text-weak bg-surface-base text-14-regular px-1.5 rounded-xs">
|
|
|
- {language.t("dialog.server.status.default")}
|
|
|
+ class="flex items-center gap-3 px-4 min-w-0 flex-1"
|
|
|
+ classList={{ "opacity-50": store.status[i]?.healthy === false }}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ classList={{
|
|
|
+ "size-1.5 rounded-full shrink-0": true,
|
|
|
+ "bg-icon-success-base": store.status[i]?.healthy === true,
|
|
|
+ "bg-icon-critical-base": store.status[i]?.healthy === false,
|
|
|
+ "bg-border-weak-base": store.status[i] === undefined,
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <span ref={nameRef} class="truncate">
|
|
|
+ {serverDisplayName(i)}
|
|
|
</span>
|
|
|
- </Show>
|
|
|
- </div>
|
|
|
+ <Show when={store.status[i]?.version}>
|
|
|
+ <span ref={versionRef} class="text-text-weak text-14-regular truncate">
|
|
|
+ {store.status[i]?.version}
|
|
|
+ </span>
|
|
|
+ </Show>
|
|
|
+ <Show when={defaultUrl() === i}>
|
|
|
+ <span class="text-text-weak bg-surface-base text-14-regular px-1.5 rounded-xs">
|
|
|
+ {language.t("dialog.server.status.default")}
|
|
|
+ </span>
|
|
|
+ </Show>
|
|
|
+ </div>
|
|
|
+ </Tooltip>
|
|
|
</Show>
|
|
|
<Show when={store.editServer.id !== i}>
|
|
|
- <div class="flex items-center justify-center gap-5 px-4">
|
|
|
+ <div class="flex items-center justify-center gap-5 pl-4">
|
|
|
<Show when={current() === i}>
|
|
|
<p class="text-text-weak text-12-regular">{language.t("dialog.server.current")}</p>
|
|
|
</Show>
|
|
|
|
|
|
- <div onClick={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}>
|
|
|
- <Popover
|
|
|
- open={popoverOpen()}
|
|
|
- onOpenChange={setPopoverOpen}
|
|
|
- placement="bottom-start"
|
|
|
- trigger={
|
|
|
- <IconButton
|
|
|
- icon="dot-grid"
|
|
|
- variant="ghost"
|
|
|
- class="bg-transparent transition-opacity shrink-0 hover:scale-110 size-8"
|
|
|
- onPointerDown={(event: PointerEvent) => event.stopPropagation()}
|
|
|
- />
|
|
|
- }
|
|
|
- class="w-max !min-w-fit !max-w-none"
|
|
|
- >
|
|
|
- <div class="flex flex-col gap-1">
|
|
|
- <Button
|
|
|
- variant="ghost"
|
|
|
- size="normal"
|
|
|
- class="justify-start text-md"
|
|
|
- onClick={(e: MouseEvent) => {
|
|
|
- e.stopPropagation()
|
|
|
- setPopoverOpen(false)
|
|
|
+ <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={() => {
|
|
|
setStore("editServer", {
|
|
|
id: i,
|
|
|
value: i,
|
|
|
@@ -453,54 +503,42 @@ export function DialogSelectServer() {
|
|
|
})
|
|
|
}}
|
|
|
>
|
|
|
- {language.t("dialog.server.menu.edit")}
|
|
|
- </Button>
|
|
|
+ <DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
|
|
+ </DropdownMenu.Item>
|
|
|
<Show when={isDesktop && defaultUrl() !== i}>
|
|
|
- <Button
|
|
|
- variant="ghost"
|
|
|
- size="normal"
|
|
|
- class="justify-start text-md"
|
|
|
- onClick={async (e: MouseEvent) => {
|
|
|
- e.stopPropagation()
|
|
|
- setPopoverOpen(false)
|
|
|
+ <DropdownMenu.Item
|
|
|
+ onSelect={async () => {
|
|
|
await platform.setDefaultServerUrl?.(i)
|
|
|
- defaultUrlActions.refetch(i)
|
|
|
+ defaultUrlActions.mutate(i)
|
|
|
}}
|
|
|
>
|
|
|
- {language.t("dialog.server.menu.default")}
|
|
|
- </Button>
|
|
|
+ <DropdownMenu.ItemLabel>
|
|
|
+ {language.t("dialog.server.menu.default")}
|
|
|
+ </DropdownMenu.ItemLabel>
|
|
|
+ </DropdownMenu.Item>
|
|
|
</Show>
|
|
|
<Show when={isDesktop && defaultUrl() === i}>
|
|
|
- <Button
|
|
|
- variant="ghost"
|
|
|
- size="normal"
|
|
|
- class="justify-start text-md"
|
|
|
- onClick={async (e: MouseEvent) => {
|
|
|
- e.stopPropagation()
|
|
|
- setPopoverOpen(false)
|
|
|
+ <DropdownMenu.Item
|
|
|
+ onSelect={async () => {
|
|
|
await platform.setDefaultServerUrl?.(null)
|
|
|
- defaultUrlActions.refetch(null)
|
|
|
+ defaultUrlActions.mutate(null)
|
|
|
}}
|
|
|
>
|
|
|
- {language.t("dialog.server.menu.defaultRemove")}
|
|
|
- </Button>
|
|
|
+ <DropdownMenu.ItemLabel>
|
|
|
+ {language.t("dialog.server.menu.defaultRemove")}
|
|
|
+ </DropdownMenu.ItemLabel>
|
|
|
+ </DropdownMenu.Item>
|
|
|
</Show>
|
|
|
- <div class="h-px bg-border-weak-base my-1" />
|
|
|
- <Button
|
|
|
- variant="ghost"
|
|
|
- size="normal"
|
|
|
- class="justify-start text-md text-text-on-critical-base hover:bg-surface-critical-weak"
|
|
|
- onClick={(e: MouseEvent) => {
|
|
|
- e.stopPropagation()
|
|
|
- setPopoverOpen(false)
|
|
|
- handleRemove(i)
|
|
|
- }}
|
|
|
+ <DropdownMenu.Separator />
|
|
|
+ <DropdownMenu.Item
|
|
|
+ onSelect={() => handleRemove(i)}
|
|
|
+ class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
|
|
>
|
|
|
- {language.t("dialog.server.menu.delete")}
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- </Popover>
|
|
|
- </div>
|
|
|
+ <DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
|
|
|
+ </DropdownMenu.Item>
|
|
|
+ </DropdownMenu.Content>
|
|
|
+ </DropdownMenu.Portal>
|
|
|
+ </DropdownMenu>
|
|
|
</div>
|
|
|
</Show>
|
|
|
</div>
|
|
|
@@ -508,7 +546,7 @@ export function DialogSelectServer() {
|
|
|
}}
|
|
|
</List>
|
|
|
|
|
|
- <div class="px-6">
|
|
|
+ <div class="px-5 pb-5">
|
|
|
<Button
|
|
|
variant="secondary"
|
|
|
icon="plus-small"
|
|
|
@@ -517,7 +555,7 @@ export function DialogSelectServer() {
|
|
|
setStore("addServer", { showForm: true, url: "", error: "" })
|
|
|
scrollListToBottom()
|
|
|
}}
|
|
|
- class="px-3 py-4"
|
|
|
+ class="py-1.5 pl-1.5 pr-3 flex items-center gap-1.5"
|
|
|
>
|
|
|
{store.addServer.adding ? language.t("dialog.server.add.checking") : language.t("dialog.server.add.button")}
|
|
|
</Button>
|