Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow: slash palette 이동 기준을 렌더 순서로 통일해 방향키/휠/Home/End 선택 체감 불일치를 해소 - ChatWindow: 최근 권한 거부 카드 액션 순서를 활용하지 않음→소극 활용→적극 활용→예외 해제로 정렬 - ChatWindow: /permissions, /allowed-tools 사용법 표기를 deny→default→acceptedits→plan→bypass→dontask→status 순서로 통일 - PermissionRequestWindow: 권한 선택/위험도/미리보기/명령 위험도 문구를 한국어 중심으로 정리하고 깨진 문자열을 복구 - README.md: 업데이트 시각을 2026-04-04 13:07(KST)로 갱신하고 이번 반영 항목 2건 추가 - docs/DEVELOPMENT.md: 연속 실행 25차 이력(슬래시 정합화, 권한 다이얼로그 복구, 품질 게이트 결과) 추가 - 검증: dotnet build 경고 0/오류 0, 슬래시/운영모드 필터 테스트 43건 통과
1172 lines
40 KiB
C#
1172 lines
40 KiB
C#
using System.Diagnostics;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Text;
|
||
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;
|
||
using AxCopilot.Services.Agent;
|
||
|
||
namespace AxCopilot.Views;
|
||
|
||
internal sealed class PermissionRequestWindow : Window
|
||
{
|
||
internal enum PermissionPromptResult
|
||
{
|
||
Reject,
|
||
AllowOnce,
|
||
AllowForSession,
|
||
}
|
||
|
||
private sealed record PermissionPromptProfile(
|
||
string HeaderIcon,
|
||
string HeaderTitle,
|
||
string Headline,
|
||
string RiskLabel,
|
||
Color RiskColor);
|
||
|
||
private sealed record UiExpressionProfile(
|
||
string Key,
|
||
int FilePeekLineCount,
|
||
int DiffLineCount,
|
||
int PreviewMaxHeight,
|
||
bool ShowHints,
|
||
bool ShowOptionDescription);
|
||
|
||
private PermissionPromptResult _result = PermissionPromptResult.Reject;
|
||
|
||
private PermissionRequestWindow(
|
||
string toolName,
|
||
string target,
|
||
AgentLoopService.PermissionPromptPreview? preview)
|
||
{
|
||
Width = 580;
|
||
MinWidth = 520;
|
||
MaxWidth = 760;
|
||
SizeToContent = SizeToContent.Height;
|
||
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||
ResizeMode = ResizeMode.NoResize;
|
||
WindowStyle = WindowStyle.None;
|
||
AllowsTransparency = true;
|
||
Background = Brushes.Transparent;
|
||
ShowInTaskbar = false;
|
||
Topmost = true;
|
||
|
||
var profile = ResolveProfile(toolName);
|
||
var uiProfile = ResolveUiExpressionProfile();
|
||
|
||
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 hoverBg = Application.Current.TryFindResource("ItemHoverBackground") as Brush
|
||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||
var riskBrush = new SolidColorBrush(profile.RiskColor);
|
||
|
||
var root = new Border
|
||
{
|
||
Background = bg,
|
||
BorderBrush = border,
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(16),
|
||
Padding = new Thickness(22, 18, 22, 16),
|
||
Effect = new DropShadowEffect
|
||
{
|
||
BlurRadius = 24,
|
||
ShadowDepth = 6,
|
||
Opacity = 0.35,
|
||
Color = Colors.Black,
|
||
},
|
||
};
|
||
|
||
var stack = new StackPanel();
|
||
|
||
var header = new Grid { Margin = new Thickness(0, 0, 0, 10) };
|
||
header.MouseLeftButtonDown += (_, _) => { try { DragMove(); } catch { } };
|
||
header.Children.Add(new StackPanel
|
||
{
|
||
Orientation = Orientation.Horizontal,
|
||
Children =
|
||
{
|
||
new TextBlock
|
||
{
|
||
Text = profile.HeaderIcon,
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 16,
|
||
Foreground = riskBrush,
|
||
Margin = new Thickness(0, 0, 8, 0),
|
||
},
|
||
new TextBlock
|
||
{
|
||
Text = GetHeaderTitle(profile.HeaderTitle, uiProfile),
|
||
FontSize = 14,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = primary,
|
||
}
|
||
}
|
||
});
|
||
|
||
var close = new Border
|
||
{
|
||
Width = 28,
|
||
Height = 28,
|
||
CornerRadius = new CornerRadius(8),
|
||
Background = Brushes.Transparent,
|
||
Cursor = Cursors.Hand,
|
||
HorizontalAlignment = HorizontalAlignment.Right,
|
||
Child = new TextBlock
|
||
{
|
||
Text = "\uE8BB",
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 11,
|
||
Foreground = secondary,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
},
|
||
};
|
||
close.MouseEnter += (s, _) => ((Border)s).Background = hoverBg;
|
||
close.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent;
|
||
close.MouseLeftButtonUp += (_, _) => CloseWith(PermissionPromptResult.Reject);
|
||
header.Children.Add(close);
|
||
stack.Children.Add(header);
|
||
|
||
stack.Children.Add(new Border
|
||
{
|
||
Height = 1,
|
||
Background = border,
|
||
Opacity = 0.35,
|
||
Margin = new Thickness(0, 0, 0, 12),
|
||
});
|
||
|
||
stack.Children.Add(new TextBlock
|
||
{
|
||
Text = GetHeadline(profile.Headline, uiProfile),
|
||
FontSize = 13,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = primary,
|
||
Margin = new Thickness(0, 0, 0, 8),
|
||
});
|
||
|
||
stack.Children.Add(new Border
|
||
{
|
||
Background = new SolidColorBrush(Color.FromArgb(0x16, profile.RiskColor.R, profile.RiskColor.G, profile.RiskColor.B)),
|
||
BorderBrush = new SolidColorBrush(Color.FromArgb(0x50, profile.RiskColor.R, profile.RiskColor.G, profile.RiskColor.B)),
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(8),
|
||
Padding = new Thickness(10, 6, 10, 6),
|
||
Margin = new Thickness(0, 0, 0, 10),
|
||
Child = new TextBlock
|
||
{
|
||
Text = GetRiskLabel(profile.RiskLabel, uiProfile),
|
||
FontSize = 11.5,
|
||
Foreground = riskBrush,
|
||
FontWeight = FontWeights.SemiBold,
|
||
}
|
||
});
|
||
|
||
stack.Children.Add(BuildInfoRow("\uE943", "Tool", toolName, primary, secondary, itemBg));
|
||
stack.Children.Add(BuildInfoRow("\uE8B7", "Target", target, primary, secondary, itemBg));
|
||
|
||
var previewCard = BuildTargetPreview(toolName, target, preview, primary, secondary, itemBg, border, uiProfile);
|
||
if (previewCard != null)
|
||
stack.Children.Add(previewCard);
|
||
|
||
if (uiProfile.ShowHints && TryBuildFileHints(target, secondary, accent, out var hintRow))
|
||
stack.Children.Add(hintRow);
|
||
|
||
if (uiProfile.Key != "simple")
|
||
{
|
||
stack.Children.Add(new TextBlock
|
||
{
|
||
Text = "沅뚰븳 ?좏깮",
|
||
FontSize = 12.5,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = primary,
|
||
Margin = new Thickness(0, 14, 0, 6),
|
||
});
|
||
}
|
||
|
||
var optionList = new StackPanel();
|
||
optionList.Children.Add(BuildOption(
|
||
icon: "\uE73E",
|
||
title: "?대쾲留??덉슜",
|
||
description: uiProfile.ShowOptionDescription ? "?꾩옱 ?붿껌 1?뚮쭔 ?덉슜?⑸땲??" : "",
|
||
fg: new SolidColorBrush(Color.FromRgb(0x05, 0x96, 0x69)),
|
||
bg: hoverBg,
|
||
onClick: () => CloseWith(PermissionPromptResult.AllowOnce)));
|
||
|
||
optionList.Children.Add(BuildOption(
|
||
icon: "\uE8FB",
|
||
title: "?대쾲 ?ㅽ뻾 ?숈븞 ?덉슜",
|
||
description: uiProfile.ShowOptionDescription ? "?꾩옱 ?ㅽ뻾 以??숈씪 踰붿쐞 ?붿껌???먮룞 ?덉슜?⑸땲??" : "",
|
||
fg: accent,
|
||
bg: hoverBg,
|
||
onClick: () => CloseWith(PermissionPromptResult.AllowForSession)));
|
||
|
||
optionList.Children.Add(BuildOption(
|
||
icon: "\uE711",
|
||
title: "嫄곕?",
|
||
description: uiProfile.ShowOptionDescription ? "?붿껌??李⑤떒?섍퀬 ???묒뾽 ?놁씠 怨꾩냽 吏꾪뻾?⑸땲??" : "",
|
||
fg: new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)),
|
||
bg: hoverBg,
|
||
onClick: () => CloseWith(PermissionPromptResult.Reject)));
|
||
|
||
stack.Children.Add(optionList);
|
||
stack.Children.Add(new TextBlock
|
||
{
|
||
Text = uiProfile.Key == "simple"
|
||
? "Esc: 嫄곕?"
|
||
: "Esc: 嫄곕? | Enter: ?대쾲留??덉슜",
|
||
FontSize = 11,
|
||
Foreground = secondary,
|
||
Margin = new Thickness(2, 12, 0, 0),
|
||
});
|
||
|
||
root.Child = stack;
|
||
Content = root;
|
||
|
||
PreviewKeyDown += (_, e) =>
|
||
{
|
||
if (e.Key == Key.Escape)
|
||
{
|
||
e.Handled = true;
|
||
CloseWith(PermissionPromptResult.Reject);
|
||
}
|
||
else if (e.Key == Key.Enter)
|
||
{
|
||
e.Handled = true;
|
||
CloseWith(PermissionPromptResult.AllowOnce);
|
||
}
|
||
};
|
||
|
||
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);
|
||
};
|
||
}
|
||
|
||
private static PermissionPromptProfile ResolveProfile(string toolName)
|
||
{
|
||
var tool = toolName?.Trim().ToLowerInvariant() ?? "";
|
||
if (tool.Contains("file_write") || tool.Contains("file_edit") || tool.Contains("file"))
|
||
{
|
||
return new PermissionPromptProfile(
|
||
HeaderIcon: "\uE8A5",
|
||
HeaderTitle: "AX Agent ?뚯씪 沅뚰븳 ?붿껌",
|
||
Headline: "?먯씠?꾪듃媛 ?뚯씪 蹂寃쎌쓣 ?붿껌?덉뒿?덈떎.",
|
||
RiskLabel: "以묎컙 ?꾪뿕",
|
||
RiskColor: Color.FromRgb(0xEA, 0x58, 0x0C));
|
||
}
|
||
|
||
if (tool.Contains("process") || tool.Contains("bash") || tool.Contains("powershell"))
|
||
{
|
||
return new PermissionPromptProfile(
|
||
HeaderIcon: "\uE756",
|
||
HeaderTitle: "AX Agent 紐낅졊 ?ㅽ뻾 沅뚰븳 ?붿껌",
|
||
Headline: "?먯씠?꾪듃媛 ??紐낅졊 ?ㅽ뻾???붿껌?덉뒿?덈떎.",
|
||
RiskLabel: "?믪? ?꾪뿕",
|
||
RiskColor: Color.FromRgb(0xDC, 0x26, 0x26));
|
||
}
|
||
|
||
if (tool.Contains("web") || tool.Contains("fetch") || tool.Contains("http"))
|
||
{
|
||
return new PermissionPromptProfile(
|
||
HeaderIcon: "\uE774",
|
||
HeaderTitle: "AX Agent ?ㅽ듃?뚰겕 沅뚰븳 ?붿껌",
|
||
Headline: "?먯씠?꾪듃媛 ?몃? 由ъ냼???묎렐???붿껌?덉뒿?덈떎.",
|
||
RiskLabel: "以묎컙 ?꾪뿕",
|
||
RiskColor: Color.FromRgb(0xD9, 0x77, 0x06));
|
||
}
|
||
|
||
return new PermissionPromptProfile(
|
||
HeaderIcon: "\uE897",
|
||
HeaderTitle: "AX Agent 沅뚰븳 ?붿껌",
|
||
Headline: "怨꾩냽 吏꾪뻾?섎젮硫?沅뚰븳 ?뺤씤???꾩슂?⑸땲??",
|
||
RiskLabel: "寃???꾩슂",
|
||
RiskColor: Color.FromRgb(0x25, 0x63, 0xEB));
|
||
}
|
||
|
||
private static UiExpressionProfile ResolveUiExpressionProfile()
|
||
{
|
||
var level = "balanced";
|
||
if (Application.Current is App app)
|
||
level = NormalizeUiExpressionLevel(app.SettingsService?.Settings?.Llm.AgentUiExpressionLevel);
|
||
|
||
return level switch
|
||
{
|
||
"rich" => new UiExpressionProfile(
|
||
Key: "rich",
|
||
FilePeekLineCount: 24,
|
||
DiffLineCount: 120,
|
||
PreviewMaxHeight: 240,
|
||
ShowHints: true,
|
||
ShowOptionDescription: true),
|
||
"simple" => new UiExpressionProfile(
|
||
Key: "simple",
|
||
FilePeekLineCount: 8,
|
||
DiffLineCount: 36,
|
||
PreviewMaxHeight: 140,
|
||
ShowHints: false,
|
||
ShowOptionDescription: false),
|
||
_ => new UiExpressionProfile(
|
||
Key: "balanced",
|
||
FilePeekLineCount: 16,
|
||
DiffLineCount: 80,
|
||
PreviewMaxHeight: 190,
|
||
ShowHints: true,
|
||
ShowOptionDescription: true),
|
||
};
|
||
}
|
||
|
||
private static bool TryBuildFileHints(
|
||
string target,
|
||
Brush secondary,
|
||
Brush accent,
|
||
out Border hintRow)
|
||
{
|
||
hintRow = new Border();
|
||
if (string.IsNullOrWhiteSpace(target))
|
||
return false;
|
||
|
||
if (!System.IO.Path.IsPathRooted(target))
|
||
return false;
|
||
|
||
string fullPath;
|
||
try
|
||
{
|
||
fullPath = System.IO.Path.GetFullPath(target);
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var panel = new StackPanel
|
||
{
|
||
Orientation = Orientation.Horizontal,
|
||
Margin = new Thickness(0, 4, 0, 0),
|
||
};
|
||
|
||
panel.Children.Add(BuildMiniAction("\uE8C8", "Copy path", accent, () =>
|
||
{
|
||
try { Clipboard.SetText(fullPath); } catch { }
|
||
}));
|
||
|
||
panel.Children.Add(BuildMiniAction("\uED25", "Reveal", accent, () =>
|
||
{
|
||
try { Process.Start("explorer.exe", $"/select,\"{fullPath}\""); } catch { }
|
||
}));
|
||
|
||
panel.Children.Add(BuildMiniAction("\uE8A7", "Open", accent, () =>
|
||
{
|
||
try
|
||
{
|
||
Process.Start(new ProcessStartInfo
|
||
{
|
||
FileName = fullPath,
|
||
UseShellExecute = true,
|
||
});
|
||
}
|
||
catch { }
|
||
}));
|
||
|
||
hintRow = new Border
|
||
{
|
||
Child = panel,
|
||
Margin = new Thickness(0, 2, 0, 0),
|
||
ToolTip = new TextBlock
|
||
{
|
||
Text = "???寃쎈줈 鍮좊Ⅸ ?묒뾽",
|
||
Foreground = secondary,
|
||
}
|
||
};
|
||
return true;
|
||
}
|
||
|
||
private static Border? BuildTargetPreview(
|
||
string toolName,
|
||
string target,
|
||
AgentLoopService.PermissionPromptPreview? preview,
|
||
Brush primary,
|
||
Brush secondary,
|
||
Brush itemBg,
|
||
Brush border,
|
||
UiExpressionProfile uiProfile)
|
||
{
|
||
if (preview != null)
|
||
{
|
||
if (string.Equals(preview.Kind, "file_edit", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return BuildFileEditPreviewCard(
|
||
preview.Title,
|
||
preview.Summary,
|
||
preview.Content,
|
||
primary,
|
||
secondary,
|
||
itemBg,
|
||
border,
|
||
uiProfile);
|
||
}
|
||
|
||
if (string.Equals(preview.Kind, "file_write", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return BuildFileWriteTwoColumnPreviewCard(
|
||
preview.Title,
|
||
preview.Summary,
|
||
previousContent: preview.PreviousContent,
|
||
newContent: preview.Content,
|
||
primary,
|
||
secondary,
|
||
itemBg,
|
||
border,
|
||
uiProfile);
|
||
}
|
||
|
||
return BuildPreviewCard(
|
||
preview.Title,
|
||
preview.Summary,
|
||
preview.Content,
|
||
primary,
|
||
secondary,
|
||
itemBg,
|
||
border,
|
||
uiProfile);
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(target))
|
||
return null;
|
||
|
||
var tool = toolName?.Trim().ToLowerInvariant() ?? "";
|
||
|
||
if (tool.Contains("process") || tool.Contains("bash") || tool.Contains("powershell"))
|
||
return BuildCommandPreview(target, primary, secondary, itemBg, border, uiProfile);
|
||
|
||
if (tool.Contains("web") || tool.Contains("fetch") || tool.Contains("http"))
|
||
return BuildWebPreview(target, primary, secondary, itemBg, border, uiProfile);
|
||
|
||
if (tool.Contains("file") || System.IO.Path.IsPathRooted(target))
|
||
return BuildFilePreview(target, primary, secondary, itemBg, border, uiProfile);
|
||
|
||
return null;
|
||
}
|
||
|
||
private static Border BuildCommandPreview(
|
||
string command,
|
||
Brush primary,
|
||
Brush secondary,
|
||
Brush itemBg,
|
||
Brush border,
|
||
UiExpressionProfile uiProfile)
|
||
{
|
||
var summary = ClassifyCommandRisk(command);
|
||
return BuildPreviewCard(
|
||
"紐낅졊 誘몃━蹂닿린",
|
||
summary,
|
||
command,
|
||
primary,
|
||
secondary,
|
||
itemBg,
|
||
border,
|
||
uiProfile);
|
||
}
|
||
|
||
private static Border BuildWebPreview(
|
||
string urlText,
|
||
Brush primary,
|
||
Brush secondary,
|
||
Brush itemBg,
|
||
Brush border,
|
||
UiExpressionProfile uiProfile)
|
||
{
|
||
var summary = "??곸쓣 ?뺤씤?????놁뒿?덈떎";
|
||
if (Uri.TryCreate(urlText, UriKind.Absolute, out var uri))
|
||
summary = $"{uri.Scheme}://{uri.Host}";
|
||
|
||
return BuildPreviewCard(
|
||
"?ㅽ듃?뚰겕 誘몃━蹂닿린",
|
||
$"?묒냽 ??? {summary}",
|
||
urlText,
|
||
primary,
|
||
secondary,
|
||
itemBg,
|
||
border,
|
||
uiProfile);
|
||
}
|
||
|
||
private static Border? BuildFilePreview(
|
||
string pathText,
|
||
Brush primary,
|
||
Brush secondary,
|
||
Brush itemBg,
|
||
Brush border,
|
||
UiExpressionProfile uiProfile)
|
||
{
|
||
string fullPath;
|
||
try
|
||
{
|
||
fullPath = System.IO.Path.GetFullPath(pathText);
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var summary = "?뚯씪??李얠쓣 ???놁뒿?덈떎";
|
||
var previewBody = fullPath;
|
||
|
||
if (File.Exists(fullPath))
|
||
{
|
||
try
|
||
{
|
||
var info = new FileInfo(fullPath);
|
||
summary = $"議댁옱??쨌 {FormatBytes(info.Length)} 쨌 ?섏젙 {info.LastWriteTime:yyyy-MM-dd HH:mm}";
|
||
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine(fullPath);
|
||
sb.AppendLine(new string('-', 36));
|
||
|
||
var lines = File.ReadLines(fullPath).Take(Math.Max(4, uiProfile.FilePeekLineCount));
|
||
foreach (var line in lines)
|
||
{
|
||
sb.AppendLine(line.Length > 180 ? line[..180] + "..." : line);
|
||
}
|
||
|
||
previewBody = sb.ToString().TrimEnd();
|
||
}
|
||
catch
|
||
{
|
||
previewBody = fullPath;
|
||
}
|
||
}
|
||
|
||
return BuildPreviewCard(
|
||
"?뚯씪 誘몃━蹂닿린",
|
||
summary,
|
||
previewBody,
|
||
primary,
|
||
secondary,
|
||
itemBg,
|
||
border,
|
||
uiProfile);
|
||
}
|
||
|
||
private static Border BuildPreviewCard(
|
||
string title,
|
||
string summary,
|
||
string content,
|
||
Brush primary,
|
||
Brush secondary,
|
||
Brush itemBg,
|
||
Brush border,
|
||
UiExpressionProfile uiProfile)
|
||
{
|
||
return new Border
|
||
{
|
||
Background = itemBg,
|
||
BorderBrush = border,
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(10),
|
||
Padding = new Thickness(10, 8, 10, 8),
|
||
Margin = new Thickness(0, 8, 0, 0),
|
||
Child = new StackPanel
|
||
{
|
||
Children =
|
||
{
|
||
new TextBlock
|
||
{
|
||
Text = title,
|
||
FontSize = 11.5,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = primary,
|
||
},
|
||
new TextBlock
|
||
{
|
||
Text = summary,
|
||
FontSize = 11,
|
||
Foreground = secondary,
|
||
Margin = new Thickness(0, 2, 0, 6),
|
||
},
|
||
new TextBox
|
||
{
|
||
Text = content,
|
||
IsReadOnly = true,
|
||
BorderThickness = new Thickness(0),
|
||
Background = Brushes.Transparent,
|
||
Foreground = primary,
|
||
FontFamily = new FontFamily("Consolas"),
|
||
FontSize = 11,
|
||
MaxHeight = uiProfile.PreviewMaxHeight,
|
||
TextWrapping = TextWrapping.Wrap,
|
||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||
}
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
private static Border BuildFileEditPreviewCard(
|
||
string title,
|
||
string summary,
|
||
string content,
|
||
Brush primary,
|
||
Brush secondary,
|
||
Brush itemBg,
|
||
Brush border,
|
||
UiExpressionProfile uiProfile)
|
||
{
|
||
var body = new StackPanel();
|
||
var rendered = 0;
|
||
foreach (var rawLine in content.Split('\n'))
|
||
{
|
||
var line = rawLine.TrimEnd();
|
||
if (string.IsNullOrWhiteSpace(line))
|
||
continue;
|
||
if (rendered >= uiProfile.DiffLineCount)
|
||
break;
|
||
|
||
Brush lineBrush = primary;
|
||
if (line.StartsWith("+ ", StringComparison.Ordinal))
|
||
lineBrush = new SolidColorBrush(Color.FromRgb(0x16, 0xA3, 0x4A));
|
||
else if (line.StartsWith("- ", StringComparison.Ordinal))
|
||
lineBrush = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26));
|
||
else if (line.Contains(") - ", StringComparison.Ordinal) || line.Contains(") + ", StringComparison.Ordinal))
|
||
lineBrush = new SolidColorBrush(Color.FromRgb(0x93, 0xC5, 0xFD));
|
||
|
||
body.Children.Add(new TextBlock
|
||
{
|
||
Text = line,
|
||
FontSize = 11,
|
||
FontFamily = new FontFamily("Consolas"),
|
||
Foreground = lineBrush,
|
||
TextWrapping = TextWrapping.Wrap,
|
||
Margin = new Thickness(0, 0, 0, 2),
|
||
});
|
||
rendered++;
|
||
}
|
||
|
||
var totalLines = content.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||
.Split('\n')
|
||
.Count(line => !string.IsNullOrWhiteSpace(line));
|
||
if (totalLines > rendered)
|
||
{
|
||
body.Children.Add(new TextBlock
|
||
{
|
||
Text = $"... {totalLines - rendered} more lines",
|
||
FontSize = 10.5,
|
||
Foreground = secondary,
|
||
Margin = new Thickness(0, 4, 0, 0),
|
||
});
|
||
}
|
||
|
||
return new Border
|
||
{
|
||
Background = itemBg,
|
||
BorderBrush = border,
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(10),
|
||
Padding = new Thickness(10, 8, 10, 8),
|
||
Margin = new Thickness(0, 8, 0, 0),
|
||
Child = new StackPanel
|
||
{
|
||
Children =
|
||
{
|
||
new TextBlock
|
||
{
|
||
Text = title,
|
||
FontSize = 11.5,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = primary,
|
||
},
|
||
new TextBlock
|
||
{
|
||
Text = summary,
|
||
FontSize = 11,
|
||
Foreground = secondary,
|
||
Margin = new Thickness(0, 2, 0, 6),
|
||
},
|
||
new ScrollViewer
|
||
{
|
||
MaxHeight = uiProfile.PreviewMaxHeight,
|
||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||
Content = body,
|
||
}
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
private static Border BuildFileWriteTwoColumnPreviewCard(
|
||
string title,
|
||
string summary,
|
||
string? previousContent,
|
||
string newContent,
|
||
Brush primary,
|
||
Brush secondary,
|
||
Brush itemBg,
|
||
Brush border,
|
||
UiExpressionProfile uiProfile)
|
||
{
|
||
var oldLines = SplitPreviewLines(previousContent ?? "(file does not exist)");
|
||
var newLines = SplitPreviewLines(string.IsNullOrWhiteSpace(newContent) ? "(empty)" : newContent);
|
||
var (oldHighlights, newHighlights) = BuildIndexedDiffHighlights(oldLines, newLines);
|
||
|
||
var grid = new Grid();
|
||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(10) });
|
||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||
|
||
var oldPanel = BuildWriteColumn(
|
||
header: previousContent == null ? "Current (missing/new file)" : "Current content",
|
||
content: previousContent ?? "(file does not exist)",
|
||
lines: oldLines,
|
||
highlights: oldHighlights,
|
||
headerBrush: new SolidColorBrush(Color.FromRgb(0xD9, 0x77, 0x06)),
|
||
textBrush: primary,
|
||
secondary,
|
||
itemBg,
|
||
border,
|
||
uiProfile);
|
||
Grid.SetColumn(oldPanel, 0);
|
||
grid.Children.Add(oldPanel);
|
||
|
||
var newPanel = BuildWriteColumn(
|
||
header: "New content",
|
||
content: string.IsNullOrWhiteSpace(newContent) ? "(empty)" : newContent,
|
||
lines: newLines,
|
||
highlights: newHighlights,
|
||
headerBrush: new SolidColorBrush(Color.FromRgb(0x16, 0xA3, 0x4A)),
|
||
textBrush: primary,
|
||
secondary,
|
||
itemBg,
|
||
border,
|
||
uiProfile);
|
||
Grid.SetColumn(newPanel, 2);
|
||
grid.Children.Add(newPanel);
|
||
|
||
return new Border
|
||
{
|
||
Background = itemBg,
|
||
BorderBrush = border,
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(10),
|
||
Padding = new Thickness(10, 8, 10, 8),
|
||
Margin = new Thickness(0, 8, 0, 0),
|
||
Child = new StackPanel
|
||
{
|
||
Children =
|
||
{
|
||
new TextBlock
|
||
{
|
||
Text = title,
|
||
FontSize = 11.5,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = primary,
|
||
},
|
||
new TextBlock
|
||
{
|
||
Text = summary,
|
||
FontSize = 11,
|
||
Foreground = secondary,
|
||
Margin = new Thickness(0, 2, 0, 6),
|
||
},
|
||
grid,
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
private static Border BuildWriteColumn(
|
||
string header,
|
||
string content,
|
||
IReadOnlyList<string> lines,
|
||
IReadOnlyList<bool> highlights,
|
||
Brush headerBrush,
|
||
Brush textBrush,
|
||
Brush secondary,
|
||
Brush itemBg,
|
||
Brush border,
|
||
UiExpressionProfile uiProfile)
|
||
{
|
||
var linePanel = new StackPanel();
|
||
var maxLines = Math.Min(lines.Count, Math.Max(8, uiProfile.DiffLineCount));
|
||
for (int i = 0; i < maxLines; i++)
|
||
{
|
||
var isChanged = i < highlights.Count && highlights[i];
|
||
linePanel.Children.Add(new Border
|
||
{
|
||
Background = isChanged
|
||
? new SolidColorBrush(Color.FromArgb(0x2B, headerBrush is SolidColorBrush hs ? hs.Color.R : (byte)0x16, headerBrush is SolidColorBrush hs2 ? hs2.Color.G : (byte)0xA3, headerBrush is SolidColorBrush hs3 ? hs3.Color.B : (byte)0x4A))
|
||
: Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(4),
|
||
Margin = new Thickness(0, 0, 0, 1),
|
||
Padding = new Thickness(4, 1, 4, 1),
|
||
Child = new TextBlock
|
||
{
|
||
Text = lines[i],
|
||
FontFamily = new FontFamily("Consolas"),
|
||
FontSize = 10.8,
|
||
Foreground = textBrush,
|
||
TextWrapping = TextWrapping.Wrap,
|
||
}
|
||
});
|
||
}
|
||
|
||
if (lines.Count > maxLines)
|
||
{
|
||
linePanel.Children.Add(new TextBlock
|
||
{
|
||
Text = $"... {lines.Count - maxLines} more lines",
|
||
FontSize = 10,
|
||
Foreground = secondary,
|
||
Margin = new Thickness(0, 4, 0, 0),
|
||
});
|
||
}
|
||
|
||
return new Border
|
||
{
|
||
Background = Brushes.Transparent,
|
||
BorderBrush = border,
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(8),
|
||
Padding = new Thickness(8, 6, 8, 6),
|
||
Child = new StackPanel
|
||
{
|
||
Children =
|
||
{
|
||
new TextBlock
|
||
{
|
||
Text = header,
|
||
FontSize = 11,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = headerBrush,
|
||
},
|
||
new Border
|
||
{
|
||
Background = itemBg,
|
||
BorderBrush = new SolidColorBrush(Color.FromArgb(0x20, 0x80, 0x80, 0x80)),
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(6),
|
||
Margin = new Thickness(0, 5, 0, 0),
|
||
Padding = new Thickness(6, 4, 6, 4),
|
||
Child = new ScrollViewer
|
||
{
|
||
MaxHeight = uiProfile.PreviewMaxHeight - 20,
|
||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||
Content = linePanel,
|
||
}
|
||
},
|
||
new TextBlock
|
||
{
|
||
Text = $"{content.Length} chars",
|
||
FontSize = 10,
|
||
Foreground = secondary,
|
||
Margin = new Thickness(0, 4, 0, 0),
|
||
}
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
private static List<string> SplitPreviewLines(string content)
|
||
{
|
||
var lines = content.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n');
|
||
var result = new List<string>(lines.Length);
|
||
foreach (var line in lines)
|
||
result.Add(line.Length <= 220 ? line : line[..220] + "...");
|
||
return result;
|
||
}
|
||
|
||
private static (List<bool> oldHighlights, List<bool> newHighlights) BuildIndexedDiffHighlights(
|
||
IReadOnlyList<string> oldLines,
|
||
IReadOnlyList<string> newLines)
|
||
{
|
||
var max = Math.Max(oldLines.Count, newLines.Count);
|
||
var oldMarks = Enumerable.Repeat(false, oldLines.Count).ToList();
|
||
var newMarks = Enumerable.Repeat(false, newLines.Count).ToList();
|
||
|
||
for (int i = 0; i < max; i++)
|
||
{
|
||
var hasOld = i < oldLines.Count;
|
||
var hasNew = i < newLines.Count;
|
||
if (hasOld && hasNew)
|
||
{
|
||
if (!string.Equals(oldLines[i], newLines[i], StringComparison.Ordinal))
|
||
{
|
||
oldMarks[i] = true;
|
||
newMarks[i] = true;
|
||
}
|
||
}
|
||
else if (hasOld)
|
||
{
|
||
oldMarks[i] = true;
|
||
}
|
||
else if (hasNew)
|
||
{
|
||
newMarks[i] = true;
|
||
}
|
||
}
|
||
|
||
return (oldMarks, newMarks);
|
||
}
|
||
|
||
private static string ClassifyCommandRisk(string command)
|
||
{
|
||
var text = command.Trim();
|
||
if (string.IsNullOrWhiteSpace(text))
|
||
return "鍮?紐낅졊";
|
||
|
||
var lower = text.ToLowerInvariant();
|
||
if (lower.Contains("del ") || lower.Contains("remove-item") || lower.Contains("format "))
|
||
return "??젣/?щ㎎ 媛?μ꽦???덈뒗 紐낅졊";
|
||
if (lower.Contains("git reset --hard") || lower.Contains("rm -rf"))
|
||
return "怨좎쐞????젣 紐낅졊";
|
||
if (lower.Contains("curl ") || lower.Contains("invoke-webrequest") || lower.Contains("wget "))
|
||
return "?ㅼ슫濡쒕뱶 ?먮뒗 ?먭꺽 ?묎렐???ы븿??紐낅졊";
|
||
return "紐낅졊 ?ㅽ뻾 ?붿껌";
|
||
}
|
||
|
||
private static string FormatBytes(long bytes)
|
||
{
|
||
if (bytes < 1024) return $"{bytes} B";
|
||
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
|
||
return $"{bytes / (1024.0 * 1024.0):F1} MB";
|
||
}
|
||
|
||
private static Border BuildMiniAction(string icon, string text, Brush fg, Action onClick)
|
||
{
|
||
var btn = new Border
|
||
{
|
||
Background = Brushes.Transparent,
|
||
BorderBrush = new SolidColorBrush(Color.FromArgb(0x35, 0x80, 0x80, 0x80)),
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(8),
|
||
Padding = new Thickness(8, 4, 8, 4),
|
||
Margin = new Thickness(0, 0, 6, 0),
|
||
Cursor = Cursors.Hand,
|
||
Child = new StackPanel
|
||
{
|
||
Orientation = Orientation.Horizontal,
|
||
Children =
|
||
{
|
||
new TextBlock
|
||
{
|
||
Text = icon,
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 11,
|
||
Foreground = fg,
|
||
Margin = new Thickness(0, 0, 5, 0),
|
||
},
|
||
new TextBlock
|
||
{
|
||
Text = text,
|
||
FontSize = 11.5,
|
||
Foreground = fg,
|
||
}
|
||
}
|
||
}
|
||
};
|
||
btn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.86;
|
||
btn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||
btn.MouseLeftButtonUp += (_, _) => onClick();
|
||
return btn;
|
||
}
|
||
|
||
private static Border BuildInfoRow(
|
||
string icon,
|
||
string label,
|
||
string value,
|
||
Brush primary,
|
||
Brush secondary,
|
||
Brush bg)
|
||
{
|
||
var row = new Grid { Margin = new Thickness(0, 0, 0, 6) };
|
||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||
|
||
row.Children.Add(new TextBlock
|
||
{
|
||
Text = icon,
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 12,
|
||
Foreground = secondary,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(0, 0, 8, 0),
|
||
});
|
||
|
||
var name = new TextBlock
|
||
{
|
||
Text = label,
|
||
FontSize = 12,
|
||
Foreground = secondary,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(0, 0, 10, 0),
|
||
};
|
||
Grid.SetColumn(name, 1);
|
||
row.Children.Add(name);
|
||
|
||
var valueBox = new Border
|
||
{
|
||
Background = bg,
|
||
CornerRadius = new CornerRadius(8),
|
||
Padding = new Thickness(8, 5, 8, 5),
|
||
Child = new TextBlock
|
||
{
|
||
Text = value,
|
||
FontSize = 12,
|
||
Foreground = primary,
|
||
TextWrapping = TextWrapping.Wrap,
|
||
MaxWidth = 460,
|
||
}
|
||
};
|
||
Grid.SetColumn(valueBox, 2);
|
||
row.Children.Add(valueBox);
|
||
|
||
return new Border { Child = row };
|
||
}
|
||
|
||
private static Border BuildOption(
|
||
string icon,
|
||
string title,
|
||
string description,
|
||
Brush fg,
|
||
Brush bg,
|
||
Action onClick)
|
||
{
|
||
var card = new Border
|
||
{
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(10),
|
||
BorderBrush = new SolidColorBrush(Color.FromArgb(0x30, 0x80, 0x80, 0x80)),
|
||
BorderThickness = new Thickness(1),
|
||
Padding = new Thickness(10, 8, 10, 8),
|
||
Margin = new Thickness(0, 0, 0, 6),
|
||
Cursor = Cursors.Hand,
|
||
};
|
||
|
||
var body = new StackPanel { Orientation = Orientation.Horizontal };
|
||
body.Children.Add(new TextBlock
|
||
{
|
||
Text = icon,
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 13,
|
||
Foreground = fg,
|
||
Margin = new Thickness(0, 1, 8, 0),
|
||
});
|
||
|
||
var textStack = new StackPanel();
|
||
textStack.Children.Add(new TextBlock
|
||
{
|
||
Text = title,
|
||
FontSize = 12.5,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = fg,
|
||
});
|
||
if (!string.IsNullOrWhiteSpace(description))
|
||
{
|
||
textStack.Children.Add(new TextBlock
|
||
{
|
||
Text = description,
|
||
FontSize = 11.5,
|
||
Foreground = new SolidColorBrush(Color.FromRgb(0x94, 0xA3, 0xB8)),
|
||
TextWrapping = TextWrapping.Wrap,
|
||
MaxWidth = 470,
|
||
});
|
||
}
|
||
body.Children.Add(textStack);
|
||
card.Child = body;
|
||
|
||
card.MouseEnter += (s, _) => ((Border)s).Background = bg;
|
||
card.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent;
|
||
card.MouseLeftButtonUp += (_, _) => onClick();
|
||
return card;
|
||
}
|
||
|
||
private static string NormalizeUiExpressionLevel(string? value)
|
||
{
|
||
return (value ?? "balanced").Trim().ToLowerInvariant() switch
|
||
{
|
||
"rich" => "rich",
|
||
"simple" => "simple",
|
||
_ => "balanced",
|
||
};
|
||
}
|
||
|
||
private static string GetHeaderTitle(string defaultTitle, UiExpressionProfile uiProfile)
|
||
{
|
||
if (uiProfile.Key == "simple")
|
||
{
|
||
return defaultTitle.Contains("Permission", StringComparison.OrdinalIgnoreCase)
|
||
? "권한 확인"
|
||
: defaultTitle;
|
||
}
|
||
return defaultTitle;
|
||
}
|
||
|
||
private static string GetHeadline(string defaultHeadline, UiExpressionProfile uiProfile)
|
||
{
|
||
return uiProfile.Key switch
|
||
{
|
||
"simple" => "이 작업을 허용할까요?",
|
||
"rich" => $"{defaultHeadline} 범위를 확인한 뒤 안전하게 진행하세요.",
|
||
_ => defaultHeadline,
|
||
};
|
||
}
|
||
|
||
private static string GetRiskLabel(string defaultRiskLabel, UiExpressionProfile uiProfile)
|
||
{
|
||
if (uiProfile.Key == "simple")
|
||
return $"위험도: {defaultRiskLabel}";
|
||
if (uiProfile.Key == "rich")
|
||
return $"위험도: {defaultRiskLabel} (검토 권장)";
|
||
return $"위험도: {defaultRiskLabel}";
|
||
}
|
||
|
||
private void CloseWith(PermissionPromptResult result)
|
||
{
|
||
_result = result;
|
||
DialogResult = true;
|
||
Close();
|
||
}
|
||
|
||
internal static PermissionPromptResult Show(
|
||
Window owner,
|
||
string toolName,
|
||
string target,
|
||
AgentLoopService.PermissionPromptPreview? preview = null)
|
||
{
|
||
var dialog = new PermissionRequestWindow(toolName, target, preview)
|
||
{
|
||
Owner = owner,
|
||
};
|
||
|
||
if (owner.Resources.MergedDictionaries.Count > 0)
|
||
dialog.Resources.MergedDictionaries.Add(owner.Resources);
|
||
|
||
dialog.ShowDialog();
|
||
return dialog._result;
|
||
}
|
||
}
|
||
|