Conversation
…upport Add a softness parameter to crop settings that creates feathered edges on cropped media. Implements rendering in both DOM preview (CSS masks in ContainedMediaLayout) and canvas/export pipeline (gradient alpha masks in applyCropFeatherMask). Fix GPU transition pipeline dropping crop softness alpha by setting premultipliedAlpha: true on texture uploads — without this, the browser un-premultiplied Canvas 2D data before writing to GPU textures, mangling semi-transparent pixels from feathered edges. Fix fade transition blend weights from cos/sin (sum=1.414 at midpoint) to cos²/sin² (sum=1.0 always) so alpha-bearing content like soft crops and masks blends correctly without over-brightness.
Anchor viewport div to far edge (right: 0 / bottom: 0) when no crop exists on that side, avoiding independent percentage rounding gaps. Round canvas clip rect to pixel boundaries to prevent antialiased seams in the export/scrub renderer path.
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughAdds full media-crop support: types, normalization, geometry and feathering utilities, a ContainedMediaLayout React component with mask-based cropping, integration into item rendering and export pipelines, preview/UI crop controls, and updates fade crossfade weighting/compositing across canvas and GPU paths. Changes
Sequence DiagramsequenceDiagram
actor User
participant UI as VideoSection UI
participant Store as Preview Store
participant Wrapper as ItemVisualWrapper
participant Layout as ContainedMediaLayout
participant Utils as MediaCropUtils
participant Canvas as Canvas Renderer
User->>UI: drag crop slider
UI->>Store: setPropertiesPreviewNew({ crop })
Store->>Wrapper: preview crop present on item props
Wrapper->>Wrapper: sees mediaContent.fitMode='contain'
Wrapper->>Layout: pass source/container dims + crop
Layout->>Utils: calculateMediaCropLayout()
Utils-->>Layout: MediaCropLayout (mediaRect, viewport, feather)
Layout->>Wrapper: render children with mask-image and positioned rect
User->>UI: commit crop
UI->>Store: updateItem({ crop }) then clearPreview
Store->>Canvas: render request with item.crop
Canvas->>Utils: calculateContainedMediaDrawLayout()
Canvas->>Canvas: drawContainedMediaSource() with feather masking
Canvas-->>User: final frame rendered with crop applied
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
src/types/project.ts (1)
1-3: Reuse the sharedCropSettingstype here.This is now another copy of the crop shape. Importing
CropSettingskeeps the project model aligned withsrc/types/transform.tsand reduces drift when crop fields evolve.♻️ Suggested cleanup
import type { AnimatableProperty, EasingType, EasingConfig } from './keyframe'; import type { Transition } from './transition'; +import type { CropSettings } from './transform'; @@ - crop?: { - left?: number; - right?: number; - top?: number; - bottom?: number; - softness?: number; - }; + crop?: CropSettings;Also applies to: 105-111
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/types/project.ts` around lines 1 - 3, The file currently duplicates the crop shape instead of reusing the canonical type; import and use the shared CropSettings type from src/types/transform.ts (add an import for CropSettings alongside the existing imports) and replace the local duplicated crop/interface usages with CropSettings (update the type references where the duplicate appears, e.g., the project model fields that currently define a local crop shape around the duplicated block at 105-111) so the project types reference CropSettings directly.src/features/preview/stores/gizmo-store.ts (1)
9-12: Store design allows partial crop payloads without deep-merging, creating fragility for future callers.
setPropertiesPreviewNewmerges properties with shallow spread at line 378:properties: { ...merged[itemId]?.properties, ...props }. Current crop updates invideo-section.tsxdefensively spread...item.cropbefore modifying a field, so they send fullCropSettings—but the store does not enforce this pattern. A future caller sending{ crop: { left: 0.2 } }would loseright/top/bottom/softness. Consider deep-mergingcropfields in the store to prevent silent data loss.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/preview/stores/gizmo-store.ts` around lines 9 - 12, The store's shallow merge in setPropertiesPreviewNew (specifically the merge at properties: { ...merged[itemId]?.properties, ...props }) can drop nested crop fields if a caller sends a partial CropSettings; change the merge to deep-merge the crop object: when building the new properties, compute mergedCrop = { ...merged[itemId]?.properties?.crop, ...props?.crop } (handling undefined) and set properties to { ...merged[itemId]?.properties, ...props, crop: mergedCrop } so existing right/top/bottom/softness are preserved even when callers provide partial crop updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/domain/timeline/transitions/renderers/basic.ts`:
- Around line 19-25: The fade curve is inconsistent: update the fade helper(s)
in the transition engine and GPU renderer to use the same cos²/sin² weighting as
calculateFadeOpacity. Either extract a shared utility (e.g., export a single
calculateFadeOpacity/calculateFadeWeight function) and import it into
src/domain/timeline/transitions/engine.ts and
src/domain/timeline/transitions/renderers/gpu.ts, or change the local helpers in
those files to compute c = cos((progress * PI)/2) and return c*c for outgoing
and 1 - c*c for incoming so both render paths use identical fade
timing/brightness.
In `@src/features/composition-runtime/components/item.tsx`:
- Around line 210-214: The wrapper is using getSourceDimensions() (mediaSource)
to populate mediaContent causing legacy items with undefined intrinsic sizes to
fallback to transformed box and mismatch export; update the component that sets
mediaContent (the mediaContent prop block around mediaContent={{...}}) to only
pass crop/contain behavior when intrinsic dimensions are known (i.e., when
getSourceDimensions() returns a defined width/height), otherwise omit/leave
mediaContent undefined or pass a flag to indicate unknown intrinsic sizes;
alternatively, plumb the real media dimensions into mediaSource earlier by
resolving actual media dimensions before rendering this wrapper (referencing
getSourceDimensions(), mediaSource, mediaContent, and item.crop) so preview uses
the true intrinsic size—apply the same gating/fix to the other occurrences noted
(lines ~315-319 and ~345-349).
In `@src/features/export/utils/canvas-item-renderer.ts`:
- Around line 1872-1895: The pooled scratch canvas acquired via
canvasPool.acquire() may be smaller than the active render target, causing the
feather mask to rasterize into an undersized buffer and clip the right/bottom
when calling ctx.drawImage(scratchCanvas, 0, 0); fix drawContainedMediaSource()
by checking the acquired pooledCanvas.canvas dimensions against the active
canvas.width/height and, when pooledCanvas exists but is too small, resize the
pooled canvas to the target dimensions (set canvas.width/height and re-obtain
the 2D context, clear the canvas) before using applyCropFeatherMask and
ctx.drawImage; ensure you still release the pooled canvas via
canvasPool.release(...) and handle the scratchCanvas/scratchCtx fallback path
the same way as the non-pooled case (clearRect when newly created).
---
Nitpick comments:
In `@src/features/preview/stores/gizmo-store.ts`:
- Around line 9-12: The store's shallow merge in setPropertiesPreviewNew
(specifically the merge at properties: { ...merged[itemId]?.properties, ...props
}) can drop nested crop fields if a caller sends a partial CropSettings; change
the merge to deep-merge the crop object: when building the new properties,
compute mergedCrop = { ...merged[itemId]?.properties?.crop, ...props?.crop }
(handling undefined) and set properties to { ...merged[itemId]?.properties,
...props, crop: mergedCrop } so existing right/top/bottom/softness are preserved
even when callers provide partial crop updates.
In `@src/types/project.ts`:
- Around line 1-3: The file currently duplicates the crop shape instead of
reusing the canonical type; import and use the shared CropSettings type from
src/types/transform.ts (add an import for CropSettings alongside the existing
imports) and replace the local duplicated crop/interface usages with
CropSettings (update the type references where the duplicate appears, e.g., the
project model fields that currently define a local crop shape around the
duplicated block at 105-111) so the project types reference CropSettings
directly.
🪄 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: c7df9d79-5d4f-4cdd-aa4f-2e97e6fd2eae
📒 Files selected for processing (24)
src/domain/timeline/transitions/renderers/basic.tssrc/features/composition-runtime/components/contained-media-layout.test.tsxsrc/features/composition-runtime/components/contained-media-layout.tsxsrc/features/composition-runtime/components/item-visual-wrapper.tsxsrc/features/composition-runtime/components/item.tsxsrc/features/composition-runtime/components/stable-video-sequence.test.tsxsrc/features/composition-runtime/components/stable-video-sequence.tsxsrc/features/composition-runtime/components/video-content.tsxsrc/features/editor/components/properties-sidebar/clip-panel/video-section.tsxsrc/features/export/utils/canvas-item-renderer.transition-state.test.tssrc/features/export/utils/canvas-item-renderer.tssrc/features/export/utils/canvas-render-orchestrator.tssrc/features/export/utils/canvas-transitions.tssrc/features/export/utils/client-render-engine.tssrc/features/preview/stores/gizmo-store.tssrc/features/project-bundle/schemas/project-schema.tssrc/features/timeline/stores/items-store.tssrc/lib/gpu-transitions/transition-pipeline.tssrc/lib/gpu-transitions/transitions/fade.tssrc/shared/utils/media-crop.test.tssrc/shared/utils/media-crop.tssrc/types/project.tssrc/types/timeline.tssrc/types/transform.ts
| function calculateFadeOpacity(progress: number, isOutgoing: boolean): number { | ||
| // cos²/sin² weights — always sum to 1, preserving alpha for soft crop & masks. | ||
| const c = Math.cos((progress * Math.PI) / 2); | ||
| if (isOutgoing) { | ||
| return Math.cos((progress * Math.PI) / 2); | ||
| return c * c; | ||
| } | ||
| return Math.sin((progress * Math.PI) / 2); | ||
| return 1 - c * c; |
There was a problem hiding this comment.
Finish the fade-curve migration in the remaining fade helpers.
calculateFadeOpacity() switched to cos²/sin² here, but src/domain/timeline/transitions/engine.ts:1-15 and src/domain/timeline/transitions/renderers/gpu.ts:1-15 still return linear cos/sin. That leaves different callers with different opacity curves, so fade timing/brightness will diverge between render paths. Please update those helpers too, or extract a shared fade-weight utility.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/domain/timeline/transitions/renderers/basic.ts` around lines 19 - 25, The
fade curve is inconsistent: update the fade helper(s) in the transition engine
and GPU renderer to use the same cos²/sin² weighting as calculateFadeOpacity.
Either extract a shared utility (e.g., export a single
calculateFadeOpacity/calculateFadeWeight function) and import it into
src/domain/timeline/transitions/engine.ts and
src/domain/timeline/transitions/renderers/gpu.ts, or change the local helpers in
those files to compute c = cos((progress * PI)/2) and return c*c for outgoing
and 1 - c*c for incoming so both render paths use identical fade
timing/brightness.
| mediaContent={{ | ||
| fitMode: 'contain', | ||
| sourceWidth: mediaSource?.width, | ||
| sourceHeight: mediaSource?.height, | ||
| crop: item.crop, |
There was a problem hiding this comment.
Use intrinsic media dimensions before enabling the contained crop path.
getSourceDimensions() only returns persisted metadata. For legacy video/image items it can be undefined, and the wrapper then falls back to the transformed box as the “source” aspect. Export still renders from the real media dimensions in src/features/export/utils/canvas-item-renderer.ts, so crop/softness can preview with a different viewport than export on older projects. Please gate mediaContent on known intrinsic sizes, or plumb the actual media dimensions into the wrapper first.
Also applies to: 315-319, 345-349
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/composition-runtime/components/item.tsx` around lines 210 - 214,
The wrapper is using getSourceDimensions() (mediaSource) to populate
mediaContent causing legacy items with undefined intrinsic sizes to fallback to
transformed box and mismatch export; update the component that sets mediaContent
(the mediaContent prop block around mediaContent={{...}}) to only pass
crop/contain behavior when intrinsic dimensions are known (i.e., when
getSourceDimensions() returns a defined width/height), otherwise omit/leave
mediaContent undefined or pass a flag to indicate unknown intrinsic sizes;
alternatively, plumb the real media dimensions into mediaSource earlier by
resolving actual media dimensions before rendering this wrapper (referencing
getSourceDimensions(), mediaSource, mediaContent, and item.crop) so preview uses
the true intrinsic size—apply the same gating/fix to the other occurrences noted
(lines ~315-319 and ~345-349).
| const pooledCanvas = canvasPool?.acquire(); | ||
| const scratchCanvas = pooledCanvas?.canvas ?? new OffscreenCanvas(canvas.width, canvas.height); | ||
| const scratchCtx = pooledCanvas?.ctx ?? scratchCanvas.getContext('2d'); | ||
| if (!scratchCtx) { | ||
| if (pooledCanvas) { | ||
| canvasPool?.release(scratchCanvas); | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| try { | ||
| if (!pooledCanvas) { | ||
| scratchCtx.clearRect(0, 0, canvas.width, canvas.height); | ||
| } | ||
|
|
||
| scratchCtx.save(); | ||
| clipToViewport(scratchCtx, drawLayout.viewportRect); | ||
| drawSource(scratchCtx); | ||
| scratchCtx.restore(); | ||
| applyCropFeatherMask(scratchCtx, drawLayout.viewportRect, drawLayout.featherPixels); | ||
| ctx.drawImage(scratchCanvas, 0, 0); | ||
| } finally { | ||
| if (pooledCanvas) { | ||
| canvasPool?.release(scratchCanvas); |
There was a problem hiding this comment.
Resize pooled scratch canvases to the active render target before feathering.
drawContainedMediaSource() assumes canvasPool.acquire() matches canvasSettings, but renderCompositionItem() and renderItemWithCornerPin() reuse the main pool while rendering into different-sized targets. When the target is larger than the pool canvas, the feather mask is rasterized into an undersized scratch buffer and ctx.drawImage(scratchCanvas, 0, 0) clips the right/bottom of the item. The same sizing guard is needed in Lines 732-752.
💡 Suggested guard
- const pooledCanvas = canvasPool?.acquire();
- const scratchCanvas = pooledCanvas?.canvas ?? new OffscreenCanvas(canvas.width, canvas.height);
- const scratchCtx = pooledCanvas?.ctx ?? scratchCanvas.getContext('2d');
+ const pooledCanvas = canvasPool?.acquire();
+ const scratchCanvas = pooledCanvas?.canvas ?? new OffscreenCanvas(canvas.width, canvas.height);
+ if (scratchCanvas.width !== canvas.width || scratchCanvas.height !== canvas.height) {
+ scratchCanvas.width = canvas.width;
+ scratchCanvas.height = canvas.height;
+ }
+ const scratchCtx = pooledCanvas?.ctx ?? scratchCanvas.getContext('2d');
if (!scratchCtx) {
if (pooledCanvas) {
canvasPool?.release(scratchCanvas);
}
return false;
}
+ scratchCtx.clearRect(0, 0, canvas.width, canvas.height);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/export/utils/canvas-item-renderer.ts` around lines 1872 - 1895,
The pooled scratch canvas acquired via canvasPool.acquire() may be smaller than
the active render target, causing the feather mask to rasterize into an
undersized buffer and clip the right/bottom when calling
ctx.drawImage(scratchCanvas, 0, 0); fix drawContainedMediaSource() by checking
the acquired pooledCanvas.canvas dimensions against the active
canvas.width/height and, when pooledCanvas exists but is too small, resize the
pooled canvas to the target dimensions (set canvas.width/height and re-obtain
the 2D context, clear the canvas) before using applyCropFeatherMask and
ctx.drawImage; ensure you still release the pooled canvas via
canvasPool.release(...) and handle the scratchCanvas/scratchCtx fallback path
the same way as the non-pooled case (clearRect when newly created).
Replace inline crop type definition with import from transform.ts to avoid maintaining duplicate type shapes.
Greptile SummaryThis PR adds a full crop-with-soft-feathering pipeline for video and image clips, covering DOM preview (CSS Key changes:
Minor issues found:
Confidence Score: 5/5Safe to merge — all identified findings are P2 style/cleanup items with no functional impact The core crop geometry math (calculateMediaCropLayout, resolveCropSettings) is well-tested and correct. The feathering pipeline is consistent across DOM, canvas 2D, and GPU paths. The fade-blend and GPU argument-order fixes are mathematically sound. The only issues found are a copy-paste duplicate gradient stop and an inline type definition that duplicates CropSettings — neither affects runtime behavior. src/features/export/utils/canvas-item-renderer.ts — duplicate addColorStop calls in applyCropFeatherMask (lines 1771–1772, 1804–1805) Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[CropSettings\nleft/right/top/bottom/softness] --> B[resolveCropSettings\nclamp + axis-pair clamping]
B --> C[calculateMediaCropLayout\nmediaRect · cropViewportRect\nviewportRect · featherPixels]
C --> D{Render path}
D -->|DOM preview| E[ContainedMediaLayout\nCSS mask-image gradients]
D -->|Canvas export\nnon-mediabunny| F[drawContainedMediaSource\nclipToViewport + applyCropFeatherMask\ndestination-in gradient passes]
D -->|Canvas export\nmediabunny extractor| G[drawExtractorFrame on scratch canvas\napplyCropFeatherMask\nctx.drawImage result]
D -->|GPU transition| H[premultipliedAlpha: true\nuploaded to WebGPU textures]
F --> I[ctx.drawImage scratchCanvas]
G --> I
H --> J[WGSL fade shader\nmix left right t\nt = sin^2 p*pi/2]
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/features/export/utils/canvas-item-renderer.ts
Line: 1771-1772
Comment:
**Duplicate `addColorStop` at position 0 (right and bottom edges)**
Lines 1771–1772 and 1804–1805 both add an identical stop at position `0` for the right and bottom feather gradient passes respectively. While browsers handle this gracefully (the duplicate is redundant), it's a copy-paste artifact — the `right` and `bottom` blocks were likely adapted from the `left`/`top` blocks without removing the extra opening stop.
The first `addColorStop(0, ...)` is sufficient; remove the duplicate on each affected block:
```suggestion
gradient.addColorStop(0, 'rgba(0, 0, 0, 1)');
gradient.addColorStop(
Math.max(0, Math.min(1, (viewportRect.width - featherPixels.right) / viewportRect.width)),
'rgba(0, 0, 0, 1)',
);
```
And similarly at line 1804–1805 for the `bottom` feather block.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/types/project.ts
Line: 105-111
Comment:
**Inline crop type duplicates `CropSettings`**
The `crop` shape defined inline here (`left`, `right`, `top`, `bottom`, `softness`) is structurally identical to the new `CropSettings` interface in `src/types/transform.ts`. If the canonical definition evolves (e.g. a `featherMode` field is added), this inline copy will silently diverge.
Consider importing and using `CropSettings` directly:
```suggestion
crop?: import('@/types/transform').CropSettings;
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(crop): eliminate sub-pixel seam at u..." | Re-trigger Greptile |
| gradient.addColorStop(0, 'rgba(0, 0, 0, 1)'); | ||
| gradient.addColorStop(0, 'rgba(0, 0, 0, 1)'); |
There was a problem hiding this comment.
Duplicate
addColorStop at position 0 (right and bottom edges)
Lines 1771–1772 and 1804–1805 both add an identical stop at position 0 for the right and bottom feather gradient passes respectively. While browsers handle this gracefully (the duplicate is redundant), it's a copy-paste artifact — the right and bottom blocks were likely adapted from the left/top blocks without removing the extra opening stop.
The first addColorStop(0, ...) is sufficient; remove the duplicate on each affected block:
| gradient.addColorStop(0, 'rgba(0, 0, 0, 1)'); | |
| gradient.addColorStop(0, 'rgba(0, 0, 0, 1)'); | |
| gradient.addColorStop(0, 'rgba(0, 0, 0, 1)'); | |
| gradient.addColorStop( | |
| Math.max(0, Math.min(1, (viewportRect.width - featherPixels.right) / viewportRect.width)), | |
| 'rgba(0, 0, 0, 1)', | |
| ); |
And similarly at line 1804–1805 for the bottom feather block.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/export/utils/canvas-item-renderer.ts
Line: 1771-1772
Comment:
**Duplicate `addColorStop` at position 0 (right and bottom edges)**
Lines 1771–1772 and 1804–1805 both add an identical stop at position `0` for the right and bottom feather gradient passes respectively. While browsers handle this gracefully (the duplicate is redundant), it's a copy-paste artifact — the `right` and `bottom` blocks were likely adapted from the `left`/`top` blocks without removing the extra opening stop.
The first `addColorStop(0, ...)` is sufficient; remove the duplicate on each affected block:
```suggestion
gradient.addColorStop(0, 'rgba(0, 0, 0, 1)');
gradient.addColorStop(
Math.max(0, Math.min(1, (viewportRect.width - featherPixels.right) / viewportRect.width)),
'rgba(0, 0, 0, 1)',
);
```
And similarly at line 1804–1805 for the `bottom` feather block.
How can I resolve this? If you propose a fix, please make it concise.| crop?: { | ||
| left?: number; | ||
| right?: number; | ||
| top?: number; | ||
| bottom?: number; | ||
| softness?: number; | ||
| }; |
There was a problem hiding this comment.
Inline crop type duplicates
CropSettings
The crop shape defined inline here (left, right, top, bottom, softness) is structurally identical to the new CropSettings interface in src/types/transform.ts. If the canonical definition evolves (e.g. a featherMode field is added), this inline copy will silently diverge.
Consider importing and using CropSettings directly:
| crop?: { | |
| left?: number; | |
| right?: number; | |
| top?: number; | |
| bottom?: number; | |
| softness?: number; | |
| }; | |
| crop?: import('@/types/transform').CropSettings; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/types/project.ts
Line: 105-111
Comment:
**Inline crop type duplicates `CropSettings`**
The `crop` shape defined inline here (`left`, `right`, `top`, `bottom`, `softness`) is structurally identical to the new `CropSettings` interface in `src/types/transform.ts`. If the canonical definition evolves (e.g. a `featherMode` field is added), this inline copy will silently diverge.
Consider importing and using `CropSettings` directly:
```suggestion
crop?: import('@/types/transform').CropSettings;
```
How can I resolve this? If you propose a fix, please make it concise.…seams The intermediate viewport div with overflow:hidden caused persistent sub-pixel seams from CSS percentage rounding — no amount of +1px hacks could reliably cover all crop/softness value combinations. Replace with a single composite CSS mask-image on the mediaRect div that handles both hard crop edges and soft feather gradients. Masks render in the element's own coordinate space (paint-time, not layout), so sub-pixel positioning issues are structurally impossible. Also fix ±1 frame shift on crop slider drag by skipping scrub overlay activation for non-GPU gizmo interactions — the DOM Player handles crop/transform previews through React props without decoder mismatch. Remove duplicate addColorStop(0) in right/bottom feather gradient passes (copy-paste artifact, functionally harmless but confusing).
Summary
premultipliedAlpha: trueon texture uploadsTest plan
Summary by CodeRabbit
New Features
Bug Fixes