AX Agent 도구·스킬 transcript 표현을 claw-code 기준으로 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
- 도구/스킬 이벤트 배지를 역할 중심 라벨로 단순화 - raw snake_case 대신 사람이 읽기 쉬운 표시명과 /skill 형식 적용 - 작업 요약 팝업의 권한/배경 작업 관측성 섹션을 debug 수준으로 제한 - parity 문서와 개발 이력 문서에 tool/skill UX 정리 기준 반영 검증 결과 - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ - 경고 0 / 오류 0
This commit is contained in:
@@ -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`를 먼저 띄우지 않고, 본문 안에서 선택지/직접 입력/전달로 응답을 완료합니다.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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<TaskRunStore.TaskRun> FilterTaskSummaryItems(IEnumerable<TaskRunStore.TaskRun> 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user