Skip to content

fix: stage character asset mutations#6

Merged
SweetSophia merged 5 commits into
fix/avatar-media-classificationfrom
fix/avatar-asset-lifecycle
May 13, 2026
Merged

fix: stage character asset mutations#6
SweetSophia merged 5 commits into
fix/avatar-media-classificationfrom
fix/avatar-asset-lifecycle

Conversation

@SweetSophia
Copy link
Copy Markdown
Owner

@SweetSophia SweetSophia commented May 12, 2026

Summary

  • Stage character editor uploads/deletes so Back and panel Cancel roll back session-created files without deleting persisted assets.
  • Defer old local asset cleanup until panel Save, including replacement/removal and deleted-character assets.
  • Guard uploader races with upload sequence tokens and stale-upload cleanup.

Validation

  • pnpm run lint
  • cd apps/webuiapps && pnpm test -- characterAssetUpload

Summary by Sourcery

Stage character asset lifecycle so editor changes only delete local assets on save and roll back session-only uploads on cancel or back.

Bug Fixes:

  • Prevent persisted character assets from being deleted when closing or cancelling the character editor or panel.
  • Avoid orphaned or stale uploaded assets when replacing or removing character images and videos.
  • Guard character asset uploads against race conditions and stale uploads overwriting or leaking assets.

Enhancements:

  • Track session-created and pending-delete character assets across the character panel and editor to centralize cleanup.
  • Make the image uploader upload-aware (sequence tokens, aria-busy, and disabled controls) to improve UX and robustness.

Copilot AI review requested due to automatic review settings May 12, 2026 22:14
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 12, 2026

Reviewer's Guide

Stages character asset lifecycle in the chat character panel so that uploads/deletes are applied transactionally on panel save, rolled back on cancel/back, and protected against upload race conditions, while updating asset path parsing for timestamped filenames.

Sequence diagram for staged character asset lifecycle in CharacterPanel

sequenceDiagram
  actor User
  participant CharacterEditor
  participant CharacterPanel
  participant AssetCleanup

  User->>CharacterEditor: handleEmotionAssetUpload / handleBaseImageUpload
  CharacterEditor->>CharacterEditor: markUploadedAsset
  CharacterEditor->>CharacterEditor: markRemovedAsset

  User->>CharacterEditor: handleSave
  CharacterEditor->>CharacterEditor: collectCharacterLocalAssetPaths
  CharacterEditor->>CharacterEditor: diffLocalAssetPaths
  CharacterEditor->>CharacterPanel: onSave(updatedCharacter, lifecycle)
  CharacterPanel->>CharacterPanel: setSessionCreatedAssets
  CharacterPanel->>CharacterPanel: setPendingDeleteAssets

  alt Panel Save
    User->>CharacterPanel: handleSave
    CharacterPanel->>CharacterPanel: collectCollectionLocalAssetPaths
    CharacterPanel->>AssetCleanup: deleteAssetPaths(stagedDeletes)
  else Panel Cancel/Back/Close
    User->>CharacterPanel: handleCancel
    CharacterPanel->>CharacterPanel: collectCollectionLocalAssetPaths
    CharacterPanel->>AssetCleanup: deleteAssetPaths(createdOnlyInSession)
  end
Loading

Sequence diagram for guarded character asset uploads in ImageUploader

sequenceDiagram
  actor User
  participant ImageUploader
  participant uploadCharacterAsset
  participant deleteCharacterAsset

  User->>ImageUploader: handleFile(file)
  ImageUploader->>ImageUploader: uploadSequenceRef += 1
  ImageUploader->>uploadCharacterAsset: uploadCharacterAsset(characterId, emotion, file, type)
  uploadCharacterAsset-->>ImageUploader: path
  ImageUploader->>ImageUploader: isCurrentUpload?
  alt Stale upload
    ImageUploader->>deleteCharacterAsset: deleteCharacterAsset(path)
  else Current upload
    ImageUploader->>ImageUploader: getCharacterAssetUrl(path)
    ImageUploader->>ImageUploader: setPreviewUrl / setUploading(false)
    ImageUploader->>User: onUpload(path, type)
  end

  User->>ImageUploader: handleRemove
  ImageUploader->>ImageUploader: uploadSequenceRef += 1
  ImageUploader->>ImageUploader: setPreviewUrl(null), setUploading(false)
Loading

File-Level Changes

Change Details Files
Stage character asset creation and deletion in CharacterPanel/CharacterEditor and only persist/delete files on panel save or rollback on cancel/back.
  • Introduce CharacterAssetLifecycle and helper functions to collect/diff local asset paths and bulk delete them with deleteCharacterAsset.
  • Track sessionCreatedAssets and pendingDeleteAssets at the CharacterPanel level, updating them via CharacterEditor onSave callbacks.
  • Change panel Cancel/overlay-close to a new handleCancel that deletes only assets created in the session and not referenced in the original collection, and adjust Save to delete only unreferenced staged assets including deleted-character assets.
  • Update CharacterEditor to track created and removed asset paths, compute stale assets via diffLocalAssetPaths, and pass created/deleteOnCommit lists back to the panel instead of deleting immediately.
  • Wire CharacterEditor overlay/Back/close actions through a handleClose that deletes only editor-created assets before closing.
apps/webuiapps/src/components/ChatPanel/CharacterPanel.tsx
Refine per-asset upload/remove behavior in ImageUploader to guard against races, ensure stale uploads are cleaned up, and prevent concurrent user interactions during upload.
  • Add an uploadSequenceRef token that increments per upload and on unmount, with helpers to detect the current upload and to delete stale uploaded assets.
  • Ensure uploads that finish after a newer upload are treated as stale: they have their server-side files deleted and do not update preview state or invoke onUpload.
  • Guard all state updates (uploading flag, preview, errors) with isCurrentUpload, and short-circuit error handling for stale uploads.
  • Disable drag-and-drop, file input, and remove button while uploading; expose aria-busy and prevent multiple concurrent uploads or removals.
  • Reset upload sequence and state on remove, keeping UI consistent when users cancel an in-flight or completed upload.
apps/webuiapps/src/components/ChatPanel/ImageUploader.tsx
Allow parseLocalCharacterAssetPath to accept timestamped/suffixed emotion asset filenames.
  • Update the local character asset path regex to support optional -<13-digit-timestamp>- segments before the file extension for emotion assets.
  • Maintain existing validation for traversal and overall path shape while expanding accepted filenames.
apps/webuiapps/src/lib/characterAssetUpload.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements an asset lifecycle management system to track and clean up character assets during creation and editing. It introduces state tracking for session-created and pending-delete assets, ensuring unreferenced files are removed on save or cancel. Additionally, the ImageUploader was updated to handle race conditions during concurrent uploads. A review comment suggests optimizing the deletion logic in CharacterPanel.tsx by using direct set iterations instead of array spreading to improve efficiency.

Comment on lines +147 to +152
const stagedDeletes = new Set(
[...pendingDeleteAssets].filter((path) => !referencedPaths.has(path)),
);
for (const path of sessionCreatedAssets) {
if (!referencedPaths.has(path)) stagedDeletes.add(path);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This logic can be simplified and made more efficient by avoiding intermediate array spreading and filtering. Using direct loops over the sets is cleaner and avoids unnecessary allocations.

Suggested change
const stagedDeletes = new Set(
[...pendingDeleteAssets].filter((path) => !referencedPaths.has(path)),
);
for (const path of sessionCreatedAssets) {
if (!referencedPaths.has(path)) stagedDeletes.add(path);
}
const stagedDeletes = new Set<string>();
for (const path of pendingDeleteAssets) {
if (!referencedPaths.has(path)) stagedDeletes.add(path);
}
for (const path of sessionCreatedAssets) {
if (!referencedPaths.has(path)) stagedDeletes.add(path);
}

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the stage character editor’s asset-handling flow so uploaded/replaced/removed character assets are treated as session-scoped until the user commits changes, and also adds race protection to prevent stale uploads from “winning” after rapid user interactions.

Changes:

  • Add upload sequence tokens + stale-upload cleanup in ImageUploader to guard against concurrent/abandoned uploads.
  • Track session-created assets and deferred deletions in CharacterPanel/CharacterEditor, cleaning up only on panel Save (and only session-created assets on Cancel/Back).
  • Broaden local asset path parsing to recognize the versioned upload filename format.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
apps/webuiapps/src/lib/characterAssetUpload.ts Updates local character asset path parsing/validation to accept versioned upload filenames.
apps/webuiapps/src/components/ChatPanel/ImageUploader.tsx Adds upload sequencing and stale-upload cleanup; disables interactions while uploading.
apps/webuiapps/src/components/ChatPanel/CharacterPanel.tsx Introduces session asset lifecycle tracking and defers local asset cleanup until commit/cancel semantics.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +90 to +96
for (const path of new Set(paths)) {
try {
await deleteCharacterAsset(path);
} catch (err) {
console.warn('Failed to delete character asset:', err);
}
}

const match = path.match(
/^\/characters\/([A-Za-z0-9_-]+)\/emotions\/([A-Za-z0-9_-]+)\.([A-Za-z0-9]+)$/,
/^\/characters\/([A-Za-z0-9_-]+)\/emotions\/([A-Za-z0-9_-]+)(?:-\d{13}-[a-z0-9]+)?\.([A-Za-z0-9]+)$/,
}
onUpload(path, type);
} catch (err) {
if (!isCurrentUpload()) 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.

🔥 The Roast: Your stale-upload guard is so focused on the happy path it completely ghosted the error path. When a stale upload throws during getCharacterAssetUrl, you bail early like a bad Tinder date — but you leave the uploaded file on the server like an unwanted houseplant. I've written this exact bug, and I'm still ashamed.

🩹 The Fix: Lift path out of the try block so it's accessible in catch, then clean up the stale asset before returning:

let path: string | undefined;
try {
  ...
  path = await uploadCharacterAsset(characterId, emotion, file, type);
  ...
} catch (err) {
  if (!isCurrentUpload()) {
    if (path) await cleanupStaleUpload(path);
    return;
  }
  ...
}

📏 Severity: warning


const markUploadedAsset = (url: string) => {
if (!isLocalCharacterAssetPath(url)) return;
setCreatedAssets((current) => new Set(current).add(url));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔥 The Roast: new Set(current).add(url) is the React equivalent of buying a new house because you got a new throw pillow. If url is already in the set, you're forcing a re-render for absolutely nothing. Same thing happens in markRemovedAsset a few lines down — you're running a two-for-one special on unnecessary allocations.

🩹 The Fix:

Suggested change
setCreatedAssets((current) => new Set(current).add(url));
setCreatedAssets((current) => (current.has(url) ? current : new Set(current).add(url)));

📏 Severity: nitpick

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented May 12, 2026

Code Review Roast 🔥

Verdict: 2 Issues Found (2 new, 0 carried forward) | Recommendation: Address before merge

Overview

Severity Count
🚨 critical 0
⚠️ warning 0
💡 suggestion 0
🤏 nitpick 2
Issue Details (click to expand)
File Line Roast
apps/webuiapps/src/components/ChatPanel/ImageUploader.tsx 95 const isVidLocal = isVid; lost its indentation
apps/webuiapps/src/lib/__tests__/characterAssetUpload.test.ts 121 it block escaped its describe indentation

🏆 Best part: The asset lifecycle logic in CharacterPanel is actually solid. The session-scoped created/deleted tracking, the diff-based stale asset detection, and the cleanup on cancel — it's like watching someone who finally read the chapter on resource management.

💀 Worst part: Two lines of code apparently decided indentation is optional. One in ImageUploader and one in the test file. It's not a bug, but it makes the diff look like it was edited in Notepad with Word Wrap off.

📊 Overall: Like a mostly-clean apartment with two socks left on the floor. Functional, but the small messes nag at you.

Files Reviewed (4 files)
  • apps/webuiapps/src/components/ChatPanel/CharacterPanel.tsx - 0 new issues (asset lifecycle tracking is well-structured)
  • apps/webuiapps/src/components/ChatPanel/ImageUploader.tsx - 1 new issue (missing indentation on isVidLocal assignment)
  • apps/webuiapps/src/lib/characterAssetUpload.ts - 0 new issues (MAX_CHARACTER_VIDEO_BYTES 50MB→20MB, blank line removed)
  • apps/webuiapps/src/lib/__tests__/characterAssetUpload.test.ts - 1 new issue (missing indentation on it block)

Previously reviewed issues:

  • ImageUploader.tsx line 103: Empty catch still swallows original error (active comment from prior review)
  • CharacterPanel.tsx sequential deletion, Set re-render, and stale-upload guard issues appear resolved in current force-pushed revision

Fix these issues in Kilo Cloud


Reviewed by kimi-for-coding · full review after force push


Reviewed by kimi-for-coding · 3,697,398 tokens

@SweetSophia SweetSophia force-pushed the fix/avatar-media-classification branch from 17ae17f to 02957d2 Compare May 13, 2026 06:44
@SweetSophia SweetSophia force-pushed the fix/avatar-asset-lifecycle branch from 90f06b7 to 24fb6ac Compare May 13, 2026 06:45
@SweetSophia SweetSophia force-pushed the fix/avatar-media-classification branch from 02957d2 to 716e71e Compare May 13, 2026 12:50
@SweetSophia SweetSophia force-pushed the fix/avatar-asset-lifecycle branch from 24fb6ac to 8d89720 Compare May 13, 2026 12:50
@SweetSophia SweetSophia force-pushed the fix/avatar-media-classification branch 2 times, most recently from b4f7e39 to c7c280a Compare May 13, 2026 14:03
let url: string | null | undefined;
try {
url = await getCharacterAssetUrl(path);
} catch {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔥 The Roast: Your catch block is so empty it echoes. getCharacterAssetUrl could be screaming about a network timeout, a missing file, or a permissions issue, but we'll never know because you caught the error and immediately sent it to /dev/null. It's like taking your car to a mechanic who says "something's wrong" and hands you a bill.

🩹 The Fix: Add an error parameter to the catch and preserve it — either log it with console.warn before cleaning up, or pass it along as cause in the thrown error: throw new Error('Failed to retrieve asset URL', { cause: err }). Debugging production issues is hard enough without you playing hide-and-seek with the stack trace.

📏 Severity: suggestion

@SweetSophia SweetSophia force-pushed the fix/avatar-asset-lifecycle branch from 4a3bd53 to 8cb4592 Compare May 13, 2026 16:01
@SweetSophia SweetSophia force-pushed the fix/avatar-asset-lifecycle branch from 8cb4592 to 167b00c Compare May 13, 2026 16:04
@SweetSophia SweetSophia merged commit cf59cca into fix/avatar-media-classification May 13, 2026

if (expectedUrlRef.current === path || !expectedUrlRef.current) {
const isVidLocal = isVid;
const isVidLocal = isVid;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔥 The Roast: Your indentation went on vacation and forgot to come back. This line is so far left it might as well be running for office.

🩹 The Fix:

Suggested change
const isVidLocal = isVid;
const isVidLocal = isVid;

📏 Severity: nitpick

});

it('detects image asset URLs with query strings and hashes', () => {
it('detects image asset URLs with query strings and hashes', () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔥 The Roast: This it block is so unindented it looks like it's trying to escape the describe. Did your spacebar break halfway through writing this test?

🩹 The Fix:

Suggested change
it('detects image asset URLs with query strings and hashes', () => {
it('detects image asset URLs with query strings and hashes', () => {

📏 Severity: nitpick

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