도구·권한·스킬 표현 정교화 1차 반영
Some checks failed
Release Gate / gate (push) Has been cancelled

- 권한 요청 카탈로그를 bash/powershell/web_fetch/mcp/skill/file_edit/file_write/git/document/filesystem 수준으로 세분화했습니다.

- 도구 결과 카탈로그에 approval_required, partial, follow-up hint, attention 메타를 추가해 후속 renderer 고도화 기반을 마련했습니다.

- 스킬 갤러리에 모델, 추론 강도, 실행 컨텍스트, 에이전트, 모델 호출 비활성화, 추천 상황을 표시하도록 확장했습니다.

- README, DEVELOPMENT, parity plan, regression prompts 문서를 2026-04-06 11:52 (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-06 12:57:15 +09:00
parent f11b8b74b7
commit e747032501
7 changed files with 248 additions and 82 deletions

View File

@@ -1206,3 +1206,7 @@ MIT License
- 업데이트: 2026-04-06 11:34 (KST)
- footer 작업 바의 chip 버튼 시각 언어를 맞췄다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml)에 `FooterChipBtn` 스타일을 추가하고, 하단의 `권한`, `Git 브랜치` 버튼이 같은 라운드/테두리/패딩 규칙을 쓰도록 정리했다.
- 이 단계는 구조 분리 이후의 visual polish 후속 작업으로, footer의 기능 버튼이 각각 다른 컨트롤처럼 보이던 차이를 줄이고 작업 바 전체를 하나의 도구 행처럼 느끼게 만드는 데 초점을 맞췄다.
- 업데이트: 2026-04-06 11:52 (KST)
- `claw-code` 대비 남아 있던 `도구/권한/스킬 표현 정교화` 1차를 반영했다. [PermissionRequestPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs)는 `bash`, `powershell`, `command`, `web_fetch`, `mcp`, `skill`, `question`, `file_edit`, `file_write`, `git`, `document`, `filesystem`까지 세분화하고 `ActionHint`, `Severity`, `RequiresPreview` 메타를 추가했다.
- [ToolResultPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs)는 기존 `success / error / reject / cancel`에 더해 `approval_required`, `partial` 상태와 `FollowUpHint`, `NeedsAttention` 메타를 추가해 후속 안내 품질을 높일 기반을 마련했다.
- [SkillGalleryWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SkillGalleryWindow.xaml.cs)는 스킬 상세에 `모델`, `추론 강도`, `실행 컨텍스트`, `에이전트`, `모델 호출 비활성화`, `추천 상황`을 표시하도록 확장해, AX 스킬도 `claw-code`처럼 실행 정책이 보이는 방향으로 정리했다.

View File

@@ -108,3 +108,21 @@
- Cowork 변경: 3, 4, 7, 8
- Code 변경: 5, 6, 7, 9, 10
- 체크 후 문서 이력에는 “어떤 묶음을 확인했는지”를 간단히 남깁니다.
## Tool / Permission Follow-up
11. 권한 거부 후 재시도
- 프롬프트 순서:
- `src 폴더에서 설정 파일을 수정해줘`
- 첫 권한 요청은 거부
- 같은 작업을 다시 요청
- 확인:
- `reject``approval_required`가 같은 결과 카드처럼 보이지 않음
- 재시도 시 권한 메시지와 도구 결과가 중복되지 않음
- `bad-approval-flow`
12. 부분 성공 / 후속 안내
- 프롬프트: `여러 문서 파일을 한 번에 읽고 요약해줘`
- 확인:
- 일부 실패가 있으면 `partial` 계열 안내가 보이는지
- 후속 안내 문구가 단순 실패와 다르게 보이는지
- `status-noise`

View File

@@ -4941,3 +4941,6 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
- Document update: 2026-04-06 11:27 (KST) - At this point preview, file browser, worktree chooser, and permission-mode popup surfaces all share nearly the same visual language; remaining work is minor spacing/color polish rather than another behavior or structure change.
- Document update: 2026-04-06 11:34 (KST) - Added `FooterChipBtn` to `ChatWindow.xaml` and aligned the footer work-bar buttons (`권한`, `Git 브랜치`) to the same rounded border, padding, and hover language instead of mixing outline and ghost button styles.
- Document update: 2026-04-06 11:34 (KST) - This is a footer polish follow-up after the structure split, aimed at making the bottom work bar feel like one coherent tool row rather than a set of unrelated controls.
- Document update: 2026-04-06 11:52 (KST) - Expanded `PermissionRequestPresentationCatalog.cs` into an action-level permission taxonomy (`bash`, `powershell`, `command`, `web_fetch`, `mcp`, `skill`, `question`, `file_edit`, `file_write`, `git`, `document`, `filesystem`) and added `ActionHint`, `Severity`, `RequiresPreview` metadata for later renderer refinement.
- Document update: 2026-04-06 11:52 (KST) - Expanded `ToolResultPresentationCatalog.cs` with richer result taxonomy and post-result guidance. AX now distinguishes `approval_required` and `partial` in addition to `success / error / reject / cancel`, and each result carries `FollowUpHint` and `NeedsAttention`.
- Document update: 2026-04-06 11:52 (KST) - Extended `SkillGalleryWindow.xaml.cs` to expose runtime-policy metadata (`model`, `effort`, `execution context`, `agent`, `disable model invocation`, `when to use`) so AX skills are easier to inspect and maintain like `claw-code` bundled skills.

View File

@@ -19,6 +19,8 @@
- Formalized the regression ritual in `docs/AX_AGENT_REGRESSION_PROMPTS.md` by adding failure classes (`blank-reply`, `duplicate-banner`, `bad-approval-flow`, `queue-drift`, `restore-drift`, `status-noise`) and required prompt bundles per change area.
- Updated: 2026-04-06 10:07 (KST)
- Continued the maintainability track by moving topic preset rendering, custom preset context menus, and topic-selection application flow into `ChatWindow.TopicPresetPresentation.cs`. This reduces mixed preset UI logic inside `ChatWindow.xaml.cs` and keeps the main window closer to orchestration-only responsibility.
- Updated: 2026-04-06 11:52 (KST)
- Continued the tool/permission/skill sophistication track by expanding AX presentation catalogs toward `claw-code` specificity. `PermissionRequestPresentationCatalog.cs` now models action-level permission kinds plus severity/action hints, `ToolResultPresentationCatalog.cs` now models `approval_required`/`partial` result states plus follow-up guidance, and the AX skill gallery now exposes runtime-policy metadata that was previously hidden.
## Preserved History (Summary)
- Core loop guards and post-tool verification gates are already partially implemented.

View File

@@ -5,8 +5,11 @@ internal sealed record PermissionRequestPresentation(
string Icon,
string Label,
string Description,
string ActionHint,
string BackgroundHex,
string ForegroundHex);
string ForegroundHex,
string Severity,
bool RequiresPreview);
internal static class PermissionRequestPresentationCatalog
{
@@ -14,88 +17,108 @@ internal static class PermissionRequestPresentationCatalog
{
var tool = (toolName ?? string.Empty).Trim().ToLowerInvariant();
if (tool.Contains("bash") || tool.Contains("powershell") || tool.Contains("process"))
return Build(
"command",
pending,
"\uE756",
"명령 실행 권한 요청",
"명령 실행 권한 확인",
"터미널 명령을 실행하기 전에 확인이 필요합니다.",
if (tool.Contains("bash"))
return Build("bash", pending, "\uE756",
"Bash 실행 권한 요청", "Bash 실행 확인",
"셸 명령을 실행하기 전에 확인이 필요합니다.",
"Bash 실행이 승인되어 계속 진행합니다.",
"명령과 작업 위치를 확인하세요.",
"#FEF2F2", "#DC2626", "high", false);
if (tool.Contains("powershell"))
return Build("powershell", pending, "\uE756",
"PowerShell 권한 요청", "PowerShell 실행 확인",
"PowerShell 명령을 실행하기 전에 확인이 필요합니다.",
"PowerShell 실행이 승인되어 계속 진행합니다.",
"스크립트와 실행 범위를 확인하세요.",
"#FEF2F2", "#DC2626", "high", false);
if (tool.Contains("process") || tool.Contains("build") || tool.Contains("test"))
return Build("command", pending, "\uE756",
"명령 실행 권한 요청", "명령 실행 확인",
"명령 또는 빌드 작업 실행 전에 확인이 필요합니다.",
"명령 실행이 승인되어 계속 진행합니다.",
"#FEF2F2",
"#DC2626");
"실행 명령과 영향 범위를 확인하세요.",
"#FEF2F2", "#DC2626", "high", false);
if (tool.Contains("web") || tool.Contains("fetch") || tool.Contains("http"))
return Build(
"web",
pending,
"\uE774",
"웹 요청 권한 요청",
"웹 요청 권한 확인",
"외부 웹 요청 전 사용자 확인이 필요합니다.",
return Build("web_fetch", pending, "\uE774",
"웹 요청 권한 요청", "웹 요청 확인",
"외부 요청을 보내기 전에 확인이 필요합니다.",
"웹 요청이 승인되어 계속 진행합니다.",
"#FFF7ED",
"#C2410C");
"조회 URL과 전송 범위를 확인하세요.",
"#FFF7ED", "#C2410C", "medium", false);
if (tool.Contains("mcp"))
return Build("mcp", pending, "\uE943",
"MCP 도구 권한 요청", "MCP 도구 확인",
"연결된 MCP 도구 사용 전에 확인이 필요합니다.",
"MCP 도구 사용이 승인되어 계속 진행합니다.",
"서버와 호출 의도를 확인하세요.",
"#F5F3FF", "#7C3AED", "medium", false);
if (tool.Contains("skill"))
return Build(
"skill",
pending,
"\uE8A5",
"스킬 실행 권한 요청",
"스킬 실행 권한 확인",
"연결된 스킬 실행 전 확인이 필요합니다.",
return Build("skill", pending, "\uE8A5",
"스킬 실행 권한 요청", "스킬 실행 확인",
"연결된 스킬을 실행하기 전에 확인이 필요합니다.",
"스킬 실행이 승인되어 계속 진행합니다.",
"#F5F3FF",
"#7C3AED");
"허용 도구와 실행 컨텍스트를 확인하세요.",
"#F5F3FF", "#7C3AED", "medium", false);
if (tool.Contains("ask"))
return Build(
"question",
pending,
"\uE897",
"의견 요청 확인",
"의견 요청 완료",
return Build("question", pending, "\uE897",
"의견 요청 확인", "의견 요청 완료",
"사용자에게 선택이나 답변을 요청합니다.",
"사용자 의견을 받아 다음 단계로 진행합니다.",
"#EFF6FF",
"#2563EB");
"사용자 답변을 받아 다음 단계로 진행합니다.",
"질문 의도와 선택지를 확인하세요.",
"#EFF6FF", "#2563EB", "low", false);
if (tool.Contains("file_edit") || tool.Contains("file_write") || tool.Contains("edit"))
return Build(
"file_edit",
pending,
"\uE70F",
"파일 수정 권한 요청",
"파일 수정 권한 확인",
"파일을 만들거나 수정하기 전에 확인이 필요합니다.",
if (tool.Contains("file_edit") || tool.Contains("edit"))
return Build("file_edit", pending, "\uE70F",
"파일 수정 권한 요청", "파일 수정 확인",
"파일을 변경하기 전에 확인이 필요합니다.",
"파일 수정이 승인되어 계속 진행합니다.",
"#FFF7ED",
"#C2410C");
"변경 diff와 대상 파일을 확인하세요.",
"#FFF7ED", "#C2410C", "high", true);
if (tool.Contains("file_write") || tool.Contains("write"))
return Build("file_write", pending, "\uE70F",
"파일 쓰기 권한 요청", "파일 쓰기 확인",
"새 파일 작성 또는 덮어쓰기 전에 확인이 필요합니다.",
"파일 쓰기가 승인되어 계속 진행합니다.",
"작성 위치와 새 내용 미리보기를 확인하세요.",
"#FFF7ED", "#C2410C", "high", true);
if (tool.Contains("git"))
return Build("git", pending, "\uE8A7",
"Git 작업 권한 요청", "Git 작업 확인",
"브랜치나 커밋 상태를 바꾸기 전에 확인이 필요합니다.",
"Git 작업이 승인되어 계속 진행합니다.",
"브랜치와 변경 범위를 확인하세요.",
"#EFF6FF", "#2563EB", "medium", false);
if (tool.Contains("document") || tool.Contains("template") || tool.Contains("format"))
return Build("document", pending, "\uE8A5",
"문서 작업 권한 요청", "문서 작업 확인",
"문서 생성 또는 변환 작업 전에 확인이 필요합니다.",
"문서 작업이 승인되어 계속 진행합니다.",
"출력 형식과 저장 위치를 확인하세요.",
"#FFF7ED", "#C2410C", "medium", false);
if (tool.Contains("file") || tool.Contains("glob") || tool.Contains("grep") || tool.Contains("folder"))
return Build(
"file_access",
pending,
"\uE8A5",
"파일 접근 권한 요청",
"파일 접근 권한 확인",
"폴더와 파일 내용을 읽기 전에 확인이 필요합니다.",
return Build("filesystem", pending, "\uE8A5",
"파일 접근 권한 요청", "파일 접근 확인",
"폴더나 파일 내용을 읽기 전에 확인이 필요합니다.",
"파일 접근이 승인되어 계속 진행합니다.",
"#FFF7ED",
"#C2410C");
"읽기 범위와 접근 경로를 확인하세요.",
"#FFF7ED", "#C2410C", "medium", false);
return Build(
"generic",
pending,
"\uE897",
"권한 요청",
"권한 확인",
"계속 진행하기 전에 사용자 확인이 필요합니다.",
return Build("generic", pending, "\uE897",
"권한 요청", "권한 확인",
"계속 진행하기 전에 사용자의 확인이 필요합니다.",
"요청이 승인되어 계속 진행합니다.",
"#FFF7ED",
"#C2410C");
"실행 의도와 대상 범위를 확인하세요.",
"#FFF7ED", "#C2410C", "medium", false);
}
private static PermissionRequestPresentation Build(
@@ -106,15 +129,21 @@ internal static class PermissionRequestPresentationCatalog
string resolvedLabel,
string pendingDescription,
string resolvedDescription,
string actionHint,
string backgroundHex,
string foregroundHex)
string foregroundHex,
string severity,
bool requiresPreview)
{
return new PermissionRequestPresentation(
kind,
pending ? icon : "\uE73E",
pending ? pendingLabel : resolvedLabel,
pending ? pendingDescription : resolvedDescription,
actionHint,
pending ? backgroundHex : "#ECFDF5",
pending ? foregroundHex : "#059669");
pending ? foregroundHex : "#059669",
severity,
requiresPreview);
}
}

View File

@@ -5,9 +5,11 @@ internal sealed record ToolResultPresentation(
string Icon,
string Label,
string Description,
string FollowUpHint,
string BackgroundHex,
string ForegroundHex,
string StatusKind);
string StatusKind,
bool NeedsAttention);
internal static class ToolResultPresentationCatalog
{
@@ -27,9 +29,11 @@ internal static class ToolResultPresentationCatalog
"\uE711",
$"{baseLabel} 취소",
"요청이 중단되어 결과가 취소되었습니다.",
"필요하면 같은 요청을 다시 실행하세요.",
"#F8FAFC",
"#475569",
"cancel");
"cancel",
false);
}
if (summary.Contains("거부", StringComparison.OrdinalIgnoreCase) ||
@@ -41,9 +45,41 @@ internal static class ToolResultPresentationCatalog
"\uE783",
$"{baseLabel} 거부",
"권한이 거부되어 작업이 중단되었습니다.",
"권한 모드를 바꾸거나 다시 승인하면 이어서 진행할 수 있습니다.",
"#FEF2F2",
"#DC2626",
"reject");
"reject",
true);
}
if (summary.Contains("승인 필요", StringComparison.OrdinalIgnoreCase) ||
summary.Contains("확인 필요", StringComparison.OrdinalIgnoreCase))
{
return new ToolResultPresentation(
kind,
"\uE8D7",
$"{baseLabel} 승인 대기",
"다음 단계로 진행하려면 사용자 승인이 필요합니다.",
"승인 후 같은 작업 흐름이 이어집니다.",
"#FFF7ED",
"#C2410C",
"approval_required",
true);
}
if (summary.Contains("부분", StringComparison.OrdinalIgnoreCase) ||
summary.Contains("일부", StringComparison.OrdinalIgnoreCase))
{
return new ToolResultPresentation(
kind,
"\uE7BA",
$"{baseLabel} 부분 완료",
"일부 단계만 완료되어 후속 확인이나 재실행이 필요할 수 있습니다.",
"남은 단계나 누락된 결과를 확인하세요.",
"#FFFBEA",
"#A16207",
"partial",
true);
}
if (!evt.Success || evt.Type == AgentEventType.Error)
@@ -53,9 +89,11 @@ internal static class ToolResultPresentationCatalog
"\uE783",
BuildFailureLabel(kind, baseLabel),
BuildFailureDescription(kind),
BuildFailureFollowUp(kind),
"#FEF2F2",
"#DC2626",
"error");
"error",
true);
}
return new ToolResultPresentation(
@@ -63,23 +101,35 @@ internal static class ToolResultPresentationCatalog
"\uE73E",
BuildSuccessLabel(kind, baseLabel),
BuildSuccessDescription(kind),
BuildSuccessFollowUp(kind),
"#ECFDF5",
"#16A34A",
"success");
"success",
false);
}
private static string ResolveKind(string tool)
{
if (tool.Contains("file_edit"))
return "file_edit";
if (tool.Contains("file_write"))
return "file_write";
if (tool.Contains("file_read") || tool.Contains("glob") || tool.Contains("grep"))
return "filesystem";
if (tool.Contains("file"))
return "file";
if (tool.Contains("build") || tool.Contains("test"))
return "build";
return "build_test";
if (tool.Contains("git") || tool.Contains("diff"))
return "git";
if (tool.Contains("document") || tool.Contains("format") || tool.Contains("template"))
return "document";
if (tool.Contains("skill"))
return "skill";
if (tool.Contains("mcp"))
return "mcp";
if (tool.Contains("ask"))
return "question";
if (tool.Contains("web") || tool.Contains("fetch") || tool.Contains("http"))
return "web";
if (tool.Contains("process") || tool.Contains("bash") || tool.Contains("powershell"))
@@ -91,11 +141,16 @@ internal static class ToolResultPresentationCatalog
{
return kind switch
{
"file_edit" => "파일 수정 완료",
"file_write" => "파일 쓰기 완료",
"filesystem" => "파일 탐색 완료",
"file" => "파일 작업 완료",
"build" => "빌드/테스트 완료",
"build_test" => "빌드/테스트 완료",
"git" => "Git 작업 완료",
"document" => "문서 작업 완료",
"skill" => "스킬 실행 완료",
"mcp" => "MCP 도구 완료",
"question" => "의견 요청 완료",
"web" => "웹 요청 완료",
"command" => "명령 실행 완료",
_ => baseLabel,
@@ -106,11 +161,16 @@ internal static class ToolResultPresentationCatalog
{
return kind switch
{
"file_edit" => "파일 수정 실패",
"file_write" => "파일 쓰기 실패",
"filesystem" => "파일 탐색 실패",
"file" => "파일 작업 실패",
"build" => "빌드/테스트 실패",
"build_test" => "빌드/테스트 실패",
"git" => "Git 작업 실패",
"document" => "문서 작업 실패",
"skill" => "스킬 실행 실패",
"mcp" => "MCP 도구 실패",
"question" => "의견 요청 실패",
"web" => "웹 요청 실패",
"command" => "명령 실행 실패",
_ => $"{baseLabel} 실패",
@@ -121,12 +181,17 @@ internal static class ToolResultPresentationCatalog
{
return kind switch
{
"file" => "파일 처리 결과가 정상적으로 반영되었습니다.",
"build" => "빌드나 테스트 단계가 성공적으로 끝났습니다.",
"git" => "Git 관련 작업이 정상적으로 완료되었습니다.",
"file_edit" => "파일 수정 결과가 저장되었습니다.",
"file_write" => "새 파일 작성 결과가 저장되었습니다.",
"filesystem" => "파일과 폴더 정보를 성공적으로 었습니다.",
"file" => "파일 관련 작업이 정상적으로 끝났습니다.",
"build_test" => "빌드 또는 테스트 단계가 성공적으로 끝났습니다.",
"git" => "Git 관련 작업이 정상적으로 끝났습니다.",
"document" => "문서 생성 또는 변환 작업이 완료되었습니다.",
"skill" => "선택 스킬이 정상적으로 실행되었습니다.",
"web" => "웹 요청이 정상적으로 완료되었습니다.",
"skill" => "선택 스킬이 정상적으로 실행되었습니다.",
"mcp" => "등록된 MCP 도구 호출이 성공적으로 끝났습니다.",
"question" => "사용자 응답을 받아 다음 단계로 넘어갈 수 있습니다.",
"web" => "웹 요청이 정상적으로 끝났습니다.",
"command" => "명령 실행이 정상적으로 끝났습니다.",
_ => "요청한 작업이 정상적으로 완료되었습니다.",
};
@@ -136,14 +201,47 @@ internal static class ToolResultPresentationCatalog
{
return kind switch
{
"file_edit" => "파일 변경 과정에서 문제가 발생했습니다.",
"file_write" => "파일 작성 또는 저장 과정에서 문제가 발생했습니다.",
"filesystem" => "파일/폴더 접근 중 문제가 발생했습니다.",
"file" => "파일 처리 중 문제가 발생했습니다.",
"build" => "빌드 테스트 단계에서 실패가 발생했습니다.",
"build_test" => "빌드 또는 테스트 단계에서 실패가 발생했습니다.",
"git" => "Git 관련 작업이 실패했습니다.",
"document" => "문서 생성 또는 변환 작업이 실패했습니다.",
"skill" => "스킬 실행 중 문제가 발생했습니다.",
"mcp" => "MCP 도구 호출 중 문제가 발생했습니다.",
"question" => "사용자 의견 요청 과정에서 문제가 발생했습니다.",
"web" => "웹 요청 처리에 실패했습니다.",
"command" => "명령 실행 중 오류가 발생했습니다.",
_ => "작업 처리 중 오류가 발생했습니다.",
};
}
private static string BuildSuccessFollowUp(string kind)
{
return kind switch
{
"file_edit" or "file_write" => "변경 내용을 preview나 diff에서 다시 확인할 수 있습니다.",
"build_test" => "출력 로그와 후속 수정 필요 여부를 확인하세요.",
"git" => "브랜치 상태나 변경 요약을 이어서 확인하세요.",
"document" => "생성된 산출물 경로를 열어 결과를 확인하세요.",
"skill" => "같은 스킬을 다른 입력으로 이어서 실행할 수 있습니다.",
_ => "필요하면 후속 요청을 이어서 실행할 수 있습니다.",
};
}
private static string BuildFailureFollowUp(string kind)
{
return kind switch
{
"file_edit" or "file_write" => "대상 파일 경로와 권한, diff를 다시 확인하세요.",
"build_test" => "실패 로그와 컴파일 오류 메시지를 먼저 확인하세요.",
"git" => "현재 브랜치, 잠금 상태, 충돌 여부를 확인하세요.",
"document" => "입력 데이터와 출력 형식, 저장 위치를 다시 확인하세요.",
"skill" => "허용 도구와 런타임 요구사항을 다시 확인하세요.",
"web" => "연결 상태와 요청 대상 URL을 다시 확인하세요.",
"mcp" => "MCP 서버 연결 상태와 도구 등록 상태를 다시 확인하세요.",
_ => "같은 요청을 재시도하기 전에 원인 메시지를 먼저 확인하세요.",
};
}
}

View File

@@ -578,6 +578,18 @@ public partial class SkillGalleryWindow : Window
AddMetaRow("런타임", skill.Requires, metaRow++);
if (!string.IsNullOrEmpty(skill.AllowedTools))
AddMetaRow("허용 도구", skill.AllowedTools, metaRow++);
if (!string.IsNullOrEmpty(skill.Model))
AddMetaRow("모델", skill.Model, metaRow++);
if (!string.IsNullOrEmpty(skill.Effort))
AddMetaRow("추론 강도", skill.Effort, metaRow++);
if (!string.IsNullOrEmpty(skill.ExecutionContext))
AddMetaRow("실행 컨텍스트", skill.ExecutionContext, metaRow++);
if (!string.IsNullOrEmpty(skill.Agent))
AddMetaRow("에이전트", skill.Agent, metaRow++);
if (skill.DisableModelInvocation)
AddMetaRow("모델 호출", "비활성화", metaRow++);
if (!string.IsNullOrEmpty(skill.WhenToUse))
AddMetaRow("추천 상황", skill.WhenToUse, metaRow++);
AddMetaRow("상태", skill.IsAvailable ? "✓ 사용 가능" : $"✗ {skill.UnavailableHint}", metaRow++);
AddMetaRow("경로", skill.FilePath, metaRow++);