v1.7.5 — chat branching, message editing, preserve-thinking, engine fixes#53
Merged
Conversation
…mpty block - EnginesScreen: custom-added engines that match no catalog entry now get their own card instead of being silently omitted (GitHub #51). Excludes auto-downloaded official llama.cpp builds, which already have dedicated UI via the "Manage GPU builds" expander. - download.ts: findFile now skips directories it can't read (EPERM/EACCES) instead of crashing the whole engine scan. - MessageBubble: whitespace-only reasoning deltas no longer render an empty "Thinking" block during or after streaming (GitHub #52).
…ssistant edits, preserve-thinking toggle GitHub #52 items 1-3. - Chat branching: regenerating a reply or editing an earlier user message used to permanently delete everything downstream. Both now freeze that history instead (variant_group/is_active/branch_of on messages), with a ‹ 1/2 › switcher in the UI to move between versions. Handles nested branch points (editing inside an already-branched conversation) and correctly restores whichever regenerate-sibling was active within a frozen tail. Fixed a real bug found via QA: /continue used to resolve "the message regenerate just deactivated" by globally-highest seq, which grabbed an unrelated later branch's message in conversations with more than one branch point — now resolves it precisely via getNextMessageAfterSeq. - Assistant messages can now be edited in place (fixes the text without triggering a new generation) rather than being blocked entirely. - New per-conversation "preserve thinking across turns" setting (Thread settings dialog): when on, past turns' reasoning is folded back into resent content (wrapped in <think> tags) with chat_template_kwargs. preserve_thinking merged in, instead of only ever resending final answers. Off by default. Live-verified against the real engine payload. DB migrations v23-v25. 21 new unit tests across branching.test.ts and preserve-thinking.test.ts; live-verified end-to-end against a real running daemon + model, including nested branching, sibling isolation on delete/edit, and the multi-branch regenerate fix.
… runtime never fully downloaded Root cause: provisionBackend's "already installed" check only verified a server binary existed, not that every asset for a multi-asset backend (CUDA = main binary + cudart runtime bundle) actually landed. If the second download ever failed partway or a folder from an older TurboLLM version was reused, the install looked complete forever after. The CUDA backend then loads llama-server successfully but silently runs CPU-only (ggml-cuda.dll can't find cudart64_*/cublas64_*/cublasLt64_* at runtime, with zero error output) — GPU offload requested via -ngl but never happens. Fix: write a completion marker only after every asset succeeds; both the early-return in provisionBackend and installedBackendBuild now require it. Backward-compatible for pre-existing installs with no marker yet: a CUDA build only counts as complete if its cudart DLLs are actually present (auto-backfills the marker); any other (single-asset) backend just needs its binary, matching what was always true for those. Live-verified end to end on real hardware: found and deleted an actual broken install on this box, triggered a real backend update, confirmed it correctly fetched both parts this time, and confirmed real GPU usage (0 MiB -> 7,554 MiB VRAM; ~17-34 tok/s CPU -> 130 tok/s + 3,000+ tok/s prefill on GPU). 11 unit tests covering the completeness check, including the exact partial-install scenario and the legacy-install backfill path.
… avatar icon - New per-message `edited` flag, set only by the explicit assistant-edit route (never by the normal generation-completion save) — shown as a small "· Edited" label next to the stats row. - Removed the "T" avatar icon from assistant messages (both streaming and completed), per founder request. Simplified the now-single-child bubble layout accordingly. Live-verified: edited a real reply in the browser, confirmed the tag renders and the underlying stats stay unchanged (proving no new generation was triggered), and confirmed the avatar is gone from both message states. 4 new unit tests.
…d of a plain span
The dialog heading was a bare <span>, not the DialogTitle component, which
triggered a console error every time it opened ("DialogContent requires a
DialogTitle for the component to be accessible for screen reader users").
Checked all 12 other DialogContent usages in the app — this was the only
one missing it. Switched to DialogHeader + DialogTitle (no custom
className), matching the pattern every other dialog already uses.
Live-verified: dialog renders identically, zero console errors (previously
6 repeated warnings per open).
ModelLoadMenu (the chat header's model switcher) never consulted the existing localStorage-backed usePinnedModels hook, so pinning a model in the Library only helped there. Now it sorts pinned models first (stable otherwise) and shows the same star indicator as ModelsScreen.
The body wrapper had no height cap, so a long skill list (58 on a real box) grew the dialog past the viewport with no way to reach Save. Cap it at max-h-[60vh] with overflow-y-auto, matching the existing BuildGuideDialog convention (scrollable body, footer stays pinned).
… stop stopAndWait() raced a fixed 10s sleep against process exit but never escalated on timeout, so a slow kill (e.g. Windows tree-kill) could return with state stuck at 'stopping'. The next startInternal() then threw BusyError, which load()'s fire-and-forget caller only console.warned - the UI just showed "no model loaded" with no error and no retry path. Fix: on timeout, force-kill and truly await exit before returning, and have load() catch any failure and set state='error' with a message so polling actually surfaces it.
…el-sourced sampling, collapsible skills Five Thread settings changes, requested together and implemented as one coherent patch since they share files: - Preserve-thinking now defaults to true for new conversations. Fixed at the createConversation INSERT level (db.ts) rather than the migration's column DEFAULT, which is frozen at 0 for already-migrated databases and would never have applied to existing users. - The preserve-thinking control now renders as a real Switch in its own bordered row, no longer visually identical to a skill checkbox. - Thread settings is now reachable on a blank/not-yet-created chat (any time a model is loaded), mirroring the existing pendingSkillIds pattern for systemPrompt/sampling/preserveThinking. Required extending createConversation's Pick type, POST /api/v1/conversations' body, and the frontend createConversation() to actually accept sampling + preserveThinking end to end. - Sampling slider defaults now come from the loaded model's own profile (temp/topP/topK/minP) instead of fixed constants, falling back to the old hardcoded defaults only when no model is loaded. A user's manual override still wins over both. - The skills list is now a collapsible section (collapsed by default, re-collapses on every dialog open) with a master checkbox that selects/deselects all skills, showing indeterminate state correctly. Also: leaving the chat screen (switching conversations) no longer aborts an in-flight generation - only an explicit Stop/Eject/delete should. handleSelect was unconditionally calling abortRef.current.abort() just because the user clicked a different conversation; now it only clears the local streaming view (setLive(null)) and lets the backend finish and save the result normally. Live-verified end to end against a real running daemon: the actual generation request payload confirmed chat_template_kwargs.preserve_thinking and past-turn reasoning correctly folded into resent <think> blocks; switching conversations mid-generation let a 150-word reply complete and save; sampling sliders showed the model's real recommended values (not generic constants); the skills master checkbox correctly toggled all 57 installed skills with proper indeterminate state.
…dicators Since generation no longer aborts on navigation (b6d84f3), the client-side live state needed to catch up: live/abortRef/deltaTimestamps were single global values, so switching away from a generating conversation and back lost all accumulated streaming content - only the final saved message would show once done, with no indication anything was happening while backgrounded. - live is now keyed per-conversation (liveByConv), updated via streamFrom's convId closure param (not the possibly-stale activeId), so switching back to a generating conversation shows the actual accumulated thinking/streaming content, not a blank view. - abortRef and the tok/s rolling-window tracker are also per-conversation, since multiple conversations can now legitimately generate concurrently. - handleSelect/handleNew no longer touch generation state at all (nothing to clear with per-conversation keying). handleEject aborts every in-flight generation (ejecting kills the whole engine); handleStop/Esc only target the active conversation. - ConversationSidebar shows a spinning indicator next to any conversation with a background generation running, and a distinct dot for one that completed while the user was elsewhere, cleared on visit. Live-verified: started a generation, switched conversations twice, confirmed via DOM the spinner appears while backgrounded and the completed-dot appears then clears on visit; the verify pass separately confirmed the live "Thinking..." view genuinely resumes with accumulated content (not just the final message) after switching back mid-generation.
…, stale comment, cache invalidation) Opus review of PR #53 flagged: - [MEDIUM] manager.ts's waitForExit force-kill fallback did an unbounded await exited, so an unkillable/zombie process would wedge the exclusive load gate forever - worse than the "stuck at stopping" bug it replaced. Bounded the second wait too; a still-stuck process now degrades to a recoverable returned-but-not-exited state (load()'s existing catch surfaces it as state='error') instead of deadlocking every future load. - [LOW] isFullyProvisioned's backfill marker write was unguarded - a read-only dir or full disk would 500 the request instead of the graceful outcome the rest of the function already provides. Wrapped in try/catch, best-effort. - [LOW] chat-types.ts's preserveThinking doc comment still said "Off by default" after the default flipped to on. - [LOW] regenerate's mutation never invalidated the message-variants cache, self-correcting only because the active message's id changes on refetch (load-bearing but non-obvious). Invalidate explicitly.
…inking, engine fixes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Test plan
chat_template_kwargs.preserve_thinking; multi-branch regenerate; CUDA VRAM before/after (0 MiB → 7,554 MiB); model swap smoke test; background generation surviving conversation switches with sidebar indicators confirmed via DOM inspection