Skip to content

fix: serialize skills selected from the / menu as skill mentions - #2

Closed
macodev00 wants to merge 127 commits into
devfrom
cursor/fix-slash-skill-mention-359c
Closed

macodev00 wants to merge 127 commits into
devfrom
cursor/fix-slash-skill-mention-359c

Conversation

@macodev00

@macodev00 macodev00 commented Sep 18, 2026 •

Copy link
Copy Markdown
Owner

Pull Request

Do not open a code pull request unless a maintainer has explicitly requested it or the change is limited to i18n/localization.

The most useful way to help is to give us a clear understanding of the problem: report reproducible bugs in Issues and share proposals in Discussions. We use that context to evaluate solutions and refine the implementation internally, accounting for the broader codebase and ongoing work. External implementations usually require substantial reworking to fit the project's standards, and coordinating those revisions usually take more effort than developing the solution internally. Please follow this process before investing time in a pull request. PRs opened outside these guidelines are generally closed without review.

Maintainer Request

No maintainer has explicitly asked for this PR. This is a fork draft-queue branch, rebased onto latest upstream open-webui/open-webui dev. Do not open an upstream PR until a maintainer requests one. open-webui/open-webui#29978 is still open. Prior upstream PR open-webui#30259 was closed.

Checklist

  • I have read and I understand the contribution policy.
  • This PR targets the dev branch.
  • This PR links to a well-described, confirmed Issue or active Discussion: Closes open-webui/open-webui#29978.
  • A maintainer explicitly asked me to open this PR, or this PR only updates i18n/localization.
  • The change is one logical unit with no unrelated commits.
  • I matched nearby code patterns and avoided unnecessary new settings, abstractions, or dependencies.
  • I manually tested the changed workflow and any nearby behavior that could be affected.
  • I have not added or rewritten automated tests, fixtures, snapshots, or testing infrastructure unless a maintainer explicitly requested them.
  • I updated relevant docs, including the Open WebUI Docs Repository, if needed.
  • I added screenshots for UI changes, and a recording when motion or interaction matters.
  • I reviewed any AI-generated code before submitting it.
  • The PR title uses one of the prefixes listed below.

Title Prefix

Use one of the following prefixes:

  • fix: Bug fixes or corrections

Summary

Selecting a skill from the / menu currently inserts a TipTap mention with no mentionSuggestionChar. TipTap then defaults that attribute to @, so the composer serializes <@id|label>.

The backend skill regex only accepts $ and / markers (SKILL_MENTION_RE in backend/open_webui/utils/skills.py), so the skill is never injected and the raw tag is left in the user message. Selecting the same skill from $ works because TipTap's built-in Mention command sets mentionSuggestionChar from the suggestion char.

/ is the only mention-producing trigger with a custom command. That command has to handle prompts and built-in slash actions, then it inserts the mention node by hand without the trigger char. This change passes mentionSuggestionChar: '/' on that insert. The existing serializer remap ('/' → '$') then emits <$id|label>, which the backend already treats as a skill mention.

Closes open-webui/open-webui#29978.

Verification

Against current upstream dev (702da1e471abd9444143114debfd511d45aa8214):

  • $ suggestion has no custom command, so TipTap sets mentionSuggestionChar to $.
  • / custom command previously used attrs: props only; it now uses attrs: { ...props, mentionSuggestionChar: '/' }.
  • RichTextInput.svelte already remaps / to $ in mention text and Turndown output.
  • None of the 14 upstream commits since the previous branch tip touched MessageInput.svelte, RichTextInput.svelte, or skills.py.

Serializer remap and backend regex (current dev):

mention char "@" -> <@es|label>   backend match: none
mention char "/" -> <$es|label>   backend match: es
mention char "$" -> <$es|label>   backend match: es

That is the same split reported in open-webui#29978: $ and / inject, @ is ignored.

The previous i18n/prettier drive-by commit was dropped. The functional change does not add locale keys, and prettier does not need to rewrite the one edited line. Live chat UI (create skill, pick it from /, send, confirm injection) has not been run in this environment.

Changelog Entry

Added

Changed

Fixed

  • Selecting a skill from the / menu now injects the skill the same way $ does, instead of inserting an ignored @ mention.

Removed

Security

Breaking Changes

Additional Context

Draft-queue only. No upstream PR. Intended base is upstream open-webui/open-webui dev.

Correct compare (ignore the fork dev base, which is still 14 commits behind):
https://github.com/open-webui/open-webui/compare/dev...macodev00:open-webui:cursor/fix-slash-skill-mention-359c?expand=1

Single commit f81ab7af073c8ca20cd08b30264e6b3e29b2a615 by maco <gosarmarcel7@gmail.com>. Single file: src/lib/components/chat/MessageInput.svelte. vs open-webui/open-webui:dev: behind_by=0, ahead_by=1.

Contributor License Agreement

Open in Web Open in Cursor 

@cursor
cursor Bot force-pushed the cursor/fix-slash-skill-mention-359c branch 4 times, most recently from f1de9a1 to f81ab7a Compare September 21, 2026 06:21
andreadegiovine and others added 26 commits September 21, 2026 08:04
* i18n: update and improve it-IT

* i18n: update and improve it-IT

---------

Co-authored-by: andreadegiovine <andreadegiovine@gmail.com>
Staan rejects any count other than its fixed page size of 10, so every search 400ed with the default result count of 3. The API always returns 10 results; the local results[:count] slice already applies the configured count, so the request parameter is simply dropped.
…pen-webui#29757)

Retiring a model leaves every chat that used it stuck. The chat still stores the old model id, so it loads with nothing usable selected and refuses to send until the user picks a replacement by hand, in every old conversation. There is no admin-side way to move those chats across.

Loading a chat now drops model ids that no longer exist, and an empty selection falls into the fallback this function already applies to new chats: the user's default model, then the admin-configured default, then the first available model. A chat that still has one live model keeps it. The filter runs before the single-model permission clamp so a restricted user keeps a live model the chat already has instead of losing it to the clamp.

The model list cannot distinguish a retired model from one whose connection is momentarily unreachable, so during such an outage a chat pinned to that connection will open on the fallback model and record it when saved. Models defined in the workspace are unaffected, since they stay in the list while their connection is down.
…home page (open-webui#29762)

Typing a message, uploading a file or toggling web search in a chat started from the home page was lost on reload: the input came back empty and the uploaded files were gone for good.

Drafts are kept per chat in sessionStorage, but the key was built from the route prop, which stays empty for these chats because creating the chat only swaps the URL with history.replaceState and never re-runs the route load. Drafts were written under the new-chat key while a reload of /c/<id> read the per-chat key. Falling back to the active chat id lines the two up, the same fallback loadChat already uses for this reason.

The Placeholder submit handler also cleared the bare key rather than the current one, which left a stale draft behind once a chat's messages had all been deleted.

Verified against the base commit on a local instance: on base the draft lands under chat-input and is lost on reload, with the change it lands under chat-input-<id> and both the text and the uploaded file come back. Image attachments stay excluded from drafts by design.

The model selection half of open-webui#29760 has a different cause and is not fixed here.

Refs open-webui#29760
…f the URL host (open-webui#29691)

The native edit_image tool fails with "400: [ERROR: Error loading image]" whenever the model hands it an absolute URL for an image Open WebUI already stores. Such a URL is treated as local only when its host string matches the incoming request's host exactly, so a default-port form, a container name or any host the model composed itself falls through to an outbound HTTP fetch instead. That fetch asks /api/v1/files/{id}/content without a session, gets a 401, and the user sees the generic 400.

Match the file URL on its path and let the existing local branch resolve it. Fetching that endpoint over the network can never succeed for a local or a remote instance, because it requires an authenticated user, so the host comparison only decided which way the request failed. Access control is unchanged: the local branch still goes through get_file_content_by_id, which enforces owner, admin or shared access.

Fixes open-webui#29220
open-webui#29262)

* fix: correct recurrence rule parsing for schedules and calendar events

An automation set to repeat a limited number of times, say ten or a hundred, was treated as a one-shot and reported no repeat interval, because any count whose digits began with a one matched a text check for the one-shot case. The scheduler already answers that question correctly by asking the rule for its next two occurrences, so the text check is gone and the count is read as the number it is.

A recurrence rule that carries its start date on the same line as the repeat text kept that date when the automation was parsed, so the schedule ran from whatever date the rule happened to carry and ignored the start the user picked. The filter that drops the start date now splits the rule on any whitespace, the same way the rule parser itself does, so both agree on where one part of the rule ends and the next begins.

The same mismatch on the calendar path anchored a recurring event to the date inside its rule, so occurrences showed up before the event had begun and at the wrong time of day. That filter splits the rule the same way now, and the series starts at the event's own start.

All three come from one place, recurrence rules being matched and cut as text. Rules written across several lines, which is what the schedule and calendar editors produce, behave exactly as before.

* refac: name rrule token vars parts to match calendar.py
…ages (open-webui#29623)

The remote chat-image fetch now asks for an identity-encoded response and skips one that comes back content-encoded.

An image whose host stores and echoes a `Content-Encoding` regardless of what the client asks for (an S3 or MinIO object uploaded with that metadata) is no longer inlined; the message is forwarded with the original URL instead, the same way an unreachable image already behaves.
…rver (open-webui#28185)

Every client in a note re-broadcasts each update it receives, with a full content snapshot attached, and the server appends each echo to the document log and writes the note again. Traffic and note writes therefore scale with the number of people who have the note open: every extra participant adds one more full echo of every keystroke.

Remote updates are now applied with the `'server'` origin the state path already uses, which the local listener ignores, so the echo stops. The echo did carry one thing worth keeping: the receiving client holds the merged document, which the sender had not seen yet, so each receiver now sends a content-only message once the edits settle, debounced 500ms, and flushes a pending one when the editor is torn down. The backend accepts an update message with no `update` field for that case, where it previously raised and dropped the save.

Replaying keystrokes at 120ms with a real Yjs document, socket messages fall 48.8% with two clients, 65.6% with three and 79.2% with five, with byte counts tracking the same on notes up to 50KB. Server-side update appends drop by a factor of the client count. The cost is one snapshot upload per receiving client per typing pause, which the server's own debounce then collapses into a single extra note write however many people are watching. A snapshot has to come from a client because the server cannot rebuild the markdown, HTML and JSON shape the note record stores. It also moves the merge 500ms later than the echo delivered it, so if two edits cross on the wire and every editor then loses its connection and closes inside that window, the note keeps what it was last sent and one of the two edits is lost. A client still connected at teardown flushes its pending snapshot, and any other editor left in the note closes the gap.
…open-webui#28184)

Long conversations get progressively more expensive to stream into. Every message-level write reloads the entire chat JSON, walks every string in it for a null-byte sanitisation pass and rewrites the whole column, and reading a single message loads and validates the whole chat too. The websocket event emitter reads and then writes, so one message event round-trips the full conversation twice to change a few hundred bytes.

Reading one message now selects only history.messages out of the JSON column and indexes it in Python, so the rest of the chat is neither loaded nor model-validated. Writing one message now sanitises only what is actually entering the chat, plus the title column that is mirrored off the blob, instead of re-walking a conversation that was already sanitised when it was written. Legacy rows are still healed on read by get_chat_by_id, which is unchanged.

One behaviour change: chats written before the null-byte sanitisation existed can still hold null bytes in the stored JSON. Those rows used to be rewritten clean as a side effect of any message write, and are now cleaned when the chat is read instead. The visible consequence is that the message write and delete endpoints echo back the updated chat, and for such a legacy row that echo now carries the raw null bytes rather than stripped ones, until the next read of that chat heals it. A GET of the chat is unaffected, and title, the one text column that PostgreSQL cannot store a null byte in, is still sanitised on every write.

Measured on SQLite with a 10.8 MB chat (3000 messages): a single-message upsert is 227 ms before the branch and 173 ms after, and the legacy single-message read is 135 ms before and 79 ms after. The whole-blob scan the delete path used to run costs 5.40 ms on a 1.8 MB chat and 38.79 ms on the 10.8 MB one, and is gone.

Ref open-webui#28169

### Contributor License Agreement

<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.

Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA.
-->

- [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.

> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
…#28176)

With the Redis websocket manager the shared model pool is a `RedisDict`, so resolving a model id fetches from Redis, and more than one of those fetches pulls the whole pool. Every chat request pays that latency and that traffic, and the cost grows with the number of models configured: at 120 models a request moves several hundred kilobytes to ask about models that have not changed since the last request.

The pool now keeps a per-worker cache of the hash and refetches it only when the signature key that `set()` already maintains changes. A cache may only trust a signature that describes the bytes it fetched, so every write invalidates the signature and no signature value is ever issued twice. Without a fresh token each time, a pool that flaps back to an earlier state returns to an earlier digest, so a reader that races the write keeps serving the old pool. `delete_many()` was a second hole in that: it updated a dead attribute and never touched the signature, so readers kept serving models it had already removed. A TTL cache would have been simpler, but it serves a pool it already knows may be stale and costs the same single round trip this check costs.

Replaying the real access pattern, a request that finds the pool unchanged moves about 1 KB no matter how many models are configured, over the same number of round trips as before, except after a write that leaves no signature, which holds reads at two commands until the next non-empty `set()` writes one. A request that first sees a changed pool costs one extra command, and so does rewriting the pool. Values stay cached serialized, so `items()` and `values()` still decode on every call, and each worker holds the whole pool in memory.
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
silentoplayz and others added 11 commits September 24, 2026 23:58
…ui#31342)

Fixes open-webui#31340

When the attached Open Terminal runs on Windows, the AGENTS.md in its home directory was never handed to the model. The home check only accepted POSIX absolute paths, so a drive-letter or UNC home such as C:\ProgramData\OpenTerminal\inst was treated as invalid and the file was skipped without any log line.

The check now also accepts Windows absolute paths. Relative and drive-relative homes are still skipped, and POSIX homes send byte-identical requests.

The file path keeps its forward-slash join. Open Terminal normalises the path on the host, so C:\Users\bob/AGENTS.md opens C:\Users\bob\AGENTS.md. Picking ntpath.join for Windows homes would give native separators on the wire but adds a second branch for no change in which file gets read.

Verified against Open Terminal's own path resolution with Windows semantics for drive-letter, forward-slash, drive-root, trailing-backslash and UNC homes, over both the backend request and the browser direct-connection path.
…webui#31356)

Korean EUC-KR and Japanese Shift-JIS text files were stored as garbled Chinese characters, so retrieval, knowledge bases and the model context all worked on text that is not in the file.

Encoding detection puts chardet's guess in front of a fixed GB18030, Big5, EUC-KR, EUC-JP try order, and GB18030 decodes almost any double-byte text without an error. The guess map was written for chardet 5. Since the bump to chardet 7 in v0.10.0, Korean text is reported as CP949 and Japanese text as cp932 or SHIFT_JIS, which the map either did not know or dropped because the codec was not in the try order, so these files fell through to GB18030.

The map now covers CP949 and cp932, and a mapped guess is always tried first. SHIFT_JIS maps to cp932, the Windows superset, because chardet also reports SHIFT_JIS for ordinary Japanese files containing characters such as ① or ㈱ that plain Shift-JIS cannot decode; this is the same subset-to-superset rule the map already applies to GB2312.

Korean and Japanese files now decode correctly, and Chinese, EUC-JP, UTF-8 and Western files decode as before. The one trade-off of trusting the guess: chardet 7 labels some files holding only a few Chinese characters (a short label or a one-line comment) as CP949, and those now read as Korean. No regressions were found in files with more Chinese text than that.

Fixes open-webui#31352
…31316)

With ENABLE_OAUTH_GROUP_CREATION on, every group in a user's OAuth claim was created on login, including groups matching OAUTH_BLOCKED_GROUPS. Membership sync already ignored those groups, so the result was empty groups nobody could join. With IdPs that send a user's full directory membership (Keycloak backed by LDAP/AD), one login could fill the group table with thousands of them.

Group creation now applies the same blocklist check as the membership add and remove steps, so a blocked group is never created, joined or left through OAuth. Groups that are not blocked are created as before.

Fixes open-webui#29558
…pen-webui#31349)

On the default SQLite setup (session sharing off), a POST to /api/v1/models/sync containing any model that already exists answered 200 with an empty list and stored nothing. After a 5 second stall, the only trace was "database is locked" in the server log.

The sync wrote each model's access grants while the model update was still uncommitted. Without session sharing the grant writes run on a second database session, and SQLite allows one writer at a time, so that write waited on the same request's uncommitted update until the busy timeout expired and the whole sync was dropped.

Grants are now written after the model changes are committed, the same order model create and update already use. A failed model commit now also leaves every grant untouched. PostgreSQL and setups with session sharing on behave as before.

Fixes open-webui#31346
@cursor
cursor Bot force-pushed the cursor/fix-slash-skill-mention-359c branch from 6b13397 to c7440a5 Compare September 25, 2026 06:08
Classic298 and others added 15 commits September 26, 2026 07:18
…ter a reload (open-webui#31439)

With a connection set to the Responses API, when the provider reported a reply as failed, the error showed while streaming but was gone after a reload, leaving an empty reply. Some other provider errors never showed up at all, not even while streaming. Both kinds of error now show up and are still there after a reload, the same as on Chat Completions connections.

Fixes open-webui#31433
…g block (open-webui#31438)

With reasoning models, when the first part of the answer arrived together with the end of the thinking block and ended with a space, that space went missing, so "The answer is 4." was shown and saved as "The answeris 4.". That space is now kept.

Fixes open-webui#31435
…en-webui#31437)

When an API request to an Ollama model set max_tokens, Open WebUI passed it on in a place Ollama does not read, so Ollama ignored it and replies ran to full length. The limit now reaches Ollama as its own output length setting, so replies stop at the requested length. It also wins over a max_tokens value saved in the model's advanced parameters, as the API docs describe. Chats in the web UI were not affected, since their limit already reached Ollama correctly.

Fixes open-webui#31432
… saved incorrectly (open-webui#31431)

Sometimes a reply where the model used tools gets saved with a tool result that no call in that reply asked for, or with a tool call that never got its result. The chat history was then sent to the provider unchanged, Anthropic and Bedrock rejected it, and every following message in that chat failed until the user deleted the broken reply. Now each tool call is only kept together with its own result from the same reply, and the unmatched calls and results are left out of what gets sent to the model. The chat itself is not changed, and correctly saved chats are sent exactly as before.

Fixes open-webui#28937
…1436)

When a model wraps its answer in <|begin_of_solution|> and <|end_of_solution|>, only the opening marker was removed. The closing marker stayed visible in the reply and was saved with the message, and anything the model wrote after it was glued onto the answer. Now both markers are removed and text after the answer shows up as a normal part of the reply.

Fixes open-webui#31434
…-webui#31429)

Picking a skill from the / menu in the chat input inserted it as an @ mention, so the skill was never loaded and its raw tag ended up in the message the model received. Picking the same skill from the $ menu worked. A skill chosen through / is now sent the same way as through $ and gets applied.

Fixes open-webui#29978
…ne (open-webui#30388)

The clone endpoint now resolves and checks the share before reading its snapshot, matching the order used by the shared chat view endpoint.
…webui#31351)

A link whose host refuses the connection, such as a closed port, still came back from POST /api/v1/retrieval/process/web as "Error querying knowledge base", so the caller was told the knowledge base failed when the link was the problem.

The web loaders log a failed fetch and return no documents. That empty result then failed while being saved to the vector store, and the save error was the one reported.

process_web now answers with the existing "Could not read content from <url>" 400 as soon as the loader returns no documents, the same message a link refused by the fetch filter already gets. The check sits in the endpoint so web search and the other users of the loaders keep their current behaviour.

With process=false or embedding bypassed, an unreachable link now gets the same 400 where it used to return 200 with empty content.

Fixes open-webui#31347
@cursor
cursor Bot force-pushed the cursor/fix-slash-skill-mention-359c branch from c7440a5 to 420b4a2 Compare September 26, 2026 06:17
@macodev00

Copy link
Copy Markdown
Owner Author

Fix landed upstream via open-webui#31429 (identical patch); closing fork draft.

@macodev00 macodev00 closed this Sep 26, 2026
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.

10 participants