Skip to content

feat(ui): forking chat - retry any answer, copy, duplicate, branch (#10645)#10654

Merged
mudler merged 5 commits into
masterfrom
feat/forking-chat
Jul 2, 2026
Merged

feat(ui): forking chat - retry any answer, copy, duplicate, branch (#10645)#10654
mudler merged 5 commits into
masterfrom
feat/forking-chat

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

What

Chat management for the built-in React chat UI, closing #10645. Adds the "forking" conversation tools requested in the issue: retry any answer, copy a whole chat, duplicate a chat, and branch a new chat from any answer - so you don't need to point a separate web frontend at your LocalAI instance.

Features

  • Retry any answer - the regenerate action now works on every assistant message, not just the last. Retrying a mid-conversation answer truncates the conversation to just before that turn and re-asks it.
  • Copy an entire chat - a new "Copy chat" action puts the whole conversation (Markdown) on the clipboard. The existing "Export as Markdown" download is preserved.
  • Duplicate a chat - clone a whole conversation into an independent new chat.
  • Branch from here - on any assistant answer, fork a new chat containing history up to and including that answer, so you can explore an alternate direction without disturbing the original.

How

Entirely client-side, no backend changes. The three duplication behaviors collapse onto one forkChat(chatId, uptoIndex?) mutator in useChat (deep-clones history via structuredClone, fresh id/name/timestamps, resets token usage). handleRegenerate was generalized to handleRegenerate(targetIndex). Message actions stay index-addressed (no stable message ids, no localStorage migration, saveChats whitelist untouched). New i18n strings go in public/locales/en/chat.json (English is the fallback for all locales).

Notable fix folded in

Generalizing retry surfaced a latent stale-closure bug: sendMessage built its API payload from the closure chats, so a mid-conversation retry would have regenerated the answer with the downstream conversation still in the model's context (the DOM truncated correctly, but the request body did not). Fixed by threading the truncated history explicitly via sendMessage(..., { baseHistory }), with an e2e test that asserts the outbound /v1/chat/completions payload.

Testing

  • New Playwright e2e spec e2e/forking-chat.spec.js (5 cases): duplicate, retry-any-answer, copy-chat (asserts clipboard contents), branch-from-message, and the retry payload assertion. All pass.
  • Existing chat specs (chat-polling-selection, chat-errors, page-render-smoke) still green (19/19).
  • UI coverage ratchet satisfied (rose, did not decrease). ESLint: no new findings.

Files

  • src/hooks/useChat.js - forkChat mutator, options.baseHistory
  • src/pages/Chat.jsx - generalized retry, per-message retry + branch buttons, serializer split, copy-chat
  • src/components/ChatsMenu.jsx - Duplicate + Copy chat per-item actions
  • public/locales/en/chat.json - new strings
  • e2e/forking-chat.spec.js - coverage

Closes #10645

Assisted-by: Claude:claude-opus-4-8 [Claude Code]

mudler added 5 commits July 2, 2026 20:55
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Mid-conversation retry regenerated an answer with the downstream turns
still in the model's context. handleRegenerate truncated the DOM history
via updateChatSettings (a scheduled state update), but the synchronous
sendMessage that followed read the stale, pre-truncation history from its
closure to build the outbound API payload. Thread the intended base
history explicitly through sendMessage's options.baseHistory so the
request body matches the truncated view. Backward compatible: the normal
send path (no baseHistory) is unchanged.

Also guard two minor issues in Chat.jsx: the "Branch from here" button now
renders under !isStreaming to match the retry button, and the duplicate
toast only fires when forkChat returns a chat (not on a null result).

Assisted-by: Claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
@mudler
mudler merged commit 237bce4 into master Jul 2, 2026
61 checks passed
@mudler
mudler deleted the feat/forking-chat branch July 2, 2026 22:04
@localai-bot localai-bot added the enhancement New feature or request label Jul 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Forking chat

2 participants