diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 47e85c0..2a75ca1 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -22,6 +22,7 @@ import { X, Cpu, Moon, + ChevronRight, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { rendererExtensionRegistry } from '@/extensions/registry'; @@ -85,6 +86,12 @@ function NavItem({ to, icon, label, badge, collapsed, onClick, testId }: NavItem } const INITIAL_NOW_MS = Date.now(); +const DEFAULT_EXPANDED_SESSION_BUCKETS: Record = { + today: true, + withinWeek: true, + withinMonth: false, + older: false, +}; function getAgentIdFromSessionKey(sessionKey: string): string { if (!sessionKey.startsWith('agent:')) return 'main'; @@ -173,6 +180,9 @@ export function Sidebar() { const [editingSessionKey, setEditingSessionKey] = useState(null); const [editingLabel, setEditingLabel] = useState(''); const [nowMs, setNowMs] = useState(INITIAL_NOW_MS); + const [expandedSessionBuckets, setExpandedSessionBuckets] = useState>( + () => ({ ...DEFAULT_EXPANDED_SESSION_BUCKETS }), + ); useEffect(() => { const timer = window.setInterval(() => { @@ -216,6 +226,13 @@ export function Sidebar() { } }; + const toggleSessionBucket = (bucketKey: SessionBucketKey) => { + setExpandedSessionBuckets((current) => ({ + ...current, + [bucketKey]: !current[bucketKey], + })); + }; + const stopResizing = useCallback(() => { stopResizeRef.current?.(); stopResizeRef.current = null; @@ -261,9 +278,7 @@ export function Sidebar() { ); const sessionBuckets: Array<{ key: SessionBucketKey; label: string; sessions: typeof sessions }> = [ { key: 'today', label: t('chat:historyBuckets.today'), sessions: [] }, - { key: 'yesterday', label: t('chat:historyBuckets.yesterday'), sessions: [] }, { key: 'withinWeek', label: t('chat:historyBuckets.withinWeek'), sessions: [] }, - { key: 'withinTwoWeeks', label: t('chat:historyBuckets.withinTwoWeeks'), sessions: [] }, { key: 'withinMonth', label: t('chat:historyBuckets.withinMonth'), sessions: [] }, { key: 'older', label: t('chat:historyBuckets.older'), sessions: [] }, ]; @@ -382,13 +397,30 @@ export function Sidebar() { {/* Session list — below Settings, only when expanded */} {!sidebarCollapsed && sessions.length > 0 && (
- {sessionBuckets.map((bucket) => ( - bucket.sessions.length > 0 ? ( + {sessionBuckets.map((bucket) => { + const isBucketExpanded = expandedSessionBuckets[bucket.key] ?? false; + return (
-
- {bucket.label} -
- {bucket.sessions.map((s) => { + + {isBucketExpanded && bucket.sessions.map((s) => { const agentId = getAgentIdFromSessionKey(s.key); const agentName = agentNameById[agentId] || agentId; const isEditing = editingSessionKey === s.key; @@ -482,8 +514,8 @@ export function Sidebar() { ); })}
- ) : null - ))} + ); + })}
)} diff --git a/src/components/layout/session-buckets.ts b/src/components/layout/session-buckets.ts index 839c609..2085dbe 100644 --- a/src/components/layout/session-buckets.ts +++ b/src/components/layout/session-buckets.ts @@ -1,10 +1,10 @@ import type { ChatSession } from '@/stores/chat'; +const DAY_MS = 24 * 60 * 60 * 1000; + export type SessionBucketKey = | 'today' - | 'yesterday' | 'withinWeek' - | 'withinTwoWeeks' | 'withinMonth' | 'older'; @@ -13,15 +13,10 @@ export function getSessionBucket(activityMs: number, nowMs: number): SessionBuck const now = new Date(nowMs); const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); - const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000; if (activityMs >= startOfToday) return 'today'; - if (activityMs >= startOfYesterday) return 'yesterday'; - - const daysAgo = (startOfToday - activityMs) / (24 * 60 * 60 * 1000); - if (daysAgo <= 7) return 'withinWeek'; - if (daysAgo <= 14) return 'withinTwoWeeks'; - if (daysAgo <= 30) return 'withinMonth'; + if (activityMs >= startOfToday - 7 * DAY_MS) return 'withinWeek'; + if (activityMs >= startOfToday - 30 * DAY_MS) return 'withinMonth'; return 'older'; } diff --git a/src/i18n/locales/en/chat.json b/src/i18n/locales/en/chat.json index 83cf31e..988203b 100644 --- a/src/i18n/locales/en/chat.json +++ b/src/i18n/locales/en/chat.json @@ -189,11 +189,9 @@ }, "historyBuckets": { "today": "Today", - "yesterday": "Yesterday", - "withinWeek": "Within 1 Week", - "withinTwoWeeks": "Within 2 Weeks", - "withinMonth": "Within 1 Month", - "older": "Older than 1 Month" + "withinWeek": "Last 7 days", + "withinMonth": "Last 30 days", + "older": "Older" }, "questionDirectory": { "title": "Question directory", diff --git a/src/i18n/locales/ja/chat.json b/src/i18n/locales/ja/chat.json index 6dc37c9..6038e71 100644 --- a/src/i18n/locales/ja/chat.json +++ b/src/i18n/locales/ja/chat.json @@ -189,11 +189,9 @@ }, "historyBuckets": { "today": "今日", - "yesterday": "昨日", - "withinWeek": "1週間以内", - "withinTwoWeeks": "2週間以内", - "withinMonth": "1か月以内", - "older": "1か月より前" + "withinWeek": "過去7日間", + "withinMonth": "過去30日間", + "older": "それ以前" }, "questionDirectory": { "title": "質問目次", diff --git a/src/i18n/locales/ru/chat.json b/src/i18n/locales/ru/chat.json index f9b8590..6292fc4 100644 --- a/src/i18n/locales/ru/chat.json +++ b/src/i18n/locales/ru/chat.json @@ -189,11 +189,9 @@ }, "historyBuckets": { "today": "Сегодня", - "yesterday": "Вчера", - "withinWeek": "В течение недели", - "withinTwoWeeks": "В течение 2 недель", - "withinMonth": "В течение месяца", - "older": "Старее месяца" + "withinWeek": "За последние 7 дней", + "withinMonth": "За последние 30 дней", + "older": "Ранее" }, "questionDirectory": { "title": "Список вопросов", diff --git a/src/i18n/locales/zh/chat.json b/src/i18n/locales/zh/chat.json index a593fae..be56c72 100644 --- a/src/i18n/locales/zh/chat.json +++ b/src/i18n/locales/zh/chat.json @@ -189,11 +189,9 @@ }, "historyBuckets": { "today": "今天", - "yesterday": "昨天", "withinWeek": "一周内", - "withinTwoWeeks": "两周内", - "withinMonth": "一个月内", - "older": "一个月之前" + "withinMonth": "一月内", + "older": "更早" }, "questionDirectory": { "title": "问题目录", diff --git a/tests/e2e/chat-new-session-date.spec.ts b/tests/e2e/chat-new-session-date.spec.ts index beb8153..10a9fe4 100644 --- a/tests/e2e/chat-new-session-date.spec.ts +++ b/tests/e2e/chat-new-session-date.spec.ts @@ -1,6 +1,11 @@ import { closeElectronApp, expect, getStableWindow, installIpcMocks, test } from './fixtures/electron'; const MAIN_SESSION_KEY = 'agent:main:main'; +const DAY_MS = 24 * 60 * 60 * 1000; +const SESSIONS_LIST_PAYLOAD = { + includeDerivedTitles: true, + includeLastMessage: true, +}; function stableStringify(value: unknown): string { if (value == null || typeof value !== 'object') return JSON.stringify(value); @@ -12,6 +17,81 @@ function stableStringify(value: unknown): string { } test.describe('ClawX chat session date grouping', () => { + test('shows four collapsible history buckets with only recent buckets expanded', async ({ launchElectronApp }) => { + const app = await launchElectronApp({ skipSetup: true }); + const nowMs = Date.now(); + const sessions = [ + { key: MAIN_SESSION_KEY, displayName: 'Today conversation', updatedAt: nowMs - 60 * 60 * 1000 }, + { key: `agent:main:session-${nowMs - 2 * DAY_MS}`, displayName: 'Week conversation', updatedAt: nowMs - 2 * DAY_MS }, + { key: `agent:main:session-${nowMs - 10 * DAY_MS}`, displayName: 'Month conversation', updatedAt: nowMs - 10 * DAY_MS }, + { key: `agent:main:session-${nowMs - 40 * DAY_MS}`, displayName: 'Older conversation', updatedAt: nowMs - 40 * DAY_MS }, + ]; + + try { + await installIpcMocks(app, { + gatewayStatus: { state: 'running', port: 18789, pid: 12345, connectedAt: nowMs }, + gatewayRpc: { + [stableStringify(['sessions.list', SESSIONS_LIST_PAYLOAD])]: { + success: true, + result: { sessions }, + }, + [stableStringify(['chat.history', { sessionKey: MAIN_SESSION_KEY, limit: 200 }])]: { + success: true, + result: { messages: [] }, + }, + [stableStringify(['chat.history', { sessionKey: MAIN_SESSION_KEY, limit: 1000 }])]: { + success: true, + result: { messages: [] }, + }, + }, + hostApi: { + [stableStringify(['/api/gateway/status', 'GET'])]: { + ok: true, + data: { + status: 200, + ok: true, + json: { state: 'running', port: 18789, pid: 12345, connectedAt: nowMs }, + }, + }, + [stableStringify(['/api/agents', 'GET'])]: { + ok: true, + data: { + status: 200, + ok: true, + json: { success: true, agents: [{ id: 'main', name: 'Main' }] }, + }, + }, + }, + }); + + const page = await getStableWindow(app); + try { + await page.reload(); + } catch (error) { + if (!String(error).includes('ERR_FILE_NOT_FOUND')) { + throw error; + } + } + + await expect(page.getByTestId('session-bucket-toggle-today')).toHaveAttribute('aria-expanded', 'true'); + await expect(page.getByTestId('session-bucket-toggle-withinWeek')).toHaveAttribute('aria-expanded', 'true'); + await expect(page.getByTestId('session-bucket-toggle-withinMonth')).toHaveAttribute('aria-expanded', 'false'); + await expect(page.getByTestId('session-bucket-toggle-older')).toHaveAttribute('aria-expanded', 'false'); + await expect(page.getByTestId('session-bucket-today').getByText('Today conversation')).toBeVisible(); + await expect(page.getByTestId('session-bucket-withinWeek').getByText('Week conversation')).toBeVisible(); + await expect(page.getByText('Month conversation')).toHaveCount(0); + await expect(page.getByText('Older conversation')).toHaveCount(0); + + await page.getByTestId('session-bucket-toggle-withinMonth').click(); + await page.getByTestId('session-bucket-toggle-older').click(); + + await expect(page.getByTestId('session-bucket-withinMonth').getByText('Month conversation')).toBeVisible(); + await expect(page.getByTestId('session-bucket-older').getByText('Older conversation')).toBeVisible(); + } finally { + await closeElectronApp(app); + } + }); + test('new chat appears in the Today session bucket', async ({ launchElectronApp }) => { const app = await launchElectronApp({ skipSetup: true }); const oldTimestampMs = Date.now() - 35 * 24 * 60 * 60 * 1000; @@ -24,7 +104,7 @@ test.describe('ClawX chat session date grouping', () => { await installIpcMocks(app, { gatewayStatus: { state: 'running', port: 18789, pid: 12345 }, gatewayRpc: { - [stableStringify(['sessions.list', {}])]: { + [stableStringify(['sessions.list', SESSIONS_LIST_PAYLOAD])]: { success: true, result: { sessions: [{ diff --git a/tests/unit/sidebar-session-buckets.test.ts b/tests/unit/sidebar-session-buckets.test.ts index bc14e2c..9c4c446 100644 --- a/tests/unit/sidebar-session-buckets.test.ts +++ b/tests/unit/sidebar-session-buckets.test.ts @@ -29,4 +29,16 @@ describe('sidebar session date buckets', () => { { [`agent:main:session-${keyCreatedAtMs}`]: messageActivityMs }, )).toBe(messageActivityMs); }); + + it('groups activity into today, seven days, thirty days, and older buckets', () => { + const nowMs = new Date(2026, 4, 18, 8, 15).getTime(); + + expect(getSessionBucket(new Date(2026, 4, 18, 0, 0).getTime(), nowMs)).toBe('today'); + expect(getSessionBucket(new Date(2026, 4, 17, 23, 59, 59, 999).getTime(), nowMs)).toBe('withinWeek'); + expect(getSessionBucket(new Date(2026, 4, 11, 0, 0).getTime(), nowMs)).toBe('withinWeek'); + expect(getSessionBucket(new Date(2026, 4, 10, 23, 59, 59, 999).getTime(), nowMs)).toBe('withinMonth'); + expect(getSessionBucket(new Date(2026, 3, 18, 0, 0).getTime(), nowMs)).toBe('withinMonth'); + expect(getSessionBucket(new Date(2026, 3, 17, 23, 59, 59, 999).getTime(), nowMs)).toBe('older'); + expect(getSessionBucket(0, nowMs)).toBe('older'); + }); });