fix(app): never hide every server from Home (#60454)

Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot]
2026-10-30 05:13:31 +00:00
committed by GitHub
Unverified
parent c9ba395ecf
commit f71cee9804
3 changed files with 72 additions and 6 deletions
+13
View File
@@ -61,6 +61,19 @@ test("a working Home session keeps its project avatar and shows its activity rin
expect(box!.x).toBeGreaterThan(title!.x + title!.width)
})
test("Home lists projects and sessions when stored state hides every server", async ({ page }) => {
await mockStressTimeline(page)
await seed(page, { usedProjects: { local: [fixture.project.id] } })
// A second seed merges into the server entry the first one wrote.
await seed(page, { storage: { "opencode.global.dat:server": { hidden: { [fixture.serverKey]: true } } } })
await page.goto("/")
await expect(
page.locator('[data-component="home-project-row"]').filter({ hasText: fixture.project.name }),
).toBeVisible()
await expect(row(page, fixture.expected.targetTitle)).toBeVisible()
})
test("the session context menu renames, exports, and deletes a Home session", async ({ page }) => {
await openHome(page)
const target = row(page, fixture.expected.targetTitle)
@@ -1,5 +1,12 @@
import { expect, test } from "bun:test"
import { canRemoveServer, createServerProjects, resolveServerList, ServerConnection, serverLabel } from "./registry"
import {
canRemoveServer,
createServerProjects,
resolveServerList,
resolveVisibleServers,
ServerConnection,
serverLabel,
} from "./registry"
import { Schema } from "effect"
import { ServerState } from "./persistence"
import { createStore } from "solid-js/store"
@@ -99,6 +106,21 @@ test("keeps exact persisted server identities and prevents removing provided ser
expect(canRemoveServer({ key, stored, provided: [{ type: "http", http: { url: key } }] })).toBe(false)
})
test("hidden servers are left out, but never every server: the built-in one, otherwise the first, stays shown", () => {
const sidecar: ServerConnection.Sidecar = { type: "sidecar", variant: "base", http: { url: "http://127.0.0.1:4096" } }
const remote: ServerConnection.Http = { type: "http", http: { url: "https://apollo.example.test" } }
const other: ServerConnection.Http = { type: "http", http: { url: "https://zeus.example.test" } }
const hide = (...servers: ServerConnection.Any[]) =>
Object.fromEntries(servers.map((server) => [ServerConnection.key(server), true]))
expect(resolveVisibleServers({ servers: [sidecar, remote], hidden: {} })).toEqual([sidecar, remote])
expect(resolveVisibleServers({ servers: [sidecar, remote], hidden: hide(sidecar) })).toEqual([remote])
expect(resolveVisibleServers({ servers: [remote, sidecar], hidden: hide(remote, sidecar) })).toEqual([sidecar])
expect(resolveVisibleServers({ servers: [remote, other], hidden: hide(remote, other) })).toEqual([remote])
expect(resolveVisibleServers({ servers: [], hidden: hide(remote) })).toEqual([])
})
/** The server props a project store follows, which the test changes. */
type ProjectScopeProps = { server: ServerConnection.Key; canonicalLocalServer?: ServerConnection.Key }
+36 -5
View File
@@ -133,6 +133,21 @@ export function canRemoveServer(input: {
return input.stored.some((server) => server.http.url === input.key)
}
/**
* The servers Home lists. Home needs at least one, so when every server is flagged hidden (stored by an older
* build, or edited by hand) the built-in server, otherwise the first, is shown anyway.
*/
export function resolveVisibleServers(input: {
servers: Array<ServerConnection.Any>
hidden: StoredState["hidden"]
}): Array<ServerConnection.Any> {
const visible = input.servers.filter((conn) => !input.hidden[ServerConnection.key(conn)])
if (visible.length > 0 || input.servers.length === 0) return visible
return [input.servers.find(ServerConnection.builtin) ?? input.servers[0]]
}
export namespace ServerConnection {
type Base = { displayName?: string; label?: string }
@@ -229,7 +244,16 @@ export const { use: useServers, provider: ServersProvider } = createSimpleContex
return resolveServerList({ stored: store.list, props: props.servers })
})
const visibleServers = createMemo(() => allServers().filter((conn) => !store.hidden[ServerConnection.key(conn)]))
const visibleServers = createMemo(() => resolveVisibleServers({ servers: allServers(), hidden: store.hidden }))
// Clears the hidden flag of any server shown despite it, so the stored flags say what Home shows.
function unhideShown(servers: Array<ServerConnection.Any>) {
for (const conn of resolveVisibleServers({ servers, hidden: store.hidden })) {
const key = ServerConnection.key(conn)
if (store.hidden[key]) setStore("hidden", key, false)
}
}
function add(input: ServerConnection.Http) {
const url_ = normalizeServerUrl(input.http.url)
@@ -254,9 +278,18 @@ export const { use: useServers, provider: ServersProvider } = createSimpleContex
const list = store.list.filter((x) => x.http.url !== key)
batch(() => {
setStore("list", list)
// Removing the only shown server must not leave the rest all hidden.
unhideShown(resolveServerList({ stored: list, props: props.servers }))
})
}
function setHidden(key: ServerConnection.Key, hidden: boolean) {
// Home always lists at least one server, so hiding the only shown one does nothing.
if (hidden && visibleServers().every((conn) => ServerConnection.key(conn) === key)) return
setStore("hidden", key, hidden)
}
function canRemove(key: ServerConnection.Key) {
return canRemoveServer({ key, provided: props.servers, stored: store.list })
}
@@ -300,11 +333,9 @@ export const { use: useServers, provider: ServersProvider } = createSimpleContex
// state wait on this, but the provider must not block first render on storage.
hydrated,
isHidden(key: ServerConnection.Key) {
return store.hidden[key] ?? false
},
setHidden(key: ServerConnection.Key, hidden: boolean) {
setStore("hidden", key, hidden)
return !!store.hidden[key] && !visibleServers().some((conn) => ServerConnection.key(conn) === key)
},
setHidden,
add,
remove,
canRemove,