Skip to content

Conversation

@rossmanko
Copy link
Contributor

@rossmanko rossmanko commented Sep 29, 2025

Summary by CodeRabbit

Release Notes

  • New Features

    • Added new file upload and management capabilities using service role keys
    • Introduced file accumulator utility for tracking uploaded files
    • Added a new terminal files retrieval tool
    • Enhanced PDF and media file handling in chat interactions
  • Improvements

    • Updated model selection logic for PDF and media files
    • Refined file preview and rendering capabilities
    • Improved file upload and storage mechanisms
  • API Changes

    • Updated various message and file-related API endpoints
    • Modified chat processing and tool creation workflows
  • Bug Fixes

    • Improved error handling in file upload and processing flows

@vercel
Copy link

vercel bot commented Sep 29, 2025

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

Project Deployment Preview Comments Updated (UTC)
hackerai Ready Ready Preview Comment Sep 29, 2025 8:13pm

@coderabbitai
Copy link

coderabbitai bot commented Sep 29, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds service-role file upload support and tooling to fetch sandbox files, accumulate uploaded file IDs, and persist them on assistant messages. Updates chat route to attach saved file IDs, adjusts model selection for PDFs, revises providers, renames Convex message mutations, enhances UI to preview saved files, and improves reasoning scroll.

Changes

Cohort / File(s) Summary of changes
Env config
\.env.local.example
Activates CONVEX_SERVICE_ROLE_KEY with a placeholder default.
Chat API & processing
app/api/chat/route.ts, lib/chat/chat-processor.ts, lib/system-prompt.ts, lib/ai/providers.ts
Exposes and uses getFileAccumulator; attaches extraFileIds on assistant messages; updates provider config and adds parallelToolCalls flag; adds PDF-aware model routing and a new vision-model-for-pdfs; trims system prompt phrasing.
Tools core & context
lib/ai/tools/index.ts, types/agent.ts
Introduces FileAccumulator into ToolContext; returns getFileAccumulator from createTools; wires new get_terminal_files tool.
New tools & utilities
lib/ai/tools/utils/file-accumulator.ts, lib/ai/tools/get-terminal-files.ts, lib/ai/tools/utils/sandbox-file-uploader.ts, lib/ai/tools/run-terminal-cmd.ts, lib/ai/tools/write-file.ts
Adds in-memory file ID accumulator; implements get_terminal_files to upload sandbox files to Convex and record IDs; adds sandbox file uploader using service key; tweaks terminal cmd flow and timeout handling; updates write-file guidance to use get_terminal_files.
Convex backend
convex/fileActions.ts, convex/fileStorage.ts, convex/messages.ts, lib/db/actions.ts
Adds service-key and userId paths for uploads; updates generateUploadUrl signature/logic; renames message mutations and removes updateMessageAppendPartsForBackend; saveMessage accepts extraFileIds and persists merged file IDs.
Frontend hooks & UI
app/hooks/useChatHandlers.ts, app/hooks/useFileUpload.ts, app/components/FilePartRenderer.tsx, app/components/Messages.tsx, components/ai-elements/reasoning.tsx
Switches to renamed Convex mutations; wraps generateUploadUrl in a callback; enables URL-clickable file preview cards; renders saved files under assistant messages; adds auto-scroll and layout tweaks to reasoning panel.
Message utils
lib/utils/message-utils.ts
Adds SavedFile type and extractor to parse tool outputs for saved file URLs.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant FE as Chat UI
  participant API as Chat Route
  participant TL as Tools (createTools)
  participant SB as Sandbox
  participant UPL as Sandbox Uploader
  participant CX as Convex (fileStorage/fileActions)
  participant DB as DB

  U->>FE: Run terminal task
  FE->>API: Send message
  API->>TL: Initialize tools + getFileAccumulator
  TL->>SB: Execute run_terminal_cmd
  SB-->>TL: stdout/stderr, files present

  TL->>TL: get_terminal_files (paths)
  TL->>UPL: uploadSandboxFileToConvex(path, userId)
  UPL->>CX: fileStorage.generateUploadUrl(serviceKey, userId)
  CX-->>UPL: upload URL
  UPL->>CX: POST blob → fileActions.saveFile(...)
  CX-->>UPL: { url, fileId, tokens }
  UPL-->>TL: UploadedFileInfo
  TL->>TL: fileAccumulator.add(fileId)  %% new/changed
  TL-->>API: Tool result with fileUrls

  API->>TL: getFileAccumulator().getAll()  %% new/changed
  API->>DB: saveMessage(..., extraFileIds=[...])  %% new/changed
  DB-->>API: ok
  API-->>FE: Assistant message + saved file URLs
  FE-->>U: Render message and preview cards with links
Loading
sequenceDiagram
  autonumber
  participant CP as Chat Processor
  participant MS as Model Selector

  CP->>MS: selectModel(mode, containsMedia, containsPdf)
  alt containsPdf && mode=="ask"  %% new branch
    MS-->>CP: vision-model-for-pdfs
  else containsMedia && mode=="ask"
    MS-->>CP: vision-model
  else containsMedia && mode=="agent"
    MS-->>CP: agent-model-with-vision
  else
    MS-->>CP: default by mode
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • feat: file upload #17 — Also implements service-key file upload and touches Convex storage/actions, upload hooks, and UI rendering of file attachments.
  • Daily branch 2025 08 13 #5 — Modifies tools factory and ToolContext; overlaps with this PR’s addition of FileAccumulator and getFileAccumulator.
  • Daily branch 2025 09 16 #31 — Updates file storage and message/file attachment handling, overlapping with Convex and message persistence changes here.

Poem

hop hop, I stash each file I find,
in sets of IDs, all neatly aligned.
from sandbox sands to Convex skies,
links appear before your eyes.
PDFs get their special seat—
models choose the proper beat.
carrots saved, the merge complete. 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 09 28” is a generic, date-based label that does not communicate the substantial, multi-file updates in this pull request—such as integrating a file accumulator, enabling service-key based uploads, renaming API endpoints, and enhancing UI components—so it fails to summarize the main purpose of the changes. Please update the pull request title to a concise, descriptive summary of the key changes (for example, “Integrate FileAccumulator for tool outputs and add service-key based file upload support”).
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6b0186d and 62dad5c.

📒 Files selected for processing (5)
  • app/api/chat/route.ts (3 hunks)
  • app/components/FilePartRenderer.tsx (5 hunks)
  • components/ai-elements/reasoning.tsx (3 hunks)
  • lib/ai/tools/utils/sandbox-file-uploader.ts (1 hunks)
  • lib/db/actions.ts (4 hunks)

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: 3

Caution

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

⚠️ Outside diff range comments (4)
convex/messages.ts (1)

315-334: Avoid .filter(); switch to an index on (chat_id, role) to fetch the last assistant message.

Per Convex guidelines, prefer index-backed predicates over filter to avoid scanning. Define an index like by_chat_id_role(chat_id, role, _creationTime) and use it here.

Apply after adding the index to your schema:

-      const lastAssistantMessage = await ctx.db
-        .query("messages")
-        .withIndex("by_chat_id", (q) => q.eq("chat_id", args.chatId))
-        .filter((q) => q.eq(q.field("role"), "assistant"))
-        .order("desc")
-        .first();
+      const lastAssistantMessage = await ctx.db
+        .query("messages")
+        .withIndex("by_chat_id_role", (q) =>
+          q.eq("chat_id", args.chatId).eq("role", "assistant"),
+        )
+        .order("desc")
+        .first();

As per coding guidelines.

lib/db/actions.ts (1)

50-60: Use Id<'files'> for extraFileIds and enforce deduplication end-to-end

  • In lib/db/actions.ts (lines 50–60): change
    extraFileIds?: string[];
    to
    extraFileIds?: Array<Id<"files">>;
    and add
    import type { Id } from "@/convex/_generated/dataModel";
  • In lib/db/actions.ts (lines 64–67): replace the current array merge with
    const mergedFileIds: Array<Id<"files">> = Array.from(
      new Set<Id<"files">>([
        ...fileIds,
        ...(extraFileIds ?? []),
      ]),
    );
    then remove any as any casts when assigning fileIds: mergedFileIds || undefined.
  • In app/api/chat/route.ts (lines 253–259): update both calls to saveMessage so that extraFileIds is passed as Array<Id<"files">> (e.g. cast newFileIds to Array<Id<"files">> instead of string[]).
app/hooks/useChatHandlers.ts (1)

80-82: Prevent sending an empty message when files lack fileId

hasValidFiles allows submission if a file has url but no fileId; later validFiles filters out those without fileId, producing an empty payload (no text, no files).

Apply:

-    const hasValidFiles = uploadedFiles.some((f) => f.uploaded && f.url);
+    const hasValidFiles = uploadedFiles.some(
+      (f) => f.uploaded && f.url && f.fileId
+    );

Optionally, fail fast with a toast if files exist but are missing IDs.

Also applies to: 106-123

convex/fileActions.ts (1)

179-187: Do not trust client-provided mediaType; use Blob.type/detection to prevent bypass.

Currently, an attacker can pass mediaType="image/png" for a non-image file to force 0 tokens and skip content persistence. The same issue affects shouldSaveContent, which relies on args.mediaType. Gate on the actual Blob.type (and/or detected type) instead.

Apply this diff to base decisions on the real file MIME and drop reliance on args.mediaType in these hotspots:

@@
-  // Check if file is an image - return 0 tokens immediately
-  if (mediaType && isImageFile(mediaType)) {
+  // Check if file is an image using the actual blob MIME
+  const blobMime =
+    typeof file !== "string" ? (file as Blob).type : undefined;
+  if (blobMime && isImageFile(blobMime)) {
     return [
       {
         content: "",
         tokens: 0,
       },
     ];
   }
@@
-    // Check if file is an image - return 0 tokens
-    if (mediaType && isImageFile(mediaType)) {
+    // Check if file is an image - return 0 tokens
+    const blobMime =
+      typeof file !== "string" ? (file as Blob).type : undefined;
+    if (blobMime && isImageFile(blobMime)) {
       return [
         {
           content: "",
           tokens: 0,
         },
       ];
-    } else if (mediaType && mediaType.startsWith("text/")) {
+    } else if (blobMime?.startsWith("text/")) {
       try {
         const blob = file as Blob;
         const fileBuffer = Buffer.from(await blob.arrayBuffer());
         const textDecoder = new TextDecoder("utf-8");
         const textContent = textDecoder.decode(fileBuffer);
         const fallbackTokens = countTokens(textContent);
@@
-      // Save content for non-image, non-PDF, non-binary files
-      const shouldSaveContent =
-        !isImageFile(args.mediaType) &&
-        args.mediaType !== "application/pdf" &&
-        chunks.length > 0 &&
-        chunks[0].content.length > 0;
+      // Save content for non-image/non-PDF when we actually have content
+      const blobMime = file.type;
+      const hasContent = chunks.some((c) => c.content.length > 0);
+      const shouldSaveContent =
+        hasContent &&
+        !(blobMime?.startsWith("image/")) &&
+        blobMime !== "application/pdf";

Also applies to: 216-261, 418-423

🧹 Nitpick comments (21)
.env.local.example (1)

6-10: Avoid shipping an “active” placeholder for CONVEX_SERVICE_ROLE_KEY; fix ordering and linter noise.

  • Keep the key empty or commented to prevent accidental reuse in real deployments.
  • Move it before NEXT_PUBLIC_CONVEX_URL per dotenv-linter and drop quotes to silence QuoteCharacter.

Apply:

-# Convex Service Role Key (Required for securing public functions)
-# Generate a secure random string and add it to your Convex environment variables
-CONVEX_SERVICE_ROLE_KEY="replace-with-a-32+char-random-secret"
-NEXT_PUBLIC_CONVEX_URL=
+# Convex Service Role Key (Required for securing public functions)
+# Generate a secure random string and add it to your Convex environment variables
+# CONVEX_SERVICE_ROLE_KEY=replace-with-a-32+char-random-secret
+NEXT_PUBLIC_CONVEX_URL=

Note: Ensure this key is only used server-side and never referenced from client bundles. Based on learnings.

app/components/ReasoningHandler.tsx (1)

55-56: LGTM; optional naming alignment.

Change reads clean and preserves behavior. If you want absolute consistency with the prop, consider naming the local boolean isStreaming.

Also applies to: 59-59

lib/ai/providers.ts (1)

33-36: Tighten types for trackedModels to match provider expectations.

Use LanguageModelV1 instead of any to prevent drift and surface type errors at compile time.

Apply:

-import type { SubscriptionTier } from "@/types";
+import type { SubscriptionTier } from "@/types";
+import type { LanguageModelV1 } from "ai";

-  const trackedModels: Record<string, any> = {};
+  const trackedModels: Record<string, LanguageModelV1> = {};
lib/ai/tools/utils/file-accumulator.ts (1)

3-21: Add a drain helper and explicit return types for clarity and safer lifecycle.

  • Returning types helps callers.
  • Providing a drain() (get-and-clear) reduces misuse where callers forget to call clear() after getAll().
 export class FileAccumulator {
-  private ids: Set<Id<"files">> = new Set();
+  private ids: Set<Id<"files">> = new Set();

-  add(id: Id<"files">) {
+  add(id: Id<"files">): void {
     this.ids.add(id);
   }

-  addMany(ids: Array<Id<"files">>) {
+  addMany(ids: Array<Id<"files">>): void {
     for (const id of ids) this.ids.add(id);
   }

-  getAll(): Array<Id<"files">> {
+  getAll(): Array<Id<"files">> {
     return Array.from(this.ids);
   }

-  clear() {
+  clear(): void {
     this.ids.clear();
   }
+
+  // Atomically get and clear to avoid duplicate attachments across saves
+  drain(): Array<Id<"files">> {
+    const all = this.getAll();
+    this.clear();
+    return all;
+  }
 }
types/agent.ts (1)

5-6: Mark FileAccumulator import as type-only to avoid unintended runtime dependency.

Prevents bundlers from pulling the module at runtime for a type-only usage.

-import { FileAccumulator } from "@/lib/ai/tools/utils/file-accumulator";
+import type { FileAccumulator } from "@/lib/ai/tools/utils/file-accumulator";

Also applies to: 25-26

components/ai-elements/reasoning.tsx (1)

53-64: Use a single Collapsible root for proper a11y semantics and simpler state wiring.

Two separate Collapsible roots (one in Trigger, one in Content) can desync aria-controls relationships. Wrap children with one Collapsible in Reasoning, and drop the inner wrappers. This keeps controlled open state and improves accessibility.

@@
-    return (
-      <ReasoningContext.Provider
-        value={{ isOpen: !!isOpen, setIsOpen, isStreaming }}
-      >
-        <div
-          className={cn("not-prose max-w-prose space-y-2", className)}
-          {...props}
-        >
-          {children}
-        </div>
-      </ReasoningContext.Provider>
-    );
+    return (
+      <ReasoningContext.Provider value={{ isOpen: !!isOpen, setIsOpen, isStreaming }}>
+        <Collapsible open={!!isOpen} onOpenChange={setIsOpen}>
+          <div className={cn("not-prose max-w-prose space-y-2", className)} {...props}>
+            {children}
+          </div>
+        </Collapsible>
+      </ReasoningContext.Provider>
+    );
@@
-    return (
-      <Collapsible onOpenChange={setIsOpen} open={isOpen}>
-        <CollapsibleTrigger
+    return (
+      <CollapsibleTrigger
           className={cn(
             "flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground",
             className,
           )}
           aria-label={title}
           {...props}
         >
@@
-        </CollapsibleTrigger>
-      </Collapsible>
+        </CollapsibleTrigger>
     );
@@
-    return (
-      <Collapsible open={isOpen}>
-        <CollapsibleContent
+    return (
+      <CollapsibleContent
         className={cn(
           "mt-2 space-y-3",
           "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
           className,
         )}
         {...props}
       >
         {children}
-        </CollapsibleContent>
-      </Collapsible>
+      </CollapsibleContent>
     );

Also applies to: 78-104, 114-126

lib/ai/tools/index.ts (1)

38-50: Expose a safe drain helper for the accumulator to avoid stale/duplicate IDs across requests.

Providing drainFileAccumulator() makes the intended lifecycle explicit, especially in serverless handlers.

   const todoManager = new TodoManager(initialTodos);
   const fileAccumulator = new FileAccumulator();
@@
   const getSandbox = () => sandbox;
   const getTodoManager = () => todoManager;
   const getFileAccumulator = () => fileAccumulator;
+  const drainFileAccumulator = () => {
+    const ids = fileAccumulator.getAll();
+    fileAccumulator.clear();
+    return ids;
+  };
 
-  return { tools, getSandbox, getTodoManager, getFileAccumulator };
+  return { tools, getSandbox, getTodoManager, getFileAccumulator, drainFileAccumulator };

If app/api/chat/route.ts currently calls getFileAccumulator().getAll() and then clear(), consider switching to drainFileAccumulator() once added.

Also applies to: 80-83

app/hooks/useChatHandlers.ts (1)

255-261: Type consistency: prefer Id<'messages'> in Convex validator over string

You cast messageId to Id<"messages"> here, while the Convex mutation (convex/messages.ts Lines 626-725 in context) validates messageId as v.string(). Prefer v.id("messages") on the server and drop the cast here for stronger typing.

Based on learnings

lib/ai/tools/write-file.ts (1)

50-51: Harden against missing accumulator or user ID

If ToolContext.userID or context.fileAccumulator is ever absent, upload/accumulate will throw and leave the user with a sandbox write but no upload. Add a clear guard.

-          const saved = await uploadSandboxFileToConvex({
+          if (!context.userID) {
+            return { result: "Wrote file to sandbox, but cannot upload without user session." };
+          }
+          const saved = await uploadSandboxFileToConvex({
             sandbox,
             userId: context.userID,
             fullPath: file_path,
           });
-          context.fileAccumulator.add(saved.fileId);
+          context.fileAccumulator?.add(saved.fileId);
lib/chat/chat-processor.ts (1)

88-95: Broaden PDF detection fallback (optional)

Some uploads may lack accurate mediaType; consider also checking filename extension (.pdf) when mediaType is missing.

-  const containsPdfFiles = messagesWithUrls.some((message: any) =>
-    (message.parts || []).some(
-      (part: any) =>
-        part?.type === "file" && part?.mediaType === "application/pdf",
-    ),
-  );
+  const containsPdfFiles = messagesWithUrls.some((message: any) =>
+    (message.parts || []).some((part: any) => {
+      if (part?.type !== "file") return false;
+      if (part?.mediaType === "application/pdf") return true;
+      const name = (part?.filename || part?.name || "").toLowerCase();
+      return name.endsWith(".pdf");
+    }),
+  );
convex/fileStorage.ts (2)

33-54: Avoid full count for limit check (perf)

checkFileUploadLimit fetches all user files via countUserFiles; you only need to know if the user has ≥100. Query up to 100/101 and short-circuit.

-export const checkFileUploadLimit = internalQuery({
-  args: { userId: v.string() },
-  returns: v.null(),
-  handler: async (ctx, args) => {
-    const currentFileCount = await ctx.runQuery(
-      internal.fileStorage.countUserFiles,
-      { userId: args.userId },
-    );
-    if (currentFileCount >= 100) {
-      throw new Error("Upload limit exceeded: Maximum of 100 files allowed per user");
-    }
-    return null;
-  },
-});
+export const checkFileUploadLimit = internalQuery({
+  args: { userId: v.string() },
+  returns: v.null(),
+  handler: async (ctx, args) => {
+    const sample = await ctx.db
+      .query("files")
+      .withIndex("by_user_id", (q) => q.eq("user_id", args.userId))
+      .take(100);
+    if (sample.length >= 100) {
+      throw new Error("Upload limit exceeded: Maximum of 100 files allowed per user");
+    }
+    return null;
+  },
+});

60-81: Service-key path skips paid-plan gating
Service flows enforce only upload limits but bypass the paid-entitlement checks applied in the user-authenticated branch. If backend uploads via serviceKey should require a paid plan, add a similar entitlement guard before generating the URL.

lib/ai/tools/run-terminal-cmd.ts (3)

164-171: Watcher cleanup is best-effort; consider explicit await when supported

closeWatcher defers close by 500ms and ignores errors. If SDK exposes an async close/stop, prefer awaiting it once per run to avoid dangling watchers; otherwise keep as is.

Can you confirm whether watchHandle exposes a promise-based close/stop in the current @e2b/code-interpreter version?


130-136: Guard missing userID before attempting uploads

If context.userID is falsy, uploads will fail server-side. Short-circuit to skip uploads and avoid noisy errors.

               const saved = await uploadSandboxFileToConvex({
-                  sandbox,
-                  userId: context.userID,
+                  sandbox,
+                  userId: context.userID!,
                   fullPath,
                 });

Or early-return in flushUploads when !context.userID.


233-239: Confirm downstream consumer for fileUrls in tool result

fileUrls is returned from the tool but I don’t see a consumer in this PR. If unused, consider omitting to reduce payload size; otherwise document the shape.

lib/ai/tools/utils/sandbox-file-uploader.ts (2)

8-23: Set accurate Content-Type based on file extension (optional but helpful)

Defaulting to application/octet-stream works but degrades preview. Infer MIME from the filename when possible.

+import { lookup as mimeLookup } from "mime-types";
@@
-  const blob = toBlob(data);
-  const mediaType = DEFAULT_MEDIA_TYPE;
+  const blob = toBlob(data);
+  const mediaType =
+    mimeLookup(fullPath) || DEFAULT_MEDIA_TYPE;

Also applies to: 68-76


66-69: Validate response shape; narrow types

Assume upload JSON shape and saved return are stable. If the action evolves, type assertions may mask bugs. Prefer runtime zod validation or at least basic guards.

Would you like a small zod schema to validate the { storageId } and UploadedFileInfo response?

Also applies to: 78-79

app/api/chat/route.ts (1)

249-261: Clear file accumulator after persisting to avoid accidental reuse

Prevents carry-over on subsequent regenerations in the same process.

             for (const message of messages) {
               await saveMessage({
                 chatId,
                 userId,
                 message,
                 extraFileIds:
                   message.role === "assistant"
                     ? (newFileIds as unknown as string[])
                     : undefined,
               });
             }
+            getFileAccumulator().clear?.();
convex/fileActions.ts (3)

400-406: Add a timeout to fetch the storage URL to avoid hanging actions.

A stuck fetch can hold action workers. Use AbortController with a reasonable timeout.

-    const response = await fetch(fileUrl);
+    const ac = new AbortController();
+    const t = setTimeout(() => ac.abort(), 30_000);
+    let response;
+    try {
+      response = await fetch(fileUrl, { signal: ac.signal });
+    } finally {
+      clearTimeout(t);
+    }

6-8: Unify LangChain loader imports (community vs core).

PDF/CSV come from @langchain/community while JSONLoader comes from langchain/. This mix often breaks with newer LangChain versions. Prefer the community package for consistency.

-import { JSONLoader } from "langchain/document_loaders/fs/json";
+import { JSONLoader } from "@langchain/community/document_loaders/fs/json";

Please confirm the repo’s LangChain versions so we can adjust all loaders consistently.

Also applies to: 300-303


391-391: Verify the userId contract (auth subject vs DB Id<'users'>).

actingUserId is a string (auth subject). Ensure internal.fileStorage.checkFileUploadLimit/saveFileToDb expect the same identifier, not an Id<'users'>. If they expect a DB id, add a lookup/mapper; if they expect subject, consider renaming to actorSubject for clarity.

Also applies to: 451-451

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95bb256 and d6dadcf.

📒 Files selected for processing (18)
  • .env.local.example (1 hunks)
  • app/api/chat/route.ts (2 hunks)
  • app/components/ReasoningHandler.tsx (1 hunks)
  • app/hooks/useChatHandlers.ts (1 hunks)
  • components/ai-elements/reasoning.tsx (1 hunks)
  • convex/fileActions.ts (3 hunks)
  • convex/fileStorage.ts (1 hunks)
  • convex/messages.ts (3 hunks)
  • lib/ai/providers.ts (1 hunks)
  • lib/ai/tools/index.ts (4 hunks)
  • lib/ai/tools/run-terminal-cmd.ts (6 hunks)
  • lib/ai/tools/utils/file-accumulator.ts (1 hunks)
  • lib/ai/tools/utils/sandbox-file-uploader.ts (1 hunks)
  • lib/ai/tools/write-file.ts (2 hunks)
  • lib/chat/chat-processor.ts (3 hunks)
  • lib/db/actions.ts (3 hunks)
  • lib/system-prompt.ts (0 hunks)
  • types/agent.ts (2 hunks)
💤 Files with no reviewable changes (1)
  • lib/system-prompt.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

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

**/*.{ts,tsx}: Use Id helper type from ./_generated/dataModel to type document IDs (e.g., Id<'users'>) instead of string
When defining Record types, specify key and value types matching validators (e.g., Record<Id<'users'>, string>)
Be strict with types for document IDs; prefer Id<'table'> over string in function args and variables
Use as const for string literals in discriminated unions
Declare arrays with explicit generic type: const arr: Array = [...]
Declare records with explicit generic types: const record: Record<KeyType, ValueType> = {...}

Files:

  • lib/ai/tools/write-file.ts
  • lib/chat/chat-processor.ts
  • types/agent.ts
  • convex/messages.ts
  • convex/fileStorage.ts
  • lib/ai/tools/utils/file-accumulator.ts
  • components/ai-elements/reasoning.tsx
  • app/components/ReasoningHandler.tsx
  • convex/fileActions.ts
  • lib/db/actions.ts
  • lib/ai/tools/utils/sandbox-file-uploader.ts
  • lib/ai/providers.ts
  • app/hooks/useChatHandlers.ts
  • lib/ai/tools/index.ts
  • lib/ai/tools/run-terminal-cmd.ts
  • app/api/chat/route.ts
convex/**/*.ts

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

convex/**/*.ts: Always use the new Convex function syntax (query/mutation/action objects with args/returns/handler) when defining Convex functions
When a function returns null, include returns: v.null() and return null explicitly
Use internalQuery/internalMutation/internalAction for private functions callable only by other Convex functions; do not expose sensitive logic via public query/mutation/action
Use query/mutation/action only for public API functions
Do not try to register functions via the api or internal objects
Always include argument and return validators for all Convex functions (query/internalQuery/mutation/internalMutation/action/internalAction)
In JS implementations, functions without an explicit return value implicitly return null
Use ctx.runQuery from queries/mutations/actions to call a query
Use ctx.runMutation from mutations/actions to call a mutation
Use ctx.runAction from actions to call an action
Only call an action from another action when crossing runtimes (e.g., V8 to Node); otherwise extract shared helper code
Minimize calls from actions to queries/mutations to avoid race conditions from splitting transactions
Pass FunctionReference values (from api/internal) to ctx.runQuery/ctx.runMutation/ctx.runAction; do not pass function implementations
When calling a function in the same file via ctx.run*, add an explicit return type annotation at the call site to avoid TS circularity
Use the generated api object for public functions and internal object for internal functions from convex/_generated/api.ts
Respect file-based routing for function references: e.g., convex/example.ts export f -> api.example.f; nested paths map to dot-separated namespaces
For paginated queries use paginationOptsValidator in args and .paginate(args.paginationOpts) on a query
v.bigint() is deprecated; use v.int64() for signed 64-bit integers
Use v.record(keys, values) for record-like data; v.map() and v.set() are not supported
For full-text search, use withSearchIndex("ind...

Files:

  • convex/messages.ts
  • convex/fileStorage.ts
  • convex/fileActions.ts
🧠 Learnings (5)
📚 Learning: 2025-09-06T13:31:08.519Z
Learnt from: RostyslavManko
PR: hackerai-tech/hackerai#21
File: app/api/chat/route.ts:121-128
Timestamp: 2025-09-06T13:31:08.519Z
Learning: The vision model (NEXT_PUBLIC_VISION_MODEL) used for processing media files also supports tool calling, so overriding to vision-model in Agent mode with media files does not break tool functionality.

Applied to files:

  • lib/chat/chat-processor.ts
📚 Learning: 2025-09-01T12:44:12.626Z
Learnt from: RostyslavManko
PR: hackerai-tech/hackerai#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/fileStorage.ts
  • convex/fileActions.ts
📚 Learning: 2025-08-29T13:33:09.937Z
Learnt from: CR
PR: hackerai-tech/hackerai#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-29T13:33:09.937Z
Learning: Applies to convex/**/*.ts : Use ctx.storage.getUrl(fileId) to obtain signed URLs; it returns null if the file does not exist

Applied to files:

  • convex/fileStorage.ts
  • convex/fileActions.ts
  • lib/ai/tools/utils/sandbox-file-uploader.ts
📚 Learning: 2025-08-27T12:38:06.662Z
Learnt from: RostyslavManko
PR: hackerai-tech/hackerai#14
File: convex/chats.ts:4-24
Timestamp: 2025-08-27T12:38:06.662Z
Learning: Convex functions with serviceKey parameters are designed for backend service-to-service communication and don't require ctx.auth.getUserIdentity() checks. The serviceKey validation against process.env.CONVEX_SERVICE_ROLE_KEY provides sufficient authentication for these backend operations.

Applied to files:

  • .env.local.example
📚 Learning: 2025-08-29T13:33:09.937Z
Learnt from: CR
PR: hackerai-tech/hackerai#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-29T13:33:09.937Z
Learning: Applies to convex/**/*.ts : Store and handle files as Blob objects; convert to/from Blob when using Convex storage

Applied to files:

  • lib/ai/tools/utils/sandbox-file-uploader.ts
🧬 Code graph analysis (12)
lib/ai/tools/write-file.ts (1)
lib/ai/tools/utils/sandbox-file-uploader.ts (1)
  • uploadSandboxFileToConvex (39-79)
types/agent.ts (1)
lib/ai/tools/utils/file-accumulator.ts (1)
  • FileAccumulator (3-21)
convex/fileStorage.ts (1)
convex/chats.ts (1)
  • validateServiceKey (5-9)
lib/ai/tools/utils/file-accumulator.ts (1)
convex/_generated/dataModel.d.ts (1)
  • Id (48-49)
app/components/ReasoningHandler.tsx (1)
components/ai-elements/reasoning.tsx (1)
  • Reasoning (37-66)
convex/fileActions.ts (1)
convex/chats.ts (1)
  • validateServiceKey (5-9)
lib/db/actions.ts (1)
lib/utils/file-token-utils.ts (1)
  • extractFileIdsFromParts (21-36)
lib/ai/tools/utils/sandbox-file-uploader.ts (1)
convex/_generated/dataModel.d.ts (1)
  • Id (48-49)
app/hooks/useChatHandlers.ts (2)
convex/_generated/api.js (2)
  • api (21-21)
  • api (21-21)
convex/messages.ts (2)
  • saveAssistantMessage (250-310)
  • regenerateWithNewContent (627-726)
lib/ai/tools/index.ts (1)
lib/ai/tools/utils/file-accumulator.ts (1)
  • FileAccumulator (3-21)
lib/ai/tools/run-terminal-cmd.ts (1)
lib/ai/tools/utils/sandbox-file-uploader.ts (1)
  • uploadSandboxFileToConvex (39-79)
app/api/chat/route.ts (3)
lib/ai/tools/index.ts (1)
  • createTools (17-83)
convex/messages.ts (1)
  • saveMessage (49-128)
lib/db/actions.ts (1)
  • saveMessage (46-81)
🪛 dotenv-linter (3.3.0)
.env.local.example

[warning] 10-10: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 10-10: [UnorderedKey] The CONVEX_SERVICE_ROLE_KEY key should go before the NEXT_PUBLIC_CONVEX_URL key

(UnorderedKey)

🔇 Additional comments (11)
convex/messages.ts (2)

250-310: Client save path looks good; confirm fileIds flow.

Auth is derived server-side (good). This path doesn’t accept fileIds, while service‑key saveMessage does. If the new FileAccumulator pipeline attaches files via the backend mutation only, this is fine; otherwise, consider parity here.

Can you confirm that assistant messages with attachments are always persisted via saveMessage (serviceKey) and never via saveAssistantMessage?


627-702: Index shape check for range query on _creationTime.

The chained .gt("_creationTime", message._creationTime) inside withIndex("by_chat_id", …) requires _creationTime to be the next field in that index. If by_chat_id doesn’t include _creationTime, add a composite index (chat_id, _creationTime) and keep this range query; otherwise pagination or a secondary query is safer.

Would you confirm the current definition of the by_chat_id index in your schema matches (chat_id, _creationTime)?

lib/ai/providers.ts (1)

13-16: Verify provider model IDs and implement fallbacks

  • Confirm qwen/qwen3-vl-235b-a22b-instruct is available (OpenRouter check passed).
  • OpenAI check returned null .data (authentication or endpoint issue); confirm gpt-4.1-2025-04-14 and gpt-4.1-mini-2025-04-14 exist and OPENAI_API_KEY is set.
  • Implement a fallback map (e.g., default to an OpenAI vision-capable model if OPENROUTER_API_KEY is missing) to avoid runtime failures.
lib/chat/chat-processor.ts (2)

18-37: selectModel invocations correctly pass the PDF flag
Verified that all calls to selectModel include the containsPdfFiles argument; no further changes needed.


112-114: Mapping Confirmed; Please Verify Pricing/Limits
The vision-model-for-pdfs entry is present in lib/ai/providers.ts (line 14). Ensure your external pricing and rate-limit configurations align with this model’s usage.

app/hooks/useChatHandlers.ts (2)

62-70: Endpoint renames verified—no stale “FromClient” references
No lingering old endpoint names detected in the codebase.


199-208: Align regenerate payload with server-side deletion

For non-temporary chats, the client still includes the last assistant message in body.messages even though the server deletes it—if the server uses that array for context, they’ll be out of sync. Confirm in app/api/chat/route.ts (or wherever regenerate is implemented) that the handler ignores the final assistant message in body.messages; otherwise, drop it from the payload in both temporary and non-temporary flows.

lib/ai/tools/write-file.ts (1)

39-56: Approve code: write→upload→accumulate flow is correct; ToolContext already includes fileAccumulator and userID.

convex/fileActions.ts (3)

364-366: Args surface for serviceKey/userId looks good; optional in dev, enforced via handler path.

This aligns with our pattern of making serviceKey optional for local DX while enforcing in prod via validation. No changes needed.

Based on learnings


373-387: Service‑role path is correctly gated and requires userId when serviceKey is used.

Good separation between service role and end‑user auth.


448-458: Good use of internal mutation from action (no DB access in actions).

Pattern is compliant with Convex guidelines.

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 (1)
app/hooks/useFileUpload.ts (1)

36-39: Good wrapper to match the util’s expected signature; consider locking the type.

Wrapping the mutation is correct. To prevent future drift and make intent explicit, add an explicit function type:

-  const generateUploadUrlFn = useCallback(() => generateUploadUrl({}), [
-    generateUploadUrl,
-  ]);
+  const generateUploadUrlFn: () => Promise<string> = useCallback(
+    () => generateUploadUrl({}),
+    [generateUploadUrl],
+  );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6dadcf and 1d6f0c4.

📒 Files selected for processing (1)
  • app/hooks/useFileUpload.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

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

**/*.{ts,tsx}: Use Id helper type from ./_generated/dataModel to type document IDs (e.g., Id<'users'>) instead of string
When defining Record types, specify key and value types matching validators (e.g., Record<Id<'users'>, string>)
Be strict with types for document IDs; prefer Id<'table'> over string in function args and variables
Use as const for string literals in discriminated unions
Declare arrays with explicit generic type: const arr: Array = [...]
Declare records with explicit generic types: const record: Record<KeyType, ValueType> = {...}

Files:

  • app/hooks/useFileUpload.ts
🧠 Learnings (5)
📚 Learning: 2025-08-29T13:33:09.937Z
Learnt from: CR
PR: hackerai-tech/hackerai#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-29T13:33:09.937Z
Learning: Applies to convex/**/*.ts : Use ctx.storage.getUrl(fileId) to obtain signed URLs; it returns null if the file does not exist

Applied to files:

  • app/hooks/useFileUpload.ts
📚 Learning: 2025-08-29T13:33:09.937Z
Learnt from: CR
PR: hackerai-tech/hackerai#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-29T13:33:09.937Z
Learning: Applies to convex/**/*.ts : Use the generated api object for public functions and internal object for internal functions from convex/_generated/api.ts

Applied to files:

  • app/hooks/useFileUpload.ts
📚 Learning: 2025-08-29T13:33:09.937Z
Learnt from: CR
PR: hackerai-tech/hackerai#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-29T13:33:09.937Z
Learning: Applies to convex/**/*.ts : Always use the new Convex function syntax (query/mutation/action objects with args/returns/handler) when defining Convex functions

Applied to files:

  • app/hooks/useFileUpload.ts
📚 Learning: 2025-08-29T13:33:09.937Z
Learnt from: CR
PR: hackerai-tech/hackerai#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-29T13:33:09.937Z
Learning: Applies to convex/**/*.ts : Use query/mutation/action only for public API functions

Applied to files:

  • app/hooks/useFileUpload.ts
📚 Learning: 2025-08-29T13:33:09.937Z
Learnt from: CR
PR: hackerai-tech/hackerai#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-29T13:33:09.937Z
Learning: Applies to convex/**/*.ts : Store and handle files as Blob objects; convert to/from Blob when using Convex storage

Applied to files:

  • app/hooks/useFileUpload.ts
🧬 Code graph analysis (1)
app/hooks/useFileUpload.ts (1)
convex/fileStorage.ts (1)
  • generateUploadUrl (59-115)
🔇 Additional comments (1)
app/hooks/useFileUpload.ts (1)

180-186: Confirm per-file URL invocation
uploadSingleFileToConvex invokes generateUploadUrlFn on each call, ensuring a fresh, single-use URL per file.

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)
app/components/FilePartRenderer.tsx (1)

163-181: Fix stale renderedFilePart memo dependencies.
renderedFilePart closes over totalFileParts and reads part.name, part.filename, and part.mediaType, but these values aren’t listed in the dependency array. When any of them change (e.g., additional saved files arrive), the memo keeps returning the old element, so image layouts and metadata stay stuck on the previous values. Add every consumed field to the dependency list to keep the preview in sync.

-  }, [messageId, partIndex, part.url, part.fileId]);
+  }, [
+    messageId,
+    partIndex,
+    part.url,
+    part.fileId,
+    part.name,
+    part.filename,
+    part.mediaType,
+    totalFileParts,
+  ]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d6f0c4 and 6b0186d.

📒 Files selected for processing (10)
  • app/api/chat/route.ts (3 hunks)
  • app/components/FilePartRenderer.tsx (5 hunks)
  • app/components/Messages.tsx (4 hunks)
  • app/hooks/useChatHandlers.ts (1 hunks)
  • app/hooks/useFileUpload.ts (2 hunks)
  • lib/ai/tools/get-terminal-files.ts (1 hunks)
  • lib/ai/tools/index.ts (5 hunks)
  • lib/ai/tools/run-terminal-cmd.ts (3 hunks)
  • lib/ai/tools/write-file.ts (2 hunks)
  • lib/utils/message-utils.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/hooks/useChatHandlers.ts
  • app/hooks/useFileUpload.ts
  • lib/ai/tools/write-file.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

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

**/*.{ts,tsx}: Use Id helper type from ./_generated/dataModel to type document IDs (e.g., Id<'users'>) instead of string
When defining Record types, specify key and value types matching validators (e.g., Record<Id<'users'>, string>)
Be strict with types for document IDs; prefer Id<'table'> over string in function args and variables
Use as const for string literals in discriminated unions
Declare arrays with explicit generic type: const arr: Array = [...]
Declare records with explicit generic types: const record: Record<KeyType, ValueType> = {...}

Files:

  • lib/ai/tools/get-terminal-files.ts
  • lib/ai/tools/index.ts
  • lib/ai/tools/run-terminal-cmd.ts
  • app/components/Messages.tsx
  • app/components/FilePartRenderer.tsx
  • app/api/chat/route.ts
  • lib/utils/message-utils.ts
🧠 Learnings (3)
📓 Common learnings
Learnt from: RostyslavManko
PR: hackerai-tech/hackerai#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.
📚 Learning: 2025-08-29T13:33:09.937Z
Learnt from: CR
PR: hackerai-tech/hackerai#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-29T13:33:09.937Z
Learning: Applies to **/*.{ts,tsx} : Be strict with types for document IDs; prefer Id<'table'> over string in function args and variables

Applied to files:

  • app/api/chat/route.ts
📚 Learning: 2025-08-29T13:33:09.937Z
Learnt from: CR
PR: hackerai-tech/hackerai#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-29T13:33:09.937Z
Learning: Applies to **/*.{ts,tsx} : Use Id helper type from ./_generated/dataModel to type document IDs (e.g., Id<'users'>) instead of string

Applied to files:

  • app/api/chat/route.ts
🧬 Code graph analysis (4)
lib/ai/tools/get-terminal-files.ts (2)
types/agent.ts (1)
  • ToolContext (17-26)
lib/ai/tools/utils/sandbox-file-uploader.ts (1)
  • uploadSandboxFileToConvex (39-79)
lib/ai/tools/index.ts (2)
lib/ai/tools/utils/file-accumulator.ts (1)
  • FileAccumulator (3-21)
lib/ai/tools/get-terminal-files.ts (1)
  • createGetTerminalFiles (6-68)
app/components/Messages.tsx (3)
types/chat.ts (1)
  • ChatMessage (68-68)
lib/utils/message-utils.ts (1)
  • extractSavedFilesFromMessage (113-149)
app/components/FilePartRenderer.tsx (1)
  • FilePartRenderer (200-215)
app/api/chat/route.ts (3)
lib/ai/tools/index.ts (1)
  • createTools (18-85)
lib/db/actions.ts (1)
  • saveMessage (46-81)
convex/messages.ts (1)
  • saveMessage (49-128)
🔇 Additional comments (1)
app/api/chat/route.ts (1)

249-259: Restore Id<'files'> type-safety for saved assistant attachments.

getFileAccumulator().getAll() already returns Array<Id<"files">>, yet we downcast to string[] to satisfy saveMessage’s current signature (which then casts back to any). This breaks the compile-time guarantee that we only persist Convex file IDs and reopens the door to mixing arbitrary strings in fileIds, despite the Convex mutation expecting v.id("files"). Please carry the Id<'files'> type end-to-end instead of casting.

Recommended fix: widen saveMessage to accept Array<Id<"files">> and drop the casts here.

-              const newFileIds = getFileAccumulator().getAll();
+              const newFileIds = getFileAccumulator().getAll();
@@
-                  extraFileIds:
-                    message.role === "assistant"
-                      ? (newFileIds as unknown as string[])
-                      : undefined,
+                  extraFileIds:
+                    message.role === "assistant" ? newFileIds : undefined,

And in lib/db/actions.ts:

+import type { Id } from "@/convex/_generated/dataModel";
@@
-  extraFileIds?: string[];
+  extraFileIds?: Array<Id<"files">>;
@@
-  const mergedFileIds = [
-    ...fileIds,
-    ...((extraFileIds || []).filter(Boolean) as string[]),
-  ];
+  const mergedFileIds: Array<Id<"files">> = [
+    ...(fileIds as Array<Id<"files">>),
+    ...((extraFileIds || []).filter(Boolean) as Array<Id<"files">>),
+  ];
@@
-      fileIds: mergedFileIds.length > 0 ? (mergedFileIds as any) : undefined,
+      fileIds: mergedFileIds.length > 0 ? mergedFileIds : undefined,

This keeps us aligned with the Convex schema and the project guideline to use Id<'table'> for document identifiers. As per coding guidelines

This was referenced Jan 16, 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