AX Agent 라이브 진행 표시 회귀 복구 및 본문 선택 유지 정리
상단 라이브 진행 카드를 이전 단계형 구조로 복구하고 스트리밍 중 현재 실행 이벤트가 본문 타임라인에 중복 표시되지 않도록 V2 렌더 컷오프를 다시 적용했습니다. 사용자 말풍선은 기존 마크다운 렌더로 되돌려 세로로 깨지던 표시를 해결하고, 어시스턴트 본문과 스트리밍 완료 본문은 계속 드래그 선택/복사가 가능하도록 유지했습니다. 또한 SkillRuntime, allowed_tools, 메인 루프 요청, 읽기 도구 조기 실행 준비, 스트리밍 도구 감지 같은 저신호 내부 문구를 추가 필터링해 화면 노이즈를 줄였습니다. 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_live_restore\\ -p:IntermediateOutputPath=obj\\verify_live_restore\\ 에서 경고 0 오류 0을 확인했습니다. 검증: AgentLoopCodeQualityTests, AgentStatusNarrativeCatalogTests, AgentProgressSummarySanitizerTests 필터로 dotnet test를 실행해 131개 테스트 통과를 확인했습니다.
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
# AX Commander
|
# AX Commander
|
||||||
|
|
||||||
|
- 업데이트: 2026-04-15 18:54 (KST)
|
||||||
|
- AX Agent 표시 회귀를 정리했습니다. `src/AxCopilot/Views/ChatWindow.V2LiveProgressPresentation.cs`, `src/AxCopilot/Views/ChatWindow.V2Rendering.cs`를 이전 라이브 진행 카드 구조로 되돌려 상단 라이브 카드가 다시 단계형으로 보이도록 복구했고, 스트리밍 중인 현재 실행 이벤트는 본문 타임라인에 중복으로 쌓이지 않도록 컷오프를 다시 적용했습니다.
|
||||||
|
- 본문 드래그 선택은 유지하되 사용자 말풍선 회귀는 되돌렸습니다. `src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs`, `src/AxCopilot/Views/ChatWindow.V2MessagePresentation.cs`에서 사용자 버블만 일반 마크다운 렌더로 복원해 세로로 깨지던 표시를 막고, 어시스턴트 본문과 스트리밍 완료 본문(`src/AxCopilot/Views/ChatWindow.ResponsePresentation.cs`)은 계속 드래그 선택/복사할 수 있게 유지했습니다.
|
||||||
|
- `src/AxCopilot/Services/Agent/AgentProgressSummarySanitizer.cs`에는 `SkillRuntime`, `allowed_tools`, 메인 루프 요청/스트리밍 감지 같은 저신호 문구를 추가로 걸러 본문과 라이브 카드에 내부성 로그가 다시 드러나지 않도록 보강했습니다.
|
||||||
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_live_restore\\ -p:IntermediateOutputPath=obj\\verify_live_restore\\` 경고 0 / 오류 0
|
||||||
|
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopCodeQualityTests|AgentStatusNarrativeCatalogTests|AgentProgressSummarySanitizerTests" -p:OutputPath=bin\\verify_live_restore_tests\\ -p:IntermediateOutputPath=obj\\verify_live_restore_tests\\` 통과 131
|
||||||
|
|
||||||
- 업데이트: 2026-04-15 18:30 (KST)
|
- 업데이트: 2026-04-15 18:30 (KST)
|
||||||
- Code 탭에서 동일 도구 호출이 같은 시그니처로 반복될 때 빠져나오지 못하던 루프를 보강했습니다. `src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs`에 일반 반복 시그니처 가드를 추가하고, `src/AxCopilot/Services/Agent/AgentLoopService.cs`가 읽기 전용 도구뿐 아니라 `build_run` 같은 실행 도구 반복도 감지해 다른 접근으로 전환하도록 정리했습니다.
|
- Code 탭에서 동일 도구 호출이 같은 시그니처로 반복될 때 빠져나오지 못하던 루프를 보강했습니다. `src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs`에 일반 반복 시그니처 가드를 추가하고, `src/AxCopilot/Services/Agent/AgentLoopService.cs`가 읽기 전용 도구뿐 아니라 `build_run` 같은 실행 도구 반복도 감지해 다른 접근으로 전환하도록 정리했습니다.
|
||||||
- `src/AxCopilot/Views/ChatWindow.V2LiveProgressPresentation.cs`, `src/AxCopilot/Views/ChatWindow.V2Rendering.cs`, `src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs`는 상단 라이브 진행 영역을 1~2줄 요약 카드로 축소하고, 실제 ToolCall/ToolResult 이력은 채팅 본문 타임라인에 계속 누적되도록 바꿨습니다. 내부 대기성 Thinking/LLM 대기 문구는 본문에서 더 공격적으로 숨겨 실행 이력이 덜 지저분하게 보이도록 조정했습니다.
|
- `src/AxCopilot/Views/ChatWindow.V2LiveProgressPresentation.cs`, `src/AxCopilot/Views/ChatWindow.V2Rendering.cs`, `src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs`는 상단 라이브 진행 영역을 1~2줄 요약 카드로 축소하고, 실제 ToolCall/ToolResult 이력은 채팅 본문 타임라인에 계속 누적되도록 바꿨습니다. 내부 대기성 Thinking/LLM 대기 문구는 본문에서 더 공격적으로 숨겨 실행 이력이 덜 지저분하게 보이도록 조정했습니다.
|
||||||
|
|||||||
@@ -1478,3 +1478,9 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫
|
|||||||
- 사용자에게 보이는 작업 설명도 `src/AxCopilot/Services/Agent/AgentStatusNarrativeCatalog.cs`에서 다시 정리했습니다. 코드 탐색, 수정, 실행, 문서화, 권한 대기 같은 단계가 더 짧고 친절한 한국어 문구로 노출되며, 대상 파일/명령/쿼리 같은 힌트는 detail 줄로 별도 노출됩니다.
|
- 사용자에게 보이는 작업 설명도 `src/AxCopilot/Services/Agent/AgentStatusNarrativeCatalog.cs`에서 다시 정리했습니다. 코드 탐색, 수정, 실행, 문서화, 권한 대기 같은 단계가 더 짧고 친절한 한국어 문구로 노출되며, 대상 파일/명령/쿼리 같은 힌트는 detail 줄로 별도 노출됩니다.
|
||||||
- 채팅 본문 드래그 복사도 지원합니다. `src/AxCopilot/Services/MarkdownRenderer.cs`에 선택 가능한 RichTextBox 기반 마크다운 렌더를 추가했고, `src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs`, `src/AxCopilot/Views/ChatWindow.V2MessagePresentation.cs`, `src/AxCopilot/Views/ChatWindow.ResponsePresentation.cs`가 Cowork/Code 본문에 이를 사용하도록 연결했습니다.
|
- 채팅 본문 드래그 복사도 지원합니다. `src/AxCopilot/Services/MarkdownRenderer.cs`에 선택 가능한 RichTextBox 기반 마크다운 렌더를 추가했고, `src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs`, `src/AxCopilot/Views/ChatWindow.V2MessagePresentation.cs`, `src/AxCopilot/Views/ChatWindow.ResponsePresentation.cs`가 Cowork/Code 본문에 이를 사용하도록 연결했습니다.
|
||||||
- 테스트는 `src/AxCopilot.Tests/Services/AgentLoopCodeQualityTests.cs`, `src/AxCopilot.Tests/Services/AgentStatusNarrativeCatalogTests.cs`, `src/AxCopilot.Tests/Services/AgentProgressSummarySanitizerTests.cs`를 갱신했고, `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_agent_ui_logs\\ -p:IntermediateOutputPath=obj\\verify_agent_ui_logs\\` 경고 0 / 오류 0, `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopCodeQualityTests|AgentStatusNarrativeCatalogTests|AgentProgressSummarySanitizerTests" -p:OutputPath=bin\\verify_agent_ui_logs_tests\\ -p:IntermediateOutputPath=obj\\verify_agent_ui_logs_tests\\` 131개 통과를 확인했습니다.
|
- 테스트는 `src/AxCopilot.Tests/Services/AgentLoopCodeQualityTests.cs`, `src/AxCopilot.Tests/Services/AgentStatusNarrativeCatalogTests.cs`, `src/AxCopilot.Tests/Services/AgentProgressSummarySanitizerTests.cs`를 갱신했고, `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_agent_ui_logs\\ -p:IntermediateOutputPath=obj\\verify_agent_ui_logs\\` 경고 0 / 오류 0, `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopCodeQualityTests|AgentStatusNarrativeCatalogTests|AgentProgressSummarySanitizerTests" -p:OutputPath=bin\\verify_agent_ui_logs_tests\\ -p:IntermediateOutputPath=obj\\verify_agent_ui_logs_tests\\` 131개 통과를 확인했습니다.
|
||||||
|
업데이트: 2026-04-15 18:54 (KST)
|
||||||
|
- AX Agent 라이브 진행 UI를 이전 구조로 복구했습니다. `src/AxCopilot/Views/ChatWindow.V2LiveProgressPresentation.cs`는 상단 요약 1~2줄 카드 대신 단계형 라이브 카드와 도구 진행 행을 다시 사용하고, `src/AxCopilot/Views/ChatWindow.V2Rendering.cs`는 스트리밍 중 현재 실행 이벤트를 본문 타임라인에서 잠시 제외해 상단 카드와 본문이 중복 표시되지 않도록 원래 흐름으로 되돌렸습니다.
|
||||||
|
- 본문 드래그 선택은 유지하되 사용자 버블 회귀는 제거했습니다. `src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs`, `src/AxCopilot/Views/ChatWindow.V2MessagePresentation.cs`에서 사용자 메시지 렌더만 기존 `MarkdownRenderer.Render(...)`로 되돌렸고, 어시스턴트 본문과 스트리밍 완료 본문은 계속 `RenderSelectable(...)`를 사용해 드래그 복사가 가능하도록 유지했습니다.
|
||||||
|
- `src/AxCopilot/Services/Agent/AgentProgressSummarySanitizer.cs`는 `SkillRuntime`, `allowed_tools`, 메인 루프 요청, 읽기 도구 조기 실행 준비, 스트리밍 도구 감지 등 저신호 내부 문구를 추가로 필터링해 본문/라이브 카드에 내부성 로그가 다시 노출되지 않도록 보강했습니다.
|
||||||
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_live_restore\\ -p:IntermediateOutputPath=obj\\verify_live_restore\\` 경고 0 / 오류 0
|
||||||
|
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopCodeQualityTests|AgentStatusNarrativeCatalogTests|AgentProgressSummarySanitizerTests" -p:OutputPath=bin\\verify_live_restore_tests\\ -p:IntermediateOutputPath=obj\\verify_live_restore_tests\\` 통과 131
|
||||||
|
|||||||
@@ -5,22 +5,22 @@ namespace AxCopilot.Services.Agent;
|
|||||||
internal static class AgentProgressSummarySanitizer
|
internal static class AgentProgressSummarySanitizer
|
||||||
{
|
{
|
||||||
private static readonly Regex s_previousToolCallRegex =
|
private static readonly Regex s_previousToolCallRegex =
|
||||||
new(@"\[이전 도구 호출[^\]\n]*(?:\]|$)", RegexOptions.Compiled);
|
new("\\[?(?:\\uC774\\uC804 \\uB3C4\\uAD6C \\uC778\\uCD9C)[^\\]\\n]*(?:\\]|$)", RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly Regex s_previousToolBundleRegex =
|
private static readonly Regex s_previousToolBundleRegex =
|
||||||
new(@"\[이전 도구 호출 묶음 축약[^\]\n]*(?:\]|$)", RegexOptions.Compiled);
|
new("\\[?(?:\\uC774\\uC804 \\uB3C4\\uAD6C \\uC778\\uCD9C \\uBB36\\uC74C \\uCD95\\uC57D)[^\\]\\n]*(?:\\]|$)", RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly Regex s_listPrefixRegex =
|
private static readonly Regex s_listPrefixRegex =
|
||||||
new(@"^\s*(?:\d+\s*[\.\)]|[-*•])\s*", RegexOptions.Compiled);
|
new("^\\s*(?:\\d+\\s*[\\.\\)]|[-*\\u2022])\\s*", RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly Regex s_punctuationOnlyRegex =
|
private static readonly Regex s_punctuationOnlyRegex =
|
||||||
new(@"^[\[\]\(\)\{\}/\\<>\.\-_:;,`'""*]+$", RegexOptions.Compiled);
|
new("^[\\[\\]\\(\\)\\{\\}/\\\\<>\\.\\-_:;,`'\"*]+$", RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly Regex s_numericFragmentRegex =
|
private static readonly Regex s_numericFragmentRegex =
|
||||||
new(@"^\d+\s*[\.\)]?$", RegexOptions.Compiled);
|
new("^\\d+\\s*[\\.\\)]?$", RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly Regex s_toolTokenRegex =
|
private static readonly Regex s_toolTokenRegex =
|
||||||
new(@"^[A-Za-z][A-Za-z0-9_\-]{1,40}\]?$", RegexOptions.Compiled);
|
new("^[A-Za-z][A-Za-z0-9_\\-]{1,40}\\]?$", RegexOptions.Compiled);
|
||||||
|
|
||||||
public static string NormalizeThinkingSummary(string? summary, string? toolName = null, int maxLength = 0)
|
public static string NormalizeThinkingSummary(string? summary, string? toolName = null, int maxLength = 0)
|
||||||
{
|
{
|
||||||
@@ -49,10 +49,10 @@ internal static class AgentProgressSummarySanitizer
|
|||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|
||||||
var normalized = string.Join(" ", cleanedLines);
|
var normalized = string.Join(" ", cleanedLines);
|
||||||
normalized = Regex.Replace(normalized, @"\s{2,}", " ").Trim();
|
normalized = Regex.Replace(normalized, "\\s{2,}", " ").Trim();
|
||||||
|
|
||||||
if (maxLength > 0 && normalized.Length > maxLength)
|
if (maxLength > 0 && normalized.Length > maxLength)
|
||||||
normalized = normalized[..maxLength].TrimEnd() + "…";
|
normalized = normalized[..maxLength].TrimEnd() + "...";
|
||||||
|
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
@@ -64,22 +64,27 @@ internal static class AgentProgressSummarySanitizer
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
var lower = normalized.ToLowerInvariant();
|
var lower = normalized.ToLowerInvariant();
|
||||||
return lower.Contains("모델에 요청하는 중")
|
return lower.Contains("\uBAA8\uB378\uC5D0 \uC694\uCCAD\uD558\uB294 \uC911")
|
||||||
|| lower.Contains("모델 첫 응답")
|
|| lower.Contains("\uBAA8\uB378 \uCCAB \uC751\uB2F5")
|
||||||
|| lower.Contains("계속 기다리는 중")
|
|| lower.Contains("\uACC4\uC18D \uAE30\uB2E4\uB9AC\uB294 \uC911")
|
||||||
|| lower.Contains("응답을 기다리는 중")
|
|| lower.Contains("\uC751\uB2F5\uC744 \uAE30\uB2E4\uB9AC\uB294 \uC911")
|
||||||
|| lower.Contains("스트리밍 중간 응답")
|
|| lower.Contains("\uC2A4\uD2B8\uB9AC\uBC0D \uC911\uAC04 \uC751\uB2F5")
|
||||||
|| lower.Contains("일시적 llm 오류")
|
|| lower.Contains("\uC77C\uC2DC\uC801 llm \uC624\uB958")
|
||||||
|| lower.Contains("gemini 무료 티어 대기")
|
|| lower.Contains("gemini \uBB34\uB8CC \uD2F0\uC5B4 \uB300\uAE30")
|
||||||
|| lower.Contains("도구명 정규화 적용")
|
|| lower.Contains("\uB3C4\uAD6C\uBA85 \uC815\uADDC\uD654 \uC801\uC6A9")
|
||||||
|
|| lower.Contains("skillruntime")
|
||||||
|
|| lower.Contains("allowed_tools")
|
||||||
|
|| lower.Contains("\uBA54\uC778 \uB8E8\uD504")
|
||||||
|
|| lower.Contains("\uC77D\uAE30 \uB3C4\uAD6C \uC870\uAE30 \uC2E4\uD589 \uC900\uBE44")
|
||||||
|
|| lower.Contains("\uC2A4\uD2B8\uB9AC\uBC0D \uB3C4\uAD6C \uAC10\uC9C0")
|
||||||
|| lower.Contains("[agentloopwait]");
|
|| lower.Contains("[agentloopwait]");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string CleanThinkingLine(string line)
|
private static string CleanThinkingLine(string line)
|
||||||
{
|
{
|
||||||
var cleaned = s_listPrefixRegex.Replace(line.Trim(), string.Empty).Trim();
|
var cleaned = s_listPrefixRegex.Replace(line.Trim(), string.Empty).Trim();
|
||||||
cleaned = cleaned.Trim('*', '-', '•', '[', ']', '(', ')', '{', '}', ':', ';');
|
cleaned = cleaned.Trim('*', '-', '\u2022', '[', ']', '(', ')', '{', '}', ':', ';');
|
||||||
cleaned = Regex.Replace(cleaned, @"\s{2,}", " ").Trim();
|
cleaned = Regex.Replace(cleaned, "\\s{2,}", " ").Trim();
|
||||||
return cleaned;
|
return cleaned;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +102,7 @@ internal static class AgentProgressSummarySanitizer
|
|||||||
if (s_toolTokenRegex.IsMatch(line))
|
if (s_toolTokenRegex.IsMatch(line))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (line.Length <= 3 && line.Any(ch => char.IsPunctuation(ch)))
|
if (line.Length <= 3 && line.Any(char.IsPunctuation))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (line.Length <= 4 && line.All(ch => char.IsPunctuation(ch) || char.IsWhiteSpace(ch)))
|
if (line.Length <= 4 && line.All(ch => char.IsPunctuation(ch) || char.IsWhiteSpace(ch)))
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public partial class ChatWindow
|
|||||||
MarkdownRenderer.EnableFilePathHighlight =
|
MarkdownRenderer.EnableFilePathHighlight =
|
||||||
(System.Windows.Application.Current as App)?.SettingsService?.Settings.Llm.EnableFilePathHighlight ?? true;
|
(System.Windows.Application.Current as App)?.SettingsService?.Settings.Llm.EnableFilePathHighlight ?? true;
|
||||||
MarkdownRenderer.EnableCodeSymbolHighlight = true;
|
MarkdownRenderer.EnableCodeSymbolHighlight = true;
|
||||||
bubble.Child = MarkdownRenderer.RenderSelectable(content, primaryText, secondaryText, accentBrush, userCodeBgBrush);
|
bubble.Child = MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, userCodeBgBrush);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
@@ -15,7 +16,7 @@ public partial class ChatWindow
|
|||||||
private DateTime _v2LiveStartTime;
|
private DateTime _v2LiveStartTime;
|
||||||
private TextBlock? _v2LiveElapsedText;
|
private TextBlock? _v2LiveElapsedText;
|
||||||
|
|
||||||
/// <summary>V2: 스트리밍 시작 시 상단 라이브 진행 요약 카드를 생성합니다.</summary>
|
/// <summary>V2: 스트리밍 시작 시 라이브 진행 컨테이너 생성</summary>
|
||||||
private void ShowAgentLiveCardV2(string runTab)
|
private void ShowAgentLiveCardV2(string runTab)
|
||||||
{
|
{
|
||||||
if (MessageList == null) return;
|
if (MessageList == null) return;
|
||||||
@@ -24,13 +25,11 @@ public partial class ChatWindow
|
|||||||
RemoveAgentLiveCardV2(animated: false);
|
RemoveAgentLiveCardV2(animated: false);
|
||||||
|
|
||||||
_v2LiveStartTime = DateTime.UtcNow;
|
_v2LiveStartTime = DateTime.UtcNow;
|
||||||
|
_v2LiveToolCards.Clear();
|
||||||
|
_v2LastLiveToolCallId = null;
|
||||||
|
|
||||||
var msgMaxWidth = GetMessageMaxWidth();
|
var msgMaxWidth = GetMessageMaxWidth();
|
||||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
|
||||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||||
var hintBg = TryFindResource("HintBackground") as Brush
|
|
||||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
|
||||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
|
||||||
|
|
||||||
_v2LiveContainer = new StackPanel
|
_v2LiveContainer = new StackPanel
|
||||||
{
|
{
|
||||||
@@ -52,12 +51,8 @@ public partial class ChatWindow
|
|||||||
{
|
{
|
||||||
var animState = new ChatIconAnimState
|
var animState = new ChatIconAnimState
|
||||||
{
|
{
|
||||||
Host = liveIconHost,
|
Host = liveIconHost, Canvas = canvas, Pixels = livePixels,
|
||||||
Canvas = canvas,
|
Glows = liveGlows, Rotate = liveRotate, Scale = liveScale,
|
||||||
Pixels = livePixels,
|
|
||||||
Glows = liveGlows,
|
|
||||||
Rotate = liveRotate,
|
|
||||||
Scale = liveScale,
|
|
||||||
IsRandomMode = _settings.Settings.Launcher.EnableChatIconRandomAnimation,
|
IsRandomMode = _settings.Settings.Launcher.EnableChatIconRandomAnimation,
|
||||||
};
|
};
|
||||||
StartChatIconAnimation(animState);
|
StartChatIconAnimation(animState);
|
||||||
@@ -83,7 +78,7 @@ public partial class ChatWindow
|
|||||||
Text = "",
|
Text = "",
|
||||||
FontSize = 10,
|
FontSize = 10,
|
||||||
Foreground = secondaryText,
|
Foreground = secondaryText,
|
||||||
Opacity = 0.7,
|
Opacity = 0.70,
|
||||||
HorizontalAlignment = HorizontalAlignment.Right,
|
HorizontalAlignment = HorizontalAlignment.Right,
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
};
|
};
|
||||||
@@ -92,39 +87,6 @@ public partial class ChatWindow
|
|||||||
|
|
||||||
_v2LiveContainer.Children.Add(headerGrid);
|
_v2LiveContainer.Children.Add(headerGrid);
|
||||||
|
|
||||||
var summaryCard = new Border
|
|
||||||
{
|
|
||||||
Background = hintBg,
|
|
||||||
BorderBrush = borderBrush,
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
CornerRadius = new CornerRadius(12),
|
|
||||||
Padding = new Thickness(12, 10, 12, 10),
|
|
||||||
};
|
|
||||||
|
|
||||||
var summaryStack = new StackPanel();
|
|
||||||
_v2LiveStatusText = new TextBlock
|
|
||||||
{
|
|
||||||
FontSize = 12.5,
|
|
||||||
FontWeight = FontWeights.SemiBold,
|
|
||||||
Foreground = primaryText,
|
|
||||||
TextWrapping = TextWrapping.Wrap,
|
|
||||||
};
|
|
||||||
summaryStack.Children.Add(_v2LiveStatusText);
|
|
||||||
|
|
||||||
_v2LiveDetailText = new TextBlock
|
|
||||||
{
|
|
||||||
FontSize = 10.5,
|
|
||||||
Foreground = secondaryText,
|
|
||||||
Opacity = 0.86,
|
|
||||||
TextWrapping = TextWrapping.Wrap,
|
|
||||||
Margin = new Thickness(0, 4, 0, 0),
|
|
||||||
};
|
|
||||||
summaryStack.Children.Add(_v2LiveDetailText);
|
|
||||||
summaryCard.Child = summaryStack;
|
|
||||||
_v2LiveContainer.Children.Add(summaryCard);
|
|
||||||
|
|
||||||
ApplyV2LiveNarrative(AgentStatusNarrativeCatalog.BuildInitial(runTab));
|
|
||||||
|
|
||||||
AddTranscriptElement(_v2LiveContainer);
|
AddTranscriptElement(_v2LiveContainer);
|
||||||
ForceScrollToEnd();
|
ForceScrollToEnd();
|
||||||
|
|
||||||
@@ -138,41 +100,225 @@ public partial class ChatWindow
|
|||||||
_v2LiveElapsedTimer.Start();
|
_v2LiveElapsedTimer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>V2: 라이브 카드는 1~2줄 요약만 유지하고, 상세 실행 이력은 본문 타임라인에 누적합니다.</summary>
|
/// <summary>V2: 에이전트 이벤트 수신 시 라이브 카드 업데이트</summary>
|
||||||
private void UpdateAgentLiveCardV2(AgentEvent agentEvent)
|
private void UpdateAgentLiveCardV2(AgentEvent agentEvent)
|
||||||
{
|
{
|
||||||
if (_v2LiveContainer == null)
|
if (_v2LiveContainer == null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
if (agentEvent.Type == AgentEventType.Thinking
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||||
&& !string.Equals(agentEvent.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase)
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||||
&& !string.Equals(agentEvent.ToolName, "context_compaction", StringComparison.OrdinalIgnoreCase)
|
var msgMaxWidth = GetMessageMaxWidth();
|
||||||
&& AgentProgressSummarySanitizer.IsLowSignalStatusSummary(agentEvent.Summary, agentEvent.ToolName))
|
|
||||||
|
switch (agentEvent.Type)
|
||||||
{
|
{
|
||||||
return;
|
case AgentEventType.ToolCall:
|
||||||
}
|
{
|
||||||
|
var (icon, iconColor) = GetV2ToolIcon(agentEvent.ToolName);
|
||||||
|
var toolId = $"{agentEvent.ToolName}_{agentEvent.Timestamp.Ticks}";
|
||||||
|
_v2LastLiveToolCallId = toolId;
|
||||||
|
|
||||||
ApplyV2LiveNarrative(AgentStatusNarrativeCatalog.BuildFromEvent(agentEvent, _activeTab));
|
var outerGrid = new Grid { Margin = new Thickness(0, 2, 0, 2) };
|
||||||
|
outerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||||
|
outerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||||
|
|
||||||
|
var accentColor = ResolveLiveProgressAccentColor(
|
||||||
|
TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue);
|
||||||
|
var pulseLine = new Border
|
||||||
|
{
|
||||||
|
Width = 2,
|
||||||
|
Background = new SolidColorBrush(Color.FromArgb(0x80, accentColor.R, accentColor.G, accentColor.B)),
|
||||||
|
CornerRadius = new CornerRadius(1),
|
||||||
|
Margin = new Thickness(12, 0, 8, 0),
|
||||||
|
};
|
||||||
|
var pulseAnim = new DoubleAnimation(0.4, 1.0, TimeSpan.FromMilliseconds(800))
|
||||||
|
{
|
||||||
|
AutoReverse = true,
|
||||||
|
RepeatBehavior = RepeatBehavior.Forever,
|
||||||
|
EasingFunction = new SineEase(),
|
||||||
|
};
|
||||||
|
pulseLine.BeginAnimation(UIElement.OpacityProperty, pulseAnim);
|
||||||
|
Grid.SetColumn(pulseLine, 0);
|
||||||
|
outerGrid.Children.Add(pulseLine);
|
||||||
|
|
||||||
|
var card = new Border
|
||||||
|
{
|
||||||
|
Background = new SolidColorBrush(Color.FromArgb(0x1C, accentColor.R, accentColor.G, accentColor.B)),
|
||||||
|
BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, accentColor.R, accentColor.G, accentColor.B)),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(8),
|
||||||
|
Padding = new Thickness(10, 6, 10, 6),
|
||||||
|
Tag = "pending",
|
||||||
|
};
|
||||||
|
Grid.SetColumn(card, 1);
|
||||||
|
|
||||||
|
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||||
|
sp.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = icon,
|
||||||
|
FontSize = 12,
|
||||||
|
Foreground = iconColor,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = new Thickness(0, 0, 6, 0),
|
||||||
|
});
|
||||||
|
sp.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = GetV2ToolDisplayName(agentEvent.ToolName),
|
||||||
|
FontSize = 11,
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = primaryText,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
});
|
||||||
|
if (!string.IsNullOrWhiteSpace(agentEvent.FilePath))
|
||||||
|
{
|
||||||
|
sp.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = TruncateFilePath(agentEvent.FilePath, 50),
|
||||||
|
FontSize = 10,
|
||||||
|
Foreground = secondaryText,
|
||||||
|
Opacity = 0.7,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = new Thickness(8, 0, 0, 0),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
sp.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "...",
|
||||||
|
FontSize = 11,
|
||||||
|
Foreground = secondaryText,
|
||||||
|
Opacity = 0.5,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = new Thickness(6, 0, 0, 0),
|
||||||
|
});
|
||||||
|
card.Child = sp;
|
||||||
|
outerGrid.Children.Add(card);
|
||||||
|
|
||||||
|
_v2LiveToolCards[toolId] = card;
|
||||||
|
_v2LiveContainer.Children.Add(outerGrid);
|
||||||
AutoScrollIfNeeded();
|
AutoScrollIfNeeded();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyV2LiveNarrative(AgentStatusNarrative narrative)
|
case AgentEventType.ToolResult:
|
||||||
{
|
{
|
||||||
if (_v2LiveStatusText != null)
|
if (_v2LastLiveToolCallId != null && _v2LiveToolCards.TryGetValue(_v2LastLiveToolCallId, out var pendingCard))
|
||||||
_v2LiveStatusText.Text = narrative.Message;
|
|
||||||
|
|
||||||
if (_v2LiveDetailText == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(narrative.Detail))
|
|
||||||
{
|
{
|
||||||
_v2LiveDetailText.Text = string.Empty;
|
var isSuccess = agentEvent.Success;
|
||||||
_v2LiveDetailText.Visibility = Visibility.Collapsed;
|
var statusIcon = isSuccess ? "\uE73E" : "\uE711";
|
||||||
return;
|
var statusColor = isSuccess
|
||||||
|
? new SolidColorBrush(Color.FromRgb(0x66, 0xBB, 0x6A))
|
||||||
|
: new SolidColorBrush(Color.FromRgb(0xEF, 0x53, 0x50));
|
||||||
|
|
||||||
|
pendingCard.Background = isSuccess
|
||||||
|
? new SolidColorBrush(Color.FromArgb(0x0A, 0x66, 0xBB, 0x6A))
|
||||||
|
: new SolidColorBrush(Color.FromArgb(0x0A, 0xEF, 0x53, 0x50));
|
||||||
|
pendingCard.BorderBrush = isSuccess
|
||||||
|
? new SolidColorBrush(Color.FromArgb(0x30, 0x66, 0xBB, 0x6A))
|
||||||
|
: new SolidColorBrush(Color.FromArgb(0x30, 0xEF, 0x53, 0x50));
|
||||||
|
pendingCard.Tag = "complete";
|
||||||
|
|
||||||
|
if (pendingCard.Child is StackPanel sp)
|
||||||
|
{
|
||||||
|
if (sp.Children.Count > 0 && sp.Children[^1] is TextBlock lastTb && lastTb.Text == "...")
|
||||||
|
sp.Children.RemoveAt(sp.Children.Count - 1);
|
||||||
|
|
||||||
|
sp.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = statusIcon,
|
||||||
|
FontFamily = s_segoeIconFont,
|
||||||
|
FontSize = 11,
|
||||||
|
Foreground = statusColor,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = new Thickness(6, 0, 0, 0),
|
||||||
|
});
|
||||||
|
|
||||||
|
var elapsed = NormalizeProgressElapsedMs(agentEvent.ElapsedMs);
|
||||||
|
if (elapsed > 0)
|
||||||
|
{
|
||||||
|
sp.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"{elapsed / 1000.0:F1}s",
|
||||||
|
FontSize = 10,
|
||||||
|
Foreground = secondaryText,
|
||||||
|
Opacity = 0.6,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = new Thickness(4, 0, 0, 0),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_v2LiveDetailText.Text = narrative.Detail;
|
var parent = pendingCard.Parent as Grid;
|
||||||
_v2LiveDetailText.Visibility = Visibility.Visible;
|
if (parent?.Children[0] is Border pulseLine)
|
||||||
|
{
|
||||||
|
pulseLine.BeginAnimation(UIElement.OpacityProperty, null);
|
||||||
|
pulseLine.Opacity = 1;
|
||||||
|
pulseLine.Background = isSuccess
|
||||||
|
? new SolidColorBrush(Color.FromArgb(0x60, 0x66, 0xBB, 0x6A))
|
||||||
|
: new SolidColorBrush(Color.FromArgb(0x60, 0xEF, 0x53, 0x50));
|
||||||
|
}
|
||||||
|
|
||||||
|
var cardToCollapse = pendingCard;
|
||||||
|
var outerGridToCollapse = parent;
|
||||||
|
var collapseTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1200) };
|
||||||
|
collapseTimer.Tick += (_, _) =>
|
||||||
|
{
|
||||||
|
collapseTimer.Stop();
|
||||||
|
if (cardToCollapse == null) return;
|
||||||
|
var padAnim = new ThicknessAnimation(
|
||||||
|
cardToCollapse.Padding,
|
||||||
|
new Thickness(8, 2, 8, 2),
|
||||||
|
TimeSpan.FromMilliseconds(200))
|
||||||
|
{ EasingFunction = new QuadraticEase() };
|
||||||
|
var opAnim = new DoubleAnimation(1.0, 0.55, TimeSpan.FromMilliseconds(200))
|
||||||
|
{ EasingFunction = new QuadraticEase() };
|
||||||
|
cardToCollapse.BeginAnimation(Border.PaddingProperty, padAnim);
|
||||||
|
cardToCollapse.BeginAnimation(UIElement.OpacityProperty, opAnim);
|
||||||
|
if (outerGridToCollapse != null)
|
||||||
|
outerGridToCollapse.Margin = new Thickness(0, 1, 0, 1);
|
||||||
|
};
|
||||||
|
collapseTimer.Start();
|
||||||
|
|
||||||
|
_v2LastLiveToolCallId = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case AgentEventType.Thinking:
|
||||||
|
{
|
||||||
|
var thinkText = AgentProgressSummarySanitizer.NormalizeThinkingSummary(
|
||||||
|
agentEvent.Summary,
|
||||||
|
agentEvent.ToolName,
|
||||||
|
maxLength: 100);
|
||||||
|
if (string.IsNullOrWhiteSpace(thinkText)) break;
|
||||||
|
|
||||||
|
var thinkRow = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
Margin = new Thickness(22, 2, 0, 2),
|
||||||
|
};
|
||||||
|
thinkRow.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "\uE915",
|
||||||
|
FontFamily = s_segoeIconFont,
|
||||||
|
FontSize = 10,
|
||||||
|
Foreground = new SolidColorBrush(Color.FromRgb(0x59, 0xA5, 0xF5)),
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = new Thickness(0, 0, 4, 0),
|
||||||
|
});
|
||||||
|
thinkRow.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = thinkText,
|
||||||
|
FontSize = 10.5,
|
||||||
|
FontStyle = FontStyles.Italic,
|
||||||
|
Foreground = secondaryText,
|
||||||
|
Opacity = 0.82,
|
||||||
|
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||||
|
MaxWidth = msgMaxWidth - 60,
|
||||||
|
});
|
||||||
|
_v2LiveContainer.Children.Add(thinkRow);
|
||||||
|
AutoScrollIfNeeded();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>V2: 스트리밍 종료 시 라이브 카드 제거</summary>
|
/// <summary>V2: 스트리밍 종료 시 라이브 카드 제거</summary>
|
||||||
@@ -181,8 +327,6 @@ public partial class ChatWindow
|
|||||||
_v2LiveElapsedTimer?.Stop();
|
_v2LiveElapsedTimer?.Stop();
|
||||||
_v2LiveElapsedTimer = null;
|
_v2LiveElapsedTimer = null;
|
||||||
_v2LiveElapsedText = null;
|
_v2LiveElapsedText = null;
|
||||||
_v2LiveStatusText = null;
|
|
||||||
_v2LiveDetailText = null;
|
|
||||||
|
|
||||||
if (_v2LiveContainer == null) return;
|
if (_v2LiveContainer == null) return;
|
||||||
|
|
||||||
@@ -190,6 +334,8 @@ public partial class ChatWindow
|
|||||||
|
|
||||||
var toRemove = _v2LiveContainer;
|
var toRemove = _v2LiveContainer;
|
||||||
_v2LiveContainer = null;
|
_v2LiveContainer = null;
|
||||||
|
_v2LiveToolCards.Clear();
|
||||||
|
_v2LastLiveToolCallId = null;
|
||||||
|
|
||||||
if (animated && ContainsTranscriptElement(toRemove))
|
if (animated && ContainsTranscriptElement(toRemove))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ public partial class ChatWindow
|
|||||||
MarkdownRenderer.EnableFilePathHighlight =
|
MarkdownRenderer.EnableFilePathHighlight =
|
||||||
(System.Windows.Application.Current as App)?.SettingsService?.Settings.Llm.EnableFilePathHighlight ?? true;
|
(System.Windows.Application.Current as App)?.SettingsService?.Settings.Llm.EnableFilePathHighlight ?? true;
|
||||||
MarkdownRenderer.EnableCodeSymbolHighlight = true;
|
MarkdownRenderer.EnableCodeSymbolHighlight = true;
|
||||||
bubble.Child = MarkdownRenderer.RenderSelectable(content, primaryText, secondaryText, accentBrush, hintBg);
|
bubble.Child = MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, hintBg);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,16 +14,14 @@ namespace AxCopilot.Views;
|
|||||||
|
|
||||||
public partial class ChatWindow
|
public partial class ChatWindow
|
||||||
{
|
{
|
||||||
// ─── V2 렌더 상태 ───────────────────────────────────────────────────
|
|
||||||
private string? _v2LastRenderedConversationId;
|
private string? _v2LastRenderedConversationId;
|
||||||
private int _v2LastRenderedMessageCount;
|
private int _v2LastRenderedMessageCount;
|
||||||
private int _v2LastRenderedEventCount;
|
private int _v2LastRenderedEventCount;
|
||||||
private readonly List<string> _v2LastRenderedKeys = new();
|
private readonly List<string> _v2LastRenderedKeys = new();
|
||||||
|
|
||||||
// V2 라이브 프로그레스 상태
|
|
||||||
private StackPanel? _v2LiveContainer;
|
private StackPanel? _v2LiveContainer;
|
||||||
private TextBlock? _v2LiveStatusText;
|
private readonly Dictionary<string, Border> _v2LiveToolCards = new();
|
||||||
private TextBlock? _v2LiveDetailText;
|
private string? _v2LastLiveToolCallId;
|
||||||
|
|
||||||
private void RenderMessagesV2(
|
private void RenderMessagesV2(
|
||||||
ChatConversation conv,
|
ChatConversation conv,
|
||||||
@@ -37,7 +35,6 @@ public partial class ChatWindow
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 대화 전환 감지 → 캐시 초기화
|
|
||||||
if (!string.Equals(_v2LastRenderedConversationId, conv.Id, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(_v2LastRenderedConversationId, conv.Id, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_v2LastRenderedConversationId = conv.Id;
|
_v2LastRenderedConversationId = conv.Id;
|
||||||
@@ -47,31 +44,21 @@ public partial class ChatWindow
|
|||||||
_elementCache.Clear();
|
_elementCache.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ★ 패널이 비어있는데 V2 캐시가 남아있는 경우 강제 리셋
|
|
||||||
// 탭 전환 시 ClearTranscriptElements()로 패널이 비워지지만
|
|
||||||
// 빈 대화 탭을 거치면 V2가 호출되지 않아 캐시가 잔류하는 버그 방지
|
|
||||||
if (GetTranscriptElementCount() == 0 && _v2LastRenderedKeys.Count > 0)
|
if (GetTranscriptElementCount() == 0 && _v2LastRenderedKeys.Count > 0)
|
||||||
{
|
{
|
||||||
LogService.Info($"[V2Render] CACHE STALE RESET: panel=0 but cachedKeys={_v2LastRenderedKeys.Count}, forcing full rebuild");
|
LogService.Debug($"[V2Render] CACHE STALE RESET: panel=0 but cachedKeys={_v2LastRenderedKeys.Count}, forcing full rebuild");
|
||||||
_v2LastRenderedKeys.Clear();
|
_v2LastRenderedKeys.Clear();
|
||||||
_v2LastRenderedMessageCount = 0;
|
_v2LastRenderedMessageCount = 0;
|
||||||
_v2LastRenderedEventCount = 0;
|
_v2LastRenderedEventCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 통합 타임라인 빌드 (메시지 + 이벤트를 시간순 병합)
|
|
||||||
var timeline = BuildV2Timeline(visibleMessages, visibleEvents);
|
var timeline = BuildV2Timeline(visibleMessages, visibleEvents);
|
||||||
|
|
||||||
LogService.Debug($"[V2Render] timeline={timeline.Count}, msgs={visibleMessages.Count}, evts={visibleEvents.Count}, convId={conv.Id[..Math.Min(8, conv.Id.Length)]}, caller={caller}");
|
LogService.Debug($"[V2Render] timeline={timeline.Count}, msgs={visibleMessages.Count}, evts={visibleEvents.Count}, convId={conv.Id[..Math.Min(8, conv.Id.Length)]}, caller={caller}");
|
||||||
|
|
||||||
// 새 키 목록 생성
|
|
||||||
var newKeys = new List<string>(timeline.Count);
|
var newKeys = new List<string>(timeline.Count);
|
||||||
foreach (var item in timeline)
|
foreach (var item in timeline)
|
||||||
newKeys.Add(item.Key);
|
newKeys.Add(item.Key);
|
||||||
|
|
||||||
// 인크리멘탈 렌더 시도: 기존 키가 새 키의 접두사인 경우 추가분만 렌더
|
|
||||||
// ★ 패널에 실제 엘리먼트가 있어야 인크리멘탈 가능 —
|
|
||||||
// 탭 전환 시 ClearTranscriptElements()로 패널이 비워지지만
|
|
||||||
// V2 캐시(_v2LastRenderedKeys)는 유지되어 "이미 렌더됨"으로 오판하는 버그 방지
|
|
||||||
var actualElementCount = GetTranscriptElementCount();
|
var actualElementCount = GetTranscriptElementCount();
|
||||||
var canIncremental = _v2LastRenderedKeys.Count > 0
|
var canIncremental = _v2LastRenderedKeys.Count > 0
|
||||||
&& actualElementCount > 0
|
&& actualElementCount > 0
|
||||||
@@ -82,7 +69,6 @@ public partial class ChatWindow
|
|||||||
|
|
||||||
if (canIncremental)
|
if (canIncremental)
|
||||||
{
|
{
|
||||||
// 라이브 컨테이너가 있으면 임시 제거 (맨 끝에 다시 추가)
|
|
||||||
if (_v2LiveContainer != null && ContainsTranscriptElement(_v2LiveContainer))
|
if (_v2LiveContainer != null && ContainsTranscriptElement(_v2LiveContainer))
|
||||||
RemoveTranscriptElement(_v2LiveContainer);
|
RemoveTranscriptElement(_v2LiveContainer);
|
||||||
|
|
||||||
@@ -99,13 +85,11 @@ public partial class ChatWindow
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 라이브 컨테이너 재삽입
|
|
||||||
if (_isStreaming)
|
if (_isStreaming)
|
||||||
EnsureAgentLiveCardVisible(_activeTab);
|
EnsureAgentLiveCardVisible(_activeTab);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 전체 재빌드
|
|
||||||
ClearTranscriptElements();
|
ClearTranscriptElements();
|
||||||
|
|
||||||
foreach (var item in timeline)
|
foreach (var item in timeline)
|
||||||
@@ -121,7 +105,6 @@ public partial class ChatWindow
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 라이브 컨테이너 재삽입
|
|
||||||
if (_isStreaming)
|
if (_isStreaming)
|
||||||
EnsureAgentLiveCardVisible(_activeTab);
|
EnsureAgentLiveCardVisible(_activeTab);
|
||||||
}
|
}
|
||||||
@@ -186,7 +169,6 @@ public partial class ChatWindow
|
|||||||
{
|
{
|
||||||
var timeline = new List<V2TimelineItem>(visibleMessages.Count + visibleEvents.Count);
|
var timeline = new List<V2TimelineItem>(visibleMessages.Count + visibleEvents.Count);
|
||||||
|
|
||||||
// 1. 메시지 추가
|
|
||||||
foreach (var msg in visibleMessages)
|
foreach (var msg in visibleMessages)
|
||||||
{
|
{
|
||||||
var capturedMsg = msg;
|
var capturedMsg = msg;
|
||||||
@@ -194,46 +176,49 @@ public partial class ChatWindow
|
|||||||
timeline.Add(new V2TimelineItem(key, msg.Timestamp, () => CreateV2MessageElement(capturedMsg)));
|
timeline.Add(new V2TimelineItem(key, msg.Timestamp, () => CreateV2MessageElement(capturedMsg)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 실행 이벤트 추가 — ToolCall+ToolResult 쌍을 병합
|
|
||||||
// 스트리밍 중이면 라이브 카드가 이미 표시하는 현재 실행 이벤트는 타임라인에서 제외
|
|
||||||
var eventIndex = 0;
|
var eventIndex = 0;
|
||||||
var events = visibleEvents.ToList();
|
var events = visibleEvents.ToList();
|
||||||
|
var liveCardCutoff = (_isStreaming && _v2LiveContainer != null)
|
||||||
|
? _v2LiveStartTime
|
||||||
|
: DateTime.MaxValue;
|
||||||
|
|
||||||
for (int i = 0; i < events.Count; i++)
|
for (int i = 0; i < events.Count; i++)
|
||||||
{
|
{
|
||||||
var executionEvent = events[i];
|
var executionEvent = events[i];
|
||||||
|
if (executionEvent.Timestamp.ToUniversalTime() >= liveCardCutoff)
|
||||||
|
continue;
|
||||||
|
|
||||||
// 스트리밍 중: 라이브 카드 시작 이후 이벤트는 라이브 카드에서 표시하므로 스킵
|
|
||||||
var agentEvent = ToAgentEvent(executionEvent);
|
var agentEvent = ToAgentEvent(executionEvent);
|
||||||
|
if (ShouldHideV2TimelineEvent(agentEvent))
|
||||||
// SessionStart / UserPromptSubmit 숨김
|
|
||||||
if (agentEvent.Type == AgentEventType.SessionStart || agentEvent.Type == AgentEventType.UserPromptSubmit)
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var key = $"v2e_{executionEvent.Timestamp.Ticks}_{eventIndex++}";
|
var key = $"v2e_{executionEvent.Timestamp.Ticks}_{eventIndex++}";
|
||||||
|
|
||||||
// ToolCall → 바로 다음 같은 도구의 ToolResult를 찾아 병합
|
|
||||||
if (agentEvent.Type == AgentEventType.ToolCall && i + 1 < events.Count)
|
if (agentEvent.Type == AgentEventType.ToolCall && i + 1 < events.Count)
|
||||||
{
|
{
|
||||||
var nextEvent = ToAgentEvent(events[i + 1]);
|
var nextEvent = ToAgentEvent(events[i + 1]);
|
||||||
if (nextEvent.Type == AgentEventType.ToolResult
|
if (!ShouldHideV2TimelineEvent(nextEvent)
|
||||||
|
&& nextEvent.Type == AgentEventType.ToolResult
|
||||||
&& string.Equals(nextEvent.ToolName, agentEvent.ToolName, StringComparison.OrdinalIgnoreCase))
|
&& string.Equals(nextEvent.ToolName, agentEvent.ToolName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var capturedCall = agentEvent;
|
var capturedCall = agentEvent;
|
||||||
var capturedResult = nextEvent;
|
var capturedResult = nextEvent;
|
||||||
timeline.Add(new V2TimelineItem(key, executionEvent.Timestamp,
|
timeline.Add(new V2TimelineItem(
|
||||||
|
key,
|
||||||
|
executionEvent.Timestamp,
|
||||||
() => CreateV2ToolExecutionCard(capturedCall, capturedResult)));
|
() => CreateV2ToolExecutionCard(capturedCall, capturedResult)));
|
||||||
i++; // ToolResult 스킵
|
i++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var capturedEvent = agentEvent;
|
var capturedEvent = agentEvent;
|
||||||
timeline.Add(new V2TimelineItem(key, executionEvent.Timestamp,
|
timeline.Add(new V2TimelineItem(
|
||||||
|
key,
|
||||||
|
executionEvent.Timestamp,
|
||||||
() => CreateV2AgentEventElement(capturedEvent)));
|
() => CreateV2AgentEventElement(capturedEvent)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 시간순 정렬
|
|
||||||
var needsSort = false;
|
var needsSort = false;
|
||||||
for (int i = 1; i < timeline.Count; i++)
|
for (int i = 1; i < timeline.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -243,9 +228,25 @@ public partial class ChatWindow
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsSort)
|
if (needsSort)
|
||||||
timeline.Sort((a, b) => a.Timestamp.CompareTo(b.Timestamp));
|
timeline.Sort((a, b) => a.Timestamp.CompareTo(b.Timestamp));
|
||||||
|
|
||||||
return timeline;
|
return timeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ShouldHideV2TimelineEvent(AgentEvent agentEvent)
|
||||||
|
{
|
||||||
|
if (agentEvent.Type == AgentEventType.SessionStart || agentEvent.Type == AgentEventType.UserPromptSubmit)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (agentEvent.Type != AgentEventType.Thinking)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (string.Equals(agentEvent.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(agentEvent.ToolName, "context_compaction", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return AgentProgressSummarySanitizer.IsLowSignalStatusSummary(agentEvent.Summary, agentEvent.ToolName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user