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();