From fc92b9fd91d8ef21bfe4bdd5654f6ca4eb50ec20 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Jul 2026 15:26:56 -0700 Subject: [PATCH] feat(ui): merge creator avatar into sidebar leading slot, move creator filter into Threads menu (#113150) * feat(ui): merge creator avatar into sidebar leading slot and move creator filter into Threads menu The per-row creator chip previously rendered next to the leading state indicator, so rows with a known creator started their titles at a different x-offset than rows without one. The avatar now occupies the single fixed leading slot: unread renders as a corner badge on it, running as a spinner ring, and open/merged PR state as a colored corner badge, while attention and pinned icons keep the slot. Child rows keep their status badges and no longer render owner chips. The standalone 'Filter by creator' select above the session list is gone; the Threads funnel menu gains a People radio section with owner chips, and the funnel trigger shows an accent dot while a creator filter is active. The empty Threads header stays visible when ownership chrome is active so the filter can always be cleared. Mock dev fixtures gain two creator identities so the ownership chrome is demonstrable. * test(ui): guard parent row spread in child-avatar ownership case --- scripts/control-ui-mock-dev.ts | 34 +++- .../app-sidebar-session-list-render.ts | 11 +- .../app-sidebar-session-menu-renderers.ts | 45 +++++ .../app-sidebar-session-navigation.ts | 6 +- .../app-sidebar-session-row-render.ts | 20 +- ui/src/components/app-sidebar.ts | 9 - .../components/session-leading-indicator.ts | 41 ++++ ui/src/components/session-owner-chip.ts | 24 --- ui/src/components/sidebar-menus-controller.ts | 5 + ui/src/components/sidebar-menus-render.ts | 12 ++ ui/src/e2e/session-ownership.e2e.test.ts | 20 +- ui/src/i18n/locales/en.ts | 2 +- ui/src/styles/components.css | 53 ++++-- ui/src/styles/layout.css | 13 ++ .../app-sidebar-cases/session-ownership.ts | 178 +++++++++++++++--- 15 files changed, 373 insertions(+), 100 deletions(-) diff --git a/scripts/control-ui-mock-dev.ts b/scripts/control-ui-mock-dev.ts index 0e0cc09ab80a..c778fd0e16c7 100644 --- a/scripts/control-ui-mock-dev.ts +++ b/scripts/control-ui-mock-dev.ts @@ -30,12 +30,26 @@ type CliOptions = { }; type SessionListOptions = { + creators?: readonly SessionCreatorFixture[]; hasMore: boolean; nextOffset: number | null; offset?: number; totalCount: number; }; +type SessionCreatorFixture = { type: "human" | "agent"; id: string; label: string }; + +// Two creator identities so the sidebar's collaborative ownership chrome +// (owner avatars + People filter) renders in the mock harness. +const MOCK_SESSION_CREATORS: readonly SessionCreatorFixture[] = [ + { type: "human", id: "profile-peter", label: "Peter" }, + { type: "human", id: "profile-mira", label: "Mira" }, +]; +const [MOCK_CREATOR_PETER, MOCK_CREATOR_MIRA] = MOCK_SESSION_CREATORS as [ + SessionCreatorFixture, + SessionCreatorFixture, +]; + const SESSION_PAGE_SIZE = 50; const TOTAL_MOCK_SESSIONS = 650; const TOTAL_TELEGRAM_SESSIONS = 180; @@ -175,6 +189,7 @@ function sessionsListResponse(sessions: unknown[], options: SessionListOptions) hasMore: options.hasMore, limitApplied: 50, nextOffset: options.nextOffset, + ...(options.creators ? { creators: options.creators } : {}), offset: options.offset ?? 0, path: "", sessions, @@ -183,11 +198,16 @@ function sessionsListResponse(sessions: unknown[], options: SessionListOptions) }; } -function pagedSessionsListResponse(sessions: unknown[], offset: number) { +function pagedSessionsListResponse( + sessions: unknown[], + offset: number, + creators?: readonly SessionCreatorFixture[], +) { const normalizedOffset = Math.max(0, Math.floor(offset)); const page = sessions.slice(normalizedOffset, normalizedOffset + SESSION_PAGE_SIZE); const nextOffset = normalizedOffset + SESSION_PAGE_SIZE; return sessionsListResponse(page, { + creators, hasMore: nextOffset < sessions.length, nextOffset: nextOffset < sessions.length ? nextOffset : null, offset: normalizedOffset, @@ -218,17 +238,18 @@ function buildSessionRows(params: { function buildSessionListCases( sessions: unknown[], matchBase: Record = {}, + creators?: readonly SessionCreatorFixture[], ): Array<{ match: Record; response: unknown }> { const cases: Array<{ match: Record; response: unknown }> = []; for (let offset = SESSION_PAGE_SIZE; offset < sessions.length; offset += SESSION_PAGE_SIZE) { cases.push({ match: { ...matchBase, offset }, - response: pagedSessionsListResponse(sessions, offset), + response: pagedSessionsListResponse(sessions, offset, creators), }); } cases.push({ match: matchBase, - response: pagedSessionsListResponse(sessions, 0), + response: pagedSessionsListResponse(sessions, 0, creators), }); return cases; } @@ -1080,6 +1101,7 @@ async function createChatPickerScenario(): Promise status: "running", }), sessionRow(NARRATION_DEMO_SESSION_KEY, "Sidebar narration demo", baseTime - 15_000, { + createdActor: MOCK_CREATOR_MIRA, hasActiveRun: true, startedAt: baseTime - 45_000, status: "running", @@ -1092,6 +1114,7 @@ async function createChatPickerScenario(): Promise icon: "name:spark", }), sessionRow("agent:main:production-export", "Production export", baseTime - 75_000, { + createdActor: MOCK_CREATOR_MIRA, execCwd: "/Users/peter/Projects/clawdbot", }), sessionRow("agent:main:model-budget", "Model budget review", baseTime - 80_000, { @@ -1100,6 +1123,7 @@ async function createChatPickerScenario(): Promise lastRunError: "Model out of credits: openai/gpt-5.6", }), sessionRow("agent:main:work-openclaw", "OpenClaw work checkout", baseTime - 85_000, { + createdActor: MOCK_CREATOR_PETER, execCwd: "/Users/peter/Work/openclaw", lastReadAt: baseTime - 120_000, observerDigest: { @@ -1144,6 +1168,8 @@ async function createChatPickerScenario(): Promise const archivedSessions = [ sessionRow("agent:main:archived-launch-notes", "Archived launch notes", baseTime - 86_400_000, { archived: true, + archivedBy: MOCK_CREATOR_MIRA, + createdActor: MOCK_CREATOR_PETER, totalTokens: 42_000, }), sessionRow( @@ -1708,7 +1734,7 @@ async function createChatPickerScenario(): Promise ...searchPrefixes("claude-sonnet-4-6"), ...searchPrefixes("anthropic"), ]), - ...buildSessionListCases([...sessions, ...archivedSessions]), + ...buildSessionListCases([...sessions, ...archivedSessions], {}, MOCK_SESSION_CREATORS), ], }, }, diff --git a/ui/src/components/app-sidebar-session-list-render.ts b/ui/src/components/app-sidebar-session-list-render.ts index 09a5ff25af20..ae0e485cbd45 100644 --- a/ui/src/components/app-sidebar-session-list-render.ts +++ b/ui/src/components/app-sidebar-session-list-render.ts @@ -168,7 +168,9 @@ function renderSessionSection(params: { ? html`