Files
AX-Copilot-Codex/src/AxCopilot/Views/PermissionRequestWindow.cs
lacvet 1948af3cc4
Some checks failed
Release Gate / gate (push) Has been cancelled
AX Agent 프리뷰 UI를 claw-code 스타일로 정리하고 프리뷰 surface를 공통화
- AX Agent 권한 승인 프리뷰에 공통 preview surface helper를 도입해 제목/요약/본문 box 구성을 일관되게 정리함
- 우측 파일 프리뷰 패널에 파일명, 경로, 형식·크기 메타 헤더를 추가하고 텍스트 프리뷰를 bordered preview box 안에 렌더하도록 개선함
- README와 DEVELOPMENT 문서에 2026-04-06 01:08 (KST) 기준 변경 이력 반영 완료
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0 확인
2026-04-05 22:03:16 +09:00

1099 lines
37 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 AgentPreviewSurfaceFactory.CreateSurface(
title,
summary,
AgentPreviewSurfaceFactory.CreatePreviewBox(
content,
primary,
secondary,
border,
uiProfile.PreviewMaxHeight),
primary,
secondary,
itemBg,
border);
}
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 AgentPreviewSurfaceFactory.CreateSurface(
title,
summary,
AgentPreviewSurfaceFactory.CreatePreviewBox(
string.Join("\n", body.Children.OfType<TextBlock>().Select(tb => tb.Text)),
primary,
secondary,
border,
uiProfile.PreviewMaxHeight),
primary,
secondary,
itemBg,
border);
}
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 AgentPreviewSurfaceFactory.CreateSurface(
title,
summary,
grid,
primary,
secondary,
itemBg,
border);
}
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,
};
dialog.WindowStartupLocation = WindowStartupLocation.CenterOwner;
dialog.Resources.MergedDictionaries.Add(owner.Resources);
dialog.ShowDialog();
return dialog._result;
}
}