diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs index ae0cfdbe..fdbb7d57 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs @@ -687,7 +687,11 @@ public abstract partial class AssistantBase : AssistantLowerBase wher case Event.ASSISTANT_SESSION_CHANGED: case Event.ASSISTANT_SESSION_FINISHED: if (data is AssistantSessionSnapshot snapshot && snapshot.Key == this.assistantSessionKey) + { await this.AttachAssistantSession(snapshot, restoreClientOnlyContent: triggeredEvent is Event.ASSISTANT_SESSION_FINISHED); + if (triggeredEvent is Event.ASSISTANT_SESSION_FINISHED) + _ = this.AssistantSessionService.TryTakeInactiveSnapshot(this.assistantSessionKey); + } break; } } @@ -699,6 +703,13 @@ public abstract partial class AssistantBase : AssistantLowerBase wher private async Task AttachAssistantSessionIfAvailable() { var snapshot = this.AssistantSessionService.TryGetSnapshot(this.assistantSessionKey); + if (snapshot?.IsActive ?? false) + { + await this.AttachAssistantSession(snapshot, restoreClientOnlyContent: true); + return; + } + + snapshot = this.AssistantSessionService.TryTakeInactiveSnapshot(this.assistantSessionKey); if (snapshot is null) return; diff --git a/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionService.cs b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionService.cs index 941a51b1..a5dd20af 100644 --- a/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionService.cs +++ b/app/MindWork AI Studio/Tools/AssistantSessions/AssistantSessionService.cs @@ -113,6 +113,33 @@ public sealed class AssistantSessionService(MessageBus messageBus) return this.sessions.TryGetValue(key, out var session) ? CreateSnapshot(session) : null; } + /// + /// Tries to get and remove an inactive assistant session snapshot. + /// + /// + /// This method intentionally does not publish a change event. It is used when + /// a UI instance consumes a finished session exactly once and should keep the + /// restored result locally until the user leaves or resets the assistant. + /// + /// The assistant session key to look up and remove. + /// The removed inactive snapshot, or null when no inactive session exists. + public AssistantSessionSnapshot? TryTakeInactiveSnapshot(AssistantSessionKey key) + { + if (!this.sessions.TryGetValue(key, out var session)) + return null; + + AssistantSessionSnapshot snapshot; + lock (session.SyncRoot) + { + if (session.Status is AssistantSessionStatus.RUNNING or AssistantSessionStatus.CANCELING) + return null; + + snapshot = CreateSnapshotWithoutLock(session); + } + + return ((ICollection>)this.sessions).Remove(new(key, session)) ? snapshot : null; + } + /// /// Starts a new assistant session when no active session exists for the key. /// @@ -303,19 +330,29 @@ public sealed class AssistantSessionService(MessageBus messageBus) { lock (session.SyncRoot) { - return new() - { - SessionId = session.SessionId, - Key = session.Key, - Title = session.Title, - Status = session.Status, - StartedAt = session.StartedAt, - UpdatedAt = session.UpdatedAt, - FinishedAt = session.FinishedAt, - ErrorMessage = session.ErrorMessage, - ChatThread = session.ChatThread, - State = new Dictionary(session.State, StringComparer.Ordinal), - }; + return CreateSnapshotWithoutLock(session); } } -} \ No newline at end of file + + /// + /// Creates a copied, external snapshot while the caller already holds the session lock. + /// + /// The runtime session to copy. + /// A snapshot safe to send to UI components. + private static AssistantSessionSnapshot CreateSnapshotWithoutLock(AssistantSessionState session) + { + return new() + { + SessionId = session.SessionId, + Key = session.Key, + Title = session.Title, + Status = session.Status, + StartedAt = session.StartedAt, + UpdatedAt = session.UpdatedAt, + FinishedAt = session.FinishedAt, + ErrorMessage = session.ErrorMessage, + ChatThread = session.ChatThread, + State = new Dictionary(session.State, StringComparer.Ordinal), + }; + } +}