Skip to content

v1.7.5 — chat branching, message editing, preserve-thinking, engine fixes#53

Merged
mohitsoni48 merged 12 commits into
mainfrom
feature/engines-fix-and-chat-ux
Jul 9, 2026
Merged

v1.7.5 — chat branching, message editing, preserve-thinking, engine fixes#53
mohitsoni48 merged 12 commits into
mainfrom
feature/engines-fix-and-chat-ux

Conversation

@mohitsoni48

Copy link
Copy Markdown
Owner

Summary

  • Chat branching: regenerate and user-message edits now branch instead of destroying history, with a switcher UI (‹ N/M ›), including nested branch points.
  • In-place message editing: assistant replies can be edited without triggering a new generation; shows an "Edited" tag.
  • Preserve-thinking toggle: per-conversation setting to fold past reasoning into resent context; now on by default with a proper Switch UI, and usable on a blank chat before the first message.
  • Model-sourced sampling defaults: Thread settings sliders now default to the loaded model's own recommended values instead of fixed constants.
  • Collapsible skills list: collapsed by default, with a master on/off checkbox and correct indeterminate state.
  • Per-conversation background generation: switching conversations no longer aborts an in-flight generation; the sidebar now shows a live spinner for background generations and a completed indicator when one finishes elsewhere.
  • Fixes: CUDA backend silently falling back to CPU on an incomplete multi-asset download; a model swap that could silently abandon the load after a slow stop, leaving no model loaded with no error; custom engines not appearing in the engine gallery; an EPERM crash during engine scanning; a streaming empty-block bug; a Thread settings dialog overflow with many skills; a missing DialogTitle a11y issue; pinned models not sorting to the top of the chat model picker.

Test plan

  • Typecheck clean, full suite green (711/711, 3 pre-existing skips) after every commit
  • Live-verified against a real running daemon + model throughout, including: actual generation request payload confirming 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
  • User's own hands-on pass (QA checklist shared earlier this cycle)

…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.
@mohitsoni48 mohitsoni48 merged commit 8323de6 into main Jul 9, 2026
1 check passed
@mohitsoni48 mohitsoni48 deleted the feature/engines-fix-and-chat-ux branch July 9, 2026 05:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant