Repository navigation
feat: add smart activity grouping for sidebar and dashboard - #143
Conversation
Add accordion-style collapsible groups to activity views: - Consecutive rollbacks grouped as "X rollbacks" - AI chat streams grouped by aiConversationId - User edit sessions grouped by changeGroupId per document Backend: Session ID tracking via changeGroupId in useDocument hook Frontend: Grouping algorithm with priority AI > Rollback > Edit session UI: New ActivityGroupItem component with expand/collapse 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Rate limit exceeded@2witstudios has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 15 minutes and 37 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds session-scoped changeGroupId to PATCH page requests, threads it into pageService.updatePage context, and implements activity grouping (rollback, AI stream, edit-session) with UI components, utilities, and tests to render collapsible grouped activity timelines. Changes
Sequence DiagramsequenceDiagram
participant User
participant Hook as useDocument
participant API as Pages API Route
participant Service as pageService
participant DB as Activity Log / DB
participant Timeline as Activity Timeline UI
participant GroupUI as ActivityGroupItem
User->>Hook: edit & save
Note over Hook: ensure stable sessionId (changeGroupId)
Hook->>API: PATCH /pages/[pageId] { ..., changeGroupId: sessionId }
API->>Service: updatePage(pageId, userId, updates, { expectedRevision, context: { changeGroupId } })
Service->>DB: persist update + activity log (changeGroupId)
User->>Timeline: open activity tab
Timeline->>DB: fetch activities
DB-->>Timeline: activities (with aiConversationId, changeGroupId)
Timeline->>Timeline: groupConsecutiveActivities(activities)
Timeline->>GroupUI: render ActivityGroupItem or ActivityItem
GroupUI->>User: collapse/expand, undo (rollback-to-point)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
apps/web/src/components/activity/__tests__/utils.test.ts (3)
13-47: Consider using deterministic values in the test factory.The factory uses
Math.random()for IDs (line 15) andnew Date()for timestamps (line 16), which creates non-deterministic test data. While this doesn't break the current tests, deterministic values improve test stability and debugging.🔎 Proposed refactor to use deterministic test data
+let activityIdCounter = 0; + function createActivity(overrides: Partial<ActivityLog> = {}): ActivityLog { return { - id: `activity_${Math.random().toString(36).slice(2)}`, - timestamp: new Date().toISOString(), + id: overrides.id ?? `activity_${++activityIdCounter}`, + timestamp: '2025-01-01T12:00:00.000Z', userId: 'user_1', actorEmail: 'test@example.com',
177-193: Strengthen the assertion for AI stream label.The regex
/AI updated \d+ page/on line 191 is too loose and won't validate singular/plural handling correctly. With 3 activities, the label should contain "pages" (plural).🔎 Proposed fix for more precise assertion
if (result[0].type !== 'single') { expect(result[0].activities).toHaveLength(3); - expect(result[0].summary.label).toMatch(/AI updated \d+ page/); + expect(result[0].summary.label).toMatch(/AI updated \d+ pages/); }Alternatively, if you want to test singular/plural handling more thoroughly, add separate test cases for 2 items ("pages") and verify the implementation handles both correctly.
219-235: Strengthen the assertion for edit session label.The regex
/3 edits to/on line 233 only validates the prefix and doesn't check the complete label format including the resource title.🔎 Proposed fix for more complete assertion
if (result[0].type !== 'single') { expect(result[0].activities).toHaveLength(3); - expect(result[0].summary.label).toMatch(/3 edits to/); + expect(result[0].summary.label).toBe('3 edits to Test Page'); }Using an exact match ensures the complete label format is validated, including the resource title.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
apps/web/src/app/api/pages/[pageId]/route.tsapps/web/src/components/activity/ActivityGroupItem.tsxapps/web/src/components/activity/ActivityTimeline.tsxapps/web/src/components/activity/__tests__/utils.test.tsapps/web/src/components/activity/types.tsapps/web/src/components/activity/utils.tsapps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsxapps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsxapps/web/src/hooks/useDocument.ts
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Never useanytypes - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase withuseprefix), Zustand stores (camelCase withuseprefix), and React components (PascalCase)
Lint with Next/ESLint as configured inapps/web/eslint.config.mjs
Message content should always use the message parts structure with{ parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from@pagespace/lib/permissions(e.g.,getUserAccessLevel,canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from@pagespace/dbpackage for database access
Use ESM modules throughout the codebase
**/*.{ts,tsx}: Never useanytypes - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting
Files:
apps/web/src/components/activity/ActivityGroupItem.tsxapps/web/src/components/activity/ActivityTimeline.tsxapps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsxapps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsxapps/web/src/components/activity/types.tsapps/web/src/components/activity/utils.tsapps/web/src/components/activity/__tests__/utils.test.tsapps/web/src/hooks/useDocument.tsapps/web/src/app/api/pages/[pageId]/route.ts
**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
**/*.tsx: React component files should use PascalCase (e.g.,UserProfile.tsx)
Use @dnd-kit for drag-and-drop functionality
Use Zustand for client state management
Use SWR for server state management and caching
Files:
apps/web/src/components/activity/ActivityGroupItem.tsxapps/web/src/components/activity/ActivityTimeline.tsxapps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsxapps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
**/*.{ts,tsx,js,jsx,json}
📄 CodeRabbit inference engine (AGENTS.md)
Format code with Prettier
Files:
apps/web/src/components/activity/ActivityGroupItem.tsxapps/web/src/components/activity/ActivityTimeline.tsxapps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsxapps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsxapps/web/src/components/activity/types.tsapps/web/src/components/activity/utils.tsapps/web/src/components/activity/__tests__/utils.test.tsapps/web/src/hooks/useDocument.tsapps/web/src/app/api/pages/[pageId]/route.ts
**/*.{tsx,css}
📄 CodeRabbit inference engine (AGENTS.md)
Use Tailwind CSS and shadcn/ui components for styling and UI
Files:
apps/web/src/components/activity/ActivityGroupItem.tsxapps/web/src/components/activity/ActivityTimeline.tsxapps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsxapps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
apps/web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
apps/web/src/**/*.{ts,tsx}: Use message parts structure for message content:{ parts: [{ type: 'text', text: '...' }] }
For database access, always use Drizzle client from@pagespace/db:import { db, pages } from '@pagespace/db';
Use centralized Drizzle ORM with PostgreSQL for all database operations - no direct SQL or other ORMs
Use Socket.IO for real-time collaboration features - imported from the realtime service at port 3001
Use Vercel AI SDK with async/await for all AI operations and streaming
Use Next.js 15 App Router and TypeScript for all routes and components
Files:
apps/web/src/components/activity/ActivityGroupItem.tsxapps/web/src/components/activity/ActivityTimeline.tsxapps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsxapps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsxapps/web/src/components/activity/types.tsapps/web/src/components/activity/utils.tsapps/web/src/components/activity/__tests__/utils.test.tsapps/web/src/hooks/useDocument.tsapps/web/src/app/api/pages/[pageId]/route.ts
apps/web/src/components/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
apps/web/src/components/**/*.{ts,tsx}: When document editing, register editing state withuseEditingStore.getState().startEditing()to prevent UI refreshes, and clean up in return statement
When AI is streaming, register streaming state withuseEditingStore.getState().startStreaming()to prevent UI refreshes, and clean up in return statement
For SWR data fetching with editing protection, useisPaused: () => hasLoadedRef.current && isEditingActive()to allow initial fetch and only pause after, withrevalidateOnFocus: false
Use Zustand for client-side state management as the primary state solution
Use SWR for server state and caching with proper configuration includingrevalidateOnFocus: falsefor editing protection
Use TipTap rich text editor with markdown support for document editing
Use Monaco Editor for code editing features
Use @dnd-kit for drag-and-drop functionality instead of other libraries
Use Tailwind CSS with shadcn/ui components for all UI styling and components
Files:
apps/web/src/components/activity/ActivityGroupItem.tsxapps/web/src/components/activity/ActivityTimeline.tsxapps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsxapps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsxapps/web/src/components/activity/types.tsapps/web/src/components/activity/utils.tsapps/web/src/components/activity/__tests__/utils.test.ts
**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
**/*.ts: React hook files should use camelCase matching the exported hook name (e.g.,useAuth.ts)
Zustand store files should use camelCase withuseprefix (e.g.,useAuthStore.ts)
Files:
apps/web/src/components/activity/types.tsapps/web/src/components/activity/utils.tsapps/web/src/components/activity/__tests__/utils.test.tsapps/web/src/hooks/useDocument.tsapps/web/src/app/api/pages/[pageId]/route.ts
apps/web/src/app/**/route.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 route handlers,paramsin dynamic routes are Promise objects and MUST be awaited before destructuring
UseResponse.json()orNextResponse.json()for returning JSON from route handlers
Get request body usingconst body = await request.json();
Get search parameters usingconst { searchParams } = new URL(request.url);
apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 dynamic routes,paramsare Promise objects and MUST be awaited before destructuring:const { id } = await context.params;
In Route Handlers, get request body withconst body = await request.json();
In Route Handlers, get search parameters withconst { searchParams } = new URL(request.url);
In Route Handlers, return JSON usingResponse.json(data)orNextResponse.json(data)
For permission logic, use centralized functions from@pagespace/lib/permissions:getUserAccessLevel(),canUserEditPage()
Files:
apps/web/src/app/api/pages/[pageId]/route.ts
🧠 Learnings (3)
📚 Learning: 2025-12-18T05:22:42.263Z
Learnt from: 2witstudios
Repo: 2witstudios/PageSpace PR: 96
File: apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarChatTab.tsx:641-657
Timestamp: 2025-12-18T05:22:42.263Z
Learning: In apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarChatTab.tsx, the provider/model selector buttons are intentionally non-functional placeholders in the compact sidebar view. The `hideModelSelector={true}` prop is passed to ChatInput to hide the full ProviderModelSelector. Users are expected to use the full GlobalAssistantView for model selection. A settings link may be added in a future iteration.
Applied to files:
apps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsxapps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to packages/lib/**/*.test.ts : Write unit tests for shared utilities in `packages/lib` with test files named `*.test.ts` alongside source or in `__tests__/` directory
Applied to files:
apps/web/src/components/activity/__tests__/utils.test.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/components/**/*.{ts,tsx} : When document editing, register editing state with `useEditingStore.getState().startEditing()` to prevent UI refreshes, and clean up in return statement
Applied to files:
apps/web/src/hooks/useDocument.ts
🧬 Code graph analysis (3)
apps/web/src/components/activity/ActivityTimeline.tsx (2)
apps/web/src/components/activity/utils.ts (1)
groupConsecutiveActivities(230-293)apps/web/src/components/activity/ActivityGroupItem.tsx (1)
ActivityGroupItem(62-182)
apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx (2)
apps/web/src/components/activity/types.ts (2)
ActivityLog(10-36)ActivityGroup(48-53)apps/web/src/components/activity/utils.ts (1)
groupConsecutiveActivities(230-293)
apps/web/src/components/activity/__tests__/utils.test.ts (2)
apps/web/src/components/activity/types.ts (1)
ActivityLog(10-36)apps/web/src/components/activity/utils.ts (4)
isRollbackOperation(80-82)hasAiConversationId(87-89)isEditSessionGroupable(95-101)groupConsecutiveActivities(230-293)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Unit Tests
🔇 Additional comments (20)
apps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsx (1)
39-40: LGTM! Test data updated to match extended ActivityItem interface.The addition of
aiConversationId: nullandchangeGroupId: nullto the test factory correctly aligns with the expanded ActivityItem interface used throughout the PR for activity grouping functionality.apps/web/src/hooks/useDocument.ts (2)
66-69: LGTM! Session ID implementation correctly supports edit grouping.The stable sessionId generated via
createId()inuseStatecorrectly groups related edits within a single editing session. The lifecycle (resets on component remount when user navigates away) aligns with the intended grouping behavior described in the PR objectives.
85-86: LGTM! ChangeGroupId correctly wired to backend.The
changeGroupId: sessionIdparameter is correctly passed to the patch API to enable activity log grouping. This aligns with the route handler changes inapps/web/src/app/api/pages/[pageId]/route.tsthat accept and propagate this field.apps/web/src/components/activity/ActivityTimeline.tsx (1)
56-86: LGTM! Two-stage rendering correctly implements activity grouping.The updated rendering logic correctly:
- Groups activities by date first (existing behavior preserved)
- Applies consecutive activity grouping within each date group via
groupConsecutiveActivities- Conditionally renders
ActivityItemfor single activities andActivityGroupItemfor grouped activities using discriminated union type checking- Maintains proper key props and context passing
The variable rename from
groupActivitiestodateActivitiesimproves clarity.apps/web/src/components/activity/ActivityGroupItem.tsx (1)
1-181: LGTM! Well-structured grouped activity component.The
ActivityGroupItemcomponent is well-implemented with:
- Clean separation of concerns (helpers for icons/badges, main component logic)
- Correct use of Collapsible UI pattern with accessible triggers
- Proper group-level actions targeting the oldest activity for rollback-to-point
- Individual activity rendering in expanded state with visual hierarchy (left border)
- Appropriate state management for open/closed and dialog visibility
The component correctly integrates with the existing
ActivityItem,RollbackToPointDialog, and activity type system.apps/web/src/app/api/pages/[pageId]/route.ts (2)
43-43: LGTM! Schema correctly extended for activity grouping.The
changeGroupIdfield is correctly added to the patch schema as an optional string with a clear inline comment explaining its purpose for grouping related edits in the activity log.
57-62: LGTM! ChangeGroupId correctly propagated to service layer.The implementation correctly:
- Destructures
changeGroupIdfrom the validated request body- Passes it to
pageService.updatePagevia a context object only when present (avoiding unnecessary object creation)- Aligns with the service layer signature shown in the relevant code snippets
apps/web/src/components/activity/types.ts (2)
26-26: LGTM! ActivityLog correctly extended for edit session grouping.The
changeGroupId: string | nullfield enables tracking of related edits within the same editing session, aligning with the PR's activity grouping objectives.
38-60: LGTM! Activity grouping types are well-designed.The new type declarations provide a clean foundation for activity grouping:
ActivityGroupTypeunion clearly defines the three grouping strategies (rollback, AI stream, edit session)ActivityGroupSummarycaptures necessary display information for group headersActivityGroupandSingleActivityform a proper discriminated union viaActivityDisplayItem- Type structure enables type-safe rendering in
ActivityTimelineand related componentsapps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx (3)
61-62: LGTM! ActivityItem interface correctly extended.The addition of
aiConversationId: string | nullandchangeGroupId: string | nullaligns with the updatedActivityLogtype inapps/web/src/components/activity/types.tsand enables activity grouping functionality.
413-641: LGTM! Grouped rendering implementation is comprehensive and well-structured.The updated rendering logic correctly:
- Differentiates between single and grouped items using discriminated union type checking
- Preserves individual activity actions (Undo this change, Rollback to point) for single items
- Implements collapsible groups with expand/collapse behavior
- Provides group-level "Undo all X changes" action targeting the oldest activity for rollback-to-point
- Renders individual activities within expanded groups with appropriate styling (left border) and individual actions
- Maintains consistent hover states and action menu visibility
The implementation aligns with the PR objectives and provides a good user experience for grouped activities.
324-341: The type casting concern is valid, but address the root cause: type incompatibility.The empty string fallback for
actorEmailis handled safely—getActorDisplayNameuses||which treats empty string as falsy and falls through to'Unknown'. However, the underlying problem is thatActivityLog.actorEmailis typed asstring(non-nullable) whileActivityItem.actorEmailisstring | null. The cast bypasses TypeScript validation.Change
ActivityLog.actorEmailtostring | nullinapps/web/src/components/activity/types.tsto align withActivityItem, then remove the type cast and the?? ''conversion. This eliminates the need for unsafe type coercion and allows the grouping function to work with both types naturally.apps/web/src/components/activity/utils.ts (4)
80-101: LGTM! Grouping predicates correctly implement the PR's prioritization logic.The predicate functions correctly identify:
- Rollback operations by checking
operation === 'rollback'- AI conversation activities by checking for non-null
aiConversationId- Edit session activities with appropriate constraints: non-null
changeGroupId,operation === 'update', and not AI-generatedThe
isEditSessionGroupablepredicate correctly excludes AI-generated updates to prevent conflicts between AI stream and edit session grouping.
106-170: LGTM! Collection functions correctly implement consecutive grouping.The collection functions properly:
collectConsecutiveRollbacks: Groups consecutive rollback operationscollectConsecutiveAiStream: Groups activities by sameaiConversationIdcollectConsecutiveEditSession: Groups activities by samechangeGroupIdANDresourceId(important for per-document session tracking)The logic correctly stops collecting when the grouping condition breaks, ensuring consecutive grouping as specified in the PR objectives.
182-219: LGTM! Group summary generation provides clear, user-friendly labels.The
createGroupSummaryfunction generates appropriate labels for each group type:
- Rollback: "Undo" (singular) or "X rollbacks" (plural)
- AI stream: "AI updated X page(s)" counting unique affected pages
- Edit session: "X edits to [title]" showing the document title
Actor information correctly extracted with fallback chain, and timestamp uses the first activity in the group.
230-293: LGTM! Priority-based grouping algorithm correctly implements the specification.The
groupConsecutiveActivitiesfunction correctly implements the PR's grouping priority (AI > Rollback > Edit session):
- Checks AI conversation grouping first
- Falls back to rollback grouping
- Falls back to edit session grouping
- Emits single activity if no grouping applies
The algorithm only creates groups when 2+ consecutive items match (using
group.length > 1check), otherwise treating them as single activities. This prevents unnecessary grouping UI for isolated activities.apps/web/src/components/activity/__tests__/utils.test.ts (4)
49-111: LGTM!The predicate tests provide comprehensive coverage with clear positive and negative test cases for each function. The
isEditSessionGroupabletests properly validate all three required conditions (update operation, non-null changeGroupId, and non-AI-generated).
134-175: Rollback grouping tests look good.The tests correctly verify that consecutive rollbacks group together while non-consecutive ones remain separate. The type narrowing pattern (lines 146-149) works correctly to satisfy TypeScript's type system after the type assertion on line 145.
262-287: LGTM!The priority tests correctly validate that AI conversation grouping takes precedence over both rollback and edit session grouping, matching the documented priority hierarchy (AI > Rollback > Edit session).
289-314: LGTM!This comprehensive integration test validates the entire grouping algorithm with a realistic mix of activity types. It correctly verifies that the algorithm produces 4 groups in the expected order: AI stream, single activity, rollback group, and edit session group.
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
apps/web/src/components/activity/utils.ts (1)
174-176: Consider consolidatinggetActorDisplayNameto avoid duplication.This function duplicates the logic in
SidebarActivityTab.tsx(lines 76-91). Consider exporting this fromutils.tsand reusing it in the sidebar component to maintain a single source of truth.🔎 Proposed refactor
Export from
utils.ts:-function getActorDisplayName(activity: ActivityLog): string { +export function getActorDisplayName(activity: ActivityLog): string {Then in
SidebarActivityTab.tsx, import and adapt:import { getActorDisplayName as getActorDisplayNameFromLog } from '@/components/activity/utils';apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx (3)
324-341: Unsafe type cast withas unknown as- consider a type-safe adapter.The double cast bypasses TypeScript's type checking. While the current implementation works because the grouping functions only access a subset of fields, this is fragile.
🔎 Proposed refactor using explicit field mapping
Create a type-safe adapter that explicitly maps only the fields needed by the grouping function:
const groupedDisplayItems = useMemo(() => { const asActivityLogs: ActivityLog[] = filteredActivities.map((a) => ({ id: a.id, timestamp: a.timestamp, userId: null, actorEmail: a.actorEmail ?? '', actorDisplayName: a.actorDisplayName, operation: a.operation, resourceType: a.resourceType, resourceId: a.resourceId, resourceTitle: a.resourceTitle, driveId: null, pageId: null, isAiGenerated: a.isAiGenerated, aiProvider: a.aiProvider, aiModel: a.aiModel, aiConversationId: a.aiConversationId, changeGroupId: a.changeGroupId, updatedFields: null, previousValues: null, newValues: null, metadata: a.metadata, rollbackFromActivityId: null, rollbackSourceOperation: a.rollbackSourceOperation, rollbackSourceTimestamp: null, rollbackSourceTitle: null, user: a.user, })); return groupConsecutiveActivities(asActivityLogs); }, [filteredActivities]);Alternatively, consider extracting the grouping-relevant fields into a shared interface that both
ActivityItemandActivityLogextend.
413-418: Potential O(n²) performance with repeatedfind()lookups.Each single item performs a
find()onfilteredActivities, resulting in O(n²) complexity for large lists. SincegroupedDisplayItemsis derived fromfilteredActivities, the activity should always exist, making this lookup redundant.Consider passing the activity directly through the display item or using a Map lookup.
🔎 Proposed optimization
Create a lookup map once:
const activityMap = useMemo( () => new Map(filteredActivities.map((a) => [a.id, a])), [filteredActivities] );Then use:
-const activity = filteredActivities.find((a) => a.id === item.activity.id); -if (!activity) return null; +const activity = activityMap.get(item.activity.id); +if (!activity) return null;
574-582: Silent no-op whenoldestActivityis not found.If
oldestActivityisnull(edge case), clicking "Undo all" does nothing without user feedback. Consider disabling the menu item or showing a toast.🔎 Optional improvement
-<DropdownMenuItem onClick={() => { - if (oldestActivity) { - setSelectedActivityForRollbackToPoint(oldestActivity); - setShowRollbackToPoint(true); - } -}}> +<DropdownMenuItem + disabled={!oldestActivity} + onClick={() => { + if (oldestActivity) { + setSelectedActivityForRollbackToPoint(oldestActivity); + setShowRollbackToPoint(true); + } + }} +>
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/web/src/components/activity/utils.tsapps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Never useanytypes - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase withuseprefix), Zustand stores (camelCase withuseprefix), and React components (PascalCase)
Lint with Next/ESLint as configured inapps/web/eslint.config.mjs
Message content should always use the message parts structure with{ parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from@pagespace/lib/permissions(e.g.,getUserAccessLevel,canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from@pagespace/dbpackage for database access
Use ESM modules throughout the codebase
**/*.{ts,tsx}: Never useanytypes - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting
Files:
apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsxapps/web/src/components/activity/utils.ts
**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
**/*.tsx: React component files should use PascalCase (e.g.,UserProfile.tsx)
Use @dnd-kit for drag-and-drop functionality
Use Zustand for client state management
Use SWR for server state management and caching
Files:
apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
**/*.{ts,tsx,js,jsx,json}
📄 CodeRabbit inference engine (AGENTS.md)
Format code with Prettier
Files:
apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsxapps/web/src/components/activity/utils.ts
**/*.{tsx,css}
📄 CodeRabbit inference engine (AGENTS.md)
Use Tailwind CSS and shadcn/ui components for styling and UI
Files:
apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
apps/web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
apps/web/src/**/*.{ts,tsx}: Use message parts structure for message content:{ parts: [{ type: 'text', text: '...' }] }
For database access, always use Drizzle client from@pagespace/db:import { db, pages } from '@pagespace/db';
Use centralized Drizzle ORM with PostgreSQL for all database operations - no direct SQL or other ORMs
Use Socket.IO for real-time collaboration features - imported from the realtime service at port 3001
Use Vercel AI SDK with async/await for all AI operations and streaming
Use Next.js 15 App Router and TypeScript for all routes and components
Files:
apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsxapps/web/src/components/activity/utils.ts
apps/web/src/components/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
apps/web/src/components/**/*.{ts,tsx}: When document editing, register editing state withuseEditingStore.getState().startEditing()to prevent UI refreshes, and clean up in return statement
When AI is streaming, register streaming state withuseEditingStore.getState().startStreaming()to prevent UI refreshes, and clean up in return statement
For SWR data fetching with editing protection, useisPaused: () => hasLoadedRef.current && isEditingActive()to allow initial fetch and only pause after, withrevalidateOnFocus: false
Use Zustand for client-side state management as the primary state solution
Use SWR for server state and caching with proper configuration includingrevalidateOnFocus: falsefor editing protection
Use TipTap rich text editor with markdown support for document editing
Use Monaco Editor for code editing features
Use @dnd-kit for drag-and-drop functionality instead of other libraries
Use Tailwind CSS with shadcn/ui components for all UI styling and components
Files:
apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsxapps/web/src/components/activity/utils.ts
**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
**/*.ts: React hook files should use camelCase matching the exported hook name (e.g.,useAuth.ts)
Zustand store files should use camelCase withuseprefix (e.g.,useAuthStore.ts)
Files:
apps/web/src/components/activity/utils.ts
🧠 Learnings (1)
📚 Learning: 2025-12-18T05:22:42.263Z
Learnt from: 2witstudios
Repo: 2witstudios/PageSpace PR: 96
File: apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarChatTab.tsx:641-657
Timestamp: 2025-12-18T05:22:42.263Z
Learning: In apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarChatTab.tsx, the provider/model selector buttons are intentionally non-functional placeholders in the compact sidebar view. The `hideModelSelector={true}` prop is passed to ChatInput to hide the full ProviderModelSelector. Users are expected to use the full GlobalAssistantView for model selection. A settings link may be added in a future iteration.
Applied to files:
apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
🧬 Code graph analysis (2)
apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx (2)
apps/web/src/components/activity/types.ts (2)
ActivityLog(10-36)ActivityGroup(48-53)apps/web/src/components/activity/utils.ts (1)
groupConsecutiveActivities(229-292)
apps/web/src/components/activity/utils.ts (2)
apps/web/src/components/activity/types.ts (4)
ActivityLog(10-36)ActivityGroupType(39-39)ActivityGroupSummary(41-46)ActivityDisplayItem(60-60)apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx (1)
getActorDisplayName(77-92)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Unit Tests
🔇 Additional comments (9)
apps/web/src/components/activity/utils.ts (5)
2-7: LGTM!Clean type imports aligned with the new grouping feature requirements.
76-100: LGTM!Clear, single-purpose predicates with appropriate null checks. The
isEditSessionGroupablecorrectly excludes AI-generated edits to avoid double-grouping.
105-169: LGTM!Collector functions correctly implement consecutive grouping with proper early returns for null IDs and clean break conditions.
181-218: LGTM!The function correctly builds summaries for each group type. The array access at Line 185 is safe since callers only invoke this when
group.length > 1.
229-292: LGTM!Well-structured grouping algorithm with clear priority order. The fall-through behavior for single-item groups correctly ensures standalone activities render individually. The
isEditSessionGroupablepredicate prevents AI activities from being double-grouped.apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx (4)
29-40: LGTM!Appropriate imports for the new collapsible grouping UI feature.
61-62: LGTM!New fields align with
ActivityLoginterface and support the grouping feature.
343-356: LGTM!Correct immutable Set update pattern with proper memoization.
404-404: LGTM!Correctly checks
groupedDisplayItemsfor the empty state.
Use Tailwind's group/item naming to prevent the nested activity item's hover from colliding with the parent group's hover scope. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
aiConversationIdshowing "AI updated X pages"changeGroupIdper document showing "X edits to [title]"changeGroupIdgenerated inuseDocumenthook, passed through to activity loggerTest plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests
Other
✏️ Tip: You can customize this high-level summary in your review settings.