Skip to content

feat: drag-and-drop file upload for sandbox file tree#1601

Merged
sweetmantech merged 7 commits into
testfrom
feature/sandbox-file-upload-chat
Mar 27, 2026
Merged

feat: drag-and-drop file upload for sandbox file tree#1601
sweetmantech merged 7 commits into
testfrom
feature/sandbox-file-upload-chat

Conversation

@recoup-coding-agent

@recoup-coding-agent recoup-coding-agent commented Mar 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Integrates useDragAndDrop hook into SandboxFileTree component
  • Upload overlay with visual feedback during drag events
  • Progress state and toast notifications for upload status
  • Calls POST /api/sandboxes/files endpoint for file commits

Related

  • Paperclip: REC-32 (Chat sub-issue of REC-29)
  • Depends on: api PR for POST /api/sandboxes/files endpoint

Test plan

  • Drag files onto file tree triggers upload
  • Upload overlay appears during drag
  • Success/error toast notifications
  • Files appear in tree after upload
  • Multiple file upload works

Summary by CodeRabbit

  • New Features
    • Drag-and-drop file upload for sandboxes with interactive dropzones (including empty-state).
    • Uploads target the currently selected folder in the file tree.
    • Visual uploading overlays and real-time progress feedback during transfers.
    • Success, warning, and error toast notifications after uploads.
    • Upload limits: up to 100 files per batch, 100MB per file.

@vercel

vercel Bot commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
recoup-chat Ready Ready Preview Mar 27, 2026 10:50pm

Request Review

@coderabbitai

coderabbitai Bot commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@sweetmantech has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 3 minutes and 37 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 3 minutes and 37 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4b6c6437-eaeb-418a-a3e4-66e0ebc66b11

📥 Commits

Reviewing files that changed from the base of the PR and between ce3e0b1 and 90af771.

📒 Files selected for processing (2)
  • app/api/sandbox/upload/route.ts
  • hooks/useSandboxFileDrop.ts
📝 Walkthrough

Walkthrough

Adds drag-and-drop file upload to the sandbox file tree: new drop-zone UI and empty-state component, a hook to handle drops and auth, a client upload utility that uses Vercel Blob, and a new POST API route to produce presigned upload tokens.

Changes

Cohort / File(s) Summary
Sandbox file tree & UI
components/Sandboxes/SandboxFileTree.tsx, components/Sandboxes/SandboxDropZone.tsx, components/Sandboxes/NoSandboxFiles.tsx
Integrated drop-zone into SandboxFileTree; replaced static empty state with NoSandboxFiles; added SandboxDropZone to wrap content and display drag/upload overlays.
File-drop hook
hooks/useSandboxFileDrop.ts
New hook that resolves target path from selectedPath/filetree, obtains auth token, calls upload mutation, shows toasts, and refetches file list.
Upload utilities
lib/sandboxes/uploadSandboxFiles.ts, lib/sandboxes/findFileNode.ts
Added uploadSandboxFiles to upload files to Vercel Blob then POST metadata to backend; added findFileNode helper to resolve nodes by path.
Server API
app/api/sandbox/upload/route.ts
New POST endpoint that authenticates bearer token and delegates to Vercel Blob handleUpload, enforcing a 100MB file-size limit and returning token payload.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant UI as SandboxFileTree / SandboxDropZone / NoSandboxFiles
    participant Hook as useSandboxFileDrop
    participant Util as uploadSandboxFiles
    participant TokenAPI as /api/sandbox/upload
    participant Blob as Vercel Blob
    participant BackendAPI as /api/sandboxes/files

    User->>UI: Drop files
    UI->>Hook: handleFilesDropped(files)
    Hook->>Hook: resolve target path (findFileNode)
    Hook->>Hook: get access token via usePrivy()
    Hook->>Util: uploadSandboxFiles({accessToken, files, path})
    Util->>TokenAPI: POST to /api/sandbox/upload (auth header)
    TokenAPI->>Blob: handleUpload (generate presigned tokens)
    Blob-->>TokenAPI: tokens
    TokenAPI-->>Util: token response
    par upload files concurrently
        Util->>Blob: upload(fileN, clientPayload)
        Blob-->>Util: blob URL/metadata
    end
    Util->>BackendAPI: POST /api/sandboxes/files (blob URLs + metadata)
    BackendAPI-->>Util: {uploaded, errors?}
    Util-->>Hook: response
    Hook->>UI: refetch()
    UI-->>User: updated file tree / toasts
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly Related PRs

Poem

✨ Files glide in on a gentle breeze,
Drop zones open with graceful ease,
Blobs are minted, metadata sings,
The sandbox blooms with uploaded things! 📁⬇️

🚥 Pre-merge checks | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning Code violates DRY principle with hardcoded upload constraints and duplicated error handling patterns across multiple files. Extract upload constraints into a shared constants file and create a utility function for consistent error message handling across all components.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/sandbox-file-upload-chat

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 229df71c71

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +34 to +37
// If selected path looks like a file (has extension), use parent dir
const lastPart = parts[parts.length - 1];
if (lastPart.includes(".")) {
targetPath = parts.slice(0, -1).join("/");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Determine upload target type from tree metadata

This branch infers “file vs folder” by checking for . in the selected path segment, which misclassifies common repo entries (for example, Dockerfile is a file with no dot and .github is a folder with a dot). In those cases the computed path is wrong, so dropped files can be uploaded to the wrong directory or fail to upload. Use the selected node’s actual type from the file tree instead of filename heuristics.

Useful? React with 👍 / 👎.

Comment thread hooks/useLaunchCampaign.ts Outdated
Comment on lines +85 to +87
if (!accessToken) {
setError("Not authenticated. Please sign in.");
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fetch a fresh token when submitting launch generation

generate exits early whenever accessToken is falsy, but this token is loaded asynchronously by useAccessToken, so a signed-in user can submit quickly after page load and hit a false “Not authenticated” error with no request sent. This also risks failures after token rotation because the callback uses cached state. Resolve the token at submit time (or disable submit until token load completes) to remove the auth race.

Useful? React with 👍 / 👎.

Integrates react-dropzone drag-and-drop with the /files page to upload
files to GitHub sandbox repos via POST /api/sandboxes/files. Shows
upload overlay, progress state, and toast notifications on completion.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@sweetmantech sweetmantech force-pushed the feature/sandbox-file-upload-chat branch from 229df71 to f11c49f Compare March 27, 2026 17:28
const { getAccessToken } = usePrivy();
const [uploading, setUploading] = useState(false);

const handleFilesDropped = async (files: File[]) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

OCP: make a new hook for the useFileDrop functions / state.

Comment on lines +111 to +135
<div
{...getRootProps()}
className={`flex w-full flex-col gap-4 lg:flex-row rounded-lg transition-all relative ${
isDragging ? "ring-2 ring-primary ring-offset-2 bg-primary/5" : ""
}`}
>
<input {...getInputProps()} />

{isDragging && (
<div className="absolute inset-0 flex items-center justify-center bg-primary/10 backdrop-blur-sm rounded-lg z-10 pointer-events-none">
<div className="flex flex-col items-center gap-3 text-primary">
<Upload className="h-12 w-12 animate-bounce" />
<p className="text-lg font-medium">Drop files to upload to repository</p>
</div>
</div>
)}

{uploading && (
<div className="absolute inset-0 flex items-center justify-center bg-background/50 backdrop-blur-sm rounded-lg z-10">
<div className="flex items-center gap-2 text-muted-foreground">
<Loader className="h-4 w-4 animate-spin" />
<span>Uploading...</span>
</div>
</div>
)}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

OCP - new component file for the drag-n-drop wrapper.

- Add /api/sandbox-upload route for generating presigned blob tokens
- Update uploadSandboxFiles to upload to Vercel Blob first, then
  pass blob URLs to the API (bypasses 4.5MB serverless limit)
- Add @vercel/blob dependency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (4)
app/api/sandbox-upload/route.ts (1)

23-23: Empty onUploadCompleted callback is acceptable but consider adding logging.

The empty callback is valid since cleanup happens in the downstream API call. However, adding observability (e.g., structured logging) here could help with debugging upload issues in production.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/sandbox-upload/route.ts` at line 23, The empty onUploadCompleted
callback currently provides no observability; update the onUploadCompleted
implementation to emit a structured log when uploads complete (e.g., using the
existing logger or processLogger), include key context such as upload
id/filename/user/client IP and any status/metadata available, and ensure errors
thrown inside the callback are caught and logged; modify the onUploadCompleted
function reference in route.ts to call logger.info/logger.error with those
fields to aid debugging.
lib/sandboxes/uploadSandboxFiles.ts (2)

41-47: Sequential uploads create unnecessary latency - consider parallelizing.

The for...of loop with await uploads files one at a time. For multiple files, this significantly increases total upload time. Using Promise.all or Promise.allSettled would upload files concurrently.

⚡ Proposed parallel upload
-  const blobFiles: { url: string; name: string }[] = [];
-
-  for (const file of files) {
-    const blob = await upload(file.name, file, {
-      access: "public",
-      handleUploadUrl: "/api/sandbox-upload",
-    });
-    blobFiles.push({ url: blob.url, name: file.name });
-  }
+  const blobFiles = await Promise.all(
+    files.map(async (file) => {
+      const blob = await upload(file.name, file, {
+        access: "public",
+        handleUploadUrl: "/api/sandbox-upload",
+      });
+      return { url: blob.url, name: file.name };
+    })
+  );

Note: If you need partial success handling (some uploads succeed, others fail), use Promise.allSettled instead and filter results.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/sandboxes/uploadSandboxFiles.ts` around lines 41 - 47, The current
sequential upload loop (for...of awaiting upload) in uploadSandboxFiles.ts
causes unnecessary latency; change it to run uploads concurrently by mapping
files to upload(file.name, file, {...}) promises and awaiting them with
Promise.all or Promise.allSettled, then populate blobFiles from the resolved
results (use Promise.allSettled if you need partial success handling and filter
fulfilled results before pushing { url: result.value.url, name: file.name } into
blobFiles). Ensure you still preserve the access and handleUploadUrl options
passed to the upload function.

49-60: Add request timeout to prevent hanging on slow/unresponsive API.

The fetch call has no timeout configured. If the external API is slow or unresponsive, this could leave users waiting indefinitely with the "Uploading..." overlay.

⏱️ Proposed fix with AbortController
+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
+
   const response = await fetch(`${NEW_API_BASE_URL}/api/sandboxes/files`, {
     method: "POST",
     headers: {
       Authorization: `Bearer ${accessToken}`,
       "Content-Type": "application/json",
     },
     body: JSON.stringify({
       files: blobFiles,
       path,
       message,
     }),
+    signal: controller.signal,
   });
+
+  clearTimeout(timeoutId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/sandboxes/uploadSandboxFiles.ts` around lines 49 - 60, The fetch in
uploadSandboxFiles.ts that posts to `${NEW_API_BASE_URL}/api/sandboxes/files`
(using accessToken, blobFiles, path, message) needs a timeout to avoid hanging;
wrap the request with an AbortController, pass controller.signal into fetch, set
a setTimeout to call controller.abort() after a chosen timeout (e.g., 15–30s),
and clear that timeout on success or failure so you don't leak timers; ensure
you handle the abort/timeout case (AbortError) and propagate/handle it the same
way other errors are handled by uploadSandboxFiles.
components/Sandboxes/SandboxFileTree.tsx (1)

97-99: Consider using cn() utility for cleaner conditional class merging.

Per coding guidelines, use the cn() utility (combining clsx and tailwind-merge) for class merging instead of template literals with ternaries.

✨ Example refactor
+import { cn } from "@/lib/utils";

// Empty state (line 97-99)
-        className={`w-full max-w-md rounded-lg p-8 border-2 border-dashed transition-all ${
-          isDragging ? "border-primary bg-primary/5" : "border-muted-foreground/25"
-        }`}
+        className={cn(
+          "w-full max-w-md rounded-lg p-8 border-2 border-dashed transition-all",
+          isDragging ? "border-primary bg-primary/5" : "border-muted-foreground/25"
+        )}

// Main container (line 113-115)
-      className={`flex w-full flex-col gap-4 lg:flex-row rounded-lg transition-all relative ${
-        isDragging ? "ring-2 ring-primary ring-offset-2 bg-primary/5" : ""
-      }`}
+      className={cn(
+        "flex w-full flex-col gap-4 lg:flex-row rounded-lg transition-all relative",
+        isDragging && "ring-2 ring-primary ring-offset-2 bg-primary/5"
+      )}

Also applies to: 113-115

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Sandboxes/SandboxFileTree.tsx` around lines 97 - 99, Replace the
template-literal className concatenation in the SandboxFileTree JSX with the
cn() utility: import cn (if not already) and call cn("w-full max-w-md rounded-lg
p-8 border-2 border-dashed transition-all", isDragging ? "border-primary
bg-primary/5" : "border-muted-foreground/25") (or use conditional entries like
isDragging && "border-primary bg-primary/5") to let clsx/tailwind-merge handle
merging; apply the same refactor to the second occurrence referenced (the
className around lines 113-115) so both usages in the SandboxFileTree component
use cn() instead of template literals.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/sandbox-upload/route.ts`:
- Around line 11-12: Move the JSON parsing into the existing try-catch inside
the POST handler so malformed JSON is handled gracefully: inside the POST
function's try block call await request.json() and cast to HandleUploadBody
(instead of parsing before the try), catch parsing errors (e.g., SyntaxError or
any thrown error) and return a 400 NextResponse with a clear message, and keep
the rest of the logic that uses the parsed body unchanged (refer to the POST
function and the HandleUploadBody type to locate where to move the parsing).
- Around line 11-32: The POST handler currently calls handleUpload and returns
presigned tokens without authenticating the caller; update the
onBeforeGenerateToken callback passed to handleUpload to extract and validate
the auth token (e.g., from request.headers), verify the caller's account
identity/permissions (same checks used by files/get-signed-url), and only return
the maximumSizeInBytes object when the accountId is validated; if validation
fails, throw or return an authentication error (HTTP 401/403) so unauthenticated
users cannot get presigned upload tokens—update SandboxFileTree.tsx to send the
same auth token (usePrivy()) in request headers when invoking this endpoint.

In `@components/Sandboxes/SandboxFileTree.tsx`:
- Around line 95-106: The dropzone lacks ARIA attributes—update the JSX where
getRootProps() and getInputProps() are used (in SandboxFileTree) to add a
descriptive aria-label (e.g., aria-label="File upload dropzone") and a role of
"button" or "region" as appropriate, and reflect the drag state with an
aria-live or aria-pressed/aria-busy attribute bound to isDragging so screen
readers get state changes; also add aria-hidden or hidden text for the
empty-state prompt (the paragraph and Upload icon) using
aria-hidden={isDragging} or an offscreen visually-hidden description and ensure
the <input {...getInputProps()} /> has an accessible id/aria-describedby
referencing that description; apply the same attribute additions to the other
dropzone markup used elsewhere in this component.
- Around line 32-41: The current selected-path handling in the SandboxFileTree
assumes any segment containing "." is a file which misclassifies dotfiles and
dotted directories; update the logic in the block that reads
fileContent.selectedPath (and sets targetPath) to resolve file vs directory by
consulting the sandbox file model instead of string matching — either use an
existing file-tree lookup (e.g. check the node for fileContent.selectedPath in
the tree or call a helper like findNode/lookupNode) or expose and consume an
explicit isFile/type flag from useSandboxFileContent; if the resolved node is a
file, set targetPath to its parent directory, otherwise leave targetPath as the
directory path.

In `@lib/sandboxes/uploadSandboxFiles.ts`:
- Around line 41-66: The current flow uploads files via the upload(...) call and
accumulates blob metadata in blobFiles, but if the POST to
`${NEW_API_BASE_URL}/api/sandboxes/files` (JSON parsed into
UploadSandboxFilesResponse) fails the blobs remain orphaned; wrap the fetch +
response.json() + error check in a try/catch (or a finally) and on any failure
iterate blobFiles and call the appropriate Vercel blob delete/cleanup operation
for each uploaded blob (use the blob object returned by upload, e.g., items in
blobFiles or the original upload result), then rethrow the error so callers
still see the failure—update code paths referencing upload, blobFiles,
NEW_API_BASE_URL, and UploadSandboxFilesResponse accordingly.

---

Nitpick comments:
In `@app/api/sandbox-upload/route.ts`:
- Line 23: The empty onUploadCompleted callback currently provides no
observability; update the onUploadCompleted implementation to emit a structured
log when uploads complete (e.g., using the existing logger or processLogger),
include key context such as upload id/filename/user/client IP and any
status/metadata available, and ensure errors thrown inside the callback are
caught and logged; modify the onUploadCompleted function reference in route.ts
to call logger.info/logger.error with those fields to aid debugging.

In `@components/Sandboxes/SandboxFileTree.tsx`:
- Around line 97-99: Replace the template-literal className concatenation in the
SandboxFileTree JSX with the cn() utility: import cn (if not already) and call
cn("w-full max-w-md rounded-lg p-8 border-2 border-dashed transition-all",
isDragging ? "border-primary bg-primary/5" : "border-muted-foreground/25") (or
use conditional entries like isDragging && "border-primary bg-primary/5") to let
clsx/tailwind-merge handle merging; apply the same refactor to the second
occurrence referenced (the className around lines 113-115) so both usages in the
SandboxFileTree component use cn() instead of template literals.

In `@lib/sandboxes/uploadSandboxFiles.ts`:
- Around line 41-47: The current sequential upload loop (for...of awaiting
upload) in uploadSandboxFiles.ts causes unnecessary latency; change it to run
uploads concurrently by mapping files to upload(file.name, file, {...}) promises
and awaiting them with Promise.all or Promise.allSettled, then populate
blobFiles from the resolved results (use Promise.allSettled if you need partial
success handling and filter fulfilled results before pushing { url:
result.value.url, name: file.name } into blobFiles). Ensure you still preserve
the access and handleUploadUrl options passed to the upload function.
- Around line 49-60: The fetch in uploadSandboxFiles.ts that posts to
`${NEW_API_BASE_URL}/api/sandboxes/files` (using accessToken, blobFiles, path,
message) needs a timeout to avoid hanging; wrap the request with an
AbortController, pass controller.signal into fetch, set a setTimeout to call
controller.abort() after a chosen timeout (e.g., 15–30s), and clear that timeout
on success or failure so you don't leak timers; ensure you handle the
abort/timeout case (AbortError) and propagate/handle it the same way other
errors are handled by uploadSandboxFiles.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fad60805-ae76-4c83-ae60-f8f9f9367cf6

📥 Commits

Reviewing files that changed from the base of the PR and between 6c01058 and 4a8dd04.

⛔ Files ignored due to path filters (2)
  • package.json is excluded by none and included by none
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml and included by none
📒 Files selected for processing (3)
  • app/api/sandbox-upload/route.ts
  • components/Sandboxes/SandboxFileTree.tsx
  • lib/sandboxes/uploadSandboxFiles.ts

Comment thread app/api/sandbox-upload/route.ts Outdated
Comment on lines +11 to +12
export async function POST(request: Request): Promise<NextResponse> {
const body = (await request.json()) as HandleUploadBody;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Move JSON parsing inside try-catch for robust error handling.

The request.json() call on line 12 is outside the try-catch block. If the request body is malformed JSON, this will throw an unhandled exception resulting in a 500 error instead of a controlled 400 response.

🛡️ Proposed fix
 export async function POST(request: Request): Promise<NextResponse> {
-  const body = (await request.json()) as HandleUploadBody;
-
   try {
+    const body = (await request.json()) as HandleUploadBody;
+
     const jsonResponse = await handleUpload({
       body,
       request,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/sandbox-upload/route.ts` around lines 11 - 12, Move the JSON parsing
into the existing try-catch inside the POST handler so malformed JSON is handled
gracefully: inside the POST function's try block call await request.json() and
cast to HandleUploadBody (instead of parsing before the try), catch parsing
errors (e.g., SyntaxError or any thrown error) and return a 400 NextResponse
with a clear message, and keep the rest of the logic that uses the parsed body
unchanged (refer to the POST function and the HandleUploadBody type to locate
where to move the parsing).

Comment thread app/api/sandbox/upload/route.ts
Comment thread components/Sandboxes/SandboxFileTree.tsx Outdated
Comment thread components/Sandboxes/SandboxFileTree.tsx Outdated
Comment thread lib/sandboxes/uploadSandboxFiles.ts Outdated
Comment on lines +41 to +66
for (const file of files) {
const blob = await upload(file.name, file, {
access: "public",
handleUploadUrl: "/api/sandbox-upload",
});
blobFiles.push({ url: blob.url, name: file.name });
}

const response = await fetch(`${NEW_API_BASE_URL}/api/sandboxes/files`, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
files: blobFiles,
path,
message,
}),
});

const data: UploadSandboxFilesResponse = await response.json();

if (!response.ok || data.status === "error") {
throw new Error(data.error || "Failed to upload files");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's any blob cleanup logic elsewhere in the codebase
rg -n "del.*blob|delete.*blob|cleanup.*blob" --type ts -i

Repository: recoupable/chat

Length of output: 270


🏁 Script executed:

fd uploadSandboxFiles.ts

Repository: recoupable/chat

Length of output: 94


🏁 Script executed:

cat -n lib/sandboxes/uploadSandboxFiles.ts

Repository: recoupable/chat

Length of output: 2439


🏁 Script executed:

fd -name "*sandbox*" -type f | grep -E "\.(ts|tsx)$"

Repository: recoupable/chat

Length of output: 289


🏁 Script executed:

rg -n "sandbox-upload|/api/sandboxes" -A 5 -B 2

Repository: recoupable/chat

Length of output: 3023


🏁 Script executed:

fd -path "*/api/sandboxes/files*" -type f

Repository: recoupable/chat

Length of output: 227


🏁 Script executed:

rg "sandboxes.*files|/api/sandboxes/files" -A 20 --type ts

Repository: recoupable/chat

Length of output: 1341


🏁 Script executed:

fd -path "*/app/api*" -type f | head -20

Repository: recoupable/chat

Length of output: 227


🏁 Script executed:

find app/api -type d -name sandboxes

Repository: recoupable/chat

Length of output: 41


🏁 Script executed:

find app/api -type f -name "route.ts" | grep sandbox

Repository: recoupable/chat

Length of output: 90


🏁 Script executed:

cat -n app/api/sandboxes/files/route.ts 2>/dev/null || echo "File not found, searching..."

Repository: recoupable/chat

Length of output: 87


🏁 Script executed:

fd "route.ts" | xargs grep -l "sandboxes.*files\|files.*blob" 2>/dev/null

Repository: recoupable/chat

Length of output: 43


🏁 Script executed:

find app/api -type f -name "route.ts" | head -20

Repository: recoupable/chat

Length of output: 681


🏁 Script executed:

rg "sandboxes/files|del.*blob|cleanup" --type-add='api:*.ts' --type api

Repository: recoupable/chat

Length of output: 411


🏁 Script executed:

rg "NEW_API_BASE_URL" -A 2 -B 2

Repository: recoupable/chat

Length of output: 21111


🏁 Script executed:

grep -r "delete.*blob\|cleanup" app/api/sandbox-upload/route.ts 2>/dev/null || echo "Checking for blob cleanup in upload handler..."

Repository: recoupable/chat

Length of output: 105


🏁 Script executed:

cat -n app/api/sandbox-upload/route.ts

Repository: recoupable/chat

Length of output: 1270


Add blob cleanup on API request failure to prevent orphaned storage.

Vercel Blobs uploaded at lines 41-47 are orphaned if the subsequent POST to /api/sandboxes/files fails (lines 49-65). While the JSDoc documents that cleanup is the API's responsibility, there's no client-side recovery if that endpoint fails, potentially accumulating storage costs over time.

Consider either:

  1. Wrapping the API call in a try-catch to clean up uploaded blobs on failure
  2. Verifying the external API endpoint has robust cleanup logic for its own failures
  3. Implementing scheduled cleanup for uncommitted blobs (time-based or manual purge)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/sandboxes/uploadSandboxFiles.ts` around lines 41 - 66, The current flow
uploads files via the upload(...) call and accumulates blob metadata in
blobFiles, but if the POST to `${NEW_API_BASE_URL}/api/sandboxes/files` (JSON
parsed into UploadSandboxFilesResponse) fails the blobs remain orphaned; wrap
the fetch + response.json() + error check in a try/catch (or a finally) and on
any failure iterate blobFiles and call the appropriate Vercel blob
delete/cleanup operation for each uploaded blob (use the blob object returned by
upload, e.g., items in blobFiles or the original upload result), then rethrow
the error so callers still see the failure—update code paths referencing upload,
blobFiles, NEW_API_BASE_URL, and UploadSandboxFilesResponse accordingly.

Comment thread app/api/sandbox/upload/route.ts
- Extract file drop logic to useSandboxFileDrop hook (OCP)
- Extract drag-n-drop wrapper to SandboxDropZone component (OCP)
- Move route from sandbox-upload to sandbox/upload (KISS)
- Add auth check to blob upload endpoint
- Move JSON parsing inside try-catch
- Fix file vs directory detection using FileNode.type instead of dot heuristic
- Parallelize blob uploads with Promise.all
- Add ARIA labels for accessibility
- Use cn() utility for class merging

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment on lines +55 to +72
<div
{...getRootProps()}
role="region"
aria-label="File upload dropzone"
className={cn(
"w-full max-w-md rounded-lg p-8 border-2 border-dashed transition-all",
isDragging
? "border-primary bg-primary/5"
: "border-muted-foreground/25",
)}
>
<input {...getInputProps()} aria-label="Upload files" />
<div className="flex flex-col items-center gap-3 text-muted-foreground">
<Upload className="h-8 w-8" />
<p className="text-sm">
No files yet. Drag and drop files here to upload.
</p>
</div>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

SRP - new component file for NoSandboxFiles.tsx

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Done — extracted to components/Sandboxes/NoSandboxFiles.tsx.

Comment thread hooks/useSandboxFileDrop.ts Outdated
/**
* Finds a node in the file tree by path.
*/
function findNode(nodes: FileNode[], path: string): FileNode | undefined {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

SRP - new lib file for findNode

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Done — extracted to lib/sandboxes/findFileNode.ts.

Comment thread hooks/useSandboxFileDrop.ts Outdated
const { getAccessToken } = usePrivy();
const [uploading, setUploading] = useState(false);

const handleFilesDropped = useCallback(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would it simplify to use tanstack useMutation here? Or, would that make this implementation more complex?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good call — switched to useMutation. It simplified things: removed manual useState for uploading, and onSuccess/onError callbacks handle toasts + refetch cleanly.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
hooks/useSandboxFileDrop.ts (1)

10-19: findNode is a pure utility that could be co-located with FileNode type.

The recursive tree search is clean and correct. Since it has no dependencies on hook state, consider moving it to lib/sandboxes/parseFileTree.ts alongside the FileNode type definition for better discoverability and potential reuse.

This is a minor organizational suggestion—the current placement is acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/useSandboxFileDrop.ts` around lines 10 - 19, Move the pure utility
function findNode (which accepts nodes: FileNode[] and path: string and returns
FileNode | undefined) out of hooks/useSandboxFileDrop.ts into the shared module
alongside the FileNode type (e.g., lib/sandboxes/parseFileTree.ts); export
findNode from that module and update imports where used so callers reference the
new exported utility instead of the local definition.
components/Sandboxes/SandboxFileTree.tsx (1)

23-28: Hook instantiated even when output is unused in non-empty state.

The useDragAndDrop hook at lines 23-28 runs on every render per React's rules of hooks, but its output (getRootProps, getInputProps, isDragging) is only used in the empty state (lines 55-66). When filetree.length > 0, SandboxDropZone creates its own independent dropzone.

This creates unnecessary event listener setup. Consider extracting the empty state into a separate component that owns its own hook, mirroring how SandboxDropZone encapsulates its dropzone:

♻️ Optional refactor sketch
// components/Sandboxes/SandboxEmptyDropZone.tsx
function SandboxEmptyDropZone({ onDrop, uploading }: { onDrop: (files: File[]) => void; uploading: boolean }) {
  const { getRootProps, getInputProps, isDragging } = useDragAndDrop({
    onDrop,
    maxFiles: 100,
    maxSizeMB: 100,
    disabled: uploading,
  });
  // ... empty state UI
}

Then in SandboxFileTree:

 if (filetree.length === 0) {
-  return (
-    <div {...getRootProps()} ... >
+  return <SandboxEmptyDropZone onDrop={handleFilesDropped} uploading={uploading} />;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Sandboxes/SandboxFileTree.tsx` around lines 23 - 28, The
useDragAndDrop hook (returning getRootProps, getInputProps, isDragging) is
currently instantiated inside SandboxFileTree even when filetree.length > 0 and
those values are unused; extract the empty-state dropzone into its own component
(e.g. SandboxEmptyDropZone) that receives onDrop (handleFilesDropped) and
uploading as props and calls useDragAndDrop locally so the hook and its event
listeners are only created for the empty state; update SandboxFileTree to render
SandboxEmptyDropZone when filetree.length === 0 and keep the existing
SandboxDropZone for the non-empty path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/sandbox/upload/route.ts`:
- Around line 14-20: The route currently checks for an Authorization header
(authHeader) which the client never sends; instead move validation into the
onBeforeGenerateToken callback that receives clientPayload. Update
onBeforeGenerateToken to parse clientPayload (JSON.parse), extract the token
field (sent as { token: accessToken } from uploadSandboxFiles.ts), verify
presence/validity of that token, and return/throw an authentication error (HTTP
401) from that callback when missing/invalid; remove or bypass the authHeader
check in the existing upload route handler (authHeader/Authorization lookup) so
authentication is performed where the token is actually provided.

In `@lib/sandboxes/uploadSandboxFiles.ts`:
- Around line 39-48: The current use of Promise.all in the blobFiles creation
(files.map -> upload(...)) will reject on the first upload failure and leave
prior successful uploads orphaned; change Promise.all to Promise.allSettled,
collect successful results (extract { url, name } from fulfilled entries) and
handle rejected entries explicitly: either proceed with only the fulfilled
uploads or perform cleanup by deleting the successfully uploaded blobs (use your
existing blob-delete helper or call the appropriate deletion API with the URLs
returned from successful entries) and then rethrow or return an aggregated
error; update any downstream code that expects blobFiles to handle the
partial-success shape or an explicit error.

---

Nitpick comments:
In `@components/Sandboxes/SandboxFileTree.tsx`:
- Around line 23-28: The useDragAndDrop hook (returning getRootProps,
getInputProps, isDragging) is currently instantiated inside SandboxFileTree even
when filetree.length > 0 and those values are unused; extract the empty-state
dropzone into its own component (e.g. SandboxEmptyDropZone) that receives onDrop
(handleFilesDropped) and uploading as props and calls useDragAndDrop locally so
the hook and its event listeners are only created for the empty state; update
SandboxFileTree to render SandboxEmptyDropZone when filetree.length === 0 and
keep the existing SandboxDropZone for the non-empty path.

In `@hooks/useSandboxFileDrop.ts`:
- Around line 10-19: Move the pure utility function findNode (which accepts
nodes: FileNode[] and path: string and returns FileNode | undefined) out of
hooks/useSandboxFileDrop.ts into the shared module alongside the FileNode type
(e.g., lib/sandboxes/parseFileTree.ts); export findNode from that module and
update imports where used so callers reference the new exported utility instead
of the local definition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: caf41b49-b97b-474a-9a25-b83ccb62788c

📥 Commits

Reviewing files that changed from the base of the PR and between 4a8dd04 and 515d5d4.

📒 Files selected for processing (5)
  • app/api/sandbox/upload/route.ts
  • components/Sandboxes/SandboxDropZone.tsx
  • components/Sandboxes/SandboxFileTree.tsx
  • hooks/useSandboxFileDrop.ts
  • lib/sandboxes/uploadSandboxFiles.ts

Comment thread app/api/sandbox/upload/route.ts Outdated
Comment on lines +39 to +48
const blobFiles = await Promise.all(
files.map(async (file) => {
const blob = await upload(file.name, file, {
access: "public",
handleUploadUrl: "/api/sandbox/upload",
clientPayload: JSON.stringify({ token: accessToken }),
});
return { url: blob.url, name: file.name };
}),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Promise.all fails fast, leaving successfully uploaded blobs orphaned on partial failure.

If any single file upload fails, Promise.all rejects immediately. Files that uploaded successfully before the failure become orphaned in Vercel Blob storage with no cleanup path.

Consider using Promise.allSettled to capture partial success and either:

  1. Proceed with successfully uploaded files
  2. Clean up successful uploads on partial failure
♻️ Suggested approach with Promise.allSettled
-  const blobFiles = await Promise.all(
-    files.map(async (file) => {
-      const blob = await upload(file.name, file, {
-        access: "public",
-        handleUploadUrl: "/api/sandbox/upload",
-        clientPayload: JSON.stringify({ token: accessToken }),
-      });
-      return { url: blob.url, name: file.name };
-    }),
-  );
+  const results = await Promise.allSettled(
+    files.map(async (file) => {
+      const blob = await upload(file.name, file, {
+        access: "public",
+        handleUploadUrl: "/api/sandbox/upload",
+        clientPayload: JSON.stringify({ token: accessToken }),
+      });
+      return { url: blob.url, name: file.name };
+    }),
+  );
+
+  const blobFiles = results
+    .filter((r): r is PromiseFulfilledResult<{ url: string; name: string }> => 
+      r.status === "fulfilled")
+    .map((r) => r.value);
+
+  const uploadErrors = results
+    .filter((r): r is PromiseRejectedResult => r.status === "rejected")
+    .map((r, i) => `${files[i].name}: ${r.reason}`);
+
+  if (blobFiles.length === 0) {
+    throw new Error("All file uploads failed");
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/sandboxes/uploadSandboxFiles.ts` around lines 39 - 48, The current use of
Promise.all in the blobFiles creation (files.map -> upload(...)) will reject on
the first upload failure and leave prior successful uploads orphaned; change
Promise.all to Promise.allSettled, collect successful results (extract { url,
name } from fulfilled entries) and handle rejected entries explicitly: either
proceed with only the fulfilled uploads or perform cleanup by deleting the
successfully uploaded blobs (use your existing blob-delete helper or call the
appropriate deletion API with the URLs returned from successful entries) and
then rethrow or return an aggregated error; update any downstream code that
expects blobFiles to handle the partial-success shape or an explicit error.

- Extract NoSandboxFiles to its own component (SRP)
- Extract findFileNode to lib/sandboxes/findFileNode.ts (SRP)
- Use tanstack useMutation in useSandboxFileDrop for cleaner state management

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread hooks/useSandboxFileDrop.ts Outdated
},
});

const handleFilesDropped = useCallback(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

KISS - is the useCallback wrapper really required with useMutation?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You're right — removed the useCallback wrapper entirely. Now just returning mutation.mutate directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
components/Sandboxes/NoSandboxFiles.tsx (1)

24-33: Prefer semantic root + data-state for drag visual state.

This is a good candidate to use a semantic container (section) and data-state-driven styling instead of branching classes.

♻️ Suggested refactor
-    <div
+    <section
       {...getRootProps()}
-      role="region"
       aria-label="File upload dropzone"
+      data-state={isDragging ? "dragging" : "idle"}
       className={cn(
-        "w-full max-w-md rounded-lg p-8 border-2 border-dashed transition-all",
-        isDragging
-          ? "border-primary bg-primary/5"
-          : "border-muted-foreground/25",
+        "w-full max-w-md rounded-lg p-8 border-2 border-dashed transition-all border-muted-foreground/25 data-[state=dragging]:border-primary data-[state=dragging]:bg-primary/5",
       )}
     >
@@
-    </div>
+    </section>

As per coding guidelines, "Use data-state attribute for visual component states (open/closed, loading, etc.)" and "Start with semantic HTML first, then augment with ARIA if needed".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Sandboxes/NoSandboxFiles.tsx` around lines 24 - 33, Replace the
non-semantic div with a semantic section and switch from conditional class
branching to a data-state attribute to drive visual state: keep the
getRootProps() spread, role, and aria-label, set data-state based on isDragging
(e.g., "dragging" vs "idle"), and simplify the className to a single static base
class (on the element referenced by getRootProps()/className) while moving the
drag-specific styles to CSS selectors that target [data-state="dragging"] so the
component (NoSandboxFiles / the element using getRootProps(), isDragging,
className) uses semantic HTML and data-state-driven styling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@components/Sandboxes/NoSandboxFiles.tsx`:
- Around line 24-33: Replace the non-semantic div with a semantic section and
switch from conditional class branching to a data-state attribute to drive
visual state: keep the getRootProps() spread, role, and aria-label, set
data-state based on isDragging (e.g., "dragging" vs "idle"), and simplify the
className to a single static base class (on the element referenced by
getRootProps()/className) while moving the drag-specific styles to CSS selectors
that target [data-state="dragging"] so the component (NoSandboxFiles / the
element using getRootProps(), isDragging, className) uses semantic HTML and
data-state-driven styling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0ffb5104-066a-4771-bca5-379e0374904b

📥 Commits

Reviewing files that changed from the base of the PR and between 515d5d4 and ce3e0b1.

📒 Files selected for processing (4)
  • components/Sandboxes/NoSandboxFiles.tsx
  • components/Sandboxes/SandboxFileTree.tsx
  • hooks/useSandboxFileDrop.ts
  • lib/sandboxes/findFileNode.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • components/Sandboxes/SandboxFileTree.tsx
  • hooks/useSandboxFileDrop.ts

The blob client controls its own request headers, so we validate
the access token passed via clientPayload in onBeforeGenerateToken.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sweetmantech sweetmantech merged commit cf8a178 into test Mar 27, 2026
3 checks passed
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