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 lines, IReadOnlyList 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 SplitPreviewLines(string content) { var lines = content.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n'); var result = new List(lines.Length); foreach (var line in lines) result.Add(line.Length <= 220 ? line : line[..220] + "..."); return result; } private static (List oldHighlights, List newHighlights) BuildIndexedDiffHighlights( IReadOnlyList oldLines, IReadOnlyList 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; } }