Show host in search results and filter sidebar by multiple hosts (#1825)

* feat(app): show host in command center and multi-select host filter

Command-center agent results now show which host the workspace lives on,
shown only when more than one host is connected (matching the sidebar).

The sidebar display-preferences filter becomes multi-select — pick any
number of hosts instead of one-or-all — and its host rows now carry a
status dot on the left like the other host pickers. Persisted filter
state migrates from the old single host to a host list.

* test(app): extract host-filter and command-center e2e DSL helpers

Move the mechanical menu/search interactions out of the new spec bodies
into focused helpers so the tests read as user intent.

* fix(app): drop the icon from the host filter "All hosts" item

* fix(app): make multi-host e2e seeding order-independent

The offline-host seed relied on Playwright running the test's init script
after the fixture's registry reset — an ordering Playwright does not
guarantee. Write the full registry and set the fixture's disable-once flag,
then reload, so the multi-host seed survives regardless of script order.

Also tag the pre-v2 hostFilter migration reader with COMPAT so the cleanup
sweep can find it.
This commit is contained in:
Mohamed Boudra
2026-06-30 09:40:24 +02:00
committed by GitHub
parent 4e28b0941b
commit e9431517a0
11 changed files with 351 additions and 85 deletions
@@ -89,7 +89,7 @@ export interface SidebarWorkspacesListResult {
}
export function useSidebarWorkspacesList(options?: {
hostFilter?: string | null;
hostFilters?: readonly string[];
enabled?: boolean;
}): SidebarWorkspacesListResult {
const runtime = getHostRuntimeStore();
@@ -97,27 +97,31 @@ export function useSidebarWorkspacesList(options?: {
const hostRegistryLoaded = useHostRegistryLoaded();
const allServerIds = useMemo(() => allHosts.map((h) => h.serverId), [allHosts]);
const storeHostFilter = useSidebarViewStore((state) => state.hostFilter);
const hostFilter = options?.hostFilter ?? storeHostFilter;
const reconcileHostFilter = useSidebarViewStore((state) => state.reconcileHostFilter);
const hasHostFilterMatch = hostFilter ? allServerIds.includes(hostFilter) : false;
const effectiveHostFilter =
hostFilter && (!hostRegistryLoaded || hasHostFilterMatch) ? hostFilter : null;
const storeHostFilters = useSidebarViewStore((state) => state.hostFilters);
const hostFilters = options?.hostFilters ?? storeHostFilters;
const reconcileHostFilters = useSidebarViewStore((state) => state.reconcileHostFilters);
const isActive = options?.enabled !== false;
const serverIds = useMemo(() => {
if (effectiveHostFilter) {
return allServerIds.filter((id) => id === effectiveHostFilter);
if (hostFilters.length === 0) {
return allServerIds;
}
return allServerIds;
}, [allServerIds, effectiveHostFilter]);
const selected = new Set(hostFilters);
const matched = allServerIds.filter((id) => selected.has(id));
// Registry has settled but none of the pinned hosts still exist — fall back to every
// host rather than leaving the sidebar empty.
if (hostRegistryLoaded && matched.length === 0) {
return allServerIds;
}
return matched;
}, [allServerIds, hostFilters, hostRegistryLoaded]);
useEffect(() => {
if (!hostRegistryLoaded) {
return;
}
reconcileHostFilter(allServerIds);
}, [allServerIds, hostRegistryLoaded, reconcileHostFilter]);
reconcileHostFilters(allServerIds);
}, [allServerIds, hostRegistryLoaded, reconcileHostFilters]);
const persistedProjectOrder = useSidebarOrderStore((state) => state.projectOrder ?? EMPTY_ORDER);