diff --git a/README.md b/README.md index c962eaa..e26c8c9 100644 --- a/README.md +++ b/README.md @@ -996,6 +996,11 @@ ow + toggle 시각 언어로 통일했습니다. - 이번 정리 후 parity 는 `core engine 100% / main transcript UI 100% / Cowork·Code runtime UX 100% / internal settings 100% / overall 100%` 기준으로 최종 마감 판단했습니다. - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0 - 업데이트: 2026-04-05 21:43 (KST) +- 업데이트: 2026-04-05 22:18 (KST) +- transcript 품질 향상 2차로 도구/스킬 표시 카탈로그를 [AgentTranscriptDisplayCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs) 로 분리했습니다. 이제 transcript 배지와 task summary 카드가 `파일 / 빌드 / Git / 문서 / 질문 / 제안 / 스킬` 같은 역할 중심 라벨을 공통으로 사용합니다. +- [ChatWindow.TranscriptPolicy.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptPolicy.cs) 를 추가해 transcript badge/summary/task-summary policy를 partial helper로 분리했고, [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 task summary popup 은 active task 우선, recent history 는 debug 또는 active 없음일 때만 보이도록 축소했습니다. +- 동일 프롬프트 회귀 세트는 [docs/AX_AGENT_REGRESSION_PROMPTS.md](/E:/AX%20Copilot%20-%20Codex/docs/AX_AGENT_REGRESSION_PROMPTS.md) 로 별도 분리해 Chat/Cowork/Code/queue/permission/slash 시나리오를 바로 비교할 수 있게 했습니다. +- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 예정 --- diff --git a/docs/AX_AGENT_REGRESSION_PROMPTS.md b/docs/AX_AGENT_REGRESSION_PROMPTS.md new file mode 100644 index 0000000..7d6453e --- /dev/null +++ b/docs/AX_AGENT_REGRESSION_PROMPTS.md @@ -0,0 +1,79 @@ +# AX Agent Regression Prompts + +업데이트: 2026-04-05 22:18 (KST) + +`claw-code`와 AX Agent를 같은 기준으로 비교하기 위한 회귀 프롬프트 세트입니다. + +## Chat + +1. 기본 답변 +- 프롬프트: `회의 일정 조정 메일을 정중한 한국어로 써줘` +- 확인: + - 빈 assistant 카드 없음 + - 재생성/재시도 후 transcript 중복 없음 + +2. 장문 설명 +- 프롬프트: `RAG와 fine-tuning 차이를 실무 관점으로 7가지로 설명해줘` +- 확인: + - 장문 렌더 유지 + - compact 이후 다음 턴 문맥 유지 + +## Cowork + +3. 문서형 작업 +- 프롬프트: `신규 ERP 도입 제안서 초안을 작성해줘. 목적, 범위, 기대효과, 추진일정 포함` +- 확인: + - 작업 유형 반영 + - 계획 이후 실제 문서형 결과 흐름 + - 기본 로그 과다 노출 없음 + +4. 데이터형 작업 +- 프롬프트: `매출 CSV를 분석해서 월별 추세와 이상치를 요약해줘` +- 확인: + - 데이터 분석형 도구 선택 + - 결과 요약 품질 + - runtime 노이즈 최소화 + +## Code + +5. 버그 수정 +- 프롬프트: `현재 프로젝트에서 설정 저장 버그 원인 찾고 수정해줘` +- 확인: + - 읽기/검색/수정 흐름 일관성 + - diff 저장 + - reopen 시 transcript 보존 + +6. 빌드/테스트 +- 프롬프트: `빌드 오류를 재현하고 수정한 뒤 다시 빌드해줘` +- 확인: + - build/test 루프 + - 실패 후 재시도 + - 완료 메시지 정합성 + +## Cross-tab + +7. 후속 큐 +- 순차 프롬프트: + - `이 창 레이아웃 문제 원인 찾아줘` + - `끝나면 README도 같이 갱신해줘` +- 확인: + - queue chaining + - 입력창 직접 변형 없이 다음 턴 수행 + +8. compact 이후 연속성 +- 프롬프트: `지금까지 논의한 내용을 5줄로 이어서 정리하고 다음 작업 제안해줘` +- 확인: + - token-only completion 없음 + - compact 후 문맥 유지 + +9. 권한 승인 +- 프롬프트: `이 파일을 수정해서 저장해줘` +- 확인: + - 승인 요청 transcript 표시 + - 승인/거부 후 결과 정합성 + +10. slash / skill +- 프롬프트: `/bug-hunt src 폴더 잠재 버그 찾아줘` +- 확인: + - slash 진입과 일반 send 경로 동일성 + - skill 실행 이유/결과 표기 diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index f3f01f7..36982a5 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4754,3 +4754,7 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - Document update: 2026-04-05 22:04 (KST) - Added a canonical 10-prompt regression set to `docs/claw-code-parity-plan.md` so AX Agent and `claw-code` can be compared on the same Chat/Cowork/Code scenarios: basic/long chat, document/data cowork, bug-fix/build code, queued follow-up, post-compaction continuity, permission approval, and slash skill entry. - Document update: 2026-04-05 22:04 (KST) - Added a tool/skill delta snapshot to the parity plan. AX remains stronger on document/office/data workflows, while `claw-code` remains stronger on transcript-native approval/tool-result/permission message taxonomy. - Document update: 2026-04-05 22:04 (KST) - Switched plan approval flow to transcript-first. `CreatePlanDecisionCallback()` now prepares `PlanViewerWindow` without auto-opening it, shows the inline approval controls in the transcript first, and keeps the bottom `계획` button as the secondary detail surface. +- 업데이트: 2026-04-05 22:18 (KST) +- transcript 도구/스킬 표시 카탈로그를 [AgentTranscriptDisplayCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs) 로 분리했습니다. 도구 결과와 task summary 카드가 공통 분류 기준(`파일 / 빌드 / Git / 문서 / 질문 / 제안 / 스킬`)을 사용하도록 맞춰 transcript 언어 일관성을 높였습니다. +- [ChatWindow.TranscriptPolicy.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptPolicy.cs) 를 추가해 transcript badge/summary/task-summary policy helper 를 partial class 로 분리했습니다. `ChatWindow.xaml.cs` 본문에는 실제 렌더만 남기고, recent task 노출 정책은 `active 우선 + debug 보조` 기준으로 축소했습니다. +- 회귀 프롬프트 세트는 [AX_AGENT_REGRESSION_PROMPTS.md](/E:/AX%20Copilot%20-%20Codex/docs/AX_AGENT_REGRESSION_PROMPTS.md) 로 별도 분리했습니다. Chat/Cowork/Code, queue follow-up, compact 이후 다음 턴, permission, slash skill 진입까지 `claw-code`와 같은 체크리스트로 비교할 수 있습니다. diff --git a/docs/claw-code-parity-plan.md b/docs/claw-code-parity-plan.md index 286623e..8b56e71 100644 --- a/docs/claw-code-parity-plan.md +++ b/docs/claw-code-parity-plan.md @@ -125,6 +125,7 @@ ## Canonical Prompt Set - Updated: 2026-04-05 22:04 (KST) - The following prompt set should be used for AX vs `claw-code` parity checks. The goal is not byte-identical output, but equivalent execution route, approval behavior, and artifact/result quality. +- Operational checklist copy: `docs/AX_AGENT_REGRESSION_PROMPTS.md` 1. Chat basic answer - Prompt: `회의 일정 조정 메일을 정중한 한국어로 써줘` diff --git a/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs b/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs new file mode 100644 index 0000000..8e3d19c --- /dev/null +++ b/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs @@ -0,0 +1,111 @@ +namespace AxCopilot.Services.Agent; + +internal static class AgentTranscriptDisplayCatalog +{ + public static string GetDisplayName(string? rawName, bool slashPrefix = false) + { + if (string.IsNullOrWhiteSpace(rawName)) + return slashPrefix ? "/스킬" : "도구"; + + var normalized = rawName.Trim(); + var mapped = normalized.ToLowerInvariant() switch + { + "file_read" => "파일 읽기", + "file_write" => "파일 쓰기", + "file_edit" => "파일 편집", + "document_reader" => "문서 읽기", + "document_planner" => "문서 계획", + "document_assembler" => "문서 조합", + "document_review" => "문서 검토", + "format_convert" => "형식 변환", + "code_search" => "코드 검색", + "code_review" => "코드 리뷰", + "build_run" => "빌드/실행", + "git_tool" => "Git", + "process" => "프로세스", + "glob" => "파일 찾기", + "grep" => "내용 검색", + "folder_map" => "폴더 맵", + "memory" => "메모리", + "user_ask" => "의견 요청", + "suggest_actions" => "다음 작업 제안", + "task_create" => "작업 생성", + "task_update" => "작업 업데이트", + "task_list" => "작업 목록", + "task_get" => "작업 조회", + "task_stop" => "작업 중지", + "task_output" => "작업 출력", + "spawn_agent" => "서브에이전트", + "wait_agents" => "에이전트 대기", + _ => normalized.Replace('_', ' ').Trim(), + }; + + if (!slashPrefix) + return mapped; + + return normalized.StartsWith('/') ? normalized : "/" + normalized.Replace(' ', '-'); + } + + public static string GetEventBadgeLabel(AgentEvent evt) + { + if (evt.Type == AgentEventType.SkillCall) + return "스킬"; + + if (evt.Type is AgentEventType.PermissionRequest or AgentEventType.PermissionGranted or AgentEventType.PermissionDenied) + return "권한"; + + return GetToolCategoryLabel(evt.ToolName); + } + + public static string GetTaskCategoryLabel(string? kind, string? title) + { + if (string.Equals(kind, "permission", System.StringComparison.OrdinalIgnoreCase)) + return "권한"; + if (string.Equals(kind, "queue", System.StringComparison.OrdinalIgnoreCase)) + return "큐"; + if (string.Equals(kind, "hook", System.StringComparison.OrdinalIgnoreCase)) + return "훅"; + if (string.Equals(kind, "subagent", System.StringComparison.OrdinalIgnoreCase)) + return "에이전트"; + if (string.Equals(kind, "tool", System.StringComparison.OrdinalIgnoreCase)) + return GetToolCategoryLabel(title); + return "작업"; + } + + public static string BuildEventSummary(AgentEvent evt, string displayName) + { + var summary = (evt.Summary ?? "").Trim(); + if (!string.IsNullOrWhiteSpace(summary)) + return summary; + + return evt.Type switch + { + AgentEventType.ToolCall => $"{displayName} 실행 준비", + AgentEventType.ToolResult => evt.Success ? $"{displayName} 실행 완료" : $"{displayName} 실행 실패", + AgentEventType.SkillCall => $"{displayName} 실행", + AgentEventType.PermissionRequest => $"{displayName} 실행 전 사용자 확인 필요", + AgentEventType.PermissionGranted => $"{displayName} 실행이 허용됨", + AgentEventType.PermissionDenied => $"{displayName} 실행이 거부됨", + _ => summary, + }; + } + + private static string GetToolCategoryLabel(string? rawName) + { + if (string.IsNullOrWhiteSpace(rawName)) + return "도구"; + + return rawName.Trim().ToLowerInvariant() switch + { + "file_read" or "file_write" or "file_edit" or "glob" or "grep" or "folder_map" or "file_watch" or "file_info" or "file_manage" => "파일", + "build_run" or "test_loop" or "dev_env_detect" => "빌드", + "git_tool" or "diff_tool" or "diff_preview" => "Git", + "document_reader" or "document_planner" or "document_assembler" or "document_review" or "format_convert" or "template_render" => "문서", + "user_ask" => "질문", + "suggest_actions" => "제안", + "process" => "실행", + "spawn_agent" or "wait_agents" => "에이전트", + _ => "도구", + }; + } +} diff --git a/src/AxCopilot/Views/ChatWindow.TranscriptPolicy.cs b/src/AxCopilot/Views/ChatWindow.TranscriptPolicy.cs new file mode 100644 index 0000000..25c82e8 --- /dev/null +++ b/src/AxCopilot/Views/ChatWindow.TranscriptPolicy.cs @@ -0,0 +1,26 @@ +using System.Windows.Media; +using AxCopilot.Services; +using AxCopilot.Services.Agent; + +namespace AxCopilot.Views; + +public partial class ChatWindow +{ + private bool IsDebugTranscriptMode() + => string.Equals(_settings.Settings.Llm.AgentLogLevel, "debug", StringComparison.OrdinalIgnoreCase); + + private string GetTranscriptBadgeLabel(AgentEvent evt) + => AgentTranscriptDisplayCatalog.GetEventBadgeLabel(evt); + + private string GetTranscriptTaskCategory(TaskRunStore.TaskRun task) + => AgentTranscriptDisplayCatalog.GetTaskCategoryLabel(task.Kind, task.Title); + + private string GetTranscriptDisplayName(string? rawName, bool slashPrefix = false) + => AgentTranscriptDisplayCatalog.GetDisplayName(rawName, slashPrefix); + + private string GetTranscriptEventSummary(AgentEvent evt, string displayName) + => AgentTranscriptDisplayCatalog.BuildEventSummary(evt, displayName); + + private bool ShouldIncludeRecentTaskSummary(IReadOnlyCollection activeTasks) + => IsDebugTranscriptMode() || activeTasks.Count == 0; +} diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index 0c96b21..fd48295 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -9375,73 +9375,16 @@ public partial class ChatWindow : Window _ => "진행 중", }; - private static string GetAgentItemDisplayName(string? rawName, bool slashPrefix = false) - { - if (string.IsNullOrWhiteSpace(rawName)) - return slashPrefix ? "/스킬" : "도구"; - - var normalized = rawName.Trim(); - var mapped = normalized.ToLowerInvariant() switch - { - "file_read" => "파일 읽기", - "file_write" => "파일 쓰기", - "file_edit" => "파일 편집", - "document_reader" => "문서 읽기", - "document_planner" => "문서 계획", - "document_assembler" => "문서 조합", - "code_search" => "코드 검색", - "code_review" => "코드 리뷰", - "build_run" => "빌드/실행", - "git_tool" => "Git", - "process" => "프로세스", - "glob" => "파일 찾기", - "grep" => "내용 검색", - "folder_map" => "폴더 맵", - "memory" => "메모리", - "user_ask" => "의견 요청", - "suggest_actions" => "다음 작업 제안", - "task_create" => "작업 생성", - "task_update" => "작업 업데이트", - "task_list" => "작업 목록", - "task_get" => "작업 조회", - "task_stop" => "작업 중지", - "task_output" => "작업 출력", - "spawn_agent" => "서브에이전트", - "wait_agents" => "에이전트 대기", - _ => normalized.Replace('_', ' ').Trim(), - }; - - if (!slashPrefix) - return mapped; - - var slashName = normalized.StartsWith('/') - ? normalized - : "/" + normalized.Replace(' ', '-'); - return slashName; - } + private string GetAgentItemDisplayName(string? rawName, bool slashPrefix = false) + => GetTranscriptDisplayName(rawName, slashPrefix); private static bool IsTranscriptToolLikeEvent(AgentEvent evt) => evt.Type is AgentEventType.ToolCall or AgentEventType.ToolResult or AgentEventType.SkillCall || (!string.IsNullOrWhiteSpace(evt.ToolName) && evt.Type is AgentEventType.PermissionRequest or AgentEventType.PermissionGranted or AgentEventType.PermissionDenied); - private static string BuildAgentEventSummaryText(AgentEvent evt, string displayName) - { - var summary = (evt.Summary ?? "").Trim(); - if (!string.IsNullOrWhiteSpace(summary)) - return summary; - - return evt.Type switch - { - AgentEventType.ToolCall => $"{displayName} 실행 준비", - AgentEventType.ToolResult => evt.Success ? $"{displayName} 실행 완료" : $"{displayName} 실행 실패", - AgentEventType.SkillCall => $"{displayName} 실행", - AgentEventType.PermissionRequest => $"{displayName} 실행 전 사용자 확인 필요", - AgentEventType.PermissionGranted => $"{displayName} 실행이 허용됨", - AgentEventType.PermissionDenied => $"{displayName} 실행이 거부됨", - _ => summary, - }; - } + private string BuildAgentEventSummaryText(AgentEvent evt, string displayName) + => GetTranscriptEventSummary(evt, displayName); private IEnumerable FilterTaskSummaryItems(IEnumerable tasks) => _taskSummaryTaskFilter switch @@ -10794,9 +10737,9 @@ public partial class ChatWindow : Window AgentEventType.PermissionGranted => GetPermissionBadgeMeta(evt.ToolName, pending: false), AgentEventType.PermissionDenied => ("\uE783", "권한 거부", "#FEF2F2", "#DC2626"), AgentEventType.Decision => GetDecisionBadgeMeta(evt.Summary), - AgentEventType.ToolCall => ("\uE8A7", "도구", "#EEF6FF", "#3B82F6"), - AgentEventType.ToolResult => ("\uE73E", "도구 결과", "#EEF9EE", "#16A34A"), - AgentEventType.SkillCall => ("\uE8A5", "스킬", "#FFF7ED", "#EA580C"), + AgentEventType.ToolCall => ("\uE8A7", GetTranscriptBadgeLabel(evt), "#EEF6FF", "#3B82F6"), + AgentEventType.ToolResult => ("\uE73E", GetTranscriptBadgeLabel(evt), "#EEF9EE", "#16A34A"), + AgentEventType.SkillCall => ("\uE8A5", GetTranscriptBadgeLabel(evt), "#FFF7ED", "#EA580C"), AgentEventType.Error => ("\uE783", "오류", "#FEF2F2", "#DC2626"), AgentEventType.Complete => ("\uE930", "완료", "#F0FFF4", "#15803D"), AgentEventType.StepDone => ("\uE73E", "단계 완료", "#EEF9EE", "#16A34A"), @@ -20795,13 +20738,19 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi } } - foreach (var task in FilterTaskSummaryItems(_appState.ActiveTasks).Take(3)) + var activeTasks = FilterTaskSummaryItems(_appState.ActiveTasks).Take(3).ToList(); + var recentTasks = FilterTaskSummaryItems(_appState.RecentTasks).Take(2).ToList(); + + foreach (var task in activeTasks) panel.Children.Add(BuildTaskSummaryCard(task, active: true)); - foreach (var task in FilterTaskSummaryItems(_appState.RecentTasks).Take(2)) - panel.Children.Add(BuildTaskSummaryCard(task, active: false)); + if (ShouldIncludeRecentTaskSummary(activeTasks)) + { + foreach (var task in recentTasks) + panel.Children.Add(BuildTaskSummaryCard(task, active: false)); + } - if (!FilterTaskSummaryItems(_appState.ActiveTasks).Any() && !FilterTaskSummaryItems(_appState.RecentTasks).Any()) + if (activeTasks.Count == 0 && recentTasks.Count == 0) { panel.Children.Add(new TextBlock { @@ -21591,6 +21540,7 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray; var (kindIcon, kindColor) = GetTaskKindVisual(task.Kind); + var categoryLabel = GetTranscriptTaskCategory(task); var displayTitle = string.Equals(task.Kind, "tool", StringComparison.OrdinalIgnoreCase) || string.Equals(task.Kind, "permission", StringComparison.OrdinalIgnoreCase) || string.Equals(task.Kind, "hook", StringComparison.OrdinalIgnoreCase) @@ -21623,6 +21573,24 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi }); taskStack.Children.Add(headerRow); + taskStack.Children.Add(new Border + { + Background = BrushFromHex("#F8FAFC"), + BorderBrush = BrushFromHex("#E5E7EB"), + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(999), + Padding = new Thickness(6, 1, 6, 1), + Margin = new Thickness(0, 0, 0, 4), + HorizontalAlignment = HorizontalAlignment.Left, + Child = new TextBlock + { + Text = categoryLabel, + FontSize = 8, + FontWeight = FontWeights.SemiBold, + Foreground = secondaryText, + }, + }); + if (!string.IsNullOrWhiteSpace(task.Summary)) { taskStack.Children.Add(new TextBlock