diff --git a/docs/concepts/multi-user.md b/docs/concepts/multi-user.md index 1d5b0c7fd971..6be639fca996 100644 --- a/docs/concepts/multi-user.md +++ b/docs/concepts/multi-user.md @@ -46,7 +46,7 @@ The sidebar's session filter menu gains an **Owners** section when ownership is - A specific person or agent shows the sessions they currently own. - **Involving me** shows sessions you own plus sessions where you have prompted at least once. This filter is evaluated by the Gateway against the full participant history and matches only your authenticated profile identity — channel-native sender ids are display-only and never match, so a numeric channel id cannot collide with your profile. -**Involving me** requires a signed-in Gateway profile. The **People** sort mode groups sessions by current owner and orders the groups by name. +**Involving me** requires a signed-in Gateway profile. When the loaded sessions have multiple owners, **Group by Person** creates a section for each current owner, and the **Owners** sort mode orders those owner groups by name. ## Reading the avatars diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index 59db4d4f1099..fc20f105a2da 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -244,7 +244,7 @@ and [Linux](/platforms/linux) desktop apps, the ## Sidebar navigation -The sidebar organizes everything around the agent. The identity row at the top is the active agent; below it, the **Pages** section starts with **Home** — the agent's rolling main session, badged with its unread or running state — followed by the pinned destinations (**Automations** and **Plugins** by default). The customize control on the Pages header opens a menu with every other destination, including **Usage** and plugin-provided tabs, plus **Edit pinned items**; right-clicking the navigation area opens the pin editor directly. The session list below splits into zones: **Other** for the agent's ungrouped chat sessions (the main session stays behind Home; sessions it spawned appear here as top-level threads, and named threads show without a type prefix), **Groups** for group and room conversations, and **Coding** for sessions bound to a managed worktree or exec node (rows show a `repo ⎇ branch` line plus the node host), ACP-backed harness sessions, and the Codex/Claude CLI catalogs. The **Other** heading is omitted when it is the only section. Coding starts collapsed on first run and remembers your choice; its collapsed header keeps the true count and shows a running indicator while contained sessions work. Custom groups (the session `category`) and **Pinned** rows sit above Other, and assigning a session to a custom group always wins over the automatic zone classification. The global **Sessions** toolbar holds the filter and sort control (Created, Last updated, or People when the Gateway reports multiple server identities), Group by, a persisted **Status** filter for Active, Archived, or All, and the **+** that opens the New session page. People orders owner groups by name and keeps Created order within each group. On multi-user gateways the same menu adds an **Owners** filter: **All owners**, one specific person or agent, or **Involving me** — sessions you own plus sessions you have prompted, evaluated by the Gateway against the full participant history (see [Multi-user mode](/concepts/multi-user#finding-sessions-by-owner)). Archived rows stay inline, dimmed with an archive glyph; they do not contribute unread or attention state and stay outside lineage promotion. Opening a session moves the selection highlight without reordering rows. Parent sessions with recent child runs show a disclosure and child count; expand it to inspect nested child sessions, live or terminal status, and runtime without leaving the sidebar. Selecting a child opens its chat and automatically reveals its ancestor path. Child rows stay outside root grouping, pinning, dragging, multi-select, and pagination; collapsed zones do not consume the visible page budget. Sessions with new activity since they were last read show an unread dot, and opening one marks it read. Admitted work that has not started shows **Queued** with an hourglass until execution starts. This state does not by itself mean every concurrency slot is occupied. A session holding composer text you typed but never sent shows a pencil badge until the draft is sent or cleared; the active session hides it because its composer is already in view. An agent can also publish a short expiring status line and optionally request attention with a curated amber icon; that declaration clears when you open the session, send the next message, clear it explicitly, or its TTL expires. Cloud-worker lifecycle states use a globe badge; local and reclaimed sessions omit a placement badge because local execution is the default. Each root session row has a context menu (kebab button or right-click) with Pin/Unpin, Mark as unread/read, Rename, Assign to me and Assign to… on multi-user gateways ([session ownership](/concepts/multi-user#assigning-an-owner)), Set icon, Fork, Copy session ID, Move to group (including New group and Remove from group), Archive or Unarchive, and Delete; touch layouts keep the direct pin and menu controls visible. The chat header composes the same single-session management actions with its pane-specific **Panels**, **Layout**, **View**, and **Continue in terminal** actions. Cmd/Ctrl-click opens a session in a new browser tab. Alt/Option-click toggles root rows into a multi-select and Shift-click extends it across the visible order; opening the menu on a selected row then offers batch actions (Mark N as unread/read, Move N to group, Archive N, Delete N) that apply to every selected session, with a single confirmation for batch delete. Drag a root session onto **Pinned** to pin it, or onto a custom group to move it. Custom group headers can be collapsed, expanded, or dragged to reorder them; group names, order, and New Session defaults live in the gateway (`sessions.groups.*`), so they follow you across browsers, while collapsed state stays in the browser profile. Each custom group header has a **+** that opens the normal New Session page and assigns the created session to that group. **New session defaults** in the group menu sets its working directory and Local or Worktree preference; the page prefills those values but leaves them editable. Leaving the directory empty uses the selected agent's workspace. The menu also has Rename group, New group, and Delete group; renaming or deleting a group updates every member session server-side, including archived ones, and deleting a group keeps its sessions and moves them back to Other. +The sidebar organizes everything around the agent. The identity row at the top is the active agent; below it, the **Pages** section starts with **Home** — the agent's rolling main session, badged with its unread or running state — followed by the pinned destinations (**Automations** and **Plugins** by default). The customize control on the Pages header opens a menu with every other destination, including **Usage** and plugin-provided tabs, plus **Edit pinned items**; right-clicking the navigation area opens the pin editor directly. The session list below splits into zones: **Other** for the agent's ungrouped chat sessions (the main session stays behind Home; sessions it spawned appear here as top-level threads, and named threads show without a type prefix), **Groups** for group and room conversations, and **Coding** for sessions bound to a managed worktree or exec node (rows show a `repo ⎇ branch` line plus the node host), ACP-backed harness sessions, and the Codex/Claude CLI catalogs. The **Other** heading is omitted when it is the only section. Coding starts collapsed on first run and remembers your choice; its collapsed header keeps the true count and shows a running indicator while contained sessions work. Custom groups (the session `category`) and **Pinned** rows sit above Other, and assigning a session to a custom group always wins over the automatic zone classification. The global **Sessions** toolbar holds the filter and sort control (Created, Last updated, or Owners when the loaded session roster contains multiple owners), Group by, a persisted **Status** filter for Active, Archived, or All, and the **+** that opens the New session page. The Owners sort mode orders owner groups by name and keeps Created order within each group. On multi-user gateways the same menu adds an **Owners** filter: **All owners**, one specific person or agent, or **Involving me** — sessions you own plus sessions you have prompted, evaluated by the Gateway against the full participant history (see [Multi-user mode](/concepts/multi-user#finding-sessions-by-owner)). Archived rows stay inline, dimmed with an archive glyph; they do not contribute unread or attention state and stay outside lineage promotion. Opening a session moves the selection highlight without reordering rows. Parent sessions with recent child runs show a disclosure and child count; expand it to inspect nested child sessions, live or terminal status, and runtime without leaving the sidebar. Selecting a child opens its chat and automatically reveals its ancestor path. Child rows stay outside root grouping, pinning, dragging, multi-select, and pagination; collapsed zones do not consume the visible page budget. Sessions with new activity since they were last read show an unread dot, and opening one marks it read. Admitted work that has not started shows **Queued** with an hourglass until execution starts. This state does not by itself mean every concurrency slot is occupied. A session holding composer text you typed but never sent shows a pencil badge until the draft is sent or cleared; the active session hides it because its composer is already in view. An agent can also publish a short expiring status line and optionally request attention with a curated amber icon; that declaration clears when you open the session, send the next message, clear it explicitly, or its TTL expires. Cloud-worker lifecycle states use a globe badge; local and reclaimed sessions omit a placement badge because local execution is the default. Each root session row has a context menu (kebab button or right-click) with Pin/Unpin, Mark as unread/read, Rename, Assign to me and Assign to… on multi-user gateways ([session ownership](/concepts/multi-user#assigning-an-owner)), Set icon, Fork, Copy session ID, Move to group (including New group and Remove from group), Archive or Unarchive, and Delete; touch layouts keep the direct pin and menu controls visible. The chat header composes the same single-session management actions with its pane-specific **Panels**, **Layout**, **View**, and **Continue in terminal** actions. Cmd/Ctrl-click opens a session in a new browser tab. Alt/Option-click toggles root rows into a multi-select and Shift-click extends it across the visible order; opening the menu on a selected row then offers batch actions (Mark N as unread/read, Move N to group, Archive N, Delete N) that apply to every selected session, with a single confirmation for batch delete. Drag a root session onto **Pinned** to pin it, or onto a custom group to move it. Custom group headers can be collapsed, expanded, or dragged to reorder them; group names, order, and New Session defaults live in the gateway (`sessions.groups.*`), so they follow you across browsers, while collapsed state stays in the browser profile. Each custom group header has a **+** that opens the normal New Session page and assigns the created session to that group. **New session defaults** in the group menu sets its working directory and Local or Worktree preference; the page prefills those values but leaves them editable. Leaving the directory empty uses the selected agent's workspace. The menu also has Rename group, New group, and Delete group; renaming or deleting a group updates every member session server-side, including archived ones, and deleting a group keeps its sessions and moves them back to Other. **Mark as unread** creates a reminder that remains unread while the current chat stays open, including while a run streams or completes. Leave and reopen the session, or choose **Mark as read**, to clear it. diff --git a/ui/src/components/app-sidebar-session-navigation.ts b/ui/src/components/app-sidebar-session-navigation.ts index 78a469e312d8..18c70f344c11 100644 --- a/ui/src/components/app-sidebar-session-navigation.ts +++ b/ui/src/components/app-sidebar-session-navigation.ts @@ -116,7 +116,8 @@ export class AppSidebarSessionNavigationElement extends AppSidebarBase { }); private sessionPeopleSortCapability(): boolean | undefined { - return this.context?.gateway.snapshot.hello?.policy?.hasMultipleSessionSharingIdentities; + const owners = this.selectedAgentSessionResult()?.owners; + return owners ? owners.length >= 2 : undefined; } sessionPeopleSortAvailable(): boolean { @@ -124,14 +125,14 @@ export class AppSidebarSessionNavigationElement extends AppSidebarBase { } effectiveSessionSortMode(): SidebarSessionSortMode { - // A reconnect can temporarily hide the capability. Render Created without - // discarding People until an authoritative single-identity hello arrives. + // A refresh can temporarily invalidate the owner facet. Render Created + // without discarding People until an authoritative single-owner list arrives. return resolveSidebarSessionSortMode(this.sessionSortMode, this.sessionPeopleSortAvailable()); } effectiveSessionsGrouping(): SidebarSessionsGrouping { - // Reconnects temporarily hide the capability; retain the stored Person - // preference so it returns when the authoritative identity policy does. + // Refreshes can temporarily invalidate the owner facet; retain the Person + // preference so it returns with the authoritative multi-owner list. const grouping = this.sessionsGrouping; return grouping === "person" && !this.sessionPeopleSortAvailable() ? "category" : grouping; } @@ -717,11 +718,7 @@ export class AppSidebarSessionNavigationElement extends AppSidebarBase { ) { projected.unshift(navigationState.toSidebarSession(selectedFallback)); } - const ownerFacet = - selected === loadedAgentId - ? this.sessionData.sessionsResult?.owners - : this.sessionData.sessionResultsByAgent[selected]?.owners; - return this.applySessionOwnerFilter(projected, ownerFacet); + return this.applySessionOwnerFilter(projected, this.selectedAgentSessionResult()?.owners); } private selectedAgentSessionResult(): SessionsListResult | null { diff --git a/ui/src/e2e/session-ownership.e2e.test.ts b/ui/src/e2e/session-ownership.e2e.test.ts index f290b0bf1e06..8b54ab090d78 100644 --- a/ui/src/e2e/session-ownership.e2e.test.ts +++ b/ui/src/e2e/session-ownership.e2e.test.ts @@ -1,5 +1,5 @@ import { mkdir } from "node:fs/promises"; -import type { Page } from "playwright"; +import type { Locator, Page } from "playwright"; import { expect as expectBrowser } from "playwright/test"; import { afterEach, expect, it } from "vitest"; import { installMockGateway } from "../test-helpers/control-ui-e2e.ts"; @@ -22,6 +22,18 @@ const suite = createControlUiE2eSuite({ }); let page: Page | undefined; + +async function selectMenuValue(menu: Locator, value: string) { + await menu.evaluate((element, selectedValue) => { + element.dispatchEvent( + new CustomEvent("wa-select", { + bubbles: true, + detail: { item: { value: selectedValue } }, + }), + ); + }, value); +} + function sessionsList(owners: [string, string], withAvatars = false) { const ownerFacet = [ { @@ -261,7 +273,7 @@ suite.define(() => { } }); - it("shows permanent owner chips and filters existing custom groups", async () => { + it("derives People controls and owner filtering from current session owners", async () => { if (captureUiProofEnabled) { await mkdir(sessionOwnerProofArtifactDir, { recursive: true }); } @@ -284,7 +296,7 @@ suite.define(() => { { id: "profile-bob", background: "#985b42", label: "B" }, ]); const gateway = await installMockGateway(currentPage, { - hasMultipleSessionSharingIdentities: true, + hasMultipleSessionSharingIdentities: false, sessionKey: "agent:main:ada", presenceUsers: [ { @@ -306,7 +318,9 @@ suite.define(() => { await expect.poll(() => currentPage.locator("openclaw-session-owner-chip").count()).toBe(3); const ownerMenu = await openSidebarSortMenu(currentPage); - await ownerMenu.locator('[value="sort:people"]').waitFor(); + await captureUiProof(currentPage, "00-people-controls-from-session-owners.png"); + await expectBrowser(ownerMenu.locator('[value="grouping:person"]')).toBeVisible(); + await expectBrowser(ownerMenu.locator('[value="sort:people"]')).toBeVisible(); const ownerRows = ownerMenu.locator('wa-dropdown-item[value^="owner:"]:not([value="owner:"])'); await expectBrowser(ownerRows).toHaveCount(3); await expectBrowser(ownerRows.first()).toHaveAttribute("value", "owner:profile-patrick"); @@ -315,14 +329,23 @@ suite.define(() => { const firstOwnerCenterDelta = await avatarLabelCenterDelta(ownerRows.first()); await captureUiProof(currentPage, "00-people-sort-available.png"); expect(firstOwnerCenterDelta).toBeLessThanOrEqual(0.5); - await ownerMenu.evaluate((element) => - element.dispatchEvent( - new CustomEvent("wa-select", { - bubbles: true, - detail: { item: { value: "sort:people" } }, - }), - ), + await selectMenuValue(ownerMenu, "grouping:person"); + await expectBrowser( + currentPage.locator('[data-session-section="person:profile-ada"]'), + ).toContainText("Ada research"); + await expectBrowser( + currentPage.locator('[data-session-section="person:profile-bob"]'), + ).toContainText("Bob operations"); + + const groupedMenu = await openSidebarSortMenu(currentPage); + await expectBrowser(groupedMenu.locator('[value="grouping:person"]')).toHaveAttribute( + "aria-checked", + "true", ); + await selectMenuValue(groupedMenu, "grouping:category"); + + const sortableMenu = await openSidebarSortMenu(currentPage); + await selectMenuValue(sortableMenu, "sort:people"); const peopleMenu = await openSidebarSortMenu(currentPage); await expectBrowser(peopleMenu.locator('[value="sort:people"]')).toHaveAttribute( "aria-checked", @@ -330,14 +353,7 @@ suite.define(() => { ); await captureUiProof(currentPage, "01-people-sort-selected.png"); await peopleMenu.locator('[value="owner:profile-ada"]').waitFor(); - await peopleMenu.evaluate((element) => - element.dispatchEvent( - new CustomEvent("wa-select", { - bubbles: true, - detail: { item: { value: "owner:profile-ada" } }, - }), - ), - ); + await selectMenuValue(peopleMenu, "owner:profile-ada"); await currentPage.getByText("Ada research", { exact: true }).first().waitFor(); await expect .poll(() => currentPage.locator('[data-session-key="agent:main:bob"]').count()) @@ -427,14 +443,7 @@ suite.define(() => { sessions: allSessions.sessions.filter((session) => session.key === "agent:main:ada"), }); const menu = await openSidebarSortMenu(currentPage); - await menu.evaluate((element) => - element.dispatchEvent( - new CustomEvent("wa-select", { - bubbles: true, - detail: { item: { value: "involving-me" } }, - }), - ), - ); + await selectMenuValue(menu, "involving-me"); await expect .poll(() => currentPage.locator('[data-session-key="agent:main:bob"]').count()) .toBe(0); diff --git a/ui/src/pages/sessions/sessions-page.roster.test.ts b/ui/src/pages/sessions/sessions-page.roster.test.ts index 9adcaffc7268..4484e8ec0034 100644 --- a/ui/src/pages/sessions/sessions-page.roster.test.ts +++ b/ui/src/pages/sessions/sessions-page.roster.test.ts @@ -39,6 +39,57 @@ afterEach(() => { }); describe("sessions page managed roster", () => { + it.each([ + { + name: "offers person grouping for multiple session owners despite a single-identity handshake", + ownerCount: 2, + handshakeIdentities: false, + available: true, + }, + { + name: "hides person grouping for one session owner despite a multiple-identity handshake", + ownerCount: 1, + handshakeIdentities: true, + available: false, + }, + { + name: "hides person grouping without session owners despite a multiple-identity handshake", + ownerCount: 0, + handshakeIdentities: true, + available: false, + }, + ])("$name", async ({ ownerCount, handshakeIdentities, available }) => { + const mutableGateway = createGateway({} as GatewayBrowserClient); + mutableGateway.emit({ + hello: { + ...sessionMutationGatewayHello(), + policy: { hasMultipleSessionSharingIdentities: handshakeIdentities }, + }, + }); + const managed = createManagedSessions(); + const context = createContext(mutableGateway.gateway, managed.sessions); + const owners = [ + { type: "human" as const, id: "profile-ada", label: "Ada Lovelace" }, + { type: "human" as const, id: "profile-bob", label: "Bob Rivera" }, + ].slice(0, ownerCount); + const page = await createRenderedPage(context, { + ts: 0, + path: "(multiple)", + count: owners.length, + defaults: { modelProvider: null, model: null, contextTokens: null }, + owners, + sessions: owners.map((owner, index) => ({ + key: `agent:main:${owner.id}`, + kind: "direct", + updatedAt: index, + owner: { actor: owner }, + })), + }); + + const personOption = page.querySelector('.session-groupby__select option[value="person"]'); + expect(personOption !== null).toBe(available); + }); + it("rejects route data from an earlier same-client connection epoch", async () => { const client = {} as GatewayBrowserClient; const mutableGateway = createGateway(client); diff --git a/ui/src/pages/sessions/sessions-page.ts b/ui/src/pages/sessions/sessions-page.ts index 1c4ca883fb02..f8566ba3e4f1 100644 --- a/ui/src/pages/sessions/sessions-page.ts +++ b/ui/src/pages/sessions/sessions-page.ts @@ -1511,8 +1511,7 @@ class SessionsPage extends OpenClawLightDomElement { override render() { const context = this.context; - const personGroupingAvailable = - context?.gateway.snapshot.hello?.policy?.hasMultipleSessionSharingIdentities === true; + const personGroupingAvailable = (this.result?.owners?.length ?? 0) > 1; if (!context) { return html``; } @@ -1563,7 +1562,7 @@ class SessionsPage extends OpenClawLightDomElement { sortColumn: this.sortColumn, sortDir: this.sortDir, // Same reconnect resilience as the sidebar: the stored Person - // preference survives a temporarily hidden identity capability. + // preference survives a temporarily unavailable owner roster. groupBy: personGroupingAvailable || this.groupBy !== "person" ? this.groupBy : "none", personGroupingAvailable, knownCategories: this.knownCategories(), diff --git a/ui/src/test-helpers/app-sidebar-cases/session-ownership.ts b/ui/src/test-helpers/app-sidebar-cases/session-ownership.ts index fc95c171198c..6755481a3633 100644 --- a/ui/src/test-helpers/app-sidebar-cases/session-ownership.ts +++ b/ui/src/test-helpers/app-sidebar-cases/session-ownership.ts @@ -372,8 +372,9 @@ describe("AppSidebar session ownership", () => { expect(sidebar.querySelector("openclaw-session-owner-chip")).toBeNull(); }); - it("owns People availability and fallback at the server identity capability", async () => { + it("owns People availability and fallback at the live session-owner roster", async () => { const gateway = createGatewayHarness({} as GatewayBrowserClient); + gateway.publish({ hello: sessionSharingHello(true) }); const keys = ["main", "b1", "a1", "b2", "a2"].map((id) => `agent:main:${id}`); const harness = createSessionsHarness("main", keys); const result = harness.sessions.state.result; @@ -409,31 +410,29 @@ describe("AppSidebar session ownership", () => { menu.dispatchEvent(new Event("wa-after-hide", { bubbles: true })); await sidebar.updateComplete; - gateway.publish({ hello: sessionSharingHello(true) }); + result.owners = [ + { type: "human", id: "profile-ada", label: "Ada" }, + { type: "human", id: "profile-bob", label: "Bob" }, + ]; + gateway.publish({ hello: sessionSharingHello(false) }); + harness.publishList({ result, agentId: "main" }); await sidebar.updateComplete; await expectSort(sidebar, "people", peopleOrder); gateway.publish({ hello: null }); await sidebar.updateComplete; menu = await openOwnerMenu(sidebar); - expect(menu.querySelector('[value="sort:people"]')).toBeNull(); - expect(menu.querySelector('[value="sort:created"]')?.getAttribute("aria-checked")).toBe("true"); + expect(menu.querySelector('[value="sort:people"]')?.getAttribute("aria-checked")).toBe("true"); + expect(visibleSessionKeys(sidebar)).toEqual(peopleOrder); menu.dispatchEvent(new Event("wa-after-hide", { bubbles: true })); await sidebar.updateComplete; - gateway.publish({ hello: sessionSharingHello(true) }); - await sidebar.updateComplete; - expect(visibleSessionKeys(sidebar)).toEqual(peopleOrder); - await expectSort(sidebar, "updated", updatedOrder); await expectSort(sidebar, "created", createdOrder); await expectSort(sidebar, "people", peopleOrder); - result.owners = [ - { type: "human", id: "profile-ada", label: "Ada" }, - { type: "human", id: "profile-bob", label: "Bob" }, - ]; + result.owners = [{ type: "human", id: "profile-bob", label: "Bob" }]; + gateway.publish({ hello: sessionSharingHello(true) }); harness.publishList({ result, agentId: "main" }); - gateway.publish({ hello: sessionSharingHello(false) }); await sidebar.updateComplete; await sidebar.updateComplete; @@ -441,16 +440,24 @@ describe("AppSidebar session ownership", () => { expect(menu.querySelector('[value="sort:people"]')).toBeNull(); expect(menu.querySelector('[value="sort:created"]')?.getAttribute("aria-checked")).toBe("true"); menu.dispatchEvent(new Event("wa-after-hide", { bubbles: true })); - gateway.publish({ hello: sessionSharingHello(true) }); + result.owners = [ + { type: "human", id: "profile-ada", label: "Ada" }, + { type: "human", id: "profile-bob", label: "Bob" }, + ]; + gateway.publish({ hello: sessionSharingHello(false) }); + harness.publishList({ result, agentId: "main" }); await sidebar.updateComplete; menu = await openOwnerMenu(sidebar); expect(menu.querySelector('[value="sort:people"]')).not.toBeNull(); expect(menu.querySelector('[value="sort:created"]')?.getAttribute("aria-checked")).toBe("true"); }); - it("groups sessions by owner only while the identity capability is available", async () => { + it("groups sessions by owner based on the live session-owner roster", async () => { const gateway = createGatewayHarness({} as GatewayBrowserClient); - gateway.publish({ selfUser: { id: "profile-zoe", name: "Zoe" } }); + gateway.publish({ + hello: sessionSharingHello(false), + selfUser: { id: "profile-zoe", name: "Zoe" }, + }); const harness = createSessionsHarness("main", [ "agent:main:main", "agent:main:ada", @@ -479,12 +486,10 @@ describe("AppSidebar session ownership", () => { await sidebar.updateComplete; let menu = await openOwnerMenu(sidebar); - expect(menu.querySelector('[value="grouping:person"]')).toBeNull(); + expect(menu.querySelector('[value="grouping:person"]')).not.toBeNull(); menu.dispatchEvent(new Event("wa-after-hide", { bubbles: true })); await sidebar.updateComplete; - gateway.publish({ hello: sessionSharingHello(true) }); - await sidebar.updateComplete; await selectSessionMenuValue(sidebar, "grouping:person"); const ownerSections = () => [ @@ -509,6 +514,18 @@ describe("AppSidebar session ownership", () => { gateway.publish({ hello: null }); await sidebar.updateComplete; + expect(ownerSections()).toHaveLength(2); + menu = await openOwnerMenu(sidebar); + expect(menu.querySelector('[value="grouping:person"]')?.getAttribute("aria-checked")).toBe( + "true", + ); + menu.dispatchEvent(new Event("wa-after-hide", { bubbles: true })); + await sidebar.updateComplete; + + gateway.publish({ hello: sessionSharingHello(true) }); + result.owners = [{ type: "human", id: "profile-zoe", label: "Zoe" }]; + harness.publishList({ result, agentId: "main" }); + await sidebar.updateComplete; expect(ownerSections()).toHaveLength(0); menu = await openOwnerMenu(sidebar); expect(menu.querySelector('[value="grouping:person"]')).toBeNull(); @@ -516,9 +533,13 @@ describe("AppSidebar session ownership", () => { "true", ); menu.dispatchEvent(new Event("wa-after-hide", { bubbles: true })); - await sidebar.updateComplete; - gateway.publish({ hello: sessionSharingHello(true) }); + result.owners = [ + { type: "human", id: "profile-ada", label: "Ada" }, + { type: "human", id: "profile-zoe", label: "Zoe" }, + ]; + gateway.publish({ hello: sessionSharingHello(false) }); + harness.publishList({ result, agentId: "main" }); await sidebar.updateComplete; expect(ownerSections()).toHaveLength(2); });