변경 목적: - AX Agent의 도구 이름, 내부 설정, 스킬 정책, 실행 루프 사이의 불일치를 줄이고 전체 동작 품질을 높인다. - claw-code 수준의 일관된 동작 품질을 참고하되 AX 구조에 맞는 고유한 카탈로그·정규화 레이어로 재구성한다. 핵심 수정사항: - 도구 canonical id, legacy alias, 탭 노출, 설정 카테고리, read-only 분류를 중앙 카탈로그로 통합했다. - ToolRegistry, AgentLoopService, 병렬 실행 분류, 권한 처리, 훅 처리, 스킬 allowed-tools 해석이 같은 이름 체계를 사용하도록 정리했다. - Agent 설정/일반 설정/도움말의 도구 카드와 훅 편집기, 스킬 설명을 현재 런타임 구조에 맞게 갱신했다. - 컨텍스트 압축, intent gate, spawn agents, session learning, model prompt adapter, workspace context 관련 변경과 테스트 추가를 함께 반영했다. - 문서 이력과 비교/로드맵 문서를 최신 상태로 갱신했다. 검증 결과: - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_toolcat\ -p:IntermediateOutputPath=obj\verify_toolcat\ : 경고 0 / 오류 0 - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentToolCatalogTests -p:OutputPath=bin\verify_toolcat_tests\ -p:IntermediateOutputPath=obj\verify_toolcat_tests\ : 통과 8
328 lines
12 KiB
C#
328 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Animation;
|
|
using System.Windows.Media.Effects;
|
|
|
|
namespace AxCopilot.Views;
|
|
|
|
/// <summary>
|
|
/// 도구 실행 승인을 위한 간결한 별도 다이얼로그 창.
|
|
/// PlanViewerV2(전체 계획 뷰어)와 분리하여 도구 단위 승인에 사용합니다.
|
|
/// </summary>
|
|
internal sealed class ToolApprovalWindow : Window
|
|
{
|
|
private string? _result;
|
|
|
|
private ToolApprovalWindow(string message, List<string> options)
|
|
{
|
|
Width = 500;
|
|
MinWidth = 400;
|
|
MaxWidth = 600;
|
|
SizeToContent = SizeToContent.Height;
|
|
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
|
ResizeMode = ResizeMode.NoResize;
|
|
WindowStyle = WindowStyle.None;
|
|
AllowsTransparency = true;
|
|
Background = Brushes.Transparent;
|
|
ShowInTaskbar = false;
|
|
Topmost = true;
|
|
|
|
var bg = Application.Current.TryFindResource("LauncherBackground") as Brush
|
|
?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
|
|
var primary = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
|
var secondary = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
var accent = Application.Current.TryFindResource("AccentColor") as Brush
|
|
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
|
var border = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
|
var itemBg = Application.Current.TryFindResource("ItemBackground") as Brush
|
|
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40));
|
|
var errorBrush = Application.Current.TryFindResource("ErrorColor") as Brush
|
|
?? new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26));
|
|
var hoverBg = Application.Current.TryFindResource("ItemHoverBackground") as Brush
|
|
?? new SolidColorBrush(Color.FromArgb(0x20, 0xFF, 0xFF, 0xFF));
|
|
|
|
var root = new Border
|
|
{
|
|
Background = bg,
|
|
BorderBrush = border,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(14),
|
|
Padding = new Thickness(20, 16, 20, 16),
|
|
Effect = new DropShadowEffect
|
|
{
|
|
BlurRadius = 20,
|
|
ShadowDepth = 4,
|
|
Opacity = 0.35,
|
|
Color = Colors.Black,
|
|
},
|
|
};
|
|
|
|
var stack = new StackPanel();
|
|
|
|
// Header
|
|
var header = new Grid { Margin = new Thickness(0, 0, 0, 12) };
|
|
header.MouseLeftButtonDown += (_, _) => { try { DragMove(); } catch { } };
|
|
header.Children.Add(new StackPanel
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
Children =
|
|
{
|
|
new TextBlock
|
|
{
|
|
Text = "\uE946", // Shield icon
|
|
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
FontSize = 15,
|
|
Foreground = accent,
|
|
Margin = new Thickness(0, 0, 8, 0),
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
},
|
|
new TextBlock
|
|
{
|
|
Text = "실행 확인",
|
|
FontSize = 13.5,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = primary,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
}
|
|
}
|
|
});
|
|
|
|
// Close button
|
|
var close = new Border
|
|
{
|
|
Width = 26,
|
|
Height = 26,
|
|
CornerRadius = new CornerRadius(7),
|
|
Background = Brushes.Transparent,
|
|
Cursor = Cursors.Hand,
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Child = new TextBlock
|
|
{
|
|
Text = "\uE8BB",
|
|
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
FontSize = 10,
|
|
Foreground = secondary,
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
},
|
|
};
|
|
close.MouseLeftButtonUp += (_, _) => { _result = "취소"; Close(); };
|
|
close.MouseEnter += (_, _) => close.Background = hoverBg;
|
|
close.MouseLeave += (_, _) => close.Background = Brushes.Transparent;
|
|
header.Children.Add(close);
|
|
stack.Children.Add(header);
|
|
|
|
// Message content
|
|
var msgBorder = new Border
|
|
{
|
|
Background = itemBg,
|
|
CornerRadius = new CornerRadius(12),
|
|
Padding = new Thickness(14, 11, 14, 11),
|
|
Margin = new Thickness(0, 0, 0, 14),
|
|
};
|
|
|
|
var msgText = new TextBlock
|
|
{
|
|
Text = message,
|
|
FontSize = 13,
|
|
FontFamily = new FontFamily("Segoe UI"),
|
|
Foreground = primary,
|
|
TextWrapping = TextWrapping.Wrap,
|
|
LineHeight = 20,
|
|
};
|
|
msgBorder.Child = msgText;
|
|
stack.Children.Add(msgBorder);
|
|
|
|
// Buttons
|
|
var btnPanel = new StackPanel
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
};
|
|
|
|
foreach (var option in options)
|
|
{
|
|
var btn = CreateOptionButton(option, primary, secondary, accent, bg, errorBrush);
|
|
btn.MouseLeftButtonUp += (_, _) => { _result = option; Close(); };
|
|
btnPanel.Children.Add(btn);
|
|
}
|
|
stack.Children.Add(btnPanel);
|
|
|
|
root.Child = stack;
|
|
Content = root;
|
|
|
|
// Entrance animation
|
|
root.Opacity = 0;
|
|
root.RenderTransformOrigin = new Point(0.5, 0.5);
|
|
root.RenderTransform = new ScaleTransform(0.96, 0.96);
|
|
Loaded += (_, _) =>
|
|
{
|
|
root.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(140)));
|
|
var sx = new DoubleAnimation(0.96, 1, TimeSpan.FromMilliseconds(180))
|
|
{
|
|
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut },
|
|
};
|
|
var sy = new DoubleAnimation(0.96, 1, TimeSpan.FromMilliseconds(180))
|
|
{
|
|
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut },
|
|
};
|
|
((ScaleTransform)root.RenderTransform).BeginAnimation(ScaleTransform.ScaleXProperty, sx);
|
|
((ScaleTransform)root.RenderTransform).BeginAnimation(ScaleTransform.ScaleYProperty, sy);
|
|
};
|
|
|
|
// ESC to cancel
|
|
KeyDown += (_, e) =>
|
|
{
|
|
if (e.Key == Key.Escape)
|
|
{
|
|
_result = "취소";
|
|
Close();
|
|
}
|
|
};
|
|
}
|
|
|
|
private Border CreateOptionButton(string label, Brush primary, Brush secondary, Brush accent, Brush bg, Brush errorBrush)
|
|
{
|
|
Brush foreground, background, borderBrush;
|
|
switch (label)
|
|
{
|
|
case "확인":
|
|
case "승인":
|
|
foreground = Brushes.White;
|
|
background = accent;
|
|
borderBrush = accent;
|
|
break;
|
|
case "취소":
|
|
case "중단":
|
|
foreground = errorBrush;
|
|
background = Brushes.Transparent;
|
|
borderBrush = errorBrush;
|
|
break;
|
|
default:
|
|
foreground = primary;
|
|
background = Brushes.Transparent;
|
|
borderBrush = secondary;
|
|
break;
|
|
}
|
|
|
|
var isDestructive = label == "취소" || label == "중단";
|
|
|
|
// Build inner content: optional left accent bar + label
|
|
FrameworkElement child;
|
|
if (isDestructive)
|
|
{
|
|
var grid = new Grid();
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
|
var accentBar = new Border
|
|
{
|
|
Width = 4,
|
|
CornerRadius = new CornerRadius(2),
|
|
Background = errorBrush,
|
|
Margin = new Thickness(0, 4, 8, 4),
|
|
VerticalAlignment = VerticalAlignment.Stretch,
|
|
};
|
|
Grid.SetColumn(accentBar, 0);
|
|
grid.Children.Add(accentBar);
|
|
var txt = new TextBlock
|
|
{
|
|
Text = label,
|
|
FontSize = 12,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = foreground,
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
};
|
|
Grid.SetColumn(txt, 1);
|
|
grid.Children.Add(txt);
|
|
child = grid;
|
|
}
|
|
else
|
|
{
|
|
child = new TextBlock
|
|
{
|
|
Text = label,
|
|
FontSize = 12,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = foreground,
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
};
|
|
}
|
|
|
|
var btn = new Border
|
|
{
|
|
MinWidth = 84,
|
|
Height = 36,
|
|
CornerRadius = new CornerRadius(10),
|
|
Background = background,
|
|
BorderBrush = borderBrush,
|
|
BorderThickness = new Thickness(1),
|
|
Padding = new Thickness(14, 0, 14, 0),
|
|
Margin = new Thickness(10, 0, 0, 0),
|
|
Cursor = Cursors.Hand,
|
|
Child = child,
|
|
};
|
|
|
|
btn.MouseEnter += (_, _) => btn.Opacity = 0.85;
|
|
btn.MouseLeave += (_, _) => btn.Opacity = 1.0;
|
|
|
|
return btn;
|
|
}
|
|
|
|
/// <summary>도구 승인 다이얼로그를 표시하고 결과를 반환합니다.</summary>
|
|
internal static string? Show(Window? owner, string message, List<string> options)
|
|
=> Show(owner, message, options, CancellationToken.None);
|
|
|
|
/// <summary>
|
|
/// 도구 승인 다이얼로그를 표시합니다. cancellationToken이 트리거되면 창을 자동으로 닫고 null을 반환합니다.
|
|
/// 장시간 미응답 시 타임아웃으로 에이전트 루프가 멈추는 것을 방지하기 위해 사용합니다.
|
|
/// </summary>
|
|
internal static string? Show(Window? owner, string message, List<string> options, CancellationToken cancellationToken)
|
|
{
|
|
var dialog = new ToolApprovalWindow(message, options);
|
|
if (owner != null && IsWindowAlive(owner))
|
|
{
|
|
dialog.WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
|
dialog.Owner = owner;
|
|
}
|
|
|
|
CancellationTokenRegistration reg = default;
|
|
if (cancellationToken.CanBeCanceled)
|
|
{
|
|
reg = cancellationToken.Register(() =>
|
|
{
|
|
// UI 스레드에서 안전하게 닫기
|
|
dialog.Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
try { if (dialog.IsVisible) dialog.Close(); }
|
|
catch { /* 이미 닫혀있거나 파괴 중 — 무시 */ }
|
|
}));
|
|
});
|
|
}
|
|
|
|
try
|
|
{
|
|
dialog.ShowDialog();
|
|
}
|
|
finally
|
|
{
|
|
reg.Dispose();
|
|
}
|
|
|
|
return dialog._result;
|
|
}
|
|
|
|
private static bool IsWindowAlive(Window? w)
|
|
{
|
|
if (w == null) return false;
|
|
try { var _ = w.IsVisible; return true; }
|
|
catch { return false; }
|
|
}
|
|
}
|