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
@@ -39,24 +39,44 @@ describe("sidebar view store", () => {
beforeEach(() => {
useSidebarViewStore.setState({
groupMode: "project",
hostFilter: null,
hostFilters: [],
});
});
it("keeps a host filter that still points at an available host", () => {
useSidebarViewStore.getState().setHostFilter("host-a");
it("toggles multiple hosts into and out of the filter", () => {
const store = useSidebarViewStore.getState();
store.toggleHostFilter("host-a");
store.toggleHostFilter("host-b");
useSidebarViewStore.getState().reconcileHostFilter(["host-a", "host-b"]);
expect(useSidebarViewStore.getState().hostFilters).toEqual(["host-a", "host-b"]);
expect(useSidebarViewStore.getState().hostFilter).toBe("host-a");
store.toggleHostFilter("host-a");
expect(useSidebarViewStore.getState().hostFilters).toEqual(["host-b"]);
store.clearHostFilters();
expect(useSidebarViewStore.getState().hostFilters).toEqual([]);
});
it("clears a host filter after that host is removed", () => {
useSidebarViewStore.getState().setHostFilter("removed-host");
it("keeps host filters that still point at available hosts", () => {
const store = useSidebarViewStore.getState();
store.toggleHostFilter("host-a");
store.toggleHostFilter("host-b");
useSidebarViewStore.getState().reconcileHostFilter(["host-a"]);
store.reconcileHostFilters(["host-a", "host-b", "host-c"]);
expect(useSidebarViewStore.getState().hostFilter).toBeNull();
expect(useSidebarViewStore.getState().hostFilters).toEqual(["host-a", "host-b"]);
});
it("drops a host filter after that host is removed", () => {
const store = useSidebarViewStore.getState();
store.toggleHostFilter("host-a");
store.toggleHostFilter("removed-host");
store.reconcileHostFilters(["host-a"]);
expect(useSidebarViewStore.getState().hostFilters).toEqual(["host-a"]);
});
it("migrates legacy per-host group modes to the new global mode", () => {
@@ -69,11 +89,11 @@ describe("sidebar view store", () => {
}),
).toEqual({
groupMode: "status",
hostFilter: null,
hostFilters: [],
});
});
it("keeps current persisted sidebar view state during version migration", () => {
it("migrates a pre-v2 single host filter to the multi-host list", () => {
expect(
migrateSidebarViewState({
groupMode: "status",
@@ -81,7 +101,19 @@ describe("sidebar view store", () => {
}),
).toEqual({
groupMode: "status",
hostFilter: "host-a",
hostFilters: ["host-a"],
});
});
it("keeps current persisted sidebar view state during version migration", () => {
expect(
migrateSidebarViewState({
groupMode: "status",
hostFilters: ["host-a", "host-b"],
}),
).toEqual({
groupMode: "status",
hostFilters: ["host-a", "host-b"],
});
});
@@ -108,8 +140,8 @@ describe("sidebar view store", () => {
it("uses the new storage key without reading the legacy key when current state exists", async () => {
const storage = createMemoryStorage({
"sidebar-view": JSON.stringify({
state: { groupMode: "project", hostFilter: "host-a" },
version: 1,
state: { groupMode: "project", hostFilters: ["host-a"] },
version: 2,
}),
"sidebar-group-mode": JSON.stringify({
state: { groupModeByServerId: { "host-b": "status" } },
@@ -121,8 +153,8 @@ describe("sidebar view store", () => {
expect(value).toBe(
JSON.stringify({
state: { groupMode: "project", hostFilter: "host-a" },
version: 1,
state: { groupMode: "project", hostFilters: ["host-a"] },
version: 2,
}),
);
expect(storage.reads).toEqual(["sidebar-view"]);