From d3ee7dbbec693d2e186d16f3958e38ac13a7dda6 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 23 Sep 2026 10:19:42 +0200 Subject: [PATCH] Added copying a chat from the chat toolbar --- .../Components/ChatComponent.razor | 4 ++ .../Components/ChatComponent.razor.cs | 47 ++++++++++++++++++- .../Components/Workspaces.razor.cs | 21 ++++++++- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor b/app/MindWork AI Studio/Components/ChatComponent.razor index f1fe5039..c27291be 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor +++ b/app/MindWork AI Studio/Components/ChatComponent.razor @@ -103,6 +103,10 @@ + + + + } diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs index a28509f1..f4c017aa 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor.cs +++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs @@ -649,7 +649,9 @@ public partial class ChatComponent : MSGComponentBase } private bool CanThreadBeSaved => this.ChatThread is not null && this.ChatThread.Blocks.Any(b => !b.HideFromUser); - + + private bool CanThreadBeCopied => this.CanThreadBeSaved && !this.IsCurrentChatStreaming && !this.MediaTranscriptionService.IsBusy(this.CurrentMediaImportOwner); + private string TooltipAddChatToWorkspace => string.Format(T("Start new chat in workspace '{0}'"), this.currentWorkspaceName); private string UserInputStyle => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence ? this.Provider.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : string.Empty; @@ -1344,7 +1346,48 @@ public partial class ChatComponent : MSGComponentBase await this.SyncWorkspaceHeaderWithChatThreadAsync(); } - + + /// + /// Copies the open chat and continues in the copy. + /// + /// + /// Copied is the chat as it stands on the screen, not the state which was stored last. That is + /// why nothing is asked about unsaved changes here, unlike everywhere else a chat is left + /// behind: none of them are lost, they move into the copy, while the original keeps what was + /// stored.

+ /// + /// The same holds for the message being written. The copy is marked as loaded right away, + /// because the parameter update which follows would otherwise take it for another chat being + /// opened and clear the composer. Only the transcripts attached to that message are exchanged: + /// those of the original live in its directory, and the copy got copies of its own. + ///
+ private async Task CopyCurrentChat() + { + if (this.ChatThread is null || !this.CanThreadBeCopied) + return; + + var sourceChat = this.ChatThread; + var copy = this.Workspaces is null + ? await WorkspaceBehaviour.CopyChatAsync(this.DialogService, sourceChat) + : await this.Workspaces.CopyChatAsync(sourceChat); + + if (copy is null) + return; + + var transcriptsOfTheOriginal = sourceChat.PendingMediaTranscripts.Select(transcript => transcript.FilePath).ToHashSet(StringComparer.Ordinal); + this.ComposerState.FileAttachments.RemoveWhere(attachment => transcriptsOfTheOriginal.Contains(attachment.FilePath)); + foreach (var transcript in copy.PendingMediaTranscripts) + this.ComposerState.FileAttachments.Add(transcript); + + this.ChatThread = copy; + this.hasUnsavedChanges = false; + + await this.SyncForegroundChatAsync(); + this.MarkCurrentChatAsLoadedParameter(); + await this.SyncWorkspaceHeaderWithChatThreadAsync(); + await this.ChatThreadChanged.InvokeAsync(this.ChatThread); + } + private async Task LoadedChatChanged(bool notifyParent = true) { this.hasUnsavedChanges = false; diff --git a/app/MindWork AI Studio/Components/Workspaces.razor.cs b/app/MindWork AI Studio/Components/Workspaces.razor.cs index 801b197c..240f7dfb 100644 --- a/app/MindWork AI Studio/Components/Workspaces.razor.cs +++ b/app/MindWork AI Studio/Components/Workspaces.razor.cs @@ -660,6 +660,24 @@ public partial class Workspaces : MSGComponentBase await this.LoadTreeItemsAsync(startPrefetch: false); } + /// + /// Copies the given chat, after asking the user for the name of the copy, and shows the copy in the tree. + /// + /// The chat to copy, as it stands in memory. + /// The persisted copy. Null when the user canceled the question, in which case nothing was copied. + /// + /// Neither asks about unsaved changes nor opens the copy: which of both a caller needs depends + /// on where the copy was asked for. + /// + public async Task CopyChatAsync(ChatThread sourceChat) + { + var copy = await WorkspaceBehaviour.CopyChatAsync(this.DialogService, sourceChat); + if (copy is not null) + await this.LoadTreeItemsAsync(startPrefetch: false); + + return copy; + } + private async Task LoadChatAsync(string? chatPath, bool switchToChat) { if (string.IsNullOrWhiteSpace(chatPath)) @@ -819,11 +837,10 @@ public partial class Workspaces : MSGComponentBase } var sourceChat = isCopyOfOpenChat ? openChat! : chat; - var copy = await WorkspaceBehaviour.CopyChatAsync(this.DialogService, sourceChat); + var copy = await this.CopyChatAsync(sourceChat); if (copy is null) return; - await this.LoadTreeItemsAsync(startPrefetch: false); this.CurrentChatThread = copy; await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread); }