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 && (
+
+ )}
))}