From b4abaa4ba163edad2b3d4abdf260eb333e4875b0 Mon Sep 17 00:00:00 2001 From: "reason-machines[bot]" <293529679+reason-machines[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 04:50:44 +0000 Subject: [PATCH] Revert "remove the Chat tab from the sidebar (#217)" This reverts commit 3e547adc05ebee1ed0728075e9c0b06945fde1ba. Co-authored-by: arnestrickmann <115920878+arnestrickmann@users.noreply.github.com> --- packages/web/src/components/nav.test.tsx | 34 ++++++++++++++++++---- packages/web/src/components/nav.tsx | 36 ++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/packages/web/src/components/nav.test.tsx b/packages/web/src/components/nav.test.tsx index a3ed70f3..2cb2b616 100644 --- a/packages/web/src/components/nav.test.tsx +++ b/packages/web/src/components/nav.test.tsx @@ -7,6 +7,7 @@ import {SidebarProvider} from '@proxy/ui/components/sidebar'; import {ThemeProvider} from '@proxy/ui/components/theme-provider'; import {TooltipProvider} from '@proxy/ui/components/tooltip'; import {AppSidebar} from '@/components/nav'; +import {type Unread, UnreadContext} from '@/hooks/use-unread'; import type {Human} from '@/lib/orgs'; import {testHuman, testOrg} from '@/test-org'; @@ -14,7 +15,16 @@ afterEach(() => { cleanup(); }); -function renderSidebar(human: Human, entry = '/acme') { +// What is unread, as the provider around the human side would have it. +function unreadOf(ids: string[]): Unread { + return { + unreadIds: new Set(ids), + answers: null, + setViewing: () => undefined, + }; +} + +function renderSidebar(human: Human, entry = '/acme', unread = unreadOf([])) { const router = createMemoryRouter( [ { @@ -31,7 +41,9 @@ function renderSidebar(human: Human, entry = '/acme') { - + + + , @@ -66,16 +78,28 @@ describe('AppSidebar', () => { expect(settings.getAttribute('href')).toBe('/acme/settings'); }); - test('shows Alerts under Activity with the harness experiment, but no Chat tab', async () => { + test('shows Chat beside Home, and Alerts under Activity, with the harness experiment', async () => { renderSidebar(testHuman({...testOrg, experiments: ['harness']})); await screen.findByRole('link', {name: 'Home'}); expect(groupLabels()).toEqual([ - ['Home'], + ['Home', 'Chat'], ['Connections', 'Agent permissions'], ['Activity', 'Alerts'], ]); - expect(screen.queryByRole('link', {name: 'Chat'})).toBeNull(); + }); + + test('counts the conversations with something unread on Chat', async () => { + renderSidebar( + testHuman({...testOrg, experiments: ['harness']}), + '/acme', + unreadOf(['general', 'finance']), + ); + await screen.findByRole('link', {name: 'Chat'}); + + const badge = screen.getByRole('status', {name: '2 unread'}); + expect(badge.textContent).toBe('2'); + expect(badge.closest('li')?.querySelector('a')?.textContent).toBe('Chat'); }); test("lights the switcher on the organization's members and settings", async () => { diff --git a/packages/web/src/components/nav.tsx b/packages/web/src/components/nav.tsx index 5f5988a1..00aa4fed 100644 --- a/packages/web/src/components/nav.tsx +++ b/packages/web/src/components/nav.tsx @@ -1,4 +1,12 @@ -import {BellIcon, BotIcon, HomeIcon, PlugIcon, ScrollTextIcon, type LucideIcon} from 'lucide-react'; +import { + BellIcon, + BotIcon, + HomeIcon, + MessageSquareIcon, + PlugIcon, + ScrollTextIcon, + type LucideIcon, +} from 'lucide-react'; import type {Experiment} from '@proxy/utils'; import {Link, useLocation} from 'react-router'; import {OrgSwitcher} from '@/components/org-switcher'; @@ -16,14 +24,34 @@ import { useSidebar, } from '@proxy/ui/components/sidebar'; import {useHuman} from '@/hooks/use-human'; +import {useUnread} from '@/hooks/use-unread'; // `experiment` is the experiment a destination is behind, shown only with it on. type Destination = {to: string; label: string; icon: LucideIcon; experiment?: Experiment}; +const CHAT = '/chat'; + +// How many conversations have something unread, on Chat: over the icon's corner on the rail, at +// the row's end where the labels show. +function UnreadBadge({count}: {count: number}) { + return ( + + {count} + + ); +} + // The organization's, under the switcher, each under its slug, in groups set apart from each // other. Its members and settings are in the switcher. const GROUPS: Destination[][] = [ - [{to: '', label: 'Home', icon: HomeIcon}], + [ + {to: '', label: 'Home', icon: HomeIcon}, + {to: '/chat', label: 'Chat', icon: MessageSquareIcon, experiment: 'harness'}, + ], [ {to: '/connections', label: 'Connections', icon: PlugIcon}, {to: '/agents', label: 'Agent permissions', icon: BotIcon}, @@ -48,6 +76,7 @@ export function AppSidebar() { const {pathname} = useLocation(); const human = useHuman(); const experiments = useExperiments(); + const unread = useUnread(); const {setOpenMobile} = useSidebar(); const home = `/${human?.org.slug ?? ''}`; const groups = GROUPS.map((group) => @@ -82,6 +111,9 @@ export function AppSidebar() { {destination.label} + {destination.to === CHAT && unread.unreadIds.size > 0 && ( + + )} ))}