AX Agent transcript 렌더 구조를 분리하고 권한/도구 결과 표시 체계를 정리한다
Some checks failed
Release Gate / gate (push) Has been cancelled

- ChatWindow.xaml.cs에 몰려 있던 의견 요청, 계획 승인, 작업 요약 렌더를 partial 파일로 분리해 transcript 책임을 낮췄다.

- PermissionRequestPresentationCatalog와 ToolResultPresentationCatalog를 추가해 권한 요청 및 도구 결과 badge를 타입별로 해석하도록 정리했다.

- AppStateService에 OperationalStatusPresentationState를 추가하고 상태선 계산을 presentation 계층으로 한 번 더 분리했다.

- README.md, docs/DEVELOPMENT.md, docs/claw-code-parity-plan.md에 2026-04-06 00:58 (KST) 기준 변경 내용을 반영했다.

- 검증: 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:
2026-04-05 22:55:56 +09:00
parent 13f0e23ed5
commit f0af86cc1e
9 changed files with 910 additions and 783 deletions

View File

@@ -0,0 +1,40 @@
namespace AxCopilot.Services.Agent;
internal sealed record PermissionRequestPresentation(
string Icon,
string Label,
string BackgroundHex,
string ForegroundHex);
internal static class PermissionRequestPresentationCatalog
{
public static PermissionRequestPresentation Resolve(string? toolName, bool pending)
{
var tool = toolName?.Trim().ToLowerInvariant() ?? "";
if (tool.Contains("process") || tool.Contains("bash") || tool.Contains("powershell"))
{
return pending
? new PermissionRequestPresentation("\uE756", "명령 권한 요청", "#FEF2F2", "#DC2626")
: new PermissionRequestPresentation("\uE73E", "명령 권한 허용", "#ECFDF5", "#059669");
}
if (tool.Contains("web") || tool.Contains("fetch") || tool.Contains("http"))
{
return pending
? new PermissionRequestPresentation("\uE774", "네트워크 권한 요청", "#FFF7ED", "#C2410C")
: new PermissionRequestPresentation("\uE73E", "네트워크 권한 허용", "#ECFDF5", "#059669");
}
if (tool.Contains("file"))
{
return pending
? new PermissionRequestPresentation("\uE8A5", "파일 권한 요청", "#FFF7ED", "#C2410C")
: new PermissionRequestPresentation("\uE73E", "파일 권한 허용", "#ECFDF5", "#059669");
}
return pending
? new PermissionRequestPresentation("\uE897", "권한 요청", "#FFF7ED", "#C2410C")
: new PermissionRequestPresentation("\uE73E", "권한 허용", "#ECFDF5", "#059669");
}
}

View File

@@ -0,0 +1,37 @@
namespace AxCopilot.Services.Agent;
internal sealed record ToolResultPresentation(
string Icon,
string Label,
string BackgroundHex,
string ForegroundHex,
string StatusKind);
internal static class ToolResultPresentationCatalog
{
public static ToolResultPresentation Resolve(AgentEvent evt, string fallbackLabel)
{
var summary = evt.Summary?.Trim() ?? "";
if (summary.Contains("취소", StringComparison.OrdinalIgnoreCase) ||
summary.Contains("중단", StringComparison.OrdinalIgnoreCase) ||
evt.Type == AgentEventType.StopRequested)
{
return new ToolResultPresentation("\uE711", "도구 취소", "#F8FAFC", "#475569", "cancel");
}
if (summary.Contains("거부", StringComparison.OrdinalIgnoreCase) ||
summary.Contains("반려", StringComparison.OrdinalIgnoreCase) ||
summary.Contains("권한 거부", StringComparison.OrdinalIgnoreCase))
{
return new ToolResultPresentation("\uE783", "도구 거부", "#FEF2F2", "#DC2626", "reject");
}
if (!evt.Success || evt.Type == AgentEventType.Error)
{
return new ToolResultPresentation("\uE783", "도구 실패", "#FEF2F2", "#DC2626", "error");
}
return new ToolResultPresentation("\uE73E", fallbackLabel, "#ECFDF5", "#16A34A", "success");
}
}

View File

@@ -158,6 +158,17 @@ public sealed class AppStateService
public string StripText { get; init; } = "";
}
public sealed class OperationalStatusPresentationState
{
public bool ShowRuntimeBadge { get; init; }
public string RuntimeLabel { get; init; } = "";
public bool ShowLastCompleted { get; init; }
public string LastCompletedText { get; init; } = "";
public bool ShowCompactStrip { get; init; }
public string StripKind { get; init; } = "none";
public string StripText { get; init; } = "";
}
public ChatSessionStateService? ChatSession { get; private set; }
public SkillCatalogState Skills { get; } = new();
public McpCatalogState Mcp { get; } = new();
@@ -620,6 +631,26 @@ public sealed class AppStateService
};
}
public OperationalStatusPresentationState GetOperationalStatusPresentation(string tab, bool hasLiveRuntimeActivity)
{
var status = GetOperationalStatus(tab);
var showCompactStrip = !string.Equals(tab, "Chat", StringComparison.OrdinalIgnoreCase)
&& (string.Equals(status.StripKind, "permission_waiting", StringComparison.OrdinalIgnoreCase)
|| string.Equals(status.StripKind, "failed_run", StringComparison.OrdinalIgnoreCase)
|| string.Equals(status.StripKind, "permission_denied", StringComparison.OrdinalIgnoreCase));
return new OperationalStatusPresentationState
{
ShowRuntimeBadge = status.ShowRuntimeBadge && hasLiveRuntimeActivity,
RuntimeLabel = status.RuntimeLabel,
ShowLastCompleted = status.ShowLastCompleted,
LastCompletedText = status.LastCompletedText,
ShowCompactStrip = showCompactStrip,
StripKind = showCompactStrip ? status.StripKind : "none",
StripText = showCompactStrip ? status.StripText : "",
};
}
public IReadOnlyList<DraftQueueItem> GetDraftQueueItems(string tab)
=> ChatSession?.GetDraftQueueItems(tab) ?? Array.Empty<DraftQueueItem>();