AX Agent 상태 메시지 내러티브 고도화 및 코워크/코드 진행 이력 개선
- AgentStatusNarrativeCatalog를 추가해 agent event를 탭(Cowork/Code), 도구 카테고리, 대상 힌트 기준으로 해석하고 상태 메시지/상세 설명/phase label/meta를 한 곳에서 생성하도록 정리함 - ChatWindow의 live pulse 상태, idle 진행 힌트, readable process feed 요약이 동일 narrative 카탈로그를 재사용하도록 변경해 단조로운 도구명 중심 문구를 작업 의도 중심 문구로 치환함 - README, DEVELOPMENT, NEXT_ROADMAP에 2026-04-15 12:14 (KST) 기준 이력과 남은 UX 마감 메모를 반영함 검증 결과 - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_status_narrative\\ -p:IntermediateOutputPath=obj\\verify_status_narrative\\ : 경고 0 / 오류 0 - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentStatusNarrativeCatalogTests|AgentLoopIterationPreparationServiceTests|AgentToolResultBudgetTests|ChatStorageServiceTests|AgentMessageInvariantHelperTests" -p:OutputPath=bin\\verify_status_narrative_tests\\ -p:IntermediateOutputPath=obj\\verify_status_narrative_tests\\ : 통과 15
This commit is contained in:
@@ -661,36 +661,7 @@ public partial class ChatWindow
|
||||
}
|
||||
|
||||
private static string BuildReadableProcessFeedSummary(AgentEvent evt, string transcriptBadgeLabel, string itemDisplayName)
|
||||
{
|
||||
var phaseLabel = ResolveProgressPhaseLabel(evt);
|
||||
if (!string.IsNullOrWhiteSpace(phaseLabel))
|
||||
return phaseLabel;
|
||||
|
||||
return evt.Type switch
|
||||
{
|
||||
AgentEventType.Thinking when string.Equals(evt.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase)
|
||||
=> "처리 중...",
|
||||
AgentEventType.Thinking when string.Equals(evt.ToolName, "context_compaction", StringComparison.OrdinalIgnoreCase)
|
||||
=> "컨텍스트 압축 중...",
|
||||
AgentEventType.Planning when evt.Steps is { Count: > 0 }
|
||||
=> $"계획 {evt.Steps.Count}단계 정리",
|
||||
AgentEventType.StepStart when evt.StepTotal > 0
|
||||
=> $"{evt.StepCurrent}/{evt.StepTotal} 단계 진행",
|
||||
AgentEventType.StepDone when evt.StepTotal > 0
|
||||
=> $"{evt.StepCurrent}/{evt.StepTotal} 단계 완료",
|
||||
AgentEventType.Thinking when !string.IsNullOrWhiteSpace(evt.Summary)
|
||||
=> evt.Summary,
|
||||
AgentEventType.ToolCall
|
||||
=> string.IsNullOrWhiteSpace(itemDisplayName)
|
||||
? $"{transcriptBadgeLabel} 실행"
|
||||
: $"{itemDisplayName} 실행",
|
||||
AgentEventType.SkillCall
|
||||
=> string.IsNullOrWhiteSpace(itemDisplayName)
|
||||
? "스킬 실행"
|
||||
: $"{itemDisplayName} 실행",
|
||||
_ => string.IsNullOrWhiteSpace(evt.Summary) ? transcriptBadgeLabel : evt.Summary,
|
||||
};
|
||||
}
|
||||
=> AgentStatusNarrativeCatalog.BuildProgressStepLabel(evt, transcriptBadgeLabel, itemDisplayName);
|
||||
|
||||
private Border CreateReadableProcessFeedCard(
|
||||
string summary,
|
||||
@@ -793,59 +764,10 @@ public partial class ChatWindow
|
||||
}
|
||||
|
||||
private static string? ResolveProgressPhaseLabel(AgentEvent evt)
|
||||
{
|
||||
var summary = (evt.Summary ?? string.Empty).Trim();
|
||||
var toolName = (evt.ToolName ?? string.Empty).Trim();
|
||||
|
||||
if (string.Equals(toolName, "context_compaction", StringComparison.OrdinalIgnoreCase))
|
||||
return "컨텍스트 압축 중...";
|
||||
if (string.Equals(toolName, "agent_wait", StringComparison.OrdinalIgnoreCase))
|
||||
return "처리 중...";
|
||||
if (summary.Contains("html_create", StringComparison.OrdinalIgnoreCase)
|
||||
|| summary.Contains("document_assemble", StringComparison.OrdinalIgnoreCase)
|
||||
|| summary.Contains("docx_create", StringComparison.OrdinalIgnoreCase))
|
||||
return "문서 결과 생성 중...";
|
||||
if (summary.Contains("검증", StringComparison.OrdinalIgnoreCase)
|
||||
|| summary.Contains("verification", StringComparison.OrdinalIgnoreCase))
|
||||
return "결과 검증 중...";
|
||||
if (summary.Contains("diff", StringComparison.OrdinalIgnoreCase))
|
||||
return "변경 내용 확인 중...";
|
||||
if (evt.Type == AgentEventType.ToolCall && !string.IsNullOrWhiteSpace(toolName))
|
||||
{
|
||||
return toolName switch
|
||||
{
|
||||
"file_read" or "directory_list" or "glob" or "grep" or "folder_map" or "multi_read" => "파일 탐색 중...",
|
||||
"file_edit" or "file_write" or "html_create" or "docx_create" or "markdown_create" => "산출물 생성 중...",
|
||||
"build_run" or "test_loop" => "실행 결과 확인 중...",
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
=> AgentStatusNarrativeCatalog.BuildProgressPhaseLabel(evt);
|
||||
|
||||
private static string? ResolveProgressPhaseMeta(AgentEvent evt)
|
||||
{
|
||||
var summary = evt.Summary ?? string.Empty;
|
||||
var toolName = evt.ToolName ?? string.Empty;
|
||||
|
||||
if (evt.Type == AgentEventType.Planning)
|
||||
return "계획";
|
||||
if (evt.Type == AgentEventType.StepStart || evt.Type == AgentEventType.StepDone)
|
||||
return "단계";
|
||||
if (string.Equals(toolName, "context_compaction", StringComparison.OrdinalIgnoreCase))
|
||||
return "압축";
|
||||
if (summary.Contains("검증", StringComparison.OrdinalIgnoreCase) || summary.Contains("verification", StringComparison.OrdinalIgnoreCase))
|
||||
return "검증";
|
||||
if (summary.Contains("fallback", StringComparison.OrdinalIgnoreCase) || summary.Contains("자동 생성", StringComparison.OrdinalIgnoreCase))
|
||||
return "폴백";
|
||||
if (summary.Contains("재시도", StringComparison.OrdinalIgnoreCase) || summary.Contains("retry", StringComparison.OrdinalIgnoreCase))
|
||||
return "재시도";
|
||||
if (evt.Type == AgentEventType.ToolCall)
|
||||
return "도구";
|
||||
|
||||
return null;
|
||||
}
|
||||
=> AgentStatusNarrativeCatalog.BuildProgressPhaseMeta(evt);
|
||||
|
||||
private Border CreateReadableProgressFeedCard(
|
||||
string summary,
|
||||
|
||||
@@ -24,14 +24,19 @@ public partial class ChatWindow
|
||||
private const int MaxStatusSubItems = 6;
|
||||
|
||||
// ShowStreamingStatusBar → 펄스 닷 바로 위임 (플로팅 상태 바 표시 안 함)
|
||||
private void ShowStreamingStatusBar(string message, string? iconCode = null)
|
||||
=> ShowPulseDots(message, iconCode);
|
||||
private void ShowStreamingStatusBar(string message, string? iconCode = null, string? detail = null)
|
||||
=> ShowPulseDots(message, iconCode, detail);
|
||||
|
||||
private void HideStreamingStatusBar()
|
||||
=> HidePulseDots();
|
||||
|
||||
private void UpdateStreamingStatusBar(string message, string? iconCode = null)
|
||||
=> UpdatePulseDotsText(message, iconCode);
|
||||
private void UpdateStreamingStatusBar(
|
||||
string message,
|
||||
string? iconCode = null,
|
||||
string? detail = null,
|
||||
bool clearSubItems = false,
|
||||
string? subItemCategory = null)
|
||||
=> UpdatePulseDotsText(message, iconCode, detail, clearSubItems, subItemCategory);
|
||||
|
||||
// ─── 입력창 위 펄스 닷 애니메이션 ──────────────────────────────────────────
|
||||
|
||||
@@ -41,6 +46,8 @@ public partial class ChatWindow
|
||||
if (PulseDotStatusText != null)
|
||||
PulseDotStatusText.Text = message ?? "생각하는 중...";
|
||||
ClearStatusSubItems();
|
||||
if (!string.IsNullOrWhiteSpace(detail))
|
||||
AddStatusSubItem(detail);
|
||||
PulseDotBar.Visibility = Visibility.Visible;
|
||||
StartStatusDiamondAnimation();
|
||||
if (_pulseDotStoryboard != null) return; // 이미 실행 중
|
||||
@@ -325,8 +332,18 @@ public partial class ChatWindow
|
||||
|
||||
var idle = DateTime.UtcNow - _lastAgentProgressEventAt;
|
||||
TryGetStreamingElapsed(out var elapsed);
|
||||
string? summary = null;
|
||||
var toolName = "agent_wait";
|
||||
var lastProgressEvent = _currentRunProgressSteps.Count > 0
|
||||
? _currentRunProgressSteps[^1]
|
||||
: null;
|
||||
var idleNarrative = AgentStatusNarrativeCatalog.BuildIdle(
|
||||
lastProgressEvent,
|
||||
runTab,
|
||||
idle,
|
||||
elapsed,
|
||||
_pendingPostCompaction);
|
||||
string? summary = idleNarrative.Message;
|
||||
var toolName = _pendingPostCompaction ? "context_compaction" : "agent_wait";
|
||||
UpdateStreamingStatusBar(idleNarrative.Message, detail: idleNarrative.Detail);
|
||||
|
||||
if (_pendingPostCompaction && idle >= TimeSpan.FromSeconds(2))
|
||||
{
|
||||
@@ -359,6 +376,17 @@ public partial class ChatWindow
|
||||
summary = "작업을 진행하는 중입니다...";
|
||||
}
|
||||
|
||||
summary = idleNarrative.Message;
|
||||
if (_pendingPostCompaction || idle >= TimeSpan.FromSeconds(30))
|
||||
{
|
||||
UpdateStreamingStatusBar(
|
||||
summary,
|
||||
_pendingPostCompaction ? "\uE72C" : "\uE895",
|
||||
idleNarrative.Detail,
|
||||
clearSubItems: true,
|
||||
subItemCategory: idleNarrative.Category);
|
||||
}
|
||||
|
||||
UpdateLiveAgentProgressHint(summary, toolName);
|
||||
}
|
||||
|
||||
|
||||
@@ -6632,21 +6632,18 @@ public partial class ChatWindow : Window
|
||||
// ── 1단계: 경량 UI 피드백 (PulseDotBar 상태 텍스트만 갱신) ───────────
|
||||
if (string.Equals(runTab, _activeTab, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
switch (evt.Type)
|
||||
if (evt.Type is AgentEventType.Complete or AgentEventType.Error)
|
||||
{
|
||||
case AgentEventType.ToolCall when !string.IsNullOrWhiteSpace(evt.ToolName):
|
||||
if (PulseDotStatusText != null && PulseDotBar?.Visibility == Visibility.Visible)
|
||||
PulseDotStatusText.Text = GetStatusInfoForTool(evt.ToolName).message + "...";
|
||||
break;
|
||||
case AgentEventType.ToolResult when !string.IsNullOrWhiteSpace(evt.ToolName):
|
||||
if (PulseDotStatusText != null && PulseDotBar?.Visibility == Visibility.Visible)
|
||||
PulseDotStatusText.Text = GetToolResultMessage(evt.ToolName) + "...";
|
||||
break;
|
||||
case AgentEventType.Complete:
|
||||
case AgentEventType.Error:
|
||||
HideStreamingStatusBar();
|
||||
FlushPendingAgentUiEvent();
|
||||
break;
|
||||
HideStreamingStatusBar();
|
||||
FlushPendingAgentUiEvent();
|
||||
}
|
||||
else if (PulseDotBar?.Visibility == Visibility.Visible)
|
||||
{
|
||||
var liveStatus = AgentStatusNarrativeCatalog.BuildFromEvent(evt, runTab);
|
||||
UpdateStreamingStatusBar(
|
||||
liveStatus.Message,
|
||||
detail: liveStatus.Detail,
|
||||
subItemCategory: liveStatus.Category);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6706,15 +6703,17 @@ public partial class ChatWindow : Window
|
||||
if (string.Equals(runTab, "Cowork", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
UpdateLiveAgentProgressHint("작업을 준비하는 중입니다...", "agent_wait");
|
||||
var initialStatus = AgentStatusNarrativeCatalog.BuildInitial(runTab);
|
||||
UpdateLiveAgentProgressHint(initialStatus.Message, "agent_wait");
|
||||
ShowStreamingStatusBar(initialStatus.Message, detail: initialStatus.Detail);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateLiveAgentProgressHint(null);
|
||||
ShowStreamingStatusBar("생각하는 중...");
|
||||
}
|
||||
_agentProgressHintTimer.Stop();
|
||||
_agentProgressHintTimer.Start();
|
||||
ShowStreamingStatusBar("생각하는 중...");
|
||||
}
|
||||
|
||||
private void StopLiveAgentProgressHints()
|
||||
|
||||
Reference in New Issue
Block a user