diff --git a/ui/src/e2e/session-ownership.e2e.test.ts b/ui/src/e2e/session-ownership.e2e.test.ts index 21325225ef0c..a8bbd1064437 100644 --- a/ui/src/e2e/session-ownership.e2e.test.ts +++ b/ui/src/e2e/session-ownership.e2e.test.ts @@ -481,11 +481,18 @@ suite.define(() => { sessions.count = 1; sessions.creators = [{ id: "profile-ada", label: "Ada" }]; sessions.sessions = [activeSession]; - const humanIdentities = Array.from({ length: 30 }, (_, index) => ({ - type: "human" as const, - id: `profile-member-${index}`, - label: `Member ${index + 1}`, - })); + const longMemberLabel = + "Alexandria Montgomery-Santiago from the International Collaboration Working Group"; + const longMemberId = `profile:${"member-without-a-display-name-".repeat(6)}`; + const humanIdentities = [ + { type: "human" as const, id: "profile-long-name", label: longMemberLabel }, + { type: "human" as const, id: longMemberId }, + ...Array.from({ length: 28 }, (_, index) => ({ + type: "human" as const, + id: `profile-member-${index}`, + label: `Member ${index + 1}`, + })), + ]; const channelIdentities = [ { type: "human" as const, id: "channel:chn_design", label: "Design" }, { type: "human" as const, id: "discord:channel:operations", label: "Operations" }, @@ -502,12 +509,41 @@ suite.define(() => { "session.visibility.set", ], operatorScopes: ["operator.read", "operator.write"], - historyMessages: [{ role: "assistant", content: [{ type: "text", text: "Ready." }] }], + historyMessages: [ + { + role: "user", + content: [ + { + type: "text", + text: "Share the launch review with the design and operations groups, then summarize the open decisions.", + }, + ], + }, + { + role: "assistant", + content: [ + { + type: "text", + text: "I prepared the rollout summary, linked the review notes, and kept the workspace visible to collaborators.", + }, + ], + }, + { + role: "user", + content: [ + { + type: "text", + text: "Add the remaining members and confirm the sharing policy before the handoff.", + }, + ], + }, + { role: "assistant", content: [{ type: "text", text: "Ready." }] }, + ], methodResponses: { "sessions.list": sessions, "session.members.list": { sessionKey: "agent:main:ada", - members: [], + members: [{ identityId: longMemberId, addedBy: "profile-ada", addedAt: 1 }], identities: [...humanIdentities, ...channelIdentities], role: "owner", allowedVisibilities: ["shared", "read-only", "suggest", "draft"], @@ -526,9 +562,29 @@ suite.define(() => { const animations = [...element.getAnimations(), ...(menu?.getAnimations() ?? [])]; await Promise.all(animations.map((animation) => animation.finished.catch(() => {}))); }); + const longNameItem = dropdown.locator('wa-dropdown-item[value="member:profile-long-name"]'); + const longIdItem = dropdown.locator(`wa-dropdown-item[value="member:${longMemberId}"]`); + + await longIdItem.scrollIntoViewIfNeeded(); + const selectedIndicator = longIdItem.locator('[slot="details"]'); + await expectBrowser(selectedIndicator).toBeVisible(); + const selectedIndicatorContained = await longIdItem.evaluate((item) => { + const indicator = item.querySelector('[slot="details"]'); + const itemRect = item.getBoundingClientRect(); + const indicatorRect = indicator?.getBoundingClientRect(); + return Boolean( + indicatorRect && + indicatorRect.left >= itemRect.left && + indicatorRect.right <= itemRect.right, + ); + }); + expect(selectedIndicatorContained).toBe(true); const beforeScroll = await dropdown.evaluate((element) => { const menu = element.shadowRoot?.querySelector('[part="menu"]'); + if (menu) { + menu.scrollTop = 0; + } const visibilityTitle = element.querySelector( ".chat-pane__sharing-visibility-title", ); @@ -537,6 +593,10 @@ suite.define(() => { ]; const membersTitle = element.querySelector(".chat-pane__sharing-members-title"); const firstMember = element.querySelector(".chat-pane__sharing-member"); + const longLabels = [ + ...element.querySelectorAll(".chat-pane__sharing-member-label"), + ].slice(0, 2); + const menuRect = menu?.getBoundingClientRect(); const previousVisibilityRect = visibilityItems.at(-2)?.getBoundingClientRect(); const lastVisibilityRect = visibilityItems.at(-1)?.getBoundingClientRect(); const membersTitleRect = membersTitle?.getBoundingClientRect(); @@ -545,6 +605,13 @@ suite.define(() => { menuTop: menu?.getBoundingClientRect().top ?? 0, scrollHeight: menu?.scrollHeight ?? 0, clientHeight: menu?.clientHeight ?? 0, + scrollWidth: menu?.scrollWidth ?? 0, + clientWidth: menu?.clientWidth ?? 0, + longLabelsContained: longLabels.every((label) => { + const rect = label.getBoundingClientRect(); + return menuRect ? rect.left >= menuRect.left && rect.right <= menuRect.right : false; + }), + longLabelsOverflow: longLabels.every((label) => label.scrollWidth > label.clientWidth), firstMemberTop: firstMember?.getBoundingClientRect().top ?? 0, groupGap: membersTitleRect && lastVisibilityRect @@ -594,6 +661,9 @@ suite.define(() => { expect(beforeScroll.menuHeight).toBeLessThanOrEqual(421); expect(beforeScroll.scrollHeight).toBeGreaterThan(beforeScroll.clientHeight); + expect(beforeScroll.scrollWidth).toBe(beforeScroll.clientWidth); + expect(beforeScroll.longLabelsContained).toBe(true); + expect(beforeScroll.longLabelsOverflow).toBe(true); expect(beforeScroll.membersTitleInset).toBe(beforeScroll.firstMemberInset); expect(beforeScroll.groupGap).toBeGreaterThanOrEqual(8); expect(beforeScroll.groupGap).toBeGreaterThan(beforeScroll.rowGap + 6); @@ -608,6 +678,14 @@ suite.define(() => { dropdown.locator(".chat-pane__sharing-member openclaw-session-owner-chip"), ).toHaveCount(30); await expectBrowser(dropdown.locator(".chat-pane__sharing-channel-icon")).toHaveCount(2); + expect( + await longNameItem.locator(".chat-pane__sharing-member-label").getAttribute("title"), + ).toBe(longMemberLabel); + expect(await longIdItem.locator(".chat-pane__sharing-member-label").getAttribute("title")).toBe( + longMemberId, + ); + await expectBrowser(selectedIndicator).toHaveCount(1); + expect(await selectedIndicator.getAttribute("aria-label")).not.toBeNull(); }); it("clears a selected draft mode when sharing policy becomes unavailable", async () => { diff --git a/ui/src/pages/chat/components/chat-session-sharing.test.ts b/ui/src/pages/chat/components/chat-session-sharing.test.ts index e283b35e8714..eecc115a18ec 100644 --- a/ui/src/pages/chat/components/chat-session-sharing.test.ts +++ b/ui/src/pages/chat/components/chat-session-sharing.test.ts @@ -191,8 +191,16 @@ describe("chat session sharing menu", () => { sessionKey: "agent:main:main", members: [{ identityId: "alice", addedBy: "owner", addedAt: 1 }], identities: [ - { type: "human", id: "alice", label: "Alice" }, - { type: "human", id: "bob", label: "Bob" }, + { + type: "human", + id: "alice", + label: "Alice with a very long selected member display name", + }, + { + type: "human", + id: "bob", + label: "Bob with a very long available member display name", + }, ], role: "owner", allowedVisibilities: ["shared", "read-only"], @@ -217,6 +225,15 @@ describe("chat session sharing menu", () => { expect( root.querySelector('wa-dropdown-item[value="member:bob"]')?.hasAttribute("disabled"), ).toBe(true); + for (const identityId of ["alice", "bob"]) { + const item = root.querySelector( + `wa-dropdown-item[value="member:${identityId}"]`, + ); + expect(item?.title).toBe("Requires write"); + expect(item?.querySelector(".chat-pane__sharing-member-label")?.hasAttribute("title")).toBe( + false, + ); + } dropdown?.dispatchEvent(new CustomEvent("wa-show")); dropdown?.dispatchEvent( diff --git a/ui/src/pages/chat/components/chat-session-sharing.ts b/ui/src/pages/chat/components/chat-session-sharing.ts index 3139510d9790..ccada5f651d8 100644 --- a/ui/src/pages/chat/components/chat-session-sharing.ts +++ b/ui/src/pages/chat/components/chat-session-sharing.ts @@ -183,12 +183,15 @@ export function renderChatSessionSharing(props: ChatSessionSharingProps) { ? renderSessionOwnerChip(identity, "header") : icons.bot} - ${members.has(identity.id) ? html`${icons.check}` diff --git a/ui/src/styles/chat/split-view.css b/ui/src/styles/chat/split-view.css index 04dcfaa62967..9fb42bfae13d 100644 --- a/ui/src/styles/chat/split-view.css +++ b/ui/src/styles/chat/split-view.css @@ -641,6 +641,7 @@ openclaw-chat-pane { } .chat-pane__sharing-member-label { + display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;