Skip to content

Conversation

@rossmanko
Copy link
Contributor

@rossmanko rossmanko commented Jan 29, 2026

Introduce a new notes system that allows users to save, manage, and organize information persistently. This replaces the previous memory functionality with a more flexible and user-friendly notes approach.

  • Add notes database schema and Convex functions
  • Create ManageNotesDialog component for viewing/editing notes
  • Add NotesToolHandler for AI tool integration
  • Update system prompt to support notes operations
  • Integrate notes into sidebar and settings
  • Update user deletion to handle notes cleanup

Summary by CodeRabbit

  • New Features

    • Full Notes system: create, list, update, delete notes with categories, tags, counts, search, pagination, and backend persistence; AI tools exposed for note actions.
  • UI Updates

    • Manage Notes dialog with infinite scroll, per-note delete and “Delete all”; Notes sidebar and main pane with color-coded category badges, counts, previews, processing/empty states, and settings/personalization controls for managing notes.

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

Introduce a new notes system that allows users to save, manage, and organize
information persistently. This replaces the previous memory functionality with
a more flexible and user-friendly notes approach.

- Add notes database schema and Convex functions
- Create ManageNotesDialog component for viewing/editing notes
- Add NotesToolHandler for AI tool integration
- Update system prompt to support notes operations
- Integrate notes into sidebar and settings
- Update user deletion to handle notes cleanup

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Jan 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
hackerai Ready Ready Preview, Comment Jan 29, 2026 2:49pm

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

📝 Walkthrough

Walkthrough

Adds a notes subsystem: Convex backend table and APIs for notes, AI tool wrappers and tool surface for create/list/update/delete notes, frontend UI and handlers (sidebar, tool renderers, Manage Notes dialog), types/sidebar extraction for SidebarNotes, system-prompt integration using notes, and user-deletion flow updated to remove notes.

Changes

Cohort / File(s) Summary
Backend: Convex notes & schema
convex/notes.ts, convex/schema.ts, convex/userDeletion.ts, convex/__tests__/userDeletion.test.ts, convex/memories.ts
Add comprehensive Convex notes CRUD, pagination, token budgeting, schema+indices+search, integrate notes into user-deletion; tests updated to include notes; memory content limit increased.
AI tools & tooling surface
lib/ai/tools/notes.ts, lib/ai/tools/index.ts, lib/ai/tools/update-memory.ts
Add AI tool wrappers for create/list/update/delete notes and expose them in the tools surface; deprecate/comment out legacy update-memory tool.
DB actions & system prompt
lib/db/actions.ts, lib/system-prompt.ts, lib/system-prompt/notes.ts
Introduce note-focused DB actions (create/list/update/delete/getNotes), replace memory context with notes in system-prompt generation, add generateNotesSection.
Types & sidebar models / utils
types/chat.ts, lib/utils/sidebar-utils.ts, app/contexts/GlobalState.tsx
Add VALID_NOTE_CATEGORIES, NoteCategory, SidebarNote, SidebarNotes, isSidebarNotes; extend sidebar extraction to emit SidebarNotes; minor typed assertion in GlobalState.updateSidebarContent.
Frontend: Sidebar, tool handlers & message rendering
app/components/ComputerSidebar.tsx, app/components/tools/NotesToolHandler.tsx, app/components/MessagePartHandler.tsx, app/share/[shareId]/components/SharedMessagePartHandler.tsx, app/components/tools/notes-tool-utils.tsx
Wire Notes into sidebar and unified header/action area, add NotesToolHandler and shared renderer, extend MessagePartHandler branches for note tools, and centralize note icons/text utilities.
Manage notes UI
app/components/ManageNotesDialog.tsx
Add ManageNotesDialog component with paginated loading, infinite scroll, per-note delete and delete-all, toast/error handling, and optimistic UI updates.
Settings / Personalization updates
app/components/PersonalizationTab.tsx, app/components/SettingsDialog.tsx
Replace Memory UI/props with Notes (add onManageNotes, relabel text and aria attributes), wire ManageNotesDialog in settings.
Message rendering / streaming & chat processor
lib/chat/chat-processor.ts, app/components/MessagePartHandler.tsx
Strip original/modified fields from tool-update_note outputs; integrate streaming/static tool states handling for note tools in message rendering.
Sidebar extraction & chat handler integration
lib/utils/sidebar-utils.ts, lib/api/chat-handler.ts
Extract note-tool outputs into sidebar content and treat note modifications similarly to memory updates when deciding system-prompt refresh.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend as Frontend (UI / Sidebar)
    participant Tool as NotesToolHandler
    participant Convex as Convex Backend
    participant System as System Prompt

    User->>Frontend: Trigger note tool action (create/list/update/delete)
    Frontend->>Tool: Render tool block / accept interaction
    Tool->>Convex: call create/list/update/delete note API
    Convex->>Convex: validate, persist, or query notes
    Convex-->>Tool: return notes / status / ids
    Tool->>Frontend: open/update sidebar with SidebarNotes
    Frontend->>System: include notes in prompt context (when applicable)
    System-->>Frontend: updated system prompt
Loading
sequenceDiagram
    participant User
    participant UI as ManageNotesDialog
    participant API as getUserNotesPaginated
    participant Convex as Convex Backend

    User->>UI: Open Manage Notes
    UI->>API: Request page (cursor, limit)
    API->>Convex: Fetch paginated notes
    Convex-->>API: notes[], nextCursor, hasMore
    API-->>UI: Return page
    UI->>UI: Render notes list
    loop on scroll
      UI->>API: Request next page
      API->>Convex: Fetch next page
      Convex-->>API: notes[], nextCursor, hasMore
      API-->>UI: Append notes
    end
    User->>UI: Delete note
    UI->>Convex: deleteUserNote(noteId)
    Convex-->>UI: Success
    UI->>UI: Optimistically remove note from list
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Poem

🐰 I nibbled thoughts into tidy rows,
Notes like clover in small tidy rows,
Titles, tags, and colors lined in time,
Sidebar opens — a hop, a little chime,
I celebrate each saved, bright line.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 clearly and concisely describes the main change: adding notes functionality to replace the memory system, which is the primary objective of this comprehensive PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (1)
lib/system-prompt.ts (1)

269-313: Disable notes section in temporary chats.

In temporary mode you skip fetching notes but still pass shouldIncludeNotes=true, so the prompt omits the explicit “notes tool disabled” message. If notes should be unavailable in temporary chats, gate the flag with !isTemporary.

🔧 Suggested fix
-  const shouldIncludeNotes = userCustomization?.include_memory_entries ?? true;
+  const shouldIncludeNotes =
+    !isTemporary && (userCustomization?.include_memory_entries ?? true);
🤖 Fix all issues with AI agents
In `@app/components/ManageNotesDialog.tsx`:
- Around line 92-107: handleDeleteAllNotes currently clears notes but leaves
pagination state (cursor and isLoadingMore) stale; after successful
deleteAllNotes and setAllNotes([]) reset the pagination state by clearing the
cursor (e.g., setCursor(null or undefined) or the variable used to track it) and
set isLoadingMore to false (and any related flags like hasMore if present)
inside handleDeleteAllNotes so subsequent fetches start from a fresh pagination
state; change references to cursor and isLoadingMore in that function to their
setter functions (e.g., setCursor, setIsLoadingMore) to perform the reset.

In `@app/components/tools/NotesToolHandler.tsx`:
- Around line 128-139: The update branch currently hardcodes category: "general"
which may be incorrect; change the construction of notes (inside the action ===
"update" branch in NotesToolHandler.tsx) so category is set from the update
payload or the existing note instead of a fixed string — e.g. use input.category
if present, otherwise pull the category from the existing note record (lookup by
input.note_id or use the result returned by the update), and only fall back to
"general" as a last resort; ensure you reference input.note_id, input.title,
input.content and keep updated_at as Date.now().

In `@convex/notes.ts`:
- Around line 239-259: The current search uses
ctx.db.query("notes").withSearchIndex("search_notes", ...) that only searches
"content", so title-only matches never make it into notes before the client-side
filter; update the logic either by adding "title" to the search index definition
(so withSearchIndex(search_notes) searches both "content" and "title") or by
performing a second DB search for title matches and merging results: run a
separate ctx.db.query("notes").withSearchIndex(...).search("title", args.search)
(apply args.category and eq("user_id", args.userId) to that query too), merge
the two result arrays deduplicating by note id, and then apply the final
client-side filtering using searchLower only if you still want extra safeguards
(ensure you use args.search.trim() and args.search.toLowerCase() as in the
existing code).

In `@lib/ai/tools/index.ts`:
- Around line 106-113: The comment "Notes tools - always available (not gated by
memory preference)" contradicts the implementation that gates note tools behind
!isTemporary && memoryEnabled; update the comment to accurately reflect the
gating (e.g., "Notes tools - available when not temporary and memory is
enabled") or, if notes truly must be always available, remove the gating
condition (!isTemporary && memoryEnabled) so
create_note/createListNotes/createUpdateNote/createDeleteNote are always
included; locate the conditional around isTemporary and memoryEnabled in
lib/ai/tools/index.ts that wraps createCreateNote, createListNotes,
createUpdateNote, and createDeleteNote and change either the comment text or the
conditional accordingly.

In `@lib/ai/tools/notes.ts`:
- Around line 286-289: The success response currently exposes the internal
note_id in the returned object (the string `Note '${note_id}' updated
successfully`); remove any direct reference to note_id and instead return a
user-facing message using the note's title when available (e.g., use note.title
or title variable), or fall back to a generic confirmation like "Note updated
successfully" to respect the "NEVER reference or cite note IDs" rule; update the
return object in the function that constructs this response (the block returning
success: true / message) to use title or a generic message instead of note_id.
- Around line 41-46: The guidance in the when_not_to_use block still references
the disabled memory tool; update the phrase "User preferences or personal facts
(use memory tool instead)" to point to an available alternative such as "User
preferences or personal facts (store in notes)" or instruct to first check with
list_notes — edit the Do NOT use notes for section in lib/ai/tools/notes.ts (the
when_not_to_use block) to remove the memory tool reference and replace it with
the chosen alternative text.

In `@lib/ai/tools/update-memory.ts`:
- Around line 1-166: The file contains an entirely commented-out
createUpdateMemory tool (symbols: createUpdateMemory, createMemory,
updateMemory, deleteMemory, getMemoryById) — remove the dead code by either
deleting this file or uncommenting and restoring a working implementation; if
you must keep it temporarily, replace the large commented block with a short
deprecation header/TODO explaining why it’s retained, the replacement (e.g.,
notes system), and a removal timeline, and ensure the file no longer contains
long commented code blocks.

In `@lib/utils/sidebar-utils.ts`:
- Around line 431-440: The update branch currently hardcodes category: "general"
which overwrites any provided category in the update input; in the block that
builds the notes entry for action === "update" (where variables action and input
are used) replace the fixed "general" with a conditional that uses
input.category when present (e.g., input.category || "general") so the sidebar
preserves a supplied category while still defaulting to "general" if none is
provided.
🧹 Nitpick comments (16)
convex/memories.ts (1)

93-95: Centralize the max content length constant to avoid drift.

Both mutations hardcode the same limit; defining a shared constant will prevent future divergence.

♻️ Suggested refactor
+const MEMORY_CONTENT_MAX_LENGTH = 8192;
+
 export const createMemoryForBackend = mutation({
   args: {
@@
-      const maxContentLength = 8192;
-      if (args.content.length > maxContentLength) {
+      if (args.content.length > MEMORY_CONTENT_MAX_LENGTH) {
         throw new ConvexError({
           code: "VALIDATION_ERROR",
-          message: `Memory content exceeds maximum length of ${maxContentLength} characters`,
+          message: `Memory content exceeds maximum length of ${MEMORY_CONTENT_MAX_LENGTH} characters`,
         });
       }
@@
 export const updateMemoryForBackend = mutation({
   args: {
@@
-      const maxContentLength = 8192;
-      if (args.content.length > maxContentLength) {
+      if (args.content.length > MEMORY_CONTENT_MAX_LENGTH) {
         throw new ConvexError({
           code: "VALIDATION_ERROR",
-          message: `Memory content exceeds maximum length of ${maxContentLength} characters`,
+          message: `Memory content exceeds maximum length of ${MEMORY_CONTENT_MAX_LENGTH} characters`,
         });
       }

Also applies to: 169-170

lib/system-prompt.ts (1)

5-8: Remove commented memory imports to reduce clutter.

These commented imports add noise now that notes are the intended path. Consider removing them outright.

♻️ Proposed cleanup
-// import { generateMemorySection } from "./system-prompt/memory";
 import { generateNotesSection } from "./system-prompt/notes";
-// import { getMemories, getNotes } from "@/lib/db/actions";
 import { getNotes } from "@/lib/db/actions";
app/components/SettingsDialog.tsx (1)

16-18: Remove commented-out memories dialog remnants.

The commented import, state, handler, and JSX block add noise and can be dropped now that notes fully replace memories.

♻️ Proposed cleanup
-// import { ManageMemoriesDialog } from "@/app/components/ManageMemoriesDialog";
...
-  // const [showMemoriesDialog, setShowMemoriesDialog] = useState(false);
...
-  // const handleManageMemories = () => {
-  //   setShowMemoriesDialog(true);
-  // };
...
-      {/* <ManageMemoriesDialog
-        open={showMemoriesDialog}
-        onOpenChange={setShowMemoriesDialog}
-      /> */}

Also applies to: 38-78, 190-194

convex/userDeletion.ts (1)

52-55: Align query iteration with project guidelines: consider using for await instead of .collect().

The .collect() method is functionally correct and follows the Convex pattern for deletions (.collect() then ctx.db.delete()). However, project guidelines prefer for await (const row of query) for async query iteration. You can refactor lines 52–55 to use for await while maintaining the parallel deletion behavior at lines 171–179:

// Instead of: ctx.db.query("notes")...collect()
const notesArray = [];
for await (const note of ctx.db.query("notes").withIndex("by_user_id", (q) => q.eq("user_id", user.subject))) {
  notesArray.push(note);
}

This maintains performance (notes are still deleted in parallel) while aligning with the project's streaming iteration preference. The same applies to other queries in the Promise.all() block if they have similar scale concerns.

app/components/tools/NotesToolHandler.tsx (1)

7-11: Consider adding stronger typing for part prop.

Using any for the part prop loses type safety. Consider defining a specific interface or importing a shared tool part type that includes toolCallId, state, input, output, and result properties.

app/components/PersonalizationTab.tsx (3)

89-94: Remove commented-out dead code.

The commented-out button and prop reference (onManageMemories) should be removed rather than left in the codebase.

Proposed cleanup
-            {/* <Button variant="outline" size="sm" onClick={onManageMemories}>
-              Manage
-            </Button> */}
             <Button variant="outline" size="sm" onClick={onManageNotes}>
               Manage
             </Button>

13-14: Remove commented-out prop declarations.

Similar to the button, the commented-out onManageMemories references in the interface and destructuring should be removed.

Proposed cleanup
 interface PersonalizationTabProps {
   onCustomInstructions: () => void;
-  // onManageMemories: () => void;
   onManageNotes: () => void;
 }

 const PersonalizationTab = ({
   onCustomInstructions,
-  // onManageMemories,
   onManageNotes,
 }: PersonalizationTabProps) => {

Also applies to: 19-20


62-66: UI/backend field naming inconsistency.

The UI now refers to "Notes" but still uses the include_memory_entries field. This works functionally but creates a terminology mismatch. Consider renaming the backend field in a future iteration or adding a comment explaining the legacy naming.

types/chat.ts (1)

129-133: Type guard could be more specific to avoid future collisions.

The current guard checks for "notes" in content && "action" in content. While this works now, if another SidebarContent variant adds an action field in the future, this could cause issues. Consider checking for a more unique property combination or adding a discriminator field.

Alternative approach with explicit check
 export const isSidebarNotes = (
   content: SidebarContent,
 ): content is SidebarNotes => {
-  return "notes" in content && "action" in content;
+  return "notes" in content && "action" in content && "totalCount" in content;
 };
app/share/[shareId]/components/SharedMessagePartHandler.tsx (2)

818-823: Duplicate local type definition instead of importing.

NoteCategory is redefined locally here but already exists in types/chat.ts. Import it instead to maintain consistency and avoid drift.

Proposed fix
+import type { NoteCategory } from "@/types/chat";
+
 // Notes tool renderer
 function renderNotesTool(
   part: MessagePart,
   idx: number,
   openSidebar: ReturnType<typeof useSharedChatContext>["openSidebar"],
 ) {
   const notesInput = part.input as {
     title?: string;
     content?: string;
     note_id?: string;
     category?: string;
     tags?: string[];
     search?: string;
   };
-  type NoteCategory =
-    | "general"
-    | "findings"
-    | "methodology"
-    | "questions"
-    | "plan";

804-1002: Significant duplication with NotesToolHandler.

The renderNotesTool function duplicates most of the logic from NotesToolHandler.tsx including getIcon, getActionText, getTarget, getActionType, and the notes payload construction. Consider extracting shared utilities to reduce maintenance burden.

Potential shared utilities could include:

  • getNotesIcon(toolName)
  • getNotesActionText(toolName, isStreaming)
  • getNotesTarget(toolName, input)
  • buildNotesPayload(action, input, output)

This would ensure consistency between live chat and shared chat views.

convex/notes.ts (4)

5-14: Type duplication creates maintenance risk.

VALID_CATEGORIES is duplicated here and in types/chat.ts (as VALID_NOTE_CATEGORIES). The comment on line 5 acknowledges this with "keep in sync", but manual synchronization is error-prone. Consider exporting the constant from a shared location or using a build-time validation to ensure consistency.


19-21: Consider a more robust ID generation approach.

Math.random() has limited entropy and could theoretically produce collisions in high-throughput scenarios. While 8 characters from base-36 (~2.8 trillion combinations) is likely sufficient for note IDs, consider using crypto.randomUUID() or a similar approach if stronger uniqueness guarantees are desired.


536-564: Pagination fetches all notes and slices in memory.

The current implementation collects all user notes (lines 539-543), sorts them (line 546), then slices by cursor position (lines 548-560). This defeats the purpose of pagination for performance since all records are loaded regardless of page size.

Consider using Convex's native cursor-based pagination with .paginate() for true database-level pagination. However, since the sort is by updated_at and the index is by user_id, you may need to add a composite index by_user_and_updated_at to support efficient paginated queries sorted by updated_at.


147-156: Redundant index ordering when custom sort is applied.

The .order("desc") on line 152 orders by _creationTime, but line 156 re-sorts by updated_at. The index ordering becomes redundant work. Consider removing .order("desc") since the results will be re-sorted anyway, or document why both are needed.

lib/db/actions.ts (1)

419-457: Remove commented-out code instead of leaving it in the codebase.

Large blocks of commented-out code (lines 419-457, 555-629) add noise and maintenance burden. Since the code is in version control, it can be retrieved if needed. Consider removing these commented blocks entirely.

Update getTarget() to properly display search criteria:
- Show category name when filtering by category
- Show "tagged: x, y" when filtering by tags
- Show search term in quotes when searching
- Combine multiple filters with separator
- Return undefined when no filters (cleaner "Listed notes" message)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@app/components/tools/NotesToolHandler.tsx`:
- Around line 172-187: The auto-open useEffect that opens the sidebar for
list_notes should only run for live streaming results: update the effect in
NotesToolHandler.tsx to require status === "streaming" in addition to state ===
"output-available" and the existing hasOpenedSidebarRef check before calling
handleOpenSidebar; keep hasOpenedSidebarRef reset on toolCallId changes and
reference the useEffect that reads state, status, toolName, hasOpenedSidebarRef,
and calls handleOpenSidebar so the logic only triggers for live streaming
outputs and not when viewing historical tool parts.

rossmanko and others added 2 commits January 28, 2026 22:05
Remove auto-open sidebar on output-available for notes tools to match
behavior of other tool handlers (Terminal, File, etc.) where sidebar
only opens on user click.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Removed outdated comments regarding notes tool availability and usage instructions to enhance code clarity and maintainability. This change streamlines the documentation within the codebase, ensuring it accurately reflects the current functionality of the notes system.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@app/components/tools/NotesToolHandler.tsx`:
- Around line 66-84: The getTarget function should not return internal note IDs
for "update_note" or "delete_note"; update it to return the user-facing title
(input.title or input.deleted_title) or a generic label like "Note" when title
is missing, and keep returning the category/tag/search string for "list_notes";
similarly, locate the other affected helper that currently surfaces
input.note_id (the block around the other reference at lines ~141-145) and
change it to return deleted_title/title or a generic label instead of the ID so
no internal IDs are exposed in tool targets or affectedTitle.

In `@lib/ai/tools/notes.ts`:
- Around line 336-339: The delete response currently falls back to the raw
note_id in the success message which may leak IDs; update the return object so
the message uses result.deleted_title if present and otherwise a generic term
like "note" (i.e., replace `note_id` fallback with a generic fallback), locating
the change where the code returns { success: true, message: `Note
'${result.deleted_title || note_id}' deleted successfully` } and modify the
template to use only result.deleted_title or "note" instead of note_id.

- Shorten note IDs from 8 to 5 characters for easier reference
- Return original/modified data from update_note for before/after comparison
- Display before/after comparison in sidebar for note updates
- Show confirmation message in sidebar for note deletes
- Remove note ID display from UI (internal use only)
- Use consistent "Failed to X note:" error message format
- Strip original/modified from persisted messages to reduce payload
- Add toModelOutput to update_note tool to strip diff data from model
- Sync SharedMessagePartHandler with regular NotesToolHandler logic

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
rossmanko and others added 2 commits January 29, 2026 09:18
Reset cursor and isLoadingMore when all notes are deleted to prevent
stale pagination state that could skip newly created notes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The filter couldn't add title-only matches since the search index
only returns content matches. Removed dead code for clarity.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@app/share/`[shareId]/components/SharedMessagePartHandler.tsx:
- Around line 934-1012: The code treats any output as a success; inside the
part.state === "output-available" block (where getActionType(), notesOutput,
notesInput, original, modified are used) validate notesOutput.success (or
equivalent boolean) before populating notes, totalCount, affectedTitle,
newNoteId or treating the action as successful—if success is false, set an
error/failure state (e.g., mark isExecuting false and pass a failure
flag/message into openSidebar) and avoid showing “Created/Updated note” UI;
update handleOpenInSidebar/openSidebar and the ToolBlock props
(isClickable/onClick) to reflect failures so the UI shows the error instead of
success.

In `@convex/notes.ts`:
- Around line 17-99: The generateNoteId() function produces short 5-char IDs
that can collide; update createNoteForBackend to guard uniqueness by checking
the notes index (e.g., query by_note_id) after generating noteId and retrying
(with a limited number of attempts) until an unused ID is found (or fall back to
a longer/UUID if retries exhausted), then persist the note using that unique ID;
reference generateNoteId, createNoteForBackend, and the DB lookup/insert logic
(ctx.db.insert and the by_note_id lookup) when implementing the retry/check
loop.
- Around line 538-611: getUserNotesPaginated currently loads all notes with
.collect() and does in-memory pagination; change it to use Convex's server-side
pagination: update the args to accept paginationOpts: paginationOptsValidator,
remove manual cursor/limit handling and sorting, and call
ctx.db.query("notes").withIndex("by_user_id", q => q.eq("user_id",
identity.subject)).order("desc", "updated_at").paginate(args.paginationOpts);
then map the returned page results to the same shape (note_id, title, content,
category, tags, _creationTime, updated_at) and return the paginator-provided
nextCursor/hasMore instead of computing them manually.
🧹 Nitpick comments (3)
app/components/ComputerSidebar.tsx (1)

156-171: Consider centralizing note category colors.
If category colors are reused elsewhere (e.g., Manage Notes dialog), a shared helper avoids drift.

app/share/[shareId]/components/SharedMessagePartHandler.tsx (1)

818-824: Reuse shared NoteCategory to avoid drift.
This local union duplicates VALID_NOTE_CATEGORIES; importing the shared type keeps the UI in sync when categories change.

♻️ Suggested refactor
-import type { Todo } from "@/types";
+import type { Todo } from "@/types";
+import type { NoteCategory } from "@/types/chat";
...
-  type NoteCategory =
-    | "general"
-    | "findings"
-    | "methodology"
-    | "questions"
-    | "plan";
types/chat.ts (1)

86-109: Use NoteCategory for update snapshots too.
Align original/modified.category with the same NoteCategory type for consistency.

♻️ Suggested refactor
   original?: {
     title: string;
     content: string;
-    category: string;
+    category: NoteCategory;
     tags: string[];
   };
   /** For update - modified note data after update (for before/after comparison) */
   modified?: {
     title: string;
     content: string;
-    category: string;
+    category: NoteCategory;
     tags: string[];
   };

…tion

- Replace manual in-memory pagination with Convex's .paginate() for O(1) memory
- Add by_user_and_updated index for efficient sorted pagination
- Remove redundant by_user_id index (covered by composite index)
- Update ManageNotesDialog to use usePaginatedQuery
- Add collision check for note ID generation with retry logic
- Extract shared notes tool utilities to reduce duplication

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@rossmanko rossmanko merged commit 90acbb8 into main Jan 29, 2026
3 of 4 checks passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@convex/notes.ts`:
- Around line 551-592: The query getUserNotesPaginated is missing a Convex
"returns" validator; add a returns validator that enforces the response shape
returned in the handler: an object with page (array of note-summary objects),
isDone (boolean) and continueCursor (string). The note-summary object should
validate the fields you map in the handler (note_id, title, content, category,
tags, _creationTime, updated_at) with appropriate primitive validators
(string/number/array/nullable as used elsewhere) so the response shape is
stable; add this returns entry alongside the existing args
(paginationOptsValidator) in the getUserNotesPaginated query definition.
🧹 Nitpick comments (1)
convex/notes.ts (1)

730-740: Prefer async iteration over .collect() when deleting all notes (Line 730-740).
This avoids loading every note into memory before deletion.

🛠️ Suggested refactor
-      const notes = await ctx.db
-        .query("notes")
-        .withIndex("by_user_and_updated", (q) =>
-          q.eq("user_id", identity.subject),
-        )
-        .collect();
-
-      for (const note of notes) {
-        await ctx.db.delete(note._id);
-      }
+      for await (const note of ctx.db
+        .query("notes")
+        .withIndex("by_user_and_updated", (q) =>
+          q.eq("user_id", identity.subject),
+        )) {
+        await ctx.db.delete(note._id);
+      }
Based on learnings: When using async iteration with queries, use the `for await (const row of query)` syntax instead of `.collect()` or `.take(n)`.

Comment on lines +551 to +592
export const getUserNotesPaginated = query({
args: {
paginationOpts: paginationOptsValidator,
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return {
page: [],
isDone: true,
continueCursor: "",
};
}

try {
const result = await ctx.db
.query("notes")
.withIndex("by_user_and_updated", (q) =>
q.eq("user_id", identity.subject),
)
.order("desc")
.paginate(args.paginationOpts);

return {
page: result.page.map((note) => ({
note_id: note.note_id,
title: note.title,
content: note.content,
category: note.category,
tags: note.tags,
_creationTime: note._creationTime,
updated_at: note.updated_at,
})),
isDone: result.isDone,
continueCursor: result.continueCursor,
};
} catch (error) {
console.error("Failed to get paginated notes:", error);
return { page: [], isDone: true, continueCursor: "" };
}
},
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find the convex/notes.ts file and check its structure
find . -name "notes.ts" -type f | head -20

Repository: hackerai-tech/hackerai

Length of output: 136


🏁 Script executed:

# Let's check if convex/notes.ts exists and examine the relevant lines
if [ -f convex/notes.ts ]; then
  wc -l convex/notes.ts
  echo "---"
  sed -n '551,592p' convex/notes.ts
fi

Repository: hackerai-tech/hackerai

Length of output: 1198


🏁 Script executed:

# Let's also check the Convex rules file mentioned in learnings
if [ -f .cursor/rules/convex_rules.mdc ]; then
  cat .cursor/rules/convex_rules.mdc | head -100
fi

Repository: hackerai-tech/hackerai

Length of output: 6055


🏁 Script executed:

# Check other query/mutation functions in the file to see if they have returns validators
rg "export const.*query\(|export const.*mutation\(|export const.*action\(" convex/notes.ts -A 5

Repository: hackerai-tech/hackerai

Length of output: 1506


Add a return validator for getUserNotesPaginated (Line 551-592).
This query is missing returns, which the Convex rules require and helps keep response shapes stable.

🛠️ Suggested fix
 export const getUserNotesPaginated = query({
   args: {
     paginationOpts: paginationOptsValidator,
   },
+  returns: v.object({
+    page: v.array(
+      v.object({
+        note_id: v.string(),
+        title: v.string(),
+        content: v.string(),
+        category: v.string(),
+        tags: v.array(v.string()),
+        _creationTime: v.number(),
+        updated_at: v.number(),
+      }),
+    ),
+    isDone: v.boolean(),
+    continueCursor: v.string(),
+  }),
   handler: async (ctx, args) => {
🤖 Prompt for AI Agents
In `@convex/notes.ts` around lines 551 - 592, The query getUserNotesPaginated is
missing a Convex "returns" validator; add a returns validator that enforces the
response shape returned in the handler: an object with page (array of
note-summary objects), isDone (boolean) and continueCursor (string). The
note-summary object should validate the fields you map in the handler (note_id,
title, content, category, tags, _creationTime, updated_at) with appropriate
primitive validators (string/number/array/nullable as used elsewhere) so the
response shape is stable; add this returns entry alongside the existing args
(paginationOptsValidator) in the getUserNotesPaginated query definition.

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.

2 participants