Skip to content

Conversation

@rossmanko
Copy link
Contributor

@rossmanko rossmanko commented Dec 30, 2025

Summary by CodeRabbit

  • New Features

    • Edit messages with attached files and selectively preserve/remove attachments.
    • File previews in the editor (image previews and placeholders for other file types).
    • Image validation when uploading files.
  • Improvements

    • Edit option visible on all user messages.
    • Save disabled unless a message has text or attachments.
    • Regeneration/edit flows preserve selected attachments and track regeneration counts.
    • Web-based tool appears more broadly in ask mode; temporary chats explicitly omit the memory tool.

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

- Remove hasFileContent restriction in MessageActions to allow editing messages with files
- Refactor MessageEditor to display file previews with remove buttons
- Add EditableFile interface for file metadata during editing
- Update Messages.tsx to extract and pass file parts to MessageEditor
- Update useChatHandlers to pass remaining file IDs on edit
- Update regenerateWithNewContent mutation to accept optional fileIds parameter
- Properly delete removed files from S3/Convex storage and update aggregates
- Fix state reset bug by removing useEffect that synced with initialFiles prop
@vercel
Copy link

vercel bot commented Dec 30, 2025

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

Project Deployment Review Updated (UTC)
hackerai Ready Ready Preview, Comment Dec 30, 2025 10:22pm

@coderabbitai
Copy link

coderabbitai bot commented Dec 30, 2025

📝 Walkthrough

Walkthrough

Editor now supports attached files (preview/remove) and returns remaining file IDs on save; front-end propagates remainingFileIds through edit/regenerate handlers to backend, which preserves specified files and deletes others during regeneration. Image validation and provider mappings were also updated.

Changes

Cohort / File(s) Summary
Message editing UI
app/components/MessageActions.tsx, app/components/MessageEditor.tsx, app/components/Messages.tsx
Edit visibility now shows for all user messages; added EditableFile type, initialFiles prop, per-file previews and remove UI; onSave updated to (content, remainingFileIds); Messages forwards initialFiles and updated onEditMessage signature.
Edit flow & handlers
app/hooks/useChatHandlers.ts
handleEditMessage gained optional remainingFileIds; added buildUpdatedParts to merge preserved file parts with new text; regeneration and edit flows pass fileIds and update message parts; todos for edited+subsequent messages are removed appropriately.
Backend message & file logic
convex/messages.ts, convex/schema.ts
regenerateWithNewContent accepts fileIds, preserves specified files, deletes others (with file deletions propagated), and updates parts/file_ids; added optional message fields (model, generation_time_ms, finish_reason, usage, regeneration_count); deleteLastAssistantMessage now returns regenerationCount.
Chat API / persistence
lib/api/chat-handler.ts, lib/db/actions.ts
Chat handler and save flows capture/forward model, generationTimeMs, finishReason, usage, regenerationCount; providerOptions.xai.store set false for streams; saved messages include new metadata fields.
File upload & validation
app/hooks/useFileUpload.ts, lib/utils/file-utils.ts
Added validateImageFile async validator and isImageFile usage; validateAndFilterFiles became async and awaits image checks; invalid files collected with errors.
AI provider config & tools
lib/ai/providers.ts, lib/ai/tools/index.ts
Swapped certain provider mappings to xai("grok-4-1-fast-non-reasoning"); removed modelsWithOnlineCapability/hasOnlineCapability; web tool gating simplified to rely on API keys only.
System prompt / ask-mode
lib/system-prompt.ts
getAskModeSection gained optional isTemporary to omit memory capability in temporary chats; system prompt updated to reflect memory availability.
Minor UI visibility tweak
app/components/MessageActions.tsx
Removed hasFileContent check so edit button is shown for all user messages.

Sequence Diagram

sequenceDiagram
    actor User
    participant Editor as MessageEditor (UI)
    participant Messages as Messages Component
    participant Handlers as useChatHandlers
    participant Convex as Convex Backend
    participant Storage as File Storage

    User->>Editor: Open editor with message files
    User->>Editor: Remove or keep files, edit text
    User->>Editor: Click Save
    Editor->>Editor: Compute remainingFileIds
    Editor->>Messages: onSave(newContent, remainingFileIds)
    Messages->>Handlers: handleEditMessage(msgId, newContent, remainingFileIds)

    Note over Handlers: buildUpdatedParts merges text + preserved file parts
    Handlers->>Convex: regenerateWithNewContent(msgId, newContent, fileIds=remainingFileIds)

    rect `#E6F7FF`
        Note over Convex: Preserve requested files, delete others
        Convex->>Convex: Determine filesToDelete (not in fileIds)
        Convex->>Storage: Delete removed files (S3/remote) — handle errors
        Convex->>Convex: Update message.parts and message.file_ids
    end

    Convex-->>Handlers: return updated message
    Handlers-->>Messages: update local state
    Messages-->>User: render edited message
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Possibly related PRs

Poem

🐰 I nibble code with nimble paws tonight,
Files kept or tossed with a hop so light,
Edit, save, and attachments stay or roam,
A carrot-click preserves the files you own,
🥕 Hooray — the convo’s fluff finds home!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Daily branch 2025 12 30' is vague and generic, using a date-based format that doesn't convey meaningful information about the changeset's actual purpose or scope. Revise the title to clearly describe the main objective of the changes. Consider summarizing the primary feature or fix, such as 'Add file handling support to message editing' or 'Enable message file attachment and regeneration tracking'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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: 0

🧹 Nitpick comments (3)
convex/messages.ts (1)

958-958: Consider using v.array(v.id("files")) for type consistency.

The fileIds parameter uses v.array(v.string()) while other file ID validators in this file use v.id("files") (e.g., line 106). This inconsistency may cause type confusion.

However, if the frontend passes plain strings (from part.fileId), this is acceptable. As per coding guidelines, prefer v.id(tableName) for document ID validation when possible.

🔎 Proposed fix
-    fileIds: v.optional(v.array(v.string())),
+    fileIds: v.optional(v.array(v.id("files"))),

This would require the frontend to cast the fileIds appropriately, but provides better type safety.

app/hooks/useChatHandlers.ts (2)

408-412: Incorrect type cast: messageId should remain a string.

The mutation regenerateWithNewContent expects messageId: v.string() (see convex/messages.ts line 956), not Id<"messages">. The cast is misleading but harmless since strings pass through.

🔎 Proposed fix
         await regenerateWithNewContent({
-          messageId: messageId as Id<"messages">,
+          messageId,
           newContent,
           fileIds: remainingFileIds,
         });

485-500: Duplicate code: reuse buildUpdatedParts helper.

The logic at lines 487-496 duplicates buildUpdatedParts defined at lines 419-437. Consider reusing the helper for consistency and maintainability.

🔎 Proposed fix
       // For temporary chats, send messages up to and including the edited message
       const messagesUpToEdit = messages.slice(0, editedMessageIndex + 1);
       const editedMessage = messages[editedMessageIndex];
-      
-      // Build updated parts for the edited message
-      const updatedParts: any[] = [];
-      if (newContent.trim()) {
-        updatedParts.push({ type: "text", text: newContent });
-      }
-      if (remainingFileIds && remainingFileIds.length > 0) {
-        const remainingFileParts = editedMessage.parts.filter(
-          (part: any) => part.type === "file" && part.fileId && remainingFileIds.includes(part.fileId)
-        );
-        updatedParts.push(...remainingFileParts);
-      }
       
       messagesUpToEdit[editedMessageIndex] = {
         ...editedMessage,
-        parts: updatedParts,
+        parts: buildUpdatedParts(editedMessage.parts),
       };
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14a727f and bf14be2.

📒 Files selected for processing (5)
  • app/components/MessageActions.tsx
  • app/components/MessageEditor.tsx
  • app/components/Messages.tsx
  • app/hooks/useChatHandlers.ts
  • convex/messages.ts
🧰 Additional context used
📓 Path-based instructions (1)
convex/**/*.{ts,js}

📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)

convex/**/*.{ts,js}: ALWAYS use the new function syntax for Convex functions with query, mutation, action decorators that include args, returns, and handler properties
Use array validators with v.array() to validate array arguments and schema fields
Use discriminated union types with v.union() and v.literal() for schema validation
Always use the v.null() validator when a function returns a null value
Use v.id(tableName) validator for document ID validation and Id<'tableName'> TypeScript type for ID fields
v.bigint() is deprecated; use v.int64() instead for representing signed 64-bit integers
Use v.record() for defining record types; v.map() and v.set() are not supported
Index fields must be queried in the same order they are defined; create separate indexes if you need different query orders
Use internalQuery, internalMutation, and internalAction for private functions that should not be part of the public API
Use query, mutation, and action for public functions that are exposed to the public API
ALWAYS include argument and return validators for all Convex functions including query, internalQuery, mutation, internalMutation, action, and internalAction
Functions that don't return anything should include returns: v.null() as the output validator
Functions implicitly return null in JavaScript if they have no return statement
Use ctx.runQuery to call a query from a query, mutation, or action
Use ctx.runMutation to call a mutation from a mutation or action
Use ctx.runAction to call an action from an action
Only call an action from another action if you need to cross runtimes (e.g., V8 to Node); otherwise extract shared code into a helper async function
Use as few calls from actions to queries and mutations as possible to avoid race conditions from split transaction logic
Pass a FunctionReference to ctx.runQuery, ctx.runMutation, or ctx.runAction; do not pass the callee function directly
When usi...

Files:

  • convex/messages.ts
🧠 Learnings (1)
📚 Learning: 2025-09-01T12:44:12.626Z
Learnt from: RostyslavManko
Repo: hackerai-tech/hackerai PR: 17
File: convex/fileStorage.ts:131-149
Timestamp: 2025-09-01T12:44:12.626Z
Learning: In convex/fileStorage.ts, serviceKey parameters are made optional (v.optional(v.string())) for queries like getFileTokensByFileIds to simplify local development workflow - developers don't need to configure service keys in the Convex dashboard when working locally. In production, the serviceKey is always provided, ensuring security. This pattern balances developer experience with production security requirements.

Applied to files:

  • convex/messages.ts
🧬 Code graph analysis (2)
convex/messages.ts (4)
convex/_generated/server.js (2)
  • mutation (49-49)
  • mutation (49-49)
convex/_generated/dataModel.d.ts (1)
  • Id (48-49)
convex/_generated/api.js (2)
  • internal (22-22)
  • internal (22-22)
convex/fileAggregate.ts (1)
  • fileCountAggregate (13-21)
app/components/Messages.tsx (1)
app/components/MessageEditor.tsx (1)
  • EditableFile (13-18)
🔇 Additional comments (11)
app/components/MessageActions.tsx (1)

133-134: LGTM! Edit button now available for messages with file attachments.

This change correctly enables editing for user messages regardless of whether they contain files, aligning with the new file attachment editing feature.

Note: The hasFileContent prop (line 31, 55) appears to no longer affect edit button visibility. Consider removing it if unused elsewhere to avoid confusion.

convex/messages.ts (2)

1036-1055: LGTM! Parts building logic handles edge cases well.

The implementation correctly:

  • Preserves text content followed by remaining file parts
  • Falls back to text-only part if newParts is empty (line 1051)
  • Clears file_ids when no files remain (line 1053)

1012-1034: Good error isolation for file deletions.

Each file deletion is wrapped in try/catch, ensuring one failed deletion doesn't block others. The pattern is consistent with deleteLastAssistantMessage (lines 502-528).

app/hooks/useChatHandlers.ts (1)

383-384: LGTM! Extended signature for file retention during edits.

The remainingFileIds parameter correctly threads through the edit flow, enabling selective file preservation during message regeneration.

app/components/Messages.tsx (2)

326-336: LGTM! File parts correctly mapped to EditableFile format.

The mapping appropriately:

  • Filters for valid file parts with fileId
  • Extracts required properties (fileId, name, mediaType)
  • Falls back to cached URL when filePart.url is unavailable (line 334)

135-149: LGTM! Callback correctly passes remaining file IDs.

The signature change and forwarding to onEditMessage properly integrates with the editing flow.

app/components/MessageEditor.tsx (5)

13-18: LGTM! Clean interface definition for editable files.

The EditableFile interface provides a clear contract for file data needed during editing, with optional fields for flexibility.


34-37: Good pattern: one-time state initialization.

Initializing state from props once (without syncing on updates) correctly preserves user edits during parent re-renders. The comment clarifies this intentional behavior.


58-60: Verify: Token validation excludes existing files.

Token counting only considers text content (countInputTokens(trimmedContent, [])), not the files being retained. This may be intentional since files are already uploaded and counted, but verify this aligns with the intended behavior for edited messages with preserved attachments.


88-147: LGTM! Clean file preview UI with removal capability.

The implementation correctly:

  • Differentiates image vs document rendering
  • Provides accessible remove buttons with appropriate labels
  • Handles long filenames with truncation
  • Uses appropriate Next.js Image props for dynamic URLs

52-72: LGTM! Save validation correctly handles content-or-files requirement.

The validation ensures either text content or files must be present, and remainingFileIds is correctly computed from the current files state.

- Add validateImageFile() using createImageBitmap API to detect corrupt images
- Only validate LLM-supported formats (PNG, JPEG, WebP, GIF)
- Skip validation for SVG and other non-AI image formats
- Reject corrupt images before upload with clear error message
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

Caution

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

⚠️ Outside diff range comments (1)
lib/api/chat-handler.ts (1)

537-550: Add an explicit role check before applying metrics to ensure they're only included for assistant messages.

The saveMessage call applies generation metrics (generationTimeMs, finishReason, usage, regenerationCount) to all messages in the loop without filtering by role. Although the messages array typically contains only assistant responses from the SDK, there's no explicit role check to guarantee this. The comment indicates metrics should only be applied to assistant messages—add a condition like if (message.role === "assistant") around the metrics properties to enforce this intent.

🧹 Nitpick comments (3)
app/components/Messages.tsx (1)

330-346: Consider strengthening the name fallback.

The name field is required in EditableFile (per app/components/MessageEditor.tsx, lines 12-17), but the fallback chain filePart.name || filePart.filename || "File" could technically produce undefined if both properties are falsy but not explicitly checked. The final "File" fallback should handle this, but the type assertion masks potential issues.

🔎 Suggested safer mapping
 .map((part) => {
   const filePart = part as any;
   return {
     fileId: filePart.fileId as string,
-    name:
-      filePart.name || filePart.filename || "File",
+    name: filePart.name ?? filePart.filename ?? "File",
     mediaType: filePart.mediaType,
     url:
       filePart.url ||
       getCachedUrl(filePart.fileId as string),
   } as EditableFile;
 })}

Using nullish coalescing (??) is more precise for this fallback pattern, as it only falls back on null/undefined rather than falsy values like empty strings.

app/hooks/useChatHandlers.ts (1)

429-450: Refactor: Extract duplicate parts-building logic.

The buildUpdatedParts helper (lines 429-450) and the inline logic for temporary chats (lines 498-512) are functionally identical. Consider reusing buildUpdatedParts in both code paths to reduce duplication and ensure consistency.

🔎 Suggested refactor
     // For temporary chats, send messages up to and including the edited message
     const messagesUpToEdit = messages.slice(0, editedMessageIndex + 1);
-    const editedMessage = messages[editedMessageIndex];
-
-    // Build updated parts for the edited message
-    const updatedParts: any[] = [];
-    if (newContent.trim()) {
-      updatedParts.push({ type: "text", text: newContent });
-    }
-    if (remainingFileIds && remainingFileIds.length > 0) {
-      const remainingFileParts = editedMessage.parts.filter(
-        (part: any) =>
-          part.type === "file" &&
-          part.fileId &&
-          remainingFileIds.includes(part.fileId),
-      );
-      updatedParts.push(...remainingFileParts);
-    }

     messagesUpToEdit[editedMessageIndex] = {
-      ...editedMessage,
-      parts: updatedParts,
+      ...messages[editedMessageIndex],
+      parts: buildUpdatedParts(messages[editedMessageIndex].parts),
     };

Also applies to: 498-516

convex/messages.ts (1)

1069-1077: Consider using Set for file part filtering.

The .some() call on line 1074 performs O(n) lookup for each file part. Since you already have the keepSet from line 1028, consider reusing it here for consistency and efficiency.

🔎 Suggested optimization
       // Keep file parts for remaining files
       if (newFileIds && newFileIds.length > 0) {
         const existingFileParts = message.parts.filter(
           (part: any) =>
             part.type === "file" &&
             part.fileId &&
-            newFileIds!.some((id) => id === part.fileId),
+            keepSet.has(part.fileId),
         );
         newParts.push(...existingFileParts);
       }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca1115e and 60329bb.

📒 Files selected for processing (8)
  • app/components/MessageEditor.tsx
  • app/components/Messages.tsx
  • app/hooks/useChatHandlers.ts
  • convex/messages.ts
  • convex/schema.ts
  • lib/ai/tools/index.ts
  • lib/api/chat-handler.ts
  • lib/db/actions.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/ai/tools/index.ts
🧰 Additional context used
📓 Path-based instructions (2)
convex/**/*.{ts,js}

📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)

convex/**/*.{ts,js}: ALWAYS use the new function syntax for Convex functions with query, mutation, action decorators that include args, returns, and handler properties
Use array validators with v.array() to validate array arguments and schema fields
Use discriminated union types with v.union() and v.literal() for schema validation
Always use the v.null() validator when a function returns a null value
Use v.id(tableName) validator for document ID validation and Id<'tableName'> TypeScript type for ID fields
v.bigint() is deprecated; use v.int64() instead for representing signed 64-bit integers
Use v.record() for defining record types; v.map() and v.set() are not supported
Index fields must be queried in the same order they are defined; create separate indexes if you need different query orders
Use internalQuery, internalMutation, and internalAction for private functions that should not be part of the public API
Use query, mutation, and action for public functions that are exposed to the public API
ALWAYS include argument and return validators for all Convex functions including query, internalQuery, mutation, internalMutation, action, and internalAction
Functions that don't return anything should include returns: v.null() as the output validator
Functions implicitly return null in JavaScript if they have no return statement
Use ctx.runQuery to call a query from a query, mutation, or action
Use ctx.runMutation to call a mutation from a mutation or action
Use ctx.runAction to call an action from an action
Only call an action from another action if you need to cross runtimes (e.g., V8 to Node); otherwise extract shared code into a helper async function
Use as few calls from actions to queries and mutations as possible to avoid race conditions from split transaction logic
Pass a FunctionReference to ctx.runQuery, ctx.runMutation, or ctx.runAction; do not pass the callee function directly
When usi...

Files:

  • convex/schema.ts
  • convex/messages.ts
convex/schema.ts

📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)

convex/schema.ts: Always define schema in convex/schema.ts using defineSchema and defineTable imported from convex/server
System fields _creationTime (v.number()) and _id (v.id(tableName)) are automatically added to all documents; do not redefine them
Always include all index fields in the index name; for example, an index on ['field1', 'field2'] should be named 'by_field1_and_field2'

Files:

  • convex/schema.ts
🧠 Learnings (2)
📚 Learning: 2025-12-24T18:09:08.574Z
Learnt from: CR
Repo: hackerai-tech/hackerai PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-24T18:09:08.574Z
Learning: Applies to convex/schema.ts : System fields `_creationTime` (v.number()) and `_id` (v.id(tableName)) are automatically added to all documents; do not redefine them

Applied to files:

  • convex/schema.ts
📚 Learning: 2025-12-24T18:09:08.574Z
Learnt from: CR
Repo: hackerai-tech/hackerai PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-24T18:09:08.574Z
Learning: Applies to convex/schema.ts : Always define schema in `convex/schema.ts` using `defineSchema` and `defineTable` imported from `convex/server`

Applied to files:

  • convex/schema.ts
🧬 Code graph analysis (4)
app/hooks/useChatHandlers.ts (1)
convex/messages.ts (1)
  • deleteLastAssistantMessage (464-578)
app/components/MessageEditor.tsx (1)
app/contexts/GlobalState.tsx (1)
  • useGlobalState (752-758)
app/components/Messages.tsx (1)
app/components/MessageEditor.tsx (1)
  • EditableFile (13-18)
convex/messages.ts (4)
convex/_generated/server.js (2)
  • mutation (49-49)
  • mutation (49-49)
convex/_generated/dataModel.d.ts (1)
  • Id (48-49)
convex/_generated/api.js (2)
  • internal (22-22)
  • internal (22-22)
convex/fileAggregate.ts (1)
  • fileCountAggregate (13-21)
🔇 Additional comments (13)
lib/db/actions.ts (1)

56-106: LGTM! Clean extension of saveMessage API.

The new optional metadata fields (model, generationTimeMs, finishReason, usage, regenerationCount) are correctly typed and forwarded to the Convex backend. The flexible Record<string, unknown> type for usage appropriately matches the v.any() validator in the schema.

convex/schema.ts (1)

66-70: LGTM! Schema extension for message metadata.

The new optional fields follow the existing snake_case naming convention and are properly defined. Using v.any() for usage is reasonable given that usage data varies across AI providers.

lib/api/chat-handler.ts (2)

285-290: LGTM! Well-structured metrics tracking initialization.

The timing and model tracking variables are properly initialized before the stream starts, enabling accurate calculation of generation time.


379-382: Good privacy enhancement.

Disabling conversation storage in XAI's database is a sensible privacy default.

app/hooks/useChatHandlers.ts (2)

306-323: LGTM! Regeneration count tracking properly implemented.

The regenerationCount is correctly extracted from the mutation result and propagated to the regeneration request. The fallback to 1 handles edge cases where no previous count exists.


389-393: LGTM! Clean signature extension for file ID preservation.

The remainingFileIds parameter allows the edit flow to preserve selected file attachments, aligning well with the MessageEditor changes.

app/components/MessageEditor.tsx (4)

13-18: LGTM! Well-defined EditableFile interface.

The interface correctly specifies fileId and name as required fields while keeping mediaType and url optional, providing appropriate flexibility for file handling.


34-37: Good design decision on state initialization.

Explicitly avoiding prop synchronization prevents state resets during parent re-renders, which is the correct behavior for an editor component maintaining user changes.


52-72: LGTM! Proper save flow with file ID preservation.

The save handler correctly validates that either content or files exist, computes remaining file IDs from the current state, and propagates them via the updated onSave callback.


88-147: Clean file preview implementation.

The UI appropriately handles both image previews (with Next.js Image) and document placeholders, with accessible remove buttons. The unoptimized prop is correctly used for user-uploaded content where dimensions are unknown.

convex/messages.ts (3)

107-111: LGTM! Metadata persistence in saveMessage.

The new optional fields are correctly validated and persisted, following the Convex coding guidelines for optional validators and field naming conventions.

Also applies to: 179-183


483-483: LGTM! Return type change for regeneration tracking.

The return type properly updated from v.null() to v.object({ regenerationCount: v.number() }), and the implementation correctly increments and returns the count. As per Convex coding guidelines, the return validator accurately reflects the actual return value.

Also applies to: 499-501, 572-572


1021-1036: LGTM! File retention logic is correct.

The logic properly handles both cases: when fileIds is provided (keep specified files), and when undefined (remove all files). The Set-based lookup for keepSet ensures efficient O(1) membership checks.

Comment on lines 1079 to 1087
await ctx.db.patch(message._id, {
parts: [{ type: "text", text: args.newContent }],
parts:
newParts.length > 0
? newParts
: [{ type: "text", text: args.newContent }],
content: args.newContent.trim() || undefined,
file_ids: undefined,
file_ids: newFileIds && newFileIds.length > 0 ? newFileIds : undefined,
update_time: Date.now(),
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's examine the function containing the code at lines 1079-1087
sed -n '1050,1110p' convex/messages.ts

Repository: hackerai-tech/hackerai

Length of output: 188


🏁 Script executed:

# Search for how empty parts are handled elsewhere
rg -n "parts.*length.*0|parts.*empty|\[\].*parts" --type=ts -C 2 convex/

Repository: hackerai-tech/hackerai

Length of output: 48


🏁 Script executed:

# Check for validation on newContent/content arguments
rg -n "newContent|args\.content" --type=ts -C 2 convex/messages.ts | head -100

Repository: hackerai-tech/hackerai

Length of output: 741


🏁 Script executed:

#!/bin/bash
# Get the full function context (lines 950-1100)
sed -n '950,1100p' convex/messages.ts

Repository: hackerai-tech/hackerai

Length of output: 4810


🏁 Script executed:

# Search for the message schema definition to see if empty parts are valid
rg -n "defineSchema|messages.*schema|parts:" --type=ts -C 3 convex/ | head -150

Repository: hackerai-tech/hackerai

Length of output: 6784


🏁 Script executed:

# Check if there's any cleanup/validation of empty parts after this patch
sed -n '1085,1150p' convex/messages.ts

Repository: hackerai-tech/hackerai

Length of output: 2344


Inconsistency: empty content fallback uses untrimmed value while the check uses trimmed value.

Line 1064 checks args.newContent.trim() before adding to newParts, but the fallback on line 1083 uses the untrimmed args.newContent. When content is whitespace-only and no files remain, this creates parts: [{ type: "text", text: " " }] with content: undefined, leaving the message in an inconsistent state where the parts array preserves whitespace that was explicitly excluded from the content field. Use args.newContent.trim() in the fallback to match the intent of line 1064, or add input validation to reject empty/whitespace-only messages.

🤖 Prompt for AI Agents
In convex/messages.ts around lines 1064 to 1087, the fallback parts creation
uses the untrimmed args.newContent while the earlier check used
args.newContent.trim(), causing whitespace-only content to produce parts with
spaces but content set to undefined; fix by using args.newContent.trim() for the
fallback parts (and for content assignment) or add validation to
reject/normalize whitespace-only input so both parts and content consistently
use the trimmed value.

@rossmanko rossmanko merged commit f190e59 into main Dec 31, 2025
4 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Jan 2, 2026
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