Skip to content

fix(desktop): retain new-workspace image attachments across navigation - #6282

Merged
Kitenite merged 1 commit into
mainfrom
fix-image-persistence-swi
Aug 9, 2026
Merged

Kitenite merged 1 commit into
mainfrom
fix-image-persistence-swi

Conversation

@Kitenite

@Kitenite Kitenite commented Aug 9, 2026 •

Copy link
Copy Markdown
Member

Problem

On the v2 new-workspace page (/new-workspace route and the empty-dashboard create screen), typed prompt text survives navigating away and back, but attached images vanish.

The text lives in the module-level useNewWorkspaceDraftStore; attachments lived in React state inside PromptInputProvider, which those surfaces mount inside the route. Navigating away unmounts the provider, dropping the file list and revoking every blob URL. The control-arm modal never had the bug only because its provider sits in the always-mounted layout.

Fix

  • packages/ui prompt-input.tsx: PromptInputProvider accepts an opt-in attachmentsStore (created via createPromptInputAttachmentsStore()). When provided, attachment state lives in that module-scoped store and blob URLs are no longer revoked on provider unmount — the store owns their lifetime. Without the prop, behavior is unchanged.
  • new-workspace-attachments.ts (new): singleton store wired into all three create surfaces (route, empty screen, modal — both experiment arms now share one draft, matching the text). A subscription on the draft store's resetKey clears attachments + revokes URLs on successful create/discard, even while no create surface is mounted.
  • NewWorkspaceScreen: the filename→source-path map backing "Reveal in Finder" was also component-local; hoisted into the same module and cleared on reset.

Attachment ids survive navigation, so the idempotent upload cache (keyed (fileId, hostUrl)) keeps its entries — no re-upload on return, and the per-host visibility filter still matches.

Testing

  • New new-workspace-attachments.test.ts (4 cases): persistence, reset clearing + URL revocation, subscriber notification. Verified the reset test fails when the reset handler is mutated.
  • Monorepo typecheck, bun run lint, and packages/ui tests pass.

Summary by cubic

Fixes lost image attachments on the v2 new-workspace screens. Attachments now persist across navigation and only clear on successful create or discard.

  • Bug Fixes

    • Store attachments in a module-scoped singleton for the new-workspace draft; all three create surfaces use it, so files survive route changes and modal toggles.
    • Clear attachments and revoke blob URLs on draft reset via a resetKey subscription; also hoist and clear the filename→source-path map for “Reveal in Finder.”
    • Preserve attachment IDs to keep the upload cache intact and avoid re-uploads.
    • Added unit tests for persistence, reset behavior, and subscriber notifications.
  • New Features

    • PromptInputProvider in @superset/ui/ai-elements/prompt-input accepts an optional attachmentsStore.
    • Added createPromptInputAttachmentsStore() to hold attachment state outside the provider; blob URLs are not revoked on provider unmount when used. Behavior is unchanged when not provided.

Written for commit e7a974f. Summary will update on new commits.

Review in cubic

The /new-workspace route mounted PromptInputProvider inside the route, so
navigating away dropped the attachment state and revoked every blob URL,
while the prompt text survived in the module-level draft store.

PromptInputProvider now accepts an opt-in attachmentsStore that moves
attachment state into a module-scoped external store (blob URLs are then
owned by the store, not revoked on unmount). All three new-workspace
create surfaces share one singleton store, cleared on draft reset. The
filename-to-source-path map for Finder reveal is hoisted with it.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds an external attachment store to PromptInputProvider. New-workspace routes and modals share attachment state, source paths, and reset cleanup across provider unmounts and draft updates.

Changes

New-workspace attachment persistence

Layer / File(s) Summary
Prompt input external attachment store
packages/ui/src/components/ai-elements/prompt-input.tsx
Adds external attachment-store types and factory support. Provider mutations use either external or local state. External attachments remain valid across provider unmounts, while clear() revokes blob URLs.
New-workspace attachment lifecycle
apps/desktop/src/renderer/stores/new-workspace-attachments.ts, apps/desktop/src/renderer/stores/new-workspace-attachments.test.ts
Adds module-scoped attachment and source-path stores. Draft reset clears both stores. Tests cover retention, reset cleanup, blob URL revocation, and subscriptions.
New-workspace provider and path integration
apps/desktop/src/renderer/routes/_authenticated/_dashboard/new-workspace/page.tsx, apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/NewWorkspaceEmptyScreen/NewWorkspaceEmptyScreen.tsx, apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/...
Configures all new-workspace prompt providers with the shared attachment store. Shared source paths now support duplicate detection, file recording, and Finder reveal lookups.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant NewWorkspaceScreen
  participant PromptInputProvider
  participant newWorkspaceAttachmentsStore
  participant DraftState
  NewWorkspaceScreen->>PromptInputProvider: add attachment
  PromptInputProvider->>newWorkspaceAttachmentsStore: store attachment item
  NewWorkspaceScreen->>newWorkspaceAttachmentsStore: store source path
  DraftState->>newWorkspaceAttachmentsStore: change resetKey
  newWorkspaceAttachmentsStore->>newWorkspaceAttachmentsStore: clear attachments and source paths
  newWorkspaceAttachmentsStore-->>PromptInputProvider: notify subscribers
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title uses conventional commit format and clearly describes retaining new-workspace image attachments across navigation.
Description check ✅ Passed The description clearly explains the problem, implementation, affected surfaces, cleanup behavior, and testing; only the template checklist is omitted.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-image-persistence-swi

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.

@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 (2)
apps/desktop/src/renderer/stores/new-workspace-attachments.test.ts (1)

16-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tests cover the store contract but not the provider-unmount claim.

The four tests verify persistence, reset cleanup, URL revocation, and subscriber notification. The central behavior of the PR is that PromptInputProvider skips blob URL revocation on unmount when attachmentsStore is set. No test asserts that.

That test belongs next to PromptInputProvider in packages/ui, because it needs to mount and unmount the provider. Add a case that mounts the provider with a store, adds a file, unmounts, and asserts URL.revokeObjectURL was not called.

Do you want me to draft that test?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/stores/new-workspace-attachments.test.ts` around
lines 16 - 63, Add a provider-level test alongside PromptInputProvider in
packages/ui that mounts the provider with an attachmentsStore, adds an
attachment, unmounts it, and asserts URL.revokeObjectURL was not called. Keep
the test isolated by restoring the revokeObjectURL mock and cleaning up the
mounted provider afterward.
apps/desktop/src/renderer/stores/new-workspace-attachments.ts (1)

12-17: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Remove path entries when attachments are removed.

newWorkspaceAttachmentPaths is keyed by filename and only cleared on resetDraft. Removing a card calls attachments.remove(id), leaving the path keyed by file.filename; a later same-named file can be poisoned if it comes from a different path. Delete/clear the corresponding entry on attachments.remove(), or key the map by attachment id.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/stores/new-workspace-attachments.ts` around lines
12 - 17, Update the attachment removal flow around attachments.remove() to
delete the removed attachment’s corresponding entry from
newWorkspaceAttachmentPaths using its filename, while preserving resetDraft’s
full-map clearing behavior. Ensure removal occurs before or alongside the card
deletion so a later same-named file cannot reuse stale path data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/desktop/src/renderer/stores/new-workspace-attachments.test.ts`:
- Around line 16-63: Add a provider-level test alongside PromptInputProvider in
packages/ui that mounts the provider with an attachmentsStore, adds an
attachment, unmounts it, and asserts URL.revokeObjectURL was not called. Keep
the test isolated by restoring the revokeObjectURL mock and cleaning up the
mounted provider afterward.

In `@apps/desktop/src/renderer/stores/new-workspace-attachments.ts`:
- Around line 12-17: Update the attachment removal flow around
attachments.remove() to delete the removed attachment’s corresponding entry from
newWorkspaceAttachmentPaths using its filename, while preserving resetDraft’s
full-map clearing behavior. Ensure removal occurs before or alongside the card
deletion so a later same-named file cannot reuse stale path data.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 395e5c0d-f395-43fa-aa8d-f53b6d5c3416

📥 Commits

Reviewing files that changed from the base of the PR and between 24f1fe4 and e7a974f.

📒 Files selected for processing (7)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/new-workspace/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/NewWorkspaceEmptyScreen/NewWorkspaceEmptyScreen.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceModal.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/NewWorkspaceScreen/NewWorkspaceScreen.tsx
  • apps/desktop/src/renderer/stores/new-workspace-attachments.test.ts
  • apps/desktop/src/renderer/stores/new-workspace-attachments.ts
  • packages/ui/src/components/ai-elements/prompt-input.tsx

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 7 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/ui/src/components/ai-elements/prompt-input.tsx">

<violation number="1" location="packages/ui/src/components/ai-elements/prompt-input.tsx:170">
P3: `get: () => files` returns the store's internal mutable array directly. Because useSyncExternalStore relies on get() returning a value that only changes when the store is actually updated, any consumer that mutates the returned array in place would bypass notify() — subscribers wouldn't re-render and React would see a stale snapshot. Today all internal call sites reassign the array via set, so it isn't triggered yet, but returning a defensive copy makes the store robust and keeps the external-API contract explicit that updates must go through set/clear.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Surf as Create Surfaces (page/empty/modal)
    participant Prov as PromptInputProvider
    participant AttStore as NewWorkspaceAttachmentsStore
    participant Paths as NewWorkspaceAttachmentPaths
    participant DraftStore as useNewWorkspaceDraftStore

    Note over Surf,DraftStore: NEW: Attachments persist across navigation via module-scoped store

    Surf->>Prov: mount with attachmentsStore
    Prov->>AttStore: subscribe + get() (useSyncExternalStore)
    AttStore-->>Prov: current files (or [])

    alt User adds/drops files
        Surf->>Prov: add(files)
        Prov->>AttStore: set(prev.concat(items)) + create object URLs
        Surf->>Paths: set(filename, sourcePath) for Finder reveal
    end

    Note over Surf,Prov: Navigate away/back (provider unmounts/remounts)
    Prov->>Prov: Do NOT revoke URLs (store owns lifetime)
    Prov->>AttStore: re-subscribe/get on remount
    AttStore-->>Prov: files still present

    Note over Surf,DraftStore: On successful create or explicit discard
    Surf->>DraftStore: resetDraft()
    DraftStore->>DraftStore: resetKey changes
    DraftStore-->>AttStore: module-level subscription notified
    AttStore->>AttStore: clear() + revoke all blob URLs
    Paths->>Paths: clear()
    AttStore-->>DraftStore: reset complete

    Note over AttStore,Surf: Stable attachment IDs keep upload cache entries valid (no re-upload)
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}
};
return {
get: () => 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.

P3: get: () => files returns the store's internal mutable array directly. Because useSyncExternalStore relies on get() returning a value that only changes when the store is actually updated, any consumer that mutates the returned array in place would bypass notify() — subscribers wouldn't re-render and React would see a stale snapshot. Today all internal call sites reassign the array via set, so it isn't triggered yet, but returning a defensive copy makes the store robust and keeps the external-API contract explicit that updates must go through set/clear.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ui/src/components/ai-elements/prompt-input.tsx, line 170:

<comment>`get: () => files` returns the store's internal mutable array directly. Because useSyncExternalStore relies on get() returning a value that only changes when the store is actually updated, any consumer that mutates the returned array in place would bypass notify() — subscribers wouldn't re-render and React would see a stale snapshot. Today all internal call sites reassign the array via set, so it isn't triggered yet, but returning a defensive copy makes the store robust and keeps the external-API contract explicit that updates must go through set/clear.</comment>

<file context>
@@ -141,8 +142,61 @@ export const useProviderAttachments = () => {
+		}
+	};
+	return {
+		get: () => files,
+		set: (next) => {
+			files = next;
</file context>

@github-actions

github-actions Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

@Kitenite
Kitenite merged commit e7a6a32 into main Aug 9, 2026
19 checks passed
@Kitenite
Kitenite deleted the fix-image-persistence-swi branch August 9, 2026 16:00

This branch was previously deployed

1 inactive deployment
preview — e7a974f5 Deployed Aug 9, 2026 by Kitenite via Deploy API #7302
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.

1 participant