Before submitting, make sure you've checked the following:
Target branch:dev
Description: Below
Changelog: Below
Documentation: No user-facing API change, no env vars, no deployment steps
Dependencies: No new dependencies
Testing: Manual coverage across open/close paths, multi-chat, embed_url branch, readOnly mode (see below)
Agentic AI Code: AI-assisted authorship. Code has been reviewed line by line by the author, manually tested across multiple scenarios, and has been running in a downstream fork in production for several days without regression.
Code review: Self-reviewed
Design & Architecture: Follows the existing embed / showEmbeds store pattern. No new settings, no UX change.
Git Hygiene: 3 atomic commits, rebased on dev
Title Prefix:feat
Changelog Entry
Description
Citations.svelte mounts <CitationModal> per instance. A chat with N RAG responses has N modal state machines in the DOM, idle until clicked.
This PR applies the same singleton pattern already used by embed / showEmbeds / showControls (writable stores in $lib/stores, sink mounted once at app level, any caller dispatches via store action). Three benefits:
Markdown citation-marker clicks can call openCitation() directly — no bind:this ref hacks back into the right <Citations> instance.
Gives forks an extension point (subscribe to citationModal, render alongside the modal).
Added
src/lib/stores/citations.ts — citationModal writable store + openCitation / closeCitation actions.
Global <CitationModal> mount in src/routes/(app)/+layout.svelte subscribed to the store.
Changed
src/lib/components/chat/Messages/Citations.svelte — dispatches via openCitation() instead of a local <CitationModal bind:show> mount. showSourceModal export kept as a thin wrapper for back-compat with external callers (e.g. ResponseMessage.svelte which still calls citationsElement.showSourceModal(id) from a Markdown click handler).
Deprecated
N/A
Removed
Per-instance modal mount and local state (showCitationModal, selectedCitation) in Citations.svelte.
Fixed
N/A — pure refactor, no bug fix.
Security
N/A
Breaking Changes
None. Existing consumers of Citations.svelte keep working without modification; showSourceModal external API preserved.
Additional Information
Bridge note. The +layout bridge between the store and <CitationModal bind:show={local}> uses one-way sync (store → local) plus a close-back handler:
let citationModalShow = false;
$: citationModalShow = $citationModal.show;
$: if (!citationModalShow && $citationModal.show) closeCitation();
The naive if ($store.show !== local) { sync } form races itself: the X click writes false to local via bind:show, then the reactive immediately re-syncs from the store (still true), undoing the close. The split form above avoids the loop because the first $: only writes local, never reads it; the second $: only triggers when local transitions to false while the store is still open.
Inline Markdown [n] marker click → modal opens (via showSourceModal wrapper, the existing path through ResponseMessage.svelte).
embed_url citation → opens right-side embed panel (unchanged path, the showSourceModal wrapper preserves the embed branch).
readOnly mode (shared chat) → opens link in new tab (unchanged).
Multiple chats in tabs → single global modal instance, no cross-chat leakage.
Follow-ups. The same pattern fits ImagePreview (Image.svelte currently mounts one per image — 7+ call sites in the chat tree) and CodeExecutionModal. Happy to open those as separate small PRs after this is reviewed.
Screenshots or Videos
Refactor with no UX change; visual behavior is identical to current main.
Contributor License Agreement
By submitting this pull request, I confirm that I have read and fully agree to the Contributor License Agreement (CLA), and I am providing my contributions under its terms.
🔄 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/24865
**Author:** [@julenlurnova](https://github.com/julenlurnova)
**Created:** 5/18/2026
**Status:** ❌ Closed
**Base:** `dev` ← **Head:** `feat/citation-modal-store`
---
### 📝 Commits (3)
- [`328474c`](https://github.com/open-webui/open-webui/commit/328474ca1fd0c3ce68d7ed16348dbfc75df44122) feat(chat): add citation modal store
- [`9f89645`](https://github.com/open-webui/open-webui/commit/9f896457628bc411689e7f154f419349b9f5a423) feat(chat): mount global citation modal in layout
- [`ab57a0a`](https://github.com/open-webui/open-webui/commit/ab57a0a93cd18cf1c1561d16f26b914aa6da5508) 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
**Before submitting, make sure you've checked the following:**
- [x] **Target branch:** `dev`
- [x] **Description:** Below
- [x] **Changelog:** Below
- [x] **Documentation:** No user-facing API change, no env vars, no deployment steps
- [x] **Dependencies:** No new dependencies
- [x] **Testing:** Manual coverage across open/close paths, multi-chat, embed_url branch, readOnly mode (see below)
- [x] **Agentic AI Code:** AI-assisted authorship. Code has been reviewed line by line by the author, manually tested across multiple scenarios, and has been running in a downstream fork in production for several days without regression.
- [x] **Code review:** Self-reviewed
- [x] **Design & Architecture:** Follows the existing `embed` / `showEmbeds` store pattern. No new settings, no UX change.
- [x] **Git Hygiene:** 3 atomic commits, rebased on `dev`
- [x] **Title Prefix:** `feat`
# Changelog Entry
### Description
`Citations.svelte` mounts `<CitationModal>` per instance. A chat with N RAG responses has N modal state machines in the DOM, idle until clicked.
This PR applies the same singleton pattern already used by `embed` / `showEmbeds` / `showControls` (writable stores in `$lib/stores`, sink mounted once at app level, any caller dispatches via store action). Three benefits:
1. Removes N-instances cost (memory, listeners, lifecycle).
2. Markdown citation-marker clicks can call `openCitation()` directly — no `bind:this` ref hacks back into the right `<Citations>` instance.
3. Gives forks an extension point (subscribe to `citationModal`, render alongside the modal).
### Added
- `src/lib/stores/citations.ts` — `citationModal` writable store + `openCitation` / `closeCitation` actions.
- Global `<CitationModal>` mount in `src/routes/(app)/+layout.svelte` subscribed to the store.
### Changed
- `src/lib/components/chat/Messages/Citations.svelte` — dispatches via `openCitation()` instead of a local `<CitationModal bind:show>` mount. `showSourceModal` export kept as a thin wrapper for back-compat with external callers (e.g. `ResponseMessage.svelte` which still calls `citationsElement.showSourceModal(id)` from a Markdown click handler).
### Deprecated
_N/A_
### Removed
- Per-instance modal mount and local state (`showCitationModal`, `selectedCitation`) in `Citations.svelte`.
### Fixed
_N/A — pure refactor, no bug fix._
### Security
_N/A_
### Breaking Changes
None. Existing consumers of `Citations.svelte` keep working without modification; `showSourceModal` external API preserved.
---
### Additional Information
**Bridge note.** The `+layout` bridge between the store and `<CitationModal bind:show={local}>` uses one-way sync (store → local) plus a close-back handler:
```svelte
let citationModalShow = false;
$: citationModalShow = $citationModal.show;
$: if (!citationModalShow && $citationModal.show) closeCitation();
```
The naive `if ($store.show !== local) { sync }` form races itself: the X click writes `false` to `local` via `bind:show`, then the reactive immediately re-syncs from the store (still `true`), undoing the close. The split form above avoids the loop because the first `$:` only writes `local`, never reads it; the second `$:` only triggers when `local` transitions to `false` while the store is still open.
**Manual tests performed.**
- Click chip pill → modal opens; X / Esc / backdrop close.
- Inline Markdown `[n]` marker click → modal opens (via `showSourceModal` wrapper, the existing path through `ResponseMessage.svelte`).
- `embed_url` citation → opens right-side embed panel (unchanged path, the `showSourceModal` wrapper preserves the embed branch).
- `readOnly` mode (shared chat) → opens link in new tab (unchanged).
- Multiple chats in tabs → single global modal instance, no cross-chat leakage.
**Follow-ups.** The same pattern fits `ImagePreview` (`Image.svelte` currently mounts one per image — 7+ call sites in the chat tree) and `CodeExecutionModal`. Happy to open those as separate small PRs after this is reviewed.
### Screenshots or Videos
Refactor with no UX change; visual behavior is identical to current `main`.
### Contributor License Agreement
- [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.
---
<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/24865
Author: @julenlurnova
Created: 5/18/2026
Status: ❌ Closed
Base:
dev← Head:feat/citation-modal-store📝 Commits (3)
328474cfeat(chat): add citation modal store9f89645feat(chat): mount global citation modal in layoutab57a0arefactor(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
Before submitting, make sure you've checked the following:
devembed/showEmbedsstore pattern. No new settings, no UX change.devfeatChangelog Entry
Description
Citations.sveltemounts<CitationModal>per instance. A chat with N RAG responses has N modal state machines in the DOM, idle until clicked.This PR applies the same singleton pattern already used by
embed/showEmbeds/showControls(writable stores in$lib/stores, sink mounted once at app level, any caller dispatches via store action). Three benefits:openCitation()directly — nobind:thisref hacks back into the right<Citations>instance.citationModal, render alongside the modal).Added
src/lib/stores/citations.ts—citationModalwritable store +openCitation/closeCitationactions.<CitationModal>mount insrc/routes/(app)/+layout.sveltesubscribed to the store.Changed
src/lib/components/chat/Messages/Citations.svelte— dispatches viaopenCitation()instead of a local<CitationModal bind:show>mount.showSourceModalexport kept as a thin wrapper for back-compat with external callers (e.g.ResponseMessage.sveltewhich still callscitationsElement.showSourceModal(id)from a Markdown click handler).Deprecated
N/A
Removed
showCitationModal,selectedCitation) inCitations.svelte.Fixed
N/A — pure refactor, no bug fix.
Security
N/A
Breaking Changes
None. Existing consumers of
Citations.sveltekeep working without modification;showSourceModalexternal API preserved.Additional Information
Bridge note. The
+layoutbridge between the store and<CitationModal bind:show={local}>uses one-way sync (store → local) plus a close-back handler:The naive
if ($store.show !== local) { sync }form races itself: the X click writesfalsetolocalviabind:show, then the reactive immediately re-syncs from the store (stilltrue), undoing the close. The split form above avoids the loop because the first$:only writeslocal, never reads it; the second$:only triggers whenlocaltransitions tofalsewhile the store is still open.Manual tests performed.
[n]marker click → modal opens (viashowSourceModalwrapper, the existing path throughResponseMessage.svelte).embed_urlcitation → opens right-side embed panel (unchanged path, theshowSourceModalwrapper preserves the embed branch).readOnlymode (shared chat) → opens link in new tab (unchanged).Follow-ups. The same pattern fits
ImagePreview(Image.sveltecurrently mounts one per image — 7+ call sites in the chat tree) andCodeExecutionModal. Happy to open those as separate small PRs after this is reviewed.Screenshots or Videos
Refactor with no UX change; visual behavior is identical to current
main.Contributor License Agreement
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.