From b598e2482dd8e8cb82f83ffadfba23d81574ac35 Mon Sep 17 00:00:00 2001
From: paisley <8197966+su8su@users.noreply.github.com>
Date: Tue, 14 Jul 2026 18:17:32 +0800
Subject: [PATCH] fix: hide empty new chats from sidebar until first message
(#1162)
---
src/components/layout/Sidebar.tsx | 9 ++++--
src/stores/chat/session-key-utils.ts | 3 ++
tests/e2e/chat-new-session-date.spec.ts | 5 +--
tests/e2e/chat-workspace-context.spec.ts | 6 ++--
tests/unit/session-key-utils.test.ts | 32 +++++++++++++++++++
tests/unit/sidebar-session-buckets.test.ts | 36 ++++++++++++++++++++++
6 files changed, 85 insertions(+), 6 deletions(-)
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx
index fff87491..af27a0e2 100644
--- a/src/components/layout/Sidebar.tsx
+++ b/src/components/layout/Sidebar.tsx
@@ -36,6 +36,7 @@ import { useChatStore } from '@/stores/chat';
import { useGatewayStore } from '@/stores/gateway';
import { useAgentsStore } from '@/stores/agents';
import { groupSessionsByWorkspace } from './session-buckets';
+import { shouldIncludeSessionInSidebarList } from '@/stores/chat/session-key-utils';
import { CHANNEL_NAMES } from '@shared/types/channel';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -320,8 +321,12 @@ export function Sidebar() {
() => Object.fromEntries((agents ?? []).map((agent) => [agent.id, agent.name])),
[agents],
);
+ const sidebarSessions = useMemo(
+ () => sessions.filter((session) => shouldIncludeSessionInSidebarList(session)),
+ [sessions],
+ );
const workspaceSessionGroups = groupSessionsByWorkspace(
- sessions,
+ sidebarSessions,
sessionLastActivity,
t('chat:workspace.defaultLabel'),
chatWorkspacePath,
@@ -476,7 +481,7 @@ export function Sidebar() {
{/* Session list — below Settings, only when expanded */}
- {!sidebarCollapsed && sessions.length > 0 && (
+ {!sidebarCollapsed && sidebarSessions.length > 0 && (
diff --git a/src/stores/chat/session-key-utils.ts b/src/stores/chat/session-key-utils.ts
index 7842b533..8d90f910 100644
--- a/src/stores/chat/session-key-utils.ts
+++ b/src/stores/chat/session-key-utils.ts
@@ -71,6 +71,9 @@ export function findHiddenOpenClawHeartbeatSession(sessionKey: string, sessions:
export function shouldIncludeSessionInSidebarList(session: ChatSession): boolean {
if (!session.key) return false;
+ // Hide renderer-local placeholders created by New Chat until the first message
+ // creates the backing ACP session (acknowledgeAcpSessionCreated clears the flag).
+ if (session.createdLocally) return false;
if (isOpenClawHeartbeatOnlySession(session)) return false;
if (isChannelSessionKey(session.key)) {
return !isPlaceholderChannelSession(session);
diff --git a/tests/e2e/chat-new-session-date.spec.ts b/tests/e2e/chat-new-session-date.spec.ts
index 7c4d0104..36f9e459 100644
--- a/tests/e2e/chat-new-session-date.spec.ts
+++ b/tests/e2e/chat-new-session-date.spec.ts
@@ -119,7 +119,7 @@ test.describe('ClawX chat workspace session list', () => {
}
});
- test('new chat appears in the default workspace group', async ({ launchElectronApp }) => {
+ test('new chat stays hidden in the sidebar until the first message', async ({ launchElectronApp }) => {
const app = await launchElectronApp({ skipSetup: true });
const oldTimestampMs = Date.now() - 35 * 24 * 60 * 60 * 1000;
const seededHistory = [
@@ -189,8 +189,9 @@ test.describe('ClawX chat workspace session list', () => {
await page.getByTestId('sidebar-new-chat').click();
- await expect(page.getByTestId(defaultWorkspaceSessionGroupTestId()).getByText(/agent:main:session-/)).toBeVisible();
+ await expect(page.getByTestId(defaultWorkspaceSessionGroupTestId()).getByText(/agent:main:session-/)).toHaveCount(0);
await expect(page.getByTestId(defaultWorkspaceSessionGroupToggleTestId())).toHaveAttribute('aria-expanded', 'true');
+ await expect(page.getByTestId('acp-chat-empty-state')).toBeVisible();
} finally {
await closeElectronApp(app);
}
diff --git a/tests/e2e/chat-workspace-context.spec.ts b/tests/e2e/chat-workspace-context.spec.ts
index c7fa930b..b943acf7 100644
--- a/tests/e2e/chat-workspace-context.spec.ts
+++ b/tests/e2e/chat-workspace-context.spec.ts
@@ -256,7 +256,7 @@ test.describe('ClawX chat workspace context', () => {
}
});
- test('new unbound chat appears under the selected global workspace group from an ACP-only page', async ({ launchElectronApp }) => {
+ test('new unbound chat stays hidden until it has content and then appears under the selected global workspace group', async ({ launchElectronApp }) => {
const app = await launchElectronApp({ skipSetup: true });
try {
@@ -282,9 +282,11 @@ test.describe('ClawX chat workspace context', () => {
await expect(async () => {
await page.getByTestId('sidebar-new-chat').click();
- await expect(globalWorkspaceGroup.getByText(/agent:main:session-/)).toBeVisible({ timeout: 500 });
+ await expect(globalWorkspaceGroup.getByText(/agent:main:session-/)).toHaveCount(0, { timeout: 500 });
}).toPass({ timeout: 30_000 });
+ await expect(page.getByTestId('acp-chat-empty-state')).toBeVisible();
+
await expect(workspaceSelector).toHaveText(GLOBAL_WORKSPACE_LABEL);
await expect(workspaceSelector).toHaveAttribute('title', GLOBAL_WORKSPACE);
await expect(workspaceSelector).not.toHaveAttribute('aria-disabled', 'true');
diff --git a/tests/unit/session-key-utils.test.ts b/tests/unit/session-key-utils.test.ts
index c541b7af..3ee14978 100644
--- a/tests/unit/session-key-utils.test.ts
+++ b/tests/unit/session-key-utils.test.ts
@@ -36,6 +36,38 @@ describe('session-key-utils', () => {
expect(shouldIncludeSessionInSidebarList(placeholder)).toBe(false);
});
+ it('hides locally-created desktop sessions until the first message', () => {
+ const pending: ChatSession = {
+ key: 'agent:main:session-1710000000000',
+ displayName: 'agent:main:session-1710000000000',
+ createdLocally: true,
+ };
+
+ expect(shouldIncludeSessionInSidebarList(pending)).toBe(false);
+
+ const acknowledged: ChatSession = {
+ ...pending,
+ createdLocally: false,
+ };
+
+ expect(shouldIncludeSessionInSidebarList(acknowledged)).toBe(true);
+ });
+
+ it('hides locally-created New Chat placeholders until the first message', () => {
+ const pending: ChatSession = {
+ key: 'agent:main:session-1710000000000',
+ displayName: 'agent:main:session-1710000000000',
+ createdLocally: true,
+ };
+ expect(shouldIncludeSessionInSidebarList(pending)).toBe(false);
+
+ const acknowledged: ChatSession = {
+ ...pending,
+ createdLocally: false,
+ };
+ expect(shouldIncludeSessionInSidebarList(acknowledged)).toBe(true);
+ });
+
it('includes channel sessions once they have a message preview', () => {
const active: ChatSession = {
key: 'agent:main:feishu:ou_abc123',
diff --git a/tests/unit/sidebar-session-buckets.test.ts b/tests/unit/sidebar-session-buckets.test.ts
index 47c0c9e5..00c71819 100644
--- a/tests/unit/sidebar-session-buckets.test.ts
+++ b/tests/unit/sidebar-session-buckets.test.ts
@@ -70,6 +70,42 @@ afterEach(() => {
});
describe('sidebar session helpers', () => {
+ it('hides locally-created empty sessions until they have content', () => {
+ const pendingKey = 'agent:main:session-pending';
+ seedSidebarState();
+ useChatStore.setState({
+ sessions: [
+ { key: pendingKey, displayName: pendingKey, createdLocally: true },
+ { key: sidebarSessionKey, displayName: 'Existing chat', updatedAt: 1 },
+ ],
+ currentSessionKey: pendingKey,
+ sessionLastActivity: { [sidebarSessionKey]: 1 },
+ });
+
+ renderSidebar();
+
+ expect(screen.queryByTestId(`sidebar-session-${pendingKey}`)).not.toBeInTheDocument();
+ expect(screen.getByTestId(`sidebar-session-${sidebarSessionKey}`)).toBeInTheDocument();
+ });
+
+ it('hides locally-created empty sessions until they have content', () => {
+ const pendingKey = 'agent:main:session-pending';
+ seedSidebarState();
+ useChatStore.setState({
+ sessions: [
+ { key: sidebarSessionKey, displayName: 'Existing chat', updatedAt: 1 },
+ { key: pendingKey, displayName: pendingKey, createdLocally: true, updatedAt: 2 },
+ ],
+ currentSessionKey: pendingKey,
+ sessionLastActivity: { [sidebarSessionKey]: 1 },
+ });
+
+ renderSidebar();
+
+ expect(screen.getByTestId(`sidebar-session-${sidebarSessionKey}`)).toBeInTheDocument();
+ expect(screen.queryByTestId(`sidebar-session-${pendingKey}`)).not.toBeInTheDocument();
+ });
+
it('marks the current chat session button as the current page', () => {
seedSidebarState();