탭 전환 빈 대화 누적 방지: 저장 게이트 + 목록 노이즈 필터
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatSessionStateService.SaveCurrentConversation에 persistable-content 검사 추가 - 무의미한 빈 새 대화는 저장/탭 기억 대상에서 제외 - ChatWindow 대화 목록에서 빈 노이즈 항목 필터링 - ChatSessionStateServiceTests 회귀 추가 및 문서 이력(2026-04-04 15:48 KST) 동기화
This commit is contained in:
@@ -225,6 +225,32 @@ public class ChatSessionStateServiceTests
|
||||
session.GetConversationId("Code").Should().Be(conv.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveCurrentConversation_DoesNotPersistEmptyFreshConversation()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var storage = new ChatStorageService();
|
||||
var conv = session.EnsureCurrentConversation("Cowork");
|
||||
conv.Messages.Clear();
|
||||
conv.ExecutionEvents.Clear();
|
||||
conv.AgentRunHistory.Clear();
|
||||
conv.DraftQueueItems.Clear();
|
||||
conv.Preview = "";
|
||||
conv.WorkFolder = "";
|
||||
conv.SystemCommand = "";
|
||||
conv.Permission = null;
|
||||
conv.DataUsage = null;
|
||||
conv.OutputFormat = null;
|
||||
conv.Mood = null;
|
||||
conv.Category = ChatCategory.General;
|
||||
conv.Title = "새 대화";
|
||||
|
||||
session.SaveCurrentConversation(storage, "Cowork");
|
||||
|
||||
session.GetConversationId("Cowork").Should().BeNull();
|
||||
storage.Load(conv.Id).Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveCurrentConversation_RemembersConversationId_WhenOnlyAgentRunHistoryExists()
|
||||
{
|
||||
@@ -701,4 +727,54 @@ public class ChatSessionStateServiceTests
|
||||
session.GetConversationId("Code").Should().BeNull();
|
||||
session.GetConversationId("Chat").Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadOrCreateConversation_RestoresRunHistoryAndExecutionEventsAfterRestart()
|
||||
{
|
||||
var storage = new ChatStorageService();
|
||||
var settings = new SettingsService();
|
||||
var sessionA = new ChatSessionStateService();
|
||||
var conv = new ChatConversation
|
||||
{
|
||||
Id = $"conv-replay-{Guid.NewGuid():N}",
|
||||
Tab = "Code",
|
||||
Title = "restore test",
|
||||
Messages = [new ChatMessage { Role = "user", Content = "build it" }],
|
||||
ExecutionEvents =
|
||||
[
|
||||
new ChatExecutionEvent
|
||||
{
|
||||
RunId = "run-restore-1",
|
||||
Type = "ToolResult",
|
||||
ToolName = "build_run",
|
||||
Summary = "build ok",
|
||||
Timestamp = DateTime.Now
|
||||
}
|
||||
],
|
||||
AgentRunHistory =
|
||||
[
|
||||
new ChatAgentRunRecord
|
||||
{
|
||||
RunId = "run-restore-1",
|
||||
Status = "completed",
|
||||
Summary = "done",
|
||||
UpdatedAt = DateTime.Now
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
sessionA.SetCurrentConversation("Code", conv, storage);
|
||||
sessionA.SaveCurrentConversation(storage, "Code");
|
||||
sessionA.Save(settings);
|
||||
|
||||
var sessionB = new ChatSessionStateService();
|
||||
sessionB.Load(settings);
|
||||
var restored = sessionB.LoadOrCreateConversation("Code", storage, settings);
|
||||
|
||||
restored.Id.Should().Be(conv.Id);
|
||||
restored.Tab.Should().Be("Code");
|
||||
restored.ExecutionEvents.Should().ContainSingle();
|
||||
restored.AgentRunHistory.Should().ContainSingle();
|
||||
restored.AgentRunHistory[0].RunId.Should().Be("run-restore-1");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ public sealed class ChatSessionStateService
|
||||
{
|
||||
var normalizedTab = NormalizeTab(tab);
|
||||
var rememberedId = GetConversationId(normalizedTab);
|
||||
var hadRememberedConversation = !string.IsNullOrWhiteSpace(rememberedId);
|
||||
if (!string.IsNullOrWhiteSpace(rememberedId))
|
||||
{
|
||||
var loaded = storage.Load(rememberedId);
|
||||
@@ -115,6 +116,30 @@ public sealed class ChatSessionStateService
|
||||
RememberConversation(normalizedTab, null);
|
||||
}
|
||||
|
||||
if (!hadRememberedConversation)
|
||||
{
|
||||
var latestMeta = storage.LoadAllMeta()
|
||||
.Where(c => string.Equals(NormalizeTab(c.Tab), normalizedTab, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(c => c.Pinned)
|
||||
.ThenByDescending(c => c.UpdatedAt)
|
||||
.FirstOrDefault();
|
||||
if (latestMeta != null)
|
||||
{
|
||||
var loaded = storage.Load(latestMeta.Id);
|
||||
if (loaded != null)
|
||||
{
|
||||
loaded.Tab = normalizedTab;
|
||||
var normalized = NormalizeLoadedConversation(loaded);
|
||||
CurrentConversation = loaded;
|
||||
RememberConversation(normalizedTab, loaded.Id);
|
||||
if (normalized)
|
||||
try { storage.Save(loaded); } catch { }
|
||||
return loaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RememberConversation(normalizedTab, null);
|
||||
return CreateFreshConversation(normalizedTab, settings);
|
||||
}
|
||||
|
||||
@@ -209,13 +234,15 @@ public sealed class ChatSessionStateService
|
||||
var conv = CurrentConversation;
|
||||
if (conv == null) return;
|
||||
|
||||
try { storage.Save(conv); } catch { }
|
||||
var conversationTab = NormalizeTab(conv.Tab);
|
||||
if (conv.Messages.Count > 0
|
||||
|| (conv.ExecutionEvents?.Count ?? 0) > 0
|
||||
|| (conv.AgentRunHistory?.Count ?? 0) > 0
|
||||
|| (conv.DraftQueueItems?.Count ?? 0) > 0)
|
||||
RememberConversation(conversationTab, conv.Id);
|
||||
if (!HasPersistableContent(conv))
|
||||
{
|
||||
RememberConversation(conversationTab, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try { storage.Save(conv); } catch { }
|
||||
RememberConversation(conversationTab, conv.Id);
|
||||
}
|
||||
|
||||
public void ClearCurrentConversation(string tab)
|
||||
@@ -571,6 +598,26 @@ public sealed class ChatSessionStateService
|
||||
return "Chat";
|
||||
}
|
||||
|
||||
private static bool HasPersistableContent(ChatConversation conv)
|
||||
{
|
||||
if ((conv.Messages?.Count ?? 0) > 0) return true;
|
||||
if ((conv.ExecutionEvents?.Count ?? 0) > 0) return true;
|
||||
if ((conv.AgentRunHistory?.Count ?? 0) > 0) return true;
|
||||
if ((conv.DraftQueueItems?.Count ?? 0) > 0) return true;
|
||||
if (conv.Pinned) return true;
|
||||
if (!string.IsNullOrWhiteSpace(conv.WorkFolder)) return true;
|
||||
if (!string.IsNullOrWhiteSpace(conv.SystemCommand)) return true;
|
||||
if (!string.IsNullOrWhiteSpace(conv.ParentId)) return true;
|
||||
if (!string.IsNullOrWhiteSpace(conv.Permission)) return true;
|
||||
if (!string.IsNullOrWhiteSpace(conv.DataUsage)) return true;
|
||||
if (!string.IsNullOrWhiteSpace(conv.OutputFormat)) return true;
|
||||
if (!string.IsNullOrWhiteSpace(conv.Mood)) return true;
|
||||
if (!string.IsNullOrWhiteSpace(conv.Preview)) return true;
|
||||
if (!string.Equals(conv.Category, ChatCategory.General, StringComparison.OrdinalIgnoreCase)) return true;
|
||||
if (!string.Equals((conv.Title ?? "").Trim(), "새 대화", StringComparison.OrdinalIgnoreCase)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool NormalizeLoadedConversation(ChatConversation conversation)
|
||||
{
|
||||
var changed = false;
|
||||
|
||||
@@ -2756,6 +2756,16 @@ public partial class ChatWindow : Window
|
||||
|
||||
// 탭 필터 — 현재 활성 탭의 대화만 표시
|
||||
items = items.Where(i => string.Equals(i.Tab, _activeTab, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
// 탭 전환 과정에서 저장된 "빈 새 대화" 노이즈 항목은 목록에서 숨김
|
||||
items = items.Where(i =>
|
||||
i.Pinned
|
||||
|| !string.IsNullOrWhiteSpace(i.ParentId)
|
||||
|| !string.Equals((i.Title ?? "").Trim(), "새 대화", StringComparison.OrdinalIgnoreCase)
|
||||
|| !string.IsNullOrWhiteSpace(i.Preview)
|
||||
|| i.AgentRunCount > 0
|
||||
|| i.FailedAgentRunCount > 0
|
||||
|| !string.Equals(i.Category, ChatCategory.General, StringComparison.OrdinalIgnoreCase)
|
||||
).ToList();
|
||||
_failedConversationCount = items.Count(i => i.FailedAgentRunCount > 0);
|
||||
_runningConversationCount = items.Count(i => i.IsRunning);
|
||||
_spotlightConversationCount = items.Count(i => i.FailedAgentRunCount > 0 || i.AgentRunCount >= 3);
|
||||
|
||||
Reference in New Issue
Block a user