Fixed chats being stored when chats are stored manually or workspaces are disabled (#1145)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-10-31 18:54:20 +01:00
1 parent 9532664a25
commit 64dc621a7d
12 files changed
+415 -71

No files matched your search

@@ -674,6 +674,9 @@ public partial class ChatComponent : MSGComponentBase
}
var leftChatHadNoThread = !isFirstChat && this.loadedParameterChatId == Guid.Empty;
if (!isFirstChat)
await this.StopAnswerOfLeftChatAsync(this.loadedParameterChatId);
this.loadedParameterChatId = chatId;
this.loadedParameterWorkspaceId = workspaceId;
@@ -1580,9 +1583,15 @@ public partial class ChatComponent : MSGComponentBase
}
/// <summary>
/// Creates and stores the chat the composer belongs to, when there is none yet, e.g., for what an
/// attachment brings along before the first message is sent.
/// Creates the chat the composer belongs to, when there is none yet, e.g., for what an attachment
/// brings along before the first message is sent.
/// </summary>
/// <remarks>
/// Only while chats are stored automatically, the new chat reaches the disk right away. Otherwise, it
/// counts as unsaved, like a message typed into a new chat: a transcript made for it waits in the
/// staging area until the user saves the chat, see WorkspaceBehaviour.StoreChatAsync, and the next
/// start removes it when they never do.
/// </remarks>
/// <param name="nameWithoutInput">The name of the chat while the user typed nothing yet.</param>
/// <returns>The chat of the composer.</returns>
private async Task<ChatThread?> EnsureChatAsync(string nameWithoutInput)
@@ -1615,7 +1624,11 @@ public partial class ChatComponent : MSGComponentBase
// lists the new chat right away, together with the progress of the transcription, instead
// of only after the first message was sent:
//
await this.StoreAndListChatAsync(chat);
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
await this.StoreAndListChatAsync(chat);
else
this.hasUnsavedChanges = true;
await this.SyncForegroundChatAsync();
return chat;
}
@@ -1766,7 +1779,7 @@ public partial class ChatComponent : MSGComponentBase
this.ChatThread.SelectedToolIds = [..this.selectedToolIds];
this.ChatThread.RuntimeSelectedToolIds = this.ToolRegistry.FilterToolIdsForProvider(this.Provider, this.selectedToolIds, this.ChatThread.RequiredOutboundDataRestriction.Restriction);
// From here on, the AI job stores the chat, in every storage mode:
// From here on, the AI job stores the chat, while chats are stored automatically:
this.unstoredChat = null;
await this.AIJobService.TryStartChatGenerationAsync(new ChatGenerationRequest
{
@@ -1786,6 +1799,28 @@ public partial class ChatComponent : MSGComponentBase
await this.AIJobService.CancelChatGenerationAsync(this.ChatThread.ChatId);
}
/// <summary>
/// Stops the answer which is still being written in a chat the user leaves, unless chats are stored automatically.
/// </summary>
/// <remarks>
/// Only while chats are stored automatically, the AI job stores the answer, so that the user finds
/// it when coming back. Otherwise, the answer would end up nowhere, and writing it would only cost
/// the user. The user was asked before, since an answer being written counts as an unsaved change
/// then.<br/><br/>
///
/// Called only where the user leaves a chat: opening another one, starting a new one, or leaving the
/// chat page. Never when this component goes away, since the page creates it anew, e.g., whenever the
/// workspace sidebar is shown or hidden, while the chat stays.
/// </remarks>
/// <param name="chatId">The chat being left.</param>
private async Task StopAnswerOfLeftChatAsync(Guid chatId)
{
if (chatId == Guid.Empty || this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
return;
await this.AIJobService.CancelChatGenerationAsync(chatId);
}
/// <summary>
/// Takes over the tool selection of the chat that was just loaded or handed to this component.
/// </summary>
@@ -1825,7 +1860,21 @@ public partial class ChatComponent : MSGComponentBase
if (!this.CanThreadBeSaved)
return;
var stagedTranscripts = this.ChatThread.Draft.MediaTranscripts.Where(transcript => transcript.IsStaged).Select(transcript => transcript.FilePath).ToHashSet(StringComparer.Ordinal);
await this.StoreAndListChatAsync(this.ChatThread);
//
// Storing moved the staged transcripts of the message being written into the chat directory,
// see WorkspaceBehaviour.StoreChatAsync. The composer still knows them by where they were, so
// sending the message would lose them:
//
if (stagedTranscripts.Count > 0)
{
this.ComposerState.FileAttachments.RemoveWhere(attachment => stagedTranscripts.Contains(attachment.FilePath));
foreach (var transcript in this.ChatThread.Draft.MediaTranscripts)
this.ComposerState.FileAttachments.Add(transcript);
}
this.hasUnsavedChanges = false;
// A new chat is on the disk now, like any other:
@@ -1876,9 +1925,11 @@ public partial class ChatComponent : MSGComponentBase
//
// Want the user to manage the chat storage manually? In that case, we have to ask the user
// about possible data loss:
// about possible data loss. An answer still being written is lost as well, whenever chats are
// not stored automatically, since leaving stops it, see StopAnswerOfLeftChatAsync:
//
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges && !this.IsCurrentChatStreaming)
var storageBehavior = this.SettingsManager.ConfigurationData.Workspace.StorageBehavior;
if ((storageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges) || (storageBehavior is not WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY && this.IsCurrentChatStreaming))
{
var dialogParameters = new DialogParameters<ConfirmDialog>
{
@@ -1930,6 +1981,9 @@ public partial class ChatComponent : MSGComponentBase
return false;
}
if (this.ChatThread is not null)
await this.StopAnswerOfLeftChatAsync(this.ChatThread.ChatId);
//
// Reset our state:
//
@@ -2034,19 +2088,6 @@ public partial class ChatComponent : MSGComponentBase
if(this.ChatThread is null)
return;
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges && !this.IsCurrentChatStreaming)
{
var confirmationDialogParameters = new DialogParameters<ConfirmDialog>
{
{ x => x.Message, T("Are you sure you want to move this chat? All unsaved changes will be lost.") },
};
var confirmationDialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Unsaved Changes"), confirmationDialogParameters, DialogOptions.FULLSCREEN);
var confirmationDialogResult = await confirmationDialogReference.Result;
if (confirmationDialogResult is null || confirmationDialogResult.Canceled)
return;
}
var dialogParameters = new DialogParameters<WorkspaceSelectionDialog>
{
{ x => x.Message, T("Please select the workspace where you want to move the chat to.") },
@@ -2065,13 +2106,15 @@ public partial class ChatComponent : MSGComponentBase
//
// Like saving and copying, the move goes through the workspace component whenever there is
// one, so its tree shows the chat in the new place right away:
// one, so its tree shows the chat in the new place right away. Nothing the user did not save
// gets lost, so nothing is asked: it reaches the new place only while chats are stored
// automatically, and otherwise stays unsaved, see WorkspaceBehaviour.MoveChatAsync:
//
var previousWorkspaceId = this.ChatThread!.WorkspaceId;
if (this.Workspaces is not null)
await this.Workspaces.MoveChatAsync(this.ChatThread!, workspaceId);
else
await WorkspaceBehaviour.MoveChatAsync(this.ChatThread!, workspaceId);
await WorkspaceBehaviour.MoveChatAsync(this.ChatThread!, workspaceId, this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY);
await this.FollowMovedChatAsync(previousWorkspaceId);
}
@@ -2204,6 +2247,9 @@ public partial class ChatComponent : MSGComponentBase
{
await this.FlushDraftAsync();
await this.PreserveNewChatDraftAsync(this.ChatThread is null);
if (this.ChatThread is not null)
await this.StopAnswerOfLeftChatAsync(this.ChatThread.ChatId);
this.hasUnsavedChanges = false;
this.ComposerState.Clear();
this.ClearWorkspaceHeaderState();
@@ -2283,8 +2329,11 @@ public partial class ChatComponent : MSGComponentBase
if (!this.blockActions.TryRemove(this.ChatThread, block))
return;
// Like every other change, stored right away only while chats are stored automatically:
this.hasUnsavedChanges = true;
await this.SaveThread();
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
await this.SaveThread();
this.StateHasChanged();
}
@@ -2310,8 +2359,12 @@ public partial class ChatComponent : MSGComponentBase
// The rollback reset the AI-selected data sources, which the data source selection still shows:
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
// Like every other change, stored right away only while chats are stored automatically:
this.hasUnsavedChanges = true;
await this.SaveThread();
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
await this.SaveThread();
this.tokenTracker?.Nudge();
this.StateHasChanged();
await this.inputField.FocusAsync();
@@ -2568,7 +2621,8 @@ public partial class ChatComponent : MSGComponentBase
this.ChatThread = this.AIJobService.TryGetLiveChatThread(snapshot.SubjectId) ?? this.ChatThread;
if (!snapshot.IsActive)
{
this.hasUnsavedChanges = false;
// The job stored the answer only while chats are stored automatically, see AIJobService.CheckpointChatAsync:
this.hasUnsavedChanges = this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY;
this.previousInputForbidden = true;
}
@@ -2587,8 +2641,9 @@ public partial class ChatComponent : MSGComponentBase
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
return Task.FromResult(new MessageBusAnswer<TResult>(true, (TResult) (object) false));
// Nothing stores an answer being written now, and leaving stops it, see StopAnswerOfLeftChatAsync:
if (this.IsCurrentChatStreaming)
return Task.FromResult(new MessageBusAnswer<TResult>(true, (TResult) (object) false));
return Task.FromResult(new MessageBusAnswer<TResult>(true, (TResult) (object) true));
//
// A message the user started counts as unsaved on its own only while there is no chat