Skip to content

feat: add smart activity grouping for sidebar and dashboard - #143

Merged
2witstudios merged 3 commits into
masterfrom
feat/activity-grouping
Dec 28, 2025
Merged

2witstudios merged 3 commits into
masterfrom
feat/activity-grouping

Conversation

@2witstudios

@2witstudios 2witstudios commented Dec 28, 2025 •

Copy link
Copy Markdown
Owner

Summary

  • Consecutive rollbacks grouped as "X rollbacks" with accordion expand/collapse
  • AI chat streams grouped by aiConversationId showing "AI updated X pages"
  • User edit sessions grouped by changeGroupId per document showing "X edits to [title]"
  • Backend session tracking: changeGroupId generated in useDocument hook, passed through to activity logger
  • Frontend-only grouping: Server returns flat list, UI groups with priority: AI > Rollback > Edit session
  • Group actions: Both group-level undo (uses rollback-to-point API) AND individual item undo when expanded

Test plan

  • Unit tests for grouping algorithm (22 tests passing)
  • SidebarActivityTab tests still passing (7 tests)
  • TypeScript type check passes
  • Manual test: Create multiple consecutive rollbacks → verify they group
  • Manual test: Use AI to update multiple pages → verify AI stream groups
  • Manual test: Make multiple edits to same document → verify edit session groups
  • Manual test: Expand group → verify individual items show with undo buttons
  • Manual test: Use group-level "Undo all" → verify rollback-to-point dialog appears

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Activity log now groups related edits (edit sessions, AI streams, rollbacks) into collapsible sections with badges and counts.
    • Group-level Undo to rollback an entire activity group.
    • Edit saves include a session identifier so consecutive edits are grouped.
  • Tests

    • Added comprehensive unit tests for activity grouping and grouping predicates.
  • Other

    • Activity items now include optional grouping metadata to enable the new behaviors.

✏️ Tip: You can customize this high-level summary in your review settings.

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>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Dec 28, 2025 •

Copy link
Copy Markdown
Contributor

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between 2ceae14 and cc2fc9c.

📒 Files selected for processing (1)
  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
API route
apps/web/src/app/api/pages/[pageId]/route.ts
PATCH schema accepts optional changeGroupId and passes it as context to pageService.updatePage.
Document hook
apps/web/src/hooks/useDocument.ts
Adds stable sessionId (createId) and sends it as changeGroupId in patch requests.
Activity types
apps/web/src/components/activity/types.ts
Adds changeGroupId to ActivityLog and new types: ActivityGroupType, ActivityGroupSummary, ActivityGroup, SingleActivity, ActivityDisplayItem.
Activity utils & tests
apps/web/src/components/activity/utils.ts, apps/web/src/components/activity/__tests__/utils.test.ts
New grouping predicates and orchestrator: isRollbackOperation, hasAiConversationId, isEditSessionGroupable, groupConsecutiveActivities with comprehensive tests covering rollback/AI/edit-session/mixed scenarios.
Activity UI components
apps/web/src/components/activity/ActivityGroupItem.tsx, apps/web/src/components/activity/ActivityTimeline.tsx
New ActivityGroupItem component; ActivityTimeline now maps through groupConsecutiveActivities and renders grouped or single items.
Sidebar integration & tests
apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx, apps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsx
Sidebar now computes grouped display items, adds per-group expand/collapse and undo action; ActivityItem shape extended with aiConversationId and changeGroupId (tests updated).

Sequence Diagram

sequenceDiagram
    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)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 A session id I lightly bring,
Hopping edits into one spring,
Rollbacks, streams, and edits nest—
Collapsed and tidy, folded best.
The timeline twitches, full of cheer! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.22% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add smart activity grouping for sidebar and dashboard' accurately summarizes the main change—implementing activity grouping across the UI.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) and new 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

📥 Commits

Reviewing files that changed from the base of the PR and between b6d4113 and 229d2d6.

📒 Files selected for processing (9)
  • apps/web/src/app/api/pages/[pageId]/route.ts
  • apps/web/src/components/activity/ActivityGroupItem.tsx
  • apps/web/src/components/activity/ActivityTimeline.tsx
  • apps/web/src/components/activity/__tests__/utils.test.ts
  • apps/web/src/components/activity/types.ts
  • apps/web/src/components/activity/utils.ts
  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
  • apps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsx
  • apps/web/src/hooks/useDocument.ts
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - 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 with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/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/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • apps/web/src/components/activity/ActivityGroupItem.tsx
  • apps/web/src/components/activity/ActivityTimeline.tsx
  • apps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsx
  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
  • apps/web/src/components/activity/types.ts
  • apps/web/src/components/activity/utils.ts
  • apps/web/src/components/activity/__tests__/utils.test.ts
  • apps/web/src/hooks/useDocument.ts
  • apps/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.tsx
  • apps/web/src/components/activity/ActivityTimeline.tsx
  • apps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsx
  • 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/activity/ActivityGroupItem.tsx
  • apps/web/src/components/activity/ActivityTimeline.tsx
  • apps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsx
  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
  • apps/web/src/components/activity/types.ts
  • apps/web/src/components/activity/utils.ts
  • apps/web/src/components/activity/__tests__/utils.test.ts
  • apps/web/src/hooks/useDocument.ts
  • apps/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.tsx
  • apps/web/src/components/activity/ActivityTimeline.tsx
  • apps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsx
  • 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/activity/ActivityGroupItem.tsx
  • apps/web/src/components/activity/ActivityTimeline.tsx
  • apps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsx
  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
  • apps/web/src/components/activity/types.ts
  • apps/web/src/components/activity/utils.ts
  • apps/web/src/components/activity/__tests__/utils.test.ts
  • apps/web/src/hooks/useDocument.ts
  • apps/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 with useEditingStore.getState().startEditing() to prevent UI refreshes, and clean up in return statement
When AI is streaming, register streaming state with useEditingStore.getState().startStreaming() to prevent UI refreshes, and clean up in return statement
For SWR data fetching with editing protection, use isPaused: () => hasLoadedRef.current && isEditingActive() to allow initial fetch and only pause after, with revalidateOnFocus: false
Use Zustand for client-side state management as the primary state solution
Use SWR for server state and caching with proper configuration including revalidateOnFocus: false for 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.tsx
  • apps/web/src/components/activity/ActivityTimeline.tsx
  • apps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsx
  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
  • apps/web/src/components/activity/types.ts
  • apps/web/src/components/activity/utils.ts
  • apps/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 with use prefix (e.g., useAuthStore.ts)

Files:

  • apps/web/src/components/activity/types.ts
  • apps/web/src/components/activity/utils.ts
  • apps/web/src/components/activity/__tests__/utils.test.ts
  • apps/web/src/hooks/useDocument.ts
  • apps/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, params in dynamic routes are Promise objects and MUST be awaited before destructuring
Use Response.json() or NextResponse.json() for returning JSON from route handlers
Get request body using const body = await request.json();
Get search parameters using const { searchParams } = new URL(request.url);

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 dynamic routes, params are Promise objects and MUST be awaited before destructuring: const { id } = await context.params;
In Route Handlers, get request body with const body = await request.json();
In Route Handlers, get search parameters with const { searchParams } = new URL(request.url);
In Route Handlers, return JSON using Response.json(data) or NextResponse.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.tsx
  • apps/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: null and changeGroupId: null to 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() in useState correctly 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: sessionId parameter is correctly passed to the patch API to enable activity log grouping. This aligns with the route handler changes in apps/web/src/app/api/pages/[pageId]/route.ts that 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 ActivityItem for single activities and ActivityGroupItem for grouped activities using discriminated union type checking
  • Maintains proper key props and context passing

The variable rename from groupActivities to dateActivities improves clarity.

apps/web/src/components/activity/ActivityGroupItem.tsx (1)

1-181: LGTM! Well-structured grouped activity component.

The ActivityGroupItem component 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 changeGroupId field 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 changeGroupId from the validated request body
  • Passes it to pageService.updatePage via 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 | null field 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:

  • ActivityGroupType union clearly defines the three grouping strategies (rollback, AI stream, edit session)
  • ActivityGroupSummary captures necessary display information for group headers
  • ActivityGroup and SingleActivity form a proper discriminated union via ActivityDisplayItem
  • Type structure enables type-safe rendering in ActivityTimeline and related components
apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx (3)

61-62: LGTM! ActivityItem interface correctly extended.

The addition of aiConversationId: string | null and changeGroupId: string | null aligns with the updated ActivityLog type in apps/web/src/components/activity/types.ts and 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 actorEmail is handled safely—getActorDisplayName uses || which treats empty string as falsy and falls through to 'Unknown'. However, the underlying problem is that ActivityLog.actorEmail is typed as string (non-nullable) while ActivityItem.actorEmail is string | null. The cast bypasses TypeScript validation.

Change ActivityLog.actorEmail to string | null in apps/web/src/components/activity/types.ts to align with ActivityItem, 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-generated

The isEditSessionGroupable predicate 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 operations
  • collectConsecutiveAiStream: Groups activities by same aiConversationId
  • collectConsecutiveEditSession: Groups activities by same changeGroupId AND resourceId (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 createGroupSummary function 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 groupConsecutiveActivities function correctly implements the PR's grouping priority (AI > Rollback > Edit session):

  1. Checks AI conversation grouping first
  2. Falls back to rollback grouping
  3. Falls back to edit session grouping
  4. Emits single activity if no grouping applies

The algorithm only creates groups when 2+ consecutive items match (using group.length > 1 check), 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 isEditSessionGroupable tests 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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
apps/web/src/components/activity/utils.ts (1)

174-176: Consider consolidating getActorDisplayName to avoid duplication.

This function duplicates the logic in SidebarActivityTab.tsx (lines 76-91). Consider exporting this from utils.ts and 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 with as 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 ActivityItem and ActivityLog extend.


413-418: Potential O(n²) performance with repeated find() lookups.

Each single item performs a find() on filteredActivities, resulting in O(n²) complexity for large lists. Since groupedDisplayItems is derived from filteredActivities, 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 when oldestActivity is not found.

If oldestActivity is null (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

📥 Commits

Reviewing files that changed from the base of the PR and between 229d2d6 and 2ceae14.

📒 Files selected for processing (2)
  • apps/web/src/components/activity/utils.ts
  • apps/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 use any types - 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 with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/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/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - 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.tsx
  • apps/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.tsx
  • apps/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.tsx
  • apps/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 with useEditingStore.getState().startEditing() to prevent UI refreshes, and clean up in return statement
When AI is streaming, register streaming state with useEditingStore.getState().startStreaming() to prevent UI refreshes, and clean up in return statement
For SWR data fetching with editing protection, use isPaused: () => hasLoadedRef.current && isEditingActive() to allow initial fetch and only pause after, with revalidateOnFocus: false
Use Zustand for client-side state management as the primary state solution
Use SWR for server state and caching with proper configuration including revalidateOnFocus: false for 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.tsx
  • apps/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 with use prefix (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 isEditSessionGroupable correctly 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 isEditSessionGroupable predicate 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 ActivityLog interface and support the grouping feature.


343-356: LGTM!

Correct immutable Set update pattern with proper memoization.


404-404: LGTM!

Correctly checks groupedDisplayItems for 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>
@2witstudios
2witstudios merged commit 088849d into master Dec 28, 2025
2 of 3 checks passed
@2witstudios
2witstudios deleted the feat/activity-grouping branch January 29, 2026 02:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant