diff --git a/README.md b/README.md index 1de5e32..c962eaa 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,11 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저 개발 참고: Claw Code 동등성 작업 추적 문서 `docs/claw-code-parity-plan.md` +- 업데이트: 2026-04-05 19:04 (KST) +- AX Agent transcript와 작업 요약에서 도구/스킬 이름을 사람이 읽기 쉬운 표시명으로 정리했습니다. raw snake_case 도구명 대신 `파일 읽기`, `빌드/실행`, `Git`, `/bug-hunt` 같은 표시명 중심으로 보입니다. +- 도구/스킬 이벤트 배지는 역할 중심(`도구`, `도구 결과`, `스킬`)으로 단순화하고, 실제 도구명은 본문 보조 텍스트로만 노출되게 바꿨습니다. +- 작업 요약 팝업의 권한/배경 작업 관측성 섹션은 `debug` 성격일 때만 두껍게 보여서 기본 UX가 더 `claw-code`처럼 조용하게 유지됩니다. + - 업데이트: 2026-04-05 18:58 (KST) - AX Agent의 `의견 요청`/`질문` 흐름을 transcript 내 카드 우선 구조로 전환했습니다. - `user_ask` 도구는 별도 `UserAskDialog`를 먼저 띄우지 않고, 본문 안에서 선택지/직접 입력/전달로 응답을 완료합니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 8016b46..f3f01f7 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,5 +1,8 @@ # AX Copilot - 媛쒕컻 臾몄꽌 +- Document update: 2026-04-05 19:04 (KST) - Added transcript-facing tool/skill display normalization in `ChatWindow.xaml.cs`. Tool and skill events now use role-first badges (`도구`, `도구 결과`, `스킬`) with human-readable item labels such as `파일 읽기`, `빌드/실행`, `Git`, or `/skill-name`, instead of exposing raw snake_case names as the primary visual label. +- Document update: 2026-04-05 19:04 (KST) - Reduced task-summary observability noise by limiting permission/background observability sections to debug-level sessions. Default AX Agent runtime UX now stays closer to `claw-code`, where transcript reading flow remains primary and diagnostics stay secondary. + - Document update: 2026-04-05 18:58 (KST) - Switched `UserAskCallback` in `ChatWindow.xaml.cs` to a transcript-first inline card flow. `user_ask` no longer defaults to `UserAskDialog`; it renders an in-stream question card with choice pills, direct text input, and submit/cancel actions, then returns only the chosen answer to the engine. - Document update: 2026-04-05 18:58 (KST) - Aligned user-question UX with the same transcript-first principle already used for plan approval. `PlanViewerWindow` remains a secondary detail surface, while the primary approval/question decision now happens inside the AX Agent message timeline. diff --git a/docs/claw-code-parity-plan.md b/docs/claw-code-parity-plan.md index 35ed820..286623e 100644 --- a/docs/claw-code-parity-plan.md +++ b/docs/claw-code-parity-plan.md @@ -207,6 +207,16 @@ - `plan approval`: transcript-first, detail view via `PlanViewerWindow` - `user ask`: transcript-first inline question card with choices / direct input / submit +## Tool / Skill UX Parity Follow-up +- Updated: 2026-04-05 19:04 (KST) +- Default transcript should prefer role-oriented badges and readable labels over raw internal tool names. +- AX implementation status: + - tool event badges: simplified to role-first labels + - item naming: normalized into readable Korean labels or `/skill-name` style + - observability panels: permission/background diagnostics reduced outside debug mode +- Remaining quality target: + - move more tool-result and permission-result presentation into smaller message-type-specific helpers, closer to `claw-code` component separation + ## Current Snapshot - Updated: 2026-04-05 19:42 (KST) - Estimated parity: diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index 8a2e414..0c96b21 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -9375,6 +9375,74 @@ 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 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 IEnumerable FilterTaskSummaryItems(IEnumerable tasks) => _taskSummaryTaskFilter switch { @@ -10726,9 +10794,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", string.IsNullOrWhiteSpace(evt.ToolName) ? "도구 실행" : evt.ToolName, "#EEF6FF", "#3B82F6"), - AgentEventType.ToolResult => ("\uE73E", string.IsNullOrWhiteSpace(evt.ToolName) ? "도구 완료" : evt.ToolName, "#EEF9EE", "#16A34A"), - AgentEventType.SkillCall => ("\uE8A5", string.IsNullOrWhiteSpace(evt.ToolName) ? "스킬 실행" : evt.ToolName, "#FFF7ED", "#EA580C"), + AgentEventType.ToolCall => ("\uE8A7", "도구", "#EEF6FF", "#3B82F6"), + AgentEventType.ToolResult => ("\uE73E", "도구 결과", "#EEF9EE", "#16A34A"), + AgentEventType.SkillCall => ("\uE8A5", "스킬", "#FFF7ED", "#EA580C"), AgentEventType.Error => ("\uE783", "오류", "#FEF2F2", "#DC2626"), AgentEventType.Complete => ("\uE930", "완료", "#F0FFF4", "#15803D"), AgentEventType.StepDone => ("\uE73E", "단계 완료", "#EEF9EE", "#16A34A"), @@ -10736,6 +10804,10 @@ public partial class ChatWindow : Window AgentEventType.Resumed => ("\uE768", "재개", "#ECFDF5", "#059669"), _ => ("\uE946", "에이전트", "#F5F5F5", "#6B7280"), }; + var itemDisplayName = evt.Type == AgentEventType.SkillCall + ? GetAgentItemDisplayName(evt.ToolName, slashPrefix: true) + : GetAgentItemDisplayName(evt.ToolName); + var eventSummaryText = BuildAgentEventSummaryText(evt, itemDisplayName); var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray; @@ -10782,6 +10854,17 @@ public partial class ChatWindow : Window Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, }); + if (IsTranscriptToolLikeEvent(evt) && !string.IsNullOrWhiteSpace(evt.ToolName)) + { + headerLeft.Children.Add(new TextBlock + { + Text = $" · {itemDisplayName}", + FontSize = 8.25, + FontWeight = FontWeights.SemiBold, + Foreground = primaryText, + VerticalAlignment = VerticalAlignment.Center, + }); + } Grid.SetColumn(headerLeft, 0); // 우측: 소요 시간 + 토큰 배지 (항상 우측 끝에 고정) @@ -10833,11 +10916,11 @@ public partial class ChatWindow : Window // simple 모드: 요약 한 줄만 표시 (본문 로그) if (logLevel == "simple") { - if (!string.IsNullOrEmpty(evt.Summary)) + if (!string.IsNullOrEmpty(eventSummaryText)) { - var shortSummary = evt.Summary.Length > 100 - ? evt.Summary[..100] + "…" - : evt.Summary; + var shortSummary = eventSummaryText.Length > 100 + ? eventSummaryText[..100] + "…" + : eventSummaryText; sp.Children.Add(new TextBlock { Text = shortSummary, @@ -10850,9 +10933,9 @@ public partial class ChatWindow : Window } } // detailed/debug 모드: 실행 줄 아래에 얕은 설명만 표시 - else if (!string.IsNullOrEmpty(evt.Summary)) + else if (!string.IsNullOrEmpty(eventSummaryText)) { - var summaryText = evt.Summary.Length > 92 ? evt.Summary[..92] + "…" : evt.Summary; + var summaryText = eventSummaryText.Length > 92 ? eventSummaryText[..92] + "…" : eventSummaryText; sp.Children.Add(new TextBlock { Text = summaryText, @@ -21508,6 +21591,11 @@ 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 displayTitle = string.Equals(task.Kind, "tool", StringComparison.OrdinalIgnoreCase) + || string.Equals(task.Kind, "permission", StringComparison.OrdinalIgnoreCase) + || string.Equals(task.Kind, "hook", StringComparison.OrdinalIgnoreCase) + ? GetAgentItemDisplayName(task.Title) + : task.Title; var taskStack = new StackPanel(); var headerRow = new StackPanel { @@ -21526,8 +21614,8 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi headerRow.Children.Add(new TextBlock { Text = active - ? $"진행 중 · {task.Title}" - : $"{GetTaskStatusLabel(task.Status)} · {task.Title}", + ? $"진행 중 · {displayTitle}" + : $"{GetTaskStatusLabel(task.Status)} · {displayTitle}", FontSize = 9.5, FontWeight = FontWeights.SemiBold, Foreground = active ? primaryText : secondaryText, @@ -21549,7 +21637,7 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi var reviewChipRow = BuildReviewSignalChipRow( kind: task.Kind, toolName: task.Title, - title: task.Title, + title: displayTitle, summary: task.Summary); if (reviewChipRow != null) taskStack.Children.Add(reviewChipRow); @@ -22033,6 +22121,9 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi private void AddTaskSummaryObservabilitySections(StackPanel panel, ChatConversation? currentConversation) { + if (!string.Equals(_settings.Settings.Llm.AgentLogLevel, "debug", StringComparison.OrdinalIgnoreCase)) + return; + AddTaskSummaryPermissionSection(panel, currentConversation); AddTaskSummaryBackgroundSection(panel); }