master
75
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6886628cb1 |
ui : Search dialog (#31173)
* ui : replace the sidebar search mode with a search dialog Search opens a spotlight dialog (Cmd+K or the Search entry under New chat in the sidebar) with keyboard navigation over recent chats and sidebar actions. Phones keep the search page. Sidebar actions show their shortcuts, with new ones for Settings (Cmd+,) and Models (Shift+Cmd+M). Assisted-by: Claude (Opus 5.5 Medium) * ui : ignore search dialog keys while an IME is composing Enter confirms an IME candidate and the arrow keys move through the candidates, so the search field leaves them to the IME instead of opening the highlighted result or moving the highlight. Assisted-by: Claude (Opus 5.5 Medium) |
||
|
|
0d5ca60098 |
ui: list org-less models first in the model selector (#31215)
The org-less group renders without a header, so when it followed a named org its models looked like they belonged to that org. Put it ahead of the named org groups. Fixes #25227 Assisted-by: Claude (Opus 5.5 Medium) |
||
|
|
1a97e387d3 |
ui: add conversation compaction (#27401)
* ui: add conversation compaction Add a compaction action that asks the model to summarize the conversation so far and stores the summary in a new compaction node appended to the branch. Later requests slice the branch at the last compaction node, keeping the system prefix and continuing from the summary instead of the full history, which frees up context while the complete history stays visible and navigable in the UI. The slice is applied once at the request funnel, covering the plain send, the agentic flow and the KV cache pre-encode, plus the continue path that bypasses it. The summary request is a one-shot outside the SSE replay machinery, streamed live into the node with a full rollback on abort, error or empty result. The action is exposed as a /compact slash command and a Compact button in the context gauge popup, with an overridable summarization prompt in the settings. * ui: pre-fill KV cache and refresh context gauge after compaction Once the summary is stored, pre-encode the compacted branch and stamp the returned prompt timings on the compaction node: re-encoding the reduced prefix is part of the compaction itself, so the server KV cache and the context gauge reflect the summary right away. The summary stream is registered under a prefixed identity (compact::convId), so the visibility kick and byte-offset resume protect it through tab switches while the reload probe never reattaches it as a chat turn. The stamped node becomes the shared context truth for the gauge and the processing state restore, and the stale live state that kept the gauge full is cleared. * chore: format * ui: allow a dedicated model for conversation compaction An opt-in setting reveals a model selector whose options come from the live router model list. The chosen model writes the summary and is loaded on demand when it is not resident, while the default path keeps resolving to the conversation's own model. * ui: align the compaction model field with project conventions * ui: fix the model option helpers import path The compaction model selector pulls filterModelOptions and groupModelOptions from the models component folder, where they no longer live, so the build fails to resolve the module. Point the import at the navigation folder, which now owns them. Missed during the rebase: the helpers moved in a commit that does not touch any compaction file, so nothing conflicted. * ui: import the model option helpers from models/utils * ui: follow the moved models selector in the compaction model field The models manager moved ModelsSelectorList under ModelsSelector and the model option helpers to /utils, where groupModelOptions takes the options alone. Drop the stale copy the rebase kept and read the helpers from their new home. * ui: drop the dedicated compaction model A summary written by another model loses the prefix the conversation model cached, so the whole history is prefilled again, and the model then resumes from text it did not write. The conversation model always writes its own summary. * ui: keep the last steps of a compacted conversation A compaction now folds the steps the model reads but the last ones, a step being a user message or an assistant message with its tool results, and stores how many it kept on the compaction node. The model then reads the system prefix, the summary, the kept steps and what follows. The summary request is the request the chat sends, with its options, tools and message shaping, cut before the preserved steps: the server reuses the prefix it cached and only the instruction is new. The number of preserved steps is a setting, 10 by default. * ui: run every compaction summary through one path Three callers needed the same thing - the automatic pre-encode pass, the action in the context gauge, and re-running a summary from the block - and each of them had grown its own copy of the request, the streaming and the storing. There is one path now: a summarization owns a node, streams into it, and reports itself through a ledger the rest of the app reads (`isCompacting`, which node belongs to which conversation). Re-running a summary is the same call on an existing node, so the two entry points cannot drift apart again. * ui: give the compaction row its own summary UI The summary was a line of text in a tool-call-shaped collapsible, which is how a compaction reads as a side note rather than as the thing the rest of the conversation now stands on. The row is its own block: a divider that names the boundary, and the summary inside it with the model that wrote it, its statistics, and the same actions an assistant turn has - copy, edit, fork, regenerate and delete all apply to the summary, and it collapses to the divider once a summary is in, because the shrunk context is the headline and the prose is the detail. The divider is a variant of the shared collapsible block, which also needed the toggle and the title split into snippets so a caller can lay them out without copying the markup. The action icons no longer take a role: they render what a message offers, and a compaction offers the same as any other stored message. * ui: report compaction progress on the row The row owns the run, so the row says how it is going: the prompt progress in the header while the context is being read, the phase once the summary starts streaming, and the token count once it is in. Before the first summary token there is nothing to collapse, so the header stands on its own instead of offering an empty block. * ui: keep the summarized turns quiet while a compaction runs The turns being summarized used to keep their own "Processing..." indicator and live reading stats while the summary streamed, so the conversation said the same thing twice in two places. The row reports the run, so the turns it replaces stay silent until it is done. * ui: hand the compact action to the context gauge Compacting is a decision about the context, so the action belongs with the numbers that show the context filling up rather than inside the form: it sits at the bottom of both the hover card and the drawer, under the breakdown it acts on, quiet enough to stay a footnote and disabled - with the row's own spinner - while a summary is already running. * ui: follow the bottom, otherwise leave the view alone The screen scrolled itself with a controller of observers, intervals, direction heuristics and landing timers, and still managed to move the view while the reader was reading: any mutation that grew the branch was treated as a reason to keep following. The page is the scroller, so it follows when the reader sits at the bottom, and stays where it is otherwise. A resize observer plus native scroll anchoring does the rest, and `pointerdown` buys the reader a moment of grace to be caught mid-drag. * ui: expand collapsible bodies with a height transition Expanding a block snapped it open: the body was mounted the moment `open` flipped, so a tall block moved the conversation around it in one frame. The body now goes through the same region the models table uses. The height interpolates and the visibility follows it, so a body is unmounted once the collapse has finished rather than the moment it starts - a closed block still costs nothing while tokens stream past it. * ui: open a compaction summary at the top of the screen Toggling a summary left the reader wherever they were, so a conversation that had just compacted showed the divider at the bottom of the screen and read down from there. A toggle now scrolls the page once and lands the divider a breath under the tabs, both ways: the summary is read from its beginning, and closing it returns to the same boundary instead of wherever the page happens to come to rest. The scroll waits for the body to reach its height, because before that the page is too short to scroll the divider up there and the call would clamp. That wait is the shared collapse duration rather than a transition event: an engine without `interpolate-size` animates the region's grid row instead of its height, so a listener on `height` would never fire there and the page would not move at all. * ui: spin the compaction icon while the summary runs The divider reported the phase in its title while the icon still showed a finished block, so a run in progress and a summary that had landed looked the same in the one place the eye reads first. The icon spins while the summary is running and folds back once it is in. * ui: let the compaction summary speak without the reasoning trimmings The summary is the thing the conversation now stands on, not a note beside it, and the conversation model always writes it, so the body drops the reasoning block's rule and indent (`flushBody` keeps them for the blocks that borrow them) and the model selector under the summary is gone - there is nothing left to swap. * ui: drop the model override from the compaction path The summary row no longer offers a model selector, so nothing hands regenerateCompaction a model anymore. Remove the parameter from the whole path, with the on-demand load of another model it carried: the summary always comes from the model the request funnel resolves. * ui: let the compaction summary think as the chat does The summary request forced thinking off, overriding the reasoning effort the user chose. The summary is the text the conversation resumes from, the task where reasoning helps the most, so the request now carries the very options of the chat. * ui: pre-encode the compacted branch as the next turn reads it The post-compaction pre-encode sent the branch without tools, while the tools render ahead of the messages, so it warmed a prefix the next turn never reads and that turn prefilled the whole compacted context again. The pre-encode now sends the tools, message shaping and reasoning handling of the chat, so the next turn only prefills its new message. * ui: pre-encode the branch the next turn actually reads The per-turn pre-encode sent the messages of the request plus one assistant message holding the streamed text, without tools. With tools on, it replaced the slot the next turn extends with a prefix nobody reads again, and after an agentic turn its history did not match the stored calls and results. Without agentic, it never ran. Both pre-encodes now read the stored branch up to its leaf, cut at the last compaction, and send it with the tools, message shaping and reasoning handling of the chat. The plain completion path triggers it too. * ui: refuse a compaction summary the output limit cut The stream handler read every chunk but dropped the finish reason, so a summary cut by max_tokens folded the conversation into half a text. The handlers now hand the finish reason to onComplete, and a summary that ends on length rolls the compaction back with an error. The summary was framed through a replacement string, so $& or 32560 in it changed the text the model read; it is framed as written now. * ui: wait for the collapse transition in the tool call tests The collapsible body now unmounts once its collapse animation ends rather than as soon as the block closes, so the "folds back" assertions ran before the body was gone. Wait out COLLAPSIBLE_TRANSITION_MS first. Assisted-by: Claude (Opus 5.5 Medium) --------- Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com> |
||
|
|
dc9e3bed42 |
ui: UX improvements (#31030)
* ui : land opened conversations at their bottom before the first paint Pin the page to its bottom with a ResizeObserver until it stops resizing or the reader interacts, and mount the last rows eagerly so the landing frame shows real content. Assisted by: Claude (Opus 5.5 Medium) * ui : make conversation tabs browser-style A plain navigation shows the chat in the current tab instead of adding one, and the new-chat tab turns into the conversation it starts. New tabs come only from explicit actions: the + button, Cmd/Ctrl-click or middle-click on a sidebar chat, and its Open in new tab menu item, which open in the background. Closing a tab moves to its neighbour among the tabs the view shows. Assisted by: Claude (Opus 5.5 Medium) * ui : share the tab surface and tab bar fade styles Move the tab item surface (shape, resting and active looks) to TAB_SURFACE_* class constants and the fade under the tab bar to a global chat-top-fade class, so other elements can match the tabs. Assisted by: Claude (Opus 5.5 Medium) * ui : fit the chat column below the tab bar The tab bar reported 1.25rem of its height while it takes 3.3125rem (pt-3.25 + h-10), so a short conversation still left a few pixels to scroll. Use the bar's full height and drop the extra 1rem from the column's minimum height, so it fills the viewport exactly; the centered new-chat form moves up by the same 1rem to keep its place. Assisted by: Claude (Opus 5.5 Medium) * ui : stop the page from bouncing past its ends Elastic overscroll on the root scroller dragged the sticky and fixed bars (tab bar, chat form) along at the top and bottom of a chat, and they jumped back on release. Set overscroll-behavior: none on html and body. Assisted by: Claude (Opus 5.5 Medium) * ui : stop scrolled dialogs from bouncing past their ends Dialogs scroll on their own (the settings panel, the search results), so the page rule did not reach them and their sticky navigation still jumped on overscroll. Turn off overscroll on dialog panels and on the scroll containers inside them. Only scroll containers: Firefox counts an overflow-hidden box such as truncated text as one, and `none` there kept the wheel from scrolling the list around it. Assisted by: Claude (Opus 5.5 Medium) * ui : fit a first exchange opened by the working directory row The last answer's minimum height leaves room for the last question only, so the synthetic row that a new chat starts with (Set working directory to ...) made a short first exchange overflow by that row. When everything above the last question is synthetic, count those rows as part of it. Assisted by: Claude (Opus 5.5 Medium) * ui : keep following a streaming response until the reader scrolls up Auto-scroll stopped whenever the page moved up, including moves the reader did not make: scroll anchoring and blocks re-rendering shorter while a response streams. Only the reader's wheel, touch, scroll keys or scrollbar drag now count as scrolling up. The send scroll also looked up the new user bubble by position, which picked an older message when the answer row had not rendered yet. Assisted by: Claude (Opus 5.5 Medium) * ui : fade the chat form between its centered and bottom positions The form no longer slides across the column between the new-chat center and the bottom. It fades out drifting toward where it goes and fades back in where it lands, quicker when opening a chat than when going back to a new one. Its wrapper keeps the same height in both positions, so the messages never shift while it moves. The new-chat greeting fades in as the form settles, also on a refresh or a first visit. Reduced motion keeps only the fades. Assisted by: Claude (Opus 5.5 Medium) |
||
|
|
e168e041ec |
ui: Models Manager as Route + UI improvements (#30988)
* ui : move models manager to its own route, keep Discover as a dialog Models now lives at #/models instead of a dialog. Discover opens as a standalone dialog over it, and leaving the page returns to the chat it was opened from. Assisted by: Claude (Opus 5.5 Medium) * ui : polish the models page layout and filters Give the models page a page header with the Discover action, calmer column labels, tinted section and family headers, and an empty state. Load the model list on a direct visit, slide the detail pane over the table below xl, and draw the filter toggles as one box matching the context select, with room for their shadow. Assisted by: Claude (Opus 5.5 Medium) * ui : round the models table family headers Assisted by: Claude (Opus 5.5 Medium) * ui : fix models page navigation and Discover gating Record the models page's way back after every navigation, so Back, a typed URL or a reload no longer return to a stale chat. Escape closes the detail pane, then leaves the page. Gate every Discover entry point on one shared check, recheck the API key on /models, skip the duplicate router fetch on a direct load, and share the route load between pages. Assisted by: Claude (Opus 5.5 Medium) * ui : show model details in a drawer Open the models manager's detail pane in a drawer, from the right on desktop and from the bottom on a phone, instead of an inline side panel and a full-screen cover. Pad the pane on both sides, drop its close button on a phone, and assert that filtered rows are gone from the page rather than merely hidden. Assisted by: Claude (Opus 5.5 Medium) * ui : make whole models table rows clickable A click anywhere on a model or quant row opens it, unless it lands on one of the row's own controls (load toggle, actions menu). The name button stays the keyboard path to the same action. Assisted by: Claude (Opus 5.5 Medium) |
||
|
|
84746c5c94 |
ui: Add "Show tool call in progress" setting (#30681)
* feat(ui): add Show tool call in progress display setting
Add the showToolCallInProgress key and its checkbox to Settings > Display,
right next to its "always show" counterpart. Upstream dropped a setting of
this exact name in
|
||
|
|
a6b83d426f |
ui : key draft sidecar badges by model instead of repo (#30919)
Two quants of one repo pulled each other's draft: the badge read every --model-draft arg of the repo, so each row showed both drafts. Assisted-by: DeepSeek-V4.1-Flash-1017 |
||
|
|
2b57148b08 |
ui: Fix markdown tables and lists being cut off on phones (#30687)
* ui : keep markdown tables inside the chat column on phones The phone rule made the table wrapper full-bleed with -1rem margins, so the first and the last column sat under the screen edge. Keep the wrapper inside the chat column and cap it at its width, so a wide table scrolls in place instead. Assisted-by: llama-ui (ggml-org/Qwen3.8-Flash-Next:IQ4_NL) * test : show a table-only model answer in the chat message story Adds a story with an answer that is mostly a table, so the table layout can be checked at a phone width. Assisted-by: llama-ui (ggml-org/Qwen3.8-Flash-Next:IQ4_NL) * ui : indent markdown lists by 2rem 1.5rem put the marker too close to the item text. Assisted-by: llama-ui (ggml-org/Qwen3.8-Flash-Next:IQ4_NL) |
||
|
|
c0fd786264 |
ui : Discover & Download Models from HF Hub (#29584)
* common : resolve <quant>-<sidecar> download tags and list cached sidecars A Q4_0-mtp style tag now resolves the sidecar file when no model file matches it, so a solo draft or mmproj download actually pulls the file. Cached sidecar files list as their own entries so the state survives a restart, and removing such a tag deletes only the sidecar. Assisted-by: pi:zai-org/GLM-5.3 * ui : optional sanitized raw HTML in markdown Add an allowHtml prop to MarkdownContent: raw HTML found in the markdown is rendered after DOMPurify sanitization instead of being escaped as literal text. Default stays escaped. Assisted-by: pi:GLM-5.3-Flash * ui : add the discover models components The Discover surface: the curated-catalog list with its search and skeletons, the details pane with its readme, metadata, Hub stats and download options, and the standalone download progress bar that replaces the plain one. The quant download button sizes its select with a new xs trigger, so the select gains that size and the muted box look. The store stays incomplete when a repo fails, so the next mount retries it. Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash * ui : put the discover view in the models manager The manager dialog gains its second view: the Discover Models call to action fades the table out, slides the title with its compass mark, and fades the discover surface in; the arrow button returns to the table. Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash # Conflicts: # tools/ui/src/lib/components/ui/button/button.svelte * ui : add the Enable Discover Models setting The flag gates browsing and downloading HuggingFace GGUF models, so it is declared on the discover layer; the repo-tree lookup in the manager layer above reads it from here. Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash * ui : resume model downloads from the pause point A stopped download keeps its partial file instead of deleting it, so re-posting the tag resumes from disk. The first progress record now carries the resume offset, the paused snapshot survives into the resumed entry, and a pause settles as soon as it is requested. Assisted-by: pi * ui : lead the discover list with downloads The discover sidebar lists running downloads above the suggested models, and steps aside while a query is active. A paused download stays in the manager table, where it is resumed or dropped. Assisted-by: pi * ui : make resuming a paused download work Re-posting the tag of a paused download hit the router's "already exists" check, because a paused download parks its entry as DOWNLOADED until the next reload: a download entry that is not running is now replaced and resumed. The UI keeps the paused row when the request still fails, instead of dropping it and looking finished. Assisted-by: pi * ui : remember unfinished model downloads A partial file is named after its content hash, so nothing on disk says which model it belongs to once the router restarts. The router now keeps the tags of the downloads it started, clears a tag when the download finishes or is deleted, and lists what is left as still downloading without starting anything. The UI adopts those tags as paused on every list fetch, so a refresh or a fresh open shows an incomplete download and can resume it from the files already on disk. Assisted-by: pi * ui : show a paused download's state on its chip A paused quant chip reads as paused at rest, the way a running one shows its spinner, and offers the resume action on hover. The selector's download row gets a View in Discover action beside its cancel. Assisted-by: pi * ui : collapse the download options by default The copyable download command starts collapsed so the option rows lead; expanding it stays one click away. Assisted-by: pi * ui : keep decision models out of the hub lists Decision models answer with calibrated probabilities instead of text, so llama-ui cannot run them: the hub data layer drops them from the catalog and every search, keyed by the decision-model tag or a text-classification pipeline. Assisted-by: pi * ui : read download tags as quant and sidecar A `<repo>:<quant>-<sidecar>` tag now parses into its quant and sidecar instead of one opaque quant token, and ModelsService.draftSidecarsByRepo collects the router listing's sidecar-only entries per repo, replacing the inline copy in the models store. Assisted-by: pi * ui : cluster draft downloads under their main model Tracked downloads group by repo: the main weights lead and the draft sidecars ride inside their item as compact sub-lines, on the selector, the discover list and the manager table. The main model's controls pause, resume and cancel the whole cluster, and the manager's download rows select into the configuration pane. Assisted-by: pi * ui : act on tracked downloads from the table rows The table passes its clusters' draft sub-lines to the model rows. A row's actions cascade pause and resume, an in-flight row offers Cancel download while a paused one keeps Delete from disk, and the confirm dialog's copy follows the state. Assisted-by: pi * ui : give downloads a configuration pane of their own Selecting a tracked download opens the pane in a download mode: live progress with pause, resume and cancel, and See in Discover, instead of metadata and load actions that a model without files cannot offer. Assisted-by: pi * ui : make the discover details self-fetching and responsive ModelsDiscoverDetailsContainer owns the details, tree and readme fetches so hosts outside the discover view can render the pane, and the details layout adapts to narrow panes with container queries instead of viewport breakpoints. Assisted-by: pi * ui : download a matching draft with the main model A main quant download now pulls the quant-matched draft of the most preferred kind the repo ships (DFlash before MTP), fire-and-forget, so speculative decoding is ready when the main lands. Manual draft chips stay available; the options row also collapses to one column in narrow panes. Assisted-by: pi * ui : keep discover off by default and couple it with the hub setting Discover Models starts unchecked. Enabling it turns the Hugging Face Hub setting on and disabling it turns it back off, so the checkbox never lies; with discover off the hub setting stays available on its own for the display data. Stored and reset state are corrected the same way on load. Assisted-by: pi * ui : open the manage models dialog on the manager view The dialog now opens on the manager instead of returning to the last view, discover entries still land on the discover view while the setting allows it, and the discover surfaces follow the setting's state. Assisted-by: pi * ui : give the discover search room for its focus ring The sticky search bar insets from the panel edges so the focus ring stays inside the scroll container, and rows scroll behind an opaque bar instead of through it. Assisted-by: pi * common: settle sidecar names on the listing and drop unfinished partials An infix or trailing sidecar token is ambiguous: draft heads use it (Model-MTP-BF16.gguf), so do full models with embedded heads (Model-Q8D-MTP.gguf). Such a name is a sidecar only next to token-free main weights of the same family, while mmproj or imatrix anywhere in the name, or a leading token, decides alone (mmproj_f16.gguf). A quant tag always carries its bit count, so a sidecar named after no quant (mtp-Model-it.gguf) gets the bare tag: the listing, the plan and remove all use it. An imatrix tag resolves into a plan slot of its own, downloaded but never wired into the params. remove resolves the tag the way a download does and deletes the partial files of every planned blob, so cancelling a running download or dropping one left unfinished by an earlier run leaves nothing on disk. The router passes its token and releases its lock around the call, and records the unfinished downloads with an atomic write. llama download succeeds on a sidecar-only plan and prints its paths. * ui : move dialog views and draft states into enums and constants The models dialog's views become the ModelsDialogView enum, the draft subline's states join ModelRowDownloadState, the view fade and the copy-feedback reset move to ui constants, and the dialog's compass marks come from DISCOVER_ICON. Assisted-by: pi * ui : drop the unused discover surface The dead openDiscoverModels twin, the commented-out command divider, the details pane's unused onClose and its header's close button, and a redundant type cast leave; the command's copy feedback joins the shared reset constant. Assisted-by: pi * ui : trim the download option comments and highlight the chat template The comment verbosity the options panel and its chips carried shrinks to the non-obvious why, and the chat template dialog renders through SyntaxHighlightedCode as jinja instead of a plain pre block. Assisted-by: pi * common: only a .gguf file is a sidecar, and mmproj/imatrix as whole words A non-GGUF file named like a sidecar (quantization-mmproj-Q8_0.json) is picked as the mmproj, and a sidecar word inside a longer word of a model name (Phi-2-OmniMatrix.i1-Q4_K_M.gguf) drops the model. * ui: dedupe draft sidecar badges per quant The draft sidecar badges deduped by kind and repo without the quant, so with a draft cached at two quants only one badge survived, and a delete of the other main quant found no badge to remove its sidecar with. Also restores the getDownloadEntries doc lost when the clusters method landed above it. Assisted-by: pi (zai-org/GLM-5.3-Flash) * ui: adopt the incomplete download status and drop the paused-ids registry The store inferred an unfinished download from a downloading entry it held no local state for, which misread a download running in another tab as paused and offered a resume the server rejects. The listing carries the "incomplete" status now, so the adoption keys off it directly. The paused ids no longer need to survive in localStorage: the server remembers them across restarts, and the progress snapshot was stale after a reload anyway. The stale localStorage key is removed. Assisted-by: pi (zai-org/GLM-5.3-Flash) * ui: quant-match the pause and cancel cascades Pause, resume and cancel cascaded to every draft sidecar of the repo while delete only took the quant-matched ones, so canceling a Q4_0 download also killed a Q8_0 draft that belongs to another quant's main. Every cascade acts on the quant-matched drafts now, matching what a delete takes with it. Assisted-by: pi (zai-org/GLM-5.3-Flash) * ui: collapse the duplicated draft subline markup The downloaded and in-flight branches repeated the identical connector, label and badge markup; one block with a conditional tail keeps the two states in sync. Assisted-by: pi (zai-org/GLM-5.3-Flash) * ui: scrub positioned styles out of sanitized model card html The style attribute stays allowed for the KaTeX output, which let a third party card draw a fixed overlay over the app. The sanitize hook drops position:fixed and position:absolute values and leaves the KaTeX layout styles alone. Assisted-by: pi (zai-org/GLM-5.3-Flash) * ui: run the discover mount fetch once and reuse the shared repoOf helper fetch() toggles its loading flag synchronously inside the mount effect, which re-ran the fetch on every toggle and retried failed catalog repos endlessly; the effect now untracks the call. The local repoOf copy and the duplicate services import give way to the shared helpers. Assisted-by: pi (zai-org/GLM-5.3-Flash) * ui: narrow the spec-type map to the draft sidecars SPEC_TYPE carried empty entries for the aux sidecars it is never indexed with; the map and the command component now type the badge as a draft sidecar through a draftBadgeOf helper instead of a cast. Assisted-by: pi (zai-org/GLM-5.3-Flash) * ui: name the decision-model tags and markdown cache keys The decision-model hub tag and pipeline tag move into the huggingface constants next to the other pipeline tags, and the markdown transform cache prefixes get names instead of bare one-letter tokens. Assisted-by: pi (zai-org/GLM-5.3-Flash) * ui: read the discover setting through one truthiness check Three places read the setting with !== false and one with Boolean(); all four read it the same way now. Assisted-by: pi (zai-org/GLM-5.3-Flash) * ui: compare the models dialog view against its enum The template compared the view state against the 'manage' literal in three spots while the script used the enum. Assisted-by: pi (zai-org/GLM-5.3-Flash) * nit: Remove `+` before draft sidecar badge * server: list unfinished downloads as incomplete A download that never finished was reported as "downloading", the same status a live download carries, so a client could not tell a running download from a dead one. The tags remembered in unfinished-downloads now list under a dedicated "incomplete" status, without an instance. Assisted-by: pi (zai-org/GLM-5.3-Flash) * server: download a model with its sidecars as -hf does Resolution goes back to master. POST /models takes the sidecar kinds to pull with the model (mmproj by default) and the download child gets them as -hf flags, so the server picks the files. /models lists real models only, each with the sidecars it has on disk, and incomplete downloads keep their sidecars across restarts. A stopped download keeps its partial files for the next attempt, and DELETE purges them through the same plan the download follows. * ui: download sidecars as parameters of their model The download options keep their chips, but a chip no longer posts a sidecar tag: the store maps it to its model and the sidecar kinds to pull, the quant chip takes its companion draft in the same download, and draft badges come from the sidecars each cached model lists. The server adds the sidecars a cached model lacks when it is posted again, and DELETE removes a single sidecar of a cached model. * ui : mirror the server's sidecar lookup in draft badges The server resolves a sidecar by the case-sensitive `<kind>-` token in the same directory as the weights. The UI matched the token loosely, so a badge could offer a draft the server never pulls. Assisted-by: DeepSeek-V4.1-Flash * ui : post the model's sidecar kinds with its download The store keeps one download per model: the chip's kinds ride along with the model's own sidecars, and a download row no longer expands into one row per sidecar. Draft badges carry no quant of their own yet, so a sidecar of a cached model is never labelled with the model's quant. Assisted-by: DeepSeek-V4.1-Flash * ui : join a quant's draft into its download chip One chip per quant, its draft badged inside it and its size covering both files. The draft is the file the server would pick: the same directory, the exact quant, else the closest bit depth, so a 4-bit model can pair with a 16-bit head. Assisted-by: DeepSeek-V4.1-Flash * ui : drop the draft download rows from the manager and selector A download is one row per model again: the drafts ride with it, and the rows read the sidecars on disk off the listing instead of pairing sidecar entries with their model. Pause, resume and cancel act on the model's own download. Assisted-by: DeepSeek-V4.1-Flash * ui : keep the discover download list one row per model The list leads with one row per downloading model, its sidecars as sub-lines. A row badges only the draft kinds the server can resolve, so a badge never outlives the file its lookup would pick. Assisted-by: DeepSeek-V4.1-Flash * ui : keep the discover chips readable in dark mode The metadata chips, the chat template button and the View on Hugging Face link carried a faint border and no surface over the dialog background, so they were near invisible. They take the tertiary button surface now, and their icons size with size-* so the variant's base rule leaves them alone. Assisted-by: DeepSeek-V4.1-Flash * ui : carry the draft through the download options One chip per main quant, its draft badged on it and the chip's downloaded state covering both files. The drafts sit behind an opt-in checkbox with an inline type pick, and each row opens its own command: it pins the row's quant and only names the draft type, so -hfd is gone. Assisted-by: DeepSeek-V4.1-Flash * ui : line the details skeleton up with the loaded pane Same root padding and container context as the loaded view, so the container queries resolve alike. The download options card pads its blocks instead of itself, the readme gets its card, and a description line stands in for the catalog paragraph. Assisted-by: DeepSeek-V4.1-Flash * common : drop the partials of a removed sidecar Removing a sidecar alone left the .downloadInProgress files of the sidecars a download had planned in blobs/. The removal purges them through the same plan the download follows, and the test covers a finished draft beside an unfinished one, whose partial goes while the model keeps its files. Assisted-by: DeepSeek-V4.1-Flash * server : name why a download is refused A model that is already downloading, or that is running, answered "already exists". It now says which: "is already downloading", or "is running, unload it first", both before the resumable check. The router test covers the two. Assisted-by: DeepSeek-V4.1-Flash * ui : post a download once per click Two quick clicks on the same chip posted twice, and the second answer was "already exists". The store tracks the model as pending, returns early while it is pending or in flight, and reads it as in flight before the server answers, so the chip shows the download at once. The new client test pins the flows: one POST per double click, the chip reading downloaded once the list reports the model, a resume keeping its sidecars, a cancel dropping the whole download, and a reload restoring the paused state. Assisted-by: DeepSeek-V4.1-Flash * ui : keep a plain download row the height of the others The sub-lines wrapper of a download row was rendered even with nothing in it, so its padding made the row taller than a plain one and the overlaid progress bar sat in that empty strip. It renders only with sub-lines now, in the selector and in the discover list, and the selector row takes the phone padding its neighbours have. Assisted-by: DeepSeek-V4.1-Flash * ui : badge a repo's draft once The args of a loaded model and the listing of a cached one report the same draft in different words, one with the file's quant and one with the kind alone, so the merge kept both and a repo row showed the same kind twice. The badges merge per kind now, the args one winning as it knows the quant, and the test pins both sources of a real repo. Assisted-by: DeepSeek-V4.1-Flash * server : report the sidecars of a preset or a download on its way A model the preset also names is registered as a preset, so its drafts were never reported and no row could badge them. A download on its way reported none either, though the record of the kinds it pulls is right there: those win over the plan now, the plan resolves through the preset's hf-repo, and a models dir stays out of it. Assisted-by: DeepSeek-V4.1-Flash * ui : badge a download's drafts and open its model from the row A draft still coming down badges the model id now, the way a landed one does, while an on-disk draft keeps its checked sub-line. The row itself opens the model, the status slot beside it pauses or resumes, and the manager resolves a focus on a download row so its panel opens. Assisted-by: DeepSeek-V4.1-Flash * common : report a removed sidecar that only had a partial Removing a sidecar alone dropped the result of the partial purge, so a sidecar that only ever landed as a .downloadInProgress blob answered false and the UI had no reason to refetch. The purge result is folded into the return now, and the test covers a partial-only removal. Assisted-by: DeepSeek-V4.1-Flash * ui : ignore a pause while the download POST is still on its way The chip reads as downloading before the server answers, so a quick second click unloaded a model that had not started and left the store neither in flight nor paused. A pause returns early while the POST is pending, before it touches the feed or the paused marker. The client test holds the POST open and pins it. Assisted-by: DeepSeek-V4.1-Flash * ui : offer a missing draft as its own chip A model already on disk without its draft had no way to add it: the quant chip carried the draft, so it read as a download of both. The quant chip keeps to its own state now, its joined draft shows only while it rides along or is on disk, and a downloaded quant missing its draft gets a chip of its own - badge, size, download - that posts the model with that kind, so the server keeps the files already there and pulls the draft. Assisted-by: DeepSeek-V4.1-Flash * server : list a paused sidecar download by the files on disk A model on disk whose sidecar download stops keeps its unfinished record, and the listing reported the kinds that record plans as if they were on disk: the UI checked a draft that never landed and offered no way to add it. The planned kinds now stand in only while the download runs. * chore: Clean up comments * server : list sidecars only for a model that resolves from a HF repo A preset on a local file has no HF repo, yet the listing resolved its name as one: the plan threw on the invalid repo format and GET /models failed for every model. The sidecars now come from the repo a preset names or a cached model is listed under, and a local file lists without them. * common : fix download cancel regression Signed-off-by: Adrien Gallouët <angt@huggingface.co> * common : preserve model partials when removing sidecars Signed-off-by: Adrien Gallouët <angt@huggingface.co> * server : one sidecar table and an unfinished downloads class Address review from @ggerganov. The sidecar kinds live in one table in common/download, mapping each name to its download option and plan file; the server validates, builds its options and lists cached sidecars through it instead of its own lists. The unfinished downloads move into server_unfinished_downloads, a record with its own mutex and a struct per entry, and server_models exposes them through unfinished_downloads_get and unfinished_downloads_remove. * common: one sidecar table, and removals that keep shared files The resolution, the cached listing and the removal follow a single table of sidecar kinds, so a kind is declared once with its file keyword, whether it is a draft, its option and its plan file. The server reads the same table for the kinds it accepts, the default sidecars and the child flags. Removing a tag keeps the files another cached tag still loads, such as an mmproj named after the removed quant. A sidecar removed on its own goes for every tag that resolves it. An unfinished download records the exact paths of its partials when it starts. A removal deletes those without asking the hub, and keeps a partial another recorded download also writes. The record is taken once the child is spawned, and the files go under the models lock, so a download of the same tag cannot start while they are removed. * server: drop stale partials and refuse a tag that resolves nothing Recording a download again deletes the partials of the earlier record that the new plan no longer writes, such as those of an older hub revision or of a sidecar the new request leaves out. A posted tag that resolves no file of the repo is refused before the download starts. Removing a model follows a single path, whether the reload erased its entry during the wait or not. * server: fix the CI race between the router download tests The pytest workers run in parallel on one shared cache, so a download test could find the file another test had just fetched and see no progress, or start from the unfinished downloads record another test wrote. Each download test now runs on an empty cache of its own. * server: refuse a DELETE of an unknown name and decide downloads in load() A DELETE of a name that is neither a model nor a recorded download went to common_download_remove, and without a tag it wiped the whole repo cache, including the files of a loaded quant or of a download on its way. Such a name is now not found, as it was before. A POST that downloads over an existing entry checked its state outside the lock, so a load of the same model in between got its instance killed. The checks now run in load(), under the lock, right before the spawn. The fake load time of the tests applies to model loads only, since a download worker loads none. * server: add incomplete_sidecars to the /models API A sidecar added to a cached model and then paused kept its record, but the listing showed only the complete model, so a fresh UI lost the pause. The model entry now carries incomplete_sidecars, the kinds its unfinished download has not written yet; a POST with the kind resumes one and a DELETE with it drops its partial. * common : drop a kept partial when a resume gets 416 Now that partials survive between attempts, an old or complete partial makes every retry fail with the same 416. Delete it so the next attempt can start over from zero. Signed-off-by: Adrien Gallouët <angt@huggingface.co> * common: drop a kept partial when a resume gets 200 A server that ignores the range answers 200 to a resume, and the kept partial then fails every attempt the same way a 416 does. Drop it as well, so the next attempt starts over without a range. * server: refuse a download that shares a partial with one on its way Two downloads of quants that share a file, the mmproj or an mtp picked by the closest bits, appended to the same partial and renamed a file with interleaved bytes. A download whose plan writes a partial that a running download also writes is now refused until that one is done. * common : check the saved etag before resuming a partial download Before resuming, compare the etag saved with the partial to the one the server now serves. If it is missing or different, start over, so a resumed download never mixes bytes from two versions of the file. Signed-off-by: Adrien Gallouët <angt@huggingface.co> --------- Signed-off-by: Adrien Gallouët <angt@huggingface.co> Co-authored-by: Pascal <admin@serveurperso.com> Co-authored-by: Adrien Gallouët <angt@huggingface.co> |
||
|
|
fd55cb98fe |
fix(ui): take the agentic turns default from DEFAULT_AGENTIC_CONFIG (#30582)
The registry hard-coded 10 for AGENTIC_MAX_TURNS while DEFAULT_AGENTIC_CONFIG said 100. The registry seeds SETTING_CONFIG_DEFAULT, so the loop's `|| DEFAULT_AGENTIC_CONFIG.maxTurns` fallback never fires and users got 10 turns. Reference the shared constant, as the adjacent MCP timeout does. Assisted by: llama-ui (ggml-org/Qwen3.8-Flash-Next:IQ4_NL) |
||
|
|
baef3ed9a1 |
ui: Models Manager Follow-up Improvements (#30228)
* common : read a GGUF's trained context from its metadata common_get_gguf_n_ctx_train opens only the file's metadata (no_alloc, like common_get_decision_type) and reads <arch>.context_length, so a caller can learn the trained context without loading the model. It accepts both u32 and u64 values and returns 0 when the file is missing, unreadable, invalid, or reports no context length. Assisted-by: pi:zai-org/GLM-5.3-Flash * server : report the trained context in the models listing update_caps already resolves the model file offline to read its modalities, so it now reads the trained context from the same GGUF metadata, and GET /models reports it as context_length when it is known. A router listing then carries the context without any Hub request, which lets the UI sort and filter by it offline. Assisted-by: pi:zai-org/GLM-5.3-Flash * ui : take the trained context from the models listing The router now reports context_length per model, so the option mapping fills contextLength from it and the manager reads it before the Hub record. The Context column, the context sort and the context filter then work with the Hugging Face Hub API turned off. A browser suite guards the sort and the search, the Hub-cache driven context filter and the re-sort when details arrive after the sort was clicked. Assisted-by: pi:zai-org/GLM-5.3-Flash * ui : mark favorite models with a heart A favorited model shows a rose heart in the selector even before its row is hovered, and the crossed heart takes its place on hover, so unfavoriting stays one hover away. The manager table marks its favorited rows with the same heart after the badges and capabilities. Assisted-by: pi:zai-org/GLM-5.3-Flash * fix: UI text nit * fix: UI nits * fix: Favorite models grouping in models table * feat: Remove sorting from Status column in Models Table * server: read the GGUF metadata once per model Read the decision type and the trained context in a single GGUF open, accept only a UINT32 context length like the model loader, and reset n_ctx_train with the other caps so a failed refresh drops it. * fix: Post-review fixes --------- Co-authored-by: Pascal <admin@serveurperso.com> |
||
|
|
b013e56a71 |
ui : add the models manager (#29583)
* ui : add the drawer, sheet and grouped list primitives Add the drawer and sheet overlay components, the toggle and toggle group, the shared searchable input, the collapsible sections and the grouped list, and the near viewport helper they position with. Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash * ui : add the shared model components and data layer Add the model row pieces shared by the selector and the manager, the models store with its download status feed, the huggingface and migration services, and the model utils, enums and constants they read through. Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash * ui : rework the models selector around its providers Group the selector components under their own folder, rework the list around the provider grouping, add the mobile trigger and the download item, and derive the selection state in one hook. Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash * ui : add the models manager Add the manager table with its repo, quant and status rows, the filters and the toolbar, the row actions in a drawer, the downloads section, the manage models dialog and the stories and tests. Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash * ui : rework the chat form actions and the mobile experience Move the add actions into a drawer and a sheet, float the model actions in a bar on a phone, open the context panel in a drawer, and derive the attachment, reasoning and tools menus in hooks. Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash * ui : move the mcp servers to the sidebar rail and tidy the dialogs Move the mcp servers dialog to the sidebar rail with its menu entries, even out the dialogs on a phone, and group the mcp and settings stores behind their own modules. Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash * ui : polish the shell The secondary button gets its own look back and the chat add button its own light surface. Pressable elements get a pointer cursor again, the font rendering smooths on the app shell, and the agent skills stay out of prettier's way. The root layout props probe rework that came with this polish reads the providers api url and stays with the providers change instead. Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash * ui : wrap the model id classes Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash * ui : load the model the new chat CTA picks Start a new chat selected the model but left it unloaded, so the chat opened against a server with nothing in memory. Both CTAs share a load helper now. Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash * refactor: Post-review fixes |
||
|
|
4f92965a7b | ui: apply ui_settings on first visit in router mode (#29668) | ||
|
|
8664eaea30 |
ui : model download pipeline (#27959)
* ui : model download pipeline Track HuggingFace downloads end to end: the server download/cancel endpoints, a status manager fed by the /models/sse download progress events, and a models-discover store holding the catalog and detail state for the discover view. Downloaded and in-flight entries are excluded from the loadable model list. Assisted-by: pi:GLM-5.3-Flash * ui : route sidecar tag lookup through the sidecars util, validate the paused list Assisted-by: pi:zai-org/GLM-5.3-Flash |
||
|
|
4cfb6d1c75 |
ui : model memory-fit estimation (#27957)
* ui : model memory-fit estimation Replace the raw runtime-memory estimate with the app's compatibility check: the smallest real Mac memory tier that fits a model file, budgeted as RAM x 0.75 minus fixed overhead with headroom on the file size. The constants move to lib; the unused runtime-memory estimate is dropped. browser-info's OS detection is exported for reuse. Assisted-by: pi:GLM-5.3-Flash * ui : cover the memory-fit and tool-use heuristics in tests Assisted-by: pi:zai-org/GLM-5.3-Flash |
||
|
|
9b43336114 |
ui : Hugging Face Hub data layer (#27947)
* ui : Hugging Face Hub data layer Add HuggingFaceService and its constants/enums/types: GGUF repo search, file tree and model detail fetching, quant/sidecar filename analysis, shard-set collapsing and the llama.app catalog feed, plus an orgOf() helper on the model name utils. Assisted-by: pi:GLM-5.3-Flash * ui : strip provider tilde prefix from hub avatar urls Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash * ui : trim redundant comments in the HF data layer service Per review: drop JSDoc that restates the method name and inline comments that restate the code; keep only comments carrying non-obvious context. Assisted-by: pi:zai-org/GLM-5.3-Flash * ui : harden the HF data layer error typing, cover the helpers in tests Carries the HTTP status on retryable fetch errors instead of matching the message text. Marks expand-dependent catalog fields optional and documents the data/models index pairing. Adds table tests for the pure helpers. Assisted-by: pi:zai-org/GLM-5.3-Flash |
||
|
|
f653250407 |
ui : model id grammar for sidecars, quants and capability parsing (#27946)
* ui : model id grammar for sidecars, quants and capability parsing Extend the shared model id parser with sidecar tokens (draft variants and auxiliary imatrix/mmproj files), weight-file and custom-quant regexes, and add the tools capability to ModelCapabilities; the selector option row picks it up from the model's declared capabilities. Assisted-by: pi:GLM-5.3-Flash * ui : escape sidecar tokens in the regex alternation Assisted-by: pi:zai-org/GLM-5.3-Flash |
||
|
|
b23efaa2ef |
ui: Fix mobile breakpoint + content overflow issues (#29108)
* ui : let the chat column shrink below its content width The chat column is a flex item, so its automatic minimum size kept it as wide as the widest row inside it. Message rows cap at max-w-3xl plus padding, so a narrower window pushed a page-level horizontal scrollbar. Set min-w-0 on the column so the inner scroll containers take over. Assisted-by: pi:deepseek-ai/DeepSeek-V4.1-Flash * ui : wrap markdown tables in a scroll container Markdown tables render as a bare <table>, which keeps its content-driven minimum width and can stretch the chat column past the window. The table-wrapper CSS already existed, but nothing produced the wrapper. Add a rehype plugin that wraps each table in div.table-wrapper, following the existing enhance-* plugins. Assisted-by: pi:deepseek-ai/DeepSeek-V4.1-Flash * ui : scroll long inline content inside markdown blocks Long unbreakable content (inline code, paths, hashes) widened the message row and spilled over the neighbour elements. Give each markdown block a horizontal scroll container, and the content root one as well, since the trailing block renders with display: contents and has no box of its own. Assisted-by: pi:deepseek-ai/DeepSeek-V4.1-Flash * ui : use exact transition properties for markdown images transition: all repainted every property and 300ms felt sluggish. Name transform and box-shadow at 200ms ease-out, and gate the hover scale behind (hover: hover) and (pointer: fine) so touch taps do not trigger it. Assisted-by: pi:deepseek-ai/DeepSeek-V4.1-Flash * ui : fit wide image attachments to the message width Attachment thumbnails used a fixed height with w-auto, so a wide image kept its aspect-driven width and, being flex-shrink-0 in a right-aligned bubble, overflowed to the left of the message row. Cap the thumbnail with max-height and max-width instead of a fixed height so it scales down proportionally, and let it shrink outside the single-row carousel. Assisted-by: pi:deepseek-ai/DeepSeek-V4.1-Flash * ui : keep long tool call titles inside the message row A tool title could not shrink below its content, so a long path escaped the message row. Let the title span shrink and scroll, and for the file tools put the value on its own line only when it does not fit, with the value as the only scroll container. Assisted-by: pi:deepseek-ai/DeepSeek-V4.1-Flash * ui : render get info as a collapsible block with a table get_info rendered its own always-open row with the values trailing the label. Use the shared ToolCallBlock chrome so it collapses like the other tools, and list os and cwd as table rows with the key as a row header. The error and pending states now show inside the body, including the plain-string errors the server tools path produces. Assisted-by: pi:deepseek-ai/DeepSeek-V4.1-Flash * test : pin the server mode in the add menu a11y story The story asserts the add menu's first enabled item is the reasoning submenu, which is mounted only outside router mode. The vitest dev server proxies /props to whichever server is running, so the assertion depended on the machine's server mode and failed whenever a router was up. Pin the mode in the story, including props.role so a re-detection cannot flip it back. Assisted-by: pi:deepseek-ai/DeepSeek-V4.1-Flash * ui: wrap long markdown tokens instead of scrolling every block Making each markdown block and the content root a horizontal scroll container turns any hover transform into a scrollbar: the blockquote translate and the image zoom overflow their block and flash a scrollbar under it. Each block also becomes a block formatting context, so the paragraph margins stop collapsing across blocks and the spacing doubles. Drop both overflow-x rules and let long unbreakable tokens wrap with overflow-wrap: break-word on the content root. break-word leaves the min-content width untouched, so wide tables and code blocks keep scrolling inside their own containers. --------- Co-authored-by: Pascal <admin@serveurperso.com> |
||
|
|
4ff829ec2e |
ui: fix removed reasoning menu in single model mode on desktop (#27985)
* ui: fix accidentally removed reasoning menu in single model mode on desktop * ui: formatting task run to fix storybook test * ui: mount the add menu reasoning submenu outside router mode only The models selector already owns the reasoning submenu in router mode, so the add menu only mounts it in single model mode. The first enabled item of the add menu is now the reasoning submenu, the accessibility story expects it. --------- Co-authored-by: Ben Babik <work@benjaminbabik.com> Co-authored-by: Pascal <admin@serveurperso.com> |
||
|
|
0afb805b19 |
ui: Improve Chat Messages rendering performance (#28460)
* ui : update active conversation fields in place updateCurrentNode, applyConversationUpdate, updateConversationTimestamp and the pin toggle replaced the whole activeConversation object, so its identity changed on every send, tool result and rename. ChatMessages tracks that identity to refresh sibling info, so each replacement triggered a full refetch of every message in the conversation. Write the changed fields instead, mirroring updateMessageAtIndex. Assisted-by: pi:zai-org/GLM-5.3 * ui : reuse the conversation load read for sibling info Opening a conversation read every message from the database twice: once in loadConversation for the active path, once in ChatMessages for the sibling map. Hand the freshly read array over once so the chat screen builds sibling info from it, and set the conversation and its messages in one sync block so effects never see the new conversation paired with the previous one's messages. Assisted-by: pi:zai-org/GLM-5.3 * ui : memoize leaf walks in sibling map build buildSiblingInfoMap resolves each sibling's leaf by walking the last-child chain, once per sibling per message, so the walk repeats along the same chains for every message in the conversation ( O(messages^2) on long chats ). Memoize leaf resolution per build with path compression so each edge is walked once. Assisted-by: pi:zai-org/GLM-5.3 * ui : skip sibling refetch for in-place message edits refreshAllMessages refetches every message of the conversation just to rebuild sibling info, but preserve-responses and non-branching assistant edits never create branches, so the sibling map stays valid. Refresh only after actions that branch (editWithBranching kept) or delete. Assisted-by: pi:zai-org/GLM-5.3 * ui : drop unused currentResponse reactive writes Nothing reads chatStore.currentResponse, but setChatStreaming reassigned it on every streamed chunk, so each token paid a reactive write and string assignment for nothing. Remove the field and the clearUIState wrapper that only reset it. Assisted-by: pi:zai-org/GLM-5.3 * ui : reuse completed agentic turn sections during streaming deriveAgenticSections runs in a $derived invalidated per streamed chunk, but re-derived every turn of the session each time, so per-chunk cost grew with session length. Cache completed turns keyed by their assistant message plus reference checks on every field that feeds derivation; only the streaming turn recomputes. Cache hits return the same section objects, so tool block props stay stable and skip their per-chunk re-derive. Assisted-by: pi:zai-org/GLM-5.3 * ui : share markdown block infrastructure Every markdown block duplicated shared work: a full copy of the hljs theme CSS per instance, and the remark/rehype plugin chain rebuilt on every processMarkdown call ( once per block at mount, again per coalesced chunk while streaming ). Use the single theme style element already maintained by SyntaxHighlightedCode, and build pipelines once - shared process-wide for attachment-less blocks, cached by attachments identity otherwise. Assisted-by: pi:zai-org/GLM-5.3 * ui : measure assistant layout only for the last message Every assistant message ran getComputedStyle, getBoundingClientRect and a ResizeObserver over the previous user bubble at mount, even off-screen ones, forcing a layout pass per message while a long conversation renders. The measured vars only feed the :last-child min-height rule, so gate the effect on isLastAssistantMessage; one measurement and one observer remain, and the effect re-runs when the last message changes. Assisted-by: pi:zai-org/GLM-5.3 * ui : trim whole-blob scans in tool block headers Tool block headers parsed their entire blobs at mount, even collapsed, and most tool results and args are large plain text or embedded file content: skip JSON.parse unless the blob starts with a JSON container, prefilter search-result extraction with a Title:/URL: substring check, and match the end-anchored exit-code marker against only the tail of exec outputs. Assisted-by: pi:zai-org/GLM-5.3 * ui : parse write_file and edit_file titles without the content blob Both block headers parsed the full args JSON at mount, even collapsed, and write_file and edit_file args embed the whole file content or edit strings, so every block paid a full-blob JSON parse just to read the path. Split the meta into a title tier that extracts the path with a targeted key match (full parse only as fallback) and a body tier that keeps the full parse; Svelte deriveds are lazy, and the body snippet renders only while the block is expanded, so collapsed blocks no longer parse args. Assisted-by: pi:zai-org/GLM-5.3 * ui : mount chat messages lazily near the viewport Every message row mounted its full component tree on load, so the cycle collector, GC and layout invalidation kept walking every live object and DOM node even for rows the user never scrolls to - which dominated the profile of long conversations. Wrap each row in a placeholder with an IntersectionObserver ( two viewport heights of runway ) that swaps in the real ChatMessage when the row approaches the viewport; the row shell keeps the content-visibility sizing, and rows stay mounted once realized. Rows targeted by the pending-edit flow mount eagerly. Assisted-by: pi:zai-org/GLM-5.3 * ui : smooth the chat navigation animations Slide the centered new-chat form to the bottom edge with a transform instead of a bottom offset - layout-property transitions need the main thread every frame and stutter while a long conversation loads, while transform transitions run on the compositor. Fade the message list in with a CSS animation keyed to the conversation id, disabled under prefers-reduced-motion. Assisted-by: pi:zai-org/GLM-5.3 * ui : follow the svelte runes guidance in chat message code Two effects detected changes with manual previous-value refs and reset flags. The permission request carries object identity, so its dismissal is now a derived comparing the dismissed request; the continue request is a bare boolean, so its dismissal only shrinks to a reset while no request is pending. Also drop a dead if (browser) guard in the markdown theme loader - effects never run on the server. Assisted-by: pi:zai-org/GLM-5.3 * test : pin the chat perf invariants in the unit suite Cover the fixes whose silent regression would be stale or wrong UI rather than a crash: the turn-section cache must reuse unchanged turns yet recompute on every field it compares; the sibling map must resolve the same leaves after the leaf-walk memoization; the active conversation must keep its identity through field updates; and the blob gates ( exec tail window, plain-text result gate, search prefilter ) must keep accepting what they gate. Only the risky invariants are pinned - no coverage for coverage's sake. Assisted-by: pi:zai-org/GLM-5.3 * refactor : address review remarks Name the tool-arg string-field pattern, move the file tools' path field aliases and the JSON container gates into lib/constants, and export the write_file / edit_file meta types from $lib/types instead of the parser modules. Assisted-by: pi:zai-org/GLM-5.3 |
||
|
|
85d5703a3b |
ui : fix MCP image attachments not displayed in tool block (#25789) (#28089)
* ui : fix MCP image attachments not displayed in tool block (#25789) Fixes regression from #25450 where ChatMessageAgenticContent passed message.extra instead of section.toolResultExtras to tool blocks, leaving tool images invisible. Also fixes TOOL_RESULT_JSON_OPEN_REGEX which misclassified "[Attachment saved: ...]" as JSON. Fixes #25789 Assisted-by: Muse Spark * Addressed PR comments: 1.- Removed ·?? mesage?extra· as it has no case left to cover 2.- Added ·[\· to cover the case of ·[[1, 2], [3, 4]]· case suggested in the PR comment 3.- Added unit test for covering up this regex case * ui : fix MCP image attachments not displayed in tool block (ggml-org#25789) - Addressed lint error on regex (redundant \) |
||
|
|
1863ac0333 | ui: export conversations from database instead of cached store (#27432) | ||
|
|
cae63579b6 |
ui: Improve Chat Form Actions UI/UX (models selector, add panel) (#27746)
* ui : strip trailing container-format segments from parsed model names * ui : show reasoning and modality icons on model options and search by modality * ui : keep reasoning submenu visible regardless of model state * ui : add show-org-name-in-trigger display setting * ui : move model list into a submenu within the model selector * ui : make model option hover and focus highlight override the active state * ui : add raw model id tooltip to model selector options * feat: Enable microphone input as default for audio models * ui : fix eslint issues in chat form and model selector * ui: show modality icons instead of file submenu in chat add menu Assisted-by: pi * chore: Format * chore: Format * ui: add ModelCapability enum and shared modality/capability icon constants Assisted by: pi:GLM-5.3-Flash * ui: derive modality badge icons and labels from shared constants Assisted by: pi:GLM-5.3-Flash * ui: split model option icons into capabilities and modalities Replace the supportsThinking flag on ModelId with a capabilities object keyed like ModelModalities, so future capabilities (tool calls, etc.) slot in alongside reasoning. Icons and labels now come from the shared CAPABILITY_ICONS/MODALITY_ICONS constants. Assisted by: pi:GLM-5.3-Flash |
||
|
|
fe235f4343 |
ui: Replace per-conversation MCP overrides with per-conversation tool policy (#27745)
* ui: replace per-conversation MCP overrides with per-conversation tool policy MCP server enabled state is now global (server.enabled); per-conversation control moves to disabled tool keys and categories seeded into each new conversation. Aligns the add sheet with the dropdown options and flattens MCP tool groups in the tools submenu. Assisted-by: pi * ui: keep tool policy migration running when defaults parse fails A corrupt disabledToolKeys localStorage entry no longer aborts the migration; it falls through with empty defaults so legacy MCP server overrides still get converted. Assisted-by: pi * ui: fall back to global defaults when agentic flow has no tool policy Passing empty disabled sets bypassed the global defaults and could enable tools for callers that do not pass a policy yet. Assisted-by: pi * ui: align preferences section headers with their methods The Reasoning Effort and Working Directory headers sat above tool policy methods; move them above setCwd and setReasoningEffort. Also clarify the disabled tools JSDoc: existing rows with an unset field have an empty policy, defaults apply only when there is no active conversation. Assisted-by: pi * ui: gate MCP server avatars on conversation tool policy Servers whose tools are disabled for the current conversation (MCP category or server-scoped key) no longer show as enabled for the chat. Assisted-by: pi * ui: drop unused MCP category toggle from tools panel hook Per-conversation MCP control is server-granular; no component renders a whole-category toggle, so remove the dead API. Assisted-by: pi * ui: skip MCP init when flow policy disables the MCP category Resolve the effective tool policy before deciding whether to initialize MCP so flows that will not send any MCP tools skip the init work. Callers without a policy keep falling back to global defaults. Assisted-by: pi * chore: format * ui: restore reasoning section in mobile add sheet The sheet rewrite dropped it; the desktop dropdown still has it. MCP Prompts and Resources stay out of the sheet on purpose. Assisted-by: pi * ui: clear MCP server group key in enableAllToolsForServer The group key disables every tool of the server regardless of per-tool keys, so re-enabling a server from Settings did nothing while it was set. Assisted-by: pi * ui: skip MCP init when no policy-enabled server remains Extends the category-level check: the flow also skips MCP init when every globally-enabled server has its server-scoped group key disabled in the tool policy. Assisted-by: pi * ui: make Settings tools tab edit defaults with category toggles Adds per-category checkboxes and a caption stating the tab applies to new conversations; tool picks inside a chat only affect that chat. Assisted-by: pi * ui: gate cwd picker and mention picker on effective tool policy Both checked the global disabled set directly, so a conversation that disabled file_search still showed search as available. Assisted-by: pi * ui: clean up tool key helpers and store docs Documents getEnabledToolsForLLM properly, unstacks the JSDoc at isEntryEnabled, makes setToolEnabled persist like setCategoryEnabled (toggleTool now delegates to it), and routes the serverId-less MCP branch of toolKey through getMcpServerToolsKey so both key formats come from one place. Preferences banner comments become plain comments so they no longer read as class member docs. Assisted-by: pi * ui: indeterminate group checkboxes and inert grayed rows A category that is on with nothing enabled under it now shows the mixed checkbox state instead of a checked box next to 0/N. Rows grayed out by a disabled parent no longer stay clickable behind opacity. Assisted-by: pi * ui: gate MCP prompt and resource capabilities on tool policy hasPromptsCapability and hasResourcesCapability accept an optional set of usable server ids; ChatFormActions resolves it from global enablement minus the active conversation's policy. Restores the per-chat gating the old mcpServerOverrides provided; callers without arguments keep global behavior. Assisted-by: pi * ui: remove unmounted MCP submenu component Never rendered anywhere; its entries are duplicates (prompts and resources live in the attachment menu, servers in the add menu and sheet) that would need capability wiring maintained for nothing. Assisted-by: pi * ui: fix model information dialog width on all screen sizes The dialog sets container-type: inline-size, so auto width ignores its contents and collapses to padding. Give it an explicit viewport width on mobile and cap at 60rem on desktop. Assisted-by: pi * ui: scroll wide chat template in model information dialog Long unbreakable Jinja tokens blew out the table and dialog width; the block now scrolls horizontally instead of stretching. Assisted-by: pi * ui: use fixed table layout in model information dialog Auto table layout sizes columns to content min-content, so the chat template's long lines kept inflating the dialog despite the scroll wrapper. Fixed layout pins the first column and gives the value column a definite width the wrapper can scroll within. min-w-0 on the grid item guards the same path on the grid side. Assisted-by: pi * ui: make model information dialog full-screen on mobile Matches the settings dialog pattern: full viewport below md, calc-sized and capped at 60rem on desktop. Assisted-by: pi * ui: stack chat template row in model information dialog Label above the block in a single full-width cell, so the template gets the whole table width and its horizontal scroll is usable on narrow screens. Assisted-by: pi * ui: scroll model information header with the content The base dialog header is sticky; this dialog overrides it to relative so the title and description scroll away with the body. relative keeps the header as the close button's containing block. Assisted-by: pi * ui: replace literal comment text in sheet group snippet A // line inside the Svelte snippet rendered as visible text; use an HTML comment. Assisted-by: pi * ui: let indeterminate state win over checked in group checkboxes The checkbox indicator snippet renders the check icon whenever checked, so the mixed state never showed. Pass the checked prop as false while indeterminate. Assisted-by: pi * ui: initialize only policy-enabled MCP servers for a flow ensureInitialized accepts an optional server id set; the agentic flow passes the servers its tool policy leaves usable, so servers disabled for the conversation no longer get connected. Callers without arguments keep the global behavior. Assisted-by: pi * ui: derive group checkbox state in useToolsPanel Moves the mixed-state derivation out of the submenu and sheet snippets into one getGroupCheckState accessor; the snippets just consume checked and indeterminate. Assisted-by: pi * ui: gate /prompt command on the conversation tool policy The slash command's availability now follows the same rule as the agentic flow instead of the global capability check, so it disables itself when the conversation's policy leaves no usable MCP server. Assisted-by: pi * ui: remove dead MCP prompt menu trigger chain The /prompt slash command is the surviving trigger; the menu-button path (onMcpPromptClick, hasMcpPromptsSupport, showMcpPromptButton, the MCP_PROMPT attachment item and its unrendered item arrays) has no consumer left. Message display for inserted prompts is untouched. Assisted-by: pi * ui: render dash for mixed-state group checkboxes The accessor refactor dropped the checked-and-not-indeterminate guard, so the category-on flag won and the dash never showed. The tooltip keeps using the raw parent flag since clicking a mixed group still disables it. Assisted-by: pi * ui: fix group checkbox sticking checked after disable Clicking a mixed-state group box let bits-ui optimistically flip its internal checked flag; the derived checked prop did not change across the transition (both mixed and off map to checked=false), so Svelte never applied the settled value and the check icon stuck while the count already read 0/7. Pass the parent flag as checked and the mix as indeterminate, so every group toggle changes checked; render the dash on top of a checked box for the mixed state. Assisted-by: pi * fix: UI for Model Information dialog * ui: keep MCP connections stable across policy switches ensureInitialized folds the policy into its config signature, so alternating two conversations with different policies tore down and reconnected every server with health checks included. Tool collection already filters by the flow policy, so initialize every settings-enabled server instead and never pass a policy into the MCP config. The duplicated policy-server check becomes one accessor on ConversationPreferences. Assisted-by: pi * ui: remove dead MCP resources menu trigger chain Same shape as the earlier prompt trigger cleanup: nothing renders the MCP resources menu button, and the only live entry into resource browsing is Settings > MCP Servers plus the attachment resource picker. Drop onMcpResourcesClick, hasMcpResourcesSupport, MCP_RESOURCES_CLICK, the AttachmentItemVisibleWhen enum and hasResourcesCapability; the resources display, browser and picker components are untouched. Assisted-by: pi |
||
|
|
f1357e4998 |
ui: ESLint config updates (#27700)
* chore: Spacing between sibling elements in html markup * chore: Formatting and linting rules |
||
|
|
8144f3192e |
ui: Chat Conversation Tabbed navigation (#27263)
* ui : add browser-style conversation tabs store Track open conversation tabs in order, persisted to localStorage and pruned against the loaded conversation list on init. The chat layout syncs the route's tab on every navigation, so any way of reaching a conversation opens a tab for it. * ui : add temporary new-chat tabs New-chat tabs are unsaved conversations carrying a temporary id used directly as the route (#/chat/<id>). They live in memory and are only persisted to the database - keeping the same id so the route and tab stay stable - when the first message is sent. Deleting one drops it without confirmation, and deleting conversations now closes their tabs. * ui : render conversation tab bar in chat layout Desktop-only tab bar above the chat screen, one tab per open conversation or new-chat tab. The active tab follows the route id; clicking navigates, middle-click or the close button closes (switching to the left neighbor), and a trailing + starts a new chat. Tabs appear only on chat-id routes; the bare #/ new-chat view has none. The bare route stays put unless a prompt/model deep-link routes it to a new-chat tab. * ui : route new-chat entry points through tabs The sidebar New chat item, Cmd+Shift+O, the search page and the arrow-key fallback now open a new-chat tab instead of navigating to the ?new_chat URL, which is removed. New chat is no longer a special route but a tab like any other conversation. * ui : track sidebar expanded state in a shared ui store Move the desktop sidebar expanded/collapsed state out of deviceStore into a dedicated uiStore so the chat tab bar can react to it. Assisted-by: pi * chat : add opt-in conversation tabs setting Add a Display setting that turns browser-style conversation tabs on or off, enabled by default. Assisted-by: pi * chat : add browser-style conversation tabs with a new-chat screen Track open conversations as tabs above the chat, one per open chat, plus a single New chat tab for the bare `#/` route. New chat is just the `#/` screen - no temporary conversations - and its tab is dropped when navigating away. Sending the first message creates a real conversation and opens a tab for it. Assisted-by: pi * chat : turn tab bar into a horizontally scrollable carousel Make the tab bar a horizontally scrollable carousel with edge scroll buttons and active-tab centering, and align its styling with the sidebar. Assisted-by: pi * chat : restyle the scroll-to-bottom button to match tab styling Assisted-by: pi * chat : add close-tab keyboard shortcut Assisted-by: pi * chat : soften tab bar fade and dim inactive tabs Assisted-by: pi * feat: Add stop button to tabs * refactor: Componentize * ui : fix carousel scrollability detection Observe the content wrapper as well as the container, since adding overflowing items does not change the container's own box size. Also expose an onScrollableChange callback. Assisted-by: pi * ui : add unified ScrollCarousel component Single carousel component with top/center variants, gap and scroll options, and hover-revealed chevrons. Rename the HorizontalScrollCarousel accessibility story accordingly. Assisted-by: pi * ui : migrate carousels to ScrollCarousel Switch the settings mobile header, attachments list, thumbnail strip, and MCP resources to the unified component, and drop HorizontalScrollCarousel. Assisted-by: pi * ui : improve chat tabs carousel UX Scroll newly added tabs into view, fade overflowing tabs at the edges, and hide the New chat button while a new-chat tab is open. Assisted-by: pi * refactor: Naming * chat : add keyboard shortcut to jump between conversation tabs Shift+Cmd/Ctrl+Left/Right cycles the open tabs, mirroring the existing Shift+Cmd/Ctrl+Up/Down conversation navigation. Assisted-by: pi * chat : make the whole tab item act as a link The full tab is now a link instead of only the inner label button, while the stop and close buttons stay interactive by swallowing their clicks. Assisted-by: pi * chat : adjust tab bar width and use a shared offset variable Widen the tab bar for the expanded sidebar and rename the tab bar height variable to --chat-tabs-offset with a smaller value so the chat screen min-height accounts for the overlay without overshooting. Assisted-by: pi * chat : account for the tab bar offset in the assistant min-height Subtract the tab bar offset when it is shown so the last assistant message does not overflow the available viewport space. Assisted-by: pi * refactor: Post-review fixes * ui : restore deep links on the chat start page - handle ?model selection, with ?load=true eager router loading - ?q now creates a conversation, sends the prompt, and clears the params - show the not-available-model dialog for unknown models - never block mount on the conversation list Assisted-by: pi * ui : fix tab item link nesting and centralize tab constants - the tab anchor covers the whole item while stop/close stay siblings, so interactive elements are never nested inside the anchor - cmd/ctrl/middle clicks are left to the browser (new window) - extract the tab labels, the active-tab data attribute, and the sidebar-offset max widths into constants Assisted-by: pi * ui : tidy scroll carousel hook and keep mobile header arrows on - drop the dead scrollLeft/scrollRight helpers and the unused onScrollableChange/scrollBy props - init the carousel once instead of inside a derived - restore items-start on the center variant - always show the settings header arrows on touch Assisted-by: pi * ui : keep the new-chat tab across reloads and fall back on close - the new-chat sentinel is no longer pruned on init, so reloading on the bare new-chat route keeps the tab the user is on - closing the active conversation falls back to the new-chat screen when Conversation tabs are off Assisted-by: pi * ui : don't block startup on the conversation list - prune persisted tabs after the list loads in the background instead of awaiting it during init - openNewChat now returns void; its return value was never read Assisted-by: pi * ui: fix routing nits * chore: Update doc comments * refactor: Mark fire-and-forget openNewChat calls as `void` * chat: fix the deep-linked prompt, the tab width and the tab shortcuts The chat start page creates the conversation and hands the prompt over to the chat route, which still sees it in the query string. Sending it on both sides queues the second copy as a pending message, which shows up as a stray user bubble once the answer lands and vanishes on reload since it never reaches the database. The tab bar takes the max width of the collapsed sidebar while it is expanded, and the other way round. The tab list is pruned against a snapshot of the loaded conversations, so a conversation created while that list is still loading loses its tab even though the route just opened it. The active tab then falls out of the list and the cycling shortcut jumps to an edge on every keypress instead of moving one tab over. Tabs synced from the route are kept as they are, only the persisted ones are pruned. The rich chat input claims ctrl or alt with shift and an arrow for its badge-aware word jump, which now belongs to the tab cycling shortcut. Holding shift hands the key combination over, the plain word jump is unchanged. The close-tab shortcut consumes the event before checking whether the setting is on, and the logo background loses its importance flag. --------- Co-authored-by: Pascal <admin@serveurperso.com> |
||
|
|
521a64cd01 |
ui: Stores split refactor (#27240)
* ui: Extract server stream lifecycle from chatStore into ChatStreamManager
Discovery, attach/replay, resume retry and the remote-running snapshot
formed a cohesive cluster inside chatStore. It now lives in
chat-streams.svelte.ts as ChatStreamManager, owned by chatStore, which
keeps the public entry points as delegates so components are
unchanged. chatStore: 2877 -> 2418 lines.
* ui: Extract user interaction gates from agenticStore into AgenticGates
Tool permission requests, turn-limit continue prompts and queued
steering messages are the state the loop waits on between turns. They
had no coupling to session state, so they now live in
agentic-gates.svelte.ts; agenticStore keeps delegates so components
are unchanged. agenticStore: 1196 -> 1073 lines.
* ui: Compose MCP resources under mcpStore.resources
Resource state was a second import scope next to mcpStore. Consumers
now go through mcpStore.resources, so the MCP surface is one store;
mcp-resources.svelte.ts stays a separate file owned by mcpStore.
* ui: Reorganize stores into domain namespaces
* fix: Update stale doc comments
* ui: Consolidate conv running-state into a chat activity ledger
Running-state was split across chatStore.chatLoadingStates (local
pipes), ChatStreamManager.remoteRunningConvs (backend sessions) and
attachingConvs (attach lifecycle), unioned by hand in
getAllLoadingChats and cross-cleaned by setChatLoading calling
streams.clearRemoteRunning - the 'spinner ghosts until tab toggle'
workaround.
chatActivityStore now owns both sets with one transition per event:
markLocal / localEnded (local pipe end also drops the stale remote
hint, no cross-owner call) / applyRemoteSnapshot (diffed). The
sidebar reads chatStore.activity.loadingConvs through the unchanged
getAllLoadingChats entry point.
Consequences:
- isStreamingActive and its five manual writers are gone; isStreaming()
now reports whether the active conversation has a live streaming
pipe, which is what all four consumers (assistant row, stop action,
context gauge, chat screen) actually check
- isLoading/isReasoning become derived from the per-conv maps plus
the active conversation, dropping the manual resync in
syncLoadingStateForChat and clearUIState
- attachingConvs and the last-attach coordination disappear from
ChatStreamManager
- getAllStreamingChats (no consumers) is removed
* ui: Give store collaborators narrow host interfaces
Collaborators took 'host: typeof <store>', i.e. the store's entire
public surface, which is how chatStore's streamChatCompletion,
createAssistantMessage, getApiOptions and setStreamingActive got
widened to public. Replace with per-collaborator interfaces carrying
only the members each one drives:
- ChatStreamHost (chat/streams) - activity, processing, streaming
states, abort controller, loading/streaming setters
- ChatFlowsHost (chat/flows) - streaming core, message creation,
per-conv state setters
- McpHealthHost (mcp/health) - connection registry + reconnection
- ModelPropsHost / ModelStatusHost (models) - model rows, feed
updates; the managers write modalities/status back onto the host's
rows, so those members stay writable
- ConversationsPreferencesHost (conversations) - the active row and
the conversation list
The store classes now declare 'implements <Host>' so the contract is
visible at the class level, and the 'import type { <store> }' back
references in the collaborators disappear entirely - the host
contract is local to each collaborator file, and collaborators can
no longer reach around their slice. Members stay public (structural
typing), but the collaborator side is now compiler-enforced.
* test: Chat Activity store test
* refactor: Cleanup
* chore: Remove legacy architecture docs
* ui: Memoize findMessageIndex for the streaming hot path
Streaming looks up the same message index on every chunk, a linear
scan of activeMessages each time. Cache the last lookup and reuse it
after validating the id still sits at the same position (O(1)); any
structural change to the array fails validation and falls back to a
full scan.
* ui: Throttle per-chunk stream state writes to localStorage
saveStreamState ran JSON.stringify + a synchronous localStorage.setItem
on every decoded chunk of the stream. The read loop now goes through a
new saveStreamStateThrottled (one write per conversation per 500ms,
latest value held pending); the public saveStreamState keeps its
immediate-write contract for stream start and pre-fetch, and also
resets the throttle window.
A pending offset is force-flushed at resume boundaries (resumeStream
reads the offset back from localStorage), on visibilitychange->hidden
and on pagehide, so a reload always finds a usable offset. The resume
offset only needs to be roughly current since the server retransmits
from a line boundary and the client discards its partial line.
Adds unit tests for the throttled/flush/clear interplay.
* ui: Compute context gauge timing stats in one pass
currentRead/Fresh/Cache/Output were separate deriveds, each running a
full reverse scan of activeMessages for the last assistant timings,
and cumulative ran its own forward scan plus an agentic filter - 4-5
O(n) passes per chunk while streaming. Replace with a single
summarizeAssistantTimings() pass (last assistant timings, last
agentic llm totals and the cumulative sums) feeding a shared derived
snapshot. Semantics unchanged, including the live-stats overrides and
the agentic llm-totals branch.
* agentic : clear session state when a conversation is deleted
Every conversation that ran an agentic flow left an AgenticSession in the
store forever; clearSession was never called. conversationsStore now
notifies deletion listeners and agenticStore drops the matching sessions,
avoiding a circular import back into conversationsStore.
* chat : extract ChatService.normalizeMessagesForApi
The DB->API message normalization (convert + drop empty system messages)
was duplicated in sendMessage, preEncode and the agentic flow. Extract it
into one shared method and call it from all three.
* sse : share record splitting and data extraction
splitSseRecords and extractSseDataPayload centralize the record-boundary
splitting and data: line extraction used by parseSseJsonStream and the
models status feed. chat.service keeps its own line-based parser for
resume support.
* api : delegate apiFetchWithParams to apiFetch
apiFetchWithParams duplicated apiFetch's headers/fetch/error handling
body-for-body; it only differs in URL construction. Build the URL and
delegate.
* chat flows : dedupe title, timings and cleanup handling
- conversationsStore.applyTitleFromContent centralizes the title-from-first-
message logic duplicated in 5 places
- ChatProcessingStore.applyStreamTimings centralizes the onTimings handler
shared by the chat and continue flows
- host.cleanupStreaming centralizes the loading/streaming/processing reset
repeated across the continue flow's exit paths
* conversations : centralize conversation update mirroring
rename, pin, mcp override, reasoning effort and cwd all repeated the same
write-DB-then-mirror-into-list-and-active dance. A single
applyConversationUpdate(id, updates) on the host collapses all five and
removes the forgot-to-mirror-one-field bug class. Drops the redundant
array reassignment in setCwd (deep field assignment is reactive).
* mcp : dedupe tool execution, server parsing and tool indexing
- executeTool delegates to executeToolByName (only diff was argument parsing)
- drop the private #parseServerSettings copy; use parseMcpServerSettings
- cache getServers() keyed on the raw config value (hot path)
- indexServerTools() unifies the three identical toolsIndex rebuild loops
Assisted-by: Claude
* mcp : share cursor pagination and tool indexing
- MCPService.paginate() collapses the identical do-while loops in
listAllResources and listAllResourceTemplates
- promoteHealthCheckToConnection now uses indexServerTools like the other
connect paths
Assisted-by: Claude
* database : share message parent-child bookkeeping
- addChildToParent() dedups the append-to-children update in createMessageBranch
and createSystemMessage
- removeChildFromParent() dedups the remove-from-children cleanup in deleteMessage
and deleteMessageCascading
- bulkAdd the cloned messages when forking a conversation instead of one add
per message
Assisted-by: Claude
* chore: Lint/format
* fix: `pagehide` event from `window`
* refactor: Api Fetch util
* docs : rewrite architecture sections in README
Update the high-level diagram, routes, hooks, stores, services and data
flow tables to match the current UI structure (mcp/settings/search
routes, agentic/tools/mcp stores, MCPService/ToolsService/SandboxService,
/tools API). Fix stale architectural patterns for per-conversation state
and modality validation.
* chore : add ESLint rule for blank lines between accessors
Enforce a blank line between consecutive class accessors. The core
padding-line-between-statements rule does not cover class members, so a
local rule is needed.
* refactor : reorder store members and unify naming
Order store class members as public fields, private fields, constructor,
getters, public methods, then private methods. Normalize private naming
to the `private` keyword (drop `#` and the `_` prefix where there is no
matching public getter). Rename conversationsStore.init() to
initialize() to match the other stores.
* refactor : prefix lookup methods with get in agentic and chat stores
Unify bare-name lookup methods with the get* prefix used across the
other stores (mcp, models, tools, settings). Renames currentTurn,
totalToolCalls, lastError, streamingToolCall, executingToolCallId,
pendingPermissionRequest, pendingContinueRequest,
pendingSteeringMessageContent, pendingSteeringMessageExtras in the
agentic store and pendingMessageContent, pendingMessageExtras in the
chat store. Updates the two consuming components and a doc comment.
* refactor: Clean up comments in stores' and services' code
* chore : add ESLint rule for class member ordering
Enforce structural order (public fields -> private fields -> constructor ->
getters -> setters -> public methods -> private methods) with alphabetical
sorting within each group via perfectionist/sort-classes. Dependency
detection keeps Svelte $derived fields in a valid dependency order instead
of alphabetizing them, since Svelte rejects forward references.
Assisted-by: Claude
* refactor : reorder class members to match new ESLint rule
Apply the sort-classes rule across stores, services, hooks and utils.
Pure reordering - verified no logic changes by comparing sorted line
multisets before/after. All tests and svelte-check pass.
|
||
|
|
77acca437f |
ui: read persisted settings before the API key probe (#27365)
The route loads run ahead of the root layout script, so validateApiKey read the settings store while it still held factory defaults and probed /props without the stored key. initStores() now hands the same startup promise to every caller and the chat loads await it before probing. The one-time admin baseline no longer overwrites a key the user has already set: on a first visit the config carries factory values only, so a diverging key comes from the user and wins. |
||
|
|
3dc7285b4f |
ui: Services consolidation refactor (#27239)
* ui: Move stream lookup and replay fetches into ChatService chatStore called fetch() directly for /v1/streams/lookup and the /v1/stream replay. These now live next to the other stream-session methods in ChatService, so services stay the only API I/O layer. * ui: Move /models/sse feed reader into ModelsService ModelsService.watchModelEvents owns the byte stream, reconnect loop and SSE record parsing; modelsStore keeps only event routing and state. * ui: Extract conversation import/export into ConversationTransferService The JSONL session format, ZIP archiving and browser downloads are pure I/O with no store state, so they move out of conversationsStore. The store keeps the DB orchestration (bulkExportConversations, downloadConversation, importConversationsData) and delegates the format work. * ui: Consolidate active model resolution into modelsStore.activeModelId The same resolution chain was duplicated in useChatScreenActiveModel, ChatForm, ChatFormActionModels and contextStatsStore, with slight drift in the single-model fallback. The canonical getter now lives in modelsStore, and the shared last-assistant-model lookup moved to utils as getConversationModel. * ui: Initialize stores explicitly via initStores() Store constructors and module-level side effects ran migrations and localStorage reads in import order. Migrations rename and rewrite localStorage keys, so a settings load racing ahead of them could clobber migrated values. initStores() is called once from the root layout and runs migrations first, then the stores that read localStorage, then the conversations DB load. * refactor: Constants for stream query params |
||
|
|
0021a77de0 |
ui: Refactor Built-In Tools naming (Server/Browser) (#27271)
* server: rename built-in tools to server tools * ui: rename built-in tools to server/browser tools |
||
|
|
ece963f41b |
ui: mask API Key field in settings and error splash to stop browser a… (#26562)
* ui: mask API Key field in settings and error splash to stop browser autofill * ui: set autocomplete=new-password on private fields The password input type makes browsers offer to save the API key in the password manager and autofill saved site credentials into the field. The new-password autocomplete value disables both. --------- Co-authored-by: Pascal <admin@serveurperso.com> |
||
|
|
0d9ceae1e3 | ui: read structuredContent from MCP tool result when content is empty (#26691) | ||
|
|
bdffafa5df |
ui: Refactor data-attrs constants, enum for bool strings (#27002)
* refactor: Data-attribute constants + boolean string enum * refactor: Use CSS class string constants * refactor: Address review comments |
||
|
|
fa4ec4590c | refactor: Naming (#27001) | ||
|
|
d86c7d62df |
ui: Clean up contexts, remove prop drilling from Chat Form Actions (#26951)
* refactor: Remove dead context for Chat Settings and create a new one for Chat Messages Actions * refactor: Contexts & types |
||
|
|
094e53db1c |
ui: Stores architecture improvements (#26910)
* refactor: Stores barrel imports + SSR gates * refactor: Drop agenticStore wrapper exports * refactor: Drop chatStore wrapper exports * refactor: Drop modelsStore wrapper exports * refactor: Drop serverStore wrapper exports * refactor: Drop unused mcpStore wrapper exports * refactor: Drop mcpResourceStore wrapper exports * refactor: Drop conversationsStore wrapper exports + move buildConversationTree to utils * refactor: Drop settingsStore wrapper exports * refactor: Drop unused toolsStore wrapper exports * refactor: Fix lint errors from store wrapper removal * fix: Missing change * refactor: Cleanup * refactor: Context Stats store |
||
|
|
a6040c925c | refactor: Clean up UI types (#26909) | ||
|
|
e21152dc96 |
ui: Constants refactor (#26908)
* refactor: Constants * refactor: Constants/Enums cleanup * refactor: Constant objects instead of multiple single value constants * refactor: Cleanup constants |
||
|
|
4dd127584b |
ui: add read_media tool (#25877)
* server: add read_image tool (#25875) Adds a server-tool that allows vision models to analyze server-side images. This tool is reading a single file for now: The image data is base64 encoded and passed to the UI, which decodes it, fills the <img> tag and removes the data URI before passing the tool result back to the model. * cleanup read_image tool: move magic strings to constants * Add dedicated constants file: tools/ui/src/lib/constants/read-image.ts with PREFIX_IMAGE, PREFIX_SIZE, PREFIX_MIME constants * Use ATTACHMENT_SAVED_REGEX from agentic.ts in ChatMessageToolCallBlockReadImage.svelte * Use NEWLINE constant from code.ts instead of hardcoded '\n' * Use PREFIX_SIZE in regex pattern for size parsing * Add SERVER_TOOL_READ_IMAGE_PREFIX_* constants in C++ server-tools.cpp to match the TypeScript PREFIX_* constants for consistency * server: rename read_image tool to read_media for images and audio * Rename server_tool_read_image to server_tool_read_media in C++ * Rename enum BuiltInTool.READ_IMAGE to READ_MEDIA * Rename UI constants, parser, and Svelte component files * Update display label from 'Read image' to 'Read media' * ui: consolidate audio data URI handling into shared utility * Extract getAudioInputFormat to a shared utility (was duplicated inline) * Store raw base64 in base64Data on the message object * Use base64Data to construct data URIs for audio rendering * Update agentic store to build INPUT_AUDIO parts from base64Data * server: read_media: restrict audio to wav/mp3 and minor fixes * Server get_mime_from_extension now only advertises audio/wav and audio/mpeg (the only formats the model's input_audio API accepts) * Case-insensitive extension matching (fixes .MP3, .Wav, etc.) * Unknown extensions return an error instead of a multi-MB data URI that inflates model context with garbage * Updated tool description to document supported formats * Frontend AUDIO_MIME_TO_EXTENSION trimmed to match server * fix a missing import in tools/ui/src/lib/stores/agentic.svelte.ts * server: read_media: add to --tools help text and README tool list * ui: fix indentation in ChatMessageToolCallBlockDefault.svelte * server: read_media tool: fix a cast to use the correct type * server: read_media: multiple fixes * server-tools.cpp import cctype, remove UTF-8 char, check mime before reading file * ui: add MimeTypePrefix.AUDIO and use it in agentic.svelte.ts * server: make read_media inherit from read_file and add uses_cwd * ui: fix formating issues * rm from server * move it to frontend-only tool * correct partial commit * rm unused * ui: address review from allozaur Replace the magic strings, regexes and number in the read_media parser and service with named constants. Path splitting reuses FILE_PATH_SEPARATOR_REGEX, the size header regex moves to READ_MEDIA_SIZE_REGEX derived from PREFIX_SIZE, and FILE_EXTENSION_SEPARATOR lands next to it in constants/code.ts. --------- Co-authored-by: ckrafft <ckrafft@epyc> Co-authored-by: Xuan Son Nguyen <son@huggingface.co> Co-authored-by: Pascal <admin@serveurperso.com> |
||
|
|
4dee52f82d |
ui: UI/chat form follow ups (#26743)
* ui: split the markdown rendering setting per surface User content and thinking get their own toggle again, so turning off markdown for a message leaves reasoning blocks formatted. Both default to markdown. A stored renderContentAsRawText unfolds onto the user key and is dropped from the config. File mentions render as badges in the raw text path too, through a narrow pass over [name](file://path) that leaves everything else untouched. * ui: let the rich chat input scroll past its max height The contenteditable renderer caps its height with max-height but had no overflow rule, so a long buffer overflowed into the input area wrapper and got clipped by its overflow-hidden, leaving no way to reach the bottom of the message. The textarea renderer scrolls natively and was never affected. * ui: apply the new lint and format config * ui: move the render keys unfolding into the migration service Address review from @allozaur: the settings store no longer rewrites persisted config on load, the raw text toggle now unfolds onto the per-surface render keys in migration.service.ts, next to the other config migrations. The mention scanner flag and the directory path suffix become named constants. |
||
|
|
92d1bb0c99 | ui: Linting & Formatting scripts (#26819) | ||
|
|
18f7ad7fc9 |
server, ui: only offer a working directory when a tool reads it (#26762)
The working directory chip showed up as soon as the server exposed any builtin tool, so a server started with just get_datetime, or a user who turned every filesystem tool off in the settings, still got a control that nothing would read. Tools now declare whether they resolve their paths and run against the working directory, next to the write permission they already publish in the /tools listing. The WebUI shows the chip and enables the /cwd command only when at least one such tool is both served and left enabled. |
||
|
|
fc6545d322 |
allozaur/feat/chat form contenteditable (#26717)
* feat: Add contenteditable tokenizer for badge/code-chip chat input * feat: Add source-space undo/redo history for the rich input * feat: Split text glued to a closing code fence onto its own line * feat: Add ChatFormContenteditable rich input renderer * feat : wire the contenteditable into ChatForm with auto-switch gating |
||
|
|
6de1b63473 |
allozaur/feat/chat slash commands (#26716)
* base : slash-command/misc foundation - model icon and focus-selector constants * feat : slash-command picker and command parsing helpers * refactor : wire command and @-mention pickers into the chat form * ui : improve model selector keyboard navigation and load/dismiss * feat: Unify markdown/raw-text rendering under one setting with migration * fix: Misc fixes - tool-call subtitle, assistant wrap, progress guards * feat: Clamp and style numeric settings inputs from registry bounds |
||
|
|
23634783c5 |
ui: Filesystem @mentions for Chat Form (#26715)
* base : @-mention picker foundation - glob search, picker nav, highlight * feat : @-mention file/folder picker and mention badges in message bubbles * fix: Imports * feat : wire the @-mention picker into the chat form * fix: Bound the glob-search result cache key and prune stale entries |
||
|
|
2f56fc3431 |
ui: CWD for agent (#26518)
* server : extend file_glob_search for UI pickers * ui : add per-conversation working directory with picker * ui : add path navigation and search scope to cwd picker Treat path-like queries (starting with / or ~) as directory navigation instead of glob-matching the whole query: search the parent for the last segment, and descend into an exactly-typed directory by listing its children. Show the effective search scope in the footer and auto-search on open so the current directory and its siblings appear immediately. Assisted-by: Claude * db : persist per-call tool cwd on tool result messages * ui : abbreviate tool paths under home with a tilde * ui : show the per-call cwd on exec shell rows * ui : clarify the synthetic cwd message for the model * ui : reuse the trailing cwd row on a repeated pick * ui : don't jump when a cwd row is injected mid-chat * chore: Formatting * refactor: Cleanup comments * ui : unify working directory naming and add a synthetic-message flag * ui : render synthetic cwd rows without a scroll jump * ui : decouple the working directory picker into utils and sub-components * ui : add get_info tool call block * chore: Formatting * refactor: Cleanup * refactor: Cleanup * refactor: Cleanup * fix: UI * server : harden file_glob_search listing (kind enum, timeout, symlink guard, absolute base) * ui : use persisted isSynthetic flag for cwd rows, drop legacy formats * ui : cache picker search, fail visibly on native resolve * ui : escape glob metacharacters in picker search glob * ui : simplify auto-scroll pin * chore: Format * fix: Use `SvelteMap` * refactor: Post-review fixes * ui: accept Windows roots in the working directory picker recognize a drive root (C:) and a UNC share (//host/share) as path navigation, alongside the POSIX root and ~, so a query like D:\repos lists that directory instead of glob-matching it under the home dir split below the root, so a bare drive resolves to its root rather than to a drive-relative prefix rewrite backslashes into forward slashes only when the query carries a Windows root, since a backslash is a legal POSIX filename character paths keep travelling with forward slashes, which is what the server returns and what Windows accepts --------- Co-authored-by: Pascal <admin@serveurperso.com> |
||
|
|
6e2bc65fb2 | ui: rendering performance follow-up (#26097) | ||
|
|
55b7d6c4c7 |
ui: detect the conversation import format from file contents (#26121)
* ui: detect the conversation import format from file contents iOS resolves every accept entry to a UTI and has none for ".jsonl", so the picker greyed out exported conversations. Drop the accept filter and pick the parser from the file contents: ZIP magic bytes, then a first "session" record for JSONL, otherwise the legacy JSON format. Also remove the unused importConversations() picker and an orphan doc comment, and cover each format with unit tests. * ui: report what a conversation import actually wrote The import summary echoed the selection back, so re-importing conversations already in the database claimed success while nothing was written and only a console warning said otherwise. Return the imported and skipped conversations from the database layer, list the written ones in the summary, and count the rest in a toast. * ui: name the literals of the JSONL conversation format Introduce SessionRecordType and SESSION_HARNESS, and reuse the existing NEWLINE constant, so the record format lives in one place. This also covers the writer side, which predates the import path under review and carried the same literals: an enum stated by the reader alone lets the two sides drift. Values are unchanged, so an export stays byte identical. |
||
|
|
555881ebc8 |
ui: reduce per-token render cost when streaming (#26053)
* performance harness - the empirical root Assisted-by: Claude Opus 4.8 * 210.36ms -> 2.67ms per streamed token Assisted-by: Claude Opus 4.8 * 11.58ms -> 0.62ms per streamed token Assisted-by: Claude Opus 4.8 * 22.02ms -> 3.33ms per streamed token Assisted-by: Claude Opus 4.8 * 3.07ms -> 1.36ms per streamed token at 40 messages Assisted-by: Claude Opus 4.8 --------- Co-authored-by: Zach Winter <dmtommy@icloud.com> |
||
|
|
95a923a64c |
ui: fix MCP server display name conflicts in tools lists (#26011)
* ui: fix MCP server display name conflicts in tools lists Tool groups were keyed by display label so two servers reporting the same name broke the keyed each blocks and only one was visible. Key rendering, expand state and toggles by the stable server id instead, and suffix duplicate labels with a counter in config order. * ui: customizable MCP server display name with autofill Add a display name field to the MCP server form, add and edit alike. The custom name takes precedence over the server-reported one, so two servers reporting the same name can be told apart; clearing the field returns to the automatic label. In the add dialog a debounced preview handshake prefills the field with the server-reported name: a manual edit freezes the autofill, stale responses are discarded, failures stay silent, and an unedited prefill is not persisted so the label keeps following the server. * ui: fix recursive fetch passthrough in the client test setup The original fetch was captured inside beforeEach, where it is the previous test's spy since vi.spyOn returns the existing one, so the default passthrough recursed on itself for any URL outside the mocked set. Capture the real fetch once at module load. |