Skip to content

fix: rollback of rollback should restore trashed pages - #147

Merged
2witstudios merged 16 commits into
masterfrom
fix/rollback-of-rollback-restore
Dec 29, 2025
Merged

2witstudios merged 16 commits into
masterfrom
fix/rollback-of-rollback-restore

Conversation

@2witstudios

@2witstudios 2witstudios commented Dec 28, 2025 •

Copy link
Copy Markdown
Owner

Summary

  • When undoing a rollback of a create operation, the system was incorrectly trying to trash the page again instead of restoring it

Problem

Scenario:

  1. AI creates a page
  2. User undos (rollback) → page is trashed ✓
  3. User undos the undo (rollback of rollback) → should restore page from trash
  4. Bug: System incorrectly tried to trash it again

Root Cause

const shouldBeTrashed = action === 'rollback';

This didn't account for rollingBackRollback. When rolling back a rollback:

  • action is still 'rollback'
  • But rollingBackRollback is true
  • So we should RESTORE (shouldBeTrashed = false), not trash

Solution

const shouldBeTrashed = action === 'rollback' && !rollingBackRollback;

Test plan

  • TypeScript checks pass
  • Rollback service tests pass (29 tests)
  • Manual test: create page → undo → undo again (should restore from trash)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Real-time activity broadcasting and live activity updates in the sidebar; server-issued IDs for AI responses to fix post-streaming undo.
  • Bug Fixes

    • Rollback/undo consistently uses isTrashed semantics and returns no-op when already trashed.
    • Prevents undo-after-streaming inconsistencies by aligning server/client message IDs.
  • Chores

    • Preview/execute flows honor undo-groups and filter no-op activities; conflict logic refined with clearer internal vs external handling and logging.
  • UI

    • Dialogs show full, scrollable lists for warnings, conflicts, and pending undo items.

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

When undoing a rollback of a create operation, the system was
incorrectly trying to trash the page again instead of restoring it.

The bug was that `shouldBeTrashed = action === 'rollback'` didn't
account for `rollingBackRollback`. When rolling back a rollback:
- action is still 'rollback'
- but rollingBackRollback is true
- so we should RESTORE (shouldBeTrashed = false), not trash

Fixed for both pages and drives.

🤖 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
📝 Walkthrough

Walkthrough

Thread undo-group activity IDs through preview and execute rollback flows; reorganize conflict detection and no-op handling (including AI-undo cases); standardize page-create rollback public shape to isTrashed; add activity broadcast/socket plumbing and client socket hook; switch AI streaming to use a server-generated assistant message ID; replace truncated UI lists with scrollable full lists.

Changes

Cohort / File(s) Summary
Rollback service (preview & execute)
apps/web/src/services/api/rollback-service.ts
Thread undoGroupActivityIds into preview/execute, skip conflict checks for create ops and internal undo-group activities, add internal vs external conflict detection/logging, adjust no-op detection, unify page-create rollback responses to isTrashed, make pageMutationMeta optional in PageChangeResult.
Rollback-to-point orchestration
apps/web/src/services/api/rollback-to-point-service.ts
Collect/preserve undoGroupActivityIds, pass into per-activity previewRollback/executeRollback, filter preview items with isNoOp from execution, maintain transactional execution.
AI undo preview
apps/web/src/services/api/ai-undo-service.ts
Filter out AI-generated activities with isNoOp during preview (debug-log skipped items), reducing actionable preview list.
AI streaming — server message ID
apps/web/src/app/api/ai/chat/route.ts
apps/web/src/app/api/ai/global/[id]/messages/route.ts
Server now generates serverAssistantMessageId and returns it at stream start; use that ID when persisting AI responses so client/server IDs remain aligned.
UI — Rollback and AI-undo dialogs
apps/web/src/components/activity/RollbackToPointDialog.tsx
apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx
Replace truncated lists with full mapped lists inside scrollable containers (max-height), remove “+ N more” badges, adjust padding/layout for scroll wrappers.
Activity socket hook & integration
apps/web/src/hooks/useActivitySocket.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
Add useActivitySocket hook (drive/page context) with join/leave lifecycle, debounced onActivityLogged callback; SidebarActivityTab wires it to reload activities; tests mock router and socket.
Instrumentation & broadcast from web app
apps/web/src/instrumentation.ts
apps/web/src/lib/websocket/socket-utils.ts
Add register() to wire activity broadcast hook; introduce ActivityOperation/ActivityEventPayload and broadcastActivityEvent with per-context debouncing (500ms) posting /api/broadcast.
Realtime server handlers
apps/realtime/src/index.ts
Add socket handlers: join_activity_drive, join_activity_page, leave_activity_drive, leave_activity_page with access checks and logging.
Activity logging library
packages/lib/src/monitoring/activity-logger.ts
Add activity broadcast hook API (setActivityBroadcastHook) and invoke it (fire-and-forget) after activity inserts/logs with structured payload.
Activity utils (grouping)
apps/web/src/components/activity/utils.ts
Tighten collectConsecutiveRollbacks to group only when consecutive rollbacks share the first activity's changeGroupId.

Sequence Diagram(s)

sequenceDiagram
  participant UI as User/UI
  participant RTP as rollback-to-point-service
  participant RS as rollback-service
  participant AI as ai-undo-service
  participant DB as DataStore
  participant SocketClient as web/socket-utils
  participant RT as realtime server

  UI->>RTP: request previewRollbackToPoint
  RTP->>RS: previewRollback(activity, { undoGroupActivityIds? })
  RS-->>RTP: preview (activitiesAffected, isNoOp, conflicts...)
  RTP->>AI: filter AI-generated no-ops (skip isNoOp)
  RTP->>RTP: derive/preserve undoGroupActivityIds
  RTP->>RS: executeRollback(activity, { undoGroupActivityIds })
  alt create op OR internal undo-group match
    RS-->>RTP: treat as no-op / skip conflict gating
  else potential conflict
    RS->>DB: check external modifications (ignore undoGroupActivityIds)
    DB-->>RS: externalMods? (yes/no)
    alt externalMods == yes
      RS-->>RTP: report blocking external conflicts
    else
      RS-->>RTP: clear internal-only conflicts -> proceed
    end
  end
  RS->>DB: apply change (e.g., trash page -> restoredValues { isTrashed: true })
  DB-->>RS: success
  RS->>SocketClient: broadcastActivityEvent(payload) (debounced)
  SocketClient->>RT: POST /api/broadcast -> emits activity:logged
  RT-->>SocketClient: activity:logged
  SocketClient->>UI: onActivityLogged (debounced) -> UI reloads activities
  RS-->>RTP: execution result
  RTP-->>UI: final result / summaries
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hop through diffs with whiskers bright,
threading undo-groups by moonlit night.
I skip the no-ops, tidy conflicts’ mess,
stamp isTrashed where creations rest. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main bug fix: addressing rollback-of-rollback behavior for trashed pages. It directly corresponds to the core change across rollback-service.ts files.
Docstring Coverage ✅ Passed Docstring coverage is 89.47% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/rollback-of-rollback-restore

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a17672 and 6d7251a.

📒 Files selected for processing (1)
  • apps/web/src/hooks/useActivitySocket.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/hooks/useActivitySocket.ts

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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.

The restoredValues was using `trashed` and `pageId` fields, but
currentValues uses `isTrashed`. This field name mismatch caused
false conflict detection when undoing a rollback.

🤖 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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/services/api/rollback-service.ts (1)

1871-1871: Critical: Inconsistent field name - same issue as pages but not fixed for drives.

Line 1871 returns { trashed: true, ... } but drive currentValues at lines 813-818 use isTrashed. This is the same field name mismatch that was fixed for pages at line 1765.

This inconsistency will cause false conflict detection when getConflictFields compares restoredValues against currentValues, as the key names don't match.

🔎 Proposed fix to align with page pattern
-    return { trashed: true, driveId: activity.driveId, pagesTrashed: true };
+    return { isTrashed: true };
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c146d1d and 14fa967.

📒 Files selected for processing (1)
  • apps/web/src/services/api/rollback-service.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/services/api/rollback-service.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/services/api/rollback-service.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/services/api/rollback-service.ts
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/services/api/rollback-service.ts
⏰ 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 (3)
apps/web/src/services/api/rollback-service.ts (3)

741-742: LGTM! Core bug fix correctly implemented.

The updated logic properly handles rollback-of-rollback scenarios for page creation. When rollingBackRollback is true, the page will be restored rather than trashed again.


822-823: LGTM! Drive logic matches page fix.

Consistent application of the rollback-of-rollback logic for drives.


1765-1765: LGTM! Field name aligned with database schema.

Correctly changed from trashed to isTrashed to match the database field name and prevent false conflict detection when comparing restoredValues with currentValues.

When undoing an AI conversation, skip conflict detection for pages since
all changes are from the same conversation - there can't be external
conflicts by definition. This is simpler and more robust than the
previous approach of querying for external modifications.

- 'create' operations: skip conflict check (just trashing)
- AI undo (undoGroupActivityIds provided): skip conflict check
- Regular rollback: normal conflict detection

🤖 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

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14fa967 and 071f393.

📒 Files selected for processing (1)
  • apps/web/src/services/api/rollback-service.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/services/api/rollback-service.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/services/api/rollback-service.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/services/api/rollback-service.ts
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/services/api/rollback-service.ts
⏰ 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). (2)
  • GitHub Check: Unit Tests
  • GitHub Check: Lint & TypeScript Check
🔇 Additional comments (3)
apps/web/src/services/api/rollback-service.ts (3)

741-742: LGTM! Correct fix for rollback-of-rollback behavior.

The logic now correctly handles the rollback-of-rollback scenario for page creation:

  • Regular rollback of create: shouldBeTrashed = true (trash the page)
  • Rollback of rollback of create: shouldBeTrashed = false (restore the page)

This directly addresses the bug described in the PR objectives.


842-843: LGTM! Consistent fix applied for drives.

The same rollback-of-rollback fix is correctly applied for drive creation, ensuring consistent behavior between pages and drives.


1785-1785: LGTM! Correct field name for pages schema.

Changed from trashed: true to isTrashed: true to match the pages table schema. This ensures consistency with the database field naming.

Comment thread apps/web/src/services/api/rollback-service.ts
Apply the same fix to all 7 resource types (page, drive, member,
permission, role, agent, message) - skip conflict detection entirely
when undoGroupActivityIds is provided since all changes are from
the same AI conversation by definition.

This removes ~130 lines of complex external-modifications queries
that weren't working correctly, replacing with simple skip logic.

🤖 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: 0

♻️ Duplicate comments (1)
apps/web/src/services/api/rollback-service.ts (1)

752-798: Critical logic error: unreachable external modifications check for page conflicts.

The outer condition at line 755 requires undoGroupActivityIds.length === 0, but the inner condition at line 769 requires undoGroupActivityIds.length > 0. These conditions are mutually exclusive, making the external modifications check (lines 770-796) unreachable.

Impact: The AI undo feature cannot distinguish internal vs external conflicts for pages, contradicting the PR's stated intent.

Fix: Remove undoGroupActivityIds.length === 0 from line 755, keeping only the effectiveOperation !== 'create' check. This mirrors the comment from the previous review.

Based on past review comments indicating this was already identified.

🧹 Nitpick comments (1)
apps/web/src/services/api/rollback-service.ts (1)

853-856: Consider adding external modifications check for consistency.

The drive conflict detection uses a simpler pattern (skip entirely when undoGroupActivityIds provided) compared to the page logic's intent to distinguish internal vs external conflicts. While the page logic has a critical bug making its check unreachable, the intended design suggests drives (and other resource types) might also benefit from the external modifications check.

Recommendation: Once the page conflict detection bug is fixed, consider whether drives, members, permissions, roles, agents, and messages should also implement the internal vs external conflict distinction.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 071f393 and 3986fe0.

📒 Files selected for processing (1)
  • apps/web/src/services/api/rollback-service.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/services/api/rollback-service.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/services/api/rollback-service.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/services/api/rollback-service.ts
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/services/api/rollback-service.ts
🧬 Code graph analysis (1)
apps/web/src/services/api/rollback-service.ts (3)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
packages/db/src/index.ts (6)
  • db (20-20)
  • and (8-8)
  • eq (8-8)
  • gt (9-9)
  • not (8-8)
  • inArray (8-8)
packages/db/src/schema/monitoring.ts (1)
  • activityLogs (430-502)
⏰ 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). (2)
  • GitHub Check: Unit Tests
  • GitHub Check: Lint & TypeScript Check
🔇 Additional comments (4)
apps/web/src/services/api/rollback-service.ts (4)

739-750: LGTM! Rollback-of-rollback logic correctly restores pages.

The logic properly handles the case where rolling back a rollback of a create operation should restore (not trash) the page. The condition shouldBeTrashed = action === 'rollback' && !rollingBackRollback ensures that:

  • Regular rollback of create: trashes the page
  • Rollback of rollback (redo): restores the page

840-851: LGTM! Drive rollback-of-rollback logic is consistent with pages.

The logic correctly implements the same behavior as pages, ensuring that rolling back a rollback of a drive create operation will restore (not trash) the drive.


955-958: Conflict detection pattern is consistent across resource types.

The conflict detection for members, permissions, roles, agents, and messages consistently skips checks when undoGroupActivityIds is provided. This implements the AI undo behavior where changes from the same conversation are not treated as conflicts.

Also applies to: 1060-1063, 1103-1106, 1189-1192, 1245-1248, 1292-1295


1676-1676: Bug fix: correct property name aligns with database schema.

The change from trashed: true to isTrashed: true correctly uses the actual database schema field name, ensuring consistency with the rest of the codebase.

2witstudios and others added 2 commits December 28, 2025 22:42
When undoing an AI conversation, individual operations may appear as
no-ops (e.g., "Already at this version") but the resource will be
affected by other operations in the same undo group (e.g., trashed
by a create rollback).

Skip no-op detection when undoGroupActivityIds is provided to avoid
misleading "Cannot undo" warnings.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Pass undoGroupActivityIds to previewRollback and executeRollback in
rollback-to-point service, matching the AI undo behavior. This skips
conflict detection and no-op warnings for activities within the same
rollback 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: 0

♻️ Duplicate comments (1)
apps/web/src/services/api/rollback-service.ts (1)

752-798: Critical: Unreachable external modifications check for pages (duplicate issue).

The outer condition at line 755 requires undoGroupActivityIds.length === 0, but the inner condition at line 769 requires undoGroupActivityIds.length > 0. These are mutually exclusive, making the external modifications check (lines 770-796) unreachable.

Impact: AI undo's ability to distinguish internal vs external conflicts for pages will never work. The drive conflict detection logic (lines 853-856) correctly handles this by not having the outer guard.

🔎 Proposed fix (from previous review)
-    // For 'create' operations, skip conflict check - we're just trashing the page
-    // For AI undo (undoGroupActivityIds provided), skip conflict check since all
-    // changes are from the same conversation by definition - no "external" conflicts
-    if (effectiveOperation !== 'create' && undoGroupActivityIds.length === 0) {
+    // For 'create' operations, skip conflict check - we're just trashing the page
+    if (effectiveOperation !== 'create') {
       conflictFields = getConflictFields(activity.newValues, currentValues);

       // If there's a conflict but we have undo group context, check if the modifications
       // came from other activities in the same undo group (internal conflict vs external)
       if (conflictFields.length > 0) {
         loggers.api.debug('[Rollback:Preview] Page conflict detected', {
           activityId: activity.id,
           resourceId: activity.resourceId,
           timestamp: activity.timestamp?.toISOString(),
           conflictFields,
           undoGroupSize: undoGroupActivityIds.length,
         });

         if (undoGroupActivityIds.length > 0) {
           const externalModifications = await db
             .select({ id: activityLogs.id })
             .from(activityLogs)
             .where(
               and(
                 eq(activityLogs.resourceId, activity.resourceId),
                 eq(activityLogs.resourceType, 'page'),
                 gt(activityLogs.timestamp, activity.timestamp),
                 not(inArray(activityLogs.id, undoGroupActivityIds))
               )
             )
             .limit(1);

           loggers.api.debug('[Rollback:Preview] External modifications check', {
             activityId: activity.id,
             externalModificationsFound: externalModifications.length,
           });

           if (externalModifications.length === 0) {
             // All modifications came from activities in the undo group - not a real conflict
             loggers.api.debug('[Rollback:Preview] Page conflict is internal to undo group, ignoring', {
               activityId: activity.id,
               conflictFields,
             });
             conflictFields = [];
           }
         }
       }
     }
🧹 Nitpick comments (1)
apps/web/src/services/api/rollback-service.ts (1)

853-856: Consider consistency: Drive conflict detection differs from pages.

Drives (and other resources) use a simpler pattern: skip all conflict detection when undoGroupActivityIds.length > 0. Pages attempt to distinguish internal vs external conflicts with an external modifications check, but the implementation is broken (see lines 752-798).

Consider whether pages should follow this simpler, working pattern, or if the external modifications check should be properly fixed and potentially extended to other resources.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3986fe0 and c5739db.

📒 Files selected for processing (1)
  • apps/web/src/services/api/rollback-service.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/services/api/rollback-service.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/services/api/rollback-service.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/services/api/rollback-service.ts
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/services/api/rollback-service.ts
🧬 Code graph analysis (1)
apps/web/src/services/api/rollback-service.ts (1)
packages/db/src/schema/monitoring.ts (1)
  • activityLogs (430-502)
⏰ 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 (5)
apps/web/src/services/api/rollback-service.ts (5)

741-742: LGTM: Correct fix for rollback-of-rollback restoring pages.

The shouldBeTrashed computation now correctly accounts for rollingBackRollback, ensuring that rolling back a rollback of a create operation restores the page instead of trashing it again.


842-843: LGTM: Consistent fix for drive rollback-of-rollback.

The shouldBeTrashed logic matches the page fix, correctly restoring drives when rolling back a rollback of create.


955-958: LGTM: Consistent AI undo conflict detection across resources.

All non-page resources (member, permission, role, agent, message) correctly skip conflict detection when undoGroupActivityIds.length > 0, using a simple, consistent pattern.

Also applies to: 1060-1063, 1103-1106, 1189-1192, 1245-1248, 1292-1295


1678-1678: LGTM: Correct field name for page trash state.

Using isTrashed instead of trashed aligns with the pages schema field name.


1318-1320: LGTM: No-op detection correctly skipped for AI undo operations.

The logic correctly skips no-op detection when undoGroupActivityIds is provided, preventing false "Cannot undo" warnings for operations that may appear as no-ops individually but are part of a larger undo group.

2witstudios and others added 4 commits December 28, 2025 22:52
Replace truncated warning lists that showed "...and X more" with
scrollable lists using ScrollArea. Users need to see exactly what
they're rolling back - truncation is unacceptable for transparency.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Apply the same fix to UndoAiChangesDialog - replace truncated lists
with scrollable ScrollArea components for warnings, conflicts, and
activities. Users need full transparency on what they're undoing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Skip create no-op check for AI undo operations - old activities from
  previous sessions may reference already-trashed pages that should be
  silently ignored (same aiConversationId spans all tool calls in thread)
- Replace ScrollArea with native overflow-y-auto for warning lists -
  ScrollArea with max-h classes causes overflow instead of scrolling
- Remove unused ScrollArea import from UndoAiChangesDialog

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add guard in execute path to skip rollback of create operations when
the page is already trashed. This prevents:
- Wasted database updates
- Unnecessary revision increments on the page and its children
- Confusing activity log entries for no-op operations

The check happens early in executeRollbackPage before any mutations,
making the operation truly idempotent for old activities from previous
sessions that share the same aiConversationId.

🤖 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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/services/api/rollback-service.ts (1)

739-752: Fix inverted logic: condition and comment contradict.

Line 739's comment says "Skip create no-op check for AI undo", but line 741 checks undoGroupActivityIds.length === 0, which means the check runs only when NOT in AI undo mode (empty array = no undo group). Either:

  • The comment should say "Skip create no-op check when NOT in AI undo", or
  • The condition should be undoGroupActivityIds.length > 0

Line 744's shouldBeTrashed logic correctly fixes the rollback-of-rollback bug.

🔎 Proposed fix for comment clarity
-    // Skip create no-op check for AI undo - old activities from previous sessions
-    // may reference already-trashed pages that should be silently ignored
-    if (effectiveOperation === 'create' && undoGroupActivityIds.length === 0) {
+    // Only check for create no-ops when NOT in AI undo mode - during AI undo,
+    // old activities from previous sessions may reference already-trashed pages that should be silently ignored
+    if (effectiveOperation === 'create' && undoGroupActivityIds.length === 0) {
♻️ Duplicate comments (1)
apps/web/src/services/api/rollback-service.ts (1)

754-800: Critical logic error: unreachable code in AI undo conflict detection.

The outer condition at line 757 checks undoGroupActivityIds.length === 0, but the inner condition at line 771 checks undoGroupActivityIds.length > 0. These are mutually exclusive, making the external modifications check (lines 772-788) unreachable.

Impact: The AI undo feature's ability to distinguish internal vs external conflicts for pages will never work, contradicting the PR's stated intent.

Inconsistency: The drive conflict detection logic (lines 855-858) correctly handles this by not having the outer undoGroupActivityIds.length === 0 guard.

🔎 Proposed fix
-    // For 'create' operations, skip conflict check - we're just trashing the page
-    // For AI undo (undoGroupActivityIds provided), skip conflict check since all
-    // changes are from the same conversation by definition - no "external" conflicts
-    if (effectiveOperation !== 'create' && undoGroupActivityIds.length === 0) {
+    // For 'create' operations, skip conflict check - we're just trashing the page
+    if (effectiveOperation !== 'create') {
       conflictFields = getConflictFields(activity.newValues, currentValues);
 
       // If there's a conflict but we have undo group context, check if the modifications
       // came from other activities in the same undo group (internal conflict vs external)
       if (conflictFields.length > 0) {
         loggers.api.debug('[Rollback:Preview] Page conflict detected', {
           activityId: activity.id,
           resourceId: activity.resourceId,
           timestamp: activity.timestamp?.toISOString(),
           conflictFields,
           undoGroupSize: undoGroupActivityIds.length,
         });
 
         if (undoGroupActivityIds.length > 0) {
           const externalModifications = await db
             .select({ id: activityLogs.id })
             .from(activityLogs)
             .where(
               and(
                 eq(activityLogs.resourceId, activity.resourceId),
                 eq(activityLogs.resourceType, 'page'),
                 gt(activityLogs.timestamp, activity.timestamp),
                 not(inArray(activityLogs.id, undoGroupActivityIds))
               )
             )
             .limit(1);
 
           loggers.api.debug('[Rollback:Preview] External modifications check', {
             activityId: activity.id,
             externalModificationsFound: externalModifications.length,
           });
 
           if (externalModifications.length === 0) {
             // All modifications came from activities in the undo group - not a real conflict
             loggers.api.debug('[Rollback:Preview] Page conflict is internal to undo group, ignoring', {
               activityId: activity.id,
               conflictFields,
             });
             conflictFields = [];
           }
         }
       }
     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e07f109 and 3bc88ea.

📒 Files selected for processing (3)
  • apps/web/src/components/activity/RollbackToPointDialog.tsx
  • apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx
  • apps/web/src/services/api/rollback-service.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/components/activity/RollbackToPointDialog.tsx
  • apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/services/api/rollback-service.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/services/api/rollback-service.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/services/api/rollback-service.ts
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/services/api/rollback-service.ts
🧬 Code graph analysis (1)
apps/web/src/services/api/rollback-service.ts (2)
packages/db/src/schema/monitoring.ts (1)
  • activityLogs (430-502)
packages/db/src/schema/core.ts (1)
  • pages (24-68)
⏰ 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). (2)
  • GitHub Check: Unit Tests
  • GitHub Check: Lint & TypeScript Check
🔇 Additional comments (6)
apps/web/src/services/api/rollback-service.ts (6)

290-290: LGTM - Optional pageMutationMeta aligns with idempotent behavior.

The change to make pageMutationMeta optional correctly reflects that some rollback operations (like skipping already-trashed pages at line 1660) don't perform mutations and return undefined.


842-858: LGTM - Drive conflict detection implements correct pattern.

Lines 855-858 correctly skip conflict detection for AI undo without the nested structure problem present in page conflict detection (lines 754-800). The shouldBeTrashed logic at line 845 correctly implements the rollback-of-rollback fix.

This is the correct pattern that should be applied to page conflict detection as well (see previous comment).


957-960: LGTM - Consistent AI undo conflict detection across resource types.

All non-page resource types (member, permission, role, agent, message) consistently use the correct pattern: if (undoGroupActivityIds.length === 0) { conflictFields = getConflictFields(...) }. This matches the drive pattern and correctly skips conflict detection during AI undo.

Only the page conflict detection (lines 754-800) deviates with the incorrect nested structure.

Also applies to: 1062-1065, 1105-1108, 1191-1194, 1247-1250, 1294-1297


1320-1322: LGTM - No-op detection correctly skipped for AI undo.

Line 1322 correctly bypasses no-op detection when undoGroupActivityIds.length > 0, as operations that appear isolated no-ops may be part of a larger undo group. This aligns with the AI undo handling pattern throughout the file.


1648-1688: LGTM - Idempotent rollback prevents redundant updates.

Lines 1655-1661 correctly implement idempotency by checking if the page is already trashed before mutation. The early return with pageMutationMeta: undefined aligns with the interface change at line 290. Both paths correctly use isTrashed (not trashed), consistent with the database schema.

This prevents wasted DB updates, unnecessary revision increments, and noisy activity logs for AI undo of old activities.


744-744: Core fix correctly implements rollback-of-rollback restoration.

The shouldBeTrashed logic at lines 744 and 845 correctly fixes the PR's stated bug:

  • Regular rollback of create: action='rollback' && !rollingBackRollback → trash page/drive
  • Rollback of rollback (redo): action='rollback' && rollingBackRollback=true → restore page/drive

This ensures that undoing a rollback restores the resource instead of trashing it again.

Also applies to: 845-845

2witstudios and others added 5 commits December 29, 2025 00:02
AI undo was showing 35 activities when only 9 actually happened in the
conversation. The aiConversationId is the page's chat thread ID which
persists across sessions, so old activities from previous sessions were
being included.

Changes:
- rollback-service.ts: Restore no-op detection for create operations,
  but for AI undo return canExecute:true with isNoOp:true (silent skip)
  instead of an error
- ai-undo-service.ts: Filter out activities where preview.isNoOp is true
  so only real changes are shown in the preview count

This ensures the preview accurately reflects only the changes from the
current AI conversation, not historical activities from past sessions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Apply the same no-op filtering to rollback-to-point-service that was
added to ai-undo-service. This ensures consistent behavior across all
rollback preview dialogs - only real changes are shown, not stale
activities for already-trashed pages.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The earlier fix bypassed conflict detection entirely for AI undo, which
also bypassed the detection of external changes (edits by other users
or manual edits after the AI change).

This fix restores proper conflict detection for all resource types:
- page, drive, member, permission, role, agent, message

For each type:
1. Always run conflict detection (getConflictFields)
2. If conflicts found AND we have undo group context:
   - Query for modifications after this activity
   - Exclude modifications from activities in the undo group
   - If any EXTERNAL modifications exist, flag as conflict
   - If only INTERNAL modifications, ignore (not a real conflict)
3. External conflicts still require force=true to proceed

This ensures external changes are protected while internal changes
from the same AI conversation are still handled smoothly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes issue where AI undo fails with "Message not found" immediately
after streaming completes, requiring a page refresh.

Root cause: The Vercel AI SDK generates assistant message IDs on the
client independently from the server. When the user clicks undo, the
client's ID doesn't match the server-saved ID in the database.

Solution: Generate the message ID on the server and send it to the
client via the stream protocol. Both client and server now use the
same ID.

- Page AI chat: Use writer.write({ type: 'start', messageId })
- Global Assistant: Use generateMessageId option in toUIMessageStreamResponse

References:
- https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-message-persistence
- https://github.com/vercel/ai/discussions/3512

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add broadcastActivityEvent with 500ms debouncing to socket-utils
- Add activity room handlers (join/leave) to realtime service
- Create useActivitySocket hook for frontend subscription
- Update SidebarActivityTab to auto-refresh on activity:logged events
- Add setActivityBroadcastHook to activity-logger for dependency injection
- Create instrumentation.ts to initialize broadcast hook at server startup
- Fix collectConsecutiveRollbacks to respect changeGroupId

Activity updates are now collaborative - all users viewing the same
drive/page see each other's activities in real-time without refresh.

🤖 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 (2)
apps/web/src/instrumentation.ts (1)

8-18: Consider adding error handling for initialization failures.

The dynamic imports and hook setup could fail silently. While this is fire-and-forget code, logging initialization failures would help with debugging production issues.

🔎 Proposed improvement
 export async function register() {
   // Only run on the server (Node.js runtime)
   if (process.env.NEXT_RUNTIME === 'nodejs') {
-    // Initialize activity broadcast hook for real-time updates
-    const { setActivityBroadcastHook } = await import('@pagespace/lib');
-    const { broadcastActivityEvent } = await import('@/lib/websocket/socket-utils');
-
-    setActivityBroadcastHook(broadcastActivityEvent);
-
-    console.log('[Instrumentation] Activity broadcast hook initialized');
+    try {
+      // Initialize activity broadcast hook for real-time updates
+      const { setActivityBroadcastHook } = await import('@pagespace/lib');
+      const { broadcastActivityEvent } = await import('@/lib/websocket/socket-utils');
+
+      setActivityBroadcastHook(broadcastActivityEvent);
+
+      console.log('[Instrumentation] Activity broadcast hook initialized');
+    } catch (error) {
+      console.error('[Instrumentation] Failed to initialize activity broadcast hook:', error);
+    }
   }
 }
apps/web/src/lib/websocket/socket-utils.ts (1)

19-30: Consider importing or re-exporting the payload type from @pagespace/lib.

ActivityEventPayload mirrors the type defined in packages/lib/src/monitoring/activity-logger.ts. While duplication is acceptable across package boundaries, consider re-exporting from a shared location to ensure they stay synchronized.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57c1d6f and 4a17672.

📒 Files selected for processing (8)
  • apps/realtime/src/index.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/useActivitySocket.ts
  • apps/web/src/instrumentation.ts
  • apps/web/src/lib/websocket/socket-utils.ts
  • packages/lib/src/monitoring/activity-logger.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/lib/websocket/socket-utils.ts
  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
  • apps/web/src/components/activity/utils.ts
  • apps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsx
  • apps/realtime/src/index.ts
  • apps/web/src/hooks/useActivitySocket.ts
  • packages/lib/src/monitoring/activity-logger.ts
  • apps/web/src/instrumentation.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/lib/websocket/socket-utils.ts
  • apps/web/src/components/activity/utils.ts
  • apps/realtime/src/index.ts
  • apps/web/src/hooks/useActivitySocket.ts
  • packages/lib/src/monitoring/activity-logger.ts
  • apps/web/src/instrumentation.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/lib/websocket/socket-utils.ts
  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
  • apps/web/src/components/activity/utils.ts
  • apps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsx
  • apps/realtime/src/index.ts
  • apps/web/src/hooks/useActivitySocket.ts
  • packages/lib/src/monitoring/activity-logger.ts
  • apps/web/src/instrumentation.ts
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/lib/websocket/socket-utils.ts
  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
  • apps/web/src/components/activity/utils.ts
  • apps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsx
  • apps/web/src/hooks/useActivitySocket.ts
  • apps/web/src/instrumentation.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
  • apps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsx
**/*.{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/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsx
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
  • apps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsx
apps/realtime/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Socket.IO for real-time collaboration features

Files:

  • apps/realtime/src/index.ts
🧠 Learnings (4)
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to apps/realtime/**/*.{ts,tsx} : Use Socket.IO for real-time collaboration features

Applied to files:

  • apps/web/src/lib/websocket/socket-utils.ts
  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
  • apps/realtime/src/index.ts
  • apps/web/src/hooks/useActivitySocket.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/**/*.{ts,tsx} : Use Socket.IO for real-time collaboration features - imported from the realtime service at port 3001

Applied to files:

  • apps/web/src/lib/websocket/socket-utils.ts
  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
  • apps/realtime/src/index.ts
  • apps/web/src/hooks/useActivitySocket.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 AI is streaming, register streaming state with `useEditingStore.getState().startStreaming()` to prevent UI refreshes, and clean up in return statement

Applied to files:

  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
  • apps/web/src/hooks/useActivitySocket.ts
📚 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
  • apps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsx
🧬 Code graph analysis (7)
apps/web/src/lib/websocket/socket-utils.ts (2)
packages/lib/src/monitoring/activity-logger.ts (1)
  • ActivityOperation (47-89)
apps/processor/src/logger.ts (1)
  • error (57-63)
apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx (1)
apps/web/src/hooks/useActivitySocket.ts (2)
  • ActivityContext (4-4)
  • useActivitySocket (16-95)
apps/web/src/components/activity/utils.ts (1)
apps/web/src/components/activity/types.ts (1)
  • ActivityLog (10-36)
apps/realtime/src/index.ts (2)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
apps/processor/src/logger.ts (1)
  • error (57-63)
apps/web/src/hooks/useActivitySocket.ts (1)
apps/web/src/hooks/useSocket.ts (1)
  • useSocket (5-30)
packages/lib/src/monitoring/activity-logger.ts (1)
packages/db/src/index.ts (1)
  • db (20-20)
apps/web/src/instrumentation.ts (2)
packages/lib/src/monitoring/activity-logger.ts (1)
  • setActivityBroadcastHook (178-180)
apps/web/src/lib/websocket/socket-utils.ts (1)
  • broadcastActivityEvent (383-452)
⏰ 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). (2)
  • GitHub Check: Lint & TypeScript Check
  • GitHub Check: Unit Tests
🔇 Additional comments (8)
apps/web/src/components/activity/utils.ts (1)

103-124: The null changeGroupId behavior is correct and intentional.

The function correctly groups consecutive rollbacks by matching changeGroupId, including when it is null. This is verified by the test suite at line 135-150, which explicitly expects three rollback activities with null changeGroupId to be grouped together.

This differs intentionally from collectConsecutiveEditSession (line 152), which requires non-null changeGroupId because edit sessions must have a changeGroupId to be valid (enforced by the isEditSessionGroupable predicate at line 96). Rollbacks have no such requirement—they can be meaningful with or without a changeGroupId, and the grouping logic correctly handles both cases by matching whatever value exists.

No changes are needed.

apps/web/src/components/layout/right-sidebar/ai-assistant/__tests__/SidebarActivityTab.test.tsx (1)

14-29: LGTM!

The added mocks correctly isolate the component from external dependencies:

  • useRouter mock provides the standard Next.js router functions needed for navigation tests.
  • useActivitySocket mock prevents real socket connections during testing while providing the expected return shape.
apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx (1)

247-256: LGTM - Real-time activity integration is well-implemented.

The socket integration correctly:

  • Determines context from route params (page > drive > null for dashboard)
  • Passes loadActivities as the reload callback
  • Uses a type fallback for context that's safe because the hook early-returns when contextId is null

The inline comment clarifies the fallback intent.

apps/web/src/hooks/useActivitySocket.ts (1)

1-94: Well-structured hook implementation.

The hook follows React best practices:

  • Uses refs to maintain stable callback identity and avoid effect re-runs
  • Properly manages room subscriptions with context tracking
  • Implements client-side debouncing to complement server-side debouncing
  • Cleans up subscriptions on context change and unmount
packages/lib/src/monitoring/activity-logger.ts (2)

157-180: Clean broadcast hook abstraction.

The broadcast hook pattern:

  • Provides clean decoupling between the logger and the broadcast mechanism
  • Uses a module-level variable for the hook, allowing runtime configuration
  • Type-safe payload definition with all necessary context

271-298: Note on transaction timing assumption.

The comment states the broadcast "will fire after the transaction commits" due to debouncing. This relies on the 500ms debounce delay exceeding transaction duration, which is typically true but not guaranteed for long-running transactions.

This is acceptable since broadcast failures don't affect data consistency, but worth noting for future debugging if activity events appear before their transactions complete.

apps/web/src/lib/websocket/socket-utils.ts (1)

370-451: Well-implemented debounced broadcasting.

The activity broadcast function:

  • Correctly implements per-context debouncing with a 500ms window
  • Handles both drive and page contexts when both IDs are present
  • Follows the established error-handling pattern (log but don't throw)
  • Properly cleans up the pending map entries after timeout fires
apps/realtime/src/index.ts (1)

273-322: LGTM - Activity channel handlers follow established patterns.

The new handlers:

  • Implement proper permission verification before joining rooms
  • Use consistent room naming (activity:drive:{id}, activity:page:{id})
  • Follow the same error handling and logging patterns as existing handlers
  • Don't disconnect on access denial (consistent with join_drive behavior)

Comment thread apps/web/src/hooks/useActivitySocket.ts
Update cleanup guard in useActivitySocket to verify socket is connected
before emitting leave event, avoiding emit on disconnected socket.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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