Skip to content

Add persona and global image galleries - #2361

Merged
SpicyMarinara merged 10 commits into
Pasta-Devs:refactorfrom
kolacheee:feat/persona-global-galleries
Jun 7, 2026
Merged

Add persona and global image galleries#2361
SpicyMarinara merged 10 commits into
Pasta-Devs:refactorfrom
kolacheee:feat/persona-global-galleries

Conversation

@kolacheee

@kolacheee kolacheee commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Linked issue

Closes #2360 (partially).

This is the first PR in a planned series implementing that feature request. It delivers the gallery foundations only (persona galleries + a global gallery panel) — the management surface the rest of the feature builds on. Emoji/sticker tagging, the Conversation-mode selectors, and AI reactions follow in later PRs, so this PR intentionally does not close the request.

Why this change

  • The emoji/sticker system needs image libraries to draw from, so this PR builds those libraries first as a self-contained, management-only change.
  • Personas had no gallery, unlike characters — this closes that parity gap.
  • A profile-wide image library gives users one place to collect and organize reusable images instead of scattering them across chats.

What changed

  • Persona gallery: a new Gallery tab on the persona editor, mirroring the character gallery. New persona-gallery storage collection + persona_gallery_upload command, with cascade-delete when a persona is removed, plus expunge, profile-export, and remote-runtime wiring.
  • Global gallery panel: a new top-level Gallery panel in the right-panel nav (alongside Characters/Personas/Lorebooks). New global-gallery collection plus a flat gallery-folders collection. Supports sorting (newest/oldest/name asc/desc), create/rename/delete folders (deleting a folder re-files its images to the root rather than deleting them), uploading into a folder, and moving images by dragging onto a folder chip or via a lightbox picker.
  • Storage plumbing so both new image collections behave like the existing galleries: managed-file cleanup on delete, gallery default fields on create, inline-data-url persistence, and managed-asset cache invalidation.
  • Registered both upload commands in src-tauri/src/lib.rs, the HTTP dispatcher, and the remote-runtime allowlist.
  • Added a discovery entry for the new Gallery panel.

Refactor impact

Primary owner: catalog (galleries) and Rust storage.

Impact areas reviewed:

  • catalog/gallery and catalog/personas (new panel, tab, hooks)
  • Rust storage (contracts.rs collections + cleanup enum, entities.rs cleanup wiring, shared.rs upload helper, media.rs commands, admin.rs expunge scopes)
  • shared API (storage-api.ts invalidation, image-generation-api.ts upload surface)
  • app shell (RightPanel.tsx, PanelNavButtons.tsx) — added the Gallery panel entry
  • engine capabilities (storage.ts entity-name union)
  • discovery registry (+ the metadata check script's panel allowlist)

Boundary notes:

  • engine: only the storage entity-name union (src/engine/capabilities/storage.ts) gained the new collection names.
  • shared API: gallery write/delete invalidation kinds extended to the new collections; new uploadPersona/uploadGlobal surfaces.
  • feature: new components/hooks are confined to catalog/gallery and catalog/personas.
  • Rust: new collections, DeletePersonaGallery/ClearGalleryFolder cleanups, and a global-gallery upload helper.
  • remote runtime: added persona_gallery_upload and global_gallery_upload to the allowlist + HTTP dispatch.

Pressure points touched:

  • src-tauri/src/lib.rs command registration — added the two upload commands. ModeSurface, GameSurface, shared mode UI, and import modules are untouched; nothing touches the RP/Game/Conversation runtime.

Validation

  • Matching validation command passes locally (for example pnpm typecheck, pnpm build, pnpm check:architecture, pnpm check:docs, or full pnpm check when warranted)
  • Full pnpm check passes before PR push/handoff
  • Human/manual validation completed by contributor or reviewer

Manual verification notes

Ran individually (the local check:line-endings step trips on pre-existing CRLF in unrelated files, so the chained pnpm check bails early; the committed versions of those files are LF, so CI is unaffected):

  • pnpm check:frontend (tsc -b) — passes
  • pnpm check:rust (cargo check --workspace) — passes
  • pnpm check:architecture — passes (no dependency violations)
  • pnpm check:unused (knip) — clean
  • pnpm check:discovery — passes
  • Added two Rust regression tests (cargo test green): deleting a persona removes its gallery rows + managed files; deleting a gallery folder re-files its images to the root.

Manual Tauri verification: uploaded images to the global gallery, created/selected/renamed/deleted folders, sorted, and moved images both by dragging onto a folder chip and via the lightbox picker; verified the persona Gallery tab uploads and displays. Confirmed in both dark and light themes.

Feature Discoverability

Check exactly one:

  • Updated src/features/shell/discovery/ because this PR adds or materially changes a user-discoverable feature, workflow, setting, mode, panel, import path, agent, media capability, or advanced tool.
  • N/A because this PR is only a bugfix, refactor, test, docs, internal wiring, visual polish, copy edit, or compatibility fix and does not add a new thing users need to find.

Reason:

  • The first box applies: this PR adds a new top-level Gallery panel, so src/features/shell/discovery/discovery-entries.json gained a global-gallery entry and the panel-target allowlists were extended.

Docs and release impact

  • No docs changes needed
  • Updated README.md
  • Updated CONTRIBUTING.md
  • Updated docs/developer/
  • Updated repo skills or AGENTS.md
  • Confirmed this PR does not restore old staging/package-workspace/release claims

UI evidence

image image image

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 58e5760c-56fc-405b-b67d-89cff282b2fa

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@github-actions github-actions Bot added the feature New feature or enhancement label Jun 5, 2026
@kolacheee
kolacheee marked this pull request as ready for review June 5, 2026 19:21
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown

✅ Bunny Review Completed

Tip

Review posted. The specimen has left the observation table.

  • Commit: ad511de - Resolve gallery upload folder before persisting bytes

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown

🐰 Bunny Review

Bunny Merge Signal: Do Not Merge

Caution

DO NOT MERGE
Repair blocking/high findings or failed controls before merge.

Findings Nitpicks Controls Reviewed Head Mode
1 0 1 fail, 1 warn, 1 pass ad511dea incremental

Note

Mode: incremental
Head: ad511de - Resolve gallery upload folder before persisting bytes
Base: 0467ed5 - Add Gallery to the mobile tools nav

🧭 Specimen Summary

  • The specimen moves global-gallery folder resolution ahead of managed byte persistence, attempting to cauterize the leak where a storage lookup failure could strand a file without its storage row.
  • The incision usefully narrows Contract 1: the folder read now happens before the asset exists, while row-create failure still enters the cleanup envelope. Yet the folder invariant remains a delicate little organism, sampled once and then trusted while deletion can continue elsewhere.

🔎 Isolated Defects

Severity Location Finding
⚠️ MEDIUM src-tauri/src/commands/storage/shared.rs:2953 Folder validation is still only a pre-write rumor

✅ Resolved Since Last Review

  • src-tauri/src/commands/storage/shared.rs:2958 - Global upload validates after creating the asset
  • src-tauri/src/commands/storage/shared.rs:2737 - Global gallery accepts ghost folders

🧹 Nitpicks

  • None recorded.
🤖 Copy prompt for isolated Bunny findings
Task: Fix `src-tauri/src/commands/storage/shared.rs:2953`.
Finding: Folder validation is still only a pre-write rumor
Severity: medium
Goal: No committed `global-gallery.folderId` may be non-null unless the referenced `gallery-folders.id` exists at the moment the gallery row is written; root images must store `folderId: null`.
Cover: A folder is deleted after the upload command resolves `folder_value` but before `create_immediate` commits the global-gallery row.; A storage lookup or revalidation fails after bytes are persisted and must not leave a managed gallery file without a row.; Generic storage create, update, patch, or move writes a fabricated or stale `folderId` without passing through this upload helper.
Avoid: Checking folder existence before file persistence and treating that as a durable write-time guarantee.; Revalidating after persistence but forgetting to remove the newly stored managed file on folder-validation failure.; Fixing upload only while move, update, or patch retain the same permissive contract.
Acceptable fixes: Revalidate `folderId` immediately before `create_immediate` and wrap that post-persist validation failure in the same managed-file cleanup used for row-create errors.; Move folder validation plus row creation into a storage operation that serializes against folder deletion.; Add backend normalization and validation for `global-gallery.folderId` inside the generic storage create, update, and patch path that commits the row.
Proof required: Fault-injection or focused Rust proof where the folder disappears between validation and row creation, and no ghost `folderId` is committed.; Proof that any post-persist folder validation failure removes the stored asset.; Proof that upload to an existing folder still stores that id, and generic move/update/patch cannot bypass the same invariant.
Run the narrowest relevant check. If stale, leave code unchanged and record why.

✅ Control Checks

Status Type Check Detail
✅ PASS CI Timing CI Status Expected CI controls completed without a reported failure.
⚠️ WARN Proof Gap Prior contract 1 Static inspection shows the original lookup-error leak path is structurally repaired by moving the gallery-folders read before persist_image_bytes; no focused fault-injection proof was present, the usual little absence wearing a lab coat.
❌ FAIL Proof Gap Prior contract 2 Upload now normalizes missing folder ids earlier, but write-time referential integrity for global-gallery.folderId remains incomplete and is reported as the surviving defect.

🧪 Observations

  • Established base 0467ed53cffada07c5ce284a7f12689bf9fff5ff, head ad511dea447b3bf0ef9e092a4b17aae42305a89e, incremental mode, and the one-file Rust diff.
  • Read AGENTS.md, architecture guard, bugfix discipline, path rules, and the supplied storage patch context.
  • Judged prior Contract 1 first: the folder lookup moved before persist_image_bytes, while create_immediate failure cleanup remains intact.

🧰 CI Status

  • pass: Frontend, Architecture, and Organization
  • pass: Rust Capability Layer
  • pass: Browser Smoke and Performance
  • note: Bunny gates only required job conclusions from ci-checks.json; advisory reports such as continue-on-error dependency checks are not included in this status.

@github-actions github-actions 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.

Bunny Review inline findings

Comment thread src-tauri/src/commands/storage/shared.rs Outdated
Comment thread src/features/catalog/gallery/hooks/use-global-gallery.ts Outdated
Comment thread src/features/catalog/personas/hooks/use-personas.ts Outdated
Comment thread src/features/catalog/gallery/components/GlobalGalleryPanel.tsx

@github-actions github-actions 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.

Bunny Review inline findings

Comment thread src/features/catalog/gallery/components/GlobalGalleryPanel.tsx Outdated

@github-actions github-actions 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.

Bunny Review inline findings

Comment thread src/features/catalog/gallery/components/GlobalGalleryPanel.tsx

@github-actions github-actions 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.

Bunny Review inline findings

Comment thread src/features/catalog/gallery/hooks/use-global-gallery.ts Outdated
Comment thread src/features/catalog/personas/hooks/use-personas.ts Outdated
@Xelvanis

Xelvanis commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Review findings

  • src/features/catalog/gallery/hooks/use-global-gallery.ts:69 and src/features/catalog/personas/hooks/use-personas.ts:316: batch uploads still persist successful rows, then throw if any sibling upload fails. The UI reports a failed batch while saved images remain visible, and retrying can duplicate the successful subset. This matches the remaining Bunny upload findings.

  • src/features/catalog/gallery/hooks/use-global-gallery.ts:114, src-tauri/src/commands/storage/commands/entities.rs:680, and src-tauri/src/commands/storage/commands/entities.rs:756: global-gallery.folderId is only checked in upload_global_gallery_image; generic storage_create/storage_update still accept missing folder IDs. Since the lightbox move uses storageApi.update, and storage_update is remote-exposed, stale UI or remote callers can still create ghost folder assignments.

  • src-tauri/src/commands/storage/admin.rs:52 and src-tauri/src/commands/storage/admin.rs:130: expunge now clears persona-gallery, global-gallery, and gallery-folders rows, but clear_runtime_media still does not remove the managed gallery asset directory. Persona/global gallery files can be orphaned by persona/media expunge or clear-all.

  • src/features/catalog/gallery/components/GlobalGalleryPanel.tsx:423: lightbox move rollback still uses value equality and an optimistic previousFolderId. Repeated targets or chained failures can restore an unconfirmed folder value, so the newer rollback finding remains valid.

  • src/features/catalog/gallery/components/GlobalGalleryPanel.tsx:465: global gallery lightbox declares aria-modal but only handles Escape. Unlike the persona lightbox, it does not trap Tab/Shift+Tab inside the dialog.

Thread cleanup: resolved addressed Bunny threads PRRT_kwDOReXVXs6HdYDC and PRRT_kwDOReXVXs6HeUSv; left the remaining threads open because they still map to current behavior.

@github-actions github-actions 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.

Bunny Review inline findings

Comment thread src-tauri/src/commands/storage/admin.rs
Comment thread src/shared/lib/gallery-upload.ts
kolacheee added 8 commits June 6, 2026 20:13
Personas gain a Gallery tab mirroring the character gallery: a new
persona-gallery collection with its own upload command, cascade-delete
on persona removal, and expunge/profile-export/remote-runtime wiring.

A new top-level Gallery panel provides a profile-wide image library
(global-gallery) organized into optional flat folders (gallery-folders),
with sorting, drag-an-image-onto-a-folder, and folder rename/delete that
re-files images back to the root rather than deleting them.

Management only; emoji/sticker tagging lands in a later change.
Addresses Bunny review on the global gallery:
- upload_global_gallery_image now drops a folderId to root when the
  gallery-folders parent does not exist, so a stale UI race or remote
  caller cannot strand a row under a ghost folder (folder-delete cleanup
  only unfiles children of folders it actually deletes).
- The lightbox folder move is now optimistic-with-rollback: a failed
  write reverts the dropdown and warns, instead of showing a folder the
  image was never filed into.
A failed move's rollback now only fires when its target is still the one
on screen, so a stale failure in a rapid A→B→C sequence can't drag the
lightbox back to an older folder. Addresses Bunny review follow-up.
The global and persona gallery batch uploads report failure when any file
fails even though successful rows persist. Add a comment explaining this
mirrors the character/chat gallery uploads on purpose, so the behavior
isn't "fixed" in one gallery and split from the others.
- Validate global-gallery folderId in the generic create/update path (not
  just the upload command), so the lightbox move and remote callers can't
  file an image under a folder that doesn't exist.
- Remove each gallery row's managed image file when its collection is
  expunged/cleared, so persona/global/media expunge and clear-all no longer
  orphan files in the shared gallery folder.
- Serialize lightbox folder moves (disable the control while a write is
  pending) so a stale rollback can't restore an outdated folder value.
- Trap Tab/Shift+Tab inside the global gallery lightbox, matching the
  persona/character lightboxes.
- Batch uploads now report partial success honestly across all galleries
  (chat, character, persona, global) via a shared runner: successful rows
  are kept and surfaced, and only a total failure is reported as an error.
Snapshot the gallery file references, clear the rows first, then delete the
files from the snapshot. Removing files before the row clear meant a failed
clear could leave live rows pointing at deleted assets (broken references);
clearing first leaves rows + files intact on failure, and only a post-clear
file hiccup orphans files — the lesser evil. Addresses Bunny review follow-up.
runGalleryUploadBatch now returns the failed files with their reasons (and
describeGalleryUploadFailures formats them), so a partial-failure toast reads
"Some images didn't upload — kek3.png — too large" instead of a bare count.
Gallery upload failures are deterministic (size/type/corrupt bytes), so this
explains which file and why rather than offering a pointless retry — and
preempts "image failed to upload for no reason" reports.
It's referenced only within PanelNavButtons now — the other consumer went
away in the mobile UX rework this branch rebased onto — so drop the unused
export to keep the lint clean.
@kolacheee
kolacheee force-pushed the feat/persona-global-galleries branch from c27ae28 to 696f0d9 Compare June 6, 2026 18:47
The mobile panel grid (TOOLS_PANELS) predates the Gallery panel, so it was
the only nav surface missing it after the mobile UX rework. Add the entry
so Gallery is reachable on mobile, matching the desktop panel buttons.

@github-actions github-actions 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.

Bunny Review inline findings

// caller could otherwise strand the row under a folder that was never created
// (or was just deleted) — and folder-delete cleanup only unfiles children of
// folders it actually deletes, leaving the orphan unreachable. Fall back to root.
let folder_value = match folder_id.map(str::trim) {

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: Global upload validates after creating the asset

Location: src-tauri/src/commands/storage/shared.rs:2958

A delightful inversion: the image bytes are persisted before the folder assignment is checked. If the gallery-folders lookup fails here, the command returns an error while the managed file has already been born, and no cleanup branch can reach it. The generic create path validates first; this upload path conducts the experiment backward and leaks an orphan on validation-time storage failure.

Tip

Suggested fix: Validate the requested folder before persist_image_bytes, or wrap every post-persist error path in the same managed-file cleanup used for failed row creation.

const qc = useQueryClient();
return useMutation({
mutationFn: ({ imageId, folderId }: { imageId: string; folderId: string | null }) =>
storageApi.update("global-gallery", imageId, { folderId }),

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: Global moves still admit ghost folders

Location: src/features/catalog/gallery/hooks/use-global-gallery.ts:91

A charming relapse: uploads may pass through a backend folder check, but moving an existing global image writes through generic storageApi.update with whatever folderId arrives. If that folder was deleted, stale, or fabricated before the mutation lands, global-gallery.folderId can still point at a parent row that no longer exists. The gallery inherits an orphaned filing system and then politely renders the lie.

Tip

Suggested fix: Route moves through a focused backend command that validates the destination folder, or add the same non-null folderId existence check to generic global-gallery update and patch normalization.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This one's a false positive — the move path already enforces the folder-existence check this asks for.

Trace:

  • useMoveGlobalGalleryImagestorageApi.update("global-gallery", id, { folderId }) (use-global-gallery.ts)
  • invokeTauri("storage_update", …)storage_updatestorage_update_inner (entities.rs)
  • storage_update_inner calls validate_gallery_folder_for_patch(state, &entity, &patch)? — the same family of per-entity folder validators the generic create path uses. When the patch touches folderId it runs validate_gallery_folder_assignment, which errors with gallery-folders/{id} was not found if the destination folder doesn't exist.

So a move to a deleted / stale / fabricated folder is rejected server-side; the ghost folderId never reaches storage. Both move entry points — the lightbox folder <select> and drag-to-folder-chip — go through useMoveGlobalGalleryImage, so they're both covered. "Generic storageApi.update" isn't unvalidated: storage_update_inner fans out to per-entity folder guards (chat / connection / lorebook / gallery), and the gallery one was added precisely to address the earlier folder-reference finding.

What's genuinely left is the check→write TOCTOU, which for a local single-process store is effectively nil, and folder-delete already unfiles its own children.

Flagging for maintainer visibility (@Xelvanis) — happy to route moves through a dedicated, narrower backend command if you'd prefer that over the generic update path, but the existence check is enforced today.

upload_global_gallery_image persisted the managed image file and only then
looked up the destination folder. A storage error during that lookup would
bubble up after the file was already written, leaking an orphan the
row-create cleanup path never sees. Resolve the folder first so any lookup
failure aborts before persisting.

@github-actions github-actions 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.

Bunny Review inline findings

// actually deletes, leaving the orphan unreachable; fall back to root. Doing the
// lookup first also means a storage error here can't leak an already-persisted
// managed file (only the row-create path below has file cleanup).
let folder_value = match folder_id.map(str::trim) {

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: Folder validation is still only a pre-write rumor

Location: src-tauri/src/commands/storage/shared.rs:2953

The command observes gallery-folders before persisting bytes, then later creates a global-gallery row carrying that same id. If the folder is deleted between the lookup and create_immediate, the row can still be born with a non-null folderId pointing at a folder the delete cleanup never saw. Contract 1's leak is improved; Contract 2's referential invariant remains incompletely sedated.

Tip

Suggested fix: Make the folder check and row creation one coherent storage-side operation, or revalidate the non-null folder id immediately before commit and clean the just-persisted asset on validation failure.

@SpicyMarinara
SpicyMarinara merged commit ad511de into Pasta-Devs:refactor Jun 7, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants