summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Hill <[email protected]>2026-01-24 20:51:53 +0000
committerDavid Hill <[email protected]>2026-01-24 22:02:08 +0000
commit02aea77e9249af5701a29a06adf47e47cedf84f3 (patch)
tree797a3aaf12229b0961ff17558caa2a2b0a99beca
parenta98add29d1b5d5f834a69b0e25156d0f301a6ec8 (diff)
downloadopencode-02aea77e9249af5701a29a06adf47e47cedf84f3.tar.gz
opencode-02aea77e9249af5701a29a06adf47e47cedf84f3.zip
feat(app): update manage servers dialog styling and behavior
-rw-r--r--packages/app/src/components/dialog-select-server.tsx106
-rw-r--r--packages/ui/src/components/dialog.css2
-rw-r--r--packages/ui/src/hooks/use-filtered-list.tsx6
3 files changed, 79 insertions, 35 deletions
diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx
index e62aa93be..12905f9f5 100644
--- a/packages/app/src/components/dialog-select-server.tsx
+++ b/packages/app/src/components/dialog-select-server.tsx
@@ -12,6 +12,7 @@ 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 { Tooltip } from "@opencode-ai/ui/tooltip"
import { useGlobalSDK } from "@/context/global-sdk"
type ServerStatus = { healthy: boolean; version?: string }
@@ -52,16 +53,16 @@ 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-3 h-14 min-w-0 flex-1">
+ <div class="relative flex-1 min-w-0">
+ <div
+ classList={{
+ "size-1.5 rounded-full absolute left-3 top-1/2 -translate-y-1/2": true,
+ "bg-icon-success-base": props.status === true,
+ "bg-icon-critical-base": props.status === false,
+ "bg-border-weak-base": props.status === undefined,
+ }}
+ />
<TextField
type="text"
hideLabel
@@ -74,6 +75,7 @@ function AddRow(props: AddRowProps) {
onChange={props.onChange}
onKeyDown={props.onKeyDown}
onBlur={props.onBlur}
+ class="pl-7"
/>
</div>
</div>
@@ -344,9 +346,10 @@ 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 pb-5">
<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}
@@ -354,7 +357,7 @@ export function DialogSelectServer() {
if (x) select(x)
}}
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"
add={
store.addServer.showForm
? {
@@ -376,6 +379,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,28 +425,34 @@ 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">
@@ -508,7 +546,7 @@ export function DialogSelectServer() {
}}
</List>
- <div class="px-6">
+ <div class="px-5">
<Button
variant="secondary"
icon="plus-small"
diff --git a/packages/ui/src/components/dialog.css b/packages/ui/src/components/dialog.css
index a0e7e111f..bc3e113bb 100644
--- a/packages/ui/src/components/dialog.css
+++ b/packages/ui/src/components/dialog.css
@@ -66,7 +66,7 @@
[data-slot="dialog-header"] {
display: flex;
- padding: 16px 16px 16px 24px;
+ padding: 20px;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
diff --git a/packages/ui/src/hooks/use-filtered-list.tsx b/packages/ui/src/hooks/use-filtered-list.tsx
index e418b55d5..2d4e2bdd1 100644
--- a/packages/ui/src/hooks/use-filtered-list.tsx
+++ b/packages/ui/src/hooks/use-filtered-list.tsx
@@ -13,6 +13,7 @@ export interface FilteredListProps<T> {
sortBy?: (a: T, b: T) => number
sortGroupsBy?: (a: { category: string; items: T[] }, b: { category: string; items: T[] }) => number
onSelect?: (value: T | undefined, index: number) => void
+ noInitialSelection?: boolean
}
export function useFilteredList<T>(props: FilteredListProps<T>) {
@@ -57,6 +58,7 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
})
function initialActive() {
+ if (props.noInitialSelection) return ""
if (props.current) return props.key(props.current)
const items = flat()
@@ -71,6 +73,10 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
})
const reset = () => {
+ if (props.noInitialSelection) {
+ list.setActive("")
+ return
+ }
const all = flat()
if (all.length === 0) return
list.setActive(props.key(all[0]))