Markdown citation-marker clicks can call openCitation() directly — no bind:this ref hacks into the right instance.
Gives forks an extension point (subscribe to citationModal, render alongside).
Changes
NEW src/lib/stores/citations.ts — writable + openCitation/closeCitation
src/routes/(app)/+layout.svelte — mount once, subscribed to store
src/lib/components/chat/Messages/Citations.svelte — dispatch via store instead of local mount; showSourceModal export kept as thin wrapper for back-compat
Bridge note
The +layout bridge is one-way sync (store→local) + close-back handler. The naive if ($store.show !== local) { sync } form races itself: the X click writes false to local via bind:show, the reactive immediately re-syncs from store (still true), undoes the close. The split form (one $: writes local from store, a separate $: writes store-close when local goes false) avoids the loop.
Tested
Click chip pill → modal opens; X / Esc / backdrop close
readOnly mode (shared chat) → opens link in new tab (unchanged)
Multiple chats in tabs → single global modal, no cross-chat leakage
Follow-ups (separate PRs if this lands)
Same pattern fits ImagePreview (Image.svelte mounts one per image — 7+ call sites in the chat tree) and CodeExecutionModal. Happy to open those as small PRs after this is reviewed.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.
## 📋 Pull Request Information
**Original PR:** https://github.com/open-webui/open-webui/pull/24864
**Author:** [@julenlurnova](https://github.com/julenlurnova)
**Created:** 5/18/2026
**Status:** ❌ Closed
**Base:** `main` ← **Head:** `feat/citation-modal-store`
---
### 📝 Commits (3)
- [`86932af`](https://github.com/open-webui/open-webui/commit/86932af9c40000b82145707fef36fb5cad38e1dc) feat(chat): add citation modal store
- [`1956616`](https://github.com/open-webui/open-webui/commit/1956616d8e407cfbb63a7818d43a77a4a3ab20cc) feat(chat): mount global citation modal in layout
- [`c486904`](https://github.com/open-webui/open-webui/commit/c48690426b7ee0952e383b0d7bb4ce71a6e53255) refactor(chat): dispatch citation modal via store
### 📊 Changes
**3 files changed** (+48 additions, -20 deletions)
<details>
<summary>View changed files</summary>
📝 `src/lib/components/chat/Messages/Citations.svelte` (+4 -20)
➕ `src/lib/stores/citations.ts` (+31 -0)
📝 `src/routes/(app)/+layout.svelte` (+13 -0)
</details>
### 📄 Description
Citations.svelte mounts <CitationModal> per instance. A chat with N RAG responses has N modal state machines in the DOM, idle until clicked.
Same shape as the embed/showEmbeds pattern already in $lib/stores. Applied to citations:
- Removes N-instances cost (memory, listeners, lifecycle).
- Markdown citation-marker clicks can call openCitation() directly — no `bind:this` ref hacks into the right <Citations> instance.
- Gives forks an extension point (subscribe to citationModal, render alongside).
### Changes
- NEW `src/lib/stores/citations.ts` — writable + openCitation/closeCitation
- `src/routes/(app)/+layout.svelte` — mount <CitationModal> once, subscribed to store
- `src/lib/components/chat/Messages/Citations.svelte` — dispatch via store instead of local mount; `showSourceModal` export kept as thin wrapper for back-compat
### Bridge note
The +layout bridge is one-way sync (store→local) + close-back handler. The naive `if ($store.show !== local) { sync }` form races itself: the X click writes false to local via `bind:show`, the reactive immediately re-syncs from store (still true), undoes the close. The split form (one `$:` writes local from store, a separate `$:` writes store-close when local goes false) avoids the loop.
### Tested
- Click chip pill → modal opens; X / Esc / backdrop close
- Inline Markdown [n] marker click → modal opens (via showSourceModal wrapper)
- embed_url citation → opens right-side embed panel (unchanged path)
- readOnly mode (shared chat) → opens link in new tab (unchanged)
- Multiple chats in tabs → single global modal, no cross-chat leakage
### Follow-ups (separate PRs if this lands)
Same pattern fits ImagePreview (Image.svelte mounts one per image — 7+ call sites in the chat tree) and CodeExecutionModal. Happy to open those as small PRs after this is reviewed.
---
<sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
📋 Pull Request Information
Original PR: https://github.com/open-webui/open-webui/pull/24864
Author: @julenlurnova
Created: 5/18/2026
Status: ❌ Closed
Base:
main← Head:feat/citation-modal-store📝 Commits (3)
86932affeat(chat): add citation modal store1956616feat(chat): mount global citation modal in layoutc486904refactor(chat): dispatch citation modal via store📊 Changes
3 files changed (+48 additions, -20 deletions)
View changed files
📝
src/lib/components/chat/Messages/Citations.svelte(+4 -20)➕
src/lib/stores/citations.ts(+31 -0)📝
src/routes/(app)/+layout.svelte(+13 -0)📄 Description
Citations.svelte mounts per instance. A chat with N RAG responses has N modal state machines in the DOM, idle until clicked.
Same shape as the embed/showEmbeds pattern already in $lib/stores. Applied to citations:
bind:thisref hacks into the right instance.Changes
src/lib/stores/citations.ts— writable + openCitation/closeCitationsrc/routes/(app)/+layout.svelte— mount once, subscribed to storesrc/lib/components/chat/Messages/Citations.svelte— dispatch via store instead of local mount;showSourceModalexport kept as thin wrapper for back-compatBridge note
The +layout bridge is one-way sync (store→local) + close-back handler. The naive
if ($store.show !== local) { sync }form races itself: the X click writes false to local viabind:show, the reactive immediately re-syncs from store (still true), undoes the close. The split form (one$:writes local from store, a separate$:writes store-close when local goes false) avoids the loop.Tested
Follow-ups (separate PRs if this lands)
Same pattern fits ImagePreview (Image.svelte mounts one per image — 7+ call sites in the chat tree) and CodeExecutionModal. Happy to open those as small PRs after this is reviewed.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.