commit 4458bb0f52442edb557568f9f144ede06233bfd3 Author: lacvet Date: Fri Apr 3 18:22:19 2026 +0900 Initial commit to new repository diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..59a710a Binary files /dev/null and b/.DS_Store differ diff --git a/.decompiled/AxCopilot.Views.ChatWindow.decompiled.cs b/.decompiled/AxCopilot.Views.ChatWindow.decompiled.cs new file mode 100644 index 0000000..a0eadb8 --- /dev/null +++ b/.decompiled/AxCopilot.Views.ChatWindow.decompiled.cs @@ -0,0 +1,13444 @@ +#define DEBUG +using System; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Net; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Forms; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Markup; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Media.Effects; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; +using System.Windows.Threading; +using AxCopilot.Models; +using AxCopilot.Services; +using AxCopilot.Services.Agent; +using Microsoft.Web.WebView2.Core; +using Microsoft.Web.WebView2.Wpf; +using Microsoft.Win32; + +namespace AxCopilot.Views; + +public class ChatWindow : Window, IComponentConnector +{ + private sealed class ConversationMeta + { + public string Id { get; init; } = ""; + + public string Title { get; init; } = ""; + + public string UpdatedAtText { get; init; } = ""; + + public bool Pinned { get; init; } + + public string Category { get; init; } = "일반"; + + public string Symbol { get; init; } = "\ue8bd"; + + public string ColorHex { get; init; } = "#6B7280"; + + public string Tab { get; init; } = "Chat"; + + public DateTime UpdatedAt { get; init; } + + public string Preview { get; init; } = ""; + + public string? ParentId { get; init; } + + public int ExecutionEventCount { get; init; } + + public int ErrorEventCount { get; init; } + } + + private readonly SettingsService _settings; + + private readonly ChatStorageService _storage; + + private readonly LlmService _llm; + + private readonly ToolRegistry _toolRegistry; + + private readonly AgentLoopService _agentLoop; + + private readonly ModelRouterService _router; + + private readonly object _convLock = new object(); + + private ChatConversation? _currentConversation; + + private CancellationTokenSource? _streamCts; + + private bool _isStreaming; + + private bool _sidebarVisible = true; + + private bool _showExecutionHistory = true; + + private readonly List _draftQueue = new List(); + + private bool _autoContinuingQueuedDraft; + + private string _selectedCategory = ""; + + private bool _forceClose = false; + + private readonly DispatcherTimer _cursorTimer; + + private bool _cursorVisible = true; + + private TextBlock? _activeStreamText; + + private string _cachedStreamContent = ""; + + private TextBlock? _activeAiIcon; + + private bool _aiIconPulseStopped; + + private WorkflowAnalyzerWindow? _analyzerWindow; + + private PlanViewerWindow? _planViewerWindow; + + private bool _userScrolled; + + private readonly DispatcherTimer _elapsedTimer; + + private DateTime _streamStartTime; + + private TextBlock? _elapsedLabel; + + private readonly DispatcherTimer _typingTimer; + + private int _displayedLength; + + private string _activeTab = "Chat"; + + private readonly Dictionary _tabConversationId = new Dictionary + { + ["Chat"] = null, + ["Cowork"] = null, + ["Code"] = null + }; + + private readonly List _attachedFiles = new List(); + + private readonly List _pendingImages = new List(); + + private bool _autoWarningDismissed; + + private static readonly HashSet ImageExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp" }; + + private const int ConversationPageSize = 50; + + private List? _pendingConversations; + + private bool _isEditing; + + private List<(string Cmd, string Label, bool IsSkill)> _slashAllMatches = new List<(string, string, bool)>(); + + private int _slashPageOffset = 0; + + private int _slashSelectedIndex = -1; + + private string? _activeSlashCmd = null; + + private static readonly Dictionary SlashCommands = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["/summary"] = ("Summary", "사용자가 제공한 내용을 핵심 포인트 위주로 간결하게 요약해 주세요. 불릿 포인트 형식을 사용하세요.", "all"), + ["/translate"] = ("Translate", "사용자가 제공한 텍스트를 영어로 번역해 주세요. 원문의 톤과 뉘앙스를 유지하세요.", "all"), + ["/explain"] = ("Explain", "사용자가 제공한 내용을 쉽고 자세하게 설명해 주세요. 필요하면 예시를 포함하세요.", "all"), + ["/fix"] = ("Fix", "사용자가 제공한 텍스트의 맞춤법, 문법, 자연스러운 표현을 교정해 주세요. 수정 사항을 명확히 표시하세요.", "all"), + ["/review"] = ("Code Review", "작업 폴더의 git diff를 분석하여 코드 리뷰를 수행해 주세요. code_review 도구를 사용하세요.", "dev"), + ["/pr"] = ("PR Summary", "작업 폴더의 변경사항을 PR 설명 형식으로 요약해 주세요. code_review(action: pr_summary) 도구를 사용하세요.", "dev"), + ["/test"] = ("Test", "작업 폴더의 코드에 대한 단위 테스트를 생성해 주세요. test_loop 도구를 사용하세요.", "dev"), + ["/structure"] = ("Structure", "작업 폴더의 프로젝트 구조를 분석하고 설명해 주세요. folder_map 도구를 사용하세요.", "dev"), + ["/build"] = ("Build", "작업 폴더의 프로젝트를 빌드해 주세요. build_run 도구를 사용하세요.", "dev"), + ["/search"] = ("Search", "작업 폴더에서 관련 코드를 검색해 주세요. search_codebase 도구를 사용하세요.", "dev"), + ["/help"] = ("Help", "__HELP__", "all") + }; + + private static readonly Dictionary> DropActions; + + private static readonly HashSet CodeExtensions; + + private static readonly HashSet DataExtensions; + + private Popup? _dropActionPopup; + + private int _agentCumulativeInputTokens; + + private int _agentCumulativeOutputTokens; + + private static readonly HashSet WriteToolNames; + + private Border? _planningCard; + + private StackPanel? _planStepsPanel; + + private System.Windows.Controls.ProgressBar? _planProgressBar; + + private TextBlock? _planProgressText; + + private List _searchMatchIndices = new List(); + + private int _searchCurrentIndex = -1; + + private static readonly string[] Tips; + + private int _tipIndex; + + private DispatcherTimer? _tipDismissTimer; + + private DispatcherTimer? _rainbowTimer; + + private DateTime _rainbowStartTime; + + private DispatcherTimer? _toastHideTimer; + + private string _selectedMood = null; + + private string _selectedLanguage = "auto"; + + private string _folderDataUsage = null; + + private string? _promptCardPlaceholder; + + private static readonly (string Id, string Label)[] GeminiModels; + + private static readonly (string Id, string Label)[] ClaudeModels; + + private static readonly HashSet _previewableExtensions; + + private readonly List _previewTabs = new List(); + + private string? _activePreviewTab; + + private bool _webViewInitialized; + + private static readonly string WebView2DataFolder; + + private Popup? _previewTabPopup; + + private DateTime _progressStartTime; + + private DispatcherTimer? _progressElapsedTimer; + + private static readonly HashSet _ignoredDirs; + + private DispatcherTimer? _fileBrowserRefreshTimer; + + private Storyboard? _statusSpinStoryboard; + + internal ColumnDefinition IconBarColumn; + + internal ColumnDefinition SidebarColumn; + + internal ColumnDefinition SplitterColumn; + + internal ColumnDefinition PreviewColumn; + + internal Border IconBarPanel; + + internal System.Windows.Controls.Button BtnUserIconBar; + + internal TextBlock UserInitialIconBar; + + internal Border SidebarPanel; + + internal System.Windows.Controls.Button BtnNewChat; + + internal System.Windows.Controls.TextBox SearchBox; + + internal System.Windows.Controls.Button BtnCategoryDrop; + + internal TextBlock CategoryIcon; + + internal TextBlock CategoryLabel; + + internal StackPanel ConversationPanel; + + internal System.Windows.Controls.Button BtnDeleteAll; + + internal TextBlock UserInitialSidebar; + + internal TextBlock UserNameText; + + internal TextBlock UserPcText; + + internal TextBlock ChatTitle; + + internal System.Windows.Controls.TextBox ChatTitleEdit; + + internal System.Windows.Controls.Button BtnPreviewToggle; + + internal Ellipse PreviewDot; + + internal Border AgentProgressBar; + + internal TextBlock ProgressIcon; + + internal TextBlock ProgressStepLabel; + + internal Border ProgressFill; + + internal TextBlock ProgressPercent; + + internal TextBlock ProgressElapsed; + + internal System.Windows.Controls.Button BtnToggleSidebar; + + internal TextBlock ToggleSidebarIcon; + + internal System.Windows.Controls.RadioButton TabChat; + + internal System.Windows.Controls.RadioButton TabCowork; + + internal System.Windows.Controls.RadioButton TabCode; + + internal TextBlock MaximizeIcon; + + internal Border MessageSearchBar; + + internal System.Windows.Controls.TextBox SearchTextBox; + + internal TextBlock SearchResultCount; + + internal ScrollViewer MessageScroll; + + internal StackPanel MessagePanel; + + internal StackPanel EmptyState; + + internal Border EmptyIcon; + + internal TranslateTransform EmptyIconTranslate; + + internal TextBlock EmptyStateTitle; + + internal TextBlock EmptyStateDesc; + + internal WrapPanel TopicButtonPanel; + + internal Popup TemplatePopup; + + internal ItemsControl TemplateItems; + + internal TextBlock TemplateEmptyHint; + + internal Popup PermissionPopup; + + internal StackPanel PermissionItems; + + internal Popup DataUsagePopup; + + internal StackPanel DataUsageItems; + + internal Popup SlashPopup; + + internal TextBlock SlashPopupTitle; + + internal TextBlock SlashPopupHint; + + internal Border SlashNavUp; + + internal TextBlock SlashNavUpText; + + internal ItemsControl SlashItems; + + internal Border SlashNavDown; + + internal TextBlock SlashNavDownText; + + internal Popup FolderMenuPopup; + + internal StackPanel FolderMenuItems; + + internal Border ToastBorder; + + internal TextBlock ToastIcon; + + internal TextBlock ToastText; + + internal Border DraftPreviewCard; + + internal TextBlock DraftPreviewText; + + internal System.Windows.Controls.Button BtnDraftEnqueue; + + internal System.Windows.Controls.Button BtnDraftEdit; + + internal System.Windows.Controls.Button BtnDraftClear; + + internal StackPanel DraftQueuePanel; + + internal Border InputGlowBorder; + + internal LinearGradientBrush RainbowBrush; + + internal Border InputBorder; + + internal System.Windows.Controls.Button BtnModelSelector; + + internal TextBlock ModelLabel; + + internal System.Windows.Controls.Button BtnTemplateSelector; + + internal ItemsControl AttachedFilesPanel; + + internal System.Windows.Controls.TextBox InputBox; + + internal TextBlock InputWatermark; + + internal Border SlashCommandChip; + + internal TextBlock SlashChipText; + + internal Border SlashChipClose; + + internal System.Windows.Controls.Button BtnAttach; + + internal Border BtnPause; + + internal TextBlock PauseIcon; + + internal System.Windows.Controls.Button BtnStop; + + internal System.Windows.Controls.Button BtnSend; + + internal Border FolderBar; + + internal TextBlock FolderPathLabel; + + internal StackPanel MoodIconPanel; + + internal Border FormatMoodSeparator; + + internal Border BtnDataUsage; + + internal TextBlock DataUsageIcon; + + internal TextBlock DataUsageLabel; + + internal System.Windows.Controls.Button BtnPermission; + + internal TextBlock PermissionIcon; + + internal TextBlock PermissionLabel; + + internal Popup FormatMenuPopup; + + internal StackPanel FormatMenuItems; + + internal Popup MoodMenuPopup; + + internal StackPanel MoodMenuItems; + + internal Border AutoPermissionWarning; + + internal System.Windows.Controls.Button BtnAutoWarningClose; + + internal Border FileBrowserPanel; + + internal TextBlock FileBrowserTitle; + + internal System.Windows.Controls.TreeView FileTreeView; + + internal TextBlock StatusDiamond; + + internal RotateTransform StatusDiamondRotate; + + internal TextBlock StatusLabel; + + internal System.Windows.Controls.Button BtnToggleExecutionLog; + + internal TextBlock ExecutionLogIcon; + + internal TextBlock ExecutionLogLabel; + + internal Border SubAgentIndicator; + + internal TextBlock SubAgentIndicatorLabel; + + internal Border BtnShowAnalyzer; + + internal TextBlock StatusElapsed; + + internal TextBlock StatusTokens; + + internal GridSplitter PreviewSplitter; + + internal Border PreviewPanel; + + internal StackPanel PreviewTabPanel; + + internal System.Windows.Controls.Button BtnOpenExternal; + + internal WebView2 PreviewWebView; + + internal ScrollViewer PreviewTextScroll; + + internal TextBlock PreviewTextBlock; + + internal DataGrid PreviewDataGrid; + + internal TextBlock PreviewEmpty; + + internal Thumb ResizeGrip; + + private bool _contentLoaded; + + private int SlashPageSize => Math.Clamp(_settings.Settings.Llm.SlashPopupPageSize, 3, 20); + + public ChatWindow(SettingsService settings) + { + //IL_0210: Unknown result type (might be due to invalid IL or missing references) + //IL_0215: Unknown result type (might be due to invalid IL or missing references) + //IL_022f: Expected O, but got Unknown + //IL_0248: Unknown result type (might be due to invalid IL or missing references) + //IL_024d: Unknown result type (might be due to invalid IL or missing references) + //IL_0267: Expected O, but got Unknown + //IL_0280: Unknown result type (might be due to invalid IL or missing references) + //IL_0285: Unknown result type (might be due to invalid IL or missing references) + //IL_029f: Expected O, but got Unknown + InitializeComponent(); + _settings = settings; + _storage = new ChatStorageService(); + _llm = new LlmService(settings); + _router = new ModelRouterService(settings); + _toolRegistry = ToolRegistry.CreateDefault(); + _agentLoop = new AgentLoopService(_llm, _toolRegistry, settings) + { + Dispatcher = delegate(Action action) + { + ((DispatcherObject)System.Windows.Application.Current).Dispatcher.Invoke(action); + }, + AskPermissionCallback = async delegate(string toolName, string filePath) + { + MessageBoxResult result = MessageBoxResult.None; + await ((DispatcherObject)System.Windows.Application.Current).Dispatcher.InvokeAsync((Action)delegate + { + result = CustomMessageBox.Show($"도구 '{toolName}'이(가) 다음 파일에 접근하려 합니다:\n\n{filePath}\n\n허용하시겠습니까?", "AX Agent — 권한 확인", MessageBoxButton.YesNo, MessageBoxImage.Question); + }); + return result == MessageBoxResult.Yes; + }, + UserAskCallback = async delegate(string question, List options, string defaultValue) + { + string response = null; + await ((DispatcherObject)System.Windows.Application.Current).Dispatcher.InvokeAsync((Action)delegate + { + response = UserAskDialog.Show(question, options, defaultValue); + }); + return response; + } + }; + SubAgentTool.StatusChanged += OnSubAgentStatusChanged; + _selectedMood = settings.Settings.Llm.DefaultMood ?? "modern"; + _folderDataUsage = settings.Settings.Llm.FolderDataUsage ?? "active"; + _cursorTimer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(530.0) + }; + _cursorTimer.Tick += CursorTimer_Tick; + _elapsedTimer = new DispatcherTimer + { + Interval = TimeSpan.FromSeconds(1.0) + }; + _elapsedTimer.Tick += ElapsedTimer_Tick; + _typingTimer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(12.0) + }; + _typingTimer.Tick += TypingTimer_Tick; + base.KeyDown += ChatWindow_KeyDown; + base.Loaded += delegate + { + SetupUserInfo(); + _selectedMood = _settings.Settings.Llm.DefaultMood ?? "modern"; + _folderDataUsage = _settings.Settings.Llm.FolderDataUsage ?? "active"; + UpdateAnalyzerButtonVisibility(); + UpdateModelLabel(); + InputBox.Focus(); + UpdateDraftPreviewCard(); + MessageScroll.ScrollChanged += MessageScroll_ScrollChanged; + ((DispatcherObject)this).Dispatcher.BeginInvoke((Delegate)(Action)delegate + { + TemplateService.LoadCustomMoods(_settings.Settings.Llm.CustomMoods); + BuildTopicButtons(); + RestoreLastConversations(); + RefreshConversationList(); + LoadMcpToolsAsync(); + UpdateExecutionHistoryUi(); + RefreshSubAgentIndicator(); + Task.Run(delegate + { + int retentionDays = _settings.Settings.Llm.RetentionDays; + if (retentionDays > 0) + { + _storage.PurgeExpired(retentionDays); + } + _storage.PurgeForDiskSpace(); + }); + }, (DispatcherPriority)2, Array.Empty()); + System.Windows.Media.Brush accentBrush = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Blue; + System.Windows.Media.Brush borderBrush = (TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + InputBox.GotFocus += delegate + { + InputBorder.BorderBrush = accentBrush; + }; + InputBox.LostFocus += delegate + { + InputBorder.BorderBrush = borderBrush; + }; + InputBorder.AllowDrop = true; + InputBorder.DragOver += delegate(object obj, System.Windows.DragEventArgs de) + { + de.Effects = (de.Data.GetDataPresent(System.Windows.DataFormats.FileDrop) ? System.Windows.DragDropEffects.Copy : System.Windows.DragDropEffects.None); + de.Handled = true; + }; + InputBorder.Drop += delegate(object obj, System.Windows.DragEventArgs de) + { + if (de.Data.GetData(System.Windows.DataFormats.FileDrop) is string[] array && array.Length != 0) + { + if (_settings.Settings.Llm.EnableDragDropAiActions && array.Length <= 5) + { + ShowDropActionMenu(array); + } + else + { + string[] array2 = array; + foreach (string filePath in array2) + { + AddAttachedFile(filePath); + } + } + } + }; + if (_settings.Settings.Llm.EnableSkillSystem) + { + SkillService.EnsureSkillFolder(); + SkillService.LoadSkills(_settings.Settings.Llm.SkillsFolderPath); + } + SlashNavUp.MouseLeftButtonUp += delegate + { + _slashPageOffset = Math.Max(0, _slashPageOffset - SlashPageSize); + RenderSlashPage(); + }; + SlashNavDown.MouseLeftButtonUp += delegate + { + _slashPageOffset = Math.Min(_slashAllMatches.Count - 1, _slashPageOffset + SlashPageSize); + RenderSlashPage(); + }; + SlashChipClose.MouseLeftButtonUp += delegate + { + HideSlashChip(restoreText: true); + InputBox.Focus(); + }; + InputBox.PreviewMouseWheel += delegate(object obj, MouseWheelEventArgs me) + { + if (SlashPopup.IsOpen) + { + me.Handled = true; + SlashPopup_ScrollByDelta(me.Delta); + } + }; + UpdateFolderBar(); + ApplyHoverBounceAnimation(BtnModelSelector); + ApplyHoverBounceAnimation(BtnTemplateSelector, -1.5); + ApplyHoverScaleAnimation(BtnSend, 1.12); + ApplyHoverScaleAnimation(BtnStop, 1.12); + }; + base.Closed += delegate + { + _streamCts?.Cancel(); + _cursorTimer.Stop(); + _elapsedTimer.Stop(); + _typingTimer.Stop(); + SubAgentTool.StatusChanged -= OnSubAgentStatusChanged; + _llm.Dispose(); + }; + } + + private async Task LoadMcpToolsAsync() + { + try + { + List servers = _settings.Settings.Llm.McpServers; + if (servers != null && servers.Count != 0) + { + int count = await _toolRegistry.RegisterMcpToolsAsync(servers); + if (count > 0) + { + LogService.Info($"MCP tools registered for AX Agent: {count}"); + } + } + } + catch (Exception ex) + { + Exception ex2 = ex; + LogService.Warn("Failed to register MCP tools: " + ex2.Message); + } + } + + protected override void OnClosing(CancelEventArgs e) + { + if (!_forceClose) + { + e.Cancel = true; + Hide(); + } + else + { + base.OnClosing(e); + } + } + + public void ForceClose() + { + lock (_convLock) + { + if (_currentConversation != null && _currentConversation.Messages.Count > 0) + { + _tabConversationId[_activeTab] = _currentConversation.Id; + try + { + _storage.Save(_currentConversation); + } + catch + { + } + } + } + SaveLastConversations(); + _forceClose = true; + Close(); + } + + private void SetupUserInfo() + { + string userName = Environment.UserName; + string text = userName; + char[] array = new char[3] { '\\', '/', ':' }; + foreach (char value in array) + { + int num = text.LastIndexOf(value); + if (num >= 0) + { + string text2 = text; + int num2 = num + 1; + text = text2.Substring(num2, text2.Length - num2); + } + } + string text3 = ((text.Length > 0) ? text.Substring(0, 1).ToUpper() : "U"); + string machineName = Environment.MachineName; + UserInitialSidebar.Text = text3; + UserInitialIconBar.Text = text3; + UserNameText.Text = text; + UserPcText.Text = machineName; + BtnUserIconBar.ToolTip = text + " (" + machineName + ")"; + } + + private void MessageScroll_ScrollChanged(object sender, ScrollChangedEventArgs e) + { + if (MessageScroll.ScrollableHeight <= 1.0) + { + _userScrolled = false; + } + else if (!(Math.Abs(e.ExtentHeightChange) > 0.5)) + { + bool flag = MessageScroll.VerticalOffset >= MessageScroll.ScrollableHeight - 40.0; + _userScrolled = !flag; + } + } + + private void AutoScrollIfNeeded() + { + if (!_userScrolled) + { + SmoothScrollToEnd(); + } + } + + private void ForceScrollToEnd() + { + _userScrolled = false; + ((DispatcherObject)this).Dispatcher.InvokeAsync((Action)delegate + { + SmoothScrollToEnd(); + }, (DispatcherPriority)4); + } + + private void SmoothScrollToEnd() + { + //IL_00e4: Unknown result type (might be due to invalid IL or missing references) + //IL_00e9: Unknown result type (might be due to invalid IL or missing references) + //IL_0103: Expected O, but got Unknown + double targetOffset = MessageScroll.ScrollableHeight; + double currentOffset = MessageScroll.VerticalOffset; + double diff = targetOffset - currentOffset; + if (diff <= 60.0) + { + MessageScroll.ScrollToEnd(); + return; + } + DoubleAnimation doubleAnimation = new DoubleAnimation + { + From = currentOffset, + To = targetOffset, + Duration = TimeSpan.FromMilliseconds(200.0), + EasingFunction = new CubicEase + { + EasingMode = EasingMode.EaseOut + } + }; + doubleAnimation.Completed += delegate + { + MessageScroll.ScrollToVerticalOffset(targetOffset); + }; + DateTime startTime = DateTime.UtcNow; + DispatcherTimer timer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(16.0) + }; + EventHandler tickHandler = null; + tickHandler = delegate + { + double totalMilliseconds = (DateTime.UtcNow - startTime).TotalMilliseconds; + double num = Math.Min(totalMilliseconds / 200.0, 1.0); + double num2 = 1.0 - Math.Pow(1.0 - num, 3.0); + double offset = currentOffset + diff * num2; + MessageScroll.ScrollToVerticalOffset(offset); + if (num >= 1.0) + { + timer.Stop(); + timer.Tick -= tickHandler; + } + }; + timer.Tick += tickHandler; + timer.Start(); + } + + private void ChatTitle_MouseDown(object sender, MouseButtonEventArgs e) + { + lock (_convLock) + { + if (_currentConversation == null) + { + return; + } + } + ChatTitle.Visibility = Visibility.Collapsed; + ChatTitleEdit.Text = ChatTitle.Text; + ChatTitleEdit.Visibility = Visibility.Visible; + ChatTitleEdit.Focus(); + ChatTitleEdit.SelectAll(); + } + + private void ChatTitleEdit_LostFocus(object sender, RoutedEventArgs e) + { + CommitTitleEdit(); + } + + private void ChatTitleEdit_KeyDown(object sender, System.Windows.Input.KeyEventArgs e) + { + //IL_0002: Unknown result type (might be due to invalid IL or missing references) + //IL_0008: Invalid comparison between Unknown and I4 + //IL_0020: Unknown result type (might be due to invalid IL or missing references) + //IL_0027: Invalid comparison between Unknown and I4 + if ((int)e.Key == 6) + { + CommitTitleEdit(); + e.Handled = true; + } + if ((int)e.Key == 13) + { + CancelTitleEdit(); + e.Handled = true; + } + } + + private void CommitTitleEdit() + { + string text = ChatTitleEdit.Text.Trim(); + ChatTitleEdit.Visibility = Visibility.Collapsed; + ChatTitle.Visibility = Visibility.Visible; + if (string.IsNullOrEmpty(text)) + { + return; + } + lock (_convLock) + { + if (_currentConversation == null) + { + return; + } + _currentConversation.Title = text; + } + ChatTitle.Text = text; + try + { + ChatConversation currentConversation; + lock (_convLock) + { + currentConversation = _currentConversation; + } + _storage.Save(currentConversation); + } + catch + { + } + RefreshConversationList(); + } + + private void CancelTitleEdit() + { + ChatTitleEdit.Visibility = Visibility.Collapsed; + ChatTitle.Visibility = Visibility.Visible; + } + + private void BtnCategoryDrop_Click(object sender, RoutedEventArgs e) + { + System.Windows.Media.Brush background = (TryFindResource("LauncherBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromRgb(26, 27, 46)); + System.Windows.Media.Brush borderBrush = (TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush primaryText = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush iconColor = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush hoverBg = (TryFindResource("ItemHoverBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromArgb(30, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + System.Windows.Media.Brush accentBrush = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.CornflowerBlue; + Popup popup = new Popup + { + StaysOpen = false, + AllowsTransparency = true, + PopupAnimation = PopupAnimation.Fade, + PlacementTarget = BtnCategoryDrop, + Placement = PlacementMode.Bottom, + HorizontalOffset = 0.0, + VerticalOffset = 4.0 + }; + Border border = new Border + { + Background = background, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1.0), + CornerRadius = new CornerRadius(12.0), + Padding = new Thickness(6.0), + MinWidth = 180.0, + Effect = new DropShadowEffect + { + BlurRadius = 16.0, + ShadowDepth = 4.0, + Opacity = 0.3, + Color = Colors.Black + } + }; + StackPanel stackPanel = new StackPanel(); + string activeTab = _activeTab; + if (1 == 0) + { + } + string text = ((activeTab == "Cowork") ? "모든 작업" : ((!(activeTab == "Code")) ? "모든 주제" : "모든 작업")); + if (1 == 0) + { + } + string text2 = text; + stackPanel.Children.Add(CreateCatItem("\ue8bd", text2, iconColor, string.IsNullOrEmpty(_selectedCategory), delegate + { + _selectedCategory = ""; + UpdateCategoryLabel(); + RefreshConversationList(); + })); + stackPanel.Children.Add(CreateSep()); + if (_activeTab == "Cowork" || _activeTab == "Code") + { + IReadOnlyList byTabWithCustom = PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets); + HashSet hashSet = new HashSet(); + foreach (TopicPreset item5 in byTabWithCustom) + { + if (!item5.IsCustom && hashSet.Add(item5.Category)) + { + string capturedCat = item5.Category; + stackPanel.Children.Add(CreateCatItem(item5.Symbol, item5.Label, BrushFromHex(item5.Color), _selectedCategory == capturedCat, delegate + { + _selectedCategory = capturedCat; + UpdateCategoryLabel(); + RefreshConversationList(); + })); + } + } + if (byTabWithCustom.Any((TopicPreset p) => p.IsCustom)) + { + stackPanel.Children.Add(CreateSep()); + stackPanel.Children.Add(CreateCatItem("\ue710", "커스텀 프리셋", iconColor, _selectedCategory == "__custom__", delegate + { + _selectedCategory = "__custom__"; + UpdateCategoryLabel(); + RefreshConversationList(); + })); + } + } + else + { + (string, string, string, string)[] all = ChatCategory.All; + for (int num = 0; num < all.Length; num++) + { + (string, string, string, string) tuple = all[num]; + string item = tuple.Item1; + string item2 = tuple.Item2; + string item3 = tuple.Item3; + string item4 = tuple.Item4; + string capturedKey = item; + stackPanel.Children.Add(CreateCatItem(item3, item2, BrushFromHex(item4), _selectedCategory == capturedKey, delegate + { + _selectedCategory = capturedKey; + UpdateCategoryLabel(); + RefreshConversationList(); + })); + } + List list = _settings.Settings.Llm.CustomPresets.Where((CustomPresetEntry c) => c.Tab == "Chat").ToList(); + if (list.Count > 0) + { + stackPanel.Children.Add(CreateSep()); + stackPanel.Children.Add(CreateCatItem("\ue710", "커스텀 프리셋", iconColor, _selectedCategory == "__custom__", delegate + { + _selectedCategory = "__custom__"; + UpdateCategoryLabel(); + RefreshConversationList(); + })); + } + } + border.Child = stackPanel; + popup.Child = border; + popup.IsOpen = true; + Border CreateCatItem(string icon, string text3, System.Windows.Media.Brush foreground, bool isSelected, Action onClick) + { + Border border2 = new Border + { + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(8.0), + Padding = new Thickness(10.0, 7.0, 10.0, 7.0), + Margin = new Thickness(0.0, 1.0, 0.0, 1.0), + Cursor = System.Windows.Input.Cursors.Hand + }; + Grid grid = new Grid + { + ColumnDefinitions = + { + new ColumnDefinition + { + Width = new GridLength(24.0) + }, + new ColumnDefinition + { + Width = new GridLength(1.0, GridUnitType.Star) + }, + new ColumnDefinition + { + Width = new GridLength(20.0) + } + } + }; + TextBlock element = new TextBlock + { + Text = icon, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 12.0, + Foreground = foreground, + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(element, 0); + grid.Children.Add(element); + TextBlock element2 = new TextBlock + { + Text = text3, + FontSize = 12.5, + Foreground = primaryText, + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(element2, 1); + grid.Children.Add(element2); + if (isSelected) + { + FrameworkElement element3 = CreateSimpleCheck(accentBrush); + Grid.SetColumn(element3, 2); + grid.Children.Add(element3); + } + border2.Child = grid; + border2.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border3) + { + border3.Background = hoverBg; + } + }; + border2.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border3) + { + border3.Background = System.Windows.Media.Brushes.Transparent; + } + }; + border2.MouseLeftButtonUp += delegate + { + popup.IsOpen = false; + onClick(); + }; + return border2; + } + Border CreateSep() + { + return new Border + { + Height = 1.0, + Background = borderBrush, + Opacity = 0.3, + Margin = new Thickness(8.0, 4.0, 8.0, 4.0) + }; + } + } + + private void UpdateCategoryLabel() + { + if (string.IsNullOrEmpty(_selectedCategory)) + { + TextBlock categoryLabel = CategoryLabel; + string activeTab = _activeTab; + if (1 == 0) + { + } + string text = ((!(activeTab == "Cowork") && !(activeTab == "Code")) ? "모든 주제" : "모든 작업"); + if (1 == 0) + { + } + categoryLabel.Text = text; + CategoryIcon.Text = "\ue8bd"; + return; + } + if (_selectedCategory == "__custom__") + { + CategoryLabel.Text = "커스텀 프리셋"; + CategoryIcon.Text = "\ue710"; + return; + } + (string, string, string, string)[] all = ChatCategory.All; + for (int i = 0; i < all.Length; i++) + { + var (text2, text3, text4, _) = all[i]; + if (text2 == _selectedCategory) + { + CategoryLabel.Text = text3; + CategoryIcon.Text = text4; + return; + } + } + IReadOnlyList byTabWithCustom = PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets); + TopicPreset topicPreset = byTabWithCustom.FirstOrDefault((TopicPreset p) => p.Category == _selectedCategory); + if (topicPreset != null) + { + CategoryLabel.Text = topicPreset.Label; + CategoryIcon.Text = topicPreset.Symbol; + } + else + { + CategoryLabel.Text = _selectedCategory; + CategoryIcon.Text = "\ue8bd"; + } + } + + protected override void OnSourceInitialized(EventArgs e) + { + base.OnSourceInitialized(e); + HwndSource.FromHwnd(new WindowInteropHelper(this).Handle)?.AddHook(WndProc); + } + + private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled) + { + //IL_0052: Unknown result type (might be due to invalid IL or missing references) + //IL_0057: Unknown result type (might be due to invalid IL or missing references) + if (msg == 36) + { + Screen screen = Screen.FromHandle(hwnd); + System.Drawing.Rectangle workingArea = screen.WorkingArea; + System.Drawing.Rectangle bounds = screen.Bounds; + HwndSource hwndSource = HwndSource.FromHwnd(hwnd); + double? obj; + if (hwndSource == null) + { + obj = null; + } + else + { + HwndTarget compositionTarget = hwndSource.CompositionTarget; + if (compositionTarget == null) + { + obj = null; + } + else + { + Matrix transformToDevice = compositionTarget.TransformToDevice; + obj = ((Matrix)(ref transformToDevice)).M11; + } + } + double num = obj ?? 1.0; + Marshal.WriteInt32(lParam, 8, workingArea.Width); + Marshal.WriteInt32(lParam, 12, workingArea.Height); + Marshal.WriteInt32(lParam, 16, workingArea.Left - bounds.Left); + Marshal.WriteInt32(lParam, 20, workingArea.Top - bounds.Top); + handled = true; + } + return IntPtr.Zero; + } + + private void BtnMinimize_Click(object sender, RoutedEventArgs e) + { + base.WindowState = WindowState.Minimized; + } + + private void BtnMaximize_Click(object sender, RoutedEventArgs e) + { + base.WindowState = ((base.WindowState != WindowState.Maximized) ? WindowState.Maximized : WindowState.Normal); + MaximizeIcon.Text = ((base.WindowState == WindowState.Maximized) ? "\ue923" : "\ue739"); + } + + private void BtnClose_Click(object sender, RoutedEventArgs e) + { + Close(); + } + + private void SaveCurrentTabConversationId() + { + lock (_convLock) + { + if (_currentConversation != null && _currentConversation.Messages.Count > 0) + { + _tabConversationId[_activeTab] = _currentConversation.Id; + try + { + _storage.Save(_currentConversation); + } + catch + { + } + } + } + SaveLastConversations(); + } + + private void StopStreamingIfActive() + { + if (_isStreaming) + { + _streamCts?.Cancel(); + _cursorTimer.Stop(); + _elapsedTimer.Stop(); + _typingTimer.Stop(); + StopRainbowGlow(); + HideStickyProgress(); + _activeStreamText = null; + _elapsedLabel = null; + _cachedStreamContent = ""; + _isStreaming = false; + BtnSend.IsEnabled = true; + BtnStop.Visibility = Visibility.Collapsed; + BtnPause.Visibility = Visibility.Collapsed; + PauseIcon.Text = "\ue769"; + BtnSend.Visibility = Visibility.Visible; + _streamCts?.Dispose(); + _streamCts = null; + SetStatusIdle(); + } + } + + private void TabChat_Checked(object sender, RoutedEventArgs e) + { + if (!(_activeTab == "Chat")) + { + StopStreamingIfActive(); + SaveCurrentTabConversationId(); + _activeTab = "Chat"; + _selectedCategory = ""; + UpdateCategoryLabel(); + UpdateTabUI(); + } + } + + private void TabCowork_Checked(object sender, RoutedEventArgs e) + { + if (!(_activeTab == "Cowork")) + { + StopStreamingIfActive(); + SaveCurrentTabConversationId(); + _activeTab = "Cowork"; + _selectedCategory = ""; + UpdateCategoryLabel(); + UpdateTabUI(); + } + } + + private void TabCode_Checked(object sender, RoutedEventArgs e) + { + if (!(_activeTab == "Code")) + { + StopStreamingIfActive(); + SaveCurrentTabConversationId(); + _activeTab = "Code"; + _selectedCategory = ""; + UpdateCategoryLabel(); + UpdateTabUI(); + } + } + + private void UpdateTabUI() + { + if (FolderBar != null) + { + FolderBar.Visibility = ((!(_activeTab != "Chat")) ? Visibility.Collapsed : Visibility.Visible); + } + if (InputWatermark != null) + { + TextBlock inputWatermark = InputWatermark; + string activeTab = _activeTab; + if (1 == 0) + { + } + string text = ((activeTab == "Cowork") ? "에이전트에게 작업을 요청하세요 (파일 읽기/쓰기, 문서 생성...)" : ((!(activeTab == "Code")) ? _promptCardPlaceholder : "코드 관련 작업을 요청하세요...")); + if (1 == 0) + { + } + inputWatermark.Text = text; + } + ApplyTabDefaultPermission(); + if (_activeTab == "Cowork") + { + BuildBottomBar(); + if (_settings.Settings.Llm.ShowFileBrowser && FileBrowserPanel != null) + { + FileBrowserPanel.Visibility = Visibility.Visible; + BuildFileTree(); + } + } + else if (_activeTab == "Code") + { + BuildCodeBottomBar(); + if (_settings.Settings.Llm.ShowFileBrowser && FileBrowserPanel != null) + { + FileBrowserPanel.Visibility = Visibility.Visible; + BuildFileTree(); + } + } + else + { + MoodIconPanel.Children.Clear(); + if (FormatMoodSeparator != null) + { + FormatMoodSeparator.Visibility = Visibility.Collapsed; + } + if (FileBrowserPanel != null) + { + FileBrowserPanel.Visibility = Visibility.Collapsed; + } + } + BuildTopicButtons(); + SwitchToTabConversation(); + ShowRandomTip(); + } + + private void SwitchToTabConversation() + { + lock (_convLock) + { + if (_currentConversation != null && _currentConversation.Messages.Count > 0) + { + try + { + _storage.Save(_currentConversation); + } + catch + { + } + } + } + string valueOrDefault = _tabConversationId.GetValueOrDefault(_activeTab); + if (!string.IsNullOrEmpty(valueOrDefault)) + { + ChatConversation chatConversation = _storage.Load(valueOrDefault); + if (chatConversation != null) + { + if (string.IsNullOrEmpty(chatConversation.Tab)) + { + chatConversation.Tab = _activeTab; + } + lock (_convLock) + { + _currentConversation = chatConversation; + } + MessagePanel.Children.Clear(); + foreach (ChatMessage message in chatConversation.Messages) + { + AddMessageBubble(message.Role, message.Content, animate: false, message); + } + EmptyState.Visibility = ((chatConversation.Messages.Count > 0) ? Visibility.Collapsed : Visibility.Visible); + UpdateChatTitle(); + RefreshConversationList(); + UpdateFolderBar(); + return; + } + } + lock (_convLock) + { + _currentConversation = new ChatConversation + { + Tab = _activeTab + }; + string workFolder = _settings.Settings.Llm.WorkFolder; + if (!string.IsNullOrEmpty(workFolder) && _activeTab != "Chat") + { + _currentConversation.WorkFolder = workFolder; + } + } + MessagePanel.Children.Clear(); + EmptyState.Visibility = Visibility.Visible; + _attachedFiles.Clear(); + RefreshAttachedFilesUI(); + UpdateChatTitle(); + RefreshConversationList(); + UpdateFolderBar(); + } + + private void FolderPathLabel_Click(object sender, MouseButtonEventArgs e) + { + ShowFolderMenu(); + } + + private void ShowFolderMenu() + { + FolderMenuItems.Children.Clear(); + System.Windows.Media.Brush brush = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.CornflowerBlue; + System.Windows.Media.Brush foreground = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush foreground2 = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + int count = Math.Clamp(_settings.Settings.Llm.MaxRecentFolders, 3, 30); + List list = _settings.Settings.Llm.RecentWorkFolders.Where((string p) => IsPathAllowed(p) && Directory.Exists(p)).Take(count).ToList(); + if (list.Count > 0) + { + FolderMenuItems.Children.Add(new TextBlock + { + Text = "최근 폴더", + FontSize = 12.5, + FontWeight = FontWeights.SemiBold, + Foreground = foreground2, + Margin = new Thickness(10.0, 6.0, 10.0, 4.0) + }); + string currentWorkFolder = GetCurrentWorkFolder(); + foreach (string item in list) + { + bool flag = item.Equals(currentWorkFolder, StringComparison.OrdinalIgnoreCase); + string text = System.IO.Path.GetFileName(item); + if (string.IsNullOrEmpty(text)) + { + text = item; + } + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + if (flag) + { + FrameworkElement frameworkElement = CreateSimpleCheck(brush); + frameworkElement.Margin = new Thickness(0.0, 0.0, 8.0, 0.0); + stackPanel.Children.Add(frameworkElement); + } + TextBlock element = new TextBlock + { + Text = text, + FontSize = 14.0, + FontWeight = (flag ? FontWeights.SemiBold : FontWeights.Normal), + Foreground = foreground, + VerticalAlignment = VerticalAlignment.Center, + MaxWidth = 340.0, + TextTrimming = TextTrimming.CharacterEllipsis + }; + stackPanel.Children.Add(element); + Border border = new Border + { + Child = stackPanel, + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(8.0), + Cursor = System.Windows.Input.Cursors.Hand, + Padding = new Thickness(10.0, 7.0, 10.0, 7.0), + ToolTip = item + }; + border.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border3) + { + border3.Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(24, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + } + }; + border.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border3) + { + border3.Background = System.Windows.Media.Brushes.Transparent; + } + }; + string capturedPath = item; + border.MouseLeftButtonUp += delegate + { + FolderMenuPopup.IsOpen = false; + SetWorkFolder(capturedPath); + }; + border.MouseRightButtonUp += delegate(object _, MouseButtonEventArgs re) + { + re.Handled = true; + ShowRecentFolderContextMenu(capturedPath); + }; + FolderMenuItems.Children.Add(border); + } + FolderMenuItems.Children.Add(new Border + { + Height = 1.0, + Background = ((TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + Margin = new Thickness(8.0, 4.0, 8.0, 4.0), + Opacity = 0.5 + }); + } + StackPanel stackPanel2 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel2.Children.Add(new TextBlock + { + Text = "\ued25", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 14.0, + Foreground = brush, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 8.0, 0.0) + }); + stackPanel2.Children.Add(new TextBlock + { + Text = "폴더 찾아보기...", + FontSize = 14.0, + Foreground = foreground, + VerticalAlignment = VerticalAlignment.Center + }); + Border border2 = new Border + { + Child = stackPanel2, + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(8.0), + Cursor = System.Windows.Input.Cursors.Hand, + Padding = new Thickness(10.0, 7.0, 10.0, 7.0) + }; + border2.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border3) + { + border3.Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(24, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + } + }; + border2.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border3) + { + border3.Background = System.Windows.Media.Brushes.Transparent; + } + }; + border2.MouseLeftButtonUp += delegate + { + FolderMenuPopup.IsOpen = false; + BrowseWorkFolder(); + }; + FolderMenuItems.Children.Add(border2); + FolderMenuPopup.IsOpen = true; + } + + private void BrowseWorkFolder() + { + FolderBrowserDialog folderBrowserDialog = new FolderBrowserDialog + { + Description = "작업 폴더를 선택하세요", + ShowNewFolderButton = false, + UseDescriptionForTitle = true + }; + string currentWorkFolder = GetCurrentWorkFolder(); + if (!string.IsNullOrEmpty(currentWorkFolder) && Directory.Exists(currentWorkFolder)) + { + folderBrowserDialog.SelectedPath = currentWorkFolder; + } + if (folderBrowserDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) + { + if (!IsPathAllowed(folderBrowserDialog.SelectedPath)) + { + CustomMessageBox.Show("이 경로는 작업 폴더로 선택할 수 없습니다.", "경로 제한", MessageBoxButton.OK, MessageBoxImage.Exclamation); + } + else + { + SetWorkFolder(folderBrowserDialog.SelectedPath); + } + } + } + + private static bool IsPathAllowed(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return false; + } + string text = path.TrimEnd('\\', '/'); + if (text.Equals("C:", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + if (path.IndexOf("Document", StringComparison.OrdinalIgnoreCase) >= 0) + { + return false; + } + return true; + } + + private void SetWorkFolder(string path) + { + string fullPath = System.IO.Path.GetFullPath(path); + string pathRoot = System.IO.Path.GetPathRoot(fullPath); + if (!string.IsNullOrEmpty(pathRoot) && fullPath.TrimEnd('\\', '/').Equals(pathRoot.TrimEnd('\\', '/'), StringComparison.OrdinalIgnoreCase)) + { + ShowToast("드라이브 루트(" + pathRoot + ")는 작업공간으로 설정할 수 없습니다. 하위 폴더를 선택하세요.", "\ue783", 3000); + return; + } + FolderPathLabel.Text = path; + FolderPathLabel.ToolTip = path; + lock (_convLock) + { + if (_currentConversation != null) + { + _currentConversation.WorkFolder = path; + } + } + List recentWorkFolders = _settings.Settings.Llm.RecentWorkFolders; + recentWorkFolders.RemoveAll((string p) => !IsPathAllowed(p)); + recentWorkFolders.Remove(path); + recentWorkFolders.Insert(0, path); + int num = Math.Clamp(_settings.Settings.Llm.MaxRecentFolders, 3, 30); + if (recentWorkFolders.Count > num) + { + recentWorkFolders.RemoveRange(num, recentWorkFolders.Count - num); + } + _settings.Settings.Llm.WorkFolder = path; + _settings.Save(); + } + + private string GetCurrentWorkFolder() + { + lock (_convLock) + { + if (_currentConversation != null && !string.IsNullOrEmpty(_currentConversation.WorkFolder)) + { + return _currentConversation.WorkFolder; + } + } + return _settings.Settings.Llm.WorkFolder; + } + + private ContextMenu CreateThemedContextMenu() + { + System.Windows.Media.Brush background = (TryFindResource("LauncherBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromRgb(30, 30, 46)); + System.Windows.Media.Brush borderBrush = (TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + return new ContextMenu + { + Background = background, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1.0), + Padding = new Thickness(4.0) + }; + } + + private void ShowRecentFolderContextMenu(string folderPath) + { + ContextMenu menu = CreateThemedContextMenu(); + System.Windows.Media.Brush primaryText = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush secondaryText = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + AddItem("\ued25", "폴더 열기", delegate + { + try + { + Process.Start(new ProcessStartInfo + { + FileName = folderPath, + UseShellExecute = true + }); + } + catch + { + } + }); + AddItem("\ue8c8", "경로 복사", delegate + { + try + { + System.Windows.Clipboard.SetText(folderPath); + } + catch + { + } + }); + menu.Items.Add(new Separator()); + AddItem("\ue74d", "목록에서 삭제", delegate + { + _settings.Settings.Llm.RecentWorkFolders.RemoveAll((string p) => p.Equals(folderPath, StringComparison.OrdinalIgnoreCase)); + _settings.Save(); + if (FolderMenuPopup.IsOpen) + { + ShowFolderMenu(); + } + }); + menu.IsOpen = true; + void AddItem(string icon, string label, Action action) + { + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel.Children.Add(new TextBlock + { + Text = icon, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 12.0, + Foreground = secondaryText, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 8.0, 0.0) + }); + stackPanel.Children.Add(new TextBlock + { + Text = label, + FontSize = 12.0, + Foreground = primaryText, + VerticalAlignment = VerticalAlignment.Center + }); + MenuItem menuItem = new MenuItem + { + Header = stackPanel, + Padding = new Thickness(8.0, 6.0, 16.0, 6.0) + }; + menuItem.Click += delegate + { + action(); + }; + menu.Items.Add(menuItem); + } + } + + private void BtnFolderClear_Click(object sender, RoutedEventArgs e) + { + FolderPathLabel.Text = "폴더를 선택하세요"; + FolderPathLabel.ToolTip = null; + lock (_convLock) + { + if (_currentConversation != null) + { + _currentConversation.WorkFolder = ""; + } + } + } + + private void UpdateFolderBar() + { + if (FolderBar == null) + { + return; + } + if (_activeTab == "Chat") + { + FolderBar.Visibility = Visibility.Collapsed; + return; + } + FolderBar.Visibility = Visibility.Visible; + string currentWorkFolder = GetCurrentWorkFolder(); + if (!string.IsNullOrEmpty(currentWorkFolder)) + { + FolderPathLabel.Text = currentWorkFolder; + FolderPathLabel.ToolTip = currentWorkFolder; + } + else + { + FolderPathLabel.Text = "폴더를 선택하세요"; + FolderPathLabel.ToolTip = null; + } + LoadConversationSettings(); + UpdatePermissionUI(); + UpdateDataUsageUI(); + } + + private void LoadConversationSettings() + { + ChatConversation currentConversation; + lock (_convLock) + { + currentConversation = _currentConversation; + } + LlmSettings llm = _settings.Settings.Llm; + if (currentConversation != null && currentConversation.Permission != null) + { + _settings.Settings.Llm.FilePermission = currentConversation.Permission; + } + _folderDataUsage = currentConversation?.DataUsage ?? llm.FolderDataUsage ?? "active"; + _selectedMood = currentConversation?.Mood ?? llm.DefaultMood ?? "modern"; + } + + private void SaveConversationSettings() + { + ChatConversation currentConversation; + lock (_convLock) + { + currentConversation = _currentConversation; + } + if (currentConversation == null) + { + return; + } + currentConversation.Permission = _settings.Settings.Llm.FilePermission; + currentConversation.DataUsage = _folderDataUsage; + currentConversation.Mood = _selectedMood; + try + { + _storage.Save(currentConversation); + } + catch (Exception ex) + { + LogService.Debug("대화 저장 실패: " + ex.Message); + } + } + + private void BtnPermission_Click(object sender, RoutedEventArgs e) + { + if (PermissionPopup == null) + { + return; + } + PermissionItems.Children.Clear(); + (string, string, string, string)[] array = new(string, string, string, string)[3] + { + ("Ask", "\ue8d7", "매번 확인 — 파일 접근 시 사용자에게 묻습니다", "#4B5EFC"), + ("Auto", "\ue73e", "자동 허용 — 파일을 자동으로 읽고 씁니다", "#DD6B20"), + ("Deny", "\ue711", "접근 차단 — 파일 접근을 허용하지 않습니다", "#C50F1F") + }; + string filePermission = _settings.Settings.Llm.FilePermission; + (string, string, string, string)[] array2 = array; + for (int i = 0; i < array2.Length; i++) + { + (string, string, string, string) tuple = array2[i]; + string item = tuple.Item1; + string item2 = tuple.Item2; + string item3 = tuple.Item3; + string item4 = tuple.Item4; + bool isChecked = item.Equals(filePermission, StringComparison.OrdinalIgnoreCase); + ControlTemplate controlTemplate = new ControlTemplate(typeof(System.Windows.Controls.Button)); + FrameworkElementFactory frameworkElementFactory = new FrameworkElementFactory(typeof(Border)); + frameworkElementFactory.SetValue(Border.BackgroundProperty, System.Windows.Media.Brushes.Transparent); + frameworkElementFactory.SetValue(Border.CornerRadiusProperty, new CornerRadius(8.0)); + frameworkElementFactory.SetValue(Border.PaddingProperty, new Thickness(12.0, 8.0, 12.0, 8.0)); + frameworkElementFactory.Name = "Bd"; + FrameworkElementFactory frameworkElementFactory2 = new FrameworkElementFactory(typeof(ContentPresenter)); + frameworkElementFactory2.SetValue(FrameworkElement.HorizontalAlignmentProperty, System.Windows.HorizontalAlignment.Left); + frameworkElementFactory2.SetValue(FrameworkElement.VerticalAlignmentProperty, VerticalAlignment.Center); + frameworkElementFactory.AppendChild(frameworkElementFactory2); + controlTemplate.VisualTree = frameworkElementFactory; + Trigger trigger = new Trigger + { + Property = UIElement.IsMouseOverProperty, + Value = true + }; + trigger.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(System.Windows.Media.Color.FromArgb(24, byte.MaxValue, byte.MaxValue, byte.MaxValue)), "Bd")); + controlTemplate.Triggers.Add(trigger); + System.Windows.Controls.Button button = new System.Windows.Controls.Button + { + Template = controlTemplate, + BorderThickness = new Thickness(0.0), + Cursor = System.Windows.Input.Cursors.Hand, + HorizontalContentAlignment = System.Windows.HorizontalAlignment.Left, + Margin = new Thickness(0.0, 1.0, 0.0, 1.0) + }; + ApplyHoverScaleAnimation(button, 1.02); + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel.Children.Add(CreateCheckIcon(isChecked)); + stackPanel.Children.Add(new TextBlock + { + Text = item2, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 14.0, + Foreground = BrushFromHex(item4), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 10.0, 0.0) + }); + StackPanel stackPanel2 = new StackPanel(); + stackPanel2.Children.Add(new TextBlock + { + Text = item, + FontSize = 13.0, + FontWeight = FontWeights.Bold, + Foreground = BrushFromHex(item4) + }); + stackPanel2.Children.Add(new TextBlock + { + Text = item3, + FontSize = 11.0, + Foreground = ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + TextWrapping = TextWrapping.Wrap, + MaxWidth = 220.0 + }); + stackPanel.Children.Add(stackPanel2); + button.Content = stackPanel; + string capturedLevel = item; + button.Click += delegate + { + _settings.Settings.Llm.FilePermission = capturedLevel; + UpdatePermissionUI(); + SaveConversationSettings(); + PermissionPopup.IsOpen = false; + }; + PermissionItems.Children.Add(button); + } + PermissionPopup.IsOpen = true; + } + + private void BtnAutoWarningClose_Click(object sender, RoutedEventArgs e) + { + _autoWarningDismissed = true; + if (AutoPermissionWarning != null) + { + AutoPermissionWarning.Visibility = Visibility.Collapsed; + } + } + + private void UpdatePermissionUI() + { + if (PermissionLabel == null || PermissionIcon == null) + { + return; + } + string filePermission = _settings.Settings.Llm.FilePermission; + PermissionLabel.Text = filePermission; + TextBlock permissionIcon = PermissionIcon; + if (1 == 0) + { + } + string text = ((filePermission == "Auto") ? "\ue73e" : ((!(filePermission == "Deny")) ? "\ue8d7" : "\ue711")); + if (1 == 0) + { + } + permissionIcon.Text = text; + if (filePermission == "Auto") + { + SolidColorBrush foreground = new SolidColorBrush(System.Windows.Media.Color.FromRgb(221, 107, 32)); + PermissionLabel.Foreground = foreground; + PermissionIcon.Foreground = foreground; + ChatConversation currentConversation; + lock (_convLock) + { + currentConversation = _currentConversation; + } + bool flag = currentConversation != null && currentConversation.Messages.Count > 0 && currentConversation.Permission == "Auto"; + if (AutoPermissionWarning != null && !_autoWarningDismissed && !flag) + { + AutoPermissionWarning.Visibility = Visibility.Visible; + } + } + else + { + _autoWarningDismissed = false; + System.Windows.Media.Brush foreground2 = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + SolidColorBrush foreground3 = ((filePermission == "Deny") ? new SolidColorBrush(System.Windows.Media.Color.FromRgb(197, 15, 31)) : new SolidColorBrush(System.Windows.Media.Color.FromRgb(75, 94, 252))); + PermissionLabel.Foreground = foreground2; + PermissionIcon.Foreground = foreground3; + if (AutoPermissionWarning != null) + { + AutoPermissionWarning.Visibility = Visibility.Collapsed; + } + } + } + + private void BtnDataUsage_Click(object sender, MouseButtonEventArgs e) + { + if (DataUsagePopup == null) + { + return; + } + DataUsageItems.Children.Clear(); + (string, string, string, string, string)[] array = new(string, string, string, string, string)[3] + { + ("active", "\ue9f5", "적극 활용", "폴더 내 문서를 자동 탐색하여 보고서 작성에 적극 활용합니다", "#107C10"), + ("passive", "\ue8fd", "소극 활용", "사용자가 요청할 때만 폴더 데이터를 참조합니다", "#D97706"), + ("none", "\ue8d8", "활용하지 않음", "폴더 내 문서를 읽거나 참조하지 않습니다", "#9CA3AF") + }; + (string, string, string, string, string)[] array2 = array; + for (int i = 0; i < array2.Length; i++) + { + (string, string, string, string, string) tuple = array2[i]; + string item = tuple.Item1; + string item2 = tuple.Item2; + string item3 = tuple.Item3; + string item4 = tuple.Item4; + string item5 = tuple.Item5; + bool isChecked = item.Equals(_folderDataUsage, StringComparison.OrdinalIgnoreCase); + ControlTemplate controlTemplate = new ControlTemplate(typeof(System.Windows.Controls.Button)); + FrameworkElementFactory frameworkElementFactory = new FrameworkElementFactory(typeof(Border)); + frameworkElementFactory.SetValue(Border.BackgroundProperty, System.Windows.Media.Brushes.Transparent); + frameworkElementFactory.SetValue(Border.CornerRadiusProperty, new CornerRadius(8.0)); + frameworkElementFactory.SetValue(Border.PaddingProperty, new Thickness(12.0, 8.0, 12.0, 8.0)); + frameworkElementFactory.Name = "Bd"; + FrameworkElementFactory frameworkElementFactory2 = new FrameworkElementFactory(typeof(ContentPresenter)); + frameworkElementFactory2.SetValue(FrameworkElement.HorizontalAlignmentProperty, System.Windows.HorizontalAlignment.Left); + frameworkElementFactory2.SetValue(FrameworkElement.VerticalAlignmentProperty, VerticalAlignment.Center); + frameworkElementFactory.AppendChild(frameworkElementFactory2); + controlTemplate.VisualTree = frameworkElementFactory; + Trigger trigger = new Trigger + { + Property = UIElement.IsMouseOverProperty, + Value = true + }; + trigger.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(System.Windows.Media.Color.FromArgb(24, byte.MaxValue, byte.MaxValue, byte.MaxValue)), "Bd")); + controlTemplate.Triggers.Add(trigger); + System.Windows.Controls.Button button = new System.Windows.Controls.Button + { + Template = controlTemplate, + BorderThickness = new Thickness(0.0), + Cursor = System.Windows.Input.Cursors.Hand, + HorizontalContentAlignment = System.Windows.HorizontalAlignment.Left, + Margin = new Thickness(0.0, 1.0, 0.0, 1.0) + }; + ApplyHoverScaleAnimation(button, 1.02); + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel.Children.Add(CreateCheckIcon(isChecked)); + stackPanel.Children.Add(new TextBlock + { + Text = item2, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 14.0, + Foreground = BrushFromHex(item5), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 10.0, 0.0) + }); + StackPanel stackPanel2 = new StackPanel(); + stackPanel2.Children.Add(new TextBlock + { + Text = item3, + FontSize = 13.0, + FontWeight = FontWeights.Bold, + Foreground = BrushFromHex(item5) + }); + stackPanel2.Children.Add(new TextBlock + { + Text = item4, + FontSize = 11.0, + Foreground = ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + TextWrapping = TextWrapping.Wrap, + MaxWidth = 240.0 + }); + stackPanel.Children.Add(stackPanel2); + button.Content = stackPanel; + string capturedKey = item; + button.Click += delegate + { + _folderDataUsage = capturedKey; + UpdateDataUsageUI(); + SaveConversationSettings(); + DataUsagePopup.IsOpen = false; + }; + DataUsageItems.Children.Add(button); + } + DataUsagePopup.IsOpen = true; + } + + private void UpdateDataUsageUI() + { + if (DataUsageLabel != null && DataUsageIcon != null) + { + string folderDataUsage = _folderDataUsage; + if (1 == 0) + { + } + (string, string, string) tuple = ((folderDataUsage == "passive") ? ("소극", "\ue8fd", "#D97706") : ((!(folderDataUsage == "none")) ? ("적극", "\ue9f5", "#107C10") : ("미사용", "\ue8d8", "#9CA3AF"))); + if (1 == 0) + { + } + var (text, text2, hex) = tuple; + DataUsageLabel.Text = text; + DataUsageIcon.Text = text2; + DataUsageIcon.Foreground = BrushFromHex(hex); + } + } + + private void ApplyTabDefaultPermission() + { + if (_activeTab == "Chat") + { + _settings.Settings.Llm.FilePermission = "Ask"; + UpdatePermissionUI(); + return; + } + string defaultAgentPermission = _settings.Settings.Llm.DefaultAgentPermission; + if (!string.IsNullOrEmpty(defaultAgentPermission)) + { + _settings.Settings.Llm.FilePermission = defaultAgentPermission; + UpdatePermissionUI(); + } + } + + private void BtnAttach_Click(object sender, RoutedEventArgs e) + { + Microsoft.Win32.OpenFileDialog openFileDialog = new Microsoft.Win32.OpenFileDialog + { + Multiselect = true, + Title = "첨부할 파일을 선택하세요", + Filter = "모든 파일 (*.*)|*.*|텍스트 (*.txt;*.md;*.csv)|*.txt;*.md;*.csv|코드 (*.cs;*.py;*.js;*.ts)|*.cs;*.py;*.js;*.ts" + }; + string currentWorkFolder = GetCurrentWorkFolder(); + if (!string.IsNullOrEmpty(currentWorkFolder) && Directory.Exists(currentWorkFolder)) + { + openFileDialog.InitialDirectory = currentWorkFolder; + } + if (openFileDialog.ShowDialog() == true) + { + string[] fileNames = openFileDialog.FileNames; + foreach (string filePath in fileNames) + { + AddAttachedFile(filePath); + } + } + } + + private void AddAttachedFile(string filePath) + { + if (_attachedFiles.Contains(filePath)) + { + return; + } + try + { + FileInfo fileInfo = new FileInfo(filePath); + if (fileInfo.Length > 10485760) + { + CustomMessageBox.Show("파일이 너무 큽니다 (10MB 초과):\n" + fileInfo.Name, "첨부 제한", MessageBoxButton.OK, MessageBoxImage.Exclamation); + return; + } + string text = fileInfo.Extension.ToLowerInvariant(); + if (ImageExtensions.Contains(text) && _settings.Settings.Llm.EnableImageInput) + { + int num = _settings.Settings.Llm.MaxImageSizeKb; + if (num <= 0) + { + num = 5120; + } + if (fileInfo.Length > num * 1024) + { + CustomMessageBox.Show($"이미지가 너무 큽니다 ({fileInfo.Length / 1024}KB, 최대 {num}KB).", "이미지 크기 초과", MessageBoxButton.OK, MessageBoxImage.Exclamation); + return; + } + byte[] inArray = File.ReadAllBytes(filePath); + if (1 == 0) + { + } + string text2; + switch (text) + { + case ".jpg": + case ".jpeg": + text2 = "image/jpeg"; + break; + case ".gif": + text2 = "image/gif"; + break; + case ".bmp": + text2 = "image/bmp"; + break; + case ".webp": + text2 = "image/webp"; + break; + default: + text2 = "image/png"; + break; + } + if (1 == 0) + { + } + string mimeType = text2; + ImageAttachment attachment = new ImageAttachment + { + Base64 = Convert.ToBase64String(inArray), + MimeType = mimeType, + FileName = fileInfo.Name + }; + if (!_pendingImages.Any((ImageAttachment i) => i.FileName == attachment.FileName)) + { + _pendingImages.Add(attachment); + AddImagePreview(attachment); + } + return; + } + } + catch + { + return; + } + _attachedFiles.Add(filePath); + RefreshAttachedFilesUI(); + } + + private void RemoveAttachedFile(string filePath) + { + _attachedFiles.Remove(filePath); + RefreshAttachedFilesUI(); + } + + private void RefreshAttachedFilesUI() + { + AttachedFilesPanel.Items.Clear(); + if (_attachedFiles.Count == 0) + { + AttachedFilesPanel.Visibility = Visibility.Collapsed; + return; + } + AttachedFilesPanel.Visibility = Visibility.Visible; + System.Windows.Media.Brush foreground = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush background = (TryFindResource("HintBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.LightGray; + foreach (string item in _attachedFiles.ToList()) + { + string fileName = System.IO.Path.GetFileName(item); + string capturedFile = item; + Border border = new Border + { + Background = background, + CornerRadius = new CornerRadius(6.0), + Padding = new Thickness(8.0, 4.0, 4.0, 4.0), + Margin = new Thickness(0.0, 0.0, 4.0, 4.0), + Cursor = System.Windows.Input.Cursors.Hand + }; + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel.Children.Add(new TextBlock + { + Text = "\ue8a5", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 10.0, + Foreground = foreground, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 4.0, 0.0) + }); + stackPanel.Children.Add(new TextBlock + { + Text = fileName, + FontSize = 11.0, + Foreground = foreground, + VerticalAlignment = VerticalAlignment.Center, + MaxWidth = 150.0, + TextTrimming = TextTrimming.CharacterEllipsis, + ToolTip = item + }); + System.Windows.Controls.Button button = new System.Windows.Controls.Button + { + Content = new TextBlock + { + Text = "\ue711", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 8.0, + Foreground = foreground + }, + Background = System.Windows.Media.Brushes.Transparent, + BorderThickness = new Thickness(0.0), + Cursor = System.Windows.Input.Cursors.Hand, + Padding = new Thickness(4.0, 2.0, 4.0, 2.0), + Margin = new Thickness(2.0, 0.0, 0.0, 0.0) + }; + button.Click += delegate + { + RemoveAttachedFile(capturedFile); + }; + stackPanel.Children.Add(button); + border.Child = stackPanel; + AttachedFilesPanel.Items.Add(border); + } + } + + private string BuildFileContextPrompt() + { + if (_attachedFiles.Count == 0) + { + return ""; + } + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.AppendLine("\n[첨부 파일 컨텍스트]"); + foreach (string attachedFile in _attachedFiles) + { + try + { + bool flag; + switch (System.IO.Path.GetExtension(attachedFile).ToLowerInvariant()) + { + case ".exe": + case ".dll": + case ".zip": + case ".7z": + case ".rar": + case ".tar": + case ".gz": + case ".png": + case ".jpg": + case ".jpeg": + case ".gif": + case ".bmp": + case ".ico": + case ".webp": + case ".svg": + case ".pdf": + case ".docx": + case ".xlsx": + case ".pptx": + case ".doc": + case ".xls": + case ".ppt": + case ".mp3": + case ".mp4": + case ".avi": + case ".mov": + case ".mkv": + case ".wav": + case ".flac": + case ".psd": + case ".ai": + case ".sketch": + case ".fig": + case ".msi": + case ".iso": + case ".img": + case ".bin": + case ".dat": + case ".db": + case ".sqlite": + flag = true; + break; + default: + flag = false; + break; + } + StringBuilder stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler; + if (flag) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder3 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(26, 1, stringBuilder2); + handler.AppendLiteral("\n--- "); + handler.AppendFormatted(System.IO.Path.GetFileName(attachedFile)); + handler.AppendLiteral(" (바이너리 파일, 내용 생략) ---"); + stringBuilder3.AppendLine(ref handler); + continue; + } + string text = File.ReadAllText(attachedFile); + if (text.Length > 8000) + { + text = text.Substring(0, 8000) + "\n... (이하 생략)"; + } + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder4 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder2); + handler.AppendLiteral("\n--- "); + handler.AppendFormatted(System.IO.Path.GetFileName(attachedFile)); + handler.AppendLiteral(" ---"); + stringBuilder4.AppendLine(ref handler); + stringBuilder.AppendLine(text); + } + catch (Exception ex) + { + StringBuilder stringBuilder2 = stringBuilder; + StringBuilder stringBuilder5 = stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(19, 2, stringBuilder2); + handler.AppendLiteral("\n--- "); + handler.AppendFormatted(System.IO.Path.GetFileName(attachedFile)); + handler.AppendLiteral(" (읽기 실패: "); + handler.AppendFormatted(ex.Message); + handler.AppendLiteral(") ---"); + stringBuilder5.AppendLine(ref handler); + } + } + return stringBuilder.ToString(); + } + + private void ResizeGrip_DragDelta(object sender, DragDeltaEventArgs e) + { + double num = base.Width + e.HorizontalChange; + double num2 = base.Height + e.VerticalChange; + if (num >= base.MinWidth) + { + base.Width = num; + } + if (num2 >= base.MinHeight) + { + base.Height = num2; + } + } + + private void BtnToggleSidebar_Click(object sender, RoutedEventArgs e) + { + _sidebarVisible = !_sidebarVisible; + if (_sidebarVisible) + { + IconBarColumn.Width = new GridLength(0.0); + IconBarPanel.Visibility = Visibility.Collapsed; + SidebarPanel.Visibility = Visibility.Visible; + ToggleSidebarIcon.Text = "\ue76b"; + AnimateSidebar(0.0, 270.0, delegate + { + SidebarColumn.MinWidth = 200.0; + }); + } + else + { + SidebarColumn.MinWidth = 0.0; + ToggleSidebarIcon.Text = "\ue76c"; + AnimateSidebar(270.0, 0.0, delegate + { + SidebarPanel.Visibility = Visibility.Collapsed; + IconBarColumn.Width = new GridLength(52.0); + IconBarPanel.Visibility = Visibility.Visible; + }); + } + } + + private void AnimateSidebar(double from, double to, Action? onComplete = null) + { + //IL_003e: Unknown result type (might be due to invalid IL or missing references) + //IL_0043: Unknown result type (might be due to invalid IL or missing references) + //IL_005d: Expected O, but got Unknown + double duration = 200.0; + DateTime start = DateTime.UtcNow; + DispatcherTimer timer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(10.0) + }; + EventHandler tickHandler = null; + tickHandler = delegate + { + double totalMilliseconds = (DateTime.UtcNow - start).TotalMilliseconds; + double num = Math.Min(totalMilliseconds / duration, 1.0); + num = 1.0 - (1.0 - num) * (1.0 - num); + SidebarColumn.Width = new GridLength(from + (to - from) * num); + if (totalMilliseconds >= duration) + { + timer.Stop(); + timer.Tick -= tickHandler; + SidebarColumn.Width = new GridLength(to); + onComplete?.Invoke(); + } + }; + timer.Tick += tickHandler; + timer.Start(); + } + + public void RefreshConversationList() + { + List source = _storage.LoadAllMeta(); + IEnumerable enumerable = PresetService.GetByTabWithCustom("Cowork", _settings.Settings.Llm.CustomPresets).Concat(PresetService.GetByTabWithCustom("Code", _settings.Settings.Llm.CustomPresets)).Concat(PresetService.GetByTabWithCustom("Chat", _settings.Settings.Llm.CustomPresets)); + Dictionary presetMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (TopicPreset item in enumerable) + { + presetMap.TryAdd(item.Category, (item.Symbol, item.Color)); + } + List source2 = source.Select(delegate(ChatConversation c) + { + string text = ChatCategory.GetSymbol(c.Category); + string text2 = ChatCategory.GetColor(c.Category); + if (text == "\ue8bd" && text2 == "#6B7280" && c.Category != "일반" && presetMap.TryGetValue(c.Category, out (string, string) value)) + { + (text, text2) = value; + } + return new ConversationMeta + { + Id = c.Id, + Title = c.Title, + Pinned = c.Pinned, + Category = c.Category, + Symbol = text, + ColorHex = text2, + Tab = (string.IsNullOrEmpty(c.Tab) ? "Chat" : c.Tab), + UpdatedAtText = FormatDate(c.UpdatedAt), + UpdatedAt = c.UpdatedAt, + Preview = (c.Preview ?? ""), + ParentId = c.ParentId, + ExecutionEventCount = (c.ExecutionEvents?.Count ?? 0), + ErrorEventCount = (c.ExecutionEvents?.Count((ChatExecutionEvent x) => string.Equals(x.Type, "Error", StringComparison.OrdinalIgnoreCase)) ?? 0) + }; + }).ToList(); + source2 = source2.Where((ConversationMeta i) => i.Tab == _activeTab).ToList(); + if (_selectedCategory == "__custom__") + { + HashSet customCats = _settings.Settings.Llm.CustomPresets.Select((CustomPresetEntry c) => "custom_" + c.Id).ToHashSet(); + source2 = source2.Where((ConversationMeta i) => customCats.Contains(i.Category)).ToList(); + } + else if (!string.IsNullOrEmpty(_selectedCategory)) + { + source2 = source2.Where((ConversationMeta i) => i.Category == _selectedCategory).ToList(); + } + string search = SearchBox?.Text?.Trim() ?? ""; + if (!string.IsNullOrEmpty(search)) + { + source2 = source2.Where((ConversationMeta i) => i.Title.Contains(search, StringComparison.OrdinalIgnoreCase) || i.Preview.Contains(search, StringComparison.OrdinalIgnoreCase)).ToList(); + } + RenderConversationList(source2); + } + + private void RenderConversationList(List items) + { + ConversationPanel.Children.Clear(); + _pendingConversations = null; + if (items.Count == 0) + { + TextBlock textBlock = new TextBlock(); + textBlock.Text = "대화가 없습니다"; + textBlock.FontSize = 12.0; + textBlock.Foreground = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + textBlock.HorizontalAlignment = System.Windows.HorizontalAlignment.Center; + textBlock.Margin = new Thickness(0.0, 20.0, 0.0, 0.0); + TextBlock element = textBlock; + ConversationPanel.Children.Add(element); + return; + } + DateTime today = DateTime.Today; + List list = items.Where((ConversationMeta i) => i.UpdatedAt.Date == today).ToList(); + List list2 = items.Where((ConversationMeta i) => i.UpdatedAt.Date < today).ToList(); + List<(string, ConversationMeta)> list3 = new List<(string, ConversationMeta)>(); + foreach (ConversationMeta item2 in list) + { + list3.Add(("오늘", item2)); + } + foreach (ConversationMeta item3 in list2) + { + list3.Add(("이전", item3)); + } + List<(string, ConversationMeta)> list4 = list3.Take(50).ToList(); + string text = null; + foreach (var (text2, item) in list4) + { + if (text2 != text) + { + AddGroupHeader(text2); + text = text2; + } + AddConversationItem(item); + } + if (list3.Count > 50) + { + _pendingConversations = items; + AddLoadMoreButton(list3.Count - 50); + } + } + + private void AddLoadMoreButton(int remaining) + { + System.Windows.Media.Brush brush = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush foreground = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.CornflowerBlue; + Border border = new Border + { + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(8.0), + Cursor = System.Windows.Input.Cursors.Hand, + Padding = new Thickness(8.0, 10.0, 8.0, 10.0), + Margin = new Thickness(6.0, 4.0, 6.0, 4.0), + HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch + }; + StackPanel stackPanel = new StackPanel + { + HorizontalAlignment = System.Windows.HorizontalAlignment.Center + }; + stackPanel.Children.Add(new TextBlock + { + Text = $"더 보기 ({remaining}개 남음)", + FontSize = 12.0, + Foreground = foreground, + HorizontalAlignment = System.Windows.HorizontalAlignment.Center + }); + border.Child = stackPanel; + border.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border2) + { + border2.Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(18, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + } + }; + border.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border2) + { + border2.Background = System.Windows.Media.Brushes.Transparent; + } + }; + border.MouseLeftButtonUp += delegate + { + if (_pendingConversations != null) + { + List pendingConversations = _pendingConversations; + _pendingConversations = null; + ConversationPanel.Children.Clear(); + DateTime today = DateTime.Today; + List list = pendingConversations.Where((ConversationMeta i) => i.UpdatedAt.Date == today).ToList(); + List list2 = pendingConversations.Where((ConversationMeta i) => i.UpdatedAt.Date < today).ToList(); + if (list.Count > 0) + { + AddGroupHeader("오늘"); + foreach (ConversationMeta item in list) + { + AddConversationItem(item); + } + } + if (list2.Count > 0) + { + AddGroupHeader("이전"); + foreach (ConversationMeta item2 in list2) + { + AddConversationItem(item2); + } + } + } + }; + ConversationPanel.Children.Add(border); + } + + private void AddGroupHeader(string text) + { + TextBlock textBlock = new TextBlock(); + textBlock.Text = text; + textBlock.FontSize = 11.0; + textBlock.FontWeight = FontWeights.SemiBold; + textBlock.Foreground = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + textBlock.Margin = new Thickness(8.0, 12.0, 0.0, 4.0); + TextBlock element = textBlock; + ConversationPanel.Children.Add(element); + } + + private void AddConversationItem(ConversationMeta item) + { + //IL_0846: Unknown result type (might be due to invalid IL or missing references) + bool isSelected = false; + lock (_convLock) + { + isSelected = _currentConversation?.Id == item.Id; + } + bool flag = !string.IsNullOrEmpty(item.ParentId); + Border border = new Border + { + Background = (isSelected ? new SolidColorBrush(System.Windows.Media.Color.FromArgb(48, 75, 94, 252)) : System.Windows.Media.Brushes.Transparent), + CornerRadius = new CornerRadius(8.0), + Padding = new Thickness(10.0, 8.0, 10.0, 8.0), + Margin = (flag ? new Thickness(16.0, 1.0, 0.0, 1.0) : new Thickness(0.0, 1.0, 0.0, 1.0)), + Cursor = System.Windows.Input.Cursors.Hand + }; + Grid grid = new Grid(); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = new GridLength(28.0) + }); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = new GridLength(1.0, GridUnitType.Star) + }); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = GridLength.Auto + }); + System.Windows.Media.Brush foreground; + if (item.Pinned) + { + foreground = System.Windows.Media.Brushes.Orange; + } + else + { + try + { + foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(item.ColorHex)); + } + catch + { + foreground = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Blue; + } + } + string text = (item.Pinned ? "\ue718" : ((!string.IsNullOrEmpty(item.ParentId)) ? "\ue8a5" : item.Symbol)); + if (!string.IsNullOrEmpty(item.ParentId)) + { + foreground = new SolidColorBrush(System.Windows.Media.Color.FromRgb(139, 92, 246)); + } + TextBlock element = new TextBlock + { + Text = text, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 13.0, + Foreground = foreground, + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(element, 0); + grid.Children.Add(element); + System.Windows.Media.Brush titleColor = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush foreground2 = (TryFindResource("HintText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.DarkGray; + StackPanel stackPanel = new StackPanel + { + VerticalAlignment = VerticalAlignment.Center + }; + TextBlock title = new TextBlock + { + Text = item.Title, + FontSize = 12.5, + Foreground = titleColor, + TextTrimming = TextTrimming.CharacterEllipsis + }; + TextBlock element2 = new TextBlock + { + Text = item.UpdatedAtText, + FontSize = 10.0, + Foreground = foreground2, + Margin = new Thickness(0.0, 2.0, 0.0, 0.0) + }; + StackPanel stackPanel2 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal, + Margin = new Thickness(0.0, 4.0, 0.0, 0.0), + Visibility = ((item.ExecutionEventCount <= 0) ? Visibility.Collapsed : Visibility.Visible) + }; + stackPanel2.Children.Add(new Border + { + Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(24, 37, 99, 235)), + CornerRadius = new CornerRadius(4.0), + Padding = new Thickness(5.0, 1.0, 5.0, 1.0), + Margin = new Thickness(0.0, 0.0, 6.0, 0.0), + Child = new TextBlock + { + Text = $"실행 {item.ExecutionEventCount}", + FontSize = 9.5, + Foreground = new SolidColorBrush(System.Windows.Media.Color.FromRgb(37, 99, 235)) + } + }); + if (item.ErrorEventCount > 0) + { + stackPanel2.Children.Add(new Border + { + Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(24, 220, 38, 38)), + CornerRadius = new CornerRadius(4.0), + Padding = new Thickness(5.0, 1.0, 5.0, 1.0), + Child = new TextBlock + { + Text = $"오류 {item.ErrorEventCount}", + FontSize = 9.5, + Foreground = new SolidColorBrush(System.Windows.Media.Color.FromRgb(220, 38, 38)) + } + }); + } + stackPanel.Children.Add(title); + stackPanel.Children.Add(element2); + stackPanel.Children.Add(stackPanel2); + Grid.SetColumn(stackPanel, 1); + grid.Children.Add(stackPanel); + System.Windows.Controls.Button catBtn = new System.Windows.Controls.Button + { + Content = new TextBlock + { + Text = "\ue70f", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 10.0, + Foreground = ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray) + }, + Background = System.Windows.Media.Brushes.Transparent, + BorderThickness = new Thickness(0.0), + Cursor = System.Windows.Input.Cursors.Hand, + VerticalAlignment = VerticalAlignment.Center, + Visibility = Visibility.Collapsed, + Padding = new Thickness(4.0), + ToolTip = ((_activeTab == "Cowork") ? "작업 유형" : "대화 주제 변경") + }; + string capturedId = item.Id; + catBtn.Click += delegate + { + ShowConversationMenu(capturedId); + }; + Grid.SetColumn(catBtn, 2); + grid.Children.Add(catBtn); + if (isSelected) + { + border.BorderBrush = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.CornflowerBlue; + border.BorderThickness = new Thickness(2.0, 0.0, 0.0, 0.0); + } + border.Child = grid; + border.RenderTransformOrigin = new Point(0.5, 0.5); + border.RenderTransform = new ScaleTransform(1.0, 1.0); + SolidColorBrush solidColorBrush = new SolidColorBrush(System.Windows.Media.Color.FromArgb(48, 75, 94, 252)); + border.MouseEnter += delegate + { + if (!isSelected) + { + border.Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(21, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + } + catBtn.Visibility = Visibility.Visible; + ScaleTransform scaleTransform = border.RenderTransform as ScaleTransform; + scaleTransform?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120.0))); + scaleTransform?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120.0))); + }; + border.MouseLeave += delegate + { + if (!isSelected) + { + border.Background = System.Windows.Media.Brushes.Transparent; + } + catBtn.Visibility = Visibility.Collapsed; + ScaleTransform scaleTransform = border.RenderTransform as ScaleTransform; + scaleTransform?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150.0))); + scaleTransform?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150.0))); + }; + border.MouseLeftButtonDown += delegate + { + try + { + if (isSelected) + { + EnterTitleEditMode(title, item.Id, titleColor); + } + else + { + if (_isStreaming) + { + _streamCts?.Cancel(); + _cursorTimer.Stop(); + _typingTimer.Stop(); + _elapsedTimer.Stop(); + _activeStreamText = null; + _elapsedLabel = null; + _isStreaming = false; + } + ChatConversation chatConversation = _storage.Load(item.Id); + if (chatConversation != null) + { + if (string.IsNullOrEmpty(chatConversation.Tab)) + { + chatConversation.Tab = _activeTab; + } + lock (_convLock) + { + _currentConversation = chatConversation; + } + _tabConversationId[_activeTab] = chatConversation.Id; + UpdateChatTitle(); + RenderMessages(); + RefreshConversationList(); + } + } + } + catch (Exception ex) + { + LogService.Error("대화 전환 오류: " + ex.Message); + } + }; + border.MouseRightButtonUp += delegate(object _, MouseButtonEventArgs me) + { + me.Handled = true; + if (!isSelected) + { + ChatConversation chatConversation = _storage.Load(item.Id); + if (chatConversation != null) + { + if (string.IsNullOrEmpty(chatConversation.Tab)) + { + chatConversation.Tab = _activeTab; + } + lock (_convLock) + { + _currentConversation = chatConversation; + } + _tabConversationId[_activeTab] = chatConversation.Id; + UpdateChatTitle(); + RenderMessages(); + } + } + ((DispatcherObject)this).Dispatcher.BeginInvoke((Delegate)(Action)delegate + { + ShowConversationMenu(item.Id); + }, (DispatcherPriority)5, Array.Empty()); + }; + ConversationPanel.Children.Add(border); + } + + private void EnterTitleEditMode(TextBlock titleTb, string conversationId, System.Windows.Media.Brush titleColor) + { + try + { + StackPanel parent = titleTb.Parent as StackPanel; + if (parent == null) + { + return; + } + int num = parent.Children.IndexOf(titleTb); + if (num < 0) + { + return; + } + System.Windows.Controls.TextBox editBox = new System.Windows.Controls.TextBox + { + Text = titleTb.Text, + FontSize = 12.5, + Foreground = titleColor, + Background = System.Windows.Media.Brushes.Transparent, + BorderBrush = ((TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Blue), + BorderThickness = new Thickness(0.0, 0.0, 0.0, 1.0), + CaretBrush = titleColor, + Padding = new Thickness(0.0), + Margin = new Thickness(0.0) + }; + parent.Children.RemoveAt(num); + parent.Children.Insert(num, editBox); + bool committed = false; + editBox.KeyDown += delegate(object _, System.Windows.Input.KeyEventArgs ke) + { + //IL_0002: Unknown result type (might be due to invalid IL or missing references) + //IL_0008: Invalid comparison between Unknown and I4 + //IL_0020: Unknown result type (might be due to invalid IL or missing references) + //IL_0027: Invalid comparison between Unknown and I4 + if ((int)ke.Key == 6) + { + ke.Handled = true; + CommitEdit(); + } + if ((int)ke.Key == 13) + { + ke.Handled = true; + CancelEdit(); + } + }; + editBox.LostFocus += delegate + { + CommitEdit(); + }; + editBox.Focus(); + editBox.SelectAll(); + void CancelEdit() + { + if (committed) + { + return; + } + committed = true; + try + { + int num2 = parent.Children.IndexOf(editBox); + if (num2 >= 0) + { + parent.Children.RemoveAt(num2); + parent.Children.Insert(num2, titleTb); + } + } + catch + { + } + } + void CommitEdit() + { + if (!committed) + { + committed = true; + string text = editBox.Text.Trim(); + if (string.IsNullOrEmpty(text)) + { + text = titleTb.Text; + } + titleTb.Text = text; + try + { + int num2 = parent.Children.IndexOf(editBox); + if (num2 >= 0) + { + parent.Children.RemoveAt(num2); + parent.Children.Insert(num2, titleTb); + } + } + catch + { + } + ChatConversation chatConversation = _storage.Load(conversationId); + if (chatConversation != null) + { + chatConversation.Title = text; + _storage.Save(chatConversation); + lock (_convLock) + { + if (_currentConversation?.Id == conversationId) + { + _currentConversation.Title = text; + } + } + UpdateChatTitle(); + } + } + } + } + catch (Exception ex) + { + LogService.Error("제목 편집 오류: " + ex.Message); + } + } + + private void ShowConversationMenu(string conversationId) + { + ChatConversation conv = _storage.Load(conversationId); + bool flag = conv?.Pinned ?? false; + System.Windows.Media.Brush background = (TryFindResource("LauncherBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromRgb(26, 27, 46)); + System.Windows.Media.Brush borderBrush = (TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush primaryText = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush brush = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush hoverBg = (TryFindResource("ItemHoverBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromArgb(30, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + Popup popup = new Popup + { + StaysOpen = false, + AllowsTransparency = true, + PopupAnimation = PopupAnimation.Fade, + Placement = PlacementMode.MousePoint + }; + Border border = new Border + { + Background = background, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1.0), + CornerRadius = new CornerRadius(12.0), + Padding = new Thickness(6.0), + MinWidth = 200.0, + Effect = new DropShadowEffect + { + BlurRadius = 16.0, + ShadowDepth = 4.0, + Opacity = 0.3, + Color = Colors.Black + } + }; + StackPanel stackPanel = new StackPanel(); + stackPanel.Children.Add(CreateMenuItem(flag ? "\ue77a" : "\ue718", flag ? "고정 해제" : "상단 고정", (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Blue, delegate + { + ChatConversation chatConversation = _storage.Load(conversationId); + if (chatConversation != null) + { + chatConversation.Pinned = !chatConversation.Pinned; + _storage.Save(chatConversation); + lock (_convLock) + { + if (_currentConversation?.Id == conversationId) + { + _currentConversation.Pinned = chatConversation.Pinned; + } + } + RefreshConversationList(); + } + })); + stackPanel.Children.Add(CreateMenuItem("\ue8ac", "이름 변경", brush, delegate + { + foreach (UIElement child2 in ConversationPanel.Children) + { + if (child2 is Border { Child: Grid child }) + { + foreach (UIElement child3 in child.Children) + { + if (child3 is StackPanel stackPanel3 && stackPanel3.Children.Count > 0 && stackPanel3.Children[0] is TextBlock textBlock && conv != null && textBlock.Text == conv.Title) + { + System.Windows.Media.Brush titleColor = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + EnterTitleEditMode(textBlock, conversationId, titleColor); + return; + } + } + } + } + })); + if ((_activeTab == "Cowork" || _activeTab == "Code") && conv != null) + { + string catKey = conv.Category ?? "일반"; + string text = "\ue8bd"; + string text2 = catKey; + string value = "#6B7280"; + (string, string, string, string) tuple = ChatCategory.All.FirstOrDefault(((string Key, string Label, string Symbol, string Color) c) => c.Key == catKey); + (string, string, string, string) tuple2 = tuple; + if ((tuple2.Item1 != null || tuple2.Item2 != null || tuple2.Item3 != null || tuple2.Item4 != null) && tuple.Item1 != "일반") + { + text = tuple.Item3; + text2 = tuple.Item2; + value = tuple.Item4; + } + else + { + TopicPreset topicPreset = PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets).FirstOrDefault((TopicPreset p) => p.Category == catKey); + if (topicPreset != null) + { + text = topicPreset.Symbol; + text2 = topicPreset.Label; + value = topicPreset.Color; + } + } + stackPanel.Children.Add(CreateSeparator()); + StackPanel stackPanel2 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal, + Margin = new Thickness(10.0, 4.0, 10.0, 4.0) + }; + try + { + SolidColorBrush foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(value)); + stackPanel2.Children.Add(new TextBlock + { + Text = text, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 12.0, + Foreground = foreground, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 6.0, 0.0) + }); + stackPanel2.Children.Add(new TextBlock + { + Text = text2, + FontSize = 12.0, + Foreground = primaryText, + VerticalAlignment = VerticalAlignment.Center + }); + } + catch + { + stackPanel2.Children.Add(new TextBlock + { + Text = text2, + FontSize = 12.0, + Foreground = primaryText + }); + } + stackPanel.Children.Add(stackPanel2); + } + if (_activeTab == "Chat") + { + stackPanel.Children.Add(CreateSeparator()); + stackPanel.Children.Add(new TextBlock + { + Text = "분류 변경", + FontSize = 10.5, + Foreground = brush, + Margin = new Thickness(10.0, 4.0, 0.0, 4.0), + FontWeight = FontWeights.SemiBold + }); + string text3 = conv?.Category ?? "일반"; + System.Windows.Media.Brush color = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.CornflowerBlue; + (string, string, string, string)[] all = ChatCategory.All; + for (int num = 0; num < all.Length; num++) + { + (string, string, string, string) tuple3 = all[num]; + string item = tuple3.Item1; + string item2 = tuple3.Item2; + string item3 = tuple3.Item3; + string item4 = tuple3.Item4; + string capturedKey = item; + bool flag2 = capturedKey == text3; + Border border2 = new Border + { + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(8.0), + Padding = new Thickness(10.0, 7.0, 10.0, 7.0), + Margin = new Thickness(0.0, 1.0, 0.0, 1.0), + Cursor = System.Windows.Input.Cursors.Hand + }; + Grid grid = new Grid(); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = new GridLength(24.0) + }); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = new GridLength(1.0, GridUnitType.Star) + }); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = new GridLength(20.0) + }); + TextBlock element = new TextBlock + { + Text = item3, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 12.0, + Foreground = BrushFromHex(item4), + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(element, 0); + grid.Children.Add(element); + TextBlock element2 = new TextBlock + { + Text = item2, + FontSize = 12.5, + Foreground = primaryText, + VerticalAlignment = VerticalAlignment.Center, + FontWeight = (flag2 ? FontWeights.Bold : FontWeights.Normal) + }; + Grid.SetColumn(element2, 1); + grid.Children.Add(element2); + if (flag2) + { + FrameworkElement element3 = CreateSimpleCheck(color); + Grid.SetColumn(element3, 2); + grid.Children.Add(element3); + } + border2.Child = grid; + border2.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border3) + { + border3.Background = hoverBg; + } + }; + border2.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border3) + { + border3.Background = System.Windows.Media.Brushes.Transparent; + } + }; + border2.MouseLeftButtonUp += delegate + { + popup.IsOpen = false; + ChatConversation chatConversation = _storage.Load(conversationId); + if (chatConversation != null) + { + chatConversation.Category = capturedKey; + TopicPreset byCategory = PresetService.GetByCategory(capturedKey); + if (byCategory != null) + { + chatConversation.SystemCommand = byCategory.SystemPrompt; + } + _storage.Save(chatConversation); + lock (_convLock) + { + if (_currentConversation?.Id == conversationId) + { + _currentConversation.Category = capturedKey; + if (byCategory != null) + { + _currentConversation.SystemCommand = byCategory.SystemPrompt; + } + } + } + bool flag3; + lock (_convLock) + { + flag3 = _currentConversation?.Id == conversationId; + } + if (flag3 && byCategory != null && !string.IsNullOrEmpty(byCategory.Placeholder)) + { + _promptCardPlaceholder = byCategory.Placeholder; + UpdateWatermarkVisibility(); + if (string.IsNullOrEmpty(InputBox.Text)) + { + InputWatermark.Text = byCategory.Placeholder; + InputWatermark.Visibility = Visibility.Visible; + } + } + else if (flag3) + { + ClearPromptCardPlaceholder(); + } + RefreshConversationList(); + } + }; + stackPanel.Children.Add(border2); + } + } + stackPanel.Children.Add(CreateSeparator()); + stackPanel.Children.Add(CreateMenuItem("\ue74d", "이 대화 삭제", System.Windows.Media.Brushes.IndianRed, delegate + { + MessageBoxResult messageBoxResult = CustomMessageBox.Show("이 대화를 삭제하시겠습니까?", "대화 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question); + if (messageBoxResult == MessageBoxResult.Yes) + { + _storage.Delete(conversationId); + lock (_convLock) + { + if (_currentConversation?.Id == conversationId) + { + _currentConversation = null; + MessagePanel.Children.Clear(); + EmptyState.Visibility = Visibility.Visible; + UpdateChatTitle(); + } + } + RefreshConversationList(); + } + })); + border.Child = stackPanel; + popup.Child = border; + popup.IsOpen = true; + Border CreateMenuItem(string icon, string text4, System.Windows.Media.Brush iconColor, Action onClick) + { + Border border3 = new Border + { + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(8.0), + Padding = new Thickness(10.0, 7.0, 10.0, 7.0), + Margin = new Thickness(0.0, 1.0, 0.0, 1.0), + Cursor = System.Windows.Input.Cursors.Hand + }; + Grid grid2 = new Grid + { + ColumnDefinitions = + { + new ColumnDefinition + { + Width = new GridLength(24.0) + }, + new ColumnDefinition + { + Width = new GridLength(1.0, GridUnitType.Star) + } + } + }; + TextBlock element4 = new TextBlock + { + Text = icon, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 12.0, + Foreground = iconColor, + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(element4, 0); + grid2.Children.Add(element4); + TextBlock element5 = new TextBlock + { + Text = text4, + FontSize = 12.5, + Foreground = primaryText, + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(element5, 1); + grid2.Children.Add(element5); + border3.Child = grid2; + border3.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border4) + { + border4.Background = hoverBg; + } + }; + border3.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border4) + { + border4.Background = System.Windows.Media.Brushes.Transparent; + } + }; + border3.MouseLeftButtonUp += delegate + { + popup.IsOpen = false; + onClick(); + }; + return border3; + } + Border CreateSeparator() + { + return new Border + { + Height = 1.0, + Background = borderBrush, + Opacity = 0.3, + Margin = new Thickness(8.0, 4.0, 8.0, 4.0) + }; + } + } + + private void SearchBox_TextChanged(object sender, TextChangedEventArgs e) + { + RefreshConversationList(); + } + + private static string FormatDate(DateTime dt) + { + TimeSpan timeSpan = DateTime.Now - dt; + if (timeSpan.TotalMinutes < 1.0) + { + return "방금 전"; + } + if (timeSpan.TotalHours < 1.0) + { + return $"{(int)timeSpan.TotalMinutes}분 전"; + } + if (timeSpan.TotalDays < 1.0) + { + return $"{(int)timeSpan.TotalHours}시간 전"; + } + if (timeSpan.TotalDays < 7.0) + { + return $"{(int)timeSpan.TotalDays}일 전"; + } + return dt.ToString("MM/dd"); + } + + private void UpdateChatTitle() + { + lock (_convLock) + { + ChatTitle.Text = _currentConversation?.Title ?? ""; + } + } + + private void RenderMessages() + { + MessagePanel.Children.Clear(); + UpdateExecutionHistoryUi(); + ChatConversation currentConversation; + lock (_convLock) + { + currentConversation = _currentConversation; + } + if (currentConversation == null || (currentConversation.Messages.Count == 0 && currentConversation.ExecutionEvents.Count == 0)) + { + EmptyState.Visibility = Visibility.Visible; + return; + } + EmptyState.Visibility = Visibility.Collapsed; + List<(DateTime, int, ChatMessage, ChatExecutionEvent)> list = new List<(DateTime, int, ChatMessage, ChatExecutionEvent)>(); + int num = 0; + foreach (ChatMessage message in currentConversation.Messages) + { + if (!(message.Role == "system")) + { + list.Add((message.Timestamp, num++, message, null)); + } + } + if (_showExecutionHistory) + { + foreach (ChatExecutionEvent executionEvent in currentConversation.ExecutionEvents) + { + list.Add((executionEvent.Timestamp, num++, null, executionEvent)); + } + } + foreach (var item in from x in list + orderby x.Timestamp, x.Sequence + select x) + { + if (item.Item3 != null) + { + AddMessageBubble(item.Item3.Role, item.Item3.Content, animate: false, item.Item3); + } + else if (item.Item4 != null) + { + AddAgentEventBanner(ToAgentEvent(item.Item4), animate: false); + } + } + ((DispatcherObject)this).Dispatcher.InvokeAsync((Action)delegate + { + MessageScroll.ScrollToEnd(); + }, (DispatcherPriority)4); + } + + private static AgentEvent ToAgentEvent(ChatExecutionEvent stored) + { + Enum.TryParse(stored.Type, ignoreCase: true, out var result); + return new AgentEvent + { + Timestamp = stored.Timestamp, + Type = result, + ToolName = stored.ToolName, + Summary = stored.Summary, + FilePath = stored.FilePath, + Success = stored.Success, + StepCurrent = stored.StepCurrent, + StepTotal = stored.StepTotal, + Steps = stored.Steps, + ElapsedMs = stored.ElapsedMs, + InputTokens = stored.InputTokens, + OutputTokens = stored.OutputTokens, + ToolInput = stored.ToolInput, + Iteration = stored.Iteration + }; + } + + private static ChatExecutionEvent ToStoredAgentEvent(AgentEvent evt) + { + return new ChatExecutionEvent + { + Timestamp = evt.Timestamp, + Type = evt.Type.ToString(), + ToolName = evt.ToolName, + Summary = evt.Summary, + FilePath = evt.FilePath, + Success = evt.Success, + StepCurrent = evt.StepCurrent, + StepTotal = evt.StepTotal, + Steps = evt.Steps?.ToList(), + ElapsedMs = evt.ElapsedMs, + InputTokens = evt.InputTokens, + OutputTokens = evt.OutputTokens, + ToolInput = evt.ToolInput, + Iteration = evt.Iteration + }; + } + + private bool ShouldPersistAgentEvent(AgentEvent evt) + { + string agentLogLevel = _settings.Settings.Llm.AgentLogLevel; + if (agentLogLevel == "simple" && evt.Type == AgentEventType.ToolCall) + { + return false; + } + AgentEventType type = evt.Type; + if (1 == 0) + { + } + bool result; + switch (type) + { + case AgentEventType.Planning: + { + List steps = evt.Steps; + result = steps != null && steps.Count > 0; + break; + } + case AgentEventType.StepStart: + result = evt.StepTotal > 0; + break; + default: + result = true; + break; + } + if (1 == 0) + { + } + return result; + } + + private void PersistAgentEvent(AgentEvent evt) + { + if (!ShouldPersistAgentEvent(evt)) + { + return; + } + ChatConversation currentConversation; + lock (_convLock) + { + currentConversation = _currentConversation; + currentConversation?.ExecutionEvents.Add(ToStoredAgentEvent(evt)); + } + if (currentConversation != null) + { + try + { + _storage.Save(currentConversation); + } + catch (Exception ex) + { + LogService.Debug("실행 이력 저장 실패: " + ex.Message); + } + UpdateExecutionHistoryUi(); + } + } + + private int GetExecutionEventCount() + { + lock (_convLock) + { + return _currentConversation?.ExecutionEvents.Count ?? 0; + } + } + + private void UpdateExecutionHistoryUi() + { + if (ExecutionLogLabel != null && ExecutionLogIcon != null && BtnToggleExecutionLog != null) + { + int executionEventCount = GetExecutionEventCount(); + ExecutionLogLabel.Text = (_showExecutionHistory ? $"실행 로그 {executionEventCount}" : $"실행 로그 숨김 ({executionEventCount})"); + ExecutionLogIcon.Text = (_showExecutionHistory ? "\ue7c1" : "\ue8f8"); + BtnToggleExecutionLog.ToolTip = (_showExecutionHistory ? "실행 로그를 숨깁니다" : "실행 로그를 다시 표시합니다"); + } + } + + private void RefreshSubAgentIndicator() + { + if (SubAgentIndicator == null || SubAgentIndicatorLabel == null) + { + return; + } + List list = (from x in SubAgentTool.ActiveTasks.Values + where !x.CompletedAt.HasValue + orderby x.StartedAt + select x).ToList(); + if (list.Count == 0) + { + SubAgentIndicator.Visibility = Visibility.Collapsed; + SubAgentIndicator.ToolTip = "실행 중인 서브에이전트가 없습니다"; + return; + } + SubAgentIndicator.Visibility = Visibility.Visible; + SubAgentIndicatorLabel.Text = $"서브에이전트 {list.Count}"; + SubAgentIndicator.ToolTip = string.Join(Environment.NewLine, list.Select((SubAgentTask x) => x.Id + ": " + TruncateForStatus(x.Task, 48))); + } + + private void AddMessageBubble(string role, string content, bool animate = true, ChatMessage? message = null) + { + //IL_05d5: Unknown result type (might be due to invalid IL or missing references) + Border border; + if (role == "user") + { + StackPanel wrapper = new StackPanel + { + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, + MaxWidth = 620.0, + Margin = new Thickness(160.0, 10.0, 48.0, 10.0) + }; + border = new Border(); + border.Background = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Blue; + border.CornerRadius = new CornerRadius(16.0, 4.0, 16.0, 16.0); + border.Padding = new Thickness(16.0, 10.0, 16.0, 10.0); + border.Child = new TextBlock + { + Text = content, + FontSize = 13.5, + Foreground = System.Windows.Media.Brushes.White, + TextWrapping = TextWrapping.Wrap, + LineHeight = 21.0 + }; + Border element = border; + wrapper.Children.Add(element); + StackPanel userActionBar = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal, + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, + Opacity = 0.0, + Margin = new Thickness(0.0, 2.0, 0.0, 0.0) + }; + string capturedUserContent = content; + System.Windows.Media.Brush foreground = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + userActionBar.Children.Add(CreateActionButton("\ue8c8", "복사", foreground, delegate + { + try + { + System.Windows.Clipboard.SetText(capturedUserContent); + } + catch + { + } + })); + userActionBar.Children.Add(CreateActionButton("\ue70f", "편집", foreground, delegate + { + EnterEditMode(wrapper, capturedUserContent); + })); + Grid grid = new Grid + { + Margin = new Thickness(0.0, 2.0, 0.0, 0.0) + }; + DateTime dateTime = message?.Timestamp ?? DateTime.Now; + grid.Children.Add(new TextBlock + { + Text = dateTime.ToString("HH:mm"), + FontSize = 10.0, + Opacity = 0.5, + Foreground = ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 4.0, 0.0) + }); + grid.Children.Add(userActionBar); + wrapper.Children.Add(grid); + wrapper.MouseEnter += delegate + { + userActionBar.Opacity = 1.0; + }; + wrapper.MouseLeave += delegate + { + userActionBar.Opacity = 0.0; + }; + string userContent = content; + wrapper.MouseRightButtonUp += delegate(object _, MouseButtonEventArgs re) + { + re.Handled = true; + ShowMessageContextMenu(userContent, "user"); + }; + if (animate) + { + ApplyMessageEntryAnimation(wrapper); + } + MessagePanel.Children.Add(wrapper); + return; + } + StackPanel stackPanel = new StackPanel + { + HorizontalAlignment = System.Windows.HorizontalAlignment.Left, + MaxWidth = GetMessageMaxWidth(), + Margin = new Thickness(48.0, 10.0, 120.0, 10.0) + }; + if (animate) + { + ApplyMessageEntryAnimation(stackPanel); + } + border = new Border(); + border.Background = (TryFindResource("ItemBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + border.BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromArgb(24, 17, 24, 39)); + border.BorderThickness = new Thickness(1.0); + border.CornerRadius = new CornerRadius(18.0); + border.Padding = new Thickness(16.0, 14.0, 16.0, 12.0); + border.Effect = new DropShadowEffect + { + BlurRadius = 16.0, + ShadowDepth = 0.0, + Opacity = 0.06 + }; + Border border2 = border; + StackPanel stackPanel2 = new StackPanel(); + (string Name, string Symbol, string Color) agentIdentity = GetAgentIdentity(); + string item = agentIdentity.Name; + string item2 = agentIdentity.Symbol; + string item3 = agentIdentity.Color; + SolidColorBrush foreground2 = BrushFromHex(item3); + StackPanel stackPanel3 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal, + Margin = new Thickness(0.0, 0.0, 0.0, 8.0) + }; + TextBlock textBlock = new TextBlock + { + Text = "◆", + FontSize = 13.0, + Foreground = foreground2, + VerticalAlignment = VerticalAlignment.Center, + RenderTransformOrigin = new Point(0.5, 0.5), + RenderTransform = new RotateTransform(0.0) + }; + if (animate) + { + DoubleAnimation animation = new DoubleAnimation + { + From = 0.0, + To = 360.0, + Duration = TimeSpan.FromSeconds(1.2), + EasingFunction = new CubicEase + { + EasingMode = EasingMode.EaseOut + } + }; + ((RotateTransform)textBlock.RenderTransform).BeginAnimation(RotateTransform.AngleProperty, animation); + } + stackPanel3.Children.Add(textBlock); + stackPanel3.Children.Add(new TextBlock + { + Text = item, + FontSize = 11.0, + FontWeight = FontWeights.SemiBold, + Foreground = foreground2, + Margin = new Thickness(6.0, 0.0, 0.0, 0.0), + VerticalAlignment = VerticalAlignment.Center + }); + stackPanel2.Children.Add(stackPanel3); + MarkdownRenderer.EnableFilePathHighlight = ((!(System.Windows.Application.Current is App app)) ? ((bool?)null) : app.SettingsService?.Settings.Llm.EnableFilePathHighlight) ?? true; + System.Windows.Media.Brush textColor = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush secondaryColor = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush accentColor = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Blue; + System.Windows.Media.Brush codeBg = (TryFindResource("HintBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.DarkGray; + StackPanel stackPanel4 = MarkdownRenderer.Render(content, textColor, secondaryColor, accentColor, codeBg); + stackPanel4.Margin = new Thickness(0.0, 0.0, 0.0, 6.0); + stackPanel2.Children.Add(stackPanel4); + StackPanel stackPanel5 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal, + HorizontalAlignment = System.Windows.HorizontalAlignment.Left, + Margin = new Thickness(0.0, 6.0, 0.0, 0.0) + }; + System.Windows.Media.Brush brush = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + SolidColorBrush solidColorBrush = new SolidColorBrush(System.Windows.Media.Color.FromRgb(139, 144, 176)); + string capturedContent = content; + stackPanel5.Children.Add(CreateActionButton("\ue8c8", "복사", brush, delegate + { + try + { + System.Windows.Clipboard.SetText(capturedContent); + } + catch + { + } + })); + stackPanel5.Children.Add(CreateActionButton("\ue72c", "다시 생성", brush, delegate + { + RegenerateLastAsync(); + })); + stackPanel5.Children.Add(CreateActionButton("\ue70f", "수정 후 재시도", brush, delegate + { + ShowRetryWithFeedbackInput(); + })); + AddLinkedFeedbackButtons(stackPanel5, brush, message); + DateTime dateTime2 = message?.Timestamp ?? DateTime.Now; + stackPanel5.Children.Add(new TextBlock + { + Text = dateTime2.ToString("HH:mm"), + FontSize = 10.0, + Opacity = 0.5, + Foreground = brush, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(8.0, 0.0, 0.0, 0.0) + }); + stackPanel2.Children.Add(stackPanel5); + border2.Child = stackPanel2; + stackPanel.Children.Add(border2); + string aiContent = content; + border2.MouseRightButtonUp += delegate(object _, MouseButtonEventArgs re) + { + re.Handled = true; + ShowMessageContextMenu(aiContent, "assistant"); + }; + MessagePanel.Children.Add(stackPanel); + } + + private string GetCheckStyle() + { + string text = (_settings.Settings.Launcher.Theme ?? "system").ToLowerInvariant(); + if (1 == 0) + { + } + string result; + switch (text) + { + case "dark": + case "system": + result = "circle"; + break; + case "oled": + result = "glow"; + break; + case "light": + result = "roundrect"; + break; + case "nord": + result = "diamond"; + break; + case "catppuccin": + result = "pill"; + break; + case "monokai": + result = "square"; + break; + case "sepia": + result = "stamp"; + break; + case "alfred": + result = "minimal"; + break; + case "alfredlight": + result = "minimal"; + break; + default: + result = "circle"; + break; + } + if (1 == 0) + { + } + return result; + } + + private FrameworkElement CreateCheckIcon(bool isChecked, System.Windows.Media.Brush? accentBrush = null) + { + System.Windows.Media.Brush color = accentBrush ?? (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.CornflowerBlue; + if (isChecked) + { + return CreateSimpleCheck(color); + } + return new System.Windows.Shapes.Rectangle + { + Width = 14.0, + Height = 14.0, + Fill = System.Windows.Media.Brushes.Transparent, + Margin = new Thickness(0.0, 0.0, 10.0, 0.0) + }; + } + + private static void AnimateScale(FrameworkElement el, double from, double to, int ms, IEasingFunction ease) + { + if (el.RenderTransform is TransformGroup transformGroup) + { + ScaleTransform scaleTransform = transformGroup.Children.OfType().FirstOrDefault(); + if (scaleTransform != null) + { + DoubleAnimation animation = new DoubleAnimation(from, to, TimeSpan.FromMilliseconds(ms)) + { + EasingFunction = ease + }; + scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, animation); + scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, animation); + return; + } + } + if (el.RenderTransform is ScaleTransform scaleTransform2) + { + DoubleAnimation animation2 = new DoubleAnimation(from, to, TimeSpan.FromMilliseconds(ms)) + { + EasingFunction = ease + }; + scaleTransform2.BeginAnimation(ScaleTransform.ScaleXProperty, animation2); + scaleTransform2.BeginAnimation(ScaleTransform.ScaleYProperty, animation2); + } + } + + private static void ApplyHoverScaleAnimation(FrameworkElement element, double hoverScale = 1.08) + { + element.Loaded += delegate + { + EnsureTransform(); + }; + element.MouseEnter += delegate + { + EnsureTransform(); + ScaleTransform scaleTransform = (ScaleTransform)element.RenderTransform; + DoubleAnimation animation = new DoubleAnimation(hoverScale, TimeSpan.FromMilliseconds(150.0)) + { + EasingFunction = new CubicEase + { + EasingMode = EasingMode.EaseOut + } + }; + scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, animation); + scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, animation); + }; + element.MouseLeave += delegate + { + EnsureTransform(); + ScaleTransform scaleTransform = (ScaleTransform)element.RenderTransform; + DoubleAnimation animation = new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(200.0)) + { + EasingFunction = new CubicEase + { + EasingMode = EasingMode.EaseOut + } + }; + scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, animation); + scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, animation); + }; + void EnsureTransform() + { + //IL_0019: Unknown result type (might be due to invalid IL or missing references) + element.RenderTransformOrigin = new Point(0.5, 0.5); + if (!(element.RenderTransform is ScaleTransform) || ((Freezable)element.RenderTransform).IsFrozen) + { + element.RenderTransform = new ScaleTransform(1.0, 1.0); + } + } + } + + private static void ApplyHoverBounceAnimation(FrameworkElement element, double bounceY = -2.5) + { + element.Loaded += delegate + { + EnsureTransform(); + }; + element.MouseEnter += delegate + { + EnsureTransform(); + TranslateTransform translateTransform = (TranslateTransform)element.RenderTransform; + translateTransform.BeginAnimation(TranslateTransform.YProperty, new DoubleAnimation(bounceY, TimeSpan.FromMilliseconds(200.0)) + { + EasingFunction = new CubicEase + { + EasingMode = EasingMode.EaseOut + } + }); + }; + element.MouseLeave += delegate + { + EnsureTransform(); + TranslateTransform translateTransform = (TranslateTransform)element.RenderTransform; + translateTransform.BeginAnimation(TranslateTransform.YProperty, new DoubleAnimation(0.0, TimeSpan.FromMilliseconds(250.0)) + { + EasingFunction = new ElasticEase + { + EasingMode = EasingMode.EaseOut, + Oscillations = 1, + Springiness = 10.0 + } + }); + }; + void EnsureTransform() + { + if (!(element.RenderTransform is TranslateTransform) || ((Freezable)element.RenderTransform).IsFrozen) + { + element.RenderTransform = new TranslateTransform(0.0, 0.0); + } + } + } + + private static FrameworkElement CreateSimpleCheck(System.Windows.Media.Brush color, double size = 14.0) + { + return new System.Windows.Shapes.Path + { + Data = Geometry.Parse($"M {size * 0.15} {size * 0.5} L {size * 0.4} {size * 0.75} L {size * 0.85} {size * 0.28}"), + Stroke = color, + StrokeThickness = 2.0, + StrokeStartLineCap = PenLineCap.Round, + StrokeEndLineCap = PenLineCap.Round, + StrokeLineJoin = PenLineJoin.Round, + Width = size, + Height = size, + Margin = new Thickness(0.0, 0.0, 10.0, 0.0), + VerticalAlignment = VerticalAlignment.Center + }; + } + + private static void ApplyMenuItemHover(Border item) + { + //IL_0067: Unknown result type (might be due to invalid IL or missing references) + System.Windows.Media.Brush originalBg = item.Background?.Clone() ?? System.Windows.Media.Brushes.Transparent; + if (((Freezable)originalBg).CanFreeze) + { + ((Freezable)originalBg).Freeze(); + } + item.RenderTransformOrigin = new Point(0.5, 0.5); + item.RenderTransform = new ScaleTransform(1.0, 1.0); + item.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border) + { + if (originalBg is SolidColorBrush { Color: { A: >32 } }) + { + border.Opacity = 0.85; + } + else + { + border.Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(24, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + } + } + ScaleTransform scaleTransform = item.RenderTransform as ScaleTransform; + scaleTransform?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120.0))); + scaleTransform?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120.0))); + }; + item.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border) + { + border.Opacity = 1.0; + border.Background = originalBg; + } + ScaleTransform scaleTransform = item.RenderTransform as ScaleTransform; + scaleTransform?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150.0))); + scaleTransform?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150.0))); + }; + } + + private System.Windows.Controls.Button CreateActionButton(string symbol, string tooltip, System.Windows.Media.Brush foreground, Action onClick) + { + System.Windows.Media.Brush hoverBrush = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + TextBlock icon = new TextBlock + { + Text = symbol, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 12.0, + Foreground = foreground, + VerticalAlignment = VerticalAlignment.Center + }; + System.Windows.Controls.Button button = new System.Windows.Controls.Button + { + Content = icon, + Background = System.Windows.Media.Brushes.Transparent, + BorderThickness = new Thickness(0.0), + Cursor = System.Windows.Input.Cursors.Hand, + Padding = new Thickness(6.0, 4.0, 6.0, 4.0), + Margin = new Thickness(0.0, 0.0, 4.0, 0.0), + ToolTip = tooltip + }; + button.MouseEnter += delegate + { + icon.Foreground = hoverBrush; + }; + button.MouseLeave += delegate + { + icon.Foreground = foreground; + }; + button.Click += delegate + { + onClick(); + }; + ApplyHoverScaleAnimation(button, 1.15); + return button; + } + + private System.Windows.Controls.Button CreateFeedbackButton(string outline, string filled, string tooltip, System.Windows.Media.Brush normalColor, System.Windows.Media.Brush activeColor, ChatMessage? message = null, string feedbackType = "", Action? resetSibling = null, Action? registerReset = null) + { + //IL_0102: Unknown result type (might be due to invalid IL or missing references) + System.Windows.Media.Brush hoverBrush = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + bool isActive = message?.Feedback == feedbackType; + TextBlock icon = new TextBlock + { + Text = (isActive ? filled : outline), + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 12.0, + Foreground = (isActive ? activeColor : normalColor), + VerticalAlignment = VerticalAlignment.Center, + RenderTransformOrigin = new Point(0.5, 0.5), + RenderTransform = new ScaleTransform(1.0, 1.0) + }; + System.Windows.Controls.Button button = new System.Windows.Controls.Button + { + Content = icon, + Background = System.Windows.Media.Brushes.Transparent, + BorderThickness = new Thickness(0.0), + Cursor = System.Windows.Input.Cursors.Hand, + Padding = new Thickness(6.0, 4.0, 6.0, 4.0), + Margin = new Thickness(0.0, 0.0, 4.0, 0.0), + ToolTip = tooltip + }; + registerReset?.Invoke(delegate + { + isActive = false; + icon.Text = outline; + icon.Foreground = normalColor; + }); + button.MouseEnter += delegate + { + if (!isActive) + { + icon.Foreground = hoverBrush; + } + }; + button.MouseLeave += delegate + { + if (!isActive) + { + icon.Foreground = normalColor; + } + }; + button.Click += delegate + { + isActive = !isActive; + icon.Text = (isActive ? filled : outline); + icon.Foreground = (isActive ? activeColor : normalColor); + if (isActive) + { + resetSibling?.Invoke(); + } + if (message != null) + { + message.Feedback = (isActive ? feedbackType : null); + try + { + ChatConversation currentConversation; + lock (_convLock) + { + currentConversation = _currentConversation; + } + if (currentConversation != null) + { + _storage.Save(currentConversation); + } + } + catch + { + } + } + ScaleTransform scaleTransform = (ScaleTransform)icon.RenderTransform; + DoubleAnimation animation = new DoubleAnimation(1.3, 1.0, TimeSpan.FromMilliseconds(250.0)) + { + EasingFunction = new ElasticEase + { + EasingMode = EasingMode.EaseOut, + Oscillations = 1, + Springiness = 5.0 + } + }; + scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, animation); + scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, animation); + }; + return button; + } + + private void AddLinkedFeedbackButtons(StackPanel actionBar, System.Windows.Media.Brush btnColor, ChatMessage? message) + { + Action resetLikeAction = null; + Action resetDislikeAction = null; + System.Windows.Controls.Button element = CreateFeedbackButton("\ue8e1", "\ueb51", "좋아요", btnColor, new SolidColorBrush(System.Windows.Media.Color.FromRgb(56, 161, 105)), message, "like", delegate + { + resetDislikeAction?.Invoke(); + }, delegate(Action reset) + { + resetLikeAction = reset; + }); + System.Windows.Controls.Button element2 = CreateFeedbackButton("\ue8e0", "\ueb50", "싫어요", btnColor, new SolidColorBrush(System.Windows.Media.Color.FromRgb(229, 62, 62)), message, "dislike", delegate + { + resetLikeAction?.Invoke(); + }, delegate(Action reset) + { + resetDislikeAction = reset; + }); + actionBar.Children.Add(element); + actionBar.Children.Add(element2); + } + + private static void ApplyMessageEntryAnimation(FrameworkElement element) + { + element.Opacity = 0.0; + element.RenderTransform = new TranslateTransform(0.0, 16.0); + element.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0.0, 1.0, TimeSpan.FromMilliseconds(350.0)) + { + EasingFunction = new CubicEase + { + EasingMode = EasingMode.EaseOut + } + }); + ((TranslateTransform)element.RenderTransform).BeginAnimation(TranslateTransform.YProperty, new DoubleAnimation(16.0, 0.0, TimeSpan.FromMilliseconds(400.0)) + { + EasingFunction = new CubicEase + { + EasingMode = EasingMode.EaseOut + } + }); + } + + private void EnterEditMode(StackPanel wrapper, string originalText) + { + if (_isStreaming || _isEditing) + { + return; + } + _isEditing = true; + int idx = MessagePanel.Children.IndexOf(wrapper); + if (idx < 0) + { + _isEditing = false; + return; + } + StackPanel stackPanel = new StackPanel + { + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, + MaxWidth = 540.0, + Margin = wrapper.Margin + }; + System.Windows.Controls.TextBox editBox = new System.Windows.Controls.TextBox + { + Text = originalText, + FontSize = 13.5, + Foreground = ((TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White), + Background = ((TryFindResource("ItemBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.DarkGray), + CaretBrush = ((TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Blue), + BorderBrush = ((TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Blue), + BorderThickness = new Thickness(1.5), + Padding = new Thickness(14.0, 10.0, 14.0, 10.0), + TextWrapping = TextWrapping.Wrap, + AcceptsReturn = false, + MaxHeight = 200.0, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto + }; + Border element = new Border + { + CornerRadius = new CornerRadius(14.0), + Child = editBox, + ClipToBounds = true + }; + stackPanel.Children.Add(element); + StackPanel stackPanel2 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal, + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, + Margin = new Thickness(0.0, 6.0, 0.0, 0.0) + }; + System.Windows.Media.Brush brush = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Blue; + System.Windows.Media.Brush foreground = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Controls.Button button = new System.Windows.Controls.Button + { + Content = new TextBlock + { + Text = "취소", + FontSize = 12.0, + Foreground = foreground + }, + Background = System.Windows.Media.Brushes.Transparent, + BorderThickness = new Thickness(0.0), + Cursor = System.Windows.Input.Cursors.Hand, + Padding = new Thickness(12.0, 5.0, 12.0, 5.0), + Margin = new Thickness(0.0, 0.0, 6.0, 0.0) + }; + button.Click += delegate + { + _isEditing = false; + if (idx >= 0 && idx < MessagePanel.Children.Count) + { + MessagePanel.Children[idx] = wrapper; + } + }; + stackPanel2.Children.Add(button); + System.Windows.Controls.Button button2 = new System.Windows.Controls.Button + { + Cursor = System.Windows.Input.Cursors.Hand, + Padding = new Thickness(0.0) + }; + button2.Template = (ControlTemplate)XamlReader.Parse(""); + button2.Click += delegate + { + string text = editBox.Text.Trim(); + if (!string.IsNullOrEmpty(text)) + { + SubmitEditAsync(idx, text); + } + }; + stackPanel2.Children.Add(button2); + stackPanel.Children.Add(stackPanel2); + MessagePanel.Children[idx] = stackPanel; + editBox.KeyDown += delegate(object _, System.Windows.Input.KeyEventArgs ke) + { + //IL_0002: Unknown result type (might be due to invalid IL or missing references) + //IL_0008: Invalid comparison between Unknown and I4 + //IL_000a: Unknown result type (might be due to invalid IL or missing references) + //IL_0010: Invalid comparison between Unknown and I4 + //IL_0056: Unknown result type (might be due to invalid IL or missing references) + //IL_005d: Invalid comparison between Unknown and I4 + if ((int)ke.Key == 6 && (int)Keyboard.Modifiers == 0) + { + ke.Handled = true; + string text = editBox.Text.Trim(); + if (!string.IsNullOrEmpty(text)) + { + _ = SubmitEditAsync(idx, text); + } + } + if ((int)ke.Key == 13) + { + ke.Handled = true; + _isEditing = false; + if (idx >= 0 && idx < MessagePanel.Children.Count) + { + MessagePanel.Children[idx] = wrapper; + } + } + }; + editBox.Focus(); + editBox.SelectAll(); + } + + private async Task SubmitEditAsync(int bubbleIndex, string newText) + { + _isEditing = false; + if (_isStreaming) + { + return; + } + ChatConversation conv; + lock (_convLock) + { + if (_currentConversation == null) + { + return; + } + conv = _currentConversation; + } + int userMsgIdx = -1; + int uiIdx = 0; + lock (_convLock) + { + for (int i = 0; i < conv.Messages.Count; i++) + { + if (!(conv.Messages[i].Role == "system")) + { + if (uiIdx == bubbleIndex) + { + userMsgIdx = i; + break; + } + uiIdx++; + } + } + } + if (userMsgIdx < 0) + { + return; + } + lock (_convLock) + { + conv.Messages[userMsgIdx].Content = newText; + while (conv.Messages.Count > userMsgIdx + 1) + { + conv.Messages.RemoveAt(conv.Messages.Count - 1); + } + } + while (MessagePanel.Children.Count > bubbleIndex + 1) + { + MessagePanel.Children.RemoveAt(MessagePanel.Children.Count - 1); + } + MessagePanel.Children.RemoveAt(bubbleIndex); + AddMessageBubble("user", newText, animate: false); + await SendRegenerateAsync(conv); + try + { + _storage.Save(conv); + } + catch (Exception ex) + { + LogService.Debug("대화 저장 실패: " + ex.Message); + } + RefreshConversationList(); + } + + private void StopAiIconPulse() + { + if (!_aiIconPulseStopped && _activeAiIcon != null) + { + _activeAiIcon.BeginAnimation(UIElement.OpacityProperty, null); + _activeAiIcon.Opacity = 1.0; + _activeAiIcon = null; + _aiIconPulseStopped = true; + } + } + + private void CursorTimer_Tick(object? sender, EventArgs e) + { + _cursorVisible = !_cursorVisible; + if (_activeStreamText != null && _displayedLength > 0) + { + string text = ((_cachedStreamContent.Length > 0) ? _cachedStreamContent.Substring(0, Math.Min(_displayedLength, _cachedStreamContent.Length)) : ""); + _activeStreamText.Text = text + (_cursorVisible ? "▌" : " "); + } + } + + private void ElapsedTimer_Tick(object? sender, EventArgs e) + { + int value = (int)(DateTime.UtcNow - _streamStartTime).TotalSeconds; + if (_elapsedLabel != null) + { + _elapsedLabel.Text = $"{value}s"; + } + if (StatusElapsed != null) + { + StatusElapsed.Text = $"{value}초"; + } + } + + private void TypingTimer_Tick(object? sender, EventArgs e) + { + if (_activeStreamText == null || string.IsNullOrEmpty(_cachedStreamContent)) + { + return; + } + int length = _cachedStreamContent.Length; + if (_displayedLength < length) + { + int num = length - _displayedLength; + int num2 = ((num > 200) ? Math.Min(num / 5, 40) : ((num <= 50) ? Math.Min(3, num) : Math.Min(num / 4, 15))); + _displayedLength += num2; + string text = _cachedStreamContent.Substring(0, _displayedLength); + _activeStreamText.Text = text + (_cursorVisible ? "▌" : " "); + if (!_userScrolled) + { + MessageScroll.ScrollToVerticalOffset(MessageScroll.ScrollableHeight); + } + } + } + + public void SendInitialMessage(string message) + { + StartNewConversation(); + InputBox.Text = message; + SendMessageAsync(); + } + + private void StartNewConversation() + { + lock (_convLock) + { + if (_currentConversation != null && _currentConversation.Messages.Count > 0) + { + try + { + _storage.Save(_currentConversation); + } + catch + { + } + } + _currentConversation = new ChatConversation + { + Tab = _activeTab + }; + string workFolder = _settings.Settings.Llm.WorkFolder; + if (!string.IsNullOrEmpty(workFolder) && _activeTab != "Chat") + { + _currentConversation.WorkFolder = workFolder; + } + } + _tabConversationId[_activeTab] = null; + MessagePanel.Children.Clear(); + EmptyState.Visibility = Visibility.Visible; + _attachedFiles.Clear(); + RefreshAttachedFilesUI(); + UpdateChatTitle(); + RefreshConversationList(); + UpdateFolderBar(); + if (_activeTab == "Cowork") + { + BuildBottomBar(); + } + } + + private void RestoreLastConversations() + { + Dictionary lastConversationIds = _settings.Settings.Llm.LastConversationIds; + if (lastConversationIds == null || lastConversationIds.Count == 0) + { + return; + } + foreach (KeyValuePair item in lastConversationIds) + { + if (_tabConversationId.ContainsKey(item.Key) && !string.IsNullOrEmpty(item.Value)) + { + _tabConversationId[item.Key] = item.Value; + } + } + string valueOrDefault = _tabConversationId.GetValueOrDefault(_activeTab); + if (string.IsNullOrEmpty(valueOrDefault)) + { + return; + } + ChatConversation chatConversation = _storage.Load(valueOrDefault); + if (chatConversation != null) + { + if (string.IsNullOrEmpty(chatConversation.Tab)) + { + chatConversation.Tab = _activeTab; + } + lock (_convLock) + { + _currentConversation = chatConversation; + } + RenderMessages(); + UpdateChatTitle(); + UpdateFolderBar(); + LoadConversationSettings(); + } + } + + private void SaveLastConversations() + { + Dictionary dictionary = new Dictionary(); + foreach (KeyValuePair item in _tabConversationId) + { + if (!string.IsNullOrEmpty(item.Value)) + { + dictionary[item.Key] = item.Value; + } + } + _settings.Settings.Llm.LastConversationIds = dictionary; + try + { + _settings.Save(); + } + catch + { + } + } + + private async void BtnSend_Click(object sender, RoutedEventArgs e) + { + await SendMessageAsync(); + } + + private async void InputBox_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e) + { + if ((int)e.Key == 65 && ((Enum)Keyboard.Modifiers).HasFlag((Enum)(object)(ModifierKeys)2) && TryPasteClipboardImage()) + { + e.Handled = true; + } + else if ((int)e.Key == 6 && !((Enum)Keyboard.Modifiers).HasFlag((Enum)(object)(ModifierKeys)4)) + { + if (SlashPopup.IsOpen && _slashSelectedIndex >= 0) + { + e.Handled = true; + ExecuteSlashSelectedItem(); + } + else if (InputBox.Text.Trim().Equals("/help", StringComparison.OrdinalIgnoreCase)) + { + e.Handled = true; + InputBox.Text = ""; + SlashPopup.IsOpen = false; + ShowSlashHelpWindow(); + } + else + { + e.Handled = true; + await SendMessageAsync(); + } + } + } + + private bool TryPasteClipboardImage() + { + if (!_settings.Settings.Llm.EnableImageInput) + { + return false; + } + if (!System.Windows.Clipboard.ContainsImage()) + { + return false; + } + try + { + BitmapSource image = System.Windows.Clipboard.GetImage(); + if (image == null) + { + return false; + } + PngBitmapEncoder pngBitmapEncoder = new PngBitmapEncoder(); + pngBitmapEncoder.Frames.Add(BitmapFrame.Create(image)); + using MemoryStream memoryStream = new MemoryStream(); + pngBitmapEncoder.Save(memoryStream); + byte[] array = memoryStream.ToArray(); + int num = _settings.Settings.Llm.MaxImageSizeKb; + if (num <= 0) + { + num = 5120; + } + if (array.Length > num * 1024) + { + CustomMessageBox.Show($"이미지가 너무 큽니다 ({array.Length / 1024}KB, 최대 {num}KB).", "이미지 크기 초과", MessageBoxButton.OK, MessageBoxImage.Exclamation); + return true; + } + string @base = Convert.ToBase64String(array); + ImageAttachment imageAttachment = new ImageAttachment + { + Base64 = @base, + MimeType = "image/png", + FileName = $"clipboard_{DateTime.Now:HHmmss}.png" + }; + _pendingImages.Add(imageAttachment); + AddImagePreview(imageAttachment, image); + return true; + } + catch (Exception ex) + { + LogService.Debug("클립보드 이미지 붙여넣기 실패: " + ex.Message); + return false; + } + } + + private void AddImagePreview(ImageAttachment attachment, BitmapSource? thumbnail = null) + { + System.Windows.Media.Brush foreground = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush background = (TryFindResource("HintBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.LightGray; + Border border = new Border + { + Background = background, + CornerRadius = new CornerRadius(6.0), + Padding = new Thickness(4.0), + Margin = new Thickness(0.0, 0.0, 4.0, 4.0) + }; + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + if (thumbnail != null) + { + stackPanel.Children.Add(new System.Windows.Controls.Image + { + Source = thumbnail, + MaxHeight = 48.0, + MaxWidth = 64.0, + Stretch = Stretch.Uniform, + Margin = new Thickness(0.0, 0.0, 4.0, 0.0) + }); + } + else + { + stackPanel.Children.Add(new TextBlock + { + Text = "\ue8b9", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 16.0, + Foreground = foreground, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(2.0, 0.0, 4.0, 0.0) + }); + } + stackPanel.Children.Add(new TextBlock + { + Text = attachment.FileName, + FontSize = 10.0, + Foreground = foreground, + VerticalAlignment = VerticalAlignment.Center, + MaxWidth = 100.0, + TextTrimming = TextTrimming.CharacterEllipsis + }); + ImageAttachment capturedAttachment = attachment; + Border capturedChip = border; + System.Windows.Controls.Button button = new System.Windows.Controls.Button + { + Content = new TextBlock + { + Text = "\ue711", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 8.0, + Foreground = foreground + }, + Background = System.Windows.Media.Brushes.Transparent, + BorderThickness = new Thickness(0.0), + Cursor = System.Windows.Input.Cursors.Hand, + Padding = new Thickness(4.0, 2.0, 4.0, 2.0), + Margin = new Thickness(2.0, 0.0, 0.0, 0.0) + }; + button.Click += delegate + { + _pendingImages.Remove(capturedAttachment); + AttachedFilesPanel.Items.Remove(capturedChip); + if (_pendingImages.Count == 0 && _attachedFiles.Count == 0) + { + AttachedFilesPanel.Visibility = Visibility.Collapsed; + } + }; + stackPanel.Children.Add(button); + border.Child = stackPanel; + AttachedFilesPanel.Items.Add(border); + AttachedFilesPanel.Visibility = Visibility.Visible; + } + + private void InputBox_TextChanged(object sender, TextChangedEventArgs e) + { + UpdateWatermarkVisibility(); + UpdateDraftPreviewCard(); + string text = InputBox.Text; + if (_activeSlashCmd != null && text.StartsWith("/")) + { + HideSlashChip(); + } + if (text.StartsWith("/") && !text.Contains(' ')) + { + string activeTab = _activeTab; + bool flag = ((activeTab == "Cowork" || activeTab == "Code") ? true : false); + bool isDev = flag; + List<(string, string, bool)> list = (from kv in SlashCommands + where kv.Key.StartsWith(text, StringComparison.OrdinalIgnoreCase) + where kv.Value.Tab == "all" || (isDev && kv.Value.Tab == "dev") + select (Cmd: kv.Key, Label: kv.Value.Label, IsSkill: false)).ToList(); + if (_settings.Settings.Llm.EnableSkillSystem) + { + IEnumerable<(string, string, bool, bool)> enumerable = from s in SkillService.MatchSlashCommand(text) + where s.IsVisibleInTab(_activeTab) + select (Cmd: "/" + s.Name, Label: s.IsAvailable ? s.Label : (s.Label + " " + s.UnavailableHint), IsSkill: true, Available: s.IsAvailable); + foreach (var item in enumerable) + { + list.Add((item.Item1, item.Item2, item.Item3)); + } + } + if (list.Count > 0) + { + List favorites = _settings.Settings.Llm.FavoriteSlashCommands; + if (favorites.Count > 0) + { + list = list.OrderByDescending<(string, string, bool), bool>(((string Cmd, string Label, bool IsSkill) m) => favorites.Contains(m.Cmd, StringComparer.OrdinalIgnoreCase)).ToList(); + } + _slashAllMatches = list; + _slashPageOffset = 0; + _slashSelectedIndex = -1; + RenderSlashPage(); + SlashPopup.IsOpen = true; + return; + } + } + SlashPopup.IsOpen = false; + } + + private void UpdateDraftPreviewCard() + { + if (DraftPreviewCard != null && DraftPreviewText != null && InputBox != null) + { + string text = InputBox.Text?.Trim() ?? ""; + string activeSlashCmd = _activeSlashCmd; + string text2 = ((activeSlashCmd != null && activeSlashCmd.Length > 0) ? (_activeSlashCmd + " ") : ""); + string text3 = (text2 + text).Trim(); + if (string.IsNullOrWhiteSpace(text3)) + { + DraftPreviewCard.Visibility = Visibility.Collapsed; + DraftPreviewText.Text = ""; + RebuildDraftQueuePanel(); + } + else + { + DraftPreviewCard.Visibility = Visibility.Visible; + DraftPreviewText.Text = ((text3.Length > 140) ? (text3.Substring(0, 140) + "...") : text3); + RebuildDraftQueuePanel(); + } + } + } + + private string GetPreparedDraftText() + { + string text = InputBox?.Text?.Trim() ?? ""; + return (_activeSlashCmd != null) ? (_activeSlashCmd + " " + text).Trim() : text; + } + + private void RebuildDraftQueuePanel() + { + if (DraftQueuePanel == null) + { + return; + } + DraftQueuePanel.Children.Clear(); + DraftQueuePanel.Visibility = ((_draftQueue.Count <= 0) ? Visibility.Collapsed : Visibility.Visible); + if (_draftQueue.Count == 0) + { + return; + } + DraftQueuePanel.Children.Add(new TextBlock + { + Text = $"다음 작업 대기열 {_draftQueue.Count}", + FontSize = 10.5, + FontWeight = FontWeights.SemiBold, + Foreground = ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + Margin = new Thickness(4.0, 0.0, 0.0, 6.0) + }); + for (int i = 0; i < _draftQueue.Count; i++) + { + int index = i; + string text = _draftQueue[i]; + Border border = new Border(); + border.Background = (TryFindResource("ItemBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + border.BorderBrush = (TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.LightGray; + border.BorderThickness = new Thickness(1.0); + border.CornerRadius = new CornerRadius(16.0); + border.Padding = new Thickness(12.0, 9.0, 12.0, 9.0); + border.Margin = new Thickness(0.0, 0.0, 0.0, 6.0); + Border border2 = border; + Grid grid = new Grid(); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = new GridLength(1.0, GridUnitType.Star) + }); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = GridLength.Auto + }); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = GridLength.Auto + }); + StackPanel stackPanel = new StackPanel(); + stackPanel.Children.Add(new TextBlock + { + Text = ((index == 0) ? "다음 순서" : $"대기 {index + 1}"), + FontSize = 10.0, + Foreground = ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray) + }); + stackPanel.Children.Add(new TextBlock + { + Text = ((text.Length > 160) ? (text.Substring(0, 160) + "...") : text), + FontSize = 12.5, + Foreground = ((TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Black), + Margin = new Thickness(0.0, 4.0, 0.0, 0.0), + TextTrimming = TextTrimming.CharacterEllipsis + }); + Grid.SetColumn(stackPanel, 0); + grid.Children.Add(stackPanel); + System.Windows.Controls.Button button = new System.Windows.Controls.Button(); + button.Style = TryFindResource("GhostBtn") as Style; + button.Padding = new Thickness(8.0, 4.0, 8.0, 4.0); + button.Margin = new Thickness(8.0, 0.0, 0.0, 0.0); + button.ToolTip = "지금 바로 실행"; + button.Content = new TextBlock + { + Text = "\ue768", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 10.0, + Foreground = ((TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.CornflowerBlue) + }; + System.Windows.Controls.Button button2 = button; + button2.Click += async delegate + { + if (index >= 0 && index < _draftQueue.Count && !_isStreaming) + { + string next = _draftQueue[index]; + _draftQueue.RemoveAt(index); + RebuildDraftQueuePanel(); + InputBox.Text = next; + InputBox.CaretIndex = InputBox.Text.Length; + UpdateDraftPreviewCard(); + await SendMessageAsync(); + } + }; + Grid.SetColumn(button2, 1); + grid.Children.Add(button2); + button = new System.Windows.Controls.Button(); + button.Style = TryFindResource("GhostBtn") as Style; + button.Padding = new Thickness(6.0, 4.0, 6.0, 4.0); + button.Margin = new Thickness(8.0, 0.0, 0.0, 0.0); + button.ToolTip = "대기열에서 제거"; + button.Content = new TextBlock + { + Text = "\ue711", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 10.0, + Foreground = ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray) + }; + System.Windows.Controls.Button button3 = button; + button3.Click += delegate + { + if (index >= 0 && index < _draftQueue.Count) + { + _draftQueue.RemoveAt(index); + RebuildDraftQueuePanel(); + } + }; + Grid.SetColumn(button3, 2); + grid.Children.Add(button3); + border2.Child = grid; + DraftQueuePanel.Children.Add(border2); + } + } + + private void BtnDraftEnqueue_Click(object sender, RoutedEventArgs e) + { + string preparedDraftText = GetPreparedDraftText(); + if (!string.IsNullOrWhiteSpace(preparedDraftText)) + { + _draftQueue.Add(preparedDraftText); + if (_activeSlashCmd != null) + { + HideSlashChip(); + } + InputBox.Clear(); + RebuildDraftQueuePanel(); + UpdateDraftPreviewCard(); + InputBox.Focus(); + } + } + + private void StartNextQueuedDraftIfAny() + { + if (_autoContinuingQueuedDraft || _isStreaming || _draftQueue.Count == 0) + { + return; + } + string next = _draftQueue[0]; + _draftQueue.RemoveAt(0); + _autoContinuingQueuedDraft = true; + RebuildDraftQueuePanel(); + ((DispatcherObject)this).Dispatcher.BeginInvoke((Delegate)(Func)async delegate + { + try + { + InputBox.Text = next; + InputBox.CaretIndex = InputBox.Text.Length; + UpdateDraftPreviewCard(); + await SendMessageAsync(); + } + finally + { + _autoContinuingQueuedDraft = false; + } + }, (DispatcherPriority)4, Array.Empty()); + } + + private void BtnDraftEdit_Click(object sender, RoutedEventArgs e) + { + InputBox.Focus(); + InputBox.CaretIndex = InputBox.Text?.Length ?? 0; + } + + private void BtnDraftClear_Click(object sender, RoutedEventArgs e) + { + if (_activeSlashCmd != null) + { + HideSlashChip(); + } + InputBox.Clear(); + UpdateDraftPreviewCard(); + InputBox.Focus(); + } + + private void RenderSlashPage() + { + SlashItems.Items.Clear(); + int count = _slashAllMatches.Count; + int slashPageOffset = _slashPageOffset; + int num = Math.Min(slashPageOffset + SlashPageSize, count); + int value = _slashAllMatches.Count<(string, string, bool)>(((string Cmd, string Label, bool IsSkill) x) => !x.IsSkill); + int num2 = _slashAllMatches.Count<(string, string, bool)>(((string Cmd, string Label, bool IsSkill) x) => x.IsSkill); + if (SlashPopupTitle != null) + { + SlashPopupTitle.Text = ((num2 > 0) ? "명령어와 스킬" : "빠른 명령어"); + } + if (SlashPopupHint != null) + { + SlashPopupHint.Text = ((num2 > 0) ? $"명령 {value}개 · 스킬 {num2}개" : $"명령 {value}개"); + } + if (slashPageOffset > 0) + { + SlashNavUp.Visibility = Visibility.Visible; + SlashNavUpText.Text = $"▲ 위로 {slashPageOffset}개"; + } + else + { + SlashNavUp.Visibility = Visibility.Collapsed; + } + bool? flag = null; + for (int num3 = slashPageOffset; num3 < num; num3++) + { + (string Cmd, string Label, bool IsSkill) tuple = _slashAllMatches[num3]; + string item = tuple.Cmd; + string item2 = tuple.Label; + bool item3 = tuple.IsSkill; + string capturedCmd = item; + SkillDefinition skillDefinition = (item3 ? SkillService.Find(item.TrimStart('/')) : null); + bool flag2 = skillDefinition?.IsAvailable ?? true; + bool flag3 = _settings.Settings.Llm.FavoriteSlashCommands.Contains(item, StringComparer.OrdinalIgnoreCase); + int num4 = num3 - slashPageOffset; + bool flag4 = num4 == _slashSelectedIndex; + System.Windows.Media.Brush brush = (TryFindResource("ItemHoverBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.LightGray; + string toolTip = ((item3 && skillDefinition != null && !string.IsNullOrWhiteSpace(skillDefinition.Description)) ? skillDefinition.Description : item2); + if (flag != item3) + { + flag = item3; + SlashItems.Items.Add(new TextBlock + { + Text = (item3 ? "스킬" : "명령어"), + FontSize = 10.5, + FontWeight = FontWeights.SemiBold, + Foreground = ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + Margin = new Thickness(8.0, (num3 == slashPageOffset) ? 2 : 10, 8.0, 6.0) + }); + } + Border item4 = new Border + { + Background = (flag4 ? brush : System.Windows.Media.Brushes.Transparent), + BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromArgb(18, 17, 24, 39)), + BorderThickness = new Thickness(1.0), + CornerRadius = new CornerRadius(12.0), + Padding = new Thickness(12.0, 10.0, 12.0, 10.0), + Margin = new Thickness(4.0, 2.0, 4.0, 2.0), + Cursor = (flag2 ? System.Windows.Input.Cursors.Hand : System.Windows.Input.Cursors.Arrow), + Opacity = (flag2 ? 1.0 : 0.5), + ToolTip = toolTip + }; + Grid grid = new Grid(); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = GridLength.Auto + }); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = new GridLength(1.0, GridUnitType.Star) + }); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = GridLength.Auto + }); + Border element = new Border + { + Width = 28.0, + Height = 28.0, + CornerRadius = new CornerRadius(8.0), + Background = (item3 ? new SolidColorBrush(System.Windows.Media.Color.FromArgb(22, 37, 99, 235)) : new SolidColorBrush(System.Windows.Media.Color.FromArgb(18, 107, 114, 128))), + Margin = new Thickness(0.0, 0.0, 10.0, 0.0), + Child = new TextBlock + { + Text = ((!item3) ? "\ue8a7" : (skillDefinition?.Icon ?? "\ue768")), + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 11.0, + Foreground = (item3 ? new SolidColorBrush(System.Windows.Media.Color.FromRgb(37, 99, 235)) : new SolidColorBrush(System.Windows.Media.Color.FromRgb(107, 114, 128))), + HorizontalAlignment = System.Windows.HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + Grid.SetColumn(element, 0); + grid.Children.Add(element); + StackPanel stackPanel = new StackPanel(); + if (flag3) + { + stackPanel.Children.Add(new TextBlock + { + Text = "\ue735 ", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 10.0, + Foreground = new SolidColorBrush(System.Windows.Media.Color.FromRgb(245, 158, 11)), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 2.0, 0.0) + }); + } + stackPanel.Children.Add(new TextBlock + { + Text = item, + FontSize = 13.0, + FontWeight = FontWeights.SemiBold, + Foreground = (flag2 ? ((TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Blue) : ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray)), + VerticalAlignment = VerticalAlignment.Center + }); + stackPanel.Children.Add(new TextBlock + { + Text = " — " + item2, + FontSize = 11.5, + Foreground = ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 4.0, 0.0, 0.0), + TextWrapping = TextWrapping.Wrap + }); + Grid.SetColumn(stackPanel, 1); + grid.Children.Add(stackPanel); + string favCapturedCmd = item; + Border border = new Border(); + border.Width = 24.0; + border.Height = 24.0; + border.CornerRadius = new CornerRadius(4.0); + border.Background = System.Windows.Media.Brushes.Transparent; + border.Cursor = System.Windows.Input.Cursors.Hand; + border.VerticalAlignment = VerticalAlignment.Center; + border.Child = new TextBlock + { + Text = (flag3 ? "\ue735" : "\ue734"), + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 11.0, + Foreground = (flag3 ? new SolidColorBrush(System.Windows.Media.Color.FromRgb(245, 158, 11)) : ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray)), + HorizontalAlignment = System.Windows.HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Opacity = (flag3 ? 1.0 : 0.4) + }; + Border border2 = border; + border2.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + ((Border)s).Opacity = 0.8; + }; + border2.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + ((Border)s).Opacity = 1.0; + }; + border2.MouseLeftButtonDown += delegate(object _, MouseButtonEventArgs me) + { + me.Handled = true; + ToggleSlashFavorite(favCapturedCmd); + }; + Grid.SetColumn(border2, 2); + grid.Children.Add(border2); + item4.Child = grid; + if (flag2) + { + System.Windows.Media.Brush hoverBrush = (TryFindResource("ItemHoverBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.LightGray; + item4.MouseEnter += delegate + { + item4.Background = hoverBrush; + }; + item4.MouseLeave += delegate + { + item4.Background = System.Windows.Media.Brushes.Transparent; + }; + item4.MouseLeftButtonDown += delegate + { + SlashPopup.IsOpen = false; + if (capturedCmd.Equals("/help", StringComparison.OrdinalIgnoreCase)) + { + InputBox.Text = ""; + ShowSlashHelpWindow(); + } + else + { + ShowSlashChip(capturedCmd); + InputBox.Focus(); + } + }; + } + SlashItems.Items.Add(item4); + } + if (num < count) + { + SlashNavDown.Visibility = Visibility.Visible; + SlashNavDownText.Text = $"▼ 아래로 {count - num}개"; + } + else + { + SlashNavDown.Visibility = Visibility.Collapsed; + } + } + + private void SlashPopup_PreviewMouseWheel(object sender, MouseWheelEventArgs e) + { + e.Handled = true; + SlashPopup_ScrollByDelta(e.Delta); + } + + private void SlashPopup_ScrollByDelta(int delta) + { + if (_slashAllMatches.Count == 0) + { + return; + } + int num = Math.Min(SlashPageSize, _slashAllMatches.Count - _slashPageOffset); + if (delta > 0) + { + if (_slashSelectedIndex > 0) + { + _slashSelectedIndex--; + } + else if (_slashSelectedIndex == 0 && _slashPageOffset > 0) + { + _slashPageOffset = Math.Max(0, _slashPageOffset - 1); + _slashSelectedIndex = 0; + } + } + else if (_slashSelectedIndex < 0) + { + _slashSelectedIndex = 0; + } + else if (_slashSelectedIndex < num - 1) + { + _slashSelectedIndex++; + } + else if (_slashPageOffset + SlashPageSize < _slashAllMatches.Count) + { + _slashPageOffset++; + _slashSelectedIndex = Math.Min(SlashPageSize - 1, _slashAllMatches.Count - _slashPageOffset - 1); + } + RenderSlashPage(); + } + + private void ExecuteSlashSelectedItem() + { + int num = _slashPageOffset + _slashSelectedIndex; + if (num < 0 || num >= _slashAllMatches.Count) + { + return; + } + (string Cmd, string Label, bool IsSkill) tuple = _slashAllMatches[num]; + string item = tuple.Cmd; + SkillDefinition skillDefinition = (tuple.IsSkill ? SkillService.Find(item.TrimStart('/')) : null); + if (skillDefinition == null || skillDefinition.IsAvailable) + { + SlashPopup.IsOpen = false; + _slashSelectedIndex = -1; + if (item.Equals("/help", StringComparison.OrdinalIgnoreCase)) + { + InputBox.Text = ""; + ShowSlashHelpWindow(); + } + else + { + ShowSlashChip(item); + InputBox.Focus(); + } + } + } + + private void ToggleSlashFavorite(string cmd) + { + List favoriteSlashCommands = _settings.Settings.Llm.FavoriteSlashCommands; + string text = favoriteSlashCommands.FirstOrDefault((string f) => f.Equals(cmd, StringComparison.OrdinalIgnoreCase)); + if (text != null) + { + favoriteSlashCommands.Remove(text); + } + else + { + favoriteSlashCommands.Add(cmd); + } + _settings.Save(); + string text2 = InputBox.Text; + InputBox.TextChanged -= InputBox_TextChanged; + InputBox.Text = ""; + InputBox.TextChanged += InputBox_TextChanged; + InputBox.Text = text2; + } + + private void ShowSlashChip(string cmd) + { + _activeSlashCmd = cmd; + SlashChipText.Text = cmd; + SlashCommandChip.Visibility = Visibility.Visible; + SlashCommandChip.UpdateLayout(); + double left = SlashCommandChip.Margin.Left + SlashCommandChip.ActualWidth + 6.0; + InputBox.Padding = new Thickness(left, 10.0, 14.0, 10.0); + InputBox.Text = ""; + UpdateDraftPreviewCard(); + } + + private void HideSlashChip(bool restoreText = false) + { + if (_activeSlashCmd != null) + { + string activeSlashCmd = _activeSlashCmd; + _activeSlashCmd = null; + SlashCommandChip.Visibility = Visibility.Collapsed; + InputBox.Padding = new Thickness(14.0, 10.0, 14.0, 10.0); + if (restoreText) + { + InputBox.Text = activeSlashCmd + " "; + InputBox.CaretIndex = InputBox.Text.Length; + } + UpdateDraftPreviewCard(); + } + } + + private static (string? slashSystem, string userText) ParseSlashCommand(string input) + { + foreach (KeyValuePair slashCommand in SlashCommands) + { + slashCommand.Deconstruct(out var key, out var value); + (string, string, string) tuple = value; + string text = key; + string item = tuple.Item2; + if (input.StartsWith(text, StringComparison.OrdinalIgnoreCase)) + { + if (item == "__HELP__") + { + return (slashSystem: null, userText: input); + } + key = input; + int length = text.Length; + string text2 = key.Substring(length, key.Length - length).Trim(); + return (slashSystem: item, userText: string.IsNullOrEmpty(text2) ? text : text2); + } + } + foreach (SkillDefinition skill in SkillService.Skills) + { + string text3 = "/" + skill.Name; + if (input.StartsWith(text3, StringComparison.OrdinalIgnoreCase)) + { + string key = input; + int length = text3.Length; + string text4 = key.Substring(length, key.Length - length).Trim(); + return (slashSystem: skill.SystemPrompt, userText: string.IsNullOrEmpty(text4) ? skill.Label : text4); + } + } + return (slashSystem: null, userText: input); + } + + private void ShowDropActionMenu(string[] files) + { + string item = System.IO.Path.GetExtension(files[0]).ToLowerInvariant(); + List<(string, string, string)> list = CollectionExtensions.GetValueOrDefault(key: CodeExtensions.Contains(item) ? "code" : (DataExtensions.Contains(item) ? "data" : ((!ImageExtensions.Contains(item)) ? "document" : "image")), dictionary: DropActions) ?? DropActions["document"]; + Popup? dropActionPopup = _dropActionPopup; + if (dropActionPopup != null) + { + ((DependencyObject)dropActionPopup).SetValue(Popup.IsOpenProperty, (object)false); + } + StackPanel stackPanel = new StackPanel(); + TextBlock textBlock = new TextBlock(); + textBlock.Text = "\ud83d\udcce " + System.IO.Path.GetFileName(files[0]) + ((files.Length > 1) ? $" 외 {files.Length - 1}개" : ""); + textBlock.FontSize = 11.0; + textBlock.Foreground = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + textBlock.Margin = new Thickness(12.0, 8.0, 12.0, 6.0); + TextBlock element = textBlock; + stackPanel.Children.Add(element); + System.Windows.Media.Brush hoverBrush = (TryFindResource("ItemHoverBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromArgb(24, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + System.Windows.Media.Brush foreground = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Blue; + System.Windows.Media.Brush foreground2 = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + foreach (var item5 in list) + { + string item2 = item5.Item1; + string item3 = item5.Item2; + string item4 = item5.Item3; + string capturedPrompt = item4; + Border row = new Border + { + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(6.0), + Padding = new Thickness(12.0, 7.0, 12.0, 7.0), + Margin = new Thickness(4.0, 1.0, 4.0, 1.0), + Cursor = System.Windows.Input.Cursors.Hand + }; + StackPanel stackPanel2 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel2.Children.Add(new TextBlock + { + Text = item3, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 13.0, + Foreground = foreground, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 8.0, 0.0) + }); + stackPanel2.Children.Add(new TextBlock + { + Text = item2, + FontSize = 13.0, + FontWeight = FontWeights.SemiBold, + Foreground = foreground2, + VerticalAlignment = VerticalAlignment.Center + }); + row.Child = stackPanel2; + row.MouseEnter += delegate + { + row.Background = hoverBrush; + }; + row.MouseLeave += delegate + { + row.Background = System.Windows.Media.Brushes.Transparent; + }; + row.MouseLeftButtonUp += delegate + { + if (_dropActionPopup != null) + { + _dropActionPopup.IsOpen = false; + } + string[] array = files; + foreach (string filePath in array) + { + AddAttachedFile(filePath); + } + InputBox.Text = capturedPrompt; + InputBox.CaretIndex = InputBox.Text.Length; + InputBox.Focus(); + if (_settings.Settings.Llm.DragDropAutoSend) + { + SendMessageAsync(); + } + }; + stackPanel.Children.Add(row); + } + Border attachOnly = new Border + { + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(6.0), + Padding = new Thickness(12.0, 7.0, 12.0, 7.0), + Margin = new Thickness(4.0, 1.0, 4.0, 1.0), + Cursor = System.Windows.Input.Cursors.Hand + }; + StackPanel stackPanel3 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel3.Children.Add(new TextBlock + { + Text = "\ue723", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 13.0, + Foreground = ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 8.0, 0.0) + }); + stackPanel3.Children.Add(new TextBlock + { + Text = "첨부만", + FontSize = 13.0, + Foreground = ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + VerticalAlignment = VerticalAlignment.Center + }); + attachOnly.Child = stackPanel3; + attachOnly.MouseEnter += delegate + { + attachOnly.Background = hoverBrush; + }; + attachOnly.MouseLeave += delegate + { + attachOnly.Background = System.Windows.Media.Brushes.Transparent; + }; + attachOnly.MouseLeftButtonUp += delegate + { + if (_dropActionPopup != null) + { + _dropActionPopup.IsOpen = false; + } + string[] array = files; + foreach (string filePath in array) + { + AddAttachedFile(filePath); + } + InputBox.Focus(); + }; + stackPanel.Children.Add(attachOnly); + Border border = new Border(); + border.Background = (TryFindResource("LauncherBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromRgb(26, 27, 46)); + border.CornerRadius = new CornerRadius(12.0); + border.BorderBrush = (TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + border.BorderThickness = new Thickness(1.0); + border.Padding = new Thickness(4.0, 4.0, 4.0, 6.0); + border.Child = stackPanel; + border.MinWidth = 200.0; + border.Effect = new DropShadowEffect + { + Color = Colors.Black, + BlurRadius = 16.0, + ShadowDepth = 4.0, + Opacity = 0.3 + }; + Border child = border; + _dropActionPopup = new Popup + { + PlacementTarget = InputBorder, + Placement = PlacementMode.Top, + StaysOpen = false, + AllowsTransparency = true, + PopupAnimation = PopupAnimation.Fade, + Child = child + }; + _dropActionPopup.IsOpen = true; + } + + private void ShowSlashHelpWindow() + { + //IL_02ec: Unknown result type (might be due to invalid IL or missing references) + //IL_0303: Unknown result type (might be due to invalid IL or missing references) + System.Windows.Media.Brush background = (TryFindResource("LauncherBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromRgb(26, 27, 46)); + System.Windows.Media.Brush brush = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush brush2 = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush accent = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Blue; + System.Windows.Media.Brush itemBg = (TryFindResource("ItemBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromArgb(30, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + System.Windows.Media.Brush hoverBg = (TryFindResource("ItemHoverBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromArgb(40, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + Window win = new Window + { + Title = "AX Agent — 슬래시 명령어 도움말", + Width = 560.0, + Height = 640.0, + MinWidth = 440.0, + MinHeight = 500.0, + WindowStyle = WindowStyle.None, + AllowsTransparency = true, + Background = System.Windows.Media.Brushes.Transparent, + ResizeMode = ResizeMode.CanResize, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + Owner = this, + Icon = base.Icon + }; + Border border = new Border(); + border.Background = background; + border.CornerRadius = new CornerRadius(16.0); + border.BorderBrush = (TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + border.BorderThickness = new Thickness(1.0); + border.Margin = new Thickness(10.0); + border.Effect = new DropShadowEffect + { + Color = Colors.Black, + BlurRadius = 20.0, + ShadowDepth = 4.0, + Opacity = 0.3 + }; + Border border2 = border; + Grid grid = new Grid(); + grid.RowDefinitions.Add(new RowDefinition + { + Height = new GridLength(52.0) + }); + grid.RowDefinitions.Add(new RowDefinition + { + Height = new GridLength(1.0, GridUnitType.Star) + }); + Border border3 = new Border + { + CornerRadius = new CornerRadius(16.0, 16.0, 0.0, 0.0), + Background = new LinearGradientBrush(System.Windows.Media.Color.FromRgb(26, 27, 46), System.Windows.Media.Color.FromRgb(59, 78, 204), new Point(0.0, 0.0), new Point(1.0, 1.0)), + Padding = new Thickness(20.0, 0.0, 20.0, 0.0) + }; + Grid grid2 = new Grid(); + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal, + VerticalAlignment = VerticalAlignment.Center + }; + stackPanel.Children.Add(new TextBlock + { + Text = "\ue946", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 16.0, + Foreground = System.Windows.Media.Brushes.LightCyan, + Margin = new Thickness(0.0, 0.0, 10.0, 0.0), + VerticalAlignment = VerticalAlignment.Center + }); + stackPanel.Children.Add(new TextBlock + { + Text = "슬래시 명령어 (/ Commands)", + FontSize = 15.0, + FontWeight = FontWeights.SemiBold, + Foreground = System.Windows.Media.Brushes.White, + VerticalAlignment = VerticalAlignment.Center + }); + grid2.Children.Add(stackPanel); + Border closeBtn = new Border + { + Width = 30.0, + Height = 30.0, + CornerRadius = new CornerRadius(8.0), + Background = System.Windows.Media.Brushes.Transparent, + Cursor = System.Windows.Input.Cursors.Hand, + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, + VerticalAlignment = VerticalAlignment.Center + }; + closeBtn.Child = new TextBlock + { + Text = "\ue711", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 11.0, + Foreground = new SolidColorBrush(System.Windows.Media.Color.FromArgb(136, 170, byte.MaxValue, 204)), + HorizontalAlignment = System.Windows.HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + closeBtn.MouseEnter += delegate + { + closeBtn.Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(34, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + }; + closeBtn.MouseLeave += delegate + { + closeBtn.Background = System.Windows.Media.Brushes.Transparent; + }; + closeBtn.MouseLeftButtonDown += delegate(object _, MouseButtonEventArgs me) + { + me.Handled = true; + win.Close(); + }; + grid2.Children.Add(closeBtn); + border3.Child = grid2; + Grid.SetRow(border3, 0); + grid.Children.Add(border3); + ScrollViewer scrollViewer = new ScrollViewer + { + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + Padding = new Thickness(20.0, 14.0, 20.0, 20.0) + }; + StackPanel stackPanel2 = new StackPanel(); + stackPanel2.Children.Add(new TextBlock + { + Text = "입력창에 /를 입력하면 사용할 수 있는 명령어가 표시됩니다.\n명령어를 선택한 후 내용을 입력하면 해당 기능이 적용됩니다.", + FontSize = 12.0, + Foreground = brush2, + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0.0, 0.0, 0.0, 16.0), + LineHeight = 20.0 + }); + AddHelpSection(stackPanel2, "\ud83d\udccc 공통 명령어", "모든 탭(Chat, Cowork, Code)에서 사용 가능", brush, brush2, accent, itemBg, hoverBg, ("/summary", "텍스트/문서를 핵심 포인트 중심으로 요약합니다."), ("/translate", "텍스트를 영어로 번역합니다. 원문의 톤을 유지합니다."), ("/explain", "내용을 쉽고 자세하게 설명합니다. 예시를 포함합니다."), ("/fix", "맞춤법, 문법, 자연스러운 표현을 교정합니다.")); + AddHelpSection(stackPanel2, "\ud83d\udee0\ufe0f 개발 명령어", "Cowork, Code 탭에서만 사용 가능", brush, brush2, accent, itemBg, hoverBg, ("/review", "Git diff를 분석하여 버그, 성능, 보안 이슈를 찾습니다."), ("/pr", "변경사항을 PR 설명 형식(Summary, Changes, Test Plan)으로 요약합니다."), ("/test", "코드에 대한 단위 테스트를 자동 생성합니다."), ("/structure", "프로젝트의 폴더/파일 구조를 분석하고 설명합니다."), ("/build", "프로젝트를 빌드합니다. 오류 발생 시 분석합니다."), ("/search", "자연어로 코드베이스를 시맨틱 검색합니다.")); + IReadOnlyList skills = SkillService.Skills; + if (skills.Count > 0) + { + (string, string)[] items = skills.Select((SkillDefinition s) => ("/" + s.Name, Description: s.Description)).ToArray(); + AddHelpSection(stackPanel2, "⚡ 스킬 명령어", $"{skills.Count}개 로드됨 — %APPDATA%\\AxCopilot\\skills\\에서 추가 가능", brush, brush2, accent, itemBg, hoverBg, items); + } + stackPanel2.Children.Add(new Border + { + Height = 1.0, + Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(30, byte.MaxValue, byte.MaxValue, byte.MaxValue)), + Margin = new Thickness(0.0, 12.0, 0.0, 12.0) + }); + StackPanel stackPanel3 = new StackPanel(); + stackPanel3.Children.Add(new TextBlock + { + Text = "\ud83d\udca1 사용 팁", + FontSize = 13.0, + FontWeight = FontWeights.SemiBold, + Foreground = brush, + Margin = new Thickness(0.0, 0.0, 0.0, 8.0) + }); + string[] array = new string[4] { "/ 입력 시 현재 탭에 맞는 명령어만 자동완성됩니다.", "파일을 드래그하면 유형별 AI 액션 팝업이 나타납니다.", "스킬 파일(*.skill.md)을 추가하면 나만의 워크플로우를 만들 수 있습니다.", "Cowork/Code 탭에서 에이전트가 도구를 활용하여 더 강력한 작업을 수행합니다." }; + string[] array2 = array; + foreach (string text in array2) + { + stackPanel3.Children.Add(new TextBlock + { + Text = "• " + text, + FontSize = 12.0, + Foreground = brush2, + Margin = new Thickness(8.0, 2.0, 0.0, 2.0), + TextWrapping = TextWrapping.Wrap, + LineHeight = 18.0 + }); + } + stackPanel2.Children.Add(stackPanel3); + scrollViewer.Content = stackPanel2; + Grid.SetRow(scrollViewer, 1); + grid.Children.Add(scrollViewer); + border2.Child = grid; + win.Content = border2; + border3.MouseLeftButtonDown += delegate + { + try + { + win.DragMove(); + } + catch + { + } + }; + win.ShowDialog(); + } + + private static void AddHelpSection(StackPanel parent, string title, string subtitle, System.Windows.Media.Brush fg, System.Windows.Media.Brush fg2, System.Windows.Media.Brush accent, System.Windows.Media.Brush itemBg, System.Windows.Media.Brush hoverBg, params (string Cmd, string Desc)[] items) + { + parent.Children.Add(new TextBlock + { + Text = title, + FontSize = 14.0, + FontWeight = FontWeights.SemiBold, + Foreground = fg, + Margin = new Thickness(0.0, 8.0, 0.0, 2.0) + }); + parent.Children.Add(new TextBlock + { + Text = subtitle, + FontSize = 11.0, + Foreground = fg2, + Margin = new Thickness(0.0, 0.0, 0.0, 8.0) + }); + for (int i = 0; i < items.Length; i++) + { + (string Cmd, string Desc) tuple = items[i]; + string item = tuple.Cmd; + string item2 = tuple.Desc; + Border row = new Border + { + Background = itemBg, + CornerRadius = new CornerRadius(8.0), + Padding = new Thickness(12.0, 8.0, 12.0, 8.0), + Margin = new Thickness(0.0, 3.0, 0.0, 3.0) + }; + Grid grid = new Grid(); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = new GridLength(120.0) + }); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = new GridLength(1.0, GridUnitType.Star) + }); + TextBlock element = new TextBlock + { + Text = item, + FontSize = 13.0, + FontWeight = FontWeights.SemiBold, + Foreground = accent, + VerticalAlignment = VerticalAlignment.Center, + FontFamily = new System.Windows.Media.FontFamily("Consolas") + }; + Grid.SetColumn(element, 0); + grid.Children.Add(element); + TextBlock element2 = new TextBlock + { + Text = item2, + FontSize = 12.0, + Foreground = fg2, + VerticalAlignment = VerticalAlignment.Center, + TextWrapping = TextWrapping.Wrap + }; + Grid.SetColumn(element2, 1); + grid.Children.Add(element2); + row.Child = grid; + row.MouseEnter += delegate + { + row.Background = hoverBg; + }; + row.MouseLeave += delegate + { + row.Background = itemBg; + }; + parent.Children.Add(row); + } + } + + private async Task SendMessageAsync() + { + string rawText = InputBox.Text.Trim(); + string text = ((_activeSlashCmd != null) ? (_activeSlashCmd + " " + rawText).Trim() : rawText); + HideSlashChip(); + if (string.IsNullOrEmpty(text) || _isStreaming) + { + return; + } + ClearPromptCardPlaceholder(); + (string, string) tuple = ParseSlashCommand(text); + var (slashSystem, _) = tuple; + _ = tuple.Item2; + string originTab = _activeTab; + ChatConversation conv; + lock (_convLock) + { + if (_currentConversation == null) + { + _currentConversation = new ChatConversation + { + Tab = _activeTab + }; + } + conv = _currentConversation; + } + ChatMessage userMsg = new ChatMessage + { + Role = "user", + Content = text + }; + lock (_convLock) + { + conv.Messages.Add(userMsg); + } + if (conv.Messages.Count((ChatMessage m) => m.Role == "user") == 1) + { + conv.Title = ((text.Length > 30) ? (text.Substring(0, 30) + "…") : text); + } + UpdateChatTitle(); + AddMessageBubble("user", text); + InputBox.Text = ""; + EmptyState.Visibility = Visibility.Collapsed; + UsageStatisticsService.RecordChat(_activeTab); + ForceScrollToEnd(); + PlayRainbowGlow(); + _isStreaming = true; + BtnSend.IsEnabled = false; + BtnSend.Visibility = Visibility.Collapsed; + BtnStop.Visibility = Visibility.Visible; + if (_activeTab == "Cowork" || _activeTab == "Code") + { + BtnPause.Visibility = Visibility.Visible; + } + _streamCts = new CancellationTokenSource(); + ChatMessage assistantMsg = new ChatMessage + { + Role = "assistant", + Content = "" + }; + lock (_convLock) + { + conv.Messages.Add(assistantMsg); + } + TextBlock streamText; + StackPanel streamContainer = CreateStreamingContainer(out streamText); + MessagePanel.Children.Add(streamContainer); + ForceScrollToEnd(); + StringBuilder sb = new StringBuilder(); + _activeStreamText = streamText; + _cachedStreamContent = ""; + _displayedLength = 0; + _cursorVisible = true; + _aiIconPulseStopped = false; + _cursorTimer.Start(); + _typingTimer.Start(); + _streamStartTime = DateTime.UtcNow; + _elapsedTimer.Start(); + SetStatus("응답 생성 중...", spinning: true); + ModelRouteResult routeResult = null; + bool queueContinuationAllowed = true; + try + { + List sendMessages; + lock (_convLock) + { + sendMessages = conv.Messages.SkipLast(1).ToList(); + } + if (!string.IsNullOrEmpty(conv.SystemCommand)) + { + sendMessages.Insert(0, new ChatMessage + { + Role = "system", + Content = conv.SystemCommand + }); + } + if (!string.IsNullOrEmpty(slashSystem)) + { + sendMessages.Insert(0, new ChatMessage + { + Role = "system", + Content = slashSystem + }); + } + if (_attachedFiles.Count > 0) + { + string fileContext = BuildFileContextPrompt(); + if (!string.IsNullOrEmpty(fileContext)) + { + int lastUserIdx = sendMessages.FindLastIndex((ChatMessage m) => m.Role == "user"); + if (lastUserIdx >= 0) + { + sendMessages[lastUserIdx] = new ChatMessage + { + Role = "user", + Content = sendMessages[lastUserIdx].Content + fileContext + }; + } + } + userMsg.AttachedFiles = _attachedFiles.ToList(); + _attachedFiles.Clear(); + RefreshAttachedFilesUI(); + } + if (_pendingImages.Count > 0) + { + userMsg.Images = _pendingImages.ToList(); + int lastUserIdx2 = sendMessages.FindLastIndex((ChatMessage m) => m.Role == "user"); + if (lastUserIdx2 >= 0) + { + sendMessages[lastUserIdx2] = new ChatMessage + { + Role = "user", + Content = sendMessages[lastUserIdx2].Content, + Images = _pendingImages.ToList() + }; + } + _pendingImages.Clear(); + AttachedFilesPanel.Items.Clear(); + if (_attachedFiles.Count == 0) + { + AttachedFilesPanel.Visibility = Visibility.Collapsed; + } + } + if (_settings.Settings.Llm.EnableAutoRouter) + { + routeResult = _router.Route(text); + if (routeResult != null) + { + _llm.PushRouteOverride(routeResult.Service, routeResult.Model); + SetStatus("라우팅: " + routeResult.DetectedIntent + " → " + routeResult.DisplayName, spinning: true); + } + } + if (_activeTab == "Cowork") + { + OpenWorkflowAnalyzerIfEnabled(); + _agentCumulativeInputTokens = 0; + _agentCumulativeOutputTokens = 0; + _agentLoop.EventOccurred += OnAgentEvent; + _agentLoop.UserDecisionCallback = CreatePlanDecisionCallback(); + try + { + string coworkSystem = BuildCoworkSystemPrompt(); + if (!string.IsNullOrEmpty(coworkSystem)) + { + sendMessages.Insert(0, new ChatMessage + { + Role = "system", + Content = coworkSystem + }); + } + _agentLoop.ActiveTab = _activeTab; + string response = await _agentLoop.RunAsync(sendMessages, _streamCts.Token); + sb.Append(response); + assistantMsg.Content = response; + StopAiIconPulse(); + _cachedStreamContent = response; + if (_settings.Settings.Llm.NotifyOnComplete) + { + NotificationService.Notify("AX Cowork Agent", "코워크 작업이 완료되었습니다."); + } + } + finally + { + _agentLoop.EventOccurred -= OnAgentEvent; + _agentLoop.UserDecisionCallback = null; + } + } + else if (_activeTab == "Code") + { + OpenWorkflowAnalyzerIfEnabled(); + _agentCumulativeInputTokens = 0; + _agentCumulativeOutputTokens = 0; + _agentLoop.EventOccurred += OnAgentEvent; + _agentLoop.UserDecisionCallback = CreatePlanDecisionCallback(); + try + { + string codeSystem = BuildCodeSystemPrompt(); + if (!string.IsNullOrEmpty(codeSystem)) + { + sendMessages.Insert(0, new ChatMessage + { + Role = "system", + Content = codeSystem + }); + } + _agentLoop.ActiveTab = "Code"; + string response2 = await _agentLoop.RunAsync(sendMessages, _streamCts.Token); + sb.Append(response2); + assistantMsg.Content = response2; + StopAiIconPulse(); + _cachedStreamContent = response2; + if (_settings.Settings.Llm.NotifyOnComplete) + { + NotificationService.Notify("AX Code Agent", "코드 작업이 완료되었습니다."); + } + } + finally + { + _agentLoop.EventOccurred -= OnAgentEvent; + _agentLoop.UserDecisionCallback = null; + } + } + else if (_settings.Settings.Llm.Streaming) + { + await foreach (string chunk in _llm.StreamAsync(sendMessages, _streamCts.Token)) + { + sb.Append(chunk); + StopAiIconPulse(); + _cachedStreamContent = sb.ToString(); + await ((DispatcherObject)this).Dispatcher.InvokeAsync((Action)delegate + { + }, (DispatcherPriority)4); + } + _cachedStreamContent = sb.ToString(); + assistantMsg.Content = _cachedStreamContent; + DateTime drainStart = DateTime.UtcNow; + while (_displayedLength < _cachedStreamContent.Length && (DateTime.UtcNow - drainStart).TotalMilliseconds < 600.0) + { + await ((DispatcherObject)this).Dispatcher.InvokeAsync((Action)delegate + { + }, (DispatcherPriority)4); + } + } + else + { + string response3 = await _llm.SendAsync(sendMessages, _streamCts.Token); + sb.Append(response3); + assistantMsg.Content = response3; + } + } + catch (OperationCanceledException) + { + if (sb.Length == 0) + { + sb.Append("(취소됨)"); + } + queueContinuationAllowed = false; + assistantMsg.Content = sb.ToString(); + } + catch (Exception ex2) + { + Exception ex3 = ex2; + string errMsg = "⚠ 오류: " + ex3.Message; + sb.Clear(); + sb.Append(errMsg); + queueContinuationAllowed = false; + assistantMsg.Content = errMsg; + AddRetryButton(); + } + finally + { + if (routeResult != null) + { + _llm.ClearRouteOverride(); + UpdateModelLabel(); + } + _cursorTimer.Stop(); + _elapsedTimer.Stop(); + _typingTimer.Stop(); + HideStickyProgress(); + StopRainbowGlow(); + _activeStreamText = null; + _elapsedLabel = null; + _cachedStreamContent = ""; + _isStreaming = false; + BtnSend.IsEnabled = true; + BtnStop.Visibility = Visibility.Collapsed; + BtnSend.Visibility = Visibility.Visible; + _streamCts?.Dispose(); + _streamCts = null; + SetStatusIdle(); + } + FinalizeStreamingContainer(streamContainer, streamText, assistantMsg.Content, assistantMsg); + AutoScrollIfNeeded(); + try + { + _storage.Save(conv); + } + catch (Exception ex4) + { + LogService.Debug("대화 저장 실패: " + ex4.Message); + } + _tabConversationId[originTab] = conv.Id; + RefreshConversationList(); + if (queueContinuationAllowed) + { + StartNextQueuedDraftIfAny(); + } + } + + private string BuildCoworkSystemPrompt() + { + string currentWorkFolder = GetCurrentWorkFolder(); + LlmSettings llm = _settings.Settings.Llm; + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.AppendLine("You are AX Copilot Agent. You can read, write, and edit files using the provided tools."); + StringBuilder stringBuilder2 = stringBuilder; + StringBuilder stringBuilder3 = stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(20, 3, stringBuilder2); + handler.AppendLiteral("Today's date: "); + handler.AppendFormatted(DateTime.Now, "yyyy년 M월 d일"); + handler.AppendLiteral(" ("); + handler.AppendFormatted(DateTime.Now, "yyyy-MM-dd"); + handler.AppendLiteral(", "); + handler.AppendFormatted(DateTime.Now, "dddd"); + handler.AppendLiteral(")."); + stringBuilder3.AppendLine(ref handler); + stringBuilder.AppendLine("Available skills: excel_create (.xlsx), docx_create (.docx), csv_create (.csv), markdown_create (.md), html_create (.html), script_create (.bat/.ps1), document_review (품질 검증), format_convert (포맷 변환)."); + stringBuilder.AppendLine("Parallel tools: spawn_agent, wait_agents. Use them for independent side research, codebase inspection, or document evidence gathering."); + stringBuilder.AppendLine("Always explain your plan step by step BEFORE executing tools. After creating files, summarize what was created."); + stringBuilder.AppendLine("IMPORTANT: When creating documents with dates, always use today's actual date above. Never use placeholder or fictional dates."); + stringBuilder.AppendLine("IMPORTANT: When asked to create a document with multiple sections (reports, proposals, analyses, etc.), you MUST:"); + stringBuilder.AppendLine(" 1. First, plan the document: decide the exact sections (headings), their order, and key points for each section based on the topic."); + stringBuilder.AppendLine(" 2. Call document_plan with sections_hint = your planned section titles (comma-separated). Example: sections_hint=\"회사 개요, 사업 현황, 재무 분석, SWOT, 전략 제언, 결론\""); + stringBuilder.AppendLine(" This ensures the document structure matches YOUR plan, not a generic template."); + stringBuilder.AppendLine(" 3. Then immediately call html_create (or docx_create/file_write) using the scaffold from document_plan."); + stringBuilder.AppendLine(" 4. Write actual detailed content for EVERY section — no skipping, no placeholders, no minimal content."); + stringBuilder.AppendLine(" 5. Do NOT call html_create directly without document_plan for multi-section documents."); + stringBuilder.AppendLine("\n## Document Quality Review"); + stringBuilder.AppendLine("After creating any document (html_create, docx_create, excel_create, etc.), you MUST perform a self-review:"); + stringBuilder.AppendLine("1. Use file_read to read the generated file and verify the content is complete"); + stringBuilder.AppendLine("2. Check for logical errors: incorrect dates, inconsistent data, missing sections, broken formatting"); + stringBuilder.AppendLine("3. Verify all requested topics/sections from the user's original request are covered"); + stringBuilder.AppendLine("4. If issues found, fix them using file_write or file_edit, then re-verify"); + stringBuilder.AppendLine("5. Report the review result to the user: what was checked and whether corrections were made"); + stringBuilder.Append(BuildSubAgentGuidanceSection(isCodeAgent: false)); + stringBuilder.AppendLine("\n## Format Conversion"); + stringBuilder.AppendLine("When the user requests format conversion (e.g., HTML→Word, Excel→CSV, Markdown→HTML):"); + stringBuilder.AppendLine("1. Use file_read or document_read to read the source file content"); + stringBuilder.AppendLine("2. Create a new file in the target format using the appropriate skill (docx_create, html_create, etc.)"); + stringBuilder.AppendLine("3. Preserve the content structure, formatting, and data as closely as possible"); + string defaultOutputFormat = llm.DefaultOutputFormat; + if (!string.IsNullOrEmpty(defaultOutputFormat) && defaultOutputFormat != "auto") + { + Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["xlsx"] = "Excel (.xlsx) using excel_create", + ["docx"] = "Word (.docx) using docx_create", + ["html"] = "HTML (.html) using html_create", + ["md"] = "Markdown (.md) using markdown_create", + ["csv"] = "CSV (.csv) using csv_create" + }; + if (dictionary.TryGetValue(defaultOutputFormat, out var value)) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder4 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(93, 1, stringBuilder2); + handler.AppendLiteral("IMPORTANT: User prefers output format: "); + handler.AppendFormatted(value); + handler.AppendLiteral(". Use this format unless the user specifies otherwise."); + stringBuilder4.AppendLine(ref handler); + } + } + if (!string.IsNullOrEmpty(_selectedMood) && _selectedMood != "modern") + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder5 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(83, 1, stringBuilder2); + handler.AppendLiteral("When creating HTML documents with html_create, use mood=\""); + handler.AppendFormatted(_selectedMood); + handler.AppendLiteral("\" for the design template."); + stringBuilder5.AppendLine(ref handler); + } + else + { + stringBuilder.AppendLine("When creating HTML documents with html_create, you can set 'mood' parameter: modern, professional, creative, minimal, elegant, dark, colorful, corporate, magazine, dashboard."); + } + if (!string.IsNullOrEmpty(currentWorkFolder)) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder6 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(21, 1, stringBuilder2); + handler.AppendLiteral("Current work folder: "); + handler.AppendFormatted(currentWorkFolder); + stringBuilder6.AppendLine(ref handler); + } + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder7 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(22, 1, stringBuilder2); + handler.AppendLiteral("File permission mode: "); + handler.AppendFormatted(llm.FilePermission); + stringBuilder7.AppendLine(ref handler); + string folderDataUsage = _folderDataUsage; + string text = folderDataUsage; + if (!(text == "active")) + { + if (text == "passive") + { + stringBuilder.AppendLine("Folder Data Usage = PASSIVE. You have 'document_read' and 'folder_map' tools. Only read folder documents when the user explicitly asks you to reference or use them."); + } + else + { + stringBuilder.AppendLine("Folder Data Usage = NONE. Do NOT read or reference documents in the work folder unless the user explicitly provides a file path."); + } + } + else + { + stringBuilder.AppendLine("IMPORTANT: Folder Data Usage = ACTIVE. You have 'document_read' and 'folder_map' tools available."); + stringBuilder.AppendLine("Before creating reports, use folder_map to scan the work folder structure. Then EVALUATE whether each document is RELEVANT to the user's current request topic. Only use document_read on files that are clearly related to the conversation subject. Do NOT read or reference files that are unrelated to the user's request, even if they exist in the folder. In your planning step, list which files you plan to read and explain WHY they are relevant."); + } + lock (_convLock) + { + if (_currentConversation != null && !string.IsNullOrEmpty(_currentConversation.SystemCommand)) + { + stringBuilder.AppendLine("\n" + _currentConversation.SystemCommand); + } + } + stringBuilder.Append(LoadProjectContext(currentWorkFolder)); + stringBuilder.Append(BuildProjectRulesSection(currentWorkFolder)); + stringBuilder.Append(BuildMemorySection(currentWorkFolder)); + stringBuilder.Append(BuildFeedbackContext()); + return stringBuilder.ToString(); + } + + private string BuildCodeSystemPrompt() + { + string currentWorkFolder = GetCurrentWorkFolder(); + LlmSettings llm = _settings.Settings.Llm; + CodeSettings code = llm.Code; + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.AppendLine("You are AX Copilot Code Agent — a senior software engineer for enterprise development."); + StringBuilder stringBuilder2 = stringBuilder; + StringBuilder stringBuilder3 = stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(18, 2, stringBuilder2); + handler.AppendLiteral("Today's date: "); + handler.AppendFormatted(DateTime.Now, "yyyy년 M월 d일"); + handler.AppendLiteral(" ("); + handler.AppendFormatted(DateTime.Now, "yyyy-MM-dd"); + handler.AppendLiteral(")."); + stringBuilder3.AppendLine(ref handler); + stringBuilder.AppendLine("Available tools: file_read, file_write, file_edit (supports replace_all), glob, grep (supports context_lines, case_sensitive), folder_map, process, dev_env_detect, build_run, git_tool."); + stringBuilder.AppendLine("Parallel tools: spawn_agent, wait_agents. Use them when side tasks can run independently."); + stringBuilder.AppendLine("IMPORTANT: When creating documents with dates, always use today's actual date above."); + stringBuilder.AppendLine("\n## Core Workflow (MANDATORY — follow this order)"); + stringBuilder.AppendLine("1. ORIENT: Run folder_map (depth=2) to understand project structure. Check .gitignore, README, config files."); + stringBuilder.AppendLine("2. BASELINE: If tests exist, run build_run action='test' FIRST to establish baseline. Record pass/fail count."); + stringBuilder.AppendLine("3. ANALYZE: Use grep (with context_lines=2) + file_read to deeply understand the code you'll modify."); + stringBuilder.AppendLine(" - Always check callers/references: grep for function/class names to find all usage points."); + stringBuilder.AppendLine(" - Read test files related to the code you're changing to understand expected behavior."); + stringBuilder.AppendLine("4. PLAN: Present your analysis + impact assessment. List ALL files that will be modified."); + stringBuilder.AppendLine(" - Explain WHY each change is needed and what could break."); + stringBuilder.AppendLine(" - Wait for user approval before proceeding."); + stringBuilder.AppendLine("5. IMPLEMENT: Apply changes using file_edit (preferred — shows diff). Use file_write only for new files."); + stringBuilder.AppendLine(" - Make the MINIMUM changes needed. Don't refactor unrelated code."); + stringBuilder.AppendLine(" - Prefer file_edit with replace_all=false for precision edits."); + stringBuilder.AppendLine("6. VERIFY: Run build_run action='build' then action='test'. Compare results with baseline."); + stringBuilder.AppendLine(" - If tests fail that passed before, fix immediately."); + stringBuilder.AppendLine(" - If build fails, analyze error output and correct."); + stringBuilder.AppendLine("7. GIT: Use git_tool to check status, create diff, and optionally commit."); + stringBuilder.AppendLine("8. REPORT: Summarize changes, test results, and any remaining concerns."); + stringBuilder.Append(BuildSubAgentGuidanceSection(isCodeAgent: true)); + stringBuilder.AppendLine("\n## Development Environment"); + stringBuilder.AppendLine("Use dev_env_detect to check installed IDEs, runtimes, and build tools before running commands."); + stringBuilder.AppendLine("IMPORTANT: Do NOT attempt to install compilers, IDEs, or build tools. Only use what is already installed."); + stringBuilder.AppendLine("\n## Package Repositories"); + if (!string.IsNullOrEmpty(code.NexusBaseUrl)) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder4 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(18, 1, stringBuilder2); + handler.AppendLiteral("Enterprise Nexus: "); + handler.AppendFormatted(code.NexusBaseUrl); + stringBuilder4.AppendLine(ref handler); + } + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder5 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(14, 1, stringBuilder2); + handler.AppendLiteral("NuGet (.NET): "); + handler.AppendFormatted(code.NugetSource); + stringBuilder5.AppendLine(ref handler); + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder6 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(21, 1, stringBuilder2); + handler.AppendLiteral("PyPI/Conda (Python): "); + handler.AppendFormatted(code.PypiSource); + stringBuilder6.AppendLine(ref handler); + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder7 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(14, 1, stringBuilder2); + handler.AppendLiteral("Maven (Java): "); + handler.AppendFormatted(code.MavenSource); + stringBuilder7.AppendLine(ref handler); + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder8 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(18, 1, stringBuilder2); + handler.AppendLiteral("npm (JavaScript): "); + handler.AppendFormatted(code.NpmSource); + stringBuilder8.AppendLine(ref handler); + stringBuilder.AppendLine("When adding dependencies, use these repository URLs."); + if (!string.IsNullOrEmpty(code.PreferredIdePath)) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder9 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(16, 1, stringBuilder2); + handler.AppendLiteral("\nPreferred IDE: "); + handler.AppendFormatted(code.PreferredIdePath); + stringBuilder9.AppendLine(ref handler); + } + if (_selectedLanguage != "auto") + { + string selectedLanguage = _selectedLanguage; + if (1 == 0) + { + } + string text = selectedLanguage switch + { + "python" => "Python", + "java" => "Java", + "csharp" => "C# (.NET)", + "cpp" => "C/C++", + "javascript" => "JavaScript/TypeScript", + _ => _selectedLanguage, + }; + if (1 == 0) + { + } + string value = text; + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder10 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(96, 1, stringBuilder2); + handler.AppendLiteral("\nIMPORTANT: User selected language: "); + handler.AppendFormatted(value); + handler.AppendLiteral(". Prioritize this language for code analysis and generation."); + stringBuilder10.AppendLine(ref handler); + } + stringBuilder.AppendLine("\n## Language Guidelines"); + stringBuilder.AppendLine("- C# (.NET): Use dotnet CLI. NuGet for packages. Follow Microsoft naming conventions."); + stringBuilder.AppendLine("- Python: Use conda/pip. Follow PEP8. Use type hints. Virtual env preferred."); + stringBuilder.AppendLine("- Java: Use Maven/Gradle. Follow Google Java Style Guide."); + stringBuilder.AppendLine("- C++: Use CMake for build. Follow C++ Core Guidelines."); + stringBuilder.AppendLine("- JavaScript/TypeScript: Use npm/yarn. Follow ESLint rules. Vue3 uses Composition API."); + stringBuilder.AppendLine("\n## Code Quality & Safety"); + stringBuilder.AppendLine("- NEVER delete or overwrite files without user confirmation."); + stringBuilder.AppendLine("- ALWAYS read a file before editing it. Don't guess contents."); + stringBuilder.AppendLine("- Prefer file_edit over file_write for existing files (shows diff)."); + stringBuilder.AppendLine("- Use grep to find ALL references before renaming/removing anything."); + stringBuilder.AppendLine("- If unsure about a change's impact, ask the user first."); + stringBuilder.AppendLine("- For large refactors, do them incrementally with build verification between steps."); + stringBuilder.AppendLine("- Use git_tool action='diff' to review your changes before committing."); + stringBuilder.AppendLine("\n## Lint & Format"); + stringBuilder.AppendLine("After code changes, check for available linters:"); + stringBuilder.AppendLine("- Python: ruff, black, flake8, pylint"); + stringBuilder.AppendLine("- JavaScript: eslint, prettier"); + stringBuilder.AppendLine("- C#: dotnet format"); + stringBuilder.AppendLine("- C++: clang-format"); + stringBuilder.AppendLine("Run the appropriate linter via process tool if detected by dev_env_detect."); + if (!string.IsNullOrEmpty(currentWorkFolder)) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder11 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(22, 1, stringBuilder2); + handler.AppendLiteral("\nCurrent work folder: "); + handler.AppendFormatted(currentWorkFolder); + stringBuilder11.AppendLine(ref handler); + } + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder12 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(22, 1, stringBuilder2); + handler.AppendLiteral("File permission mode: "); + handler.AppendFormatted(llm.FilePermission); + stringBuilder12.AppendLine(ref handler); + stringBuilder.AppendLine("\nFolder Data Usage = ACTIVE. Use folder_map and file_read to understand the codebase."); + stringBuilder.AppendLine("Analyze project structure before making changes. Read relevant files to understand context."); + lock (_convLock) + { + string text2 = _currentConversation?.SystemCommand; + if (text2 != null && text2.Length > 0) + { + stringBuilder.AppendLine("\n" + text2); + } + } + stringBuilder.Append(LoadProjectContext(currentWorkFolder)); + stringBuilder.Append(BuildProjectRulesSection(currentWorkFolder)); + stringBuilder.Append(BuildMemorySection(currentWorkFolder)); + stringBuilder.Append(BuildFeedbackContext()); + return stringBuilder.ToString(); + } + + private string BuildSubAgentGuidanceSection(bool isCodeAgent) + { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.AppendLine("\n## Sub-Agent Delegation"); + stringBuilder.AppendLine("Use spawn_agent only for bounded side tasks that do not block your immediate next step."); + stringBuilder.AppendLine("Good uses:"); + stringBuilder.AppendLine(isCodeAgent ? "- Inspect another module, trace references, review a diff, or summarize a subsystem while you continue main-line analysis." : "- Gather evidence from several files, inspect a side topic, or summarize background material while you continue the main task."); + stringBuilder.AppendLine("- Launch multiple independent read-only investigations in parallel when they answer different questions."); + stringBuilder.AppendLine("Avoid spawn_agent when:"); + stringBuilder.AppendLine("- The very next action depends on that result."); + stringBuilder.AppendLine("- The task is tiny and faster to do directly."); + stringBuilder.AppendLine("- The task requires file edits, shell commands, or user interaction."); + stringBuilder.AppendLine("Delegation rules:"); + stringBuilder.AppendLine("- Give each sub-agent a concrete task and unique id."); + stringBuilder.AppendLine("- Keep the prompt self-contained: mention target files, folders, functions, or the exact question."); + stringBuilder.AppendLine("- After launching sub-agents, continue non-overlapping work locally."); + stringBuilder.AppendLine("- Use wait_agents(ids:[...]) when you need specific results now."); + stringBuilder.AppendLine("- Use wait_agents(completed_only=true) to collect finished background results without blocking."); + stringBuilder.AppendLine("- When a sub-agent returns, integrate only the relevant findings into your final answer."); + return stringBuilder.ToString(); + } + + private string BuildProjectRulesSection(string? workFolder) + { + if (string.IsNullOrEmpty(workFolder)) + { + return ""; + } + if (!_settings.Settings.Llm.EnableProjectRules) + { + return ""; + } + try + { + List list = ProjectRulesService.LoadRules(workFolder); + if (list.Count == 0) + { + return ""; + } + string when = ((_activeTab == "Code") ? "always" : "always"); + List rules = ProjectRulesService.FilterRules(list, when); + return ProjectRulesService.FormatForSystemPrompt(rules); + } + catch + { + return ""; + } + } + + private string BuildMemorySection(string? workFolder) + { + if (!_settings.Settings.Llm.EnableAgentMemory) + { + return ""; + } + AgentMemoryService agentMemoryService = ((System.Windows.Application.Current is App app) ? app.MemoryService : null); + if (agentMemoryService == null || agentMemoryService.Count == 0) + { + return ""; + } + agentMemoryService.Load(workFolder ?? ""); + IReadOnlyList all = agentMemoryService.All; + if (all.Count == 0) + { + return ""; + } + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.AppendLine("\n## 프로젝트 메모리 (이전 대화에서 학습한 내용)"); + stringBuilder.AppendLine("아래는 이전 대화에서 학습한 규칙과 선호도입니다. 작업 시 참고하세요."); + stringBuilder.AppendLine("새로운 규칙이나 선호도를 발견하면 memory 도구의 save 액션으로 저장하세요."); + stringBuilder.AppendLine("사용자가 이전 학습 내용과 다른 지시를 하면 memory 도구의 delete 후 새로 save 하세요.\n"); + foreach (IGrouping item in from e in all + group e by e.Type) + { + string key = item.Key; + if (1 == 0) + { + } + string text = key switch + { + "rule" => "프로젝트 규칙", + "preference" => "사용자 선호", + "fact" => "프로젝트 사실", + "correction" => "이전 교정", + _ => item.Key, + }; + if (1 == 0) + { + } + string value = text; + StringBuilder stringBuilder2 = stringBuilder; + StringBuilder stringBuilder3 = stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(2, 1, stringBuilder2); + handler.AppendLiteral("["); + handler.AppendFormatted(value); + handler.AppendLiteral("]"); + stringBuilder3.AppendLine(ref handler); + foreach (MemoryEntry item2 in item.OrderByDescending((MemoryEntry e) => e.UseCount).Take(15)) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder4 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(2, 1, stringBuilder2); + handler.AppendLiteral("- "); + handler.AppendFormatted(item2.Content); + stringBuilder4.AppendLine(ref handler); + } + stringBuilder.AppendLine(); + } + return stringBuilder.ToString(); + } + + private void OpenWorkflowAnalyzerIfEnabled() + { + LlmSettings llm = _settings.Settings.Llm; + if (!llm.DevMode || !llm.WorkflowVisualizer) + { + return; + } + if (_analyzerWindow == null) + { + _analyzerWindow = new WorkflowAnalyzerWindow(); + _analyzerWindow.Closed += delegate + { + _analyzerWindow = null; + }; + foreach (ResourceDictionary mergedDictionary in System.Windows.Application.Current.Resources.MergedDictionaries) + { + _analyzerWindow.Resources.MergedDictionaries.Add(mergedDictionary); + } + _analyzerWindow.Show(); + } + else if (!_analyzerWindow.IsVisible) + { + _analyzerWindow.Show(); + _analyzerWindow.Activate(); + } + else + { + _analyzerWindow.Reset(); + _analyzerWindow.Activate(); + } + _analyzerWindow.SwitchToTimelineTab(); + _agentLoop.EventOccurred -= _analyzerWindow.OnAgentEvent; + _agentLoop.EventOccurred += _analyzerWindow.OnAgentEvent; + } + + private void UpdateAnalyzerButtonVisibility() + { + LlmSettings llm = _settings.Settings.Llm; + BtnShowAnalyzer.Visibility = ((!llm.DevMode || !llm.WorkflowVisualizer) ? Visibility.Collapsed : Visibility.Visible); + } + + private void BtnShowAnalyzer_Click(object sender, MouseButtonEventArgs e) + { + if (_analyzerWindow == null) + { + _analyzerWindow = new WorkflowAnalyzerWindow(); + _analyzerWindow.Closed += delegate + { + _analyzerWindow = null; + }; + foreach (ResourceDictionary mergedDictionary in System.Windows.Application.Current.Resources.MergedDictionaries) + { + _analyzerWindow.Resources.MergedDictionaries.Add(mergedDictionary); + } + _agentLoop.EventOccurred -= _analyzerWindow.OnAgentEvent; + _agentLoop.EventOccurred += _analyzerWindow.OnAgentEvent; + _analyzerWindow.Show(); + } + else if (!_analyzerWindow.IsVisible) + { + _analyzerWindow.Show(); + _analyzerWindow.Activate(); + } + else + { + _analyzerWindow.Activate(); + } + } + + private void OnAgentEvent(AgentEvent evt) + { + if (_showExecutionHistory) + { + AddAgentEventBanner(evt); + } + PersistAgentEvent(evt); + AutoScrollIfNeeded(); + UpdateStatusBar(evt); + if (evt.InputTokens > 0 || evt.OutputTokens > 0) + { + _agentCumulativeInputTokens += evt.InputTokens; + _agentCumulativeOutputTokens += evt.OutputTokens; + UpdateStatusTokens(_agentCumulativeInputTokens, _agentCumulativeOutputTokens); + } + UpdateAgentProgressBar(evt); + if (evt.StepCurrent > 0 && evt.StepTotal > 0) + { + UpdatePlanViewerStep(evt); + } + if (evt.Type == AgentEventType.Complete) + { + CompletePlanViewer(); + } + if (evt.Success && !string.IsNullOrEmpty(evt.FilePath)) + { + RefreshFileTreeIfVisible(); + } + if (evt.Type == AgentEventType.ToolResult && evt.ToolName == "suggest_actions" && evt.Success) + { + RenderSuggestActionChips(evt.Summary); + } + if (!evt.Success || string.IsNullOrEmpty(evt.FilePath) || (evt.Type != AgentEventType.ToolResult && evt.Type != AgentEventType.Complete) || !WriteToolNames.Contains(evt.ToolName)) + { + return; + } + string autoPreview = _settings.Settings.Llm.AutoPreview; + if (autoPreview == "auto") + { + if (PreviewWindow.IsOpen) + { + PreviewWindow.RefreshIfOpen(evt.FilePath); + } + else + { + TryShowPreview(evt.FilePath); + } + if (!PreviewWindow.IsOpen) + { + TryShowPreview(evt.FilePath); + } + } + } + + private void OnSubAgentStatusChanged(SubAgentStatusEvent evt) + { + if (!((DispatcherObject)this).Dispatcher.CheckAccess()) + { + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + OnSubAgentStatusChanged(evt); + }); + return; + } + RefreshSubAgentIndicator(); + SubAgentRunStatus status = evt.Status; + if (1 == 0) + { + } + AgentEvent agentEvent = status switch + { + SubAgentRunStatus.Started => new AgentEvent + { + Timestamp = evt.Timestamp, + Type = AgentEventType.Thinking, + ToolName = "sub_agent", + Summary = "Sub-agent " + evt.Id + " started\nTask: " + evt.Task + }, + SubAgentRunStatus.Completed => new AgentEvent + { + Timestamp = evt.Timestamp, + Type = AgentEventType.ToolResult, + ToolName = "sub_agent", + Summary = "Sub-agent " + evt.Id + " completed\n" + (evt.Result ?? evt.Summary), + Success = true + }, + _ => new AgentEvent + { + Timestamp = evt.Timestamp, + Type = AgentEventType.Error, + ToolName = "sub_agent", + Summary = "Sub-agent " + evt.Id + " failed\n" + (evt.Result ?? evt.Summary), + Success = false + }, + }; + if (1 == 0) + { + } + AgentEvent evt2 = agentEvent; + OnAgentEvent(evt2); + } + + private void BtnToggleExecutionLog_Click(object sender, RoutedEventArgs e) + { + _showExecutionHistory = !_showExecutionHistory; + UpdateExecutionHistoryUi(); + RenderMessages(); + } + + private void AddPlanningCard(AgentEvent evt, bool animate = true) + { + List steps = evt.Steps; + Border border = new Border + { + Background = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#F0F4FF")), + CornerRadius = new CornerRadius(10.0), + Padding = new Thickness(14.0, 10.0, 14.0, 10.0), + Margin = new Thickness(40.0, 4.0, 80.0, 4.0), + HorizontalAlignment = System.Windows.HorizontalAlignment.Left, + MaxWidth = 560.0 + }; + StackPanel stackPanel = new StackPanel(); + StackPanel stackPanel2 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal, + Margin = new Thickness(0.0, 0.0, 0.0, 6.0) + }; + stackPanel2.Children.Add(new TextBlock + { + Text = "\ue9d5", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 13.0, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#4B5EFC")), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 6.0, 0.0) + }); + stackPanel2.Children.Add(new TextBlock + { + Text = $"작업 계획 — {steps.Count}단계", + FontSize = 12.5, + FontWeight = FontWeights.SemiBold, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#3730A3")), + VerticalAlignment = VerticalAlignment.Center + }); + stackPanel.Children.Add(stackPanel2); + Grid grid = new Grid + { + Margin = new Thickness(0.0, 0.0, 0.0, 8.0) + }; + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = new GridLength(1.0, GridUnitType.Star) + }); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = GridLength.Auto + }); + _planProgressBar = new System.Windows.Controls.ProgressBar + { + Minimum = 0.0, + Maximum = steps.Count, + Value = 0.0, + Height = 4.0, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#4B5EFC")), + Background = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#D0D5FF")), + VerticalAlignment = VerticalAlignment.Center + }; + _planProgressBar.BorderThickness = new Thickness(0.0); + Grid.SetColumn(_planProgressBar, 0); + grid.Children.Add(_planProgressBar); + _planProgressText = new TextBlock + { + Text = "0%", + FontSize = 10.5, + FontWeight = FontWeights.SemiBold, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#4B5EFC")), + Margin = new Thickness(8.0, 0.0, 0.0, 0.0), + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(_planProgressText, 1); + grid.Children.Add(_planProgressText); + stackPanel.Children.Add(grid); + _planStepsPanel = new StackPanel(); + for (int i = 0; i < steps.Count; i++) + { + StackPanel stackPanel3 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal, + Margin = new Thickness(0.0, 1.0, 0.0, 1.0), + Tag = i + }; + stackPanel3.Children.Add(new TextBlock + { + Text = "○", + FontSize = 11.0, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#9CA3AF")), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 6.0, 0.0), + Tag = "status" + }); + stackPanel3.Children.Add(new TextBlock + { + Text = $"{i + 1}. {steps[i]}", + FontSize = 11.5, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#4B5563")), + TextWrapping = TextWrapping.Wrap, + MaxWidth = 480.0, + VerticalAlignment = VerticalAlignment.Center + }); + _planStepsPanel.Children.Add(stackPanel3); + } + stackPanel.Children.Add(_planStepsPanel); + border.Child = stackPanel; + _planningCard = border; + if (animate) + { + border.Opacity = 0.0; + border.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0.0, 1.0, TimeSpan.FromMilliseconds(300.0))); + } + MessagePanel.Children.Add(border); + } + + private void AddDecisionButtons(TaskCompletionSource tcs, List options) + { + System.Windows.Media.Brush accentBrush = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.CornflowerBlue; + System.Windows.Media.Color color = ((SolidColorBrush)accentBrush).Color; + System.Windows.Media.Brush foreground = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + Border border = new Border + { + Margin = new Thickness(40.0, 2.0, 80.0, 6.0), + HorizontalAlignment = System.Windows.HorizontalAlignment.Left, + MaxWidth = 560.0 + }; + StackPanel outerStack = new StackPanel(); + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal, + Margin = new Thickness(0.0, 0.0, 0.0, 0.0) + }; + Border border2 = new Border + { + Background = accentBrush, + CornerRadius = new CornerRadius(16.0), + Padding = new Thickness(16.0, 7.0, 16.0, 7.0), + Margin = new Thickness(0.0, 0.0, 8.0, 0.0), + Cursor = System.Windows.Input.Cursors.Hand + }; + StackPanel stackPanel2 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel2.Children.Add(new TextBlock + { + Text = "\ue73e", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 11.0, + Foreground = System.Windows.Media.Brushes.White, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 5.0, 0.0) + }); + stackPanel2.Children.Add(new TextBlock + { + Text = "승인", + FontSize = 12.5, + FontWeight = FontWeights.SemiBold, + Foreground = System.Windows.Media.Brushes.White + }); + border2.Child = stackPanel2; + ApplyMenuItemHover(border2); + border2.MouseLeftButtonUp += delegate + { + CollapseDecisionButtons(outerStack, "✓ 승인됨", accentBrush); + tcs.TrySetResult(null); + }; + stackPanel.Children.Add(border2); + Border border3 = new Border + { + Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(24, color.R, color.G, color.B)), + CornerRadius = new CornerRadius(16.0), + Padding = new Thickness(14.0, 7.0, 14.0, 7.0), + Margin = new Thickness(0.0, 0.0, 8.0, 0.0), + Cursor = System.Windows.Input.Cursors.Hand, + BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromArgb(64, color.R, color.G, color.B)), + BorderThickness = new Thickness(1.0) + }; + StackPanel stackPanel3 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel3.Children.Add(new TextBlock + { + Text = "\ue70f", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 11.0, + Foreground = accentBrush, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 5.0, 0.0) + }); + stackPanel3.Children.Add(new TextBlock + { + Text = "수정 요청", + FontSize = 12.5, + FontWeight = FontWeights.SemiBold, + Foreground = accentBrush + }); + border3.Child = stackPanel3; + ApplyMenuItemHover(border3); + Border editInputPanel = new Border + { + Visibility = Visibility.Collapsed, + Background = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#F8F9FC")), + CornerRadius = new CornerRadius(10.0), + Padding = new Thickness(10.0, 8.0, 10.0, 8.0), + Margin = new Thickness(0.0, 8.0, 0.0, 0.0) + }; + StackPanel stackPanel4 = new StackPanel(); + stackPanel4.Children.Add(new TextBlock + { + Text = "수정 사항을 입력하세요:", + FontSize = 11.5, + Foreground = foreground, + Margin = new Thickness(0.0, 0.0, 0.0, 6.0) + }); + System.Windows.Controls.TextBox editTextBox = new System.Windows.Controls.TextBox + { + MinHeight = 36.0, + MaxHeight = 100.0, + AcceptsReturn = true, + TextWrapping = TextWrapping.Wrap, + FontSize = 12.5, + Background = System.Windows.Media.Brushes.White, + BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromArgb(64, color.R, color.G, color.B)), + BorderThickness = new Thickness(1.0), + Padding = new Thickness(8.0, 6.0, 8.0, 6.0) + }; + stackPanel4.Children.Add(editTextBox); + Border border4 = new Border + { + Background = accentBrush, + CornerRadius = new CornerRadius(8.0), + Padding = new Thickness(12.0, 5.0, 12.0, 5.0), + Margin = new Thickness(0.0, 6.0, 0.0, 0.0), + Cursor = System.Windows.Input.Cursors.Hand, + HorizontalAlignment = System.Windows.HorizontalAlignment.Right + }; + border4.Child = new TextBlock + { + Text = "전송", + FontSize = 12.0, + FontWeight = FontWeights.SemiBold, + Foreground = System.Windows.Media.Brushes.White + }; + ApplyHoverScaleAnimation(border4, 1.05); + border4.MouseLeftButtonUp += delegate + { + string text = editTextBox.Text.Trim(); + if (!string.IsNullOrEmpty(text)) + { + CollapseDecisionButtons(outerStack, "✎ 수정 요청됨", accentBrush); + tcs.TrySetResult(text); + } + }; + stackPanel4.Children.Add(border4); + editInputPanel.Child = stackPanel4; + border3.MouseLeftButtonUp += delegate + { + editInputPanel.Visibility = ((editInputPanel.Visibility == Visibility.Visible) ? Visibility.Collapsed : Visibility.Visible); + if (editInputPanel.Visibility == Visibility.Visible) + { + editTextBox.Focus(); + } + }; + stackPanel.Children.Add(border3); + Border border5 = new Border + { + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(16.0), + Padding = new Thickness(14.0, 7.0, 14.0, 7.0), + Cursor = System.Windows.Input.Cursors.Hand, + BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromArgb(48, 220, 38, 38)), + BorderThickness = new Thickness(1.0) + }; + StackPanel stackPanel5 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel5.Children.Add(new TextBlock + { + Text = "\ue711", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 11.0, + Foreground = new SolidColorBrush(System.Windows.Media.Color.FromRgb(220, 38, 38)), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 5.0, 0.0) + }); + stackPanel5.Children.Add(new TextBlock + { + Text = "취소", + FontSize = 12.5, + FontWeight = FontWeights.SemiBold, + Foreground = new SolidColorBrush(System.Windows.Media.Color.FromRgb(220, 38, 38)) + }); + border5.Child = stackPanel5; + ApplyMenuItemHover(border5); + border5.MouseLeftButtonUp += delegate + { + CollapseDecisionButtons(outerStack, "✕ 취소됨", new SolidColorBrush(System.Windows.Media.Color.FromRgb(220, 38, 38))); + tcs.TrySetResult("취소"); + }; + stackPanel.Children.Add(border5); + outerStack.Children.Add(stackPanel); + outerStack.Children.Add(editInputPanel); + border.Child = outerStack; + ApplyMessageEntryAnimation(border); + MessagePanel.Children.Add(border); + ForceScrollToEnd(); + StackPanel capturedOuterStack = outerStack; + System.Windows.Media.Brush capturedAccent = accentBrush; + tcs.Task.ContinueWith(delegate(Task t) + { + ((DispatcherObject)this).Dispatcher.BeginInvoke((Delegate)(Action)delegate + { + if (capturedOuterStack.Children.Count > 1) + { + string resultText = ((t.Result == null) ? "✓ 승인됨" : ((t.Result == "취소") ? "✕ 취소됨" : "✎ 수정 요청됨")); + System.Windows.Media.Brush fg = ((t.Result == "취소") ? new SolidColorBrush(System.Windows.Media.Color.FromRgb(220, 38, 38)) : capturedAccent); + CollapseDecisionButtons(capturedOuterStack, resultText, fg); + } + }, Array.Empty()); + }, TaskScheduler.Default); + } + + private void CollapseDecisionButtons(StackPanel outerStack, string resultText, System.Windows.Media.Brush fg) + { + outerStack.Children.Clear(); + TextBlock element = new TextBlock + { + Text = resultText, + FontSize = 12.0, + FontWeight = FontWeights.SemiBold, + Foreground = fg, + Opacity = 0.8, + Margin = new Thickness(0.0, 2.0, 0.0, 2.0) + }; + outerStack.Children.Add(element); + } + + private Func, Task> CreatePlanDecisionCallback() + { + return async delegate(string planSummary, List options) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + List steps = TaskDecomposer.ExtractSteps(planSummary); + await ((DispatcherObject)this).Dispatcher.InvokeAsync((Action)delegate + { + if (_planViewerWindow == null || !IsWindowAlive(_planViewerWindow)) + { + _planViewerWindow = new PlanViewerWindow(); + _planViewerWindow.Closing += delegate(object? _, CancelEventArgs e) + { + e.Cancel = true; + _planViewerWindow.Hide(); + }; + } + _planViewerWindow.ShowPlanAsync(planSummary, steps, tcs); + AddDecisionButtons(tcs, options); + ShowPlanButton(show: true); + }); + if (await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromMinutes(5.0))) != tcs.Task) + { + await ((DispatcherObject)this).Dispatcher.InvokeAsync((Action)delegate + { + _planViewerWindow?.Hide(); + }); + return "취소"; + } + string result = await tcs.Task; + if (result != null) + { + await ((DispatcherObject)this).Dispatcher.InvokeAsync((Action)delegate + { + _planViewerWindow?.Hide(); + }); + } + else + { + await ((DispatcherObject)this).Dispatcher.InvokeAsync((Action)delegate + { + _planViewerWindow?.SwitchToExecutionMode(); + _planViewerWindow?.Hide(); + }); + } + return result; + }; + } + + private void ShowPlanButton(bool show) + { + if (!show) + { + for (int num = MoodIconPanel.Children.Count - 1; num >= 0; num--) + { + if (MoodIconPanel.Children[num] is Border { Tag: var tag } && tag?.ToString() == "PlanBtn") + { + if (num > 0 && MoodIconPanel.Children[num - 1] is Border { Tag: var tag2 } && tag2?.ToString() == "PlanSep") + { + MoodIconPanel.Children.RemoveAt(num - 1); + } + if (num < MoodIconPanel.Children.Count) + { + MoodIconPanel.Children.RemoveAt(Math.Min(num, MoodIconPanel.Children.Count - 1)); + } + break; + } + } + return; + } + foreach (object child in MoodIconPanel.Children) + { + if (child is Border { Tag: var tag3 } && tag3?.ToString() == "PlanBtn") + { + return; + } + } + Border border4 = new Border(); + border4.Width = 1.0; + border4.Height = 18.0; + border4.Background = (TryFindResource("SeparatorColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + border4.Margin = new Thickness(4.0, 0.0, 4.0, 0.0); + border4.VerticalAlignment = VerticalAlignment.Center; + border4.Tag = "PlanSep"; + Border element = border4; + MoodIconPanel.Children.Add(element); + Border border5 = CreateFolderBarButton("\ue9d2", "계획", "실행 계획 보기", "#10B981"); + border5.Tag = "PlanBtn"; + border5.MouseLeftButtonUp += delegate(object _, MouseButtonEventArgs e) + { + e.Handled = true; + if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow)) + { + _planViewerWindow.Show(); + _planViewerWindow.Activate(); + } + }; + MoodIconPanel.Children.Add(border5); + } + + private void UpdatePlanViewerStep(AgentEvent evt) + { + if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow) && evt.StepCurrent > 0) + { + _planViewerWindow.UpdateCurrentStep(evt.StepCurrent - 1); + } + } + + private void CompletePlanViewer() + { + if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow)) + { + _planViewerWindow.MarkComplete(); + } + ShowPlanButton(show: false); + } + + private static bool IsWindowAlive(Window? w) + { + if (w == null) + { + return false; + } + try + { + bool isVisible = w.IsVisible; + return true; + } + catch + { + return false; + } + } + + private void RenderSuggestActionChips(string jsonSummary) + { + List<(string, string)> list = new List<(string, string)>(); + try + { + if (jsonSummary.Contains("\"label\"")) + { + using JsonDocument jsonDocument = JsonDocument.Parse(jsonSummary); + if (jsonDocument.RootElement.ValueKind == JsonValueKind.Array) + { + foreach (JsonElement item4 in jsonDocument.RootElement.EnumerateArray()) + { + JsonElement value; + string text = (item4.TryGetProperty("label", out value) ? (value.GetString() ?? "") : ""); + JsonElement value2; + string item = (item4.TryGetProperty("command", out value2) ? (value2.GetString() ?? text) : text); + if (!string.IsNullOrEmpty(text)) + { + list.Add((text, item)); + } + } + } + } + else + { + string[] array = jsonSummary.Split('\n'); + foreach (string text2 in array) + { + string text3 = text2.Trim().TrimStart('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', ' '); + if (!string.IsNullOrEmpty(text3)) + { + string[] array2 = text3.Split('→', ':', '—'); + if (array2.Length >= 2) + { + list.Add((array2[0].Trim(), array2[1].Trim())); + } + else if (!string.IsNullOrEmpty(text3)) + { + list.Add((text3, text3)); + } + } + } + } + } + catch + { + return; + } + if (list.Count == 0) + { + return; + } + System.Windows.Media.Brush brush = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.CornflowerBlue; + System.Windows.Media.Brush foreground = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush brush2 = (TryFindResource("ItemHoverBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromArgb(24, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + Border container = new Border + { + Margin = new Thickness(40.0, 4.0, 40.0, 8.0), + HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch + }; + StackPanel stackPanel = new StackPanel + { + Margin = new Thickness(0.0, 0.0, 0.0, 6.0) + }; + stackPanel.Children.Add(new TextBlock + { + Text = "\ud83d\udca1 다음 작업 제안:", + FontSize = 12.0, + Foreground = foreground + }); + WrapPanel wrapPanel = new WrapPanel + { + Margin = new Thickness(0.0, 2.0, 0.0, 0.0) + }; + foreach (var item5 in list.Take(5)) + { + string item2 = item5.Item1; + string item3 = item5.Item2; + string capturedCmd = item3; + Border border = new Border + { + CornerRadius = new CornerRadius(16.0), + Padding = new Thickness(14.0, 7.0, 14.0, 7.0), + Margin = new Thickness(0.0, 0.0, 8.0, 6.0), + Cursor = System.Windows.Input.Cursors.Hand, + Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(21, ((SolidColorBrush)brush).Color.R, ((SolidColorBrush)brush).Color.G, ((SolidColorBrush)brush).Color.B)), + BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromArgb(64, ((SolidColorBrush)brush).Color.R, ((SolidColorBrush)brush).Color.G, ((SolidColorBrush)brush).Color.B)), + BorderThickness = new Thickness(1.0) + }; + border.Child = new TextBlock + { + Text = item2, + FontSize = 12.5, + Foreground = brush, + FontWeight = FontWeights.SemiBold + }; + border.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + ((Border)s).Opacity = 0.8; + }; + border.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + ((Border)s).Opacity = 1.0; + }; + border.MouseLeftButtonUp += delegate + { + MessagePanel.Children.Remove(container); + if (capturedCmd.StartsWith("/")) + { + InputBox.Text = capturedCmd + " "; + InputBox.CaretIndex = InputBox.Text.Length; + InputBox.Focus(); + } + else + { + InputBox.Text = capturedCmd; + SendMessageAsync(); + } + }; + wrapPanel.Children.Add(border); + } + StackPanel stackPanel2 = new StackPanel(); + stackPanel2.Children.Add(stackPanel); + stackPanel2.Children.Add(wrapPanel); + container.Child = stackPanel2; + ApplyMessageEntryAnimation(container); + MessagePanel.Children.Add(container); + ForceScrollToEnd(); + } + + private string BuildFeedbackContext() + { + try + { + List list = (from m in _storage.LoadAllMeta() + orderby m.UpdatedAt descending + select m).Take(20).ToList(); + List list2 = new List(); + List list3 = new List(); + foreach (ChatConversation item2 in list) + { + ChatConversation chatConversation = _storage.Load(item2.Id); + if (chatConversation == null) + { + continue; + } + foreach (ChatMessage item3 in chatConversation.Messages.Where((ChatMessage m) => m.Role == "assistant" && m.Feedback != null)) + { + string content = item3.Content; + string item = ((content != null && content.Length > 80) ? item3.Content.Substring(0, 80) : (item3.Content ?? "")); + if (item3.Feedback == "like") + { + list2.Add(item); + } + else if (item3.Feedback == "dislike") + { + list3.Add(item); + } + } + } + if (list2.Count == 0 && list3.Count == 0) + { + return ""; + } + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.AppendLine("\n[사용자 선호도 참고]"); + if (list2.Count > 0) + { + StringBuilder stringBuilder2 = stringBuilder; + StringBuilder stringBuilder3 = stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(20, 1, stringBuilder2); + handler.AppendLiteral("사용자가 좋아한 응답 스타일 ("); + handler.AppendFormatted(list2.Count); + handler.AppendLiteral("건):"); + stringBuilder3.AppendLine(ref handler); + foreach (string item4 in list2.Take(5)) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder4 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder2); + handler.AppendLiteral(" - \""); + handler.AppendFormatted(item4); + handler.AppendLiteral("...\""); + stringBuilder4.AppendLine(ref handler); + } + } + if (list3.Count > 0) + { + StringBuilder stringBuilder2 = stringBuilder; + StringBuilder stringBuilder5 = stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(20, 1, stringBuilder2); + handler.AppendLiteral("사용자가 싫어한 응답 스타일 ("); + handler.AppendFormatted(list3.Count); + handler.AppendLiteral("건):"); + stringBuilder5.AppendLine(ref handler); + foreach (string item5 in list3.Take(5)) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder6 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder2); + handler.AppendLiteral(" - \""); + handler.AppendFormatted(item5); + handler.AppendLiteral("...\""); + stringBuilder6.AppendLine(ref handler); + } + } + stringBuilder.AppendLine("위 선호도를 참고하여 응답 스타일을 조정하세요."); + return stringBuilder.ToString(); + } + catch + { + return ""; + } + } + + private void UpdateProgressBar(AgentEvent evt) + { + if (_planProgressBar == null || _planStepsPanel == null || _planProgressText == null) + { + return; + } + int num = evt.StepCurrent - 1; + int stepTotal = evt.StepTotal; + _planProgressBar.Value = evt.StepCurrent; + int value = (int)((double)evt.StepCurrent / (double)stepTotal * 100.0); + _planProgressText.Text = $"{value}%"; + for (int i = 0; i < _planStepsPanel.Children.Count; i++) + { + if (!(_planStepsPanel.Children[i] is StackPanel stackPanel) || stackPanel.Children.Count < 2) + { + continue; + } + TextBlock textBlock = stackPanel.Children[0] as TextBlock; + TextBlock textBlock2 = stackPanel.Children[1] as TextBlock; + if (textBlock != null && textBlock2 != null) + { + if (i < num) + { + textBlock.Text = "●"; + textBlock.Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#16A34A")); + textBlock2.Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#6B7280")); + } + else if (i == num) + { + textBlock.Text = "◉"; + textBlock.Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#4B5EFC")); + textBlock2.Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#1E293B")); + textBlock2.FontWeight = FontWeights.SemiBold; + } + else + { + textBlock.Text = "○"; + textBlock.Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#9CA3AF")); + textBlock2.Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#4B5563")); + textBlock2.FontWeight = FontWeights.Normal; + } + } + } + } + + private static UIElement BuildDiffView(string text) + { + StackPanel stackPanel = new StackPanel + { + Background = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#FAFAFA")), + MaxWidth = 520.0 + }; + bool flag = false; + string[] array = text.Split('\n'); + foreach (string text2 in array) + { + string text3 = text2.TrimEnd('\r'); + if (!flag && !text3.StartsWith("--- ")) + { + stackPanel.Children.Add(new TextBlock + { + Text = text3, + FontSize = 11.0, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#4B5563")), + FontFamily = new System.Windows.Media.FontFamily("Consolas"), + Margin = new Thickness(0.0, 0.0, 0.0, 1.0) + }); + continue; + } + flag = true; + string text4; + string value; + if (text3.StartsWith("---") || text3.StartsWith("+++")) + { + text4 = "#F3F4F6"; + value = "#374151"; + } + else if (text3.StartsWith("@@")) + { + text4 = "#EFF6FF"; + value = "#3B82F6"; + } + else if (text3.StartsWith("+")) + { + text4 = "#ECFDF5"; + value = "#059669"; + } + else if (text3.StartsWith("-")) + { + text4 = "#FEF2F2"; + value = "#DC2626"; + } + else + { + text4 = "Transparent"; + value = "#6B7280"; + } + TextBlock textBlock = new TextBlock + { + Text = text3, + FontSize = 10.5, + FontFamily = new System.Windows.Media.FontFamily("Consolas"), + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(value)), + Padding = new Thickness(4.0, 1.0, 4.0, 1.0) + }; + if (text4 != "Transparent") + { + textBlock.Background = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(text4)); + } + stackPanel.Children.Add(textBlock); + } + return stackPanel; + } + + private void AddAgentEventBanner(AgentEvent evt, bool animate = true) + { + string agentLogLevel = _settings.Settings.Llm.AgentLogLevel; + if (evt.Type == AgentEventType.Planning) + { + List steps = evt.Steps; + if (steps != null && steps.Count > 0) + { + AddPlanningCard(evt, animate); + return; + } + } + if (evt.Type == AgentEventType.StepStart && evt.StepTotal > 0) + { + UpdateProgressBar(evt); + } + else + { + if (agentLogLevel == "simple" && evt.Type == AgentEventType.ToolCall) + { + return; + } + bool flag = evt.Type == AgentEventType.StepDone && evt.ToolName == "total_stats"; + string text; + string text2; + string value; + string value2; + if (flag) + { + text = "\ue9d2"; + text2 = "Total Stats"; + value = "#F3EEFF"; + value2 = "#7C3AED"; + (string, string, string, string) tuple = ("\ue9d2", "Total Stats", "#F3EEFF", "#7C3AED"); + } + else + { + AgentEventType type = evt.Type; + if (1 == 0) + { + } + (string, string, string, string) tuple2 = type switch + { + AgentEventType.Thinking => ("\ue8bd", "Thinking", "#F0F0FF", "#6B7BC4"), + AgentEventType.ToolCall => ("\ue8a7", evt.ToolName, "#EEF6FF", "#3B82F6"), + AgentEventType.ToolResult => ("\ue73e", evt.ToolName, "#EEF9EE", "#16A34A"), + AgentEventType.SkillCall => ("\ue8a5", evt.ToolName, "#FFF7ED", "#EA580C"), + AgentEventType.Error => ("\ue783", "Error", "#FEF2F2", "#DC2626"), + AgentEventType.Complete => ("\ue930", "Complete", "#F0FFF4", "#15803D"), + AgentEventType.StepDone => ("\ue73e", "Step Done", "#EEF9EE", "#16A34A"), + AgentEventType.Paused => ("\ue769", "Paused", "#FFFBEB", "#D97706"), + AgentEventType.Resumed => ("\ue768", "Resumed", "#ECFDF5", "#059669"), + _ => ("\ue946", "Agent", "#F5F5F5", "#6B7280"), + }; + if (1 == 0) + { + } + (string, string, string, string) tuple3 = tuple2; + text = tuple3.Item1; + text2 = tuple3.Item2; + value = tuple3.Item3; + value2 = tuple3.Item4; + (string, string, string, string) tuple = (tuple3.Item1, tuple3.Item2, tuple3.Item3, tuple3.Item4); + } + Border border = new Border + { + Background = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(value)), + BorderBrush = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(value2)), + BorderThickness = new Thickness(1.0), + CornerRadius = new CornerRadius(16.0), + Padding = new Thickness(14.0, 10.0, 14.0, 10.0), + Margin = new Thickness(48.0, 4.0, 120.0, 4.0), + HorizontalAlignment = System.Windows.HorizontalAlignment.Left, + MaxWidth = GetMessageMaxWidth(), + Effect = new DropShadowEffect + { + BlurRadius = 12.0, + ShadowDepth = 0.0, + Opacity = 0.05 + } + }; + StackPanel stackPanel = new StackPanel(); + Grid grid = new Grid(); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = new GridLength(1.0, GridUnitType.Star) + }); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = GridLength.Auto + }); + StackPanel stackPanel2 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel2.Children.Add(new TextBlock + { + Text = text, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 11.0, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(value2)), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 6.0, 0.0) + }); + stackPanel2.Children.Add(new TextBlock + { + Text = text2, + FontSize = 11.5, + FontWeight = FontWeights.SemiBold, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(value2)), + VerticalAlignment = VerticalAlignment.Center + }); + Grid.SetColumn(stackPanel2, 0); + StackPanel stackPanel3 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + if (agentLogLevel != "simple" && evt.ElapsedMs > 0) + { + stackPanel3.Children.Add(new TextBlock + { + Text = ((evt.ElapsedMs < 1000) ? $"{evt.ElapsedMs}ms" : $"{(double)evt.ElapsedMs / 1000.0:F1}s"), + FontSize = 10.0, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#9CA3AF")), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(8.0, 0.0, 0.0, 0.0) + }); + } + if (agentLogLevel != "simple" && (evt.InputTokens > 0 || evt.OutputTokens > 0)) + { + string text3 = ((evt.InputTokens > 0 && evt.OutputTokens > 0) ? $"{evt.InputTokens}→{evt.OutputTokens}t" : ((evt.InputTokens > 0) ? $"↑{evt.InputTokens}t" : $"↓{evt.OutputTokens}t")); + stackPanel3.Children.Add(new Border + { + Background = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#F0F0F5")), + CornerRadius = new CornerRadius(4.0), + Padding = new Thickness(5.0, 1.0, 5.0, 1.0), + Margin = new Thickness(6.0, 0.0, 0.0, 0.0), + VerticalAlignment = VerticalAlignment.Center, + Child = new TextBlock + { + Text = text3, + FontSize = 9.5, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#8B8FA3")), + FontFamily = new System.Windows.Media.FontFamily("Consolas") + } + }); + } + Grid.SetColumn(stackPanel3, 1); + grid.Children.Add(stackPanel2); + grid.Children.Add(stackPanel3); + StackPanel stackPanel4 = stackPanel2; + stackPanel.Children.Add(grid); + if (agentLogLevel == "simple") + { + if (!string.IsNullOrEmpty(evt.Summary)) + { + string text4 = ((evt.Summary.Length > 100) ? (evt.Summary.Substring(0, 100) + "…") : evt.Summary); + stackPanel.Children.Add(new TextBlock + { + Text = text4, + FontSize = 11.0, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#6B7280")), + TextWrapping = TextWrapping.NoWrap, + TextTrimming = TextTrimming.CharacterEllipsis, + Margin = new Thickness(0.0, 2.0, 0.0, 0.0) + }); + } + } + else if (!string.IsNullOrEmpty(evt.Summary)) + { + string summary = evt.Summary; + if ((evt.Type == AgentEventType.ToolCall || evt.Type == AgentEventType.ToolResult) && summary.Length > 60) + { + string text5 = ((summary.Length > 80) ? (summary.Substring(0, 80) + "...") : summary); + TextBlock summaryTb = new TextBlock + { + Text = text5, + FontSize = 11.5, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#4B5563")), + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0.0, 3.0, 0.0, 0.0), + Cursor = System.Windows.Input.Cursors.Hand + }; + UIElement fullContent; + if (summary.Contains("--- ") && summary.Contains("+++ ")) + { + fullContent = BuildDiffView(summary); + } + else + { + fullContent = new TextBlock + { + Text = summary, + FontSize = 11.0, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#6B7280")), + TextWrapping = TextWrapping.Wrap, + FontFamily = new System.Windows.Media.FontFamily("Consolas") + }; + } + fullContent.Visibility = Visibility.Collapsed; + ((FrameworkElement)fullContent).Margin = new Thickness(0.0, 4.0, 0.0, 0.0); + TextBlock expandIcon = new TextBlock + { + Text = "\ue70d", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 9.0, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#9CA3AF")), + Margin = new Thickness(6.0, 0.0, 0.0, 0.0), + VerticalAlignment = VerticalAlignment.Center + }; + stackPanel4.Children.Add(expandIcon); + bool isExpanded = false; + border.MouseLeftButtonDown += delegate + { + isExpanded = !isExpanded; + fullContent.Visibility = ((!isExpanded) ? Visibility.Collapsed : Visibility.Visible); + summaryTb.Visibility = (isExpanded ? Visibility.Collapsed : Visibility.Visible); + expandIcon.Text = (isExpanded ? "\ue70e" : "\ue70d"); + }; + stackPanel.Children.Add(summaryTb); + stackPanel.Children.Add(fullContent); + } + else + { + stackPanel.Children.Add(new TextBlock + { + Text = summary, + FontSize = 11.5, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#4B5563")), + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0.0, 3.0, 0.0, 0.0) + }); + } + } + if (agentLogLevel == "debug" && !string.IsNullOrEmpty(evt.ToolInput)) + { + stackPanel.Children.Add(new Border + { + Background = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#F8F8FC")), + CornerRadius = new CornerRadius(4.0), + Padding = new Thickness(8.0, 4.0, 8.0, 4.0), + Margin = new Thickness(0.0, 4.0, 0.0, 0.0), + Child = new TextBlock + { + Text = ((evt.ToolInput.Length > 500) ? (evt.ToolInput.Substring(0, 500) + "…") : evt.ToolInput), + FontSize = 10.0, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#7C7F93")), + FontFamily = new System.Windows.Media.FontFamily("Consolas"), + TextWrapping = TextWrapping.Wrap + } + }); + } + if (!string.IsNullOrEmpty(evt.FilePath)) + { + Border border2 = new Border + { + Background = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#F8FAFC")), + CornerRadius = new CornerRadius(4.0), + Padding = new Thickness(8.0, 4.0, 8.0, 4.0), + Margin = new Thickness(0.0, 4.0, 0.0, 0.0) + }; + StackPanel stackPanel5 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel5.Children.Add(new TextBlock + { + Text = "\ue8b7", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 10.0, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#9CA3AF")), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 4.0, 0.0) + }); + stackPanel5.Children.Add(new TextBlock + { + Text = evt.FilePath, + FontSize = 10.5, + Foreground = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#6B7280")), + FontFamily = new System.Windows.Media.FontFamily("Consolas"), + VerticalAlignment = VerticalAlignment.Center, + TextTrimming = TextTrimming.CharacterEllipsis + }); + StackPanel element = BuildFileQuickActions(evt.FilePath); + stackPanel5.Children.Add(element); + border2.Child = stackPanel5; + stackPanel.Children.Add(border2); + } + border.Child = stackPanel; + if (flag) + { + border.Cursor = System.Windows.Input.Cursors.Hand; + border.ToolTip = "클릭하여 병목 분석 보기"; + border.MouseLeftButtonUp += delegate + { + OpenWorkflowAnalyzerIfEnabled(); + _analyzerWindow?.SwitchToBottleneckTab(); + _analyzerWindow?.Activate(); + }; + } + if (animate) + { + border.Opacity = 0.0; + border.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0.0, 1.0, TimeSpan.FromMilliseconds(200.0))); + } + MessagePanel.Children.Add(border); + } + } + + private StackPanel BuildFileQuickActions(string filePath) + { + StackPanel panel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal, + Margin = new Thickness(6.0, 0.0, 0.0, 0.0), + VerticalAlignment = VerticalAlignment.Center + }; + System.Windows.Media.Color color = (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#3B82F6"); + SolidColorBrush accentBrush = new SolidColorBrush(color); + string item = System.IO.Path.GetExtension(filePath).ToLowerInvariant(); + if (_previewableExtensions.Contains(item)) + { + string path1 = filePath; + panel.Children.Add(MakeBtn("\ue8a1", "프리뷰", delegate + { + ShowPreviewPanel(path1); + })); + } + string path2 = filePath; + panel.Children.Add(MakeBtn("\ue8a7", "열기", delegate + { + try + { + Process.Start(new ProcessStartInfo + { + FileName = path2, + UseShellExecute = true + }); + } + catch + { + } + })); + string path3 = filePath; + panel.Children.Add(MakeBtn("\ued25", "폴더", delegate + { + try + { + Process.Start("explorer.exe", "/select,\"" + path3 + "\""); + } + catch + { + } + })); + string path4 = filePath; + panel.Children.Add(MakeBtn("\ue8c8", "복사", delegate + { + //IL_00b9: Unknown result type (might be due to invalid IL or missing references) + //IL_00be: Unknown result type (might be due to invalid IL or missing references) + //IL_00d8: Expected O, but got Unknown + try + { + System.Windows.Clipboard.SetText(path4); + UIElementCollection children = panel.Children; + if (children[children.Count - 1] is Border { Child: StackPanel child }) + { + TextBlock origLabel = child.Children.OfType().LastOrDefault(); + if (origLabel != null) + { + string prev = origLabel.Text; + origLabel.Text = "복사됨 ✓"; + DispatcherTimer timer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(1500.0) + }; + timer.Tick += delegate + { + origLabel.Text = prev; + timer.Stop(); + }; + timer.Start(); + } + } + } + catch + { + } + })); + return panel; + Border MakeBtn(string mdlIcon, string label, Action action) + { + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel.Children.Add(new TextBlock + { + Text = mdlIcon, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 9.0, + Foreground = accentBrush, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 3.0, 0.0) + }); + stackPanel.Children.Add(new TextBlock + { + Text = label, + FontSize = 10.0, + Foreground = accentBrush, + VerticalAlignment = VerticalAlignment.Center + }); + Border border = new Border + { + Child = stackPanel, + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(4.0), + Padding = new Thickness(5.0, 2.0, 5.0, 2.0), + Cursor = System.Windows.Input.Cursors.Hand + }; + border.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border2) + { + border2.Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(21, 59, 130, 246)); + } + }; + border.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border2) + { + border2.Background = System.Windows.Media.Brushes.Transparent; + } + }; + border.MouseLeftButtonUp += delegate + { + action(); + }; + return border; + } + } + + private async Task RegenerateLastAsync() + { + if (_isStreaming) + { + return; + } + ChatConversation conv; + lock (_convLock) + { + if (_currentConversation == null) + { + return; + } + conv = _currentConversation; + } + lock (_convLock) + { + int num; + if (conv.Messages.Count > 0) + { + List messages = conv.Messages; + num = ((messages[messages.Count - 1].Role == "assistant") ? 1 : 0); + } + else + { + num = 0; + } + if (num != 0) + { + conv.Messages.RemoveAt(conv.Messages.Count - 1); + } + } + if (MessagePanel.Children.Count > 0) + { + MessagePanel.Children.RemoveAt(MessagePanel.Children.Count - 1); + } + await SendRegenerateAsync(conv); + } + + private void ShowRetryWithFeedbackInput() + { + if (_isStreaming) + { + return; + } + System.Windows.Media.Brush background = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.CornflowerBlue; + System.Windows.Media.Brush background2 = (TryFindResource("ItemBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromRgb(42, 43, 64)); + System.Windows.Media.Brush brush = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush foreground = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush borderBrush = (TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + Border container = new Border + { + Margin = new Thickness(40.0, 4.0, 40.0, 8.0), + Padding = new Thickness(14.0, 10.0, 14.0, 10.0), + CornerRadius = new CornerRadius(12.0), + Background = background2, + HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch + }; + StackPanel stackPanel = new StackPanel(); + stackPanel.Children.Add(new TextBlock + { + Text = "어떻게 수정하면 좋을지 알려주세요:", + FontSize = 12.0, + Foreground = foreground, + Margin = new Thickness(0.0, 0.0, 0.0, 6.0) + }); + System.Windows.Controls.TextBox textBox = new System.Windows.Controls.TextBox + { + MinHeight = 38.0, + MaxHeight = 80.0, + AcceptsReturn = true, + TextWrapping = TextWrapping.Wrap, + FontSize = 13.0, + Background = ((TryFindResource("LauncherBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Black), + Foreground = brush, + CaretBrush = brush, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1.0), + Padding = new Thickness(10.0, 6.0, 10.0, 6.0) + }; + stackPanel.Children.Add(textBox); + StackPanel stackPanel2 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal, + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, + Margin = new Thickness(0.0, 8.0, 0.0, 0.0) + }; + Border border = new Border + { + Background = background, + CornerRadius = new CornerRadius(8.0), + Padding = new Thickness(14.0, 6.0, 14.0, 6.0), + Cursor = System.Windows.Input.Cursors.Hand, + Margin = new Thickness(6.0, 0.0, 0.0, 0.0) + }; + border.Child = new TextBlock + { + Text = "재시도", + FontSize = 12.0, + FontWeight = FontWeights.SemiBold, + Foreground = System.Windows.Media.Brushes.White + }; + border.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + ((Border)s).Opacity = 0.85; + }; + border.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + ((Border)s).Opacity = 1.0; + }; + border.MouseLeftButtonUp += delegate + { + string text = textBox.Text.Trim(); + if (!string.IsNullOrEmpty(text)) + { + MessagePanel.Children.Remove(container); + RetryWithFeedbackAsync(text); + } + }; + Border border2 = new Border + { + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(8.0), + Padding = new Thickness(12.0, 6.0, 12.0, 6.0), + Cursor = System.Windows.Input.Cursors.Hand + }; + border2.Child = new TextBlock + { + Text = "취소", + FontSize = 12.0, + Foreground = foreground + }; + border2.MouseLeftButtonUp += delegate + { + MessagePanel.Children.Remove(container); + }; + stackPanel2.Children.Add(border2); + stackPanel2.Children.Add(border); + stackPanel.Children.Add(stackPanel2); + container.Child = stackPanel; + ApplyMessageEntryAnimation(container); + MessagePanel.Children.Add(container); + ForceScrollToEnd(); + textBox.Focus(); + } + + private async Task RetryWithFeedbackAsync(string feedback) + { + if (_isStreaming) + { + return; + } + ChatConversation conv; + lock (_convLock) + { + if (_currentConversation == null) + { + return; + } + conv = _currentConversation; + } + lock (_convLock) + { + int num; + if (conv.Messages.Count > 0) + { + List messages = conv.Messages; + num = ((messages[messages.Count - 1].Role == "assistant") ? 1 : 0); + } + else + { + num = 0; + } + if (num != 0) + { + conv.Messages.RemoveAt(conv.Messages.Count - 1); + } + } + if (MessagePanel.Children.Count > 0) + { + MessagePanel.Children.RemoveAt(MessagePanel.Children.Count - 1); + } + ChatMessage feedbackMsg = new ChatMessage + { + Role = "user", + Content = "[이전 응답에 대한 수정 요청] " + feedback + "\n\n위 피드백을 반영하여 다시 작성해주세요." + }; + lock (_convLock) + { + conv.Messages.Add(feedbackMsg); + } + AddMessageBubble("user", "[수정 요청] " + feedback); + await SendRegenerateAsync(conv); + } + + private async Task SendRegenerateAsync(ChatConversation conv) + { + _isStreaming = true; + BtnSend.IsEnabled = false; + BtnSend.Visibility = Visibility.Collapsed; + BtnStop.Visibility = Visibility.Visible; + _streamCts = new CancellationTokenSource(); + ChatMessage assistantMsg = new ChatMessage + { + Role = "assistant", + Content = "" + }; + lock (_convLock) + { + conv.Messages.Add(assistantMsg); + } + TextBlock streamText; + StackPanel streamContainer = CreateStreamingContainer(out streamText); + MessagePanel.Children.Add(streamContainer); + ForceScrollToEnd(); + StringBuilder sb = new StringBuilder(); + _activeStreamText = streamText; + _cachedStreamContent = ""; + _displayedLength = 0; + _cursorVisible = true; + _aiIconPulseStopped = false; + _cursorTimer.Start(); + _typingTimer.Start(); + _streamStartTime = DateTime.UtcNow; + _elapsedTimer.Start(); + SetStatus("에이전트 작업 중...", spinning: true); + try + { + List sendMessages; + lock (_convLock) + { + sendMessages = conv.Messages.SkipLast(1).ToList(); + } + if (!string.IsNullOrEmpty(conv.SystemCommand)) + { + sendMessages.Insert(0, new ChatMessage + { + Role = "system", + Content = conv.SystemCommand + }); + } + await foreach (string chunk in _llm.StreamAsync(sendMessages, _streamCts.Token)) + { + sb.Append(chunk); + StopAiIconPulse(); + _cachedStreamContent = sb.ToString(); + await ((DispatcherObject)this).Dispatcher.InvokeAsync((Action)delegate + { + }, (DispatcherPriority)4); + } + _cachedStreamContent = sb.ToString(); + assistantMsg.Content = _cachedStreamContent; + DateTime drainStart2 = DateTime.UtcNow; + while (_displayedLength < _cachedStreamContent.Length && (DateTime.UtcNow - drainStart2).TotalMilliseconds < 600.0) + { + await ((DispatcherObject)this).Dispatcher.InvokeAsync((Action)delegate + { + }, (DispatcherPriority)4); + } + } + catch (OperationCanceledException) + { + if (sb.Length == 0) + { + sb.Append("(취소됨)"); + } + assistantMsg.Content = sb.ToString(); + } + catch (Exception ex2) + { + Exception ex3 = ex2; + string errMsg = "⚠ 오류: " + ex3.Message; + sb.Clear(); + sb.Append(errMsg); + assistantMsg.Content = errMsg; + AddRetryButton(); + } + finally + { + _cursorTimer.Stop(); + _elapsedTimer.Stop(); + _typingTimer.Stop(); + HideStickyProgress(); + StopRainbowGlow(); + _activeStreamText = null; + _elapsedLabel = null; + _cachedStreamContent = ""; + _isStreaming = false; + BtnSend.IsEnabled = true; + BtnStop.Visibility = Visibility.Collapsed; + BtnSend.Visibility = Visibility.Visible; + _streamCts?.Dispose(); + _streamCts = null; + SetStatusIdle(); + } + FinalizeStreamingContainer(streamContainer, streamText, assistantMsg.Content, assistantMsg); + AutoScrollIfNeeded(); + try + { + _storage.Save(conv); + } + catch (Exception ex4) + { + LogService.Debug("대화 저장 실패: " + ex4.Message); + } + _tabConversationId[conv.Tab ?? _activeTab] = conv.Id; + RefreshConversationList(); + } + + private double GetMessageMaxWidth() + { + double num = MessageScroll.ActualWidth; + if (num < 100.0) + { + num = 700.0; + } + double value = (num - 180.0) * 0.82; + return Math.Clamp(value, 520.0, 900.0); + } + + private StackPanel CreateStreamingContainer(out TextBlock streamText) + { + double messageMaxWidth = GetMessageMaxWidth(); + StackPanel stackPanel = new StackPanel + { + HorizontalAlignment = System.Windows.HorizontalAlignment.Left, + Width = messageMaxWidth, + MaxWidth = messageMaxWidth, + Margin = new Thickness(40.0, 8.0, 80.0, 8.0), + Opacity = 0.0, + RenderTransform = new TranslateTransform(0.0, 10.0) + }; + stackPanel.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0.0, 1.0, TimeSpan.FromMilliseconds(280.0))); + ((TranslateTransform)stackPanel.RenderTransform).BeginAnimation(TranslateTransform.YProperty, new DoubleAnimation(10.0, 0.0, TimeSpan.FromMilliseconds(300.0)) + { + EasingFunction = new QuadraticEase + { + EasingMode = EasingMode.EaseOut + } + }); + Grid grid = new Grid + { + Margin = new Thickness(0.0, 0.0, 0.0, 4.0) + }; + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = GridLength.Auto + }); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = GridLength.Auto + }); + grid.ColumnDefinitions.Add(new ColumnDefinition + { + Width = new GridLength(1.0, GridUnitType.Star) + }); + TextBlock textBlock = new TextBlock(); + textBlock.Text = "\ue8bd"; + textBlock.FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"); + textBlock.FontSize = 12.0; + textBlock.Foreground = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Blue; + textBlock.VerticalAlignment = VerticalAlignment.Center; + TextBlock textBlock2 = textBlock; + textBlock2.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(1.0, 0.35, TimeSpan.FromMilliseconds(700.0)) + { + AutoReverse = true, + RepeatBehavior = RepeatBehavior.Forever, + EasingFunction = new SineEase() + }); + _activeAiIcon = textBlock2; + Grid.SetColumn(textBlock2, 0); + grid.Children.Add(textBlock2); + string item = GetAgentIdentity().Name; + textBlock = new TextBlock(); + textBlock.Text = item; + textBlock.FontSize = 11.0; + textBlock.FontWeight = FontWeights.SemiBold; + textBlock.Foreground = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Blue; + textBlock.Margin = new Thickness(6.0, 0.0, 0.0, 0.0); + textBlock.VerticalAlignment = VerticalAlignment.Center; + TextBlock element = textBlock; + Grid.SetColumn(element, 1); + grid.Children.Add(element); + _elapsedLabel = new TextBlock + { + Text = "0s", + FontSize = 10.5, + Foreground = ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, + VerticalAlignment = VerticalAlignment.Center, + Opacity = 0.5 + }; + Grid.SetColumn(_elapsedLabel, 2); + grid.Children.Add(_elapsedLabel); + stackPanel.Children.Add(grid); + streamText = new TextBlock + { + Text = "▌", + FontSize = 13.5, + Foreground = ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + TextWrapping = TextWrapping.Wrap, + LineHeight = 22.0 + }; + stackPanel.Children.Add(streamText); + return stackPanel; + } + + private void FinalizeStreamingContainer(StackPanel container, TextBlock streamText, string finalContent, ChatMessage? message = null) + { + //IL_06ca: Unknown result type (might be due to invalid IL or missing references) + container.Children.Remove(streamText); + System.Windows.Media.Brush textColor = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush secondaryColor = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush accentColor = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Blue; + System.Windows.Media.Brush codeBg = (TryFindResource("HintBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.DarkGray; + StackPanel stackPanel = MarkdownRenderer.Render(finalContent, textColor, secondaryColor, accentColor, codeBg); + stackPanel.Margin = new Thickness(0.0, 0.0, 0.0, 4.0); + stackPanel.Opacity = 0.0; + container.Children.Add(stackPanel); + stackPanel.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0.0, 1.0, TimeSpan.FromMilliseconds(180.0))); + System.Windows.Media.Brush brush = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + string capturedContent = finalContent; + StackPanel stackPanel2 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal, + HorizontalAlignment = System.Windows.HorizontalAlignment.Left, + Margin = new Thickness(0.0, 6.0, 0.0, 0.0) + }; + stackPanel2.Children.Add(CreateActionButton("\ue8c8", "복사", brush, delegate + { + try + { + System.Windows.Clipboard.SetText(capturedContent); + } + catch + { + } + })); + stackPanel2.Children.Add(CreateActionButton("\ue72c", "다시 생성", brush, delegate + { + RegenerateLastAsync(); + })); + stackPanel2.Children.Add(CreateActionButton("\ue70f", "수정 후 재시도", brush, delegate + { + ShowRetryWithFeedbackInput(); + })); + AddLinkedFeedbackButtons(stackPanel2, brush, message); + container.Children.Add(stackPanel2); + TimeSpan timeSpan = DateTime.UtcNow - _streamStartTime; + string text = ((timeSpan.TotalSeconds < 60.0) ? $"{timeSpan.TotalSeconds:0.#}s" : $"{(int)timeSpan.TotalMinutes}m {timeSpan.Seconds}s"); + TokenUsage lastTokenUsage = _llm.LastTokenUsage; + string activeTab = _activeTab; + bool flag = ((activeTab == "Cowork" || activeTab == "Code") ? true : false); + bool flag2 = flag; + int num = ((flag2 && _agentCumulativeInputTokens > 0) ? _agentCumulativeInputTokens : (lastTokenUsage?.PromptTokens ?? 0)); + int num2 = ((flag2 && _agentCumulativeOutputTokens > 0) ? _agentCumulativeOutputTokens : (lastTokenUsage?.CompletionTokens ?? 0)); + if (num > 0 || num2 > 0) + { + UpdateStatusTokens(num, num2); + UsageStatisticsService.RecordTokens(num, num2); + } + string text2 = ((num > 0 || num2 > 0) ? $"{FormatTokenCount(num)} + {FormatTokenCount(num2)} = {FormatTokenCount(num + num2)} tokens" : ((!(lastTokenUsage != null)) ? ("~" + FormatTokenCount(EstimateTokenCount(finalContent)) + " tokens") : $"{FormatTokenCount(lastTokenUsage.PromptTokens)} + {FormatTokenCount(lastTokenUsage.CompletionTokens)} = {FormatTokenCount(lastTokenUsage.TotalTokens)} tokens")); + TextBlock textBlock = new TextBlock(); + textBlock.Text = text + " · " + text2; + textBlock.FontSize = 10.5; + textBlock.Foreground = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + textBlock.HorizontalAlignment = System.Windows.HorizontalAlignment.Right; + textBlock.Margin = new Thickness(0.0, 6.0, 0.0, 0.0); + textBlock.Opacity = 0.6; + TextBlock element = textBlock; + container.Children.Add(element); + List<(string, string)> list = ParseSuggestionChips(finalContent); + if (list.Count <= 0) + { + return; + } + WrapPanel wrapPanel = new WrapPanel + { + Margin = new Thickness(0.0, 8.0, 0.0, 4.0), + HorizontalAlignment = System.Windows.HorizontalAlignment.Left + }; + foreach (var item3 in list) + { + string item = item3.Item1; + string item2 = item3.Item2; + Border border = new Border(); + border.Background = (TryFindResource("ItemBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Transparent; + border.BorderBrush = (TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + border.BorderThickness = new Thickness(1.0); + border.CornerRadius = new CornerRadius(16.0); + border.Padding = new Thickness(14.0, 7.0, 14.0, 7.0); + border.Margin = new Thickness(0.0, 0.0, 8.0, 6.0); + border.Cursor = System.Windows.Input.Cursors.Hand; + border.RenderTransformOrigin = new Point(0.5, 0.5); + border.RenderTransform = new ScaleTransform(1.0, 1.0); + Border border2 = border; + border2.Child = new TextBlock + { + Text = item + ". " + item2, + FontSize = 12.5, + Foreground = ((TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White) + }; + System.Windows.Media.Brush chipHover = (TryFindResource("ItemHoverBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Transparent; + System.Windows.Media.Brush chipNormal = (TryFindResource("ItemBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Transparent; + border2.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border { RenderTransform: ScaleTransform renderTransform } border3) + { + renderTransform.ScaleX = 1.02; + renderTransform.ScaleY = 1.02; + border3.Background = chipHover; + } + }; + border2.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border { RenderTransform: ScaleTransform renderTransform } border3) + { + renderTransform.ScaleX = 1.0; + renderTransform.ScaleY = 1.0; + border3.Background = chipNormal; + } + }; + string capturedLabel = item + ". " + item2; + WrapPanel capturedPanel = wrapPanel; + border2.MouseLeftButtonDown += delegate + { + if (capturedPanel.Parent is System.Windows.Controls.Panel panel) + { + panel.Children.Remove(capturedPanel); + } + InputBox.Text = capturedLabel; + SendMessageAsync(); + }; + wrapPanel.Children.Add(border2); + } + container.Children.Add(wrapPanel); + } + + private static List<(string Num, string Label)> ParseSuggestionChips(string content) + { + List<(string, string)> list = new List<(string, string)>(); + if (string.IsNullOrEmpty(content)) + { + return list; + } + string[] array = content.Split('\n'); + List<(string, string)> list2 = new List<(string, string)>(); + int num = -1; + for (int i = 0; i < array.Length; i++) + { + string text = array[i].Trim(); + Match match = Regex.Match(text, "^(\\d+)[.\\)]\\s+(.+)$"); + if (match.Success) + { + if (num < 0 || i == num + list2.Count) + { + if (num < 0) + { + num = i; + list2.Clear(); + } + list2.Add((match.Groups[1].Value, match.Groups[2].Value.TrimEnd())); + } + else + { + num = i; + list2.Clear(); + list2.Add((match.Groups[1].Value, match.Groups[2].Value.TrimEnd())); + } + } + else if (!string.IsNullOrWhiteSpace(text)) + { + num = -1; + list2.Clear(); + } + } + if (list2.Count >= 2 && list2.Count <= 10) + { + list.AddRange(list2); + } + return list; + } + + private static string FormatTokenCount(int count) + { + if (1 == 0) + { + } + string result = ((count >= 1000000) ? $"{(double)count / 1000000.0:0.#}m" : ((count < 1000) ? count.ToString() : $"{(double)count / 1000.0:0.#}k")); + if (1 == 0) + { + } + return result; + } + + private static int EstimateTokenCount(string text) + { + if (string.IsNullOrEmpty(text)) + { + return 0; + } + int num = 0; + foreach (char c in text) + { + if ((c >= '가' && c <= '힣') || (c >= '\u3000' && c <= '鿿')) + { + num++; + } + } + double num2 = ((text.Length > 0) ? ((double)num / (double)text.Length) : 0.0); + double num3 = 4.0 - num2 * 2.0; + return Math.Max(1, (int)Math.Round((double)text.Length / num3)); + } + + private void StopGeneration() + { + _streamCts?.Cancel(); + } + + private void ForkConversation(ChatConversation source, int atIndex) + { + int value = _storage.LoadAllMeta().Count((ChatConversation m) => m.ParentId == source.Id) + 1; + ChatConversation chatConversation = new ChatConversation + { + Title = $"{source.Title} (분기 {value})", + Tab = source.Tab, + Category = source.Category, + WorkFolder = source.WorkFolder, + SystemCommand = source.SystemCommand, + ParentId = source.Id, + BranchLabel = $"분기 {value}", + BranchAtIndex = atIndex + }; + for (int num = 0; num <= atIndex && num < source.Messages.Count; num++) + { + ChatMessage chatMessage = source.Messages[num]; + chatConversation.Messages.Add(new ChatMessage + { + Role = chatMessage.Role, + Content = chatMessage.Content, + Timestamp = chatMessage.Timestamp + }); + } + try + { + _storage.Save(chatConversation); + ShowToast("분기 생성: " + chatConversation.Title); + lock (_convLock) + { + _currentConversation = chatConversation; + } + ChatTitle.Text = chatConversation.Title; + RenderMessages(); + RefreshConversationList(); + } + catch (Exception ex) + { + ShowToast("분기 실패: " + ex.Message, "\ue783"); + } + } + + private void OpenCommandPalette() + { + CommandPaletteWindow commandPaletteWindow = new CommandPaletteWindow(ExecuteCommand) + { + Owner = this + }; + commandPaletteWindow.ShowDialog(); + } + + private void ExecuteCommand(string commandId) + { + switch (commandId) + { + case "tab:chat": + TabChat.IsChecked = true; + break; + case "tab:cowork": + TabCowork.IsChecked = true; + break; + case "tab:code": + if (TabCode.IsEnabled) + { + TabCode.IsChecked = true; + } + break; + case "new_conversation": + StartNewConversation(); + break; + case "search_conversation": + ToggleMessageSearch(); + break; + case "change_model": + BtnModelSelector_Click(this, new RoutedEventArgs()); + break; + case "open_settings": + BtnSettings_Click(this, new RoutedEventArgs()); + break; + case "open_statistics": + new StatisticsWindow().Show(); + break; + case "change_folder": + FolderPathLabel_Click(FolderPathLabel, null); + break; + case "toggle_devmode": + { + LlmSettings llm = _settings.Settings.Llm; + llm.DevMode = !llm.DevMode; + _settings.Save(); + UpdateAnalyzerButtonVisibility(); + ShowToast(llm.DevMode ? "개발자 모드 켜짐" : "개발자 모드 꺼짐"); + break; + } + case "open_audit_log": + try + { + Process.Start("explorer.exe", AuditLogService.GetAuditFolder()); + break; + } + catch + { + break; + } + case "paste_clipboard": + try + { + string text = System.Windows.Clipboard.GetText(); + if (!string.IsNullOrEmpty(text)) + { + InputBox.Text += text; + } + break; + } + catch + { + break; + } + case "export_conversation": + ExportConversation(); + break; + } + } + + private void ExportConversation() + { + ChatConversation currentConversation; + lock (_convLock) + { + currentConversation = _currentConversation; + } + if (currentConversation == null || currentConversation.Messages.Count == 0) + { + return; + } + Microsoft.Win32.SaveFileDialog saveFileDialog = new Microsoft.Win32.SaveFileDialog + { + FileName = (currentConversation.Title ?? ""), + DefaultExt = ".md", + Filter = "Markdown (*.md)|*.md|JSON (*.json)|*.json|HTML (*.html)|*.html|PDF 인쇄용 HTML (*.pdf.html)|*.pdf.html|Text (*.txt)|*.txt" + }; + if (saveFileDialog.ShowDialog() != true) + { + return; + } + string text = System.IO.Path.GetExtension(saveFileDialog.FileName).ToLowerInvariant(); + string contents; + if (text == ".json") + { + contents = JsonSerializer.Serialize(currentConversation, new JsonSerializerOptions + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }); + } + else + { + if (saveFileDialog.FileName.EndsWith(".pdf.html")) + { + contents = PdfExportService.BuildHtml(currentConversation); + File.WriteAllText(saveFileDialog.FileName, contents, Encoding.UTF8); + PdfExportService.OpenInBrowser(saveFileDialog.FileName); + ShowToast("PDF 인쇄용 HTML이 생성되어 브라우저에서 열렸습니다"); + return; + } + if (text == ".html") + { + contents = ExportToHtml(currentConversation); + } + else + { + StringBuilder stringBuilder = new StringBuilder(); + StringBuilder stringBuilder2 = stringBuilder; + StringBuilder stringBuilder3 = stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(2, 1, stringBuilder2); + handler.AppendLiteral("# "); + handler.AppendFormatted(currentConversation.Title); + stringBuilder3.AppendLine(ref handler); + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder4 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(13, 2, stringBuilder2); + handler.AppendLiteral("_생성: "); + handler.AppendFormatted(currentConversation.CreatedAt, "yyyy-MM-dd HH:mm"); + handler.AppendLiteral(" · 주제: "); + handler.AppendFormatted(currentConversation.Category); + handler.AppendLiteral("_"); + stringBuilder4.AppendLine(ref handler); + stringBuilder.AppendLine(); + foreach (ChatMessage message in currentConversation.Messages) + { + if (!(message.Role == "system")) + { + string value = ((message.Role == "user") ? "**사용자**" : "**AI**"); + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder5 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(3, 2, stringBuilder2); + handler.AppendFormatted(value); + handler.AppendLiteral(" ("); + handler.AppendFormatted(message.Timestamp, "HH:mm"); + handler.AppendLiteral(")"); + stringBuilder5.AppendLine(ref handler); + stringBuilder.AppendLine(); + stringBuilder.AppendLine(message.Content); + List attachedFiles = message.AttachedFiles; + if (attachedFiles != null && attachedFiles.Count > 0) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine("_첨부 파일: " + string.Join(", ", message.AttachedFiles.Select(System.IO.Path.GetFileName)) + "_"); + } + stringBuilder.AppendLine(); + stringBuilder.AppendLine("---"); + stringBuilder.AppendLine(); + } + } + contents = stringBuilder.ToString(); + } + } + File.WriteAllText(saveFileDialog.FileName, contents, Encoding.UTF8); + } + + private static string ExportToHtml(ChatConversation conv) + { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.AppendLine(""); + StringBuilder stringBuilder2 = stringBuilder; + StringBuilder stringBuilder3 = stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(15, 1, stringBuilder2); + handler.AppendLiteral(""); + handler.AppendFormatted(WebUtility.HtmlEncode(conv.Title)); + handler.AppendLiteral(""); + stringBuilder3.AppendLine(ref handler); + stringBuilder.AppendLine(""); + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder4 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder2); + handler.AppendLiteral("

"); + handler.AppendFormatted(WebUtility.HtmlEncode(conv.Title)); + handler.AppendLiteral("

"); + stringBuilder4.AppendLine(ref handler); + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder5 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(55, 2, stringBuilder2); + handler.AppendLiteral("

생성: "); + handler.AppendFormatted(conv.CreatedAt, "yyyy-MM-dd HH:mm"); + handler.AppendLiteral(" · 주제: "); + handler.AppendFormatted(conv.Category); + handler.AppendLiteral("

"); + stringBuilder5.AppendLine(ref handler); + foreach (ChatMessage message in conv.Messages) + { + if (!(message.Role == "system")) + { + string value = ((message.Role == "user") ? "user" : "ai"); + string value2 = ((message.Role == "user") ? "사용자" : "AI"); + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder6 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(18, 1, stringBuilder2); + handler.AppendLiteral("
"); + stringBuilder6.AppendLine(ref handler); + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder7 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(27, 2, stringBuilder2); + handler.AppendLiteral("
"); + handler.AppendFormatted(value2); + handler.AppendLiteral(" · "); + handler.AppendFormatted(message.Timestamp, "HH:mm"); + handler.AppendLiteral("
"); + stringBuilder7.AppendLine(ref handler); + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder8 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(27, 1, stringBuilder2); + handler.AppendLiteral("
"); + handler.AppendFormatted(WebUtility.HtmlEncode(message.Content)); + handler.AppendLiteral("
"); + stringBuilder8.AppendLine(ref handler); + stringBuilder.AppendLine("
"); + } + } + stringBuilder.AppendLine(""); + return stringBuilder.ToString(); + } + + private void ChatWindow_KeyDown(object sender, System.Windows.Input.KeyEventArgs e) + { + //IL_0001: Unknown result type (might be due to invalid IL or missing references) + //IL_0006: Unknown result type (might be due to invalid IL or missing references) + //IL_0007: Unknown result type (might be due to invalid IL or missing references) + //IL_0009: Invalid comparison between Unknown and I4 + //IL_01b6: Unknown result type (might be due to invalid IL or missing references) + //IL_01b8: Invalid comparison between Unknown and I4 + //IL_0014: Unknown result type (might be due to invalid IL or missing references) + //IL_0019: Unknown result type (might be due to invalid IL or missing references) + //IL_001a: Unknown result type (might be due to invalid IL or missing references) + //IL_001b: Unknown result type (might be due to invalid IL or missing references) + //IL_001c: Unknown result type (might be due to invalid IL or missing references) + //IL_001f: Invalid comparison between Unknown and I4 + //IL_02d1: Unknown result type (might be due to invalid IL or missing references) + //IL_02d8: Invalid comparison between Unknown and I4 + //IL_01c5: Unknown result type (might be due to invalid IL or missing references) + //IL_01ca: Unknown result type (might be due to invalid IL or missing references) + //IL_01cc: Unknown result type (might be due to invalid IL or missing references) + //IL_01ce: Unknown result type (might be due to invalid IL or missing references) + //IL_01d0: Unknown result type (might be due to invalid IL or missing references) + //IL_01d4: Invalid comparison between Unknown and I4 + //IL_005a: Unknown result type (might be due to invalid IL or missing references) + //IL_005d: Unknown result type (might be due to invalid IL or missing references) + //IL_006f: Expected I4, but got Unknown + //IL_0021: Unknown result type (might be due to invalid IL or missing references) + //IL_0024: Unknown result type (might be due to invalid IL or missing references) + //IL_0036: Expected I4, but got Unknown + //IL_01ec: Unknown result type (might be due to invalid IL or missing references) + //IL_01f0: Invalid comparison between Unknown and I4 + //IL_01d6: Unknown result type (might be due to invalid IL or missing references) + //IL_01da: Invalid comparison between Unknown and I4 + //IL_033c: Unknown result type (might be due to invalid IL or missing references) + //IL_0343: Invalid comparison between Unknown and I4 + //IL_0071: Unknown result type (might be due to invalid IL or missing references) + //IL_0074: Invalid comparison between Unknown and I4 + //IL_0038: Unknown result type (might be due to invalid IL or missing references) + //IL_003b: Unknown result type (might be due to invalid IL or missing references) + //IL_0055: Expected I4, but got Unknown + //IL_036c: Unknown result type (might be due to invalid IL or missing references) + //IL_0373: Invalid comparison between Unknown and I4 + //IL_01f7: Unknown result type (might be due to invalid IL or missing references) + //IL_01fb: Invalid comparison between Unknown and I4 + //IL_01de: Unknown result type (might be due to invalid IL or missing references) + //IL_01e2: Invalid comparison between Unknown and I4 + //IL_0391: Unknown result type (might be due to invalid IL or missing references) + //IL_0398: Invalid comparison between Unknown and I4 + //IL_0078: Unknown result type (might be due to invalid IL or missing references) + //IL_007e: Invalid comparison between Unknown and I4 + //IL_03b6: Unknown result type (might be due to invalid IL or missing references) + //IL_03bc: Invalid comparison between Unknown and I4 + ModifierKeys modifiers = Keyboard.Modifiers; + if ((int)modifiers == 2) + { + Key key = e.Key; + Key val = key; + if ((int)val <= 49) + { + switch (val - 35) + { + default: + switch (val - 45) + { + case 3: + ExportConversation(); + e.Handled = true; + break; + case 0: + BtnToggleSidebar_Click(this, new RoutedEventArgs()); + e.Handled = true; + break; + case 4: + ToggleMessageSearch(); + e.Handled = true; + break; + } + break; + case 0: + TabChat.IsChecked = true; + e.Handled = true; + break; + case 1: + TabCowork.IsChecked = true; + e.Handled = true; + break; + case 2: + if (TabCode.IsEnabled) + { + TabCode.IsChecked = true; + } + e.Handled = true; + break; + } + } + else + { + switch (val - 55) + { + default: + if ((int)val != 66) + { + if ((int)val == 142) + { + BtnSettings_Click(this, new RoutedEventArgs()); + e.Handled = true; + } + } + else + { + Close(); + e.Handled = true; + } + break; + case 2: + BtnNewChat_Click(this, new RoutedEventArgs()); + e.Handled = true; + break; + case 0: + InputBox.Text = ""; + InputBox.Focus(); + e.Handled = true; + break; + case 1: + BtnModelSelector_Click(this, new RoutedEventArgs()); + e.Handled = true; + break; + } + } + } + if ((int)modifiers == 6) + { + Key key2 = e.Key; + Key val2 = key2; + if ((int)val2 <= 47) + { + if ((int)val2 != 46) + { + if ((int)val2 == 47) + { + BtnDeleteAll_Click(this, new RoutedEventArgs()); + e.Handled = true; + } + } + else + { + ChatConversation currentConversation; + lock (_convLock) + { + currentConversation = _currentConversation; + } + if (currentConversation != null) + { + ChatMessage chatMessage = currentConversation.Messages.LastOrDefault((ChatMessage m) => m.Role == "assistant"); + if (chatMessage != null) + { + try + { + System.Windows.Clipboard.SetText(chatMessage.Content); + } + catch + { + } + } + } + e.Handled = true; + } + } + else if ((int)val2 != 59) + { + if ((int)val2 == 61) + { + RegenerateLastAsync(); + e.Handled = true; + } + } + else + { + OpenCommandPalette(); + e.Handled = true; + } + } + if ((int)e.Key == 13) + { + if (MessageSearchBar.Visibility == Visibility.Visible) + { + CloseMessageSearch(); + e.Handled = true; + } + else if (_isStreaming) + { + StopGeneration(); + e.Handled = true; + } + } + if (SlashPopup.IsOpen) + { + if ((int)e.Key == 13) + { + SlashPopup.IsOpen = false; + _slashSelectedIndex = -1; + e.Handled = true; + } + else if ((int)e.Key == 24) + { + SlashPopup_ScrollByDelta(120); + e.Handled = true; + } + else if ((int)e.Key == 26) + { + SlashPopup_ScrollByDelta(-120); + e.Handled = true; + } + else if ((int)e.Key == 6 && _slashSelectedIndex >= 0) + { + e.Handled = true; + ExecuteSlashSelectedItem(); + } + } + } + + private void BtnStop_Click(object sender, RoutedEventArgs e) + { + StopGeneration(); + } + + private void BtnPause_Click(object sender, MouseButtonEventArgs e) + { + if (_agentLoop.IsPaused) + { + _agentLoop.Resume(); + PauseIcon.Text = "\ue769"; + BtnPause.ToolTip = "일시정지"; + } + else + { + _agentLoop.PauseAsync(); + PauseIcon.Text = "\ue768"; + BtnPause.ToolTip = "재개"; + } + } + + private void BtnExport_Click(object sender, RoutedEventArgs e) + { + ExportConversation(); + } + + private void ToggleMessageSearch() + { + if (MessageSearchBar.Visibility == Visibility.Visible) + { + CloseMessageSearch(); + return; + } + MessageSearchBar.Visibility = Visibility.Visible; + SearchTextBox.Focus(); + SearchTextBox.SelectAll(); + } + + private void CloseMessageSearch() + { + MessageSearchBar.Visibility = Visibility.Collapsed; + SearchTextBox.Text = ""; + SearchResultCount.Text = ""; + _searchMatchIndices.Clear(); + _searchCurrentIndex = -1; + ClearSearchHighlights(); + } + + private void SearchTextBox_TextChanged(object sender, TextChangedEventArgs e) + { + string value = SearchTextBox.Text.Trim(); + if (string.IsNullOrEmpty(value)) + { + SearchResultCount.Text = ""; + _searchMatchIndices.Clear(); + _searchCurrentIndex = -1; + ClearSearchHighlights(); + return; + } + ChatConversation currentConversation; + lock (_convLock) + { + currentConversation = _currentConversation; + } + if (currentConversation == null) + { + return; + } + _searchMatchIndices.Clear(); + for (int i = 0; i < currentConversation.Messages.Count; i++) + { + if (currentConversation.Messages[i].Content.Contains(value, StringComparison.OrdinalIgnoreCase)) + { + _searchMatchIndices.Add(i); + } + } + if (_searchMatchIndices.Count > 0) + { + _searchCurrentIndex = 0; + SearchResultCount.Text = $"1/{_searchMatchIndices.Count}"; + HighlightSearchResult(); + } + else + { + _searchCurrentIndex = -1; + SearchResultCount.Text = "결과 없음"; + } + } + + private void SearchPrev_Click(object sender, RoutedEventArgs e) + { + if (_searchMatchIndices.Count != 0) + { + _searchCurrentIndex = (_searchCurrentIndex - 1 + _searchMatchIndices.Count) % _searchMatchIndices.Count; + SearchResultCount.Text = $"{_searchCurrentIndex + 1}/{_searchMatchIndices.Count}"; + HighlightSearchResult(); + } + } + + private void SearchNext_Click(object sender, RoutedEventArgs e) + { + if (_searchMatchIndices.Count != 0) + { + _searchCurrentIndex = (_searchCurrentIndex + 1) % _searchMatchIndices.Count; + SearchResultCount.Text = $"{_searchCurrentIndex + 1}/{_searchMatchIndices.Count}"; + HighlightSearchResult(); + } + } + + private void SearchClose_Click(object sender, RoutedEventArgs e) + { + CloseMessageSearch(); + } + + private void HighlightSearchResult() + { + if (_searchCurrentIndex < 0 || _searchCurrentIndex >= _searchMatchIndices.Count) + { + return; + } + int num = _searchMatchIndices[_searchCurrentIndex]; + if (num < MessagePanel.Children.Count) + { + if (MessagePanel.Children[num] is FrameworkElement frameworkElement) + { + frameworkElement.BringIntoView(); + } + } + else if (MessagePanel.Children.Count > 0) + { + UIElementCollection children = MessagePanel.Children; + (children[children.Count - 1] as FrameworkElement)?.BringIntoView(); + } + } + + private void ClearSearchHighlights() + { + } + + private void AddRetryButton() + { + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + Border border = new Border + { + Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(24, 239, 68, 68)), + CornerRadius = new CornerRadius(8.0), + Padding = new Thickness(12.0, 8.0, 12.0, 8.0), + Margin = new Thickness(40.0, 4.0, 80.0, 4.0), + HorizontalAlignment = System.Windows.HorizontalAlignment.Left, + Cursor = System.Windows.Input.Cursors.Hand + }; + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel.Children.Add(new TextBlock + { + Text = "\ue72c", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 12.0, + Foreground = new SolidColorBrush(System.Windows.Media.Color.FromRgb(239, 68, 68)), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 6.0, 0.0) + }); + stackPanel.Children.Add(new TextBlock + { + Text = "재시도", + FontSize = 12.0, + FontWeight = FontWeights.SemiBold, + Foreground = new SolidColorBrush(System.Windows.Media.Color.FromRgb(239, 68, 68)), + VerticalAlignment = VerticalAlignment.Center + }); + border.Child = stackPanel; + border.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border2) + { + border2.Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(48, 239, 68, 68)); + } + }; + border.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border2) + { + border2.Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(24, 239, 68, 68)); + } + }; + border.MouseLeftButtonUp += delegate + { + lock (_convLock) + { + if (_currentConversation != null) + { + int num = _currentConversation.Messages.Count - 1; + if (num >= 0 && _currentConversation.Messages[num].Role == "assistant") + { + _currentConversation.Messages.RemoveAt(num); + } + } + } + RegenerateLastAsync(); + }; + MessagePanel.Children.Add(border); + ForceScrollToEnd(); + }); + } + + private void ShowMessageContextMenu(string content, string role) + { + ContextMenu menu = CreateThemedContextMenu(); + System.Windows.Media.Brush primaryText = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush secondaryText = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + AddItem("\ue8c8", "텍스트 복사", delegate + { + try + { + System.Windows.Clipboard.SetText(content); + ShowToast("복사되었습니다"); + } + catch + { + } + }); + AddItem("\ue943", "마크다운 복사", delegate + { + try + { + System.Windows.Clipboard.SetText(content); + ShowToast("마크다운으로 복사됨"); + } + catch + { + } + }); + AddItem("\ue97a", "인용하여 답장", delegate + { + string text = ((content.Length > 200) ? (content.Substring(0, 200) + "...") : content); + string[] source = text.Split('\n'); + string text2 = string.Join("\n", source.Select((string l) => "> " + l)); + InputBox.Text = text2 + "\n\n"; + InputBox.Focus(); + InputBox.CaretIndex = InputBox.Text.Length; + }); + menu.Items.Add(new Separator()); + if (role == "assistant") + { + AddItem("\ue72c", "응답 재생성", delegate + { + RegenerateLastAsync(); + }); + } + AddItem("\ue8a5", "여기서 분기", delegate + { + ChatConversation currentConversation; + lock (_convLock) + { + currentConversation = _currentConversation; + } + if (currentConversation != null) + { + int num = currentConversation.Messages.FindLastIndex((ChatMessage m) => m.Role == role && m.Content == content); + if (num >= 0) + { + ForkConversation(currentConversation, num); + } + } + }); + menu.Items.Add(new Separator()); + string msgContent = content; + string msgRole = role; + AddItem("\ue74d", "이후 메시지 모두 삭제", delegate + { + ChatConversation currentConversation; + lock (_convLock) + { + currentConversation = _currentConversation; + } + if (currentConversation != null) + { + int num = currentConversation.Messages.FindLastIndex((ChatMessage m) => m.Role == msgRole && m.Content == msgContent); + if (num >= 0) + { + int num2 = currentConversation.Messages.Count - num; + if (System.Windows.MessageBox.Show($"이 메시지 포함 {num2}개 메시지를 삭제하시겠습니까?", "메시지 삭제", MessageBoxButton.YesNo, MessageBoxImage.Exclamation) == MessageBoxResult.Yes) + { + currentConversation.Messages.RemoveRange(num, num2); + try + { + _storage.Save(currentConversation); + } + catch (Exception ex) + { + LogService.Debug("대화 저장 실패: " + ex.Message); + } + RenderMessages(); + ShowToast($"{num2}개 메시지 삭제됨"); + } + } + } + }); + menu.IsOpen = true; + void AddItem(string icon, string label, Action action) + { + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel.Children.Add(new TextBlock + { + Text = icon, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 12.0, + Foreground = secondaryText, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 8.0, 0.0) + }); + stackPanel.Children.Add(new TextBlock + { + Text = label, + FontSize = 12.0, + Foreground = primaryText, + VerticalAlignment = VerticalAlignment.Center + }); + MenuItem menuItem = new MenuItem + { + Header = stackPanel, + Padding = new Thickness(8.0, 6.0, 16.0, 6.0) + }; + menuItem.Click += delegate + { + action(); + }; + menu.Items.Add(menuItem); + } + } + + private void ShowRandomTip() + { + if (_settings.Settings.Llm.ShowTips && (!(_activeTab != "Cowork") || !(_activeTab != "Code"))) + { + string message = Tips[_tipIndex % Tips.Length]; + _tipIndex++; + ShowTip(message); + } + } + + private void ShowTip(string message) + { + //IL_009d: Unknown result type (might be due to invalid IL or missing references) + //IL_00a2: Unknown result type (might be due to invalid IL or missing references) + //IL_00b5: Expected O, but got Unknown + DispatcherTimer? tipDismissTimer = _tipDismissTimer; + if (tipDismissTimer != null) + { + tipDismissTimer.Stop(); + } + ToastText.Text = message; + ToastIcon.Text = "\ue82f"; + ToastBorder.Visibility = Visibility.Visible; + ToastBorder.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0.0, 1.0, TimeSpan.FromMilliseconds(300.0))); + int tipDurationSeconds = _settings.Settings.Llm.TipDurationSeconds; + if (tipDurationSeconds <= 0) + { + return; + } + _tipDismissTimer = new DispatcherTimer + { + Interval = TimeSpan.FromSeconds(tipDurationSeconds) + }; + _tipDismissTimer.Tick += delegate + { + _tipDismissTimer.Stop(); + DoubleAnimation doubleAnimation = new DoubleAnimation(1.0, 0.0, TimeSpan.FromMilliseconds(300.0)); + doubleAnimation.Completed += delegate + { + ToastBorder.Visibility = Visibility.Collapsed; + }; + ToastBorder.BeginAnimation(UIElement.OpacityProperty, doubleAnimation); + }; + _tipDismissTimer.Start(); + } + + private static string LoadProjectContext(string workFolder) + { + if (string.IsNullOrEmpty(workFolder)) + { + return ""; + } + List list = new List(); + string text = workFolder; + for (int i = 0; i < 3; i++) + { + if (string.IsNullOrEmpty(text)) + { + break; + } + list.Add(text); + text = Directory.GetParent(text)?.FullName; + } + list.Reverse(); + HashSet seen = new HashSet(StringComparer.OrdinalIgnoreCase); + List orderedFiles = new List(); + foreach (string item in list) + { + AddIfExists(System.IO.Path.Combine(item, "AX.md")); + AddIfExists(System.IO.Path.Combine(item, "AX.local.md")); + AddIfExists(System.IO.Path.Combine(item, ".ax", "AX.md")); + string path = System.IO.Path.Combine(item, ".ax", "rules"); + if (!Directory.Exists(path)) + { + continue; + } + foreach (string item2 in Directory.GetFiles(path, "*.md").OrderBy((string p) => p, StringComparer.OrdinalIgnoreCase)) + { + if (seen.Add(item2)) + { + orderedFiles.Add(item2); + } + } + } + if (orderedFiles.Count == 0) + { + return ""; + } + StringBuilder stringBuilder = new StringBuilder(); + foreach (string item3 in orderedFiles) + { + try + { + string text2 = File.ReadAllText(item3); + if (!string.IsNullOrWhiteSpace(text2)) + { + if (text2.Length > 6000) + { + text2 = text2.Substring(0, 6000) + "\n... (truncated)"; + } + string fileName = System.IO.Path.GetFileName(item3); + string value = (item3.StartsWith(workFolder, StringComparison.OrdinalIgnoreCase) ? System.IO.Path.GetRelativePath(workFolder, item3) : item3); + StringBuilder stringBuilder2 = stringBuilder; + StringBuilder stringBuilder3 = stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(22, 1, stringBuilder2); + handler.AppendLiteral("\n## Project Context ("); + handler.AppendFormatted(fileName); + handler.AppendLiteral(")"); + stringBuilder3.AppendLine(ref handler); + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder4 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(8, 1, stringBuilder2); + handler.AppendLiteral("Source: "); + handler.AppendFormatted(value); + stringBuilder4.AppendLine(ref handler); + stringBuilder.AppendLine(text2); + } + } + catch + { + } + } + return stringBuilder.ToString(); + void AddIfExists(string text3) + { + if (File.Exists(text3) && seen.Add(text3)) + { + orderedFiles.Add(text3); + } + } + } + + private void PlayRainbowGlow() + { + //IL_007b: Unknown result type (might be due to invalid IL or missing references) + //IL_0080: Unknown result type (might be due to invalid IL or missing references) + //IL_009a: Expected O, but got Unknown + if (!_settings.Settings.Llm.EnableChatRainbowGlow) + { + return; + } + DispatcherTimer? rainbowTimer = _rainbowTimer; + if (rainbowTimer != null) + { + rainbowTimer.Stop(); + } + _rainbowStartTime = DateTime.UtcNow; + InputGlowBorder.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0.0, 0.9, TimeSpan.FromMilliseconds(150.0))); + _rainbowTimer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(16.0) + }; + _rainbowTimer.Tick += delegate + { + //IL_009c: Unknown result type (might be due to invalid IL or missing references) + //IL_00dc: Unknown result type (might be due to invalid IL or missing references) + double totalMilliseconds = (DateTime.UtcNow - _rainbowStartTime).TotalMilliseconds; + double num = totalMilliseconds / 1500.0 % 1.0; + if (InputGlowBorder.BorderBrush is LinearGradientBrush linearGradientBrush) + { + double num2 = num * Math.PI * 2.0; + linearGradientBrush.StartPoint = new Point(0.5 + 0.5 * Math.Cos(num2), 0.5 + 0.5 * Math.Sin(num2)); + linearGradientBrush.EndPoint = new Point(0.5 - 0.5 * Math.Cos(num2), 0.5 - 0.5 * Math.Sin(num2)); + } + }; + _rainbowTimer.Start(); + } + + private void StopRainbowGlow() + { + DispatcherTimer? rainbowTimer = _rainbowTimer; + if (rainbowTimer != null) + { + rainbowTimer.Stop(); + } + _rainbowTimer = null; + if (InputGlowBorder.Opacity > 0.0) + { + DoubleAnimation doubleAnimation = new DoubleAnimation(InputGlowBorder.Opacity, 0.0, TimeSpan.FromMilliseconds(600.0)); + doubleAnimation.Completed += delegate + { + InputGlowBorder.Opacity = 0.0; + }; + InputGlowBorder.BeginAnimation(UIElement.OpacityProperty, doubleAnimation); + } + } + + private void ShowToast(string message, string icon = "\ue73e", int durationMs = 2000) + { + //IL_0076: Unknown result type (might be due to invalid IL or missing references) + //IL_007b: Unknown result type (might be due to invalid IL or missing references) + //IL_008e: Expected O, but got Unknown + DispatcherTimer? toastHideTimer = _toastHideTimer; + if (toastHideTimer != null) + { + toastHideTimer.Stop(); + } + ToastText.Text = message; + ToastIcon.Text = icon; + ToastBorder.Visibility = Visibility.Visible; + ToastBorder.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0.0, 1.0, TimeSpan.FromMilliseconds(200.0))); + _toastHideTimer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(durationMs) + }; + _toastHideTimer.Tick += delegate + { + _toastHideTimer.Stop(); + DoubleAnimation doubleAnimation = new DoubleAnimation(1.0, 0.0, TimeSpan.FromMilliseconds(300.0)); + doubleAnimation.Completed += delegate + { + ToastBorder.Visibility = Visibility.Collapsed; + }; + ToastBorder.BeginAnimation(UIElement.OpacityProperty, doubleAnimation); + }; + _toastHideTimer.Start(); + } + + private void BuildTopicButtons() + { + //IL_022e: Unknown result type (might be due to invalid IL or missing references) + //IL_07bc: Unknown result type (might be due to invalid IL or missing references) + //IL_0bd7: Unknown result type (might be due to invalid IL or missing references) + TopicButtonPanel.Children.Clear(); + TopicButtonPanel.Visibility = Visibility.Visible; + if (_activeTab == "Cowork" || _activeTab == "Code") + { + if (EmptyStateTitle != null) + { + EmptyStateTitle.Text = "작업 유형을 선택하세요"; + } + if (EmptyStateDesc != null) + { + EmptyStateDesc.Text = ((_activeTab == "Code") ? "코딩 에이전트가 코드 분석, 수정, 빌드, 테스트를 수행합니다" : "에이전트가 상세한 데이터를 작성합니다"); + } + } + else + { + if (EmptyStateTitle != null) + { + EmptyStateTitle.Text = "대화 주제를 선택하세요"; + } + if (EmptyStateDesc != null) + { + EmptyStateDesc.Text = "주제에 맞는 전문 프리셋이 자동 적용됩니다"; + } + } + IReadOnlyList byTabWithCustom = PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets); + Border border; + foreach (TopicPreset item in byTabWithCustom) + { + TopicPreset capturedPreset = item; + SolidColorBrush solidColorBrush = BrushFromHex(item.Color); + border = new Border(); + border.Background = (TryFindResource("ItemBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Transparent; + border.CornerRadius = new CornerRadius(14.0); + border.Padding = new Thickness(14.0, 12.0, 14.0, 12.0); + border.Margin = new Thickness(4.0, 4.0, 4.0, 8.0); + border.Cursor = System.Windows.Input.Cursors.Hand; + border.Width = 120.0; + border.Height = 105.0; + border.ClipToBounds = true; + border.RenderTransformOrigin = new Point(0.5, 0.5); + border.RenderTransform = new ScaleTransform(1.0, 1.0); + Border border2 = border; + StackPanel stackPanel = new StackPanel + { + HorizontalAlignment = System.Windows.HorizontalAlignment.Center + }; + Border border3 = new Border + { + Width = 40.0, + Height = 40.0, + CornerRadius = new CornerRadius(20.0), + Background = new SolidColorBrush(solidColorBrush.Color) + { + Opacity = 0.15 + }, + HorizontalAlignment = System.Windows.HorizontalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 0.0, 10.0) + }; + TextBlock child = new TextBlock + { + Text = item.Symbol, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 18.0, + Foreground = solidColorBrush, + HorizontalAlignment = System.Windows.HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + border3.Child = child; + stackPanel.Children.Add(border3); + stackPanel.Children.Add(new TextBlock + { + Text = item.Label, + FontSize = 13.0, + FontWeight = FontWeights.SemiBold, + Foreground = ((TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White), + HorizontalAlignment = System.Windows.HorizontalAlignment.Center + }); + stackPanel.Children.Add(new TextBlock + { + Text = item.Description, + FontSize = 9.0, + TextWrapping = TextWrapping.Wrap, + TextTrimming = TextTrimming.CharacterEllipsis, + MaxHeight = 28.0, + Foreground = ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + HorizontalAlignment = System.Windows.HorizontalAlignment.Center, + Margin = new Thickness(0.0, 2.0, 0.0, 0.0), + TextAlignment = TextAlignment.Center + }); + if (capturedPreset.IsCustom) + { + Grid grid = new Grid(); + grid.Children.Add(stackPanel); + Border border4 = new Border + { + Width = 16.0, + Height = 16.0, + CornerRadius = new CornerRadius(4.0), + Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(96, byte.MaxValue, byte.MaxValue, byte.MaxValue)), + HorizontalAlignment = System.Windows.HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top, + Margin = new Thickness(2.0, 2.0, 0.0, 0.0), + ToolTip = "커스텀 프리셋" + }; + border4.Child = new TextBlock + { + Text = "\ue710", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 8.0, + Foreground = solidColorBrush, + HorizontalAlignment = System.Windows.HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + grid.Children.Add(border4); + border2.Child = grid; + } + else + { + border2.Child = stackPanel; + } + System.Windows.Media.Brush hoverBg = (TryFindResource("ItemHoverBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Transparent; + System.Windows.Media.Brush normalBg = (TryFindResource("ItemBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Transparent; + border2.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border { RenderTransform: ScaleTransform renderTransform } border8) + { + renderTransform.ScaleX = 1.03; + renderTransform.ScaleY = 1.03; + border8.Background = hoverBg; + } + }; + border2.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border { RenderTransform: ScaleTransform renderTransform } border8) + { + renderTransform.ScaleX = 1.0; + renderTransform.ScaleY = 1.0; + border8.Background = normalBg; + } + }; + border2.MouseLeftButtonDown += delegate + { + SelectTopic(capturedPreset); + }; + if (capturedPreset.IsCustom) + { + border2.MouseRightButtonUp += delegate(object s, MouseButtonEventArgs e) + { + e.Handled = true; + ShowCustomPresetContextMenu(s as Border, capturedPreset); + }; + } + TopicButtonPanel.Children.Add(border2); + } + SolidColorBrush solidColorBrush2 = BrushFromHex("#6B7280"); + border = new Border(); + border.Background = (TryFindResource("ItemBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Transparent; + border.CornerRadius = new CornerRadius(14.0); + border.Padding = new Thickness(14.0, 12.0, 14.0, 12.0); + border.Margin = new Thickness(4.0, 4.0, 4.0, 8.0); + border.Cursor = System.Windows.Input.Cursors.Hand; + border.Width = 120.0; + border.Height = 105.0; + border.ClipToBounds = true; + border.RenderTransformOrigin = new Point(0.5, 0.5); + border.RenderTransform = new ScaleTransform(1.0, 1.0); + Border border5 = border; + StackPanel stackPanel2 = new StackPanel + { + HorizontalAlignment = System.Windows.HorizontalAlignment.Center + }; + Border border6 = new Border + { + Width = 40.0, + Height = 40.0, + CornerRadius = new CornerRadius(20.0), + Background = new SolidColorBrush(solidColorBrush2.Color) + { + Opacity = 0.15 + }, + HorizontalAlignment = System.Windows.HorizontalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 0.0, 10.0) + }; + border6.Child = new TextBlock + { + Text = "\ue70f", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 18.0, + Foreground = solidColorBrush2, + HorizontalAlignment = System.Windows.HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + stackPanel2.Children.Add(border6); + stackPanel2.Children.Add(new TextBlock + { + Text = "기타", + FontSize = 13.0, + FontWeight = FontWeights.SemiBold, + Foreground = ((TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White), + HorizontalAlignment = System.Windows.HorizontalAlignment.Center + }); + stackPanel2.Children.Add(new TextBlock + { + Text = "프리셋 없이 자유롭게 대화합니다", + FontSize = 9.0, + TextWrapping = TextWrapping.Wrap, + TextTrimming = TextTrimming.CharacterEllipsis, + MaxHeight = 28.0, + Foreground = ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + HorizontalAlignment = System.Windows.HorizontalAlignment.Center, + Margin = new Thickness(0.0, 2.0, 0.0, 0.0), + TextAlignment = TextAlignment.Center + }); + border5.Child = stackPanel2; + System.Windows.Media.Brush hoverBg2 = (TryFindResource("ItemHoverBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Transparent; + System.Windows.Media.Brush normalBg2 = (TryFindResource("ItemBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Transparent; + border5.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border { RenderTransform: ScaleTransform renderTransform } border8) + { + renderTransform.ScaleX = 1.03; + renderTransform.ScaleY = 1.03; + border8.Background = hoverBg2; + } + }; + border5.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border { RenderTransform: ScaleTransform renderTransform } border8) + { + renderTransform.ScaleX = 1.0; + renderTransform.ScaleY = 1.0; + border8.Background = normalBg2; + } + }; + border5.MouseLeftButtonDown += delegate + { + EmptyState.Visibility = Visibility.Collapsed; + InputBox.Focus(); + }; + TopicButtonPanel.Children.Add(border5); + SolidColorBrush solidColorBrush3 = BrushFromHex("#6366F1"); + border = new Border(); + border.Background = System.Windows.Media.Brushes.Transparent; + border.CornerRadius = new CornerRadius(14.0); + border.Padding = new Thickness(14.0, 12.0, 14.0, 12.0); + border.Margin = new Thickness(4.0, 4.0, 4.0, 8.0); + border.Cursor = System.Windows.Input.Cursors.Hand; + border.Width = 120.0; + border.Height = 105.0; + border.ClipToBounds = true; + border.BorderBrush = (TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + border.BorderThickness = new Thickness(1.5); + border.RenderTransformOrigin = new Point(0.5, 0.5); + border.RenderTransform = new ScaleTransform(1.0, 1.0); + Border border7 = border; + if (border7.BorderBrush is SolidColorBrush brush) + { + System.Windows.Media.Pen pen = new System.Windows.Media.Pen(brush, 1.5) + { + DashStyle = DashStyles.Dash + }; + } + StackPanel stackPanel3 = new StackPanel + { + HorizontalAlignment = System.Windows.HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + TextBlock textBlock = new TextBlock(); + textBlock.Text = "\ue710"; + textBlock.FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"); + textBlock.FontSize = 24.0; + textBlock.Foreground = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + textBlock.HorizontalAlignment = System.Windows.HorizontalAlignment.Center; + textBlock.Margin = new Thickness(0.0, 8.0, 0.0, 8.0); + TextBlock element = textBlock; + stackPanel3.Children.Add(element); + stackPanel3.Children.Add(new TextBlock + { + Text = "프리셋 추가", + FontSize = 12.0, + Foreground = ((TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + HorizontalAlignment = System.Windows.HorizontalAlignment.Center + }); + border7.Child = stackPanel3; + System.Windows.Media.Brush hoverBg3 = (TryFindResource("ItemHoverBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Transparent; + border7.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border { RenderTransform: ScaleTransform renderTransform } border8) + { + renderTransform.ScaleX = 1.03; + renderTransform.ScaleY = 1.03; + border8.Background = hoverBg3; + } + }; + border7.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border { RenderTransform: ScaleTransform renderTransform } border8) + { + renderTransform.ScaleX = 1.0; + renderTransform.ScaleY = 1.0; + border8.Background = System.Windows.Media.Brushes.Transparent; + } + }; + border7.MouseLeftButtonDown += delegate + { + ShowCustomPresetDialog(); + }; + TopicButtonPanel.Children.Add(border7); + } + + private void ShowCustomPresetDialog(CustomPresetEntry? existing = null) + { + bool flag = existing != null; + CustomPresetDialog customPresetDialog = new CustomPresetDialog(existing?.Label ?? "", existing?.Description ?? "", existing?.SystemPrompt ?? "", existing?.Color ?? "#6366F1", existing?.Symbol ?? "\ue713", existing?.Tab ?? _activeTab) + { + Owner = this + }; + if (customPresetDialog.ShowDialog() == true) + { + if (flag) + { + existing.Label = customPresetDialog.PresetName; + existing.Description = customPresetDialog.PresetDescription; + existing.SystemPrompt = customPresetDialog.PresetSystemPrompt; + existing.Color = customPresetDialog.PresetColor; + existing.Symbol = customPresetDialog.PresetSymbol; + existing.Tab = customPresetDialog.PresetTab; + } + else + { + _settings.Settings.Llm.CustomPresets.Add(new CustomPresetEntry + { + Label = customPresetDialog.PresetName, + Description = customPresetDialog.PresetDescription, + SystemPrompt = customPresetDialog.PresetSystemPrompt, + Color = customPresetDialog.PresetColor, + Symbol = customPresetDialog.PresetSymbol, + Tab = customPresetDialog.PresetTab + }); + } + _settings.Save(); + BuildTopicButtons(); + } + } + + private void ShowCustomPresetContextMenu(Border? anchor, TopicPreset preset) + { + if (anchor == null || preset.CustomId == null) + { + return; + } + Popup popup = new Popup + { + PlacementTarget = anchor, + Placement = PlacementMode.Bottom, + StaysOpen = false, + AllowsTransparency = true + }; + System.Windows.Media.Brush background = (TryFindResource("LauncherBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Black; + System.Windows.Media.Brush fg = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush secondaryFg = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush borderBrush = (TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + Border border = new Border + { + Background = background, + CornerRadius = new CornerRadius(10.0), + BorderBrush = borderBrush, + BorderThickness = new Thickness(1.0), + Padding = new Thickness(4.0), + MinWidth = 120.0, + Effect = new DropShadowEffect + { + BlurRadius = 12.0, + ShadowDepth = 2.0, + Opacity = 0.3, + Color = Colors.Black + } + }; + StackPanel stackPanel = new StackPanel(); + Border border2 = CreateContextMenuItem("\ue70f", "편집", fg, secondaryFg); + border2.MouseLeftButtonDown += delegate + { + popup.IsOpen = false; + CustomPresetEntry customPresetEntry = _settings.Settings.Llm.CustomPresets.FirstOrDefault((CustomPresetEntry c) => c.Id == preset.CustomId); + if (customPresetEntry != null) + { + ShowCustomPresetDialog(customPresetEntry); + } + }; + stackPanel.Children.Add(border2); + Border border3 = CreateContextMenuItem("\ue74d", "삭제", new SolidColorBrush(System.Windows.Media.Color.FromRgb(239, 68, 68)), secondaryFg); + border3.MouseLeftButtonDown += delegate + { + popup.IsOpen = false; + MessageBoxResult messageBoxResult = CustomMessageBox.Show("'" + preset.Label + "' 프리셋을 삭제하시겠습니까?", "프리셋 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question); + if (messageBoxResult == MessageBoxResult.Yes) + { + _settings.Settings.Llm.CustomPresets.RemoveAll((CustomPresetEntry c) => c.Id == preset.CustomId); + _settings.Save(); + BuildTopicButtons(); + } + }; + stackPanel.Children.Add(border3); + border.Child = stackPanel; + popup.Child = border; + popup.IsOpen = true; + } + + private Border CreateContextMenuItem(string icon, string label, System.Windows.Media.Brush fg, System.Windows.Media.Brush secondaryFg) + { + Border border = new Border + { + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(6.0), + Padding = new Thickness(10.0, 6.0, 14.0, 6.0), + Cursor = System.Windows.Input.Cursors.Hand + }; + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel.Children.Add(new TextBlock + { + Text = icon, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 13.0, + Foreground = fg, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 8.0, 0.0) + }); + stackPanel.Children.Add(new TextBlock + { + Text = label, + FontSize = 13.0, + Foreground = fg, + VerticalAlignment = VerticalAlignment.Center + }); + border.Child = stackPanel; + System.Windows.Media.Brush hoverBg = (TryFindResource("ItemHoverBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Transparent; + border.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border2) + { + border2.Background = hoverBg; + } + }; + border.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border2) + { + border2.Background = System.Windows.Media.Brushes.Transparent; + } + }; + return border; + } + + private void SelectTopic(TopicPreset preset) + { + bool flag; + lock (_convLock) + { + ChatConversation? currentConversation = _currentConversation; + flag = currentConversation != null && currentConversation.Messages.Count > 0; + } + bool flag2 = !string.IsNullOrEmpty(InputBox.Text); + bool flag3 = flag || flag2; + if (!flag3) + { + StartNewConversation(); + } + lock (_convLock) + { + if (_currentConversation != null) + { + _currentConversation.SystemCommand = preset.SystemPrompt; + _currentConversation.Category = preset.Category; + } + } + if (!flag3) + { + EmptyState.Visibility = Visibility.Collapsed; + } + InputBox.Focus(); + if (!string.IsNullOrEmpty(preset.Placeholder)) + { + _promptCardPlaceholder = preset.Placeholder; + if (!flag3) + { + ShowPlaceholder(); + } + } + if (flag3) + { + ShowToast("프리셋 변경: " + preset.Label); + } + if (_activeTab == "Cowork") + { + BuildBottomBar(); + } + } + + private void BuildBottomBar() + { + MoodIconPanel.Children.Clear(); + System.Windows.Media.Brush brush = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + string key = _settings.Settings.Llm.DefaultOutputFormat ?? "auto"; + string formatLabel = GetFormatLabel(key); + Border border = CreateFolderBarButton("\ue9f9", formatLabel, "보고서 형태 선택", "#8B5CF6"); + border.MouseLeftButtonUp += delegate(object _, MouseButtonEventArgs e) + { + e.Handled = true; + ShowFormatMenu(); + }; + try + { + RegisterName("BtnFormatMenu", border); + } + catch + { + try + { + UnregisterName("BtnFormatMenu"); + RegisterName("BtnFormatMenu", border); + } + catch + { + } + } + MoodIconPanel.Children.Add(border); + MoodIconPanel.Children.Add(new Border + { + Width = 1.0, + Height = 18.0, + Background = ((TryFindResource("SeparatorColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + Margin = new Thickness(4.0, 0.0, 4.0, 0.0), + VerticalAlignment = VerticalAlignment.Center + }); + TemplateMood templateMood = TemplateService.AllMoods.FirstOrDefault((TemplateMood m) => m.Key == _selectedMood); + string text = templateMood?.Label ?? "모던"; + string text2 = templateMood?.Icon ?? "\ud83d\udd37"; + Border border2 = CreateFolderBarButton(null, text2 + " " + text, "디자인 무드 선택"); + border2.MouseLeftButtonUp += delegate(object _, MouseButtonEventArgs e) + { + e.Handled = true; + ShowMoodMenu(); + }; + try + { + RegisterName("BtnMoodMenu", border2); + } + catch + { + try + { + UnregisterName("BtnMoodMenu"); + RegisterName("BtnMoodMenu", border2); + } + catch + { + } + } + MoodIconPanel.Children.Add(border2); + MoodIconPanel.Children.Add(new Border + { + Width = 1.0, + Height = 18.0, + Background = ((TryFindResource("SeparatorColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + Margin = new Thickness(4.0, 0.0, 4.0, 0.0), + VerticalAlignment = VerticalAlignment.Center + }); + Border border3 = CreateFolderBarButton("\ued25", "파일", "파일 탐색기 열기/닫기", "#D97706"); + border3.MouseLeftButtonUp += delegate(object _, MouseButtonEventArgs e) + { + e.Handled = true; + ToggleFileBrowser(); + }; + MoodIconPanel.Children.Add(border3); + AppendLogLevelButton(); + if (FormatMoodSeparator != null) + { + FormatMoodSeparator.Visibility = Visibility.Visible; + } + } + + private void BuildCodeBottomBar() + { + MoodIconPanel.Children.Clear(); + System.Windows.Media.Brush brush = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + string selectedLanguage = _selectedLanguage; + if (1 == 0) + { + } + string text = selectedLanguage switch + { + "python" => "\ud83d\udc0d Python", + "java" => "☕ Java", + "csharp" => "\ud83d\udd37 C#", + "cpp" => "⚙ C++", + "javascript" => "\ud83c\udf10 JavaScript", + _ => "\ud83d\udd27 자동 감지", + }; + if (1 == 0) + { + } + string label = text; + Border border = CreateFolderBarButton(null, label, "개발 언어 선택"); + border.MouseLeftButtonUp += delegate(object _, MouseButtonEventArgs e) + { + e.Handled = true; + ShowLanguageMenu(); + }; + try + { + RegisterName("BtnLangMenu", border); + } + catch + { + try + { + UnregisterName("BtnLangMenu"); + RegisterName("BtnLangMenu", border); + } + catch + { + } + } + MoodIconPanel.Children.Add(border); + MoodIconPanel.Children.Add(new Border + { + Width = 1.0, + Height = 18.0, + Background = ((TryFindResource("SeparatorColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + Margin = new Thickness(4.0, 0.0, 4.0, 0.0), + VerticalAlignment = VerticalAlignment.Center + }); + Border border2 = CreateFolderBarButton("\ued25", "파일", "파일 탐색기 열기/닫기", "#D97706"); + border2.MouseLeftButtonUp += delegate(object _, MouseButtonEventArgs e) + { + e.Handled = true; + ToggleFileBrowser(); + }; + MoodIconPanel.Children.Add(border2); + AppendLogLevelButton(); + if (FormatMoodSeparator != null) + { + FormatMoodSeparator.Visibility = Visibility.Visible; + } + } + + private void AppendLogLevelButton() + { + MoodIconPanel.Children.Add(new Border + { + Width = 1.0, + Height = 18.0, + Background = ((TryFindResource("SeparatorColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray), + Margin = new Thickness(4.0, 0.0, 4.0, 0.0), + VerticalAlignment = VerticalAlignment.Center + }); + string text = _settings.Settings.Llm.AgentLogLevel ?? "simple"; + if (1 == 0) + { + } + string text2 = ((text == "debug") ? "디버그" : ((!(text == "detailed")) ? "간략" : "상세")); + if (1 == 0) + { + } + string label = text2; + Border border = CreateFolderBarButton("\ue946", label, "실행 이력 상세도", "#059669"); + border.MouseLeftButtonUp += delegate(object _, MouseButtonEventArgs e) + { + e.Handled = true; + ShowLogLevelMenu(); + }; + try + { + RegisterName("BtnLogLevelMenu", border); + } + catch + { + try + { + UnregisterName("BtnLogLevelMenu"); + RegisterName("BtnLogLevelMenu", border); + } + catch + { + } + } + MoodIconPanel.Children.Add(border); + } + + private void ShowLogLevelMenu() + { + FormatMenuItems.Children.Clear(); + System.Windows.Media.Brush brush = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush brush2 = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.CornflowerBlue; + System.Windows.Media.Brush foreground = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + (string, string, string)[] array = new(string, string, string)[3] + { + ("simple", "Simple (간략)", "도구 결과만 한 줄로 표시"), + ("detailed", "Detailed (상세)", "도구 호출/결과 + 접이식 상세"), + ("debug", "Debug (디버그)", "모든 정보 + 파라미터 표시") + }; + string text = _settings.Settings.Llm.AgentLogLevel ?? "simple"; + (string, string, string)[] array2 = array; + for (int i = 0; i < array2.Length; i++) + { + (string, string, string) tuple = array2[i]; + string key = tuple.Item1; + string item = tuple.Item2; + string item2 = tuple.Item3; + bool flag = text == key; + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel.Children.Add(CreateCheckIcon(flag, brush2)); + stackPanel.Children.Add(new TextBlock + { + Text = item, + FontSize = 13.0, + Foreground = (flag ? brush2 : brush), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 8.0, 0.0) + }); + stackPanel.Children.Add(new TextBlock + { + Text = item2, + FontSize = 10.0, + Foreground = foreground, + VerticalAlignment = VerticalAlignment.Center + }); + Border border = new Border + { + Child = stackPanel, + Padding = new Thickness(12.0, 8.0, 12.0, 8.0), + CornerRadius = new CornerRadius(6.0), + Background = System.Windows.Media.Brushes.Transparent, + Cursor = System.Windows.Input.Cursors.Hand + }; + System.Windows.Media.Brush hoverBg = (TryFindResource("ItemHoverBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromArgb(24, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + border.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + ((Border)s).Background = hoverBg; + }; + border.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + ((Border)s).Background = System.Windows.Media.Brushes.Transparent; + }; + border.MouseLeftButtonUp += delegate + { + _settings.Settings.Llm.AgentLogLevel = key; + _settings.Save(); + FormatMenuPopup.IsOpen = false; + if (_activeTab == "Cowork") + { + BuildBottomBar(); + } + else if (_activeTab == "Code") + { + BuildCodeBottomBar(); + } + }; + FormatMenuItems.Children.Add(border); + } + try + { + if (FindName("BtnLogLevelMenu") is UIElement placementTarget) + { + FormatMenuPopup.PlacementTarget = placementTarget; + } + } + catch + { + } + FormatMenuPopup.IsOpen = true; + } + + private void ShowLanguageMenu() + { + FormatMenuItems.Children.Clear(); + System.Windows.Media.Brush brush = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush brush2 = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.CornflowerBlue; + (string, string, string)[] array = new(string, string, string)[6] + { + ("auto", "자동 감지", "\ud83d\udd27"), + ("python", "Python", "\ud83d\udc0d"), + ("java", "Java", "☕"), + ("csharp", "C# (.NET)", "\ud83d\udd37"), + ("cpp", "C/C++", "⚙"), + ("javascript", "JavaScript / Vue", "\ud83c\udf10") + }; + (string, string, string)[] array2 = array; + for (int i = 0; i < array2.Length; i++) + { + (string, string, string) tuple = array2[i]; + string item = tuple.Item1; + string item2 = tuple.Item2; + string item3 = tuple.Item3; + bool flag = _selectedLanguage == item; + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel.Children.Add(CreateCheckIcon(flag, brush2)); + stackPanel.Children.Add(new TextBlock + { + Text = item3, + FontSize = 13.0, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 8.0, 0.0) + }); + stackPanel.Children.Add(new TextBlock + { + Text = item2, + FontSize = 13.0, + Foreground = (flag ? brush2 : brush), + FontWeight = (flag ? FontWeights.SemiBold : FontWeights.Normal) + }); + Border border = new Border + { + Child = stackPanel, + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(8.0), + Cursor = System.Windows.Input.Cursors.Hand, + Padding = new Thickness(8.0, 7.0, 12.0, 7.0) + }; + ApplyMenuItemHover(border); + string capturedKey = item; + border.MouseLeftButtonUp += delegate + { + FormatMenuPopup.IsOpen = false; + _selectedLanguage = capturedKey; + BuildCodeBottomBar(); + }; + FormatMenuItems.Children.Add(border); + } + if (FindName("BtnLangMenu") is UIElement placementTarget) + { + FormatMenuPopup.PlacementTarget = placementTarget; + } + FormatMenuPopup.IsOpen = true; + } + + private Border CreateFolderBarButton(string? mdlIcon, string label, string tooltip, string? iconColorHex = null) + { + System.Windows.Media.Brush brush = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush foreground = ((iconColorHex != null) ? BrushFromHex(iconColorHex) : brush); + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + if (mdlIcon != null) + { + stackPanel.Children.Add(new TextBlock + { + Text = mdlIcon, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 12.0, + Foreground = foreground, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 4.0, 0.0) + }); + } + stackPanel.Children.Add(new TextBlock + { + Text = label, + FontSize = 12.0, + Foreground = brush, + VerticalAlignment = VerticalAlignment.Center + }); + return new Border + { + Child = stackPanel, + Background = System.Windows.Media.Brushes.Transparent, + Padding = new Thickness(6.0, 4.0, 6.0, 4.0), + Cursor = System.Windows.Input.Cursors.Hand, + ToolTip = tooltip + }; + } + + private static string GetFormatLabel(string key) + { + if (1 == 0) + { + } + string result = key switch + { + "xlsx" => "Excel", + "html" => "HTML 보고서", + "docx" => "Word", + "md" => "Markdown", + "csv" => "CSV", + _ => "AI 자동", + }; + if (1 == 0) + { + } + return result; + } + + private (string Name, string Symbol, string Color) GetAgentIdentity() + { + string text = null; + lock (_convLock) + { + text = _currentConversation?.Category; + } + string text2 = text; + if (1 == 0) + { + } + (string, string, string) result = text2 switch + { + "보고서" => ("보고서 에이전트", "◆", "#3B82F6"), + "데이터" => ("데이터 분석 에이전트", "◆", "#10B981"), + "문서" => ("문서 작성 에이전트", "◆", "#6366F1"), + "논문" => ("논문 분석 에이전트", "◆", "#6366F1"), + "파일" => ("파일 관리 에이전트", "◆", "#8B5CF6"), + "자동화" => ("자동화 에이전트", "◆", "#EF4444"), + "코드개발" => ("코드 개발 에이전트", "◆", "#3B82F6"), + "리팩터링" => ("리팩터링 에이전트", "◆", "#6366F1"), + "코드리뷰" => ("코드 리뷰 에이전트", "◆", "#10B981"), + "보안점검" => ("보안 점검 에이전트", "◆", "#EF4444"), + "테스트" => ("테스트 에이전트", "◆", "#F59E0B"), + "연구개발" => ("연구개발 에이전트", "◆", "#0EA5E9"), + "시스템" => ("시스템 에이전트", "◆", "#64748B"), + "수율분석" => ("수율분석 에이전트", "◆", "#F59E0B"), + "제품분석" => ("제품분석 에이전트", "◆", "#EC4899"), + "경영" => ("경영 분석 에이전트", "◆", "#8B5CF6"), + "인사" => ("인사 관리 에이전트", "◆", "#14B8A6"), + "제조기술" => ("제조기술 에이전트", "◆", "#F97316"), + "재무" => ("재무 분석 에이전트", "◆", "#6366F1"), + _ => (_activeTab == "Code") ? ("코드 에이전트", "◆", "#3B82F6") : ((!(_activeTab == "Cowork")) ? ("AX 에이전트", "◆", "#4B5EFC") : ("코워크 에이전트", "◆", "#4B5EFC")), + }; + if (1 == 0) + { + } + return result; + } + + private void ShowFormatMenu() + { + FormatMenuItems.Children.Clear(); + System.Windows.Media.Brush foreground = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush brush = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush accentBrush = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.CornflowerBlue; + string text = _settings.Settings.Llm.DefaultOutputFormat ?? "auto"; + (string, string, string, string)[] array = new(string, string, string, string)[6] + { + ("auto", "AI 자동 선택", "\ue8bd", "#8B5CF6"), + ("xlsx", "Excel", "\ue9f9", "#217346"), + ("html", "HTML 보고서", "\ue12b", "#E44D26"), + ("docx", "Word", "\ue8a5", "#2B579A"), + ("md", "Markdown", "\ue943", "#6B7280"), + ("csv", "CSV", "\ue9d9", "#10B981") + }; + (string, string, string, string)[] array2 = array; + for (int i = 0; i < array2.Length; i++) + { + (string, string, string, string) tuple = array2[i]; + string item = tuple.Item1; + string item2 = tuple.Item2; + string item3 = tuple.Item3; + string item4 = tuple.Item4; + bool isChecked = item == text; + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel.Children.Add(CreateCheckIcon(isChecked, accentBrush)); + stackPanel.Children.Add(new TextBlock + { + Text = item3, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 13.0, + Foreground = BrushFromHex(item4), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 8.0, 0.0) + }); + stackPanel.Children.Add(new TextBlock + { + Text = item2, + FontSize = 13.0, + Foreground = foreground, + VerticalAlignment = VerticalAlignment.Center + }); + Border border = new Border + { + Child = stackPanel, + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(8.0), + Cursor = System.Windows.Input.Cursors.Hand, + Padding = new Thickness(8.0, 7.0, 12.0, 7.0) + }; + ApplyMenuItemHover(border); + string capturedKey = item; + border.MouseLeftButtonUp += delegate + { + FormatMenuPopup.IsOpen = false; + _settings.Settings.Llm.DefaultOutputFormat = capturedKey; + _settings.Save(); + BuildBottomBar(); + }; + FormatMenuItems.Children.Add(border); + } + if (FindName("BtnFormatMenu") is UIElement placementTarget) + { + FormatMenuPopup.PlacementTarget = placementTarget; + } + FormatMenuPopup.IsOpen = true; + } + + private void ShowMoodMenu() + { + MoodMenuItems.Children.Clear(); + System.Windows.Media.Brush foreground = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush foreground2 = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush brush = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.CornflowerBlue; + System.Windows.Media.Brush fill = (TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + UniformGrid uniformGrid = new UniformGrid + { + Columns = 2 + }; + foreach (TemplateMood mood in TemplateService.AllMoods) + { + bool flag = _selectedMood == mood.Key; + bool flag2 = _settings.Settings.Llm.CustomMoods.Any((CustomMoodEntry cm) => cm.Key == mood.Key); + TemplateService.MoodColors moodColors = TemplateService.GetMoodColors(mood.Key); + Border border = new Border + { + Width = 160.0, + Height = 80.0, + CornerRadius = new CornerRadius(6.0), + Background = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(moodColors.Background)), + BorderBrush = (flag ? brush : new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(moodColors.Border))), + BorderThickness = new Thickness((!flag) ? 1 : 2), + Padding = new Thickness(8.0, 6.0, 8.0, 6.0), + Margin = new Thickness(2.0) + }; + StackPanel stackPanel = new StackPanel(); + stackPanel.Children.Add(new Border + { + Width = 60.0, + Height = 6.0, + CornerRadius = new CornerRadius(2.0), + Background = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(moodColors.PrimaryText)), + HorizontalAlignment = System.Windows.HorizontalAlignment.Left, + Margin = new Thickness(0.0, 0.0, 0.0, 4.0) + }); + stackPanel.Children.Add(new Border + { + Width = 40.0, + Height = 3.0, + CornerRadius = new CornerRadius(1.0), + Background = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(moodColors.Accent)), + HorizontalAlignment = System.Windows.HorizontalAlignment.Left, + Margin = new Thickness(0.0, 0.0, 0.0, 6.0) + }); + for (int num = 0; num < 3; num++) + { + stackPanel.Children.Add(new Border + { + Width = 120 - num * 20, + Height = 3.0, + CornerRadius = new CornerRadius(1.0), + Background = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(moodColors.SecondaryText)) + { + Opacity = 0.5 + }, + HorizontalAlignment = System.Windows.HorizontalAlignment.Left, + Margin = new Thickness(0.0, 0.0, 0.0, 3.0) + }); + } + StackPanel stackPanel2 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal, + Margin = new Thickness(0.0, 2.0, 0.0, 0.0) + }; + for (int num2 = 0; num2 < 2; num2++) + { + stackPanel2.Children.Add(new Border + { + Width = 28.0, + Height = 14.0, + CornerRadius = new CornerRadius(2.0), + Background = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(moodColors.CardBg)), + BorderBrush = new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(moodColors.Border)), + BorderThickness = new Thickness(0.5), + Margin = new Thickness(0.0, 0.0, 4.0, 0.0) + }); + } + stackPanel.Children.Add(stackPanel2); + border.Child = stackPanel; + StackPanel stackPanel3 = new StackPanel + { + Margin = new Thickness(4.0, 2.0, 4.0, 4.0) + }; + StackPanel stackPanel4 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel4.Children.Add(new TextBlock + { + Text = mood.Icon, + FontSize = 12.0, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 4.0, 0.0) + }); + stackPanel4.Children.Add(new TextBlock + { + Text = mood.Label, + FontSize = 11.5, + Foreground = foreground, + FontWeight = (flag ? FontWeights.SemiBold : FontWeights.Normal), + VerticalAlignment = VerticalAlignment.Center + }); + if (flag) + { + stackPanel4.Children.Add(new TextBlock + { + Text = " ✓", + FontSize = 11.0, + Foreground = brush, + VerticalAlignment = VerticalAlignment.Center + }); + } + stackPanel3.Children.Add(stackPanel4); + Border border2 = new Border + { + CornerRadius = new CornerRadius(8.0), + Background = System.Windows.Media.Brushes.Transparent, + Cursor = System.Windows.Input.Cursors.Hand, + Padding = new Thickness(4.0), + Margin = new Thickness(2.0) + }; + StackPanel stackPanel5 = new StackPanel(); + stackPanel5.Children.Add(border); + stackPanel5.Children.Add(stackPanel3); + border2.Child = stackPanel5; + border2.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border4) + { + border4.Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(18, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + } + }; + border2.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border4) + { + border4.Background = System.Windows.Media.Brushes.Transparent; + } + }; + TemplateMood capturedMood = mood; + border2.MouseLeftButtonUp += delegate + { + MoodMenuPopup.IsOpen = false; + _selectedMood = capturedMood.Key; + _settings.Settings.Llm.DefaultMood = capturedMood.Key; + _settings.Save(); + BuildBottomBar(); + }; + if (flag2) + { + border2.MouseRightButtonUp += delegate(object s, MouseButtonEventArgs e) + { + e.Handled = true; + MoodMenuPopup.IsOpen = false; + ShowCustomMoodContextMenu(s as Border, capturedMood.Key); + }; + } + uniformGrid.Children.Add(border2); + } + MoodMenuItems.Children.Add(uniformGrid); + MoodMenuItems.Children.Add(new System.Windows.Shapes.Rectangle + { + Height = 1.0, + Fill = fill, + Margin = new Thickness(8.0, 4.0, 8.0, 4.0), + Opacity = 0.4 + }); + StackPanel stackPanel6 = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel6.Children.Add(new TextBlock + { + Text = "\ue710", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 13.0, + Foreground = foreground2, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(4.0, 0.0, 8.0, 0.0) + }); + stackPanel6.Children.Add(new TextBlock + { + Text = "커스텀 무드 추가", + FontSize = 13.0, + Foreground = foreground2, + VerticalAlignment = VerticalAlignment.Center + }); + Border border3 = new Border + { + Child = stackPanel6, + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(8.0), + Cursor = System.Windows.Input.Cursors.Hand, + Padding = new Thickness(8.0, 6.0, 12.0, 6.0) + }; + ApplyMenuItemHover(border3); + border3.MouseLeftButtonUp += delegate + { + MoodMenuPopup.IsOpen = false; + ShowCustomMoodDialog(); + }; + MoodMenuItems.Children.Add(border3); + if (FindName("BtnMoodMenu") is UIElement placementTarget) + { + MoodMenuPopup.PlacementTarget = placementTarget; + } + MoodMenuPopup.IsOpen = true; + } + + private void ShowCustomMoodDialog(CustomMoodEntry? existing = null) + { + bool flag = existing != null; + CustomMoodDialog customMoodDialog = new CustomMoodDialog(existing?.Key ?? "", existing?.Label ?? "", existing?.Icon ?? "\ud83c\udfaf", existing?.Description ?? "", existing?.Css ?? "") + { + Owner = this + }; + if (customMoodDialog.ShowDialog() == true) + { + if (flag) + { + existing.Label = customMoodDialog.MoodLabel; + existing.Icon = customMoodDialog.MoodIcon; + existing.Description = customMoodDialog.MoodDescription; + existing.Css = customMoodDialog.MoodCss; + } + else + { + _settings.Settings.Llm.CustomMoods.Add(new CustomMoodEntry + { + Key = customMoodDialog.MoodKey, + Label = customMoodDialog.MoodLabel, + Icon = customMoodDialog.MoodIcon, + Description = customMoodDialog.MoodDescription, + Css = customMoodDialog.MoodCss + }); + } + _settings.Save(); + TemplateService.LoadCustomMoods(_settings.Settings.Llm.CustomMoods); + BuildBottomBar(); + } + } + + private void ShowCustomMoodContextMenu(Border? anchor, string moodKey) + { + if (anchor == null) + { + return; + } + Popup popup = new Popup + { + PlacementTarget = anchor, + Placement = PlacementMode.Right, + StaysOpen = false, + AllowsTransparency = true + }; + System.Windows.Media.Brush background = (TryFindResource("LauncherBackground") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Black; + System.Windows.Media.Brush fg = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush secondaryFg = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush borderBrush = (TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + Border border = new Border + { + Background = background, + CornerRadius = new CornerRadius(10.0), + BorderBrush = borderBrush, + BorderThickness = new Thickness(1.0), + Padding = new Thickness(4.0), + MinWidth = 120.0, + Effect = new DropShadowEffect + { + BlurRadius = 12.0, + ShadowDepth = 2.0, + Opacity = 0.3, + Color = Colors.Black + } + }; + StackPanel stackPanel = new StackPanel(); + Border border2 = CreateContextMenuItem("\ue70f", "편집", fg, secondaryFg); + border2.MouseLeftButtonDown += delegate + { + popup.IsOpen = false; + CustomMoodEntry customMoodEntry = _settings.Settings.Llm.CustomMoods.FirstOrDefault((CustomMoodEntry c) => c.Key == moodKey); + if (customMoodEntry != null) + { + ShowCustomMoodDialog(customMoodEntry); + } + }; + stackPanel.Children.Add(border2); + Border border3 = CreateContextMenuItem("\ue74d", "삭제", new SolidColorBrush(System.Windows.Media.Color.FromRgb(239, 68, 68)), secondaryFg); + border3.MouseLeftButtonDown += delegate + { + popup.IsOpen = false; + MessageBoxResult messageBoxResult = CustomMessageBox.Show("이 디자인 무드를 삭제하시겠습니까?", "무드 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question); + if (messageBoxResult == MessageBoxResult.Yes) + { + _settings.Settings.Llm.CustomMoods.RemoveAll((CustomMoodEntry c) => c.Key == moodKey); + if (_selectedMood == moodKey) + { + _selectedMood = "modern"; + } + _settings.Save(); + TemplateService.LoadCustomMoods(_settings.Settings.Llm.CustomMoods); + BuildBottomBar(); + } + }; + stackPanel.Children.Add(border3); + border.Child = stackPanel; + popup.Child = border; + popup.IsOpen = true; + } + + private void ShowPlaceholder() + { + if (!string.IsNullOrEmpty(_promptCardPlaceholder)) + { + InputWatermark.Text = _promptCardPlaceholder; + InputWatermark.Visibility = Visibility.Visible; + InputBox.Text = ""; + InputBox.Focus(); + } + } + + private void UpdateWatermarkVisibility() + { + if (_activeSlashCmd != null) + { + InputWatermark.Visibility = Visibility.Collapsed; + } + else if (_promptCardPlaceholder != null && string.IsNullOrEmpty(InputBox.Text)) + { + InputWatermark.Visibility = Visibility.Visible; + } + else + { + InputWatermark.Visibility = Visibility.Collapsed; + } + } + + private void ClearPromptCardPlaceholder() + { + _promptCardPlaceholder = null; + InputWatermark.Visibility = Visibility.Collapsed; + } + + private void BtnSettings_Click(object sender, RoutedEventArgs e) + { + if (System.Windows.Application.Current is App app) + { + app.OpenSettingsFromChat(); + } + } + + private void BtnTemplateSelector_Click(object sender, RoutedEventArgs e) + { + List promptTemplates = _settings.Settings.Llm.PromptTemplates; + TemplateItems.Items.Clear(); + if (promptTemplates == null || promptTemplates.Count == 0) + { + TemplateEmptyHint.Visibility = Visibility.Visible; + TemplatePopup.IsOpen = true; + return; + } + TemplateEmptyHint.Visibility = Visibility.Collapsed; + foreach (PromptTemplate item in promptTemplates) + { + Border border = new Border + { + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(8.0), + Padding = new Thickness(10.0, 8.0, 10.0, 8.0), + Margin = new Thickness(2.0), + Cursor = System.Windows.Input.Cursors.Hand, + Tag = item.Content + }; + StackPanel stackPanel = new StackPanel(); + stackPanel.Children.Add(new TextBlock + { + Text = item.Name, + FontSize = 13.0, + FontWeight = FontWeights.SemiBold, + Foreground = (System.Windows.Media.Brush)FindResource("PrimaryText") + }); + string text = ((item.Content.Length > 60) ? (item.Content.Substring(0, 60) + "…") : item.Content); + stackPanel.Children.Add(new TextBlock + { + Text = text, + FontSize = 11.0, + Foreground = (System.Windows.Media.Brush)FindResource("SecondaryText"), + TextTrimming = TextTrimming.CharacterEllipsis, + Margin = new Thickness(0.0, 2.0, 0.0, 0.0) + }); + border.Child = stackPanel; + border.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border2) + { + border2.Background = (System.Windows.Media.Brush)FindResource("ItemBackground"); + } + }; + border.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border2) + { + border2.Background = System.Windows.Media.Brushes.Transparent; + } + }; + border.MouseLeftButtonUp += delegate(object s, MouseButtonEventArgs _) + { + if (s is Border { Tag: string tag }) + { + InputBox.Text = tag; + InputBox.CaretIndex = InputBox.Text.Length; + InputBox.Focus(); + TemplatePopup.IsOpen = false; + } + }; + TemplateItems.Items.Add(border); + } + TemplatePopup.IsOpen = true; + } + + private string GetCurrentModelDisplayName() + { + LlmSettings llm = _settings.Settings.Llm; + string text = llm.Service.ToLowerInvariant(); + if ((text == "ollama" || text == "vllm") ? true : false) + { + RegisteredModel registeredModel = llm.RegisteredModels.FirstOrDefault((RegisteredModel rm) => rm.EncryptedModelName == llm.Model); + if (registeredModel != null) + { + return registeredModel.Alias; + } + return string.IsNullOrEmpty(llm.Model) ? "(미설정)" : "••••"; + } + if (text == "gemini") + { + return GeminiModels.FirstOrDefault(((string Id, string Label) g) => g.Id == llm.Model).Label ?? llm.Model; + } + if (text == "claude") + { + return ClaudeModels.FirstOrDefault(((string Id, string Label) c) => c.Id == llm.Model).Label ?? llm.Model; + } + return string.IsNullOrEmpty(llm.Model) ? "(미설정)" : llm.Model; + } + + private void UpdateModelLabel() + { + string text = _settings.Settings.Llm.Service.ToLowerInvariant(); + if (1 == 0) + { + } + string text2 = text switch + { + "gemini" => "Gemini", + "claude" => "Claude", + "vllm" => "vLLM", + _ => "Ollama", + }; + if (1 == 0) + { + } + string text3 = text2; + ModelLabel.Text = text3 + " · " + GetCurrentModelDisplayName(); + } + + private void BtnModelSelector_Click(object sender, RoutedEventArgs e) + { + LlmSettings llm = _settings.Settings.Llm; + string originalService = llm.Service; + string originalModel = llm.Model; + bool modelConfirmed = false; + System.Windows.Media.Brush background = (TryFindResource("LauncherBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromRgb(26, 27, 46)); + System.Windows.Media.Brush borderBrush = (TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush primaryText = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush secondaryText = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush hoverBg = (TryFindResource("ItemHoverBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromArgb(30, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + SolidColorBrush checkColor = new SolidColorBrush(System.Windows.Media.Color.FromRgb(56, 161, 105)); + SolidColorBrush solidColorBrush = new SolidColorBrush(System.Windows.Media.Color.FromArgb(24, 75, 94, 252)); + Popup popup = new Popup + { + StaysOpen = false, + AllowsTransparency = true, + PopupAnimation = PopupAnimation.Fade, + PlacementTarget = BtnModelSelector, + Placement = PlacementMode.Top + }; + Border border = new Border + { + Background = background, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1.0), + CornerRadius = new CornerRadius(12.0), + Padding = new Thickness(6.0), + MinWidth = 240.0, + MaxHeight = 460.0, + Effect = new DropShadowEffect + { + BlurRadius = 16.0, + ShadowDepth = 4.0, + Opacity = 0.3, + Color = Colors.Black + } + }; + ScrollViewer scrollViewer = new ScrollViewer + { + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + MaxHeight = 440.0 + }; + StackPanel stackPanel = new StackPanel(); + StackPanel modelSection = new StackPanel(); + stackPanel.Children.Add(new TextBlock + { + Text = "서비스", + FontSize = 11.0, + Foreground = secondaryText, + Margin = new Thickness(10.0, 4.0, 0.0, 4.0), + FontWeight = FontWeights.SemiBold + }); + StackPanel serviceItems = new StackPanel(); + (string, string)[] services = new(string, string)[4] + { + ("ollama", "Ollama"), + ("vllm", "vLLM"), + ("gemini", "Gemini"), + ("claude", "Claude") + }; + BuildServiceItems(); + stackPanel.Children.Add(serviceItems); + stackPanel.Children.Add(CreateSeparator()); + RebuildModelList(llm.Service.ToLowerInvariant()); + stackPanel.Children.Add(modelSection); + popup.Closed += delegate + { + if (!modelConfirmed) + { + _settings.Settings.Llm.Service = originalService; + _settings.Settings.Llm.Model = originalModel; + UpdateModelLabel(); + } + }; + scrollViewer.Content = stackPanel; + border.Child = scrollViewer; + popup.Child = border; + popup.IsOpen = true; + void BuildServiceItems() + { + serviceItems.Children.Clear(); + string text = llm.Service.ToLowerInvariant(); + (string, string)[] array = services; + for (int i = 0; i < array.Length; i++) + { + (string, string) tuple = array[i]; + string item = tuple.Item1; + string item2 = tuple.Item2; + string capturedSvc = item; + serviceItems.Children.Add(CreateMenuItem(item2, text == item, delegate + { + _settings.Settings.Llm.Service = capturedSvc; + UpdateModelLabel(); + BuildServiceItems(); + RebuildModelList(capturedSvc); + }, closeOnClick: false)); + } + } + Border CreateMenuItem(string text, bool isChecked, Action onClick, bool closeOnClick = true) + { + //IL_021d: Unknown result type (might be due to invalid IL or missing references) + Border item = new Border + { + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(8.0), + Padding = new Thickness(10.0, 7.0, 10.0, 7.0), + Margin = new Thickness(0.0, 1.0, 0.0, 1.0), + Cursor = System.Windows.Input.Cursors.Hand + }; + Grid grid = new Grid + { + ColumnDefinitions = + { + new ColumnDefinition + { + Width = new GridLength(1.0, GridUnitType.Star) + }, + new ColumnDefinition + { + Width = new GridLength(20.0) + } + } + }; + TextBlock element = new TextBlock + { + Text = text, + FontSize = 13.0, + Foreground = primaryText, + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(element, 0); + grid.Children.Add(element); + if (isChecked) + { + Canvas canvas = new Canvas + { + Width = 14.0, + Height = 14.0, + VerticalAlignment = VerticalAlignment.Center + }; + canvas.Children.Add(new System.Windows.Shapes.Path + { + Data = Geometry.Parse("M 2 7 L 5.5 10.5 L 12 4"), + Stroke = checkColor, + StrokeThickness = 2.0, + StrokeStartLineCap = PenLineCap.Round, + StrokeEndLineCap = PenLineCap.Round, + StrokeLineJoin = PenLineJoin.Round + }); + Grid.SetColumn(canvas, 1); + grid.Children.Add(canvas); + } + item.Child = grid; + item.RenderTransformOrigin = new Point(0.5, 0.5); + item.RenderTransform = new ScaleTransform(1.0, 1.0); + item.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border2) + { + border2.Background = hoverBg; + } + ScaleTransform scaleTransform = item.RenderTransform as ScaleTransform; + scaleTransform?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120.0))); + scaleTransform?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120.0))); + }; + item.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border2) + { + border2.Background = System.Windows.Media.Brushes.Transparent; + } + ScaleTransform scaleTransform = item.RenderTransform as ScaleTransform; + scaleTransform?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150.0))); + scaleTransform?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150.0))); + }; + item.MouseLeftButtonUp += delegate + { + if (closeOnClick) + { + popup.IsOpen = false; + } + onClick(); + }; + return item; + } + Border CreateSeparator() + { + return new Border + { + Height = 1.0, + Background = borderBrush, + Opacity = 0.3, + Margin = new Thickness(8.0, 4.0, 8.0, 4.0) + }; + } + void RebuildModelList(string service) + { + modelSection.Children.Clear(); + modelSection.Children.Add(new TextBlock + { + Text = "모델", + FontSize = 11.0, + Foreground = secondaryText, + Margin = new Thickness(10.0, 4.0, 0.0, 4.0), + FontWeight = FontWeights.SemiBold + }); + string text = service; + if ((text == "ollama" || text == "vllm") ? true : false) + { + List list = llm.RegisteredModels.Where((RegisteredModel rm) => rm.Service == service).ToList(); + if (list.Count != 0) + { + foreach (RegisteredModel item5 in list) + { + string capturedEnc = item5.EncryptedModelName; + modelSection.Children.Add(CreateMenuItem(item5.Alias, llm.Model == item5.EncryptedModelName, delegate + { + _settings.Settings.Llm.Model = capturedEnc; + _settings.Save(); + modelConfirmed = true; + UpdateModelLabel(); + })); + } + return; + } + modelSection.Children.Add(new TextBlock + { + Text = "등록된 모델 없음 — 설정에서 추가", + FontSize = 11.5, + Foreground = secondaryText, + FontStyle = FontStyles.Italic, + Margin = new Thickness(10.0, 4.0, 10.0, 4.0) + }); + } + else if (service == "gemini") + { + (string, string)[] geminiModels = GeminiModels; + for (int num = 0; num < geminiModels.Length; num++) + { + (string, string) tuple = geminiModels[num]; + string item = tuple.Item1; + string item2 = tuple.Item2; + string capturedId = item; + modelSection.Children.Add(CreateMenuItem(item2, llm.Model == item, delegate + { + _settings.Settings.Llm.Model = capturedId; + _settings.Save(); + modelConfirmed = true; + UpdateModelLabel(); + })); + } + } + else if (service == "claude") + { + (string, string)[] claudeModels = ClaudeModels; + for (int num2 = 0; num2 < claudeModels.Length; num2++) + { + (string, string) tuple2 = claudeModels[num2]; + string item3 = tuple2.Item1; + string item4 = tuple2.Item2; + string capturedId2 = item3; + modelSection.Children.Add(CreateMenuItem(item4, llm.Model == item3, delegate + { + _settings.Settings.Llm.Model = capturedId2; + _settings.Save(); + modelConfirmed = true; + UpdateModelLabel(); + })); + } + } + } + } + + private void BtnNewChat_Click(object sender, RoutedEventArgs e) + { + StartNewConversation(); + InputBox.Focus(); + } + + public void ResumeConversation(string conversationId) + { + ChatConversation chatConversation = _storage.Load(conversationId); + if (chatConversation != null) + { + lock (_convLock) + { + _currentConversation = chatConversation; + } + UpdateChatTitle(); + RefreshConversationList(); + RenderMessages(); + UpdateFolderBar(); + } + InputBox.Focus(); + } + + public void StartNewAndFocus() + { + StartNewConversation(); + InputBox.Focus(); + } + + private void BtnDeleteAll_Click(object sender, RoutedEventArgs e) + { + MessageBoxResult messageBoxResult = CustomMessageBox.Show("저장된 모든 대화 내역을 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.", "대화 전체 삭제", MessageBoxButton.YesNo, MessageBoxImage.Exclamation); + if (messageBoxResult == MessageBoxResult.Yes) + { + _storage.DeleteAll(); + lock (_convLock) + { + _currentConversation = null; + } + MessagePanel.Children.Clear(); + EmptyState.Visibility = Visibility.Visible; + UpdateChatTitle(); + RefreshConversationList(); + } + } + + private void TryShowPreview(string filePath) + { + if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath)) + { + PreviewWindow.ShowPreview(filePath, _selectedMood); + } + } + + private void ShowPreviewPanel(string filePath) + { + if (!_previewTabs.Contains(filePath, StringComparer.OrdinalIgnoreCase)) + { + _previewTabs.Add(filePath); + } + _activePreviewTab = filePath; + if (PreviewColumn.Width.Value < 100.0) + { + PreviewColumn.Width = new GridLength(420.0); + SplitterColumn.Width = new GridLength(5.0); + } + PreviewPanel.Visibility = Visibility.Visible; + PreviewSplitter.Visibility = Visibility.Visible; + BtnPreviewToggle.Visibility = Visibility.Visible; + RebuildPreviewTabs(); + LoadPreviewContent(filePath); + } + + private void RebuildPreviewTabs() + { + PreviewTabPanel.Children.Clear(); + System.Windows.Media.Brush brush = (TryFindResource("AccentColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.CornflowerBlue; + System.Windows.Media.Brush brush2 = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush brush3 = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush background = (TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + foreach (string previewTab in _previewTabs) + { + string fileName = System.IO.Path.GetFileName(previewTab); + bool flag = string.Equals(previewTab, _activePreviewTab, StringComparison.OrdinalIgnoreCase); + Border border = new Border + { + Background = (flag ? new SolidColorBrush(System.Windows.Media.Color.FromArgb(21, byte.MaxValue, byte.MaxValue, byte.MaxValue)) : System.Windows.Media.Brushes.Transparent), + BorderBrush = (flag ? brush : System.Windows.Media.Brushes.Transparent), + BorderThickness = new Thickness(0.0, 0.0, 0.0, flag ? 2 : 0), + Padding = new Thickness(8.0, 6.0, 4.0, 6.0), + Cursor = System.Windows.Input.Cursors.Hand, + MaxWidth = ((_previewTabs.Count <= 3) ? 200 : ((_previewTabs.Count <= 5) ? 140 : 100)) + }; + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel.Children.Add(new TextBlock + { + Text = fileName, + FontSize = 11.0, + Foreground = (flag ? brush3 : brush2), + FontWeight = (flag ? FontWeights.SemiBold : FontWeights.Normal), + VerticalAlignment = VerticalAlignment.Center, + TextTrimming = TextTrimming.CharacterEllipsis, + MaxWidth = border.MaxWidth - 30.0, + ToolTip = previewTab + }); + System.Windows.Media.Brush closeFg = (flag ? brush3 : brush2); + TextBlock child = new TextBlock + { + Text = "\ue711", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 10.0, + Foreground = closeFg, + VerticalAlignment = VerticalAlignment.Center + }; + Border border2 = new Border + { + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(3.0), + Padding = new Thickness(3.0, 2.0, 3.0, 2.0), + Margin = new Thickness(5.0, 0.0, 0.0, 0.0), + Cursor = System.Windows.Input.Cursors.Hand, + VerticalAlignment = VerticalAlignment.Center, + Visibility = ((!flag) ? Visibility.Hidden : Visibility.Visible), + Child = child + }; + string closePath = previewTab; + border2.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border3) + { + border3.Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(64, byte.MaxValue, 80, 80)); + if (border3.Child is TextBlock textBlock) + { + textBlock.Foreground = new SolidColorBrush(System.Windows.Media.Color.FromRgb(byte.MaxValue, 96, 96)); + } + } + }; + border2.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border3) + { + border3.Background = System.Windows.Media.Brushes.Transparent; + if (border3.Child is TextBlock textBlock) + { + textBlock.Foreground = closeFg; + } + } + }; + border2.Tag = "close"; + border2.MouseLeftButtonUp += delegate(object _, MouseButtonEventArgs e) + { + e.Handled = true; + ClosePreviewTab(closePath); + }; + stackPanel.Children.Add(border2); + border.Child = stackPanel; + string clickPath = previewTab; + border.MouseLeftButtonUp += delegate(object _, MouseButtonEventArgs e) + { + if (!e.Handled) + { + e.Handled = true; + _activePreviewTab = clickPath; + RebuildPreviewTabs(); + LoadPreviewContent(clickPath); + } + }; + string ctxPath = previewTab; + border.MouseRightButtonUp += delegate(object _, MouseButtonEventArgs e) + { + e.Handled = true; + ShowPreviewTabContextMenu(ctxPath); + }; + string dblPath = previewTab; + border.MouseLeftButtonDown += delegate(object _, MouseButtonEventArgs e) + { + if (!e.Handled && e.ClickCount == 2) + { + e.Handled = true; + OpenPreviewPopupWindow(dblPath); + } + }; + bool capturedIsActive = flag; + Border capturedCloseBtn = border2; + border.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border3 && !capturedIsActive) + { + border3.Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(16, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + } + if (!capturedIsActive) + { + capturedCloseBtn.Visibility = Visibility.Visible; + } + }; + border.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border3 && !capturedIsActive) + { + border3.Background = System.Windows.Media.Brushes.Transparent; + } + if (!capturedIsActive) + { + capturedCloseBtn.Visibility = Visibility.Hidden; + } + }; + PreviewTabPanel.Children.Add(border); + List previewTabs = _previewTabs; + if (previewTab != previewTabs[previewTabs.Count - 1]) + { + PreviewTabPanel.Children.Add(new Border + { + Width = 1.0, + Height = 14.0, + Background = background, + Margin = new Thickness(0.0, 4.0, 0.0, 4.0), + VerticalAlignment = VerticalAlignment.Center + }); + } + } + } + + private void ClosePreviewTab(string filePath) + { + _previewTabs.Remove(filePath); + if (_previewTabs.Count == 0) + { + HidePreviewPanel(); + return; + } + if (string.Equals(filePath, _activePreviewTab, StringComparison.OrdinalIgnoreCase)) + { + List previewTabs = _previewTabs; + _activePreviewTab = previewTabs[previewTabs.Count - 1]; + LoadPreviewContent(_activePreviewTab); + } + RebuildPreviewTabs(); + } + + private async void LoadPreviewContent(string filePath) + { + string ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant(); + PreviewWebView.Visibility = Visibility.Collapsed; + PreviewTextScroll.Visibility = Visibility.Collapsed; + PreviewDataGrid.Visibility = Visibility.Collapsed; + PreviewEmpty.Visibility = Visibility.Collapsed; + if (!File.Exists(filePath)) + { + PreviewEmpty.Text = "파일을 찾을 수 없습니다"; + PreviewEmpty.Visibility = Visibility.Visible; + return; + } + try + { + switch (ext) + { + case ".html": + case ".htm": + await EnsureWebViewInitializedAsync(); + PreviewWebView.Source = new Uri(filePath); + PreviewWebView.Visibility = Visibility.Visible; + break; + case ".csv": + LoadCsvPreview(filePath); + PreviewDataGrid.Visibility = Visibility.Visible; + break; + case ".md": + { + await EnsureWebViewInitializedAsync(); + string mdText = File.ReadAllText(filePath); + if (mdText.Length > 50000) + { + mdText = mdText.Substring(0, 50000); + } + string mdHtml = TemplateService.RenderMarkdownToHtml(mdText, _selectedMood); + PreviewWebView.NavigateToString(mdHtml); + PreviewWebView.Visibility = Visibility.Visible; + break; + } + case ".txt": + case ".json": + case ".xml": + case ".log": + { + string text = File.ReadAllText(filePath); + if (text.Length > 50000) + { + text = text.Substring(0, 50000) + "\n\n... (이후 생략)"; + } + PreviewTextBlock.Text = text; + PreviewTextScroll.Visibility = Visibility.Visible; + break; + } + default: + PreviewEmpty.Text = "미리보기할 수 없는 파일 형식입니다"; + PreviewEmpty.Visibility = Visibility.Visible; + break; + } + } + catch (Exception ex) + { + PreviewTextBlock.Text = "미리보기 오류: " + ex.Message; + PreviewTextScroll.Visibility = Visibility.Visible; + } + } + + private async Task EnsureWebViewInitializedAsync() + { + if (_webViewInitialized) + { + return; + } + try + { + CoreWebView2Environment env = await CoreWebView2Environment.CreateAsync(null, WebView2DataFolder); + await PreviewWebView.EnsureCoreWebView2Async(env); + _webViewInitialized = true; + } + catch (Exception ex) + { + LogService.Warn("WebView2 초기화 실패: " + ex.Message); + } + } + + private void LoadCsvPreview(string filePath) + { + try + { + string[] array = File.ReadAllLines(filePath); + if (array.Length == 0) + { + return; + } + DataTable dataTable = new DataTable(); + string[] array2 = ParseCsvLine(array[0]); + string[] array3 = array2; + foreach (string columnName in array3) + { + dataTable.Columns.Add(columnName); + } + int num = Math.Min(array.Length, 501); + for (int j = 1; j < num; j++) + { + string[] array4 = ParseCsvLine(array[j]); + DataRow dataRow = dataTable.NewRow(); + for (int k = 0; k < Math.Min(array4.Length, array2.Length); k++) + { + dataRow[k] = array4[k]; + } + dataTable.Rows.Add(dataRow); + } + PreviewDataGrid.ItemsSource = dataTable.DefaultView; + } + catch (Exception ex) + { + PreviewTextBlock.Text = "CSV 로드 오류: " + ex.Message; + PreviewTextScroll.Visibility = Visibility.Visible; + PreviewDataGrid.Visibility = Visibility.Collapsed; + } + } + + private static string[] ParseCsvLine(string line) + { + List list = new List(); + StringBuilder stringBuilder = new StringBuilder(); + bool flag = false; + for (int i = 0; i < line.Length; i++) + { + char c = line[i]; + if (flag) + { + if (c == '"' && i + 1 < line.Length && line[i + 1] == '"') + { + stringBuilder.Append('"'); + i++; + } + else if (c == '"') + { + flag = false; + } + else + { + stringBuilder.Append(c); + } + continue; + } + switch (c) + { + case '"': + flag = true; + break; + case ',': + list.Add(stringBuilder.ToString()); + stringBuilder.Clear(); + break; + default: + stringBuilder.Append(c); + break; + } + } + list.Add(stringBuilder.ToString()); + return list.ToArray(); + } + + private void HidePreviewPanel() + { + _previewTabs.Clear(); + _activePreviewTab = null; + PreviewColumn.Width = new GridLength(0.0); + SplitterColumn.Width = new GridLength(0.0); + PreviewPanel.Visibility = Visibility.Collapsed; + PreviewSplitter.Visibility = Visibility.Collapsed; + PreviewWebView.Visibility = Visibility.Collapsed; + PreviewTextScroll.Visibility = Visibility.Collapsed; + PreviewDataGrid.Visibility = Visibility.Collapsed; + try + { + if (_webViewInitialized) + { + PreviewWebView.CoreWebView2?.NavigateToString(""); + } + } + catch + { + } + } + + private void PreviewTabBar_PreviewMouseDown(object sender, MouseButtonEventArgs e) + { + if ((PreviewWebView.IsFocused || PreviewWebView.IsKeyboardFocusWithin) && sender is Border border) + { + border.Focus(); + } + } + + private void BtnClosePreview_Click(object sender, RoutedEventArgs e) + { + HidePreviewPanel(); + BtnPreviewToggle.Visibility = Visibility.Collapsed; + } + + private void BtnPreviewToggle_Click(object sender, RoutedEventArgs e) + { + if (PreviewPanel.Visibility == Visibility.Visible) + { + PreviewPanel.Visibility = Visibility.Collapsed; + PreviewSplitter.Visibility = Visibility.Collapsed; + PreviewColumn.Width = new GridLength(0.0); + SplitterColumn.Width = new GridLength(0.0); + } + else if (_previewTabs.Count > 0) + { + PreviewPanel.Visibility = Visibility.Visible; + PreviewSplitter.Visibility = Visibility.Visible; + PreviewColumn.Width = new GridLength(420.0); + SplitterColumn.Width = new GridLength(5.0); + RebuildPreviewTabs(); + if (_activePreviewTab != null) + { + LoadPreviewContent(_activePreviewTab); + } + } + } + + private void BtnOpenExternal_Click(object sender, RoutedEventArgs e) + { + if (string.IsNullOrEmpty(_activePreviewTab) || !File.Exists(_activePreviewTab)) + { + return; + } + try + { + Process.Start(new ProcessStartInfo + { + FileName = _activePreviewTab, + UseShellExecute = true + }); + } + catch (Exception ex) + { + Debug.WriteLine("외부 프로그램 실행 오류: " + ex.Message); + } + } + + private void ShowPreviewTabContextMenu(string filePath) + { + if (_previewTabPopup != null) + { + _previewTabPopup.IsOpen = false; + } + System.Windows.Media.Brush background = (TryFindResource("LauncherBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromRgb(30, 30, 46)); + System.Windows.Media.Brush borderBrush = (TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush primaryText = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush secondaryText = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush hoverBg = (TryFindResource("ItemHoverBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromArgb(24, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + StackPanel stack = new StackPanel(); + AddItem("\ue8a7", "#64B5F6", "외부 프로그램으로 열기", delegate + { + try + { + Process.Start(new ProcessStartInfo + { + FileName = filePath, + UseShellExecute = true + }); + } + catch + { + } + }); + AddItem("\ue838", "#FFB74D", "파일 위치 열기", delegate + { + try + { + Process.Start("explorer.exe", "/select,\"" + filePath + "\""); + } + catch + { + } + }); + AddItem("\ue8a7", "#81C784", "별도 창에서 보기", delegate + { + OpenPreviewPopupWindow(filePath); + }); + AddSeparator(); + AddItem("\ue8c8", "", "경로 복사", delegate + { + try + { + System.Windows.Clipboard.SetText(filePath); + } + catch + { + } + }); + AddSeparator(); + AddItem("\ue711", "#EF5350", "이 탭 닫기", delegate + { + ClosePreviewTab(filePath); + }); + if (_previewTabs.Count > 1) + { + AddItem("\ue8bb", "#EF5350", "다른 탭 모두 닫기", delegate + { + string keep = filePath; + _previewTabs.RemoveAll((string p) => !string.Equals(p, keep, StringComparison.OrdinalIgnoreCase)); + _activePreviewTab = keep; + RebuildPreviewTabs(); + LoadPreviewContent(keep); + }); + } + Border child = new Border + { + Background = background, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1.0), + CornerRadius = new CornerRadius(12.0), + Padding = new Thickness(4.0, 6.0, 4.0, 6.0), + MinWidth = 180.0, + Effect = new DropShadowEffect + { + BlurRadius = 16.0, + Opacity = 0.4, + ShadowDepth = 4.0, + Color = Colors.Black + }, + Child = stack + }; + _previewTabPopup = new Popup + { + Child = child, + Placement = PlacementMode.MousePoint, + StaysOpen = false, + AllowsTransparency = true, + PopupAnimation = PopupAnimation.Fade + }; + _previewTabPopup.IsOpen = true; + void AddItem(string icon, string iconColor, string label, Action action) + { + Border border = new Border + { + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(6.0), + Padding = new Thickness(10.0, 7.0, 16.0, 7.0), + Cursor = System.Windows.Input.Cursors.Hand + }; + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel.Children.Add(new TextBlock + { + Text = icon, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 12.0, + Foreground = (string.IsNullOrEmpty(iconColor) ? secondaryText : new SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(iconColor))), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 8.0, 0.0) + }); + stackPanel.Children.Add(new TextBlock + { + Text = label, + FontSize = 13.0, + Foreground = primaryText, + VerticalAlignment = VerticalAlignment.Center + }); + border.Child = stackPanel; + border.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border2) + { + border2.Background = hoverBg; + } + }; + border.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border2) + { + border2.Background = System.Windows.Media.Brushes.Transparent; + } + }; + border.MouseLeftButtonUp += delegate + { + _previewTabPopup.IsOpen = false; + action(); + }; + stack.Children.Add(border); + } + void AddSeparator() + { + stack.Children.Add(new Border + { + Height = 1.0, + Background = borderBrush, + Margin = new Thickness(8.0, 3.0, 8.0, 3.0) + }); + } + } + + private void OpenPreviewPopupWindow(string filePath) + { + if (!File.Exists(filePath)) + { + return; + } + string text = System.IO.Path.GetExtension(filePath).ToLowerInvariant(); + string fileName = System.IO.Path.GetFileName(filePath); + System.Windows.Media.Brush background = (TryFindResource("LauncherBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromRgb(30, 30, 46)); + System.Windows.Media.Brush foreground = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + Window window = new Window + { + Title = "미리보기 — " + fileName, + Width = 900.0, + Height = 700.0, + WindowStartupLocation = WindowStartupLocation.CenterScreen, + Background = background + }; + FrameworkElement content; + switch (text) + { + case ".html": + case ".htm": + { + WebView2 wv = new WebView2(); + wv.Loaded += async delegate + { + try + { + CoreWebView2Environment env = await CoreWebView2Environment.CreateAsync(null, WebView2DataFolder); + await wv.EnsureCoreWebView2Async(env); + wv.Source = new Uri(filePath); + } + catch + { + } + }; + content = wv; + break; + } + case ".md": + { + WebView2 mdWv = new WebView2(); + string mdMood = _selectedMood; + mdWv.Loaded += async delegate + { + try + { + CoreWebView2Environment env = await CoreWebView2Environment.CreateAsync(null, WebView2DataFolder); + await mdWv.EnsureCoreWebView2Async(env); + string mdSrc = File.ReadAllText(filePath); + if (mdSrc.Length > 100000) + { + mdSrc = mdSrc.Substring(0, 100000); + } + string html = TemplateService.RenderMarkdownToHtml(mdSrc, mdMood); + mdWv.NavigateToString(html); + } + catch + { + } + }; + content = mdWv; + break; + } + case ".csv": + { + DataGrid dataGrid = new DataGrid + { + AutoGenerateColumns = true, + IsReadOnly = true, + Background = System.Windows.Media.Brushes.Transparent, + Foreground = System.Windows.Media.Brushes.White, + BorderThickness = new Thickness(0.0), + FontSize = 12.0 + }; + try + { + string[] array = File.ReadAllLines(filePath); + if (array.Length != 0) + { + DataTable dataTable = new DataTable(); + string[] array2 = ParseCsvLine(array[0]); + string[] array3 = array2; + foreach (string columnName in array3) + { + dataTable.Columns.Add(columnName); + } + for (int num2 = 1; num2 < Math.Min(array.Length, 1001); num2++) + { + string[] array4 = ParseCsvLine(array[num2]); + DataRow dataRow = dataTable.NewRow(); + for (int num3 = 0; num3 < Math.Min(array4.Length, dataTable.Columns.Count); num3++) + { + dataRow[num3] = array4[num3]; + } + dataTable.Rows.Add(dataRow); + } + dataGrid.ItemsSource = dataTable.DefaultView; + } + } + catch + { + } + content = dataGrid; + break; + } + default: + { + string text2 = File.ReadAllText(filePath); + if (text2.Length > 100000) + { + text2 = text2.Substring(0, 100000) + "\n\n... (이후 생략)"; + } + ScrollViewer scrollViewer = new ScrollViewer + { + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + Padding = new Thickness(20.0), + Content = new TextBlock + { + Text = text2, + TextWrapping = TextWrapping.Wrap, + FontFamily = new System.Windows.Media.FontFamily("Consolas"), + FontSize = 13.0, + Foreground = foreground + } + }; + content = scrollViewer; + break; + } + } + window.Content = content; + window.Show(); + } + + private void UpdateAgentProgressBar(AgentEvent evt) + { + switch (evt.Type) + { + case AgentEventType.Planning: + { + List steps = evt.Steps; + if (steps != null && steps.Count > 0) + { + ShowStickyProgress(evt.Steps.Count); + } + break; + } + case AgentEventType.StepStart: + if (evt.StepTotal > 0) + { + UpdateStickyProgress(evt.StepCurrent, evt.StepTotal, evt.Summary); + } + break; + case AgentEventType.Complete: + HideStickyProgress(); + break; + } + } + + private void ShowStickyProgress(int totalSteps) + { + //IL_00b4: Unknown result type (might be due to invalid IL or missing references) + //IL_00b9: Unknown result type (might be due to invalid IL or missing references) + //IL_00d3: Expected O, but got Unknown + _progressStartTime = DateTime.Now; + AgentProgressBar.Visibility = Visibility.Visible; + ProgressIcon.Text = "\ue768"; + ProgressStepLabel.Text = $"작업 준비 중... (0/{totalSteps})"; + ProgressPercent.Text = "0%"; + ProgressElapsed.Text = "0:00"; + ProgressFill.Width = 0.0; + DispatcherTimer? progressElapsedTimer = _progressElapsedTimer; + if (progressElapsedTimer != null) + { + progressElapsedTimer.Stop(); + } + _progressElapsedTimer = new DispatcherTimer + { + Interval = TimeSpan.FromSeconds(1.0) + }; + _progressElapsedTimer.Tick += delegate + { + TimeSpan timeSpan = DateTime.Now - _progressStartTime; + ProgressElapsed.Text = ((timeSpan.TotalHours >= 1.0) ? timeSpan.ToString("h\\:mm\\:ss") : timeSpan.ToString("m\\:ss")); + }; + _progressElapsedTimer.Start(); + } + + private void UpdateStickyProgress(int currentStep, int totalSteps, string stepDescription) + { + if (AgentProgressBar.Visibility == Visibility.Visible) + { + double num = ((totalSteps > 0) ? ((double)currentStep / (double)totalSteps) : 0.0); + ProgressStepLabel.Text = $"{stepDescription} ({currentStep}/{totalSteps})"; + ProgressPercent.Text = $"{(int)(num * 100.0)}%"; + if (ProgressFill.Parent is Border border) + { + double toValue = border.ActualWidth * num; + DoubleAnimation animation = new DoubleAnimation(ProgressFill.Width, toValue, TimeSpan.FromMilliseconds(300.0)) + { + EasingFunction = new QuadraticEase() + }; + ProgressFill.BeginAnimation(FrameworkElement.WidthProperty, animation); + } + } + } + + private void HideStickyProgress() + { + //IL_00cb: Unknown result type (might be due to invalid IL or missing references) + //IL_00d0: Unknown result type (might be due to invalid IL or missing references) + //IL_00ea: Expected O, but got Unknown + DispatcherTimer? progressElapsedTimer = _progressElapsedTimer; + if (progressElapsedTimer != null) + { + progressElapsedTimer.Stop(); + } + _progressElapsedTimer = null; + if (AgentProgressBar.Visibility != Visibility.Visible) + { + return; + } + ProgressIcon.Text = "\ue930"; + ProgressStepLabel.Text = "작업 완료"; + ProgressPercent.Text = "100%"; + if (ProgressFill.Parent is Border border) + { + DoubleAnimation animation = new DoubleAnimation(ProgressFill.Width, border.ActualWidth, TimeSpan.FromMilliseconds(200.0)); + ProgressFill.BeginAnimation(FrameworkElement.WidthProperty, animation); + } + DispatcherTimer hideTimer = new DispatcherTimer + { + Interval = TimeSpan.FromSeconds(3.0) + }; + hideTimer.Tick += delegate + { + hideTimer.Stop(); + DoubleAnimation doubleAnimation = new DoubleAnimation(1.0, 0.0, TimeSpan.FromMilliseconds(300.0)); + doubleAnimation.Completed += delegate + { + AgentProgressBar.Visibility = Visibility.Collapsed; + AgentProgressBar.Opacity = 1.0; + ProgressFill.BeginAnimation(FrameworkElement.WidthProperty, null); + ProgressFill.Width = 0.0; + }; + AgentProgressBar.BeginAnimation(UIElement.OpacityProperty, doubleAnimation); + }; + hideTimer.Start(); + } + + private void ToggleFileBrowser() + { + if (FileBrowserPanel.Visibility == Visibility.Visible) + { + FileBrowserPanel.Visibility = Visibility.Collapsed; + _settings.Settings.Llm.ShowFileBrowser = false; + } + else + { + FileBrowserPanel.Visibility = Visibility.Visible; + _settings.Settings.Llm.ShowFileBrowser = true; + BuildFileTree(); + } + _settings.Save(); + } + + private void BtnFileBrowserRefresh_Click(object sender, RoutedEventArgs e) + { + BuildFileTree(); + } + + private void BtnFileBrowserOpenFolder_Click(object sender, RoutedEventArgs e) + { + string currentWorkFolder = GetCurrentWorkFolder(); + if (string.IsNullOrEmpty(currentWorkFolder) || !Directory.Exists(currentWorkFolder)) + { + return; + } + try + { + Process.Start(new ProcessStartInfo + { + FileName = currentWorkFolder, + UseShellExecute = true + }); + } + catch + { + } + } + + private void BtnFileBrowserClose_Click(object sender, RoutedEventArgs e) + { + FileBrowserPanel.Visibility = Visibility.Collapsed; + } + + private void BuildFileTree() + { + FileTreeView.Items.Clear(); + string currentWorkFolder = GetCurrentWorkFolder(); + if (string.IsNullOrEmpty(currentWorkFolder) || !Directory.Exists(currentWorkFolder)) + { + FileTreeView.Items.Add(new TreeViewItem + { + Header = "작업 폴더를 선택하세요", + IsEnabled = false + }); + } + else + { + FileBrowserTitle.Text = "파일 탐색기 — " + System.IO.Path.GetFileName(currentWorkFolder); + int count = 0; + PopulateDirectory(new DirectoryInfo(currentWorkFolder), FileTreeView.Items, 0, ref count); + } + } + + private void PopulateDirectory(DirectoryInfo dir, ItemCollection items, int depth, ref int count) + { + if (depth > 4 || count > 200) + { + return; + } + try + { + foreach (DirectoryInfo item in from d in dir.GetDirectories() + orderby d.Name + select d) + { + if (count > 200) + { + break; + } + if (_ignoredDirs.Contains(item.Name) || item.Name.StartsWith('.')) + { + continue; + } + count++; + TreeViewItem treeViewItem = new TreeViewItem + { + Header = CreateFileTreeHeader("\ued25", item.Name, null), + Tag = item.FullName, + IsExpanded = (depth < 1) + }; + if (depth < 3) + { + treeViewItem.Items.Add(new TreeViewItem + { + Header = "로딩 중..." + }); + DirectoryInfo capturedDir = item; + int capturedDepth = depth; + treeViewItem.Expanded += delegate(object s, RoutedEventArgs _) + { + if (s is TreeViewItem treeViewItem3 && treeViewItem3.Items.Count == 1 && treeViewItem3.Items[0] is TreeViewItem { Header: var header } && header?.ToString() == "로딩 중...") + { + treeViewItem3.Items.Clear(); + int count2 = 0; + PopulateDirectory(capturedDir, treeViewItem3.Items, capturedDepth + 1, ref count2); + } + }; + } + else + { + PopulateDirectory(item, treeViewItem.Items, depth + 1, ref count); + } + items.Add(treeViewItem); + } + } + catch + { + } + try + { + foreach (FileInfo item2 in from f in dir.GetFiles() + orderby f.Name + select f) + { + if (count > 200) + { + break; + } + count++; + string ext = item2.Extension.ToLowerInvariant(); + string fileIcon = GetFileIcon(ext); + string sizeText = FormatFileSize(item2.Length); + TreeViewItem treeViewItem2 = new TreeViewItem + { + Header = CreateFileTreeHeader(fileIcon, item2.Name, sizeText), + Tag = item2.FullName + }; + string capturedPath = item2.FullName; + treeViewItem2.MouseDoubleClick += delegate(object s, MouseButtonEventArgs e) + { + e.Handled = true; + TryShowPreview(capturedPath); + }; + treeViewItem2.MouseRightButtonUp += delegate(object s, MouseButtonEventArgs e) + { + e.Handled = true; + if (s is TreeViewItem treeViewItem3) + { + treeViewItem3.IsSelected = true; + } + ShowFileTreeContextMenu(capturedPath); + }; + items.Add(treeViewItem2); + } + } + catch + { + } + } + + private static StackPanel CreateFileTreeHeader(string icon, string name, string? sizeText) + { + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel.Children.Add(new TextBlock + { + Text = icon, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 11.0, + Foreground = new SolidColorBrush(System.Windows.Media.Color.FromRgb(156, 163, 175)), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 5.0, 0.0) + }); + stackPanel.Children.Add(new TextBlock + { + Text = name, + FontSize = 11.5, + VerticalAlignment = VerticalAlignment.Center + }); + if (sizeText != null) + { + stackPanel.Children.Add(new TextBlock + { + Text = " " + sizeText, + FontSize = 10.0, + Foreground = new SolidColorBrush(System.Windows.Media.Color.FromRgb(107, 114, 128)), + VerticalAlignment = VerticalAlignment.Center + }); + } + return stackPanel; + } + + private static string GetFileIcon(string ext) + { + if (1 == 0) + { + } + string result; + switch (ext) + { + case ".html": + case ".htm": + result = "\ueb41"; + break; + case ".xlsx": + case ".xls": + result = "\ue9f9"; + break; + case ".docx": + case ".doc": + result = "\ue8a5"; + break; + case ".pdf": + result = "\uea90"; + break; + case ".csv": + result = "\ue80a"; + break; + case ".md": + result = "\ue70b"; + break; + case ".json": + case ".xml": + result = "\ue943"; + break; + case ".png": + case ".jpg": + case ".jpeg": + case ".gif": + case ".svg": + case ".webp": + result = "\ueb9f"; + break; + case ".cs": + case ".py": + case ".js": + case ".ts": + case ".java": + case ".cpp": + result = "\ue943"; + break; + case ".bat": + case ".cmd": + case ".ps1": + case ".sh": + result = "\ue756"; + break; + case ".txt": + case ".log": + result = "\ue8a5"; + break; + default: + result = "\ue7c3"; + break; + } + if (1 == 0) + { + } + return result; + } + + private static string FormatFileSize(long bytes) + { + if (1 == 0) + { + } + string result = ((bytes < 1024) ? $"{bytes} B" : ((bytes >= 1048576) ? $"{(double)bytes / 1048576.0:F1} MB" : $"{(double)bytes / 1024.0:F1} KB")); + if (1 == 0) + { + } + return result; + } + + private void ShowFileTreeContextMenu(string filePath) + { + System.Windows.Media.Brush background = (TryFindResource("LauncherBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromRgb(30, 30, 46)); + System.Windows.Media.Brush borderBrush = (TryFindResource("BorderColor") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush primaryText = (TryFindResource("PrimaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.White; + System.Windows.Media.Brush secondaryText = (TryFindResource("SecondaryText") as System.Windows.Media.Brush) ?? System.Windows.Media.Brushes.Gray; + System.Windows.Media.Brush hoverBg = (TryFindResource("HintBackground") as System.Windows.Media.Brush) ?? new SolidColorBrush(System.Windows.Media.Color.FromArgb(24, byte.MaxValue, byte.MaxValue, byte.MaxValue)); + SolidColorBrush solidColorBrush = new SolidColorBrush(System.Windows.Media.Color.FromRgb(231, 76, 60)); + Popup popup = new Popup + { + StaysOpen = false, + AllowsTransparency = true, + PopupAnimation = PopupAnimation.Fade, + Placement = PlacementMode.MousePoint + }; + StackPanel panel = new StackPanel + { + Margin = new Thickness(2.0) + }; + Border child = new Border + { + Background = background, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1.0), + CornerRadius = new CornerRadius(10.0), + Padding = new Thickness(6.0), + MinWidth = 200.0, + Effect = new DropShadowEffect + { + BlurRadius = 16.0, + ShadowDepth = 4.0, + Opacity = 0.3, + Color = Colors.Black, + Direction = 270.0 + }, + Child = panel + }; + popup.Child = child; + string item = System.IO.Path.GetExtension(filePath).ToLowerInvariant(); + if (_previewableExtensions.Contains(item)) + { + AddItem("\ue8a1", "미리보기", delegate + { + ShowPreviewPanel(filePath); + }); + } + AddItem("\ue8a7", "외부 프로그램으로 열기", delegate + { + try + { + Process.Start(new ProcessStartInfo + { + FileName = filePath, + UseShellExecute = true + }); + } + catch + { + } + }); + AddItem("\ued25", "폴더에서 보기", delegate + { + try + { + Process.Start("explorer.exe", "/select,\"" + filePath + "\""); + } + catch + { + } + }); + AddItem("\ue8c8", "경로 복사", delegate + { + try + { + System.Windows.Clipboard.SetText(filePath); + ShowToast("경로 복사됨"); + } + catch + { + } + }); + AddSep(); + AddItem("\ue8ac", "이름 변경", delegate + { + string path = System.IO.Path.GetDirectoryName(filePath) ?? ""; + string fileName = System.IO.Path.GetFileName(filePath); + InputDialog inputDialog = new InputDialog("이름 변경", "새 파일 이름:", fileName) + { + Owner = this + }; + if (inputDialog.ShowDialog() == true && !string.IsNullOrWhiteSpace(inputDialog.ResponseText)) + { + string destFileName = System.IO.Path.Combine(path, inputDialog.ResponseText.Trim()); + try + { + File.Move(filePath, destFileName); + BuildFileTree(); + ShowToast("이름 변경: " + inputDialog.ResponseText.Trim()); + } + catch (Exception ex) + { + ShowToast("이름 변경 실패: " + ex.Message, "\ue783"); + } + } + }); + AddItem("\ue74d", "삭제", delegate + { + MessageBoxResult messageBoxResult = System.Windows.MessageBox.Show("파일을 삭제하시겠습니까?\n" + System.IO.Path.GetFileName(filePath), "파일 삭제 확인", MessageBoxButton.YesNo, MessageBoxImage.Exclamation); + if (messageBoxResult == MessageBoxResult.Yes) + { + try + { + File.Delete(filePath); + BuildFileTree(); + ShowToast("파일 삭제됨"); + } + catch (Exception ex) + { + ShowToast("삭제 실패: " + ex.Message, "\ue783"); + } + } + }, solidColorBrush, solidColorBrush); + ((DispatcherObject)this).Dispatcher.BeginInvoke((Delegate)(Action)delegate + { + popup.IsOpen = true; + }, (DispatcherPriority)5, Array.Empty()); + void AddItem(string icon, string label, Action action, System.Windows.Media.Brush? labelColor = null, System.Windows.Media.Brush? iconColor = null) + { + StackPanel stackPanel = new StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + stackPanel.Children.Add(new TextBlock + { + Text = icon, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 13.0, + Foreground = (iconColor ?? secondaryText), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0.0, 0.0, 10.0, 0.0) + }); + stackPanel.Children.Add(new TextBlock + { + Text = label, + FontSize = 12.5, + Foreground = (labelColor ?? primaryText), + VerticalAlignment = VerticalAlignment.Center + }); + Border border = new Border + { + Child = stackPanel, + Background = System.Windows.Media.Brushes.Transparent, + CornerRadius = new CornerRadius(7.0), + Cursor = System.Windows.Input.Cursors.Hand, + Padding = new Thickness(10.0, 8.0, 14.0, 8.0), + Margin = new Thickness(0.0, 1.0, 0.0, 1.0) + }; + border.MouseEnter += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border2) + { + border2.Background = hoverBg; + } + }; + border.MouseLeave += delegate(object s, System.Windows.Input.MouseEventArgs _) + { + if (s is Border border2) + { + border2.Background = System.Windows.Media.Brushes.Transparent; + } + }; + border.MouseLeftButtonUp += delegate + { + popup.IsOpen = false; + action(); + }; + panel.Children.Add(border); + } + void AddSep() + { + panel.Children.Add(new Border + { + Height = 1.0, + Margin = new Thickness(10.0, 4.0, 10.0, 4.0), + Background = borderBrush, + Opacity = 0.3 + }); + } + } + + private void RefreshFileTreeIfVisible() + { + //IL_0028: Unknown result type (might be due to invalid IL or missing references) + //IL_002d: Unknown result type (might be due to invalid IL or missing references) + //IL_0047: Expected O, but got Unknown + if (FileBrowserPanel.Visibility == Visibility.Visible) + { + DispatcherTimer? fileBrowserRefreshTimer = _fileBrowserRefreshTimer; + if (fileBrowserRefreshTimer != null) + { + fileBrowserRefreshTimer.Stop(); + } + _fileBrowserRefreshTimer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(500.0) + }; + _fileBrowserRefreshTimer.Tick += delegate + { + _fileBrowserRefreshTimer.Stop(); + BuildFileTree(); + }; + _fileBrowserRefreshTimer.Start(); + } + } + + private void UpdateStatusBar(AgentEvent evt) + { + string toolName = evt.ToolName; + if (1 == 0) + { + } + string text; + switch (toolName) + { + case "sub_agent": + text = "서브에이전트"; + break; + case "file_read": + case "document_read": + text = "파일 읽기"; + break; + case "file_write": + text = "파일 쓰기"; + break; + case "file_edit": + text = "파일 수정"; + break; + case "html_create": + text = "HTML 생성"; + break; + case "xlsx_create": + text = "Excel 생성"; + break; + case "docx_create": + text = "Word 생성"; + break; + case "csv_create": + text = "CSV 생성"; + break; + case "md_create": + text = "Markdown 생성"; + break; + case "folder_map": + text = "폴더 탐색"; + break; + case "glob": + text = "파일 검색"; + break; + case "grep": + text = "내용 검색"; + break; + case "process": + text = "명령 실행"; + break; + default: + text = evt.ToolName; + break; + } + if (1 == 0) + { + } + string text2 = text; + switch (evt.Type) + { + case AgentEventType.Thinking: + SetStatus("생각 중...", spinning: true); + break; + case AgentEventType.Planning: + SetStatus($"계획 수립 중 — {evt.StepTotal}단계", spinning: true); + break; + case AgentEventType.ToolCall: + SetStatus(text2 + " 실행 중...", spinning: true); + break; + case AgentEventType.ToolResult: + SetStatus(evt.Success ? (text2 + " 완료") : (text2 + " 실패"), spinning: false); + break; + case AgentEventType.StepStart: + SetStatus($"[{evt.StepCurrent}/{evt.StepTotal}] {TruncateForStatus(evt.Summary)}", spinning: true); + break; + case AgentEventType.StepDone: + SetStatus($"[{evt.StepCurrent}/{evt.StepTotal}] 단계 완료", spinning: true); + break; + case AgentEventType.SkillCall: + SetStatus("스킬 실행 중: " + TruncateForStatus(evt.Summary), spinning: true); + break; + case AgentEventType.Complete: + SetStatus("작업 완료", spinning: false); + StopStatusAnimation(); + break; + case AgentEventType.Error: + SetStatus("오류 발생", spinning: false); + StopStatusAnimation(); + break; + case AgentEventType.Paused: + SetStatus("⏸ 일시정지", spinning: false); + break; + case AgentEventType.Resumed: + SetStatus("▶ 재개됨", spinning: true); + break; + case AgentEventType.Decision: + break; + } + } + + private void SetStatus(string text, bool spinning) + { + if (StatusLabel != null) + { + StatusLabel.Text = text; + } + if (spinning) + { + StartStatusAnimation(); + } + } + + private void StartStatusAnimation() + { + if (_statusSpinStoryboard == null) + { + DoubleAnimation doubleAnimation = new DoubleAnimation + { + From = 0.0, + To = 360.0, + Duration = TimeSpan.FromSeconds(2.0), + RepeatBehavior = RepeatBehavior.Forever + }; + _statusSpinStoryboard = new Storyboard(); + Storyboard.SetTarget((DependencyObject)(object)doubleAnimation, (DependencyObject)(object)StatusDiamond); + Storyboard.SetTargetProperty((DependencyObject)(object)doubleAnimation, new PropertyPath("(UIElement.RenderTransform).(RotateTransform.Angle)")); + _statusSpinStoryboard.Children.Add(doubleAnimation); + _statusSpinStoryboard.Begin(); + } + } + + private void StopStatusAnimation() + { + _statusSpinStoryboard?.Stop(); + _statusSpinStoryboard = null; + } + + private void SetStatusIdle() + { + StopStatusAnimation(); + if (StatusLabel != null) + { + StatusLabel.Text = "대기 중"; + } + if (StatusElapsed != null) + { + StatusElapsed.Text = ""; + } + if (StatusTokens != null) + { + StatusTokens.Text = ""; + } + } + + private void UpdateStatusTokens(int inputTokens, int outputTokens) + { + if (StatusTokens != null) + { + LlmSettings llm = _settings.Settings.Llm; + (double InputCost, double OutputCost) tuple = TokenEstimator.EstimateCost(inputTokens, outputTokens, llm.Service, llm.Model); + double item = tuple.InputCost; + double item2 = tuple.OutputCost; + double num = item + item2; + string value = ((num > 0.0) ? (" · " + TokenEstimator.FormatCost(num)) : ""); + StatusTokens.Text = $"↑{TokenEstimator.Format(inputTokens)} ↓{TokenEstimator.Format(outputTokens)}{value}"; + } + } + + private static string TruncateForStatus(string? text, int max = 40) + { + if (string.IsNullOrEmpty(text)) + { + return ""; + } + return (text.Length <= max) ? text : (text.Substring(0, max) + "…"); + } + + private static SolidColorBrush BrushFromHex(string hex) + { + System.Windows.Media.Color color = (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(hex); + return new SolidColorBrush(color); + } + + [DebuggerNonUserCode] + [GeneratedCode("PresentationBuildTasks", "10.0.5.0")] + public void InitializeComponent() + { + if (!_contentLoaded) + { + _contentLoaded = true; + Uri resourceLocator = new Uri("/AxCopilot;component/views/chatwindow.xaml", UriKind.Relative); + System.Windows.Application.LoadComponent(this, resourceLocator); + } + } + + [DebuggerNonUserCode] + [GeneratedCode("PresentationBuildTasks", "10.0.5.0")] + [EditorBrowsable(EditorBrowsableState.Never)] + void IComponentConnector.Connect(int connectionId, object target) + { + switch (connectionId) + { + case 1: + IconBarColumn = (ColumnDefinition)target; + break; + case 2: + SidebarColumn = (ColumnDefinition)target; + break; + case 3: + SplitterColumn = (ColumnDefinition)target; + break; + case 4: + PreviewColumn = (ColumnDefinition)target; + break; + case 5: + IconBarPanel = (Border)target; + break; + case 6: + ((System.Windows.Controls.Button)target).Click += BtnNewChat_Click; + break; + case 7: + ((System.Windows.Controls.Button)target).Click += BtnToggleSidebar_Click; + break; + case 8: + ((System.Windows.Controls.Button)target).Click += BtnCategoryDrop_Click; + break; + case 9: + BtnUserIconBar = (System.Windows.Controls.Button)target; + break; + case 10: + UserInitialIconBar = (TextBlock)target; + break; + case 11: + SidebarPanel = (Border)target; + break; + case 12: + BtnNewChat = (System.Windows.Controls.Button)target; + BtnNewChat.Click += BtnNewChat_Click; + break; + case 13: + SearchBox = (System.Windows.Controls.TextBox)target; + SearchBox.TextChanged += SearchBox_TextChanged; + break; + case 14: + BtnCategoryDrop = (System.Windows.Controls.Button)target; + BtnCategoryDrop.Click += BtnCategoryDrop_Click; + break; + case 15: + CategoryIcon = (TextBlock)target; + break; + case 16: + CategoryLabel = (TextBlock)target; + break; + case 17: + ConversationPanel = (StackPanel)target; + break; + case 18: + BtnDeleteAll = (System.Windows.Controls.Button)target; + BtnDeleteAll.Click += BtnDeleteAll_Click; + break; + case 19: + UserInitialSidebar = (TextBlock)target; + break; + case 20: + UserNameText = (TextBlock)target; + break; + case 21: + UserPcText = (TextBlock)target; + break; + case 22: + ChatTitle = (TextBlock)target; + ChatTitle.MouseLeftButtonDown += ChatTitle_MouseDown; + break; + case 23: + ChatTitleEdit = (System.Windows.Controls.TextBox)target; + ChatTitleEdit.LostFocus += ChatTitleEdit_LostFocus; + ChatTitleEdit.KeyDown += ChatTitleEdit_KeyDown; + break; + case 24: + BtnPreviewToggle = (System.Windows.Controls.Button)target; + BtnPreviewToggle.Click += BtnPreviewToggle_Click; + break; + case 25: + PreviewDot = (Ellipse)target; + break; + case 26: + AgentProgressBar = (Border)target; + break; + case 27: + ProgressIcon = (TextBlock)target; + break; + case 28: + ProgressStepLabel = (TextBlock)target; + break; + case 29: + ProgressFill = (Border)target; + break; + case 30: + ProgressPercent = (TextBlock)target; + break; + case 31: + ProgressElapsed = (TextBlock)target; + break; + case 32: + BtnToggleSidebar = (System.Windows.Controls.Button)target; + BtnToggleSidebar.Click += BtnToggleSidebar_Click; + break; + case 33: + ToggleSidebarIcon = (TextBlock)target; + break; + case 34: + TabChat = (System.Windows.Controls.RadioButton)target; + TabChat.Checked += TabChat_Checked; + break; + case 35: + TabCowork = (System.Windows.Controls.RadioButton)target; + TabCowork.Checked += TabCowork_Checked; + break; + case 36: + TabCode = (System.Windows.Controls.RadioButton)target; + TabCode.Checked += TabCode_Checked; + break; + case 37: + ((System.Windows.Controls.Button)target).Click += BtnSettings_Click; + break; + case 38: + ((System.Windows.Controls.Button)target).Click += BtnMinimize_Click; + break; + case 39: + ((System.Windows.Controls.Button)target).Click += BtnMaximize_Click; + break; + case 40: + MaximizeIcon = (TextBlock)target; + break; + case 41: + ((System.Windows.Controls.Button)target).Click += BtnClose_Click; + break; + case 42: + MessageSearchBar = (Border)target; + break; + case 43: + SearchTextBox = (System.Windows.Controls.TextBox)target; + SearchTextBox.TextChanged += SearchTextBox_TextChanged; + break; + case 44: + SearchResultCount = (TextBlock)target; + break; + case 45: + ((System.Windows.Controls.Button)target).Click += SearchPrev_Click; + break; + case 46: + ((System.Windows.Controls.Button)target).Click += SearchNext_Click; + break; + case 47: + ((System.Windows.Controls.Button)target).Click += SearchClose_Click; + break; + case 48: + MessageScroll = (ScrollViewer)target; + break; + case 49: + MessagePanel = (StackPanel)target; + break; + case 50: + EmptyState = (StackPanel)target; + break; + case 51: + EmptyIcon = (Border)target; + break; + case 52: + EmptyIconTranslate = (TranslateTransform)target; + break; + case 53: + EmptyStateTitle = (TextBlock)target; + break; + case 54: + EmptyStateDesc = (TextBlock)target; + break; + case 55: + TopicButtonPanel = (WrapPanel)target; + break; + case 56: + TemplatePopup = (Popup)target; + break; + case 57: + TemplateItems = (ItemsControl)target; + break; + case 58: + TemplateEmptyHint = (TextBlock)target; + break; + case 59: + PermissionPopup = (Popup)target; + break; + case 60: + PermissionItems = (StackPanel)target; + break; + case 61: + DataUsagePopup = (Popup)target; + break; + case 62: + DataUsageItems = (StackPanel)target; + break; + case 63: + SlashPopup = (Popup)target; + break; + case 64: + ((Border)target).PreviewMouseWheel += SlashPopup_PreviewMouseWheel; + break; + case 65: + SlashPopupTitle = (TextBlock)target; + break; + case 66: + SlashPopupHint = (TextBlock)target; + break; + case 67: + SlashNavUp = (Border)target; + break; + case 68: + SlashNavUpText = (TextBlock)target; + break; + case 69: + SlashItems = (ItemsControl)target; + break; + case 70: + SlashNavDown = (Border)target; + break; + case 71: + SlashNavDownText = (TextBlock)target; + break; + case 72: + FolderMenuPopup = (Popup)target; + break; + case 73: + FolderMenuItems = (StackPanel)target; + break; + case 74: + ToastBorder = (Border)target; + break; + case 75: + ToastIcon = (TextBlock)target; + break; + case 76: + ToastText = (TextBlock)target; + break; + case 77: + DraftPreviewCard = (Border)target; + break; + case 78: + DraftPreviewText = (TextBlock)target; + break; + case 79: + BtnDraftEnqueue = (System.Windows.Controls.Button)target; + BtnDraftEnqueue.Click += BtnDraftEnqueue_Click; + break; + case 80: + BtnDraftEdit = (System.Windows.Controls.Button)target; + BtnDraftEdit.Click += BtnDraftEdit_Click; + break; + case 81: + BtnDraftClear = (System.Windows.Controls.Button)target; + BtnDraftClear.Click += BtnDraftClear_Click; + break; + case 82: + DraftQueuePanel = (StackPanel)target; + break; + case 83: + InputGlowBorder = (Border)target; + break; + case 84: + RainbowBrush = (LinearGradientBrush)target; + break; + case 85: + InputBorder = (Border)target; + break; + case 86: + BtnModelSelector = (System.Windows.Controls.Button)target; + BtnModelSelector.Click += BtnModelSelector_Click; + break; + case 87: + ModelLabel = (TextBlock)target; + break; + case 88: + BtnTemplateSelector = (System.Windows.Controls.Button)target; + BtnTemplateSelector.Click += BtnTemplateSelector_Click; + break; + case 89: + AttachedFilesPanel = (ItemsControl)target; + break; + case 90: + InputBox = (System.Windows.Controls.TextBox)target; + InputBox.PreviewKeyDown += InputBox_PreviewKeyDown; + InputBox.TextChanged += InputBox_TextChanged; + break; + case 91: + InputWatermark = (TextBlock)target; + break; + case 92: + SlashCommandChip = (Border)target; + break; + case 93: + SlashChipText = (TextBlock)target; + break; + case 94: + SlashChipClose = (Border)target; + break; + case 95: + BtnAttach = (System.Windows.Controls.Button)target; + BtnAttach.Click += BtnAttach_Click; + break; + case 96: + ((System.Windows.Controls.Button)target).Click += BtnExport_Click; + break; + case 97: + BtnPause = (Border)target; + BtnPause.MouseLeftButtonUp += BtnPause_Click; + break; + case 98: + PauseIcon = (TextBlock)target; + break; + case 99: + BtnStop = (System.Windows.Controls.Button)target; + BtnStop.Click += BtnStop_Click; + break; + case 100: + BtnSend = (System.Windows.Controls.Button)target; + BtnSend.Click += BtnSend_Click; + break; + case 101: + FolderBar = (Border)target; + break; + case 102: + FolderPathLabel = (TextBlock)target; + FolderPathLabel.MouseLeftButtonUp += FolderPathLabel_Click; + break; + case 103: + ((System.Windows.Controls.Button)target).Click += BtnFolderClear_Click; + break; + case 104: + MoodIconPanel = (StackPanel)target; + break; + case 105: + FormatMoodSeparator = (Border)target; + break; + case 106: + BtnDataUsage = (Border)target; + BtnDataUsage.MouseLeftButtonUp += BtnDataUsage_Click; + break; + case 107: + DataUsageIcon = (TextBlock)target; + break; + case 108: + DataUsageLabel = (TextBlock)target; + break; + case 109: + BtnPermission = (System.Windows.Controls.Button)target; + BtnPermission.Click += BtnPermission_Click; + break; + case 110: + PermissionIcon = (TextBlock)target; + break; + case 111: + PermissionLabel = (TextBlock)target; + break; + case 112: + FormatMenuPopup = (Popup)target; + break; + case 113: + FormatMenuItems = (StackPanel)target; + break; + case 114: + MoodMenuPopup = (Popup)target; + break; + case 115: + MoodMenuItems = (StackPanel)target; + break; + case 116: + AutoPermissionWarning = (Border)target; + break; + case 117: + BtnAutoWarningClose = (System.Windows.Controls.Button)target; + BtnAutoWarningClose.Click += BtnAutoWarningClose_Click; + break; + case 118: + FileBrowserPanel = (Border)target; + break; + case 119: + FileBrowserTitle = (TextBlock)target; + break; + case 120: + ((System.Windows.Controls.Button)target).Click += BtnFileBrowserRefresh_Click; + break; + case 121: + ((System.Windows.Controls.Button)target).Click += BtnFileBrowserOpenFolder_Click; + break; + case 122: + ((System.Windows.Controls.Button)target).Click += BtnFileBrowserClose_Click; + break; + case 123: + FileTreeView = (System.Windows.Controls.TreeView)target; + break; + case 124: + StatusDiamond = (TextBlock)target; + break; + case 125: + StatusDiamondRotate = (RotateTransform)target; + break; + case 126: + StatusLabel = (TextBlock)target; + break; + case 127: + BtnToggleExecutionLog = (System.Windows.Controls.Button)target; + BtnToggleExecutionLog.Click += BtnToggleExecutionLog_Click; + break; + case 128: + ExecutionLogIcon = (TextBlock)target; + break; + case 129: + ExecutionLogLabel = (TextBlock)target; + break; + case 130: + SubAgentIndicator = (Border)target; + break; + case 131: + SubAgentIndicatorLabel = (TextBlock)target; + break; + case 132: + BtnShowAnalyzer = (Border)target; + BtnShowAnalyzer.MouseLeftButtonUp += BtnShowAnalyzer_Click; + break; + case 133: + StatusElapsed = (TextBlock)target; + break; + case 134: + StatusTokens = (TextBlock)target; + break; + case 135: + PreviewSplitter = (GridSplitter)target; + break; + case 136: + PreviewPanel = (Border)target; + break; + case 137: + ((Border)target).PreviewMouseDown += PreviewTabBar_PreviewMouseDown; + break; + case 138: + PreviewTabPanel = (StackPanel)target; + break; + case 139: + BtnOpenExternal = (System.Windows.Controls.Button)target; + BtnOpenExternal.Click += BtnOpenExternal_Click; + break; + case 140: + ((System.Windows.Controls.Button)target).Click += BtnClosePreview_Click; + break; + case 141: + PreviewWebView = (WebView2)target; + break; + case 142: + PreviewTextScroll = (ScrollViewer)target; + break; + case 143: + PreviewTextBlock = (TextBlock)target; + break; + case 144: + PreviewDataGrid = (DataGrid)target; + break; + case 145: + PreviewEmpty = (TextBlock)target; + break; + case 146: + ResizeGrip = (Thumb)target; + ResizeGrip.DragDelta += ResizeGrip_DragDelta; + break; + default: + _contentLoaded = true; + break; + } + } + + static ChatWindow() + { + Dictionary> dictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); + int num = 4; + List<(string, string, string)> list = new List<(string, string, string)>(num); + CollectionsMarshal.SetCount(list, num); + Span<(string, string, string)> span = CollectionsMarshal.AsSpan(list); + span[0] = ("코드 리뷰", "\ue943", "첨부된 코드를 리뷰해 주세요. 버그, 성능 이슈, 보안 취약점, 개선점을 찾아 구체적으로 제안하세요."); + span[1] = ("코드 설명", "\ue946", "첨부된 코드를 상세히 설명해 주세요. 주요 함수, 데이터 흐름, 설계 패턴을 포함하세요."); + span[2] = ("리팩토링 제안", "\ue70f", "첨부된 코드의 리팩토링 방안을 제안해 주세요. 가독성, 유지보수성, 성능을 고려하세요."); + span[3] = ("테스트 생성", "\ue9d5", "첨부된 코드에 대한 단위 테스트 코드를 생성해 주세요."); + dictionary["code"] = list; + num = 3; + List<(string, string, string)> list2 = new List<(string, string, string)>(num); + CollectionsMarshal.SetCount(list2, num); + Span<(string, string, string)> span2 = CollectionsMarshal.AsSpan(list2); + span2[0] = ("요약", "\ue8ab", "첨부된 문서를 핵심 포인트 위주로 간결하게 요약해 주세요."); + span2[1] = ("분석", "\ue9d9", "첨부된 문서의 내용을 분석하고 주요 인사이트를 도출해 주세요."); + span2[2] = ("번역", "\ue8c1", "첨부된 문서를 영어로 번역해 주세요. 원문의 톤과 뉘앙스를 유지하세요."); + dictionary["document"] = list2; + num = 3; + List<(string, string, string)> list3 = new List<(string, string, string)>(num); + CollectionsMarshal.SetCount(list3, num); + Span<(string, string, string)> span3 = CollectionsMarshal.AsSpan(list3); + span3[0] = ("데이터 분석", "\ue9d9", "첨부된 데이터를 분석해 주세요. 통계, 추세, 이상치를 찾아 보고하세요."); + span3[1] = ("시각화 제안", "\ue9d9", "첨부된 데이터를 시각화할 최적의 차트 유형을 제안하고 chart_create로 생성해 주세요."); + span3[2] = ("포맷 변환", "\ue8ab", "첨부된 데이터를 다른 형식으로 변환해 주세요. (CSV↔JSON↔Excel 등)"); + dictionary["data"] = list3; + num = 2; + List<(string, string, string)> list4 = new List<(string, string, string)>(num); + CollectionsMarshal.SetCount(list4, num); + Span<(string, string, string)> span4 = CollectionsMarshal.AsSpan(list4); + span4[0] = ("이미지 설명", "\ue946", "첨부된 이미지를 자세히 설명해 주세요. 내용, 레이아웃, 텍스트를 분석하세요."); + span4[1] = ("UI 리뷰", "\ue70f", "첨부된 UI 스크린샷을 리뷰해 주세요. UX 개선점, 접근성, 디자인 일관성을 평가하세요."); + dictionary["image"] = list4; + DropActions = dictionary; + CodeExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) + { + ".cs", ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".cpp", ".c", ".h", + ".go", ".rs", ".rb", ".php", ".swift", ".kt", ".scala", ".sh", ".ps1", ".bat", + ".cmd", ".sql", ".xaml", ".vue" + }; + DataExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { ".csv", ".json", ".xml", ".yaml", ".yml", ".tsv" }; + WriteToolNames = new HashSet(StringComparer.OrdinalIgnoreCase) { "file_write", "file_edit", "html_create", "xlsx_create", "docx_create", "csv_create", "md_create", "script_create", "diff_preview", "open_external" }; + Tips = new string[24] + { + "\ud83d\udca1 작업 폴더에 AX.md 파일을 만들면 매번 시스템 프롬프트에 자동 주입됩니다. 프로젝트 설계 원칙이나 코딩 규칙을 기록하세요.", "\ud83d\udca1 Ctrl+1/2/3으로 Chat/Cowork/Code 탭을 빠르게 전환할 수 있습니다.", "\ud83d\udca1 Ctrl+F로 현재 대화 내 메시지를 검색할 수 있습니다.", "\ud83d\udca1 메시지를 우클릭하면 복사, 인용 답장, 재생성, 삭제를 할 수 있습니다.", "\ud83d\udca1 코드 블록을 더블클릭하면 전체화면으로 볼 수 있고, \ud83d\udcbe 버튼으로 파일 저장이 가능합니다.", "\ud83d\udca1 Cowork 에이전트가 만든 파일은 자동으로 날짜_시간 접미사가 붙어 덮어쓰기를 방지합니다.", "\ud83d\udca1 Code 탭에서 개발 언어를 선택하면 해당 언어 우선으로 코드를 생성합니다.", "\ud83d\udca1 파일 탐색기(하단 바 '파일' 버튼)에서 더블클릭으로 프리뷰, 우클릭으로 관리할 수 있습니다.", "\ud83d\udca1 에이전트가 계획을 제시하면 '수정 요청'으로 방향을 바꾸거나 '취소'로 중단할 수 있습니다.", "\ud83d\udca1 Code 탭은 빌드/테스트를 자동으로 실행합니다. 프로젝트 폴더를 먼저 선택하세요.", + "\ud83d\udca1 무드 갤러리에서 10가지 디자인 템플릿 중 원하는 스타일을 미리보기로 선택할 수 있습니다.", "\ud83d\udca1 Git 연동: Code 탭에서 에이전트가 git status, diff, commit을 수행합니다. (push는 직접)", "\ud83d\udca1 설정 → AX Agent → 공통에서 개발자 모드를 켜면 에이전트 동작을 스텝별로 검증할 수 있습니다.", "\ud83d\udca1 트레이 아이콘 우클릭 → '사용 통계'에서 대화 빈도와 토큰 사용량을 확인할 수 있습니다.", "\ud83d\udca1 대화 제목을 클릭하면 이름을 변경할 수 있습니다.", "\ud83d\udca1 LLM 오류 발생 시 '재시도' 버튼이 자동으로 나타납니다.", "\ud83d\udca1 검색란에서 대화 제목뿐 아니라 첫 메시지 내용까지 검색됩니다.", "\ud83d\udca1 프리셋 선택 후에도 대화가 리셋되지 않습니다. 진행 중인 대화에서 프리셋을 변경할 수 있습니다.", "\ud83d\udca1 Shift+Enter로 퍼지 검색 결과의 파일이 있는 폴더를 열 수 있습니다.", "\ud83d\udca1 최근 폴더를 우클릭하면 '폴더 열기', '경로 복사', '목록에서 삭제'가 가능합니다.", + "\ud83d\udca1 Cowork/Code 에이전트 작업 완료 시 시스템 트레이에 알림이 표시됩니다.", "\ud83d\udca1 마크다운 테이블, 인용(>), 취소선(~~), 링크([text](url))가 모두 렌더링됩니다.", "\ud83d\udca1 ⚠ 데이터 폴더를 워크스페이스로 지정할 때는 반드시 백업을 먼저 만드세요!", "\ud83d\udca1 드라이브 루트(C:\\, D:\\)는 작업공간으로 설정할 수 없습니다. 하위 폴더를 선택하세요." + }; + GeminiModels = new(string, string)[5] + { + ("gemini-2.5-pro", "Gemini 2.5 Pro"), + ("gemini-2.5-flash", "Gemini 2.5 Flash"), + ("gemini-2.5-flash-lite", "Gemini 2.5 Flash Lite"), + ("gemini-2.0-flash", "Gemini 2.0 Flash"), + ("gemini-2.0-flash-lite", "Gemini 2.0 Flash Lite") + }; + ClaudeModels = new(string, string)[5] + { + ("claude-opus-4-6", "Claude Opus 4.6"), + ("claude-sonnet-4-6", "Claude Sonnet 4.6"), + ("claude-haiku-4-5-20251001", "Claude Haiku 4.5"), + ("claude-sonnet-4-5-20250929", "Claude Sonnet 4.5"), + ("claude-opus-4-20250514", "Claude Opus 4") + }; + _previewableExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { ".html", ".htm", ".md", ".txt", ".csv", ".json", ".xml", ".log" }; + WebView2DataFolder = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "WebView2"); + _ignoredDirs = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "bin", "obj", "node_modules", ".git", ".vs", ".idea", ".vscode", "__pycache__", ".mypy_cache", ".pytest_cache", + "dist", "build", ".cache", ".next", ".nuxt", "coverage", ".terraform" + }; + } +} diff --git a/.decompiledproj/--z__ReadOnlyArray.cs b/.decompiledproj/--z__ReadOnlyArray.cs new file mode 100644 index 0000000..840f515 --- /dev/null +++ b/.decompiledproj/--z__ReadOnlyArray.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +[CompilerGenerated] +internal sealed class _003C_003Ez__ReadOnlyArray : IEnumerable, ICollection, IList, IEnumerable, IReadOnlyCollection, IReadOnlyList, ICollection, IList +{ + int ICollection.Count => _items.Length; + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => this; + + object? IList.this[int index] + { + get + { + return _items[index]; + } + set + { + throw new NotSupportedException(); + } + } + + bool IList.IsFixedSize => true; + + bool IList.IsReadOnly => true; + + int IReadOnlyCollection.Count => _items.Length; + + T IReadOnlyList.this[int index] => _items[index]; + + int ICollection.Count => _items.Length; + + bool ICollection.IsReadOnly => true; + + T IList.this[int index] + { + get + { + return _items[index]; + } + set + { + throw new NotSupportedException(); + } + } + + public _003C_003Ez__ReadOnlyArray(T[] items) + { + _items = items; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_items).GetEnumerator(); + } + + void ICollection.CopyTo(Array array, int index) + { + ((ICollection)_items).CopyTo(array, index); + } + + int IList.Add(object? value) + { + throw new NotSupportedException(); + } + + void IList.Clear() + { + throw new NotSupportedException(); + } + + bool IList.Contains(object? value) + { + return ((IList)_items).Contains(value); + } + + int IList.IndexOf(object? value) + { + return ((IList)_items).IndexOf(value); + } + + void IList.Insert(int index, object? value) + { + throw new NotSupportedException(); + } + + void IList.Remove(object? value) + { + throw new NotSupportedException(); + } + + void IList.RemoveAt(int index) + { + throw new NotSupportedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_items).GetEnumerator(); + } + + void ICollection.Add(T item) + { + throw new NotSupportedException(); + } + + void ICollection.Clear() + { + throw new NotSupportedException(); + } + + bool ICollection.Contains(T item) + { + return ((ICollection)_items).Contains(item); + } + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + ((ICollection)_items).CopyTo(array, arrayIndex); + } + + bool ICollection.Remove(T item) + { + throw new NotSupportedException(); + } + + int IList.IndexOf(T item) + { + return ((IList)_items).IndexOf(item); + } + + void IList.Insert(int index, T item) + { + throw new NotSupportedException(); + } + + void IList.RemoveAt(int index) + { + throw new NotSupportedException(); + } +} diff --git a/.decompiledproj/--z__ReadOnlySingleElementList.cs b/.decompiledproj/--z__ReadOnlySingleElementList.cs new file mode 100644 index 0000000..05fd541 --- /dev/null +++ b/.decompiledproj/--z__ReadOnlySingleElementList.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +[CompilerGenerated] +internal sealed class _003C_003Ez__ReadOnlySingleElementList : IEnumerable, ICollection, IList, IEnumerable, IReadOnlyCollection, IReadOnlyList, ICollection, IList +{ + private sealed class Enumerator : IDisposable, IEnumerator, IEnumerator + { + object IEnumerator.Current => _item; + + T IEnumerator.Current => _item; + + public Enumerator(T item) + { + _item = item; + } + + bool IEnumerator.MoveNext() + { + return !_moveNextCalled && (_moveNextCalled = true); + } + + void IEnumerator.Reset() + { + _moveNextCalled = false; + } + + void IDisposable.Dispose() + { + } + } + + int ICollection.Count => 1; + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => this; + + object? IList.this[int index] + { + get + { + if (index != 0) + { + throw new IndexOutOfRangeException(); + } + return _item; + } + set + { + throw new NotSupportedException(); + } + } + + bool IList.IsFixedSize => true; + + bool IList.IsReadOnly => true; + + int IReadOnlyCollection.Count => 1; + + T IReadOnlyList.this[int index] + { + get + { + if (index != 0) + { + throw new IndexOutOfRangeException(); + } + return _item; + } + } + + int ICollection.Count => 1; + + bool ICollection.IsReadOnly => true; + + T IList.this[int index] + { + get + { + if (index != 0) + { + throw new IndexOutOfRangeException(); + } + return _item; + } + set + { + throw new NotSupportedException(); + } + } + + public _003C_003Ez__ReadOnlySingleElementList(T item) + { + _item = item; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(_item); + } + + void ICollection.CopyTo(Array array, int index) + { + array.SetValue(_item, index); + } + + int IList.Add(object? value) + { + throw new NotSupportedException(); + } + + void IList.Clear() + { + throw new NotSupportedException(); + } + + bool IList.Contains(object? value) + { + return EqualityComparer.Default.Equals(_item, (T)value); + } + + int IList.IndexOf(object? value) + { + return (!EqualityComparer.Default.Equals(_item, (T)value)) ? (-1) : 0; + } + + void IList.Insert(int index, object? value) + { + throw new NotSupportedException(); + } + + void IList.Remove(object? value) + { + throw new NotSupportedException(); + } + + void IList.RemoveAt(int index) + { + throw new NotSupportedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(_item); + } + + void ICollection.Add(T item) + { + throw new NotSupportedException(); + } + + void ICollection.Clear() + { + throw new NotSupportedException(); + } + + bool ICollection.Contains(T item) + { + return EqualityComparer.Default.Equals(_item, item); + } + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + array[arrayIndex] = _item; + } + + bool ICollection.Remove(T item) + { + throw new NotSupportedException(); + } + + int IList.IndexOf(T item) + { + return (!EqualityComparer.Default.Equals(_item, item)) ? (-1) : 0; + } + + void IList.Insert(int index, T item) + { + throw new NotSupportedException(); + } + + void IList.RemoveAt(int index) + { + throw new NotSupportedException(); + } +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.code_개발.json b/.decompiledproj/AxCopilot.Assets.Presets.code_개발.json new file mode 100644 index 0000000..cd7859e --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.code_개발.json @@ -0,0 +1,11 @@ +{ + "category": "코드개발", + "tab": "Code", + "order": 10, + "label": "코드 개발", + "symbol": "\uE943", + "color": "#3B82F6", + "description": "새 기능 개발, 코드 작성, 프로젝트 구성", + "systemPrompt": "당신은 AX Copilot Code Agent — 사내 소프트웨어 개발 전문 에이전트입니다.\n\n## 역할\n새 기능 개발, 코드 작성, 프로젝트 구성을 담당합니다.\n\n## 워크플로우\n1. dev_env_detect로 설치된 개발 도구 확인\n2. folder_map + grep으로 기존 코드베이스 구조 분석\n3. 기존 코드 패턴과 컨벤션을 파악 (네이밍, 아키텍처, 의존성)\n4. 단계별 구현 계획을 사용자에게 제시\n5. 승인 후 file_write/file_edit으로 코드 작성\n6. build_run으로 빌드 및 테스트 검증\n\n## 핵심 원칙\n- 기존 코드 스타일과 아키텍처 패턴을 따르세요\n- SOLID 원칙과 DRY 원칙을 준수하세요\n- 적절한 에러 처리와 로깅을 포함하세요\n- 의미 있는 변수/함수 이름을 사용하세요\n- 복잡한 로직에는 주석을 추가하세요\n- 새 의존성 추가 시 사내 Nexus 저장소를 우선 사용하세요", + "placeholder": "어떤 기능을 개발할까요? (프로젝트 폴더를 먼저 선택하세요)" +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.code_리뷰.json b/.decompiledproj/AxCopilot.Assets.Presets.code_리뷰.json new file mode 100644 index 0000000..38308c9 --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.code_리뷰.json @@ -0,0 +1,11 @@ +{ + "category": "코드리뷰", + "tab": "Code", + "order": 20, + "label": "코드 리뷰", + "symbol": "\uE71B", + "color": "#10B981", + "description": "코드 품질 분석, 모범 사례 검토, 개선 제안", + "systemPrompt": "당신은 AX Copilot Code Reviewer — 코드 품질 분석 전문 에이전트입니다.\n\n## 역할\n코드 리뷰를 수행하여 품질, 가독성, 유지보수성, 성능을 평가합니다.\n\n## 리뷰 관점 (Google Code Review 가이드 기반)\n1. **정확성**: 논리 오류, 경계 조건, null 처리\n2. **가독성**: 네이밍, 주석, 코드 구조\n3. **유지보수성**: 결합도, 응집도, 확장성\n4. **성능**: 불필요한 연산, 메모리 누수, N+1 쿼리\n5. **보안**: 입력 검증, SQL 인젝션, XSS, 하드코딩된 시크릿\n6. **테스트**: 테스트 커버리지, 엣지 케이스\n\n## 워크플로우\n1. folder_map으로 프로젝트 전체 구조 파악\n2. 대상 파일을 file_read로 꼼꼼히 읽기\n3. 관련 파일도 grep/glob으로 확인 (의존성, 호출 관계)\n4. 이슈별로 분류하여 리뷰 의견 제시:\n - [CRITICAL] 반드시 수정해야 하는 문제\n - [WARNING] 개선을 권장하는 부분\n - [INFO] 참고 사항\n - [GOOD] 잘 작성된 부분 (칭찬)\n5. 전체 코드 품질을 A~F 등급으로 평가\n6. 개선 우선순위 제안\n\n## 출력 형식\n리뷰 결과를 구조화된 보고서로 작성하세요:\n- 파일별 이슈 목록 (라인 번호 포함)\n- 종합 평가 및 등급\n- 개선 액션 플랜", + "placeholder": "어떤 코드를 리뷰할까요?" +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.code_리팩터링.json b/.decompiledproj/AxCopilot.Assets.Presets.code_리팩터링.json new file mode 100644 index 0000000..5800497 --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.code_리팩터링.json @@ -0,0 +1,11 @@ +{ + "category": "리팩터링", + "tab": "Code", + "order": 30, + "label": "코드 리팩터링", + "symbol": "\uE777", + "color": "#6366F1", + "description": "코드 구조 개선, 중복 제거, 성능 최적화", + "systemPrompt": "당신은 AX Copilot Refactoring Agent — 코드 품질 개선 전문 에이전트입니다.\n\n## 역할\n기존 코드의 구조를 개선하고 기술 부채를 줄이는 리팩터링을 수행합니다.\n\n## 리팩터링 원칙 (Martin Fowler 기반)\n- Extract Method: 긴 메서드를 의미 단위로 분리\n- Move Method/Field: 응집도가 높은 클래스로 이동\n- Replace Conditional with Polymorphism: 복잡한 조건문을 다형성으로\n- Introduce Parameter Object: 관련 파라미터 묶기\n- Replace Magic Number with Symbolic Constant\n\n## 워크플로우\n1. folder_map + grep으로 대상 코드 구조 분석\n2. 코드 스멜(Code Smell) 식별:\n - Long Method, Large Class, Feature Envy\n - Duplicate Code, Dead Code\n - God Object, Shotgun Surgery\n3. 리팩터링 계획을 사용자에게 제시 (변경 전/후 설명)\n4. 승인 후 file_edit으로 점진적 수정 (한 번에 하나의 리팩터링)\n5. 각 단계마다 build_run으로 빌드/테스트 검증\n6. 동작 변경 없이 구조만 개선되었는지 확인\n\n## 주의사항\n- 기능을 변경하지 마세요 (행동 보존 리팩터링)\n- 테스트가 있으면 테스트를 먼저 실행하여 기준선 확보\n- 대규모 변경은 단계별로 나누어 진행\n- 변경 사항을 명확히 설명하세요", + "placeholder": "어떤 코드를 리팩터링할까요?" +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.code_보안점검.json b/.decompiledproj/AxCopilot.Assets.Presets.code_보안점검.json new file mode 100644 index 0000000..5ef5611 --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.code_보안점검.json @@ -0,0 +1,11 @@ +{ + "category": "보안점검", + "tab": "Code", + "order": 50, + "label": "보안 점검", + "symbol": "\uE72E", + "color": "#EF4444", + "description": "OWASP Top 10 기반 보안 취약점 분석", + "systemPrompt": "당신은 AX Copilot Security Analyst — 코드 보안 취약점 분석 전문 에이전트입니다.\n\n## 역할\nOWASP Top 10 및 CWE 기반으로 코드의 보안 취약점을 분석합니다.\n\n## 점검 항목 (OWASP Top 10 2021)\n1. **A01 Broken Access Control**: 권한 검증 누락, 경로 조작\n2. **A02 Cryptographic Failures**: 약한 암호화, 평문 저장, 하드코딩 키\n3. **A03 Injection**: SQL Injection, Command Injection, XSS, LDAP Injection\n4. **A04 Insecure Design**: 비즈니스 로직 결함, 경쟁 조건\n5. **A05 Security Misconfiguration**: 디폴트 설정, 불필요한 기능 활성\n6. **A06 Vulnerable Components**: 알려진 취약 라이브러리 사용\n7. **A07 Authentication Failures**: 약한 인증, 세션 관리 미흡\n8. **A08 Data Integrity Failures**: 직렬화 취약점, 무결성 검증 없음\n9. **A09 Logging Failures**: 민감 정보 로깅, 로그 부재\n10. **A10 SSRF**: Server-Side Request Forgery\n\n## 워크플로우\n1. folder_map으로 프로젝트 구조 + 설정 파일 확인\n2. grep으로 위험 패턴 검색:\n - 하드코딩된 비밀번호/API키: `password|secret|api_key|token`\n - SQL 문자열 조합: `SELECT.*\\+|string\\.Format.*SELECT`\n - 입력 미검증: `Request\\.|input\\.|args\\[`\n - 위험 함수: `eval|exec|system|Process\\.Start`\n3. 의심 파일을 file_read로 상세 분석\n4. 발견된 취약점을 위험도별 분류:\n - [CRITICAL] 즉시 수정 필요 (데이터 유출/코드 실행 가능)\n - [HIGH] 빠른 수정 권장\n - [MEDIUM] 개선 권장\n - [LOW] 참고\n5. 각 취약점에 대한 수정 코드 제안\n\n## 출력 형식\n보안 분석 보고서:\n- 발견 취약점 목록 (CWE 번호, 위험도, 파일:라인)\n- 수정 권장 사항\n- 전체 보안 수준 평가 (A~F)", + "placeholder": "어떤 코드의 보안 점검을 수행할까요?" +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.code_테스트.json b/.decompiledproj/AxCopilot.Assets.Presets.code_테스트.json new file mode 100644 index 0000000..6096945 --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.code_테스트.json @@ -0,0 +1,11 @@ +{ + "category": "테스트", + "tab": "Code", + "order": 40, + "label": "테스트 작성", + "symbol": "\uE9D5", + "color": "#F59E0B", + "description": "단위 테스트, 통합 테스트 작성 및 실행", + "systemPrompt": "당신은 AX Copilot Test Agent — 소프트웨어 테스트 전문 에이전트입니다.\n\n## 역할\n코드에 대한 단위 테스트, 통합 테스트를 작성하고 실행합니다.\n\n## 테스트 원칙 (Kent Beck TDD 기반)\n- **Arrange-Act-Assert**: 명확한 3단계 구조\n- **FIRST**: Fast, Independent, Repeatable, Self-validating, Timely\n- **하나의 테스트 = 하나의 검증**: 단일 책임\n- **경계값 테스트**: 0, 1, N, N+1, null, 빈 문자열\n- **실패 케이스 우선**: 정상 경로보다 에러 경로 먼저\n\n## 언어별 테스트 프레임워크\n- C#: xUnit, NUnit, MSTest + Moq/NSubstitute\n- Python: pytest, unittest + mock\n- Java: JUnit5, TestNG + Mockito\n- JavaScript: Jest, Vitest, Mocha + Testing Library\n- C++: Google Test, Catch2\n\n## 워크플로우\n1. dev_env_detect로 설치된 테스트 프레임워크 확인\n2. folder_map으로 기존 테스트 구조 파악 (테스트 디렉토리, 네이밍 패턴)\n3. 대상 코드를 file_read로 분석 (공개 API, 분기 경로)\n4. 테스트 계획 제시:\n - 테스트 대상 메서드/클래스\n - 테스트 시나리오 (정상/에러/경계)\n - 예상 커버리지\n5. 승인 후 테스트 코드 작성 (file_write)\n6. build_run action='test'로 실행 및 결과 확인\n7. 실패 테스트 분석 및 수정\n\n## 주의사항\n- 기존 테스트 파일의 네이밍 컨벤션을 따르세요\n- 테스트 데이터는 테스트 내에서 생성하세요 (외부 의존 최소화)\n- Mocking은 외부 의존성에만 사용하세요\n- 테스트 이름은 무엇을_어떤조건에서_어떻게 형식으로", + "placeholder": "어떤 코드에 테스트를 작성할까요?" +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.cowork_논문.json b/.decompiledproj/AxCopilot.Assets.Presets.cowork_논문.json new file mode 100644 index 0000000..21bb110 --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.cowork_논문.json @@ -0,0 +1,11 @@ +{ + "category": "논문", + "tab": "Cowork", + "order": 30, + "label": "논문 분석·작성", + "symbol": "\uE736", + "color": "#6366F1", + "description": "학술 논문 분석, 리뷰, 초안 작성을 지원합니다", + "systemPrompt": "당신은 AX Copilot Agent입니다. 학술 논문 분석과 작성을 전문적으로 지원합니다.\n\n## 핵심 원칙\n- 학술적 엄밀성을 유지합니다. 주장에는 근거를 명시합니다.\n- 논문 구조(Abstract, Introduction, Methods, Results, Discussion, Conclusion)를 준수합니다.\n- 기존 논문 분석 시: 연구 목적, 방법론, 핵심 발견, 한계점, 시사점을 체계적으로 정리합니다.\n- 문헌 리뷰 시: 논문 간 관계(지지/반박/보완)를 분석하고 연구 동향을 파악합니다.\n- 초안 작성 시: 연구 질문을 명확히 하고, 논리적 흐름을 갖춘 구조를 제안합니다.\n- 결과물은 HTML(.html) 또는 Word(.docx) 형식으로 작성합니다.\n\n## 문서 품질 가이드\n\n### HTML 논문 분석 (html_create)\n- **toc: true**, **numbered: true** 로 목차와 섹션 번호 자동 생성.\n- mood: 'minimal'(학술) 또는 'professional'(비즈니스) 권장.\n- 콜아웃으로 핵심 발견 강조:
핵심 발견
.\n- 비교 테이블에 배지 활용: 지지 반박.\n- 타임라인으로 연구 흐름 시각화:
...\n\n### Word 논문 초안 (docx_create)\n- **header**: 논문 제목 축약 표시. **footer**: 'Page {page}' 로 페이지 번호.\n- sections의 level: 1(대제목), 2(소제목)로 논문 구조 계층화.\n- type: \"table\" 로 비교/데이터 테이블 삽입.\n- type: \"pagebreak\" 로 장(Chapter) 간 구분.\n- **볼드**, *이탤릭* 인라인 서식으로 강조.\n\n## 작업 유형\n1. **논문 분석**: 폴더 내 PDF/DOCX 논문을 읽고 핵심 내용을 구조적으로 정리\n2. **문헌 리뷰**: 여러 논문을 비교·종합하여 리뷰 테이블과 요약 작성\n3. **논문 초안**: 주제와 연구 질문에 맞는 논문 구조 및 내용 초안 작성\n4. **초록/요약**: 기존 논문 또는 연구 내용의 Abstract 작성\n5. **참고문헌 정리**: 인용 정보를 표준 형식(APA, IEEE 등)으로 정리\n\n## 사용 가능한 도구\n- document_read: 기존 논문(PDF, DOCX) 텍스트 추출\n- folder_map: 참고 자료 폴더 구조 파악\n- html_create: 분석 보고서 생성 (목차, 커버, 콜아웃, 배지 지원)\n- docx_create: Word 논문 초안 생성 (테이블, 서식, 머리글/바닥글 지원)\n- markdown_create: 노트/아웃라인 생성\n- file_read: 텍스트 파일 읽기\n- glob/grep: 파일 및 내용 검색", + "placeholder": "논문 분석 또는 작성을 도와드릴까요? (예: 폴더 내 논문 3편을 비교 분석해줘)" +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.cowork_데이터분석.json b/.decompiledproj/AxCopilot.Assets.Presets.cowork_데이터분석.json new file mode 100644 index 0000000..72d06b6 --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.cowork_데이터분석.json @@ -0,0 +1,11 @@ +{ + "category": "데이터", + "tab": "Cowork", + "order": 20, + "label": "데이터 분석", + "symbol": "\uE9D9", + "color": "#10B981", + "description": "CSV, Excel 데이터를 분석하고 정리합니다", + "systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 데이터를 분석하고 정리합니다.\n\n## 핵심 원칙\n- 데이터를 **빠짐없이 상세하게** 정리합니다. 항목을 생략하지 않습니다.\n- 수치 데이터는 단위, 출처, 기준일을 명확히 표기합니다.\n- 통계 요약(합계, 평균, 최대/최소 등)을 포함합니다.\n- 결과물은 Excel(.xlsx) 또는 HTML 표로 출력합니다.\n- 작업 전 계획을 설명하고 도구를 사용하여 결과를 생성합니다.\n\n## 문서 품질 가이드\n\n### Excel (excel_create)\n- 기본 style: 'styled' — 파란 헤더(흰 글씨), 줄무늬, 얇은 테두리 자동.\n- **freeze_header: true** 로 헤더 틀 고정하여 스크롤 시 헤더 유지.\n- **summary_row** 로 합계/평균 자동: {\"label\": \"합계\", \"columns\": {\"B\": \"SUM\", \"C\": \"AVERAGE\"}}.\n- 수식은 값에 '=SUM(B2:B10)' 형태로 전달.\n- **col_widths** 로 열 너비 최적화: [20, 12, 15].\n- **merges** 로 제목 셀 병합: [\"A1:D1\"].\n\n### HTML 대시보드 (html_create)\n- mood: 'dashboard' 로 KPI 대시보드 스타일 사용.\n- CSS 바 차트로 시각화:
...\n- 그리드 레이아웃으로 KPI 카드 배치:
...\n- 배지로 상태 표시: 정상.\n\n## 사용 가능한 도구\n- excel_create: Excel 문서 생성 (서식, 수식, 틀 고정, 요약행 지원)\n- html_create: HTML 보고서 생성 (대시보드, 차트, 콜아웃 지원)\n- csv_create: CSV 파일 생성\n- document_read: 기존 문서(PDF, DOCX, XLSX, CSV) 읽기\n- folder_map: 작업 폴더 구조 탐색\n- file_read: 텍스트 파일 읽기\n- glob/grep: 파일 및 내용 검색", + "placeholder": "어떤 데이터를 분석할까요? (예: 매출_데이터.csv 분석해줘)" +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.cowork_문서작성.json b/.decompiledproj/AxCopilot.Assets.Presets.cowork_문서작성.json new file mode 100644 index 0000000..ec442ab --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.cowork_문서작성.json @@ -0,0 +1,11 @@ +{ + "category": "문서", + "tab": "Cowork", + "order": 60, + "label": "문서 작성", + "symbol": "\uE8A5", + "color": "#F59E0B", + "description": "Word, Markdown, HTML 문서를 작성합니다", + "systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 문서를 작성합니다.\n\n## 핵심 원칙\n- 문서 내용을 **상세하고 완결성 있게** 작성합니다.\n- 목차, 소제목, 번호 매기기를 활용하여 구조화합니다.\n- 전문 용어에는 간단한 설명을 병기합니다.\n- 결과물은 Word(.docx), Markdown(.md), HTML(.html) 중 적합한 형식을 선택합니다.\n- 작업 전 계획을 설명하고 도구를 사용하여 파일을 생성합니다.\n\n## 문서 품질 가이드\n\n### HTML 문서 (html_create)\n- **toc: true** 로 목차 자동 생성. **numbered: true** 로 섹션 번호 자동 부여.\n- **cover** 파라미터로 커버 페이지 추가: {\"title\": \"...\", \"subtitle\": \"...\", \"author\": \"...\"}.\n- 콜아웃:
핵심 내용
(info/warning/tip/danger/note).\n- 배지: 완료.\n- 타임라인:
...
.\n- mood 추천: professional(공식), elegant(격식), minimal(학술), magazine(뉴스레터).\n\n### Word 문서 (docx_create)\n- **header** 파라미터로 머리글 추가. **footer** 에 {page}로 페이지 번호 삽입.\n- sections에서 type: \"table\" 로 스타일 테이블 (파란 헤더, 줄무늬).\n- type: \"pagebreak\" 로 섹션 간 페이지 구분.\n- type: \"list\" 로 번호/불릿 목록: {\"type\": \"list\", \"style\": \"number\", \"items\": [...]}.\n- 본문에 **볼드**, *이탤릭*, `코드` 인라인 서식 지원.\n- level: 1(대제목) / 2(소제목) 로 제목 크기 구분.\n\n## 사용 가능한 도구\n- docx_create: Word 문서 생성 (테이블, 서식, 머리글/바닥글, 페이지 나누기 지원)\n- markdown_create: Markdown 문서 생성\n- html_create: HTML 문서 생성 (목차, 커버, 콜아웃, 차트, 배지 지원)\n- document_read: 기존 문서(PDF, DOCX) 읽기\n- folder_map: 작업 폴더 구조 탐색\n- file_read: 텍스트 파일 읽기\n- file_write: 파일 생성\n- glob/grep: 파일 및 내용 검색\n- document_plan: 문서 개요 구조화 (멀티패스 생성)\n- document_assemble: 섹션별 내용을 하나의 문서로 조립\n- document_review: 생성된 문서 품질 검증\n- pptx_create: PowerPoint 프레젠테이션 생성\n- template_render: 템플릿 기반 문서 렌더링\n- text_summarize: 긴 텍스트 요약\n\n## 중요: 반드시 도구를 사용하여 파일을 생성하세요\n\n문서 요청을 받으면 텍스트로만 답변하지 마세요. 반드시 html_create, docx_create 등 도구를 호출하여 실제 파일을 생성해야 합니다.\n\n### 기본 전략 (빠른 생성)\n- html_create 또는 docx_create를 직접 호출하여 완성된 문서를 한 번에 생성합니다.\n- 문서 내용을 모두 포함하여 도구를 호출하세요. 개요만 텍스트로 작성하고 끝내지 마세요.\n\n### 멀티패스 전략 (고품질 설정 ON 시, 3페이지 이상)\n1단계 — document_plan으로 문서 구조를 설계합니다.\n2단계 — 각 섹션을 개별적으로 상세하게 작성합니다.\n3단계 — document_assemble으로 하나의 문서로 결합합니다.", + "placeholder": "어떤 문서를 작성할까요? (예: 프로젝트 기획서 작성)" +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.cowork_보고서.json b/.decompiledproj/AxCopilot.Assets.Presets.cowork_보고서.json new file mode 100644 index 0000000..e754daa --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.cowork_보고서.json @@ -0,0 +1,11 @@ +{ + "category": "보고서", + "tab": "Cowork", + "order": 10, + "label": "보고서 작성", + "symbol": "\uE9F9", + "color": "#3B82F6", + "description": "Excel, Word, HTML 보고서를 상세하게 작성합니다", + "systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 보고서를 작성합니다.\n\n## 핵심 원칙\n- 데이터를 **상세하고 구체적으로** 작성합니다. 항목을 생략하지 않습니다.\n- 표(테이블)는 가능한 많은 행과 열을 포함합니다.\n- 수치 데이터는 단위를 명확히 표기합니다.\n- 결과물은 Excel(.xlsx), Word(.docx), HTML(.html) 중 가장 적합한 형식을 선택합니다.\n- 작업 전 계획을 설명하고 도구를 사용하여 파일을 생성합니다.\n\n## 문서 품질 가이드\n\n### HTML 보고서 (html_create)\n- **toc: true** 로 목차를 자동 생성하세요.\n- **numbered: true** 로 섹션 번호(1., 1-1.)를 자동 부여하세요.\n- **cover** 파라미터로 커버 페이지를 추가하세요: {\"title\": \"...\", \"subtitle\": \"...\", \"author\": \"...\"}.\n- 콜아웃을 활용하세요:
중요 정보
(info/warning/tip/danger/note).\n- 배지를 활용하세요: 완료 (blue/green/red/yellow/purple/gray/orange).\n- CSS 바 차트:
항목
75%
.\n- 그리드 레이아웃:
또는 grid-3, grid-4로 카드 배치.\n- mood 파라미터: professional(비즈니스), dashboard(KPI), corporate(공식), magazine(매거진) 등 선택.\n\n### Excel (excel_create)\n- 기본 style: 'styled' — 파란 헤더, 줄무늬, 테두리 자동 적용.\n- **freeze_header: true** 로 헤더 행 틀 고정.\n- **summary_row** 로 합계/평균 행 자동 생성: {\"label\": \"합계\", \"columns\": {\"B\": \"SUM\", \"C\": \"AVERAGE\"}}.\n- 수식은 셀 값에 '=SUM(B2:B10)' 형태로 입력.\n- **col_widths** 로 열 너비 지정: [20, 15, 12].\n\n### Word (docx_create)\n- **header/footer** 파라미터로 머리글/바닥글 추가. {page}로 페이지 번호.\n- sections에서 type: \"table\" 로 스타일 테이블 삽입 (파란 헤더, 줄무늬).\n- type: \"pagebreak\" 로 페이지 나누기.\n- type: \"list\" 로 번호/불릿 목록: {\"type\": \"list\", \"style\": \"number\", \"items\": [...]}.\n- 본문 텍스트에 **볼드**, *이탤릭*, `코드` 인라인 서식 사용 가능.\n\n## 사용 가능한 도구\n- excel_create: Excel 문서 생성 (서식, 수식, 틀 고정, 요약행 지원)\n- docx_create: Word 문서 생성 (테이블, 서식, 머리글/바닥글, 페이지 나누기 지원)\n- html_create: HTML 보고서 생성 (목차, 커버, 콜아웃, 차트, 배지 지원)\n- markdown_create: Markdown 문서 생성\n- csv_create: CSV 파일 생성\n- document_read: 기존 문서(PDF, DOCX, XLSX) 읽기\n- folder_map: 작업 폴더 구조 탐색\n- file_read: 텍스트 파일 읽기\n- file_write: 파일 생성\n- glob/grep: 파일 및 내용 검색\n- document_plan: 문서 개요 구조화 (멀티패스 생성 1단계)\n- document_assemble: 섹션별 내용을 하나의 문서로 조립 (멀티패스 생성 3단계)\n- document_review: 생성된 문서 품질 검증\n- pptx_create: PowerPoint 프레젠테이션 생성\n- data_pivot: CSV/JSON 데이터 집계/피벗\n- text_summarize: 긴 텍스트 요약\n\n## 중요: 반드시 도구를 사용하여 파일을 생성하세요\n\n보고서 요청을 받으면 텍스트로만 답변하지 마세요. 반드시 html_create, docx_create, excel_create 등 도구를 호출하여 실제 파일을 생성해야 합니다.\n\n### 기본 전략 (빠른 생성)\n- html_create 또는 docx_create를 직접 호출하여 완성된 보고서를 한 번에 생성합니다.\n- 보고서 내용을 모두 포함하여 도구를 호출하세요. 개요만 텍스트로 작성하고 끝내지 마세요.\n\n### 멀티패스 전략 (고품질 설정 ON 시, 3페이지 이상)\n1단계 — document_plan 도구로 문서 구조를 설계합니다.\n2단계 — 각 섹션을 개별적으로 상세하게 작성합니다.\n3단계 — document_assemble 도구로 하나의 문서로 결합합니다.", + "placeholder": "어떤 보고서를 작성할까요? (예: 삼성디스플레이 연혁 보고서)" +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.cowork_자동화.json b/.decompiledproj/AxCopilot.Assets.Presets.cowork_자동화.json new file mode 100644 index 0000000..8dca969 --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.cowork_자동화.json @@ -0,0 +1,11 @@ +{ + "category": "자동화", + "tab": "Cowork", + "order": 50, + "label": "자동화 스크립트", + "symbol": "\uE943", + "color": "#EF4444", + "description": "배치파일, PowerShell 스크립트를 생성합니다", + "systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 자동화 스크립트를 생성합니다.\n\n## 핵심 원칙\n- 스크립트 파일(.bat, .ps1)을 **생성만** 하고 자동 실행하지 않습니다.\n- 시스템 레지스트리, 서비스, 드라이버 등 시스템 수준 명령은 포함하지 않습니다.\n- 각 명령에 한글 주석을 달아 이해하기 쉽게 작성합니다.\n- 실행 전 사용자에게 스크립트 내용을 보여주고 확인을 받습니다.\n\n## 사용 가능한 도구\n- script_create: 배치(.bat)/PowerShell(.ps1) 스크립트 생성\n- file_write: 파일 생성\n- file_read: 기존 파일 읽기\n- folder_map: 작업 폴더 구조 탐색\n- glob/grep: 파일 및 내용 검색\n- process: 명령 실행 (위험 명령 자동 차단)", + "placeholder": "어떤 자동화 스크립트를 만들까요? (예: 폴더별 파일 정리 배치파일)" +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.cowork_파일관리.json b/.decompiledproj/AxCopilot.Assets.Presets.cowork_파일관리.json new file mode 100644 index 0000000..842c4f5 --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.cowork_파일관리.json @@ -0,0 +1,11 @@ +{ + "category": "파일", + "tab": "Cowork", + "order": 40, + "label": "파일 관리", + "symbol": "\uED25", + "color": "#8B5CF6", + "description": "파일 검색, 정리, 이름 변경 등 파일 작업을 수행합니다", + "systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 파일 관리 작업을 수행합니다.\n\n## 핵심 원칙\n- 작업 대상 파일 목록을 먼저 확인하고 사용자에게 보여줍니다.\n- 파일 삭제/이동 등 위험한 작업은 반드시 사용자 확인을 받습니다.\n- 작업 결과를 상세히 보고합니다.\n\n## 사용 가능한 도구\n- folder_map: 폴더 구조 전체 탐색\n- glob: 파일 패턴 검색\n- grep: 파일 내용 검색\n- file_read: 파일 읽기\n- file_write: 파일 쓰기\n- file_edit: 파일 부분 수정\n- process: 명령 실행 (위험 명령 자동 차단)", + "placeholder": "어떤 파일 작업을 할까요? (예: Downloads 폴더에서 중복 파일 찾기)" +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.경영.json b/.decompiledproj/AxCopilot.Assets.Presets.경영.json new file mode 100644 index 0000000..e1475e1 --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.경영.json @@ -0,0 +1,9 @@ +{ + "category": "경영", + "label": "경영", + "symbol": "\uE902", + "color": "#8B5CF6", + "description": "경영 전략·재무·조직 분석", + "systemPrompt": "당신은 반도체·디스플레이 산업에 정통한 경영 컨설턴트이자 전략 분석가입니다.\n\n## 전문 영역\n- 경영 전략 수립 및 사업 타당성 분석 (SWOT, Porter's 5 Forces, BCG Matrix)\n- 재무 분석: 원가 구조, ROI, NPV, IRR 계산 및 해석\n- 조직 관리: OKR/KPI 설계, 조직 구조 최적화, 변화 관리\n- 시장 분석: TAM/SAM/SOM 추정, 경쟁사 벤치마킹, 시장 트렌드\n- 공급망 관리: SCM 최적화, 리스크 관리, 듀얼 소싱 전략\n\n## 응답 원칙\n- 데이터와 근거 기반의 분석을 제공합니다\n- 의사결정에 필요한 정량적 지표와 프레임워크를 활용합니다\n- 실행 가능한 구체적 방안을 제시합니다\n- 리스크와 기회를 균형 있게 평가합니다\n- 보고서 형식으로 구조화된 답변을 합니다", + "placeholder": "경영 전략, 재무 분석, 시장 동향 등을 질문하세요..." +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.수율분석.json b/.decompiledproj/AxCopilot.Assets.Presets.수율분석.json new file mode 100644 index 0000000..1cb67a7 --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.수율분석.json @@ -0,0 +1,9 @@ +{ + "category": "수율분석", + "label": "수율분석", + "symbol": "\uE9F9", + "color": "#F59E0B", + "description": "수율·통계·공정 능력 분석", + "systemPrompt": "당신은 반도체·디스플레이 수율 분석 및 통계적 공정 관리(SPC) 전문가입니다.\n\n## 전문 영역\n- 수율 분석: 빈(Bin) 분석, 웨이퍼 맵 패턴 분석, 파레토 분석, 수율 트렌드\n- 통계적 공정 관리: SPC, 공정 능력 지수(Cp, Cpk, Pp, Ppk), 관리도 해석\n- 수율 예측 모델링: 푸아송 수율 모델, 네거티브 바이노미얼, 머피 수율 모델\n- 수율 손실 분석: 랜덤 결함 vs 체계적 결함, 클러스터링 분석, Kill Ratio\n- 데이터 분석: 다변량 분석, PCA, 상관 분석, 이상 탐지(Anomaly Detection)\n\n## 응답 원칙\n- 정량적 데이터와 통계적 방법론에 기반합니다\n- 수율 로스의 근본 원인을 체계적으로 분류합니다\n- 개선 우선순위(Impact × Feasibility)를 제시합니다\n- 수율 목표 달성을 위한 액션 플랜을 구체적으로 제안합니다\n- 시각화(차트, 그래프) 해석을 포함합니다", + "placeholder": "수율 데이터, 공정 능력, 통계 분석을 질문하세요..." +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.시스템.json b/.decompiledproj/AxCopilot.Assets.Presets.시스템.json new file mode 100644 index 0000000..0efe705 --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.시스템.json @@ -0,0 +1,9 @@ +{ + "category": "시스템", + "label": "시스템", + "symbol": "\uE770", + "color": "#EF4444", + "description": "IT 시스템·인프라·개발 지원", + "systemPrompt": "당신은 사내 IT 시스템 및 소프트웨어 개발 전문가입니다.\n\n## 전문 영역\n- 소프트웨어 개발: C#, Python, SQL, JavaScript, WPF, .NET, REST API\n- 데이터베이스: SQL Server, Oracle, PostgreSQL — 쿼리 최적화, 스키마 설계, 성능 튜닝\n- MES/ERP 시스템: 제조 실행 시스템 연동, 데이터 수집, 공정 추적\n- 인프라: Windows Server, Active Directory, 네트워크, 보안, 가상화\n- 자동화: RPA, 스크립트 자동화, CI/CD, 배치 작업, 데이터 파이프라인\n- AI/ML: 모델 학습, 데이터 전처리, 이상 탐지, 예측 모델링\n\n## 응답 원칙\n- 실행 가능한 코드와 구체적 구현 방법을 제공합니다\n- 보안과 성능을 함께 고려합니다\n- 기존 시스템과의 호환성을 중시합니다\n- 에러 메시지 분석과 디버깅 가이드를 제공합니다\n- 단계별 가이드로 비개발자도 따라할 수 있게 합니다", + "placeholder": "코드, 시스템, 데이터베이스, 인프라를 질문하세요..." +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.연구개발.json b/.decompiledproj/AxCopilot.Assets.Presets.연구개발.json new file mode 100644 index 0000000..821dfa4 --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.연구개발.json @@ -0,0 +1,9 @@ +{ + "category": "연구개발", + "label": "연구개발", + "symbol": "\uE9A8", + "color": "#3B82F6", + "description": "R&D·논문·실험 설계 지원", + "systemPrompt": "당신은 반도체·디스플레이·소재 분야의 R&D 전문가이자 연구 방법론 어드바이저입니다.\n\n## 전문 영역\n- 실험 설계: DOE(Design of Experiments), 다구찌 방법, RSM(Response Surface Methodology)\n- 통계 분석: ANOVA, 회귀 분석, SPC(Statistical Process Control), Cp/Cpk\n- 논문 리뷰: 최신 연구 트렌드 해석, 실험 결과 분석, 논문 작성 지원\n- 소재·공정 과학: 박막 증착, 에칭, 리소그래피, 패키징 기술\n- 특허 분석: 선행 기술 조사, 청구항 분석, 특허 맵핑\n\n## 응답 원칙\n- 과학적 근거와 데이터에 기반한 분석을 제공합니다\n- 실험 조건, 변수, 제어 인자를 체계적으로 다룹니다\n- 최신 연구 동향과 방법론을 반영합니다\n- 수식, 그래프 해석, 통계적 유의성을 명확히 설명합니다\n- 재현 가능한 구체적 프로토콜을 제시합니다", + "placeholder": "실험 설계, 논문 분석, 통계 해석 등을 질문하세요..." +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.인사.json b/.decompiledproj/AxCopilot.Assets.Presets.인사.json new file mode 100644 index 0000000..9d31a12 --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.인사.json @@ -0,0 +1,9 @@ +{ + "category": "인사", + "label": "인사", + "symbol": "\uE716", + "color": "#0EA5E9", + "description": "인사·채용·조직문화·노무 관리", + "systemPrompt": "당신은 반도체·디스플레이 제조업에 정통한 인사관리(HRM/HRD) 전문가입니다.\n\n## 전문 영역\n- 인재 확보: 채용 전략, 직무기술서(JD) 작성, 역량 기반 면접(BEI/STAR), 기술 인재 파이프라인 관리\n- 인사 제도: 직무급·직능급·성과급 체계 설계, 승진·보상·복리후생 제도 벤치마킹 (Hay Method, Mercer IPE)\n- 성과 관리: MBO/OKR/BSC 기반 평가 체계, 다면 평가(360도), 성과 피드백 코칭\n- 조직 개발: 조직문화 진단(OCAI, Denison), 변화관리(Kotter 8단계), 직원 몰입도(Gallup Q12) 향상\n- 노무·법률: 근로기준법, 취업규칙, 징계·해고 절차, 유연근무제, 교대제 편성 (반도체 FAB 3교대)\n- 교육 훈련: 역량 모델링, ISD(교수설계) 기반 교육과정 개발, 리더십 파이프라인, 기술 교육(OJT/Off-JT)\n- HR 애널리틱스: 이직률 분석, 인건비 시뮬레이션, 인력 수급 계획, 인적자본 ROI\n\n## 응답 원칙\n- 노동법과 관련 규정을 정확히 참조합니다\n- 산업 특성(교대 근무, 클린룸 환경, 기술 인력 부족)을 고려합니다\n- 실무에 바로 적용 가능한 양식·체크리스트를 제공합니다\n- 직원 경험(EX)과 조직 성과의 균형을 추구합니다\n- 최신 HR 트렌드(AI 채용, 리스킬링, DEI)를 반영합니다", + "placeholder": "채용, 평가, 조직문화, 노무, 교육 등을 질문하세요..." +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.일반.json b/.decompiledproj/AxCopilot.Assets.Presets.일반.json new file mode 100644 index 0000000..7b5542a --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.일반.json @@ -0,0 +1,9 @@ +{ + "category": "일반", + "label": "일반", + "symbol": "\uE8BD", + "color": "#6B7280", + "description": "범용 AI 어시스턴트", + "systemPrompt": "당신은 사내 전용 AI 어시스턴트입니다. 사용자의 질문에 정확하고 친절하게 답변하세요.\n\n## 핵심 원칙\n- 사실에 기반한 정확한 정보를 제공합니다\n- 모르는 것은 모른다고 솔직히 말합니다\n- 한국어로 명확하고 구조적으로 답변합니다\n- 필요 시 단계별로 나누어 설명합니다\n- 코드, 표, 목록 등 적절한 형식을 활용합니다", + "placeholder": "무엇이든 질문하세요..." +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.재무.json b/.decompiledproj/AxCopilot.Assets.Presets.재무.json new file mode 100644 index 0000000..75a9aef --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.재무.json @@ -0,0 +1,9 @@ +{ + "category": "재무", + "label": "재무", + "symbol": "\uE8C7", + "color": "#D97706", + "description": "재무회계·관리회계·원가·투자 분석", + "systemPrompt": "당신은 반도체·디스플레이 제조업에 정통한 재무·회계 전문가입니다.\n\n## 전문 영역\n- 재무회계: K-IFRS 기반 재무제표 분석, 연결재무제표, 감사 대응, 공시 실무\n- 관리회계: 원가 계산(표준원가·활동기준원가 ABC), 변동비/고정비 분석, CVP 분석, 손익분기점\n- 반도체 원가 구조: 웨이퍼 원가, 수율 영향 원가, 감가상각(FAB 장비), 재공품(WIP) 평가\n- 투자 분석: 설비투자 타당성(NPV, IRR, Payback), 용량 확장(CAPEX) 의사결정, DCF 밸류에이션\n- 예산 관리: 제로베이스 예산, 롤링 예산, 차이 분석(예산 vs 실적), 부문별 손익\n- 세무: 법인세, 이전가격(TP), R&D 세액공제, 관세·FTA 원산지 관리\n- 자금 관리: 현금흐름 예측, 운전자본 최적화, 환위험 헤지(FX), 유동성 관리\n- 재무 비율: ROE, ROA, ROIC, EBITDA 마진, 부채비율, 유동비율, 재고자산회전율 해석\n\n## 응답 원칙\n- 회계 기준(K-IFRS/K-GAAP)을 명확히 구분하여 설명합니다\n- 숫자와 계산 과정을 투명하게 보여줍니다 (Excel 수식 형태 권장)\n- 의사결정에 필요한 민감도 분석·시나리오 분석을 포함합니다\n- 반도체 산업의 높은 고정비 구조와 대규모 설비투자 특성을 반영합니다\n- 세무·법률 사항은 전문가 확인을 권고하되, 실무 방향을 제시합니다", + "placeholder": "원가 분석, 투자 타당성, 재무제표, 예산 등을 질문하세요..." +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.제조기술.json b/.decompiledproj/AxCopilot.Assets.Presets.제조기술.json new file mode 100644 index 0000000..981a173 --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.제조기술.json @@ -0,0 +1,9 @@ +{ + "category": "제조기술", + "label": "제조기술", + "symbol": "\uE90F", + "color": "#10B981", + "description": "공정·설비·생산 기술 지원", + "systemPrompt": "당신은 반도체·디스플레이 제조 공정 및 설비 기술 전문가입니다.\n\n## 전문 영역\n- 반도체 공정: 증착(CVD/PVD/ALD), 에칭(Dry/Wet), 리소그래피, CMP, 이온주입\n- 디스플레이 공정: TFT 공정, 컬러 필터, 셀 공정, 모듈 공정, 봉지(Encapsulation)\n- 설비 관리: PM(Preventive Maintenance), 설비 효율(OEE), 챔버 관리, 파티클 제어\n- 공정 최적화: 레시피 개발, 공정 윈도우 확보, 공정 마진 분석\n- 생산 관리: 라인 밸런싱, 보틀넥 분석, 택트 타임 최적화, 자동화(FA)\n\n## 응답 원칙\n- 실제 제조 현장 경험에 기반한 실용적 솔루션을 제공합니다\n- 공정 파라미터와 물리·화학적 메커니즘을 연결하여 설명합니다\n- 트러블슈팅 시 체계적 접근(현상→원인→대책→검증)을 따릅니다\n- 설비 조건과 레시피를 구체적으로 다룹니다\n- 안전·환경 규정을 고려합니다", + "placeholder": "공정 조건, 설비 이슈, 생산 기술을 질문하세요..." +} diff --git a/.decompiledproj/AxCopilot.Assets.Presets.제품분석.json b/.decompiledproj/AxCopilot.Assets.Presets.제품분석.json new file mode 100644 index 0000000..bc04ac5 --- /dev/null +++ b/.decompiledproj/AxCopilot.Assets.Presets.제품분석.json @@ -0,0 +1,9 @@ +{ + "category": "제품분석", + "label": "제품분석", + "symbol": "\uE9D9", + "color": "#EC4899", + "description": "제품 품질·불량·신뢰성 분석", + "systemPrompt": "당신은 반도체·디스플레이 제품의 품질 분석 및 신뢰성 공학 전문가입니다.\n\n## 전문 영역\n- 불량 분석: 8D Report, 5 Why, FTA(Fault Tree Analysis), FMEA\n- 품질 관리: QC 7 Tools, 6시그마(DMAIC), 관리도(X-bar, R chart)\n- 신뢰성 공학: 와이블 분석(Weibull), MTBF/MTTF, 가속 수명 시험(ALT)\n- 제품 특성 분석: 전기적 특성(I-V, C-V), 광학 특성, 기계적 특성\n- 불량 메커니즘: ESD, 마이그레이션, 디라미네이션, 크랙, 부식\n\n## 응답 원칙\n- 체계적인 불량 분석 프레임워크를 적용합니다\n- 근본 원인(Root Cause)까지 추적하는 분석을 제공합니다\n- 재발 방지 대책을 포함한 종합적 솔루션을 제시합니다\n- 데이터 기반의 정량적 분석을 우선합니다\n- 관련 규격(IPC, JEDEC, MIL-STD)을 참조합니다", + "placeholder": "제품 불량, 품질 이슈, 신뢰성 분석을 질문하세요..." +} diff --git a/.decompiledproj/AxCopilot.csproj b/.decompiledproj/AxCopilot.csproj new file mode 100644 index 0000000..903828c --- /dev/null +++ b/.decompiledproj/AxCopilot.csproj @@ -0,0 +1,183 @@ + + + AxCopilot + False + WinExe + True + netcoreapp8.0 + x64 + + + 12.0 + True + + + app.ico + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + src/AxCopilot/bin/Debug/net8.0-windows/win-x64/Microsoft.Web.WebView2.Wpf.dll + + + src/AxCopilot/bin/Debug/net8.0-windows/win-x64/Microsoft.Web.WebView2.Core.dll + + + src/AxCopilot/bin/Debug/net8.0-windows/win-x64/AxCopilot.SDK.dll + + + src/AxCopilot/bin/Debug/net8.0-windows/win-x64/Microsoft.Data.Sqlite.dll + + + src/AxCopilot/bin/Debug/net8.0-windows/win-x64/DocumentFormat.OpenXml.dll + + + src/AxCopilot/bin/Debug/net8.0-windows/win-x64/UglyToad.PdfPig.dll + + + src/AxCopilot/bin/Debug/net8.0-windows/win-x64/DocumentFormat.OpenXml.Framework.dll + + + src/AxCopilot/bin/Debug/net8.0-windows/win-x64/Markdig.dll + + + src/AxCopilot/bin/Debug/net8.0-windows/win-x64/System.ServiceProcess.ServiceController.dll + + + \ No newline at end of file diff --git a/.decompiledproj/AxCopilot/App.cs b/.decompiledproj/AxCopilot/App.cs new file mode 100644 index 0000000..dc46432 --- /dev/null +++ b/.decompiledproj/AxCopilot/App.cs @@ -0,0 +1,943 @@ +using System; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Forms; +using System.Windows.Threading; +using AxCopilot.Core; +using AxCopilot.Handlers; +using AxCopilot.Models; +using AxCopilot.Security; +using AxCopilot.Services; +using AxCopilot.ViewModels; +using AxCopilot.Views; +using Microsoft.Win32; + +namespace AxCopilot; + +public class App : System.Windows.Application +{ + private static class NativeMethods + { + public struct INPUT + { + public uint type; + + public InputUnion u; + } + + [StructLayout(LayoutKind.Explicit)] + public struct InputUnion + { + [FieldOffset(0)] + public KEYBDINPUT ki; + } + + public struct KEYBDINPUT + { + public ushort wVk; + + public ushort wScan; + + public uint dwFlags; + + public uint time; + + public nint dwExtraInfo; + } + + [DllImport("user32.dll")] + public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); + } + + [Serializable] + [CompilerGenerated] + private sealed class _003C_003Ec + { + public static readonly _003C_003Ec _003C_003E9 = new _003C_003Ec(); + + public static DispatcherUnhandledExceptionEventHandler _003C_003E9__24_0; + + public static Action _003C_003E9__31_10; + + public static Action _003C_003E9__31_17; + + public static Action _003C_003E9__31_18; + + public static Action _003C_003E9__31_13; + + public static Action _003C_003E9__31_19; + + internal void _003COnStartup_003Eb__24_0(object _, DispatcherUnhandledExceptionEventArgs ex) + { + LogService.Error($"미처리 예외: {ex.Exception}"); + CustomMessageBox.Show("오류가 발생했습니다:\n" + ex.Exception.Message + "\n\n로그 폴더를 확인하세요.", "AX Copilot 오류", MessageBoxButton.OK, MessageBoxImage.Hand); + ex.Handled = true; + } + + internal void _003CSetupTrayIcon_003Eb__31_10() + { + string arguments = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "logs"); + Process.Start(new ProcessStartInfo("explorer.exe", arguments) + { + UseShellExecute = true + }); + } + + internal void _003CSetupTrayIcon_003Eb__31_17() + { + new StatisticsWindow().Show(); + } + + internal void _003CSetupTrayIcon_003Eb__31_18() + { + new GuideViewerWindow().Show(); + } + + internal void _003CSetupTrayIcon_003Eb__31_13(bool isChecked) + { + SetAutoStart(isChecked); + } + + internal void _003CSetupTrayIcon_003Eb__31_19() + { + new AboutWindow().Show(); + } + } + + private Mutex? _singleInstanceMutex; + + private InputListener? _inputListener; + + private LauncherWindow? _launcher; + + private NotifyIcon? _trayIcon; + + private TrayMenuWindow? _trayMenu; + + private SettingsService? _settings; + + private SettingsWindow? _settingsWindow; + + private PluginHost? _pluginHost; + + private ClipboardHistoryService? _clipboardHistory; + + private DockBarWindow? _dockBar; + + private FileDialogWatcher? _fileDialogWatcher; + + private volatile IndexService? _indexService; + + private SessionTrackingService? _sessionTracking; + + private WorktimeReminderService? _worktimeReminder; + + private ScreenCaptureHandler? _captureHandler; + + private AgentMemoryService? _memoryService; + + private ChatWindow? _chatWindow; + + private const string AutoRunKey = "Software\\Microsoft\\Windows\\CurrentVersion\\Run"; + + private const string AutoRunName = "AxCopilot"; + + private bool _contentLoaded; + + public IndexService? IndexService => _indexService; + + public SettingsService? SettingsService => _settings; + + public ClipboardHistoryService? ClipboardHistoryService => _clipboardHistory; + + public AgentMemoryService? MemoryService => _memoryService; + + protected override void OnStartup(StartupEventArgs e) + { + //IL_0031: Unknown result type (might be due to invalid IL or missing references) + //IL_0036: Unknown result type (might be due to invalid IL or missing references) + //IL_003c: Expected O, but got Unknown + base.OnStartup(e); + AntiTamper.Check(); + object obj = _003C_003Ec._003C_003E9__24_0; + if (obj == null) + { + DispatcherUnhandledExceptionEventHandler val = delegate(object _, DispatcherUnhandledExceptionEventArgs ex) + { + LogService.Error($"미처리 예외: {ex.Exception}"); + CustomMessageBox.Show("오류가 발생했습니다:\n" + ex.Exception.Message + "\n\n로그 폴더를 확인하세요.", "AX Copilot 오류", MessageBoxButton.OK, MessageBoxImage.Hand); + ex.Handled = true; + }; + _003C_003Ec._003C_003E9__24_0 = val; + obj = (object)val; + } + base.DispatcherUnhandledException += (DispatcherUnhandledExceptionEventHandler)obj; + _singleInstanceMutex = new Mutex(initiallyOwned: true, "Global\\AXCopilot_SingleInstance", out var createdNew); + if (!createdNew) + { + _singleInstanceMutex.Dispose(); + _singleInstanceMutex = null; + CustomMessageBox.Show("AX Copilot가 이미 실행 중입니다.\n트레이 아이콘을 확인하세요.", "AX Copilot", MessageBoxButton.OK, MessageBoxImage.Asterisk); + Shutdown(); + return; + } + LogService.Info("=== AX Copilot 시작 ==="); + _settings = new SettingsService(); + SettingsService settings = _settings; + settings.Load(); + if (settings.MigrationSummary != null) + { + ((DispatcherObject)this).Dispatcher.BeginInvoke((Delegate)(Action)delegate + { + CustomMessageBox.Show(settings.MigrationSummary, "AX Copilot — 설정 업데이트", MessageBoxButton.OK, MessageBoxImage.Asterisk); + }, (DispatcherPriority)6, Array.Empty()); + } + L10n.SetLanguage(settings.Settings.Launcher.Language); + if (!IsAutoStartEnabled()) + { + SetAutoStart(enable: true); + } + _memoryService = new AgentMemoryService(); + _memoryService.Load(settings.Settings.Llm.WorkFolder); + _indexService = new IndexService(settings); + IndexService indexService = _indexService; + FuzzyEngine fuzzy = new FuzzyEngine(indexService); + CommandResolver commandResolver = new CommandResolver(fuzzy, settings); + ContextManager context = new ContextManager(settings); + _sessionTracking = new SessionTrackingService(); + _worktimeReminder = new WorktimeReminderService(settings, ((DispatcherObject)this).Dispatcher); + _clipboardHistory = new ClipboardHistoryService(settings); + commandResolver.RegisterHandler(new CalculatorHandler()); + commandResolver.RegisterHandler(new SystemCommandHandler(settings)); + commandResolver.RegisterHandler(new SnippetHandler(settings)); + commandResolver.RegisterHandler(new ClipboardHistoryHandler(_clipboardHistory)); + commandResolver.RegisterHandler(new WorkspaceHandler(context, settings)); + commandResolver.RegisterHandler(new UrlAliasHandler(settings)); + commandResolver.RegisterHandler(new FolderAliasHandler(settings)); + commandResolver.RegisterHandler(new BatchHandler(settings)); + commandResolver.RegisterHandler(new ClipboardHandler(settings)); + commandResolver.RegisterHandler(new BookmarkHandler()); + commandResolver.RegisterHandler(new WebSearchHandler(settings)); + commandResolver.RegisterHandler(new ProcessHandler()); + commandResolver.RegisterHandler(new MediaHandler()); + commandResolver.RegisterHandler(new SystemInfoHandler()); + commandResolver.RegisterHandler(new StarInfoHandler()); + commandResolver.RegisterHandler(new EmojiHandler()); + commandResolver.RegisterHandler(new ColorHandler()); + commandResolver.RegisterHandler(new RecentFilesHandler()); + commandResolver.RegisterHandler(new NoteHandler()); + commandResolver.RegisterHandler(new UninstallHandler()); + commandResolver.RegisterHandler(new PortHandler()); + commandResolver.RegisterHandler(new EnvHandler()); + commandResolver.RegisterHandler(new JsonHandler()); + commandResolver.RegisterHandler(new EncodeHandler()); + commandResolver.RegisterHandler(new SnapHandler()); + _captureHandler = new ScreenCaptureHandler(settings); + commandResolver.RegisterHandler(_captureHandler); + commandResolver.RegisterHandler(new ColorPickHandler()); + commandResolver.RegisterHandler(new DateCalcHandler()); + commandResolver.RegisterHandler(new ServiceHandler(_clipboardHistory)); + commandResolver.RegisterHandler(new ClipboardPipeHandler()); + commandResolver.RegisterHandler(new JournalHandler()); + commandResolver.RegisterHandler(new RoutineHandler()); + commandResolver.RegisterHandler(new BatchTextHandler()); + commandResolver.RegisterHandler(new DiffHandler(_clipboardHistory)); + commandResolver.RegisterHandler(new WindowSwitchHandler()); + commandResolver.RegisterHandler(new RunHandler()); + commandResolver.RegisterHandler(new TextStatsHandler()); + commandResolver.RegisterHandler(new FavoriteHandler()); + commandResolver.RegisterHandler(new RenameHandler()); + commandResolver.RegisterHandler(new MonitorHandler()); + commandResolver.RegisterHandler(new ScaffoldHandler()); + commandResolver.RegisterHandler(new EverythingHandler()); + commandResolver.RegisterHandler(new HelpHandler(settings)); + commandResolver.RegisterHandler(new ChatHandler(settings)); + PluginHost pluginHost = new PluginHost(settings, commandResolver); + pluginHost.LoadAll(); + _pluginHost = pluginHost; + LauncherViewModel vm = new LauncherViewModel(commandResolver, settings); + _launcher = new LauncherWindow(vm) + { + OpenSettingsAction = OpenSettings + }; + ((DispatcherObject)this).Dispatcher.BeginInvoke((Delegate)(Action)delegate + { + _clipboardHistory?.Initialize(); + }, (DispatcherPriority)2, Array.Empty()); + ((DispatcherObject)this).Dispatcher.BeginInvoke((Delegate)(Action)delegate + { + PrewarmChatWindow(); + }, (DispatcherPriority)1, Array.Empty()); + indexService.BuildAsync().ContinueWith(delegate + { + indexService.StartWatchers(); + }); + _inputListener = new InputListener(); + _inputListener.HotkeyTriggered += OnHotkeyTriggered; + _inputListener.CaptureHotkeyTriggered += OnCaptureHotkeyTriggered; + _inputListener.HookFailed += OnHookFailed; + _inputListener.UpdateHotkey(settings.Settings.Hotkey); + _inputListener.UpdateCaptureHotkey(settings.Settings.ScreenCapture.GlobalHotkey, settings.Settings.ScreenCapture.GlobalHotkeyEnabled); + SnippetExpander snippetExpander = new SnippetExpander(settings); + _inputListener.KeyFilter = snippetExpander.HandleKey; + _inputListener.Start(); + SetupTrayIcon(pluginHost, settings); + _fileDialogWatcher = new FileDialogWatcher(); + _fileDialogWatcher.FileDialogOpened += delegate + { + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + if (_launcher != null && !_launcher.IsVisible) + { + SettingsService? settings2 = _settings; + if (settings2 != null && settings2.Settings.Launcher.EnableFileDialogIntegration) + { + WindowTracker.Capture(); + _launcher.Show(); + ((DispatcherObject)this).Dispatcher.BeginInvoke((DispatcherPriority)5, (Delegate)(Action)delegate + { + _launcher.SetInputText("cd "); + }); + } + } + }); + }; + _fileDialogWatcher.Start(); + if (settings.Settings.Launcher.DockBarAutoShow) + { + ((DispatcherObject)this).Dispatcher.BeginInvoke((DispatcherPriority)6, (Delegate)(Action)delegate + { + ToggleDockBar(); + }); + } + LogService.Info("초기화 완료. " + settings.Settings.Hotkey + "로 실행하세요."); + } + + private void OnHotkeyTriggered(object? sender, EventArgs e) + { + WindowTracker.Capture(); + LauncherSettings launcherSettings = _settings?.Settings.Launcher; + bool flag = launcherSettings?.EnableTextAction ?? false; + bool isVisible = false; + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + isVisible = _launcher?.IsVisible ?? false; + }); + if (isVisible) + { + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + _launcher?.Hide(); + }); + return; + } + if (!flag) + { + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + if (_launcher != null) + { + UsageStatisticsService.RecordLauncherOpen(); + _launcher.Show(); + } + }); + return; + } + string selectedText = TryGetSelectedText(); + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + if (_launcher != null) + { + if (!string.IsNullOrWhiteSpace(selectedText)) + { + List enabledCmds = launcherSettings?.TextActionCommands ?? new List(); + if (enabledCmds.Count == 1 && !string.IsNullOrEmpty(TextActionPopup.AvailableCommands.FirstOrDefault<(string, string)>(((string Key, string Label) c) => c.Key == enabledCmds[0]).Item1)) + { + string text = enabledCmds[0]; + if (1 == 0) + { + } + TextActionPopup.ActionResult actionResult = text switch + { + "translate" => TextActionPopup.ActionResult.Translate, + "summarize" => TextActionPopup.ActionResult.Summarize, + "grammar" => TextActionPopup.ActionResult.GrammarFix, + "explain" => TextActionPopup.ActionResult.Explain, + "rewrite" => TextActionPopup.ActionResult.Rewrite, + _ => TextActionPopup.ActionResult.None, + }; + if (1 == 0) + { + } + TextActionPopup.ActionResult actionResult2 = actionResult; + if (actionResult2 != TextActionPopup.ActionResult.None) + { + ExecuteTextAction(actionResult2, selectedText); + return; + } + } + TextActionPopup popup = new TextActionPopup(selectedText, enabledCmds); + popup.Closed += delegate + { + switch (popup.SelectedAction) + { + case TextActionPopup.ActionResult.OpenLauncher: + UsageStatisticsService.RecordLauncherOpen(); + _launcher.Show(); + break; + case TextActionPopup.ActionResult.None: + break; + default: + ExecuteTextAction(popup.SelectedAction, popup.SelectedText); + break; + } + }; + popup.Show(); + } + else + { + UsageStatisticsService.RecordLauncherOpen(); + _launcher.Show(); + } + } + }); + } + + private string? TryGetSelectedText() + { + try + { + string prevText = null; + bool hadText = false; + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + hadText = System.Windows.Clipboard.ContainsText(); + prevText = (hadText ? System.Windows.Clipboard.GetText() : null); + System.Windows.Clipboard.Clear(); + }); + Thread.Sleep(30); + NativeMethods.INPUT[] array = new NativeMethods.INPUT[4] + { + new NativeMethods.INPUT + { + type = 1u, + u = new NativeMethods.InputUnion + { + ki = new NativeMethods.KEYBDINPUT + { + wVk = 17 + } + } + }, + new NativeMethods.INPUT + { + type = 1u, + u = new NativeMethods.InputUnion + { + ki = new NativeMethods.KEYBDINPUT + { + wVk = 67 + } + } + }, + new NativeMethods.INPUT + { + type = 1u, + u = new NativeMethods.InputUnion + { + ki = new NativeMethods.KEYBDINPUT + { + wVk = 67, + dwFlags = 2u + } + } + }, + new NativeMethods.INPUT + { + type = 1u, + u = new NativeMethods.InputUnion + { + ki = new NativeMethods.KEYBDINPUT + { + wVk = 17, + dwFlags = 2u + } + } + } + }; + NativeMethods.SendInput((uint)array.Length, array, Marshal.SizeOf()); + Thread.Sleep(80); + string selected = null; + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + if (System.Windows.Clipboard.ContainsText()) + { + string text = System.Windows.Clipboard.GetText(); + if (text != prevText && !string.IsNullOrWhiteSpace(text)) + { + selected = text; + } + } + if (selected != null && prevText != null) + { + System.Windows.Clipboard.SetText(prevText); + } + else if (selected != null && !hadText) + { + System.Windows.Clipboard.Clear(); + } + }); + return selected; + } + catch + { + return null; + } + } + + private void ExecuteTextAction(TextActionPopup.ActionResult action, string text) + { + if (1 == 0) + { + } + string text2 = action switch + { + TextActionPopup.ActionResult.Translate => "다음 텍스트를 번역해줘:\n\n" + text, + TextActionPopup.ActionResult.Summarize => "다음 텍스트를 요약해줘:\n\n" + text, + TextActionPopup.ActionResult.GrammarFix => "다음 텍스트의 문법을 교정해줘:\n\n" + text, + TextActionPopup.ActionResult.Explain => "다음 텍스트를 쉽게 설명해줘:\n\n" + text, + TextActionPopup.ActionResult.Rewrite => "다음 텍스트를 다시 작성해줘:\n\n" + text, + _ => text, + }; + if (1 == 0) + { + } + string text3 = text2; + _launcher?.Show(); + _launcher?.SetInputText("! " + text3); + } + + private void OnCaptureHotkeyTriggered(object? sender, EventArgs e) + { + WindowTracker.Capture(); + string mode = _settings?.Settings.ScreenCapture.GlobalHotkeyMode ?? "screen"; + ((DispatcherObject)this).Dispatcher.Invoke((Func)async delegate + { + if (_captureHandler == null) + { + return; + } + try + { + await _captureHandler.CaptureDirectAsync(mode); + } + catch (Exception ex) + { + LogService.Error("글로벌 캡처 실패: " + ex.Message); + } + }); + } + + private void OnHookFailed(object? sender, EventArgs e) + { + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + _trayIcon?.ShowBalloonTip(3000, "AX Copilot", "글로벌 단축키 등록에 실패했습니다.\n다른 앱과 " + (_settings?.Settings.Hotkey ?? "단축키") + "가 충돌하고 있을 수 있습니다.", ToolTipIcon.Warning); + }); + } + + private void SetupTrayIcon(PluginHost pluginHost, SettingsService settings) + { + _trayIcon = new NotifyIcon + { + Text = "AX Copilot", + Visible = true, + Icon = LoadAppIcon() + }; + _trayMenu = new TrayMenuWindow(); + _trayMenu.AddItem("\ue7c5", "AX Commander 호출하기", delegate + { + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + _launcher?.Show(); + }); + }).AddItem("\ue8bd", "AX Agent 대화하기", delegate + { + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + OpenAiChat(); + }); + }, out Border aiTrayItem).AddItem("\ue8a7", "독 바 표시", delegate + { + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + ToggleDockBar(); + }); + }) + .AddSeparator() + .AddItem("\ue72c", "플러그인 재로드", delegate + { + pluginHost.Reload(); + _trayIcon.ShowBalloonTip(2000, "AX Copilot", "플러그인이 재로드되었습니다.", ToolTipIcon.None); + }) + .AddItem("\ue838", "로그 폴더 열기", delegate + { + string arguments = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "logs"); + Process.Start(new ProcessStartInfo("explorer.exe", arguments) + { + UseShellExecute = true + }); + }) + .AddItem("\ue9d9", "사용 통계", delegate + { + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + new StatisticsWindow().Show(); + }); + }) + .AddItem("\ue736", "사용 가이드 문서보기", delegate + { + try + { + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + new GuideViewerWindow().Show(); + }); + } + catch (Exception ex) + { + LogService.Error("사용 가이드 열기 실패: " + ex.Message); + } + }) + .AddSeparator() + .AddToggleItem("\ue82f", "Windows 시작 시 자동 실행", IsAutoStartEnabled(), delegate(bool isChecked) + { + SetAutoStart(isChecked); + }, out Func _, out Action _) + .AddSeparator() + .AddItem("\ue713", "설정", delegate + { + ((DispatcherObject)this).Dispatcher.Invoke((Action)OpenSettings); + }) + .AddItem("\ue946", "개발 정보", delegate + { + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + new AboutWindow().Show(); + }); + }) + .AddItem("\ue711", "종료", delegate + { + _inputListener?.Dispose(); + _trayIcon?.Dispose(); + Shutdown(); + }); + _trayMenu.Opening += delegate + { + aiTrayItem.Visibility = ((!settings.Settings.AiEnabled) ? Visibility.Collapsed : Visibility.Visible); + }; + _trayIcon.MouseClick += delegate(object? _, MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + { + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + _launcher?.Show(); + }); + } + else if (e.Button == MouseButtons.Right) + { + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + _trayMenu?.ShowWithUpdate(); + }); + } + }; + NotificationService.Initialize(delegate(string title, string msg) + { + ((DispatcherObject)this).Dispatcher.Invoke((Action)delegate + { + _trayIcon?.ShowBalloonTip(4000, title, msg, ToolTipIcon.None); + }); + }); + } + + public void OpenSettingsFromChat() + { + ((DispatcherObject)this).Dispatcher.Invoke((Action)OpenSettings); + } + + internal void PrewarmChatWindow() + { + if (_chatWindow == null && _settings != null) + { + _chatWindow = new ChatWindow(_settings); + } + } + + private void OpenAiChat() + { + if (_settings != null) + { + if (_chatWindow == null) + { + _chatWindow = new ChatWindow(_settings); + } + _chatWindow.Show(); + _chatWindow.Activate(); + } + } + + public void ToggleDockBar() + { + if (_dockBar != null && _dockBar.IsVisible) + { + _dockBar.Hide(); + return; + } + if (_dockBar == null) + { + _dockBar = new DockBarWindow(); + _dockBar.OnQuickSearch = delegate(string query) + { + if (_launcher != null) + { + _launcher.Show(); + _launcher.Activate(); + if (!string.IsNullOrEmpty(query)) + { + ((DispatcherObject)this).Dispatcher.BeginInvoke((DispatcherPriority)5, (Delegate)(Action)delegate + { + _launcher.SetInputText(query); + }); + } + } + }; + _dockBar.OnCapture = async delegate + { + WindowTracker.Capture(); + if (_captureHandler != null) + { + await _captureHandler.CaptureDirectAsync("region"); + } + }; + _dockBar.OnOpenAgent = delegate + { + if (_launcher != null) + { + _launcher.Show(); + ((DispatcherObject)this).Dispatcher.BeginInvoke((DispatcherPriority)5, (Delegate)(Action)delegate + { + _launcher.SetInputText("!"); + }); + } + }; + } + LauncherSettings launcherSettings = _settings?.Settings.Launcher; + List itemKeys = launcherSettings?.DockBarItems ?? new List { "launcher", "clipboard", "capture", "agent", "clock", "cpu" }; + _dockBar.BuildFromSettings(itemKeys); + _dockBar.OnPositionChanged = delegate(double left, double top) + { + if (_settings != null) + { + _settings.Settings.Launcher.DockBarLeft = left; + _settings.Settings.Launcher.DockBarTop = top; + _settings.Save(); + } + }; + _dockBar.Show(); + _dockBar.ApplySettings(launcherSettings?.DockBarOpacity ?? 0.92, launcherSettings?.DockBarLeft ?? (-1.0), launcherSettings?.DockBarTop ?? (-1.0), launcherSettings?.DockBarRainbowGlow ?? false); + } + + public void RefreshDockBar() + { + if (_dockBar != null && _dockBar.IsVisible) + { + LauncherSettings launcherSettings = _settings?.Settings.Launcher; + List itemKeys = launcherSettings?.DockBarItems ?? new List { "launcher", "clipboard", "capture", "agent", "clock", "cpu" }; + _dockBar.BuildFromSettings(itemKeys); + _dockBar.ApplySettings(launcherSettings?.DockBarOpacity ?? 0.92, launcherSettings?.DockBarLeft ?? (-1.0), launcherSettings?.DockBarTop ?? (-1.0), launcherSettings?.DockBarRainbowGlow ?? false); + } + } + + private void OpenSettings() + { + SettingsViewModel vm; + if (_settingsWindow != null && _settingsWindow.IsVisible) + { + _settingsWindow.Activate(); + } + else + { + if (_settings == null || _launcher == null) + { + return; + } + vm = new SettingsViewModel(_settings); + _settingsWindow = new SettingsWindow(vm, PreviewCallback, RevertCallback) + { + SuspendHotkeyCallback = delegate(bool suspend) + { + if (_inputListener != null) + { + _inputListener.SuspendHotkey = suspend; + } + } + }; + vm.SaveCompleted += delegate + { + if (_inputListener != null && _settings != null) + { + _inputListener.UpdateHotkey(_settings.Settings.Hotkey); + _inputListener.UpdateCaptureHotkey(_settings.Settings.ScreenCapture.GlobalHotkey, _settings.Settings.ScreenCapture.GlobalHotkeyEnabled); + } + _worktimeReminder?.RestartTimer(); + }; + _settingsWindow.Show(); + } + void PreviewCallback(string themeKey) + { + if (themeKey == "custom") + { + CustomThemeColors customThemeColors = new CustomThemeColors(); + foreach (ColorRowModel colorRow in vm.ColorRows) + { + typeof(CustomThemeColors).GetProperty(colorRow.Property)?.SetValue(customThemeColors, colorRow.Hex); + } + _launcher.ApplyTheme(themeKey, customThemeColors); + } + else + { + _launcher.ApplyTheme(themeKey, _settings.Settings.Launcher.CustomTheme); + } + } + void RevertCallback() + { + _launcher.ApplyTheme(_settings.Settings.Launcher.Theme ?? "system", _settings.Settings.Launcher.CustomTheme); + } + } + + private static Icon LoadAppIcon() + { + Size smallIconSize = SystemInformation.SmallIconSize; + try + { + string path = Path.GetDirectoryName(Environment.ProcessPath) ?? AppContext.BaseDirectory; + string text = Path.Combine(path, "Assets", "icon.ico"); + if (File.Exists(text)) + { + return new Icon(text, smallIconSize); + } + } + catch + { + } + try + { + Uri uriResource = new Uri("pack://application:,,,/Assets/icon.ico"); + Stream stream = System.Windows.Application.GetResourceStream(uriResource)?.Stream; + if (stream != null) + { + return new Icon(stream, smallIconSize); + } + } + catch + { + } + return SystemIcons.Application; + } + + private static bool IsAutoStartEnabled() + { + try + { + using RegistryKey registryKey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Run", writable: false); + return registryKey?.GetValue("AxCopilot") != null; + } + catch + { + return false; + } + } + + private static void SetAutoStart(bool enable) + { + try + { + using RegistryKey registryKey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Run", writable: true); + if (registryKey == null) + { + return; + } + if (enable) + { + string text = Environment.ProcessPath ?? Process.GetCurrentProcess().MainModule?.FileName ?? string.Empty; + if (!string.IsNullOrEmpty(text)) + { + registryKey.SetValue("AxCopilot", "\"" + text + "\""); + } + } + else + { + registryKey.DeleteValue("AxCopilot", throwOnMissingValue: false); + } + } + catch (Exception ex) + { + LogService.Warn("자동 시작 레지스트리 설정 실패: " + ex.Message); + } + } + + protected override void OnExit(ExitEventArgs e) + { + _chatWindow?.ForceClose(); + _inputListener?.Dispose(); + _clipboardHistory?.Dispose(); + _indexService?.Dispose(); + _sessionTracking?.Dispose(); + _worktimeReminder?.Dispose(); + _trayIcon?.Dispose(); + try + { + _singleInstanceMutex?.ReleaseMutex(); + } + catch + { + } + _singleInstanceMutex?.Dispose(); + LogService.Info("=== AX Copilot 종료 ==="); + base.OnExit(e); + } + + [DebuggerNonUserCode] + [GeneratedCode("PresentationBuildTasks", "10.0.5.0")] + public void InitializeComponent() + { + if (!_contentLoaded) + { + _contentLoaded = true; + Uri resourceLocator = new Uri("/AxCopilot;component/app.xaml", UriKind.Relative); + System.Windows.Application.LoadComponent(this, resourceLocator); + } + } + + [STAThread] + [DebuggerNonUserCode] + [GeneratedCode("PresentationBuildTasks", "10.0.5.0")] + public static void Main() + { + App app = new App(); + app.InitializeComponent(); + app.Run(); + } +} diff --git a/.decompiledproj/AxCopilot/Core/BulkObservableCollection.cs b/.decompiledproj/AxCopilot/Core/BulkObservableCollection.cs new file mode 100644 index 0000000..a88509b --- /dev/null +++ b/.decompiledproj/AxCopilot/Core/BulkObservableCollection.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; + +namespace AxCopilot.Core; + +public sealed class BulkObservableCollection : ObservableCollection +{ + private bool _suppressNotification; + + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + if (!_suppressNotification) + { + base.OnCollectionChanged(e); + } + } + + protected override void OnPropertyChanged(PropertyChangedEventArgs e) + { + if (!_suppressNotification) + { + base.OnPropertyChanged(e); + } + } + + public void ReplaceAll(IEnumerable items) + { + _suppressNotification = true; + try + { + base.Items.Clear(); + foreach (T item in items) + { + base.Items.Add(item); + } + } + finally + { + _suppressNotification = false; + } + OnPropertyChanged(new PropertyChangedEventArgs("Count")); + OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } +} diff --git a/.decompiledproj/AxCopilot/Core/CommandResolver.cs b/.decompiledproj/AxCopilot/Core/CommandResolver.cs new file mode 100644 index 0000000..bbcca8d --- /dev/null +++ b/.decompiledproj/AxCopilot/Core/CommandResolver.cs @@ -0,0 +1,249 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Core; + +public class CommandResolver +{ + private readonly FuzzyEngine _fuzzy; + + private readonly SettingsService _settings; + + private readonly Dictionary _handlers = new Dictionary(); + + private readonly List _fuzzyHandlers = new List(); + + public IReadOnlyDictionary RegisteredHandlers => _handlers; + + public CommandResolver(FuzzyEngine fuzzy, SettingsService settings) + { + _fuzzy = fuzzy; + _settings = settings; + } + + public void RegisterHandler(IActionHandler handler) + { + if (handler.Prefix == null) + { + _fuzzyHandlers.Add(handler); + LogService.Info("FuzzyHandler 등록: name='" + handler.Metadata.Name + "'"); + return; + } + if (_handlers.ContainsKey(handler.Prefix)) + { + LogService.Warn($"Prefix '{handler.Prefix}' 중복 등록: '{handler.Metadata.Name}'이 기존 핸들러를 덮어씁니다."); + } + _handlers[handler.Prefix] = handler; + LogService.Info($"Handler 등록: prefix='{handler.Prefix}', name='{handler.Metadata.Name}'"); + } + + public async Task> ResolveAsync(string input, CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(input)) + { + return Enumerable.Empty(); + } + foreach (var (prefix, handler) in _handlers) + { + if (input.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + object obj; + if (input.Length <= prefix.Length) + { + obj = ""; + } + else + { + string text2 = input; + int length = prefix.Length; + obj = text2.Substring(length, text2.Length - length).Trim(); + } + string query = (string)obj; + try + { + return await handler.GetItemsAsync(query, ct); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex2) + { + LogService.Error("Handler '" + handler.Metadata.Name + "' 오류: " + ex2.Message); + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("오류: " + ex2.Message, handler.Metadata.Name, null, null)); + } + } + } + int maxResults = _settings.Settings.Launcher.MaxResults; + HashSet seenPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + List fuzzyItems = UsageRankingService.SortByUsage((from r in _fuzzy.Search(input, maxResults * 2) + where seenPaths.Add(r.Entry.Path) + select r).Take(maxResults).Select(delegate(FuzzyResult r) + { + string displayName = r.Entry.DisplayName; + string subtitle; + if (r.Entry.Type == IndexEntryType.Alias) + { + string aliasType = r.Entry.AliasType; + if (1 == 0) + { + } + string text3 = ((aliasType == "url") ? "URL 단축키" : ((!(aliasType == "batch")) ? r.Entry.Path : "명령 단축키")); + if (1 == 0) + { + } + subtitle = text3; + } + else + { + subtitle = r.Entry.Path + " ⇧ Shift+Enter: 폴더 열기"; + } + object entry = r.Entry; + IndexEntryType type = r.Entry.Type; + if (1 == 0) + { + } + string symbol; + switch (type) + { + case IndexEntryType.App: + symbol = "\uecaa"; + break; + case IndexEntryType.Folder: + symbol = "\ue8b7"; + break; + case IndexEntryType.Alias: + { + string aliasType2 = r.Entry.AliasType; + if (1 == 0) + { + } + string text3 = ((aliasType2 == "url") ? "\ue774" : ((!(aliasType2 == "batch")) ? "\uecca" : "\ue756")); + if (1 == 0) + { + } + symbol = text3; + break; + } + default: + symbol = "\ue8a5"; + break; + } + if (1 == 0) + { + } + return new LauncherItem(displayName, subtitle, null, entry, null, symbol); + }), (LauncherItem item) => (item.Data as IndexEntry)?.Path).ToList(); + if (_fuzzyHandlers.Count > 0) + { + List>> extraTasks = _fuzzyHandlers.Select((IActionHandler h) => SafeGetItemsAsync(h, input, ct)).ToList(); + await Task.WhenAll(extraTasks); + foreach (Task> task in extraTasks) + { + if (task.IsCompletedSuccessfully) + { + fuzzyItems.AddRange(task.Result.Take(3)); + } + } + } + return fuzzyItems; + } + + public async Task ExecuteAsync(LauncherItem item, string lastInput, CancellationToken ct) + { + foreach (var (prefix, handler) in _handlers) + { + if (lastInput.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + object obj; + if (lastInput.Length <= prefix.Length) + { + obj = ""; + } + else + { + int length = prefix.Length; + obj = lastInput.Substring(length, lastInput.Length - length).Trim().Split(' ')[0]; + } + string q = (string)obj; + string cmdKey = (string.IsNullOrEmpty(q) ? prefix : (prefix + q)); + UsageStatisticsService.RecordCommandUsage(cmdKey); + await handler.ExecuteAsync(item, ct); + return; + } + } + object data = item.Data; + if (data is string urlData && urlData.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + await ExecuteNullPrefixAsync(item, ct); + return; + } + data = item.Data; + IndexEntry entry = data as IndexEntry; + if (entry == null) + { + return; + } + string expanded = Environment.ExpandEnvironmentVariables(entry.Path); + try + { + await Task.Run(() => Process.Start(new ProcessStartInfo(expanded) + { + UseShellExecute = true + })); + } + catch (Exception ex) + { + Exception ex2 = ex; + LogService.Error("실행 실패: " + expanded + " - " + ex2.Message); + } + Task.Run(delegate + { + UsageRankingService.RecordExecution(entry.Path); + }); + } + + public async Task ExecuteNullPrefixAsync(LauncherItem item, CancellationToken ct) + { + foreach (IActionHandler handler in _fuzzyHandlers) + { + try + { + await handler.ExecuteAsync(item, ct); + return; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex2) + { + LogService.Error("FuzzyHandler '" + handler.Metadata.Name + "' 실행 오류: " + ex2.Message); + } + } + } + + private static async Task> SafeGetItemsAsync(IActionHandler handler, string query, CancellationToken ct) + { + try + { + return await handler.GetItemsAsync(query, ct); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex2) + { + Exception ex3 = ex2; + LogService.Error("FuzzyHandler '" + handler.Metadata.Name + "' 오류: " + ex3.Message); + return Enumerable.Empty(); + } + } +} diff --git a/.decompiledproj/AxCopilot/Core/ContextManager.cs b/.decompiledproj/AxCopilot/Core/ContextManager.cs new file mode 100644 index 0000000..5c74f03 --- /dev/null +++ b/.decompiledproj/AxCopilot/Core/ContextManager.cs @@ -0,0 +1,384 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.Models; +using AxCopilot.Services; + +namespace AxCopilot.Core; + +public class ContextManager +{ + private struct RECT + { + public int Left; + + public int Top; + + public int Right; + + public int Bottom; + } + + private struct WINDOWPLACEMENT + { + public uint length; + + public uint flags; + + public uint showCmd; + + public POINT ptMinPosition; + + public POINT ptMaxPosition; + + public RECT rcNormalPosition; + } + + private struct POINT + { + public int x; + + public int y; + } + + private delegate bool EnumWindowsProc(nint hWnd, nint lParam); + + private delegate bool MonitorEnumProc(nint hMonitor, nint hdcMonitor, ref RECT lprcMonitor, nint dwData); + + private readonly SettingsService _settings; + + private const uint SWP_NOZORDER = 4u; + + private const uint SWP_NOACTIVATE = 16u; + + private const uint MONITOR_DEFAULTTONEAREST = 2u; + + public ContextManager(SettingsService settings) + { + _settings = settings; + } + + public WorkspaceProfile CaptureProfile(string name) + { + List snapshots = new List(); + Dictionary monitorMap = BuildMonitorMap(); + EnumWindows(delegate(nint hWnd, nint _) + { + if (!IsWindowVisible(hWnd)) + { + return true; + } + if (IsIconic(hWnd)) + { + return true; + } + string windowTitle = GetWindowTitle(hWnd); + if (string.IsNullOrWhiteSpace(windowTitle)) + { + return true; + } + if (IsSystemWindow(hWnd)) + { + return true; + } + string processPath = GetProcessPath(hWnd); + if (string.IsNullOrEmpty(processPath)) + { + return true; + } + GetWindowRect(hWnd, out var lpRect); + GetWindowPlacement(hWnd, out var lpwndpl); + uint showCmd = lpwndpl.showCmd; + if (1 == 0) + { + } + string text = showCmd switch + { + 1u => "Normal", + 2u => "Minimized", + 3u => "Maximized", + _ => "Normal", + }; + if (1 == 0) + { + } + string showCmd2 = text; + int monitorIndex = GetMonitorIndex(hWnd, monitorMap); + snapshots.Add(new WindowSnapshot + { + Exe = processPath, + Title = windowTitle, + Rect = new WindowRect + { + X = lpRect.Left, + Y = lpRect.Top, + Width = lpRect.Right - lpRect.Left, + Height = lpRect.Bottom - lpRect.Top + }, + ShowCmd = showCmd2, + Monitor = monitorIndex + }); + return true; + }, IntPtr.Zero); + WorkspaceProfile workspaceProfile = new WorkspaceProfile + { + Name = name, + Windows = snapshots, + CreatedAt = DateTime.Now + }; + WorkspaceProfile workspaceProfile2 = _settings.Settings.Profiles.FirstOrDefault((WorkspaceProfile p) => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + if (workspaceProfile2 != null) + { + _settings.Settings.Profiles.Remove(workspaceProfile2); + } + _settings.Settings.Profiles.Add(workspaceProfile); + _settings.Save(); + LogService.Info($"프로필 '{name}' 저장 완료: {snapshots.Count}개 창"); + return workspaceProfile; + } + + public async Task RestoreProfileAsync(string name, CancellationToken ct = default(CancellationToken)) + { + WorkspaceProfile profile = _settings.Settings.Profiles.FirstOrDefault((WorkspaceProfile p) => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + if (profile == null) + { + return new RestoreResult(Success: false, "프로필 '" + name + "'을 찾을 수 없습니다."); + } + List results = new List(); + int monitorCount = GetMonitorCount(); + foreach (WindowSnapshot snapshot in profile.Windows) + { + ct.ThrowIfCancellationRequested(); + nint hWnd = FindMatchingWindow(snapshot); + if (hWnd == IntPtr.Zero && File.Exists(snapshot.Exe)) + { + try + { + Process.Start(new ProcessStartInfo(snapshot.Exe) + { + UseShellExecute = true + }); + hWnd = await WaitForWindowAsync(snapshot.Exe, TimeSpan.FromSeconds(3.0), ct); + } + catch (Exception ex) + { + Exception ex2 = ex; + results.Add($"⚠ {snapshot.Title}: 실행 실패 ({ex2.Message})"); + LogService.Warn("앱 실행 실패: " + snapshot.Exe + " - " + ex2.Message); + continue; + } + } + if (hWnd == IntPtr.Zero) + { + results.Add("⏭ " + snapshot.Title + ": 창 없음, 건너뜀"); + continue; + } + if (snapshot.Monitor >= monitorCount) + { + string policy = _settings.Settings.MonitorMismatch; + if (policy == "skip") + { + results.Add("⏭ " + snapshot.Title + ": 모니터 불일치, 건너뜀"); + continue; + } + } + try + { + nint hWnd2 = hWnd; + string showCmd = snapshot.ShowCmd; + if (1 == 0) + { + } + string text = showCmd; + int nCmdShow = ((text == "Maximized") ? 3 : ((!(text == "Minimized")) ? 9 : 2)); + if (1 == 0) + { + } + ShowWindow(hWnd2, nCmdShow); + if (snapshot.ShowCmd == "Normal") + { + SetWindowPos(hWnd, IntPtr.Zero, snapshot.Rect.X, snapshot.Rect.Y, snapshot.Rect.Width, snapshot.Rect.Height, 20u); + } + results.Add("✓ " + snapshot.Title + ": 복원 완료"); + } + catch (Exception ex3) + { + results.Add($"⚠ {snapshot.Title}: 복원 실패 ({ex3.Message})"); + LogService.Warn("창 복원 실패 (권한 문제 가능): " + snapshot.Exe); + } + } + LogService.Info("프로필 '" + name + "' 복원: " + string.Join(", ", results)); + return new RestoreResult(Success: true, string.Join("\n", results)); + } + + public bool DeleteProfile(string name) + { + WorkspaceProfile workspaceProfile = _settings.Settings.Profiles.FirstOrDefault((WorkspaceProfile p) => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + if (workspaceProfile == null) + { + return false; + } + _settings.Settings.Profiles.Remove(workspaceProfile); + _settings.Save(); + return true; + } + + public bool RenameProfile(string oldName, string newName) + { + WorkspaceProfile workspaceProfile = _settings.Settings.Profiles.FirstOrDefault((WorkspaceProfile p) => p.Name.Equals(oldName, StringComparison.OrdinalIgnoreCase)); + if (workspaceProfile == null) + { + return false; + } + workspaceProfile.Name = newName; + _settings.Save(); + return true; + } + + private static nint FindMatchingWindow(WindowSnapshot snapshot) + { + nint found = IntPtr.Zero; + EnumWindows(delegate(nint hWnd, nint _) + { + string processPath = GetProcessPath(hWnd); + if (string.Equals(processPath, snapshot.Exe, StringComparison.OrdinalIgnoreCase)) + { + found = hWnd; + return false; + } + return true; + }, IntPtr.Zero); + return found; + } + + private static async Task WaitForWindowAsync(string exePath, TimeSpan timeout, CancellationToken ct) + { + DateTime deadline = DateTime.UtcNow + timeout; + while (DateTime.UtcNow < deadline) + { + ct.ThrowIfCancellationRequested(); + nint hWnd = FindMatchingWindow(new WindowSnapshot + { + Exe = exePath + }); + if (hWnd != IntPtr.Zero) + { + return hWnd; + } + await Task.Delay(200, ct); + } + return IntPtr.Zero; + } + + private static string GetWindowTitle(nint hWnd) + { + StringBuilder stringBuilder = new StringBuilder(256); + GetWindowText(hWnd, stringBuilder, stringBuilder.Capacity); + return stringBuilder.ToString(); + } + + private static string GetProcessPath(nint hWnd) + { + try + { + GetWindowThreadProcessId(hWnd, out var lpdwProcessId); + if (lpdwProcessId == 0) + { + return ""; + } + Process processById = Process.GetProcessById((int)lpdwProcessId); + return processById.MainModule?.FileName ?? ""; + } + catch + { + return ""; + } + } + + private static bool IsSystemWindow(nint hWnd) + { + StringBuilder stringBuilder = new StringBuilder(256); + GetClassName(hWnd, stringBuilder, stringBuilder.Capacity); + switch (stringBuilder.ToString()) + { + case "Shell_TrayWnd": + case "Progman": + case "WorkerW": + case "DV2ControlHost": + return true; + default: + return false; + } + } + + private static Dictionary BuildMonitorMap() + { + List monitors = new List(); + EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, delegate(nint hMonitor, nint hdcMonitor, ref RECT lprc, nint dwData) + { + monitors.Add(hMonitor); + return true; + }, IntPtr.Zero); + return monitors.Select((nint hm, int idx) => (hm: hm, idx: idx)).ToDictionary(((nint hm, int idx) t) => t.hm, ((nint hm, int idx) t) => t.idx); + } + + private static int GetMonitorIndex(nint hWnd, Dictionary map) + { + nint key = MonitorFromWindow(hWnd, 2u); + int value; + return map.TryGetValue(key, out value) ? value : 0; + } + + private static int GetMonitorCount() + { + int count = 0; + EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, delegate + { + count++; + return true; + }, IntPtr.Zero); + return count; + } + + [DllImport("user32.dll")] + private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, nint lParam); + + [DllImport("user32.dll")] + private static extern bool IsWindowVisible(nint hWnd); + + [DllImport("user32.dll")] + private static extern bool IsIconic(nint hWnd); + + [DllImport("user32.dll")] + private static extern int GetWindowText(nint hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll")] + private static extern bool GetWindowRect(nint hWnd, out RECT lpRect); + + [DllImport("user32.dll")] + private static extern bool GetWindowPlacement(nint hWnd, out WINDOWPLACEMENT lpwndpl); + + [DllImport("user32.dll")] + private static extern uint GetWindowThreadProcessId(nint hWnd, out uint lpdwProcessId); + + [DllImport("user32.dll")] + private static extern int GetClassName(nint hWnd, StringBuilder lpClassName, int nMaxCount); + + [DllImport("user32.dll")] + private static extern bool SetWindowPos(nint hWnd, nint hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + + [DllImport("user32.dll")] + private static extern bool ShowWindow(nint hWnd, int nCmdShow); + + [DllImport("user32.dll")] + private static extern nint MonitorFromWindow(nint hwnd, uint dwFlags); + + [DllImport("user32.dll")] + private static extern bool EnumDisplayMonitors(nint hdc, nint lprcClip, MonitorEnumProc lpfnEnum, nint dwData); +} diff --git a/.decompiledproj/AxCopilot/Core/FuzzyEngine.cs b/.decompiledproj/AxCopilot/Core/FuzzyEngine.cs new file mode 100644 index 0000000..96a6cce --- /dev/null +++ b/.decompiledproj/AxCopilot/Core/FuzzyEngine.cs @@ -0,0 +1,468 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using AxCopilot.Services; + +namespace AxCopilot.Core; + +public class FuzzyEngine +{ + private readonly IndexService _index; + + private static readonly char[] Chosungs = new char[19] + { + 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', + 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' + }; + + private static readonly HashSet ChosungSet = new HashSet(new _003C_003Ez__ReadOnlyArray(new char[19] + { + 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', + 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' + })); + + private static readonly char[] Jungsungs = new char[21] + { + 'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', + 'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', + 'ㅣ' + }; + + private static readonly char[] Jongsungs = new char[28] + { + '\0', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ', 'ㄹ', 'ㄺ', + 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', + 'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' + }; + + public FuzzyEngine(IndexService index) + { + _index = index; + } + + public IEnumerable Search(string query, int maxResults = 7) + { + if (string.IsNullOrWhiteSpace(query)) + { + return Enumerable.Empty(); + } + string normalized = query.Trim().ToLowerInvariant(); + IReadOnlyList entries = _index.Entries; + bool queryHasKorean = false; + string text = normalized; + foreach (char c in text) + { + if ((c >= '가' && c <= '힣') || ChosungSet.Contains(c)) + { + queryHasKorean = true; + break; + } + } + if (entries.Count > 300) + { + return (from e in entries.AsParallel() + select new FuzzyResult(e, CalculateScoreFast(normalized, e, queryHasKorean)) into r + where r.Score > 0 + orderby r.Score descending + select r).Take(maxResults); + } + return (from e in entries + select new FuzzyResult(e, CalculateScoreFast(normalized, e, queryHasKorean)) into r + where r.Score > 0 + orderby r.Score descending + select r).Take(maxResults); + } + + private static int CalculateScoreFast(string query, IndexEntry entry, bool queryHasKorean) + { + string text = (string.IsNullOrEmpty(entry.NameLower) ? entry.Name.ToLowerInvariant() : entry.NameLower); + if (query.Length == 0) + { + return 0; + } + if (text == query) + { + return 1000 + entry.Score; + } + if (text.StartsWith(query)) + { + return 800 + entry.Score; + } + if (text.Contains(query)) + { + return 600 + entry.Score; + } + if (!queryHasKorean) + { + int num = FuzzyMatch(query, text); + return (num > 0) ? (num + entry.Score) : 0; + } + int num2 = JamoContainsScoreFast(string.IsNullOrEmpty(entry.NameJamo) ? DecomposeToJamo(text) : entry.NameJamo, query); + if (num2 > 0) + { + return num2 + entry.Score; + } + int num3 = ChosungMatchScoreFast(string.IsNullOrEmpty(entry.NameChosung) ? null : entry.NameChosung, text, query); + if (num3 > 0) + { + return num3 + entry.Score; + } + int num4 = FuzzyMatch(query, text); + if (num4 > 0) + { + return num4 + entry.Score; + } + return 0; + } + + internal static int CalculateScore(string query, string target, int baseScore) + { + if (query.Length == 0) + { + return 0; + } + if (target == query) + { + return 1000 + baseScore; + } + if (target.StartsWith(query)) + { + return 800 + baseScore; + } + if (target.Contains(query)) + { + return 600 + baseScore; + } + int num = JamoContainsScore(target, query); + if (num > 0) + { + return num + baseScore; + } + int num2 = ChosungMatchScore(target, query); + if (num2 > 0) + { + return num2 + baseScore; + } + int num3 = FuzzyMatch(query, target); + if (num3 > 0) + { + return num3 + baseScore; + } + return 0; + } + + internal static int FuzzyMatch(string query, string target) + { + int num = 0; + int num2 = 0; + int num3 = 0; + int num4 = -1; + while (num < query.Length && num2 < target.Length) + { + if (query[num] == target[num2]) + { + num3 = ((num4 != num2 - 1) ? (num3 + 10) : (num3 + 30)); + if (num2 == 0) + { + num3 += 15; + } + num4 = num2; + num++; + } + num2++; + } + return (num == query.Length) ? Math.Max(num3, 50) : 0; + } + + internal static string DecomposeToJamo(string text) + { + StringBuilder stringBuilder = new StringBuilder(text.Length * 3); + foreach (char c in text) + { + if (c >= '가' && c <= '힣') + { + int num = c - 44032; + int num2 = num / 588; + int num3 = num % 588 / 28; + int num4 = num % 28; + stringBuilder.Append(Chosungs[num2]); + stringBuilder.Append(Jungsungs[num3]); + if (num4 > 0) + { + stringBuilder.Append(Jongsungs[num4]); + } + } + else + { + stringBuilder.Append(c); + } + } + return stringBuilder.ToString(); + } + + internal static char GetChosung(char hangul) + { + if (hangul < '가' || hangul > '힣') + { + return '\0'; + } + int num = hangul - 44032; + return Chosungs[num / 588]; + } + + internal static int JamoContainsScore(string target, string query) + { + if (!HasKorean(query)) + { + return 0; + } + string text = DecomposeToJamo(target); + string text2 = DecomposeToJamo(query); + if (text2.Length == 0 || text.Length == 0) + { + return 0; + } + if (text.Contains(text2)) + { + return (text.IndexOf(text2) == 0) ? 580 : 550; + } + int num = 0; + for (int i = 0; i < text.Length; i++) + { + if (num >= text2.Length) + { + break; + } + if (text2[num] == text[i]) + { + num++; + } + } + if (num == text2.Length) + { + return 400; + } + return 0; + } + + internal static bool HasChosung(string text) + { + return text.Any((char c) => ChosungSet.Contains(c)); + } + + internal static bool IsChosung(string text) + { + return text.Length > 0 && text.All((char c) => ChosungSet.Contains(c)); + } + + private static bool HasKorean(string text) + { + return text.Any((char c) => c >= '가' && c <= '힣'); + } + + internal static int ChosungMatchScore(string target, string query) + { + if (!HasChosung(query)) + { + return 0; + } + List list = new List(); + List list2 = new List(); + foreach (char c in target) + { + char chosung = GetChosung(c); + if (chosung != 0) + { + list.Add(chosung); + list2.Add(c); + } + else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) + { + list.Add(c); + list2.Add(c); + } + } + if (list.Count == 0) + { + return 0; + } + if (IsChosung(query)) + { + if (ContainsChosungConsecutive(list, query)) + { + return 520; + } + if (ContainsChosungSubsequence(list, query)) + { + return 480; + } + return 0; + } + return MixedChosungMatch(list2, list, query); + } + + private static bool ContainsChosungConsecutive(List targetChosungs, string query) + { + for (int i = 0; i <= targetChosungs.Count - query.Length; i++) + { + bool flag = true; + for (int j = 0; j < query.Length; j++) + { + if (targetChosungs[i + j] != query[j]) + { + flag = false; + break; + } + } + if (flag) + { + return true; + } + } + return false; + } + + private static bool ContainsChosungSubsequence(List targetChosungs, string query) + { + int num = 0; + for (int i = 0; i < targetChosungs.Count; i++) + { + if (num >= query.Length) + { + break; + } + if (targetChosungs[i] == query[num]) + { + num++; + } + } + return num == query.Length; + } + + private static int MixedChosungMatch(List targetChars, List targetChosungs, string query) + { + int num = 0; + int num2 = 0; + while (num < query.Length && num2 < targetChars.Count) + { + char c = query[num]; + if (ChosungSet.Contains(c)) + { + if (targetChosungs[num2] == c) + { + num++; + } + } + else if (targetChars[num2] == c) + { + num++; + } + num2++; + } + return (num == query.Length) ? 460 : 0; + } + + private static int JamoContainsScoreFast(string targetJamo, string query) + { + if (!HasKorean(query)) + { + return 0; + } + string text = DecomposeToJamo(query); + if (text.Length == 0 || targetJamo.Length == 0) + { + return 0; + } + if (targetJamo.Contains(text)) + { + return (targetJamo.IndexOf(text, StringComparison.Ordinal) == 0) ? 580 : 550; + } + int num = 0; + for (int i = 0; i < targetJamo.Length; i++) + { + if (num >= text.Length) + { + break; + } + if (text[num] == targetJamo[i]) + { + num++; + } + } + return (num == text.Length) ? 400 : 0; + } + + private static int ChosungMatchScoreFast(string? targetChosung, string targetLower, string query) + { + if (!HasChosung(query)) + { + return 0; + } + if (IsChosung(query)) + { + if (string.IsNullOrEmpty(targetChosung)) + { + return 0; + } + if (targetChosung.Contains(query, StringComparison.Ordinal)) + { + return 520; + } + int num = 0; + for (int i = 0; i < targetChosung.Length; i++) + { + if (num >= query.Length) + { + break; + } + if (targetChosung[i] == query[num]) + { + num++; + } + } + if (num == query.Length) + { + return 480; + } + return 0; + } + int num2 = 0; + int num3 = 0; + while (num2 < query.Length && num3 < targetLower.Length) + { + char c = query[num2]; + char c2 = targetLower[num3]; + if (ChosungSet.Contains(c)) + { + char c3 = GetChosung(c2); + if (c3 == '\0' && ((c2 >= 'a' && c2 <= 'z') || (c2 >= '0' && c2 <= '9'))) + { + c3 = c2; + } + if (c3 == c) + { + num2++; + } + } + else if (c2 == c) + { + num2++; + } + num3++; + } + return (num2 == query.Length) ? 460 : 0; + } + + internal static bool ContainsChosung(string target, string chosungQuery) + { + List list = (from c in target.Select(GetChosung) + where c != '\0' + select c).ToList(); + if (list.Count < chosungQuery.Length) + { + return false; + } + return ContainsChosungConsecutive(list, chosungQuery) || ContainsChosungSubsequence(list, chosungQuery); + } +} diff --git a/.decompiledproj/AxCopilot/Core/FuzzyResult.cs b/.decompiledproj/AxCopilot/Core/FuzzyResult.cs new file mode 100644 index 0000000..4093926 --- /dev/null +++ b/.decompiledproj/AxCopilot/Core/FuzzyResult.cs @@ -0,0 +1,5 @@ +using AxCopilot.Services; + +namespace AxCopilot.Core; + +public record FuzzyResult(IndexEntry Entry, int Score); diff --git a/.decompiledproj/AxCopilot/Core/HotkeyDefinition.cs b/.decompiledproj/AxCopilot/Core/HotkeyDefinition.cs new file mode 100644 index 0000000..0ec6112 --- /dev/null +++ b/.decompiledproj/AxCopilot/Core/HotkeyDefinition.cs @@ -0,0 +1,3 @@ +namespace AxCopilot.Core; + +public record struct HotkeyDefinition(int VkCode, bool Ctrl, bool Alt, bool Shift, bool Win); diff --git a/.decompiledproj/AxCopilot/Core/HotkeyParser.cs b/.decompiledproj/AxCopilot/Core/HotkeyParser.cs new file mode 100644 index 0000000..3b44290 --- /dev/null +++ b/.decompiledproj/AxCopilot/Core/HotkeyParser.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; + +namespace AxCopilot.Core; + +public static class HotkeyParser +{ + private static readonly Dictionary _keyMap; + + static HotkeyParser() + { + _keyMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Space"] = 32, + ["Enter"] = 13, + ["Return"] = 13, + ["Tab"] = 9, + ["Esc"] = 27, + ["Escape"] = 27, + ["Backspace"] = 8, + ["Back"] = 8, + ["Delete"] = 46, + ["Del"] = 46, + ["Insert"] = 45, + ["Ins"] = 45, + ["Home"] = 36, + ["End"] = 35, + ["PageUp"] = 33, + ["PgUp"] = 33, + ["PageDown"] = 34, + ["PgDn"] = 34, + ["PrintScreen"] = 44, + ["PrtSc"] = 44, + ["Snapshot"] = 44, + ["Pause"] = 19, + ["Break"] = 19, + ["ScrollLock"] = 145, + ["Left"] = 37, + ["Up"] = 38, + ["Right"] = 39, + ["Down"] = 40, + ["`"] = 192, + ["Grave"] = 192, + ["-"] = 189, + ["="] = 187, + ["["] = 219, + ["]"] = 221, + ["\\"] = 220, + [";"] = 186, + ["'"] = 222, + [","] = 188, + ["."] = 190, + ["/"] = 191 + }; + for (char c = 'A'; c <= 'Z'; c = (char)(c + 1)) + { + _keyMap[c.ToString()] = c; + } + for (char c2 = '0'; c2 <= '9'; c2 = (char)(c2 + 1)) + { + _keyMap[c2.ToString()] = c2; + } + for (int i = 1; i <= 24; i++) + { + _keyMap[$"F{i}"] = 111 + i; + } + for (int j = 0; j <= 9; j++) + { + _keyMap[$"Num{j}"] = 96 + j; + } + } + + public static bool TryParse(string hotkey, out HotkeyDefinition result) + { + result = default(HotkeyDefinition); + if (string.IsNullOrWhiteSpace(hotkey)) + { + return false; + } + string[] array = hotkey.Split('+', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + bool ctrl = false; + bool alt = false; + bool shift = false; + bool win = false; + int? num = null; + string[] array2 = array; + foreach (string text in array2) + { + if (text.Equals("Ctrl", StringComparison.OrdinalIgnoreCase) || text.Equals("Control", StringComparison.OrdinalIgnoreCase)) + { + ctrl = true; + continue; + } + if (text.Equals("Alt", StringComparison.OrdinalIgnoreCase)) + { + alt = true; + continue; + } + if (text.Equals("Shift", StringComparison.OrdinalIgnoreCase)) + { + shift = true; + continue; + } + if (text.Equals("Win", StringComparison.OrdinalIgnoreCase) || text.Equals("Windows", StringComparison.OrdinalIgnoreCase)) + { + win = true; + continue; + } + if (_keyMap.TryGetValue(text, out var value)) + { + num = value; + continue; + } + return false; + } + if (!num.HasValue) + { + return false; + } + result = new HotkeyDefinition(num.Value, ctrl, alt, shift, win); + return true; + } + + public static string Format(HotkeyDefinition def) + { + List list = new List(5); + if (def.Ctrl) + { + list.Add("Ctrl"); + } + if (def.Alt) + { + list.Add("Alt"); + } + if (def.Shift) + { + list.Add("Shift"); + } + if (def.Win) + { + list.Add("Win"); + } + list.Add(VkToName(def.VkCode)); + return string.Join("+", list); + } + + private static string VkToName(int vk) + { + if (vk >= 65 && vk <= 90) + { + return ((char)vk).ToString(); + } + if (vk >= 48 && vk <= 57) + { + return ((char)vk).ToString(); + } + if (vk >= 112 && vk <= 135) + { + return $"F{vk - 111}"; + } + if (vk >= 96 && vk <= 105) + { + return $"Num{vk - 96}"; + } + string text = null; + foreach (var (text3, num2) in _keyMap) + { + if (num2 == vk && (text == null || text3.Length > text.Length)) + { + text = text3; + } + } + return text ?? $"0x{vk:X2}"; + } +} diff --git a/.decompiledproj/AxCopilot/Core/InputListener.cs b/.decompiledproj/AxCopilot/Core/InputListener.cs new file mode 100644 index 0000000..b7b692f --- /dev/null +++ b/.decompiledproj/AxCopilot/Core/InputListener.cs @@ -0,0 +1,249 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using AxCopilot.Services; + +namespace AxCopilot.Core; + +public class InputListener : IDisposable +{ + private delegate nint LowLevelKeyboardProc(int nCode, nint wParam, nint lParam); + + private const int WH_KEYBOARD_LL = 13; + + private const int WM_KEYDOWN = 256; + + private const int WM_SYSKEYDOWN = 260; + + private const int WM_KEYUP = 257; + + private const int WM_SYSKEYUP = 261; + + private const int VK_SHIFT = 16; + + private const int VK_CONTROL = 17; + + private const int VK_MENU = 18; + + private const int VK_LWIN = 91; + + private const int VK_RWIN = 92; + + private nint _hookHandle = IntPtr.Zero; + + private LowLevelKeyboardProc? _proc; + + private int _retryCount = 0; + + private const int MaxRetry = 3; + + private volatile bool _suppressNextAltUp; + + private volatile bool _suppressNextKeyUp; + + private volatile int _suppressKeyUpVk; + + private HotkeyDefinition _hotkey = new HotkeyDefinition(32, Ctrl: false, Alt: true, Shift: false, Win: false); + + private HotkeyDefinition _captureHotkey; + + private bool _captureHotkeyEnabled; + + public bool SuspendHotkey { get; set; } + + public Func? KeyFilter { get; set; } + + public event EventHandler? HotkeyTriggered; + + public event EventHandler? CaptureHotkeyTriggered; + + public event EventHandler? HookFailed; + + public void UpdateHotkey(string hotkeyStr) + { + if (HotkeyParser.TryParse(hotkeyStr, out var result)) + { + _hotkey = result; + LogService.Info("핫키 변경: " + hotkeyStr); + } + else + { + LogService.Warn("핫키 파싱 실패: '" + hotkeyStr + "' — 기존 핫키 유지"); + } + } + + public void UpdateCaptureHotkey(string hotkeyStr, bool enabled) + { + _captureHotkeyEnabled = enabled; + if (enabled && HotkeyParser.TryParse(hotkeyStr, out var result)) + { + _captureHotkey = result; + LogService.Info("캡처 단축키 활성화: " + hotkeyStr); + } + else if (!enabled) + { + LogService.Info("캡처 단축키 비활성화"); + } + } + + public void Start() + { + _proc = HookCallback; + Register(); + } + + private void Register() + { + using Process process = Process.GetCurrentProcess(); + using ProcessModule processModule = process.MainModule; + _hookHandle = SetWindowsHookEx(13, _proc, GetModuleHandle(processModule.ModuleName), 0u); + if (_hookHandle == IntPtr.Zero) + { + int lastWin32Error = Marshal.GetLastWin32Error(); + LogService.Error($"Global Hook 등록 실패 (에러 코드: {lastWin32Error})"); + TryRetryRegister(); + } + else + { + _retryCount = 0; + LogService.Info("Global Keyboard Hook 등록 완료 (" + HotkeyParser.Format(_hotkey) + ")"); + } + } + + private void TryRetryRegister() + { + if (_retryCount < 3) + { + _retryCount++; + LogService.Warn($"Hook 재등록 시도 {_retryCount}/{3}"); + Task.Delay(1000).ContinueWith(delegate + { + Register(); + }); + } + else + { + LogService.Error("Hook 재등록 최대 횟수 초과"); + this.HookFailed?.Invoke(this, EventArgs.Empty); + } + } + + private static bool IsSuppressedForegroundWindow() + { + nint foregroundWindow = GetForegroundWindow(); + if (foregroundWindow == IntPtr.Zero) + { + return false; + } + StringBuilder stringBuilder = new StringBuilder(64); + GetClassName(foregroundWindow, stringBuilder, 64); + string text = stringBuilder.ToString(); + return text == "#32770" || text == "SunAwtDialog"; + } + + private nint HookCallback(int nCode, nint wParam, nint lParam) + { + if (nCode < 0) + { + return CallNextHookEx(_hookHandle, nCode, wParam, lParam); + } + int num = Marshal.ReadInt32(lParam); + if (wParam == 257 || wParam == 261) + { + if (_suppressNextAltUp && num == 18) + { + _suppressNextAltUp = false; + return 1; + } + if (_suppressNextKeyUp && num == _suppressKeyUpVk) + { + _suppressNextKeyUp = false; + return 1; + } + return CallNextHookEx(_hookHandle, nCode, wParam, lParam); + } + if (wParam != 256 && wParam != 260) + { + return CallNextHookEx(_hookHandle, nCode, wParam, lParam); + } + if (IsSuppressedForegroundWindow()) + { + return CallNextHookEx(_hookHandle, nCode, wParam, lParam); + } + if (!SuspendHotkey && num == _hotkey.VkCode) + { + bool flag = !_hotkey.Ctrl || (GetAsyncKeyState(17) & 0x8000) != 0; + bool flag2 = !_hotkey.Alt || (GetAsyncKeyState(18) & 0x8000) != 0; + bool flag3 = !_hotkey.Shift || (GetAsyncKeyState(16) & 0x8000) != 0; + bool flag4 = !_hotkey.Win || (GetAsyncKeyState(91) & 0x8000) != 0 || (GetAsyncKeyState(92) & 0x8000) != 0; + if (flag && flag2 && flag3 && flag4) + { + this.HotkeyTriggered?.Invoke(this, EventArgs.Empty); + _suppressNextKeyUp = true; + _suppressKeyUpVk = num; + if (_hotkey.Alt) + { + _suppressNextAltUp = true; + } + return 1; + } + } + if (!SuspendHotkey && _captureHotkeyEnabled && num == _captureHotkey.VkCode) + { + bool flag5 = !_captureHotkey.Ctrl || (GetAsyncKeyState(17) & 0x8000) != 0; + bool flag6 = !_captureHotkey.Alt || (GetAsyncKeyState(18) & 0x8000) != 0; + bool flag7 = !_captureHotkey.Shift || (GetAsyncKeyState(16) & 0x8000) != 0; + bool flag8 = !_captureHotkey.Win || (GetAsyncKeyState(91) & 0x8000) != 0 || (GetAsyncKeyState(92) & 0x8000) != 0; + if (flag5 && flag6 && flag7 && flag8) + { + this.CaptureHotkeyTriggered?.Invoke(this, EventArgs.Empty); + _suppressNextKeyUp = true; + _suppressKeyUpVk = num; + if (_captureHotkey.Alt) + { + _suppressNextAltUp = true; + } + return 1; + } + } + Func? keyFilter = KeyFilter; + if (keyFilter != null && keyFilter(num)) + { + return 1; + } + return CallNextHookEx(_hookHandle, nCode, wParam, lParam); + } + + public void Dispose() + { + if (_hookHandle != IntPtr.Zero) + { + UnhookWindowsHookEx(_hookHandle); + _hookHandle = IntPtr.Zero; + } + } + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern nint SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, nint hMod, uint dwThreadId); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool UnhookWindowsHookEx(nint hhk); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern nint CallNextHookEx(nint hhk, int nCode, nint wParam, nint lParam); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern nint GetModuleHandle(string lpModuleName); + + [DllImport("user32.dll")] + private static extern short GetAsyncKeyState(int vKey); + + [DllImport("user32.dll")] + private static extern nint GetForegroundWindow(); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern int GetClassName(nint hWnd, StringBuilder lpClassName, int nMaxCount); +} diff --git a/.decompiledproj/AxCopilot/Core/PluginHost.cs b/.decompiledproj/AxCopilot/Core/PluginHost.cs new file mode 100644 index 0000000..8efa1b8 --- /dev/null +++ b/.decompiledproj/AxCopilot/Core/PluginHost.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Reflection; +using AxCopilot.Handlers; +using AxCopilot.Models; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Core; + +public class PluginHost +{ + private readonly SettingsService _settings; + + private readonly CommandResolver _resolver; + + private readonly List _loadedPlugins = new List(); + + public IReadOnlyList LoadedPlugins => _loadedPlugins; + + public PluginHost(SettingsService settings, CommandResolver resolver) + { + _settings = settings; + _resolver = resolver; + } + + public void LoadAll() + { + _loadedPlugins.Clear(); + foreach (PluginEntry item in _settings.Settings.Plugins.Where((PluginEntry p) => p.Enabled)) + { + LoadPlugin(item.Path); + } + string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills"); + if (!Directory.Exists(path)) + { + return; + } + foreach (string item2 in Directory.EnumerateFiles(path, "*.skill.json")) + { + LoadJsonSkill(item2); + } + } + + private void LoadPlugin(string dllPath) + { + if (!File.Exists(dllPath)) + { + LogService.Warn("플러그인 파일 없음: " + dllPath); + return; + } + try + { + Assembly assembly = Assembly.LoadFrom(dllPath); + IEnumerable enumerable = from t in assembly.GetExportedTypes() + where typeof(IActionHandler).IsAssignableFrom(t) && !t.IsAbstract + select t; + foreach (Type item in enumerable) + { + if (Activator.CreateInstance(item) is IActionHandler actionHandler) + { + _resolver.RegisterHandler(actionHandler); + _loadedPlugins.Add(actionHandler); + LogService.Info("플러그인 로드: " + actionHandler.Metadata.Name + " v" + actionHandler.Metadata.Version); + } + } + } + catch (Exception ex) + { + LogService.Error("플러그인 로드 실패 (" + dllPath + "): " + ex.Message); + } + } + + private void LoadJsonSkill(string skillPath) + { + try + { + IActionHandler actionHandler = JsonSkillLoader.Load(skillPath); + if (actionHandler != null) + { + _resolver.RegisterHandler(actionHandler); + _loadedPlugins.Add(actionHandler); + LogService.Info("JSON 스킬 로드: " + actionHandler.Metadata.Name); + } + } + catch (Exception ex) + { + LogService.Error("JSON 스킬 로드 실패 (" + skillPath + "): " + ex.Message); + } + } + + public void Reload() + { + LogService.Info("플러그인 전체 재로드 시작"); + LoadAll(); + } + + public int InstallFromZip(string zipPath) + { + if (!File.Exists(zipPath)) + { + LogService.Warn("플러그인 zip 파일 없음: " + zipPath); + return 0; + } + string text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "plugins"); + Directory.CreateDirectory(text); + int num = 0; + try + { + using ZipArchive zipArchive = ZipFile.OpenRead(zipPath); + string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(zipPath); + string text2 = Path.Combine(text, fileNameWithoutExtension); + Directory.CreateDirectory(text2); + foreach (ZipArchiveEntry entry in zipArchive.Entries) + { + if (!string.IsNullOrEmpty(entry.Name)) + { + string text3 = Path.Combine(text2, entry.Name); + if (!Path.GetFullPath(text3).StartsWith(Path.GetFullPath(text2))) + { + LogService.Warn("플러그인 zip 경로 위험: " + entry.FullName); + } + else + { + entry.ExtractToFile(text3, overwrite: true); + } + } + } + foreach (string dllFile in Directory.EnumerateFiles(text2, "*.dll")) + { + if (!_settings.Settings.Plugins.Any((PluginEntry p) => p.Path == dllFile)) + { + _settings.Settings.Plugins.Add(new PluginEntry + { + Enabled = true, + Path = dllFile + }); + LoadPlugin(dllFile); + num++; + } + } + string text4 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills"); + Directory.CreateDirectory(text4); + foreach (string item in Directory.EnumerateFiles(text2, "*.skill.json")) + { + string text5 = Path.Combine(text4, Path.GetFileName(item)); + File.Copy(item, text5, overwrite: true); + LoadJsonSkill(text5); + num++; + } + if (num > 0) + { + _settings.Save(); + } + LogService.Info($"플러그인 설치 완료: {zipPath} → {num}개 핸들러"); + } + catch (Exception ex) + { + LogService.Error("플러그인 zip 설치 실패: " + ex.Message); + } + return num; + } + + public bool UninstallPlugin(string dllPath) + { + try + { + PluginEntry pluginEntry = _settings.Settings.Plugins.FirstOrDefault((PluginEntry p) => p.Path == dllPath); + if (pluginEntry != null) + { + _settings.Settings.Plugins.Remove(pluginEntry); + _settings.Save(); + } + string directoryName = Path.GetDirectoryName(dllPath); + if (directoryName != null && Directory.Exists(directoryName)) + { + string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "plugins"); + if (Path.GetFullPath(directoryName).StartsWith(Path.GetFullPath(path))) + { + Directory.Delete(directoryName, recursive: true); + } + } + LogService.Info("플러그인 제거 완료: " + dllPath); + return true; + } + catch (Exception ex) + { + LogService.Error("플러그인 제거 실패: " + ex.Message); + return false; + } + } +} diff --git a/.decompiledproj/AxCopilot/Core/RestoreResult.cs b/.decompiledproj/AxCopilot/Core/RestoreResult.cs new file mode 100644 index 0000000..ba5df7b --- /dev/null +++ b/.decompiledproj/AxCopilot/Core/RestoreResult.cs @@ -0,0 +1,3 @@ +namespace AxCopilot.Core; + +public record RestoreResult(bool Success, string Message); diff --git a/.decompiledproj/AxCopilot/Core/SnippetExpander.cs b/.decompiledproj/AxCopilot/Core/SnippetExpander.cs new file mode 100644 index 0000000..5ddb9ef --- /dev/null +++ b/.decompiledproj/AxCopilot/Core/SnippetExpander.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Windows; +using System.Windows.Threading; +using AxCopilot.Models; +using AxCopilot.Services; + +namespace AxCopilot.Core; + +public class SnippetExpander +{ + private struct INPUT + { + public int type; + + public InputUnion u; + } + + [StructLayout(LayoutKind.Explicit)] + private struct InputUnion + { + [FieldOffset(0)] + public MOUSEINPUT mi; + + [FieldOffset(0)] + public KEYBDINPUT ki; + + [FieldOffset(0)] + public HARDWAREINPUT hi; + } + + private struct KEYBDINPUT + { + public ushort wVk; + + public ushort wScan; + + public uint dwFlags; + + public uint time; + + public nint dwExtraInfo; + } + + private struct MOUSEINPUT + { + public int dx; + + public int dy; + + public uint mouseData; + + public uint dwFlags; + + public uint time; + + public nint dwExtraInfo; + } + + private struct HARDWAREINPUT + { + public uint uMsg; + + public ushort wParamL; + + public ushort wParamH; + } + + private readonly SettingsService _settings; + + private readonly StringBuilder _buffer = new StringBuilder(); + + private bool _tracking; + + private const ushort VK_BACK = 8; + + private const int VK_ESCAPE = 27; + + private const int VK_SPACE = 32; + + private const int VK_RETURN = 13; + + private const int VK_OEM_1 = 186; + + private const int VK_SHIFT = 16; + + private const int VK_CONTROL = 17; + + private const int VK_MENU = 18; + + private const ushort VK_CTRL_US = 17; + + private static readonly HashSet ClearKeys = new HashSet { 33, 34, 35, 36, 37, 38, 39, 40, 46 }; + + public SnippetExpander(SettingsService settings) + { + _settings = settings; + } + + public bool HandleKey(int vkCode) + { + if (!_settings.Settings.Launcher.SnippetAutoExpand) + { + return false; + } + if ((GetAsyncKeyState(17) & 0x8000) != 0) + { + _tracking = false; + _buffer.Clear(); + return false; + } + if ((GetAsyncKeyState(18) & 0x8000) != 0) + { + _tracking = false; + _buffer.Clear(); + return false; + } + if (vkCode == 186 && (GetAsyncKeyState(16) & 0x8000) == 0) + { + _tracking = true; + _buffer.Clear(); + _buffer.Append(';'); + return false; + } + if (!_tracking) + { + return false; + } + if ((vkCode >= 65 && vkCode <= 90) || (vkCode >= 48 && vkCode <= 57) || (vkCode >= 96 && vkCode <= 105) || vkCode == 189) + { + bool shifted = (GetAsyncKeyState(16) & 0x8000) != 0; + char c = VkToChar(vkCode, shifted); + if (c != 0) + { + _buffer.Append(char.ToLowerInvariant(c)); + } + return false; + } + switch (vkCode) + { + case 8: + if (_buffer.Length > 1) + { + _buffer.Remove(_buffer.Length - 1, 1); + } + else + { + _tracking = false; + _buffer.Clear(); + } + return false; + default: + if (vkCode != 13) + { + if (vkCode == 27 || ClearKeys.Contains(vkCode) || vkCode >= 112) + { + _tracking = false; + _buffer.Clear(); + } + return false; + } + goto case 32; + case 32: + if (_buffer.Length > 1) + { + string keyword = _buffer.ToString(1, _buffer.Length - 1); + _tracking = false; + _buffer.Clear(); + SnippetEntry snippetEntry = _settings.Settings.Snippets.FirstOrDefault((SnippetEntry s) => s.Key.Equals(keyword, StringComparison.OrdinalIgnoreCase)); + if (snippetEntry != null) + { + string expanded = ExpandVariables(snippetEntry.Content); + int deleteCount = keyword.Length + 1; + ((DispatcherObject)Application.Current).Dispatcher.BeginInvoke((Delegate)(Action)delegate + { + PasteExpansion(expanded, deleteCount); + }, Array.Empty()); + return true; + } + } + _tracking = false; + _buffer.Clear(); + return false; + } + } + + private static void PasteExpansion(string text, int deleteCount) + { + try + { + INPUT[] array = new INPUT[deleteCount * 2]; + for (int i = 0; i < deleteCount; i++) + { + array[i * 2] = MakeKeyInput(8, keyUp: false); + array[i * 2 + 1] = MakeKeyInput(8, keyUp: true); + } + SendInput((uint)array.Length, array, Marshal.SizeOf()); + Clipboard.SetText(text); + INPUT[] array2 = new INPUT[4] + { + MakeKeyInput(17, keyUp: false), + MakeKeyInput(86, keyUp: false), + MakeKeyInput(86, keyUp: true), + MakeKeyInput(17, keyUp: true) + }; + SendInput((uint)array2.Length, array2, Marshal.SizeOf()); + LogService.Info($"스니펫 확장 완료: {deleteCount}자 삭제 후 붙여넣기"); + } + catch (Exception ex) + { + LogService.Warn("스니펫 확장 실패: " + ex.Message); + } + } + + private static INPUT MakeKeyInput(ushort vk, bool keyUp) + { + INPUT result = new INPUT + { + type = 1 + }; + result.u.ki.wVk = vk; + result.u.ki.dwFlags = (keyUp ? 2u : 0u); + return result; + } + + private static string ExpandVariables(string content) + { + DateTime now = DateTime.Now; + return content.Replace("{date}", now.ToString("yyyy-MM-dd")).Replace("{time}", now.ToString("HH:mm:ss")).Replace("{datetime}", now.ToString("yyyy-MM-dd HH:mm:ss")) + .Replace("{year}", now.Year.ToString()) + .Replace("{month}", now.Month.ToString("D2")) + .Replace("{day}", now.Day.ToString("D2")); + } + + private static char VkToChar(int vk, bool shifted) + { + if (vk >= 65 && vk <= 90) + { + return shifted ? ((char)vk) : char.ToLowerInvariant((char)vk); + } + if (vk >= 48 && vk <= 57) + { + return shifted ? ")!@#$%^&*("[vk - 48] : ((char)vk); + } + if (vk >= 96 && vk <= 105) + { + return (char)(48 + (vk - 96)); + } + if (vk == 189) + { + return shifted ? '_' : '-'; + } + return '\0'; + } + + [DllImport("user32.dll")] + private static extern short GetAsyncKeyState(int vKey); + + [DllImport("user32.dll", SetLastError = true)] + private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); +} diff --git a/.decompiledproj/AxCopilot/Handlers/BatchHandler.cs b/.decompiledproj/AxCopilot/Handlers/BatchHandler.cs new file mode 100644 index 0000000..c847a20 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/BatchHandler.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.Models; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class BatchHandler : IActionHandler +{ + private readonly SettingsService _settings; + + public string? Prefix => ">"; + + public PluginMetadata Metadata => new PluginMetadata("batch", "명령 실행", "1.0", "AX"); + + public BatchHandler(SettingsService settings) + { + _settings = settings; + } + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + List list = new List(); + IEnumerable collection = from a in _settings.Settings.Aliases + where a.Type == "batch" && (string.IsNullOrEmpty(query) || a.Key.Contains(query, StringComparison.OrdinalIgnoreCase)) + select new LauncherItem(a.Key, a.Target, null, a, null, "\ue756"); + list.AddRange(collection); + if (!string.IsNullOrEmpty(query)) + { + string text = query.Replace("\"", "\\\""); + list.Insert(0, new LauncherItem("실행: " + query, "PowerShell에서 직접 실행", null, new AliasEntry + { + Type = "batch", + Target = "powershell -NoProfile -Command \"" + text + "\"", + ShowWindow = true + }, null, "\ue756")); + } + return Task.FromResult((IEnumerable)list); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is AliasEntry aliasEntry) + { + string text = Environment.ExpandEnvironmentVariables(aliasEntry.Target); + string[] array = text.Split(' ', 2); + ProcessStartInfo startInfo = new ProcessStartInfo(array[0]) + { + Arguments = ((array.Length > 1) ? array[1] : ""), + UseShellExecute = aliasEntry.ShowWindow, + CreateNoWindow = !aliasEntry.ShowWindow, + WindowStyle = ((!aliasEntry.ShowWindow) ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal) + }; + Process.Start(startInfo); + } + return Task.CompletedTask; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/BatchTextHandler.cs b/.decompiledproj/AxCopilot/Handlers/BatchTextHandler.cs new file mode 100644 index 0000000..5429dd5 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/BatchTextHandler.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class BatchTextHandler : IActionHandler +{ + private static readonly (string Cmd, string Desc)[] Commands = new(string, string)[14] + { + ("prefix [텍스트]", "각 줄 앞에 텍스트 추가"), + ("suffix [텍스트]", "각 줄 뒤에 텍스트 추가"), + ("wrap [문자]", "각 줄을 지정 문자로 감싸기 (예: wrap \")"), + ("number", "줄번호 추가 (1. 2. 3. ...)"), + ("sort", "줄 오름차순 정렬"), + ("sortd", "줄 내림차순 정렬"), + ("reverse", "줄 순서 뒤집기"), + ("unique", "중복 줄 제거"), + ("trim", "각 줄 앞뒤 공백 제거"), + ("replace [A] [B]", "A를 B로 전체 치환"), + ("csv", "줄들을 쉼표로 합쳐 한 줄로"), + ("split [구분자]", "한 줄을 구분자로 분리하여 여러 줄로"), + ("indent [N]", "각 줄 앞에 공백 N개 추가"), + ("unindent", "각 줄의 선행 공백/탭 제거") + }; + + public string? Prefix => "batch"; + + public PluginMetadata Metadata => new PluginMetadata("BatchText", "텍스트 일괄 처리 — batch", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string text = query.Trim(); + if (string.IsNullOrWhiteSpace(text)) + { + List list = Commands.Select(((string Cmd, string Desc) c) => new LauncherItem("batch " + c.Cmd, c.Desc, null, null, null, "\ue8d2")).ToList(); + list.Insert(0, new LauncherItem("텍스트 일괄 처리", "클립보드 텍스트의 각 줄에 변환 적용 · 명령 입력", null, null, null, "\ue946")); + return Task.FromResult((IEnumerable)list); + } + string text2 = null; + try + { + Application current = Application.Current; + if (current != null && ((DispatcherObject)current).Dispatcher.Invoke((Func)(() => Clipboard.ContainsText()))) + { + text2 = ((DispatcherObject)Application.Current).Dispatcher.Invoke((Func)(() => Clipboard.GetText())); + } + } + catch + { + } + if (string.IsNullOrEmpty(text2)) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("클립보드에 텍스트가 없습니다", "텍스트를 복사한 후 시도하세요", null, null, null, "\ue7ba"))); + } + string[] array = (from l in text2.Split('\n') + select l.TrimEnd('\r')).ToArray(); + string[] array2 = text.Split(' ', 2, StringSplitOptions.TrimEntries); + string text3 = array2[0].ToLowerInvariant(); + string arg = ((array2.Length > 1) ? array2[1] : ""); + string text4 = null; + string text5 = null; + try + { + switch (text3) + { + case "prefix": + text4 = string.Join("\n", array.Select((string l) => arg + l)); + text5 = "각 줄 앞에 '" + arg + "' 추가"; + break; + case "suffix": + text4 = string.Join("\n", array.Select((string l) => l + arg)); + text5 = "각 줄 뒤에 '" + arg + "' 추가"; + break; + case "wrap": + { + string w = (string.IsNullOrEmpty(arg) ? "\"" : arg); + text4 = string.Join("\n", array.Select((string l) => w + l + w)); + text5 = "각 줄을 '" + w + "'로 감싸기"; + break; + } + case "number": + text4 = string.Join("\n", array.Select((string l, int i) => $"{i + 1}. {l}")); + text5 = "줄번호 추가"; + break; + case "sort": + text4 = string.Join("\n", array.Order()); + text5 = "오름차순 정렬"; + break; + case "sortd": + text4 = string.Join("\n", array.OrderDescending()); + text5 = "내림차순 정렬"; + break; + case "reverse": + text4 = string.Join("\n", array.Reverse()); + text5 = "줄 순서 뒤집기"; + break; + case "unique": + { + string[] array4 = array.Distinct().ToArray(); + text4 = string.Join("\n", array4); + text5 = $"중복 제거: {array.Length}줄 → {array4.Length}줄"; + break; + } + case "trim": + text4 = string.Join("\n", array.Select((string l) => l.Trim())); + text5 = "각 줄 공백 제거"; + break; + case "replace": + { + string[] array3 = arg.Split(' ', 2, StringSplitOptions.TrimEntries); + if (array3.Length == 2) + { + text4 = text2.Replace(array3[0], array3[1]); + text5 = $"'{array3[0]}' → '{array3[1]}' 치환"; + } + break; + } + case "csv": + text4 = string.Join(",", array.Select((string l) => l.Trim())); + text5 = $"{array.Length}줄 → CSV 한 줄"; + break; + case "split": + { + string text6 = (string.IsNullOrEmpty(arg) ? "," : arg); + text4 = string.Join("\n", text2.Split(text6)); + text5 = "'" + text6 + "' 기준 분리"; + break; + } + case "indent": + { + int result; + int num = (int.TryParse(arg, out result) ? result : 4); + string pad = new string(' ', num); + text4 = string.Join("\n", array.Select((string l) => pad + l)); + text5 = $"{num}칸 들여쓰기"; + break; + } + case "unindent": + text4 = string.Join("\n", array.Select((string l) => l.TrimStart(' ', '\t'))); + text5 = "선행 공백 제거"; + break; + } + } + catch (Exception ex) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("처리 오류: " + ex.Message, "입력 데이터를 확인하세요", null, null, null, "\uea39"))); + } + if (text4 == null) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("알 수 없는 명령: " + text3, "batch 만 입력하면 전체 명령 목록", null, null, null, "\ue7ba"))); + } + string text7 = ((text4.Length > 120) ? (text4.Substring(0, 117) + "…") : text4); + text7 = text7.Replace("\n", "↵ "); + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("[" + text5 + "] → Enter로 클립보드 복사", text7, null, text4, null, "\ue8d2"))); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + string text = data as string; + if (text != null) + { + try + { + Application current = Application.Current; + if (current != null) + { + ((DispatcherObject)current).Dispatcher.Invoke((Action)delegate + { + Clipboard.SetText(text); + }); + } + } + catch + { + } + NotificationService.Notify("일괄 처리 완료", "변환 결과가 클립보드에 복사되었습니다"); + } + return Task.CompletedTask; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/BookmarkHandler.cs b/.decompiledproj/AxCopilot/Handlers/BookmarkHandler.cs new file mode 100644 index 0000000..3d61f07 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/BookmarkHandler.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class BookmarkHandler : IActionHandler +{ + private record BookmarkEntry(string Name, string? Url); + + private List? _cache; + + private DateTime _cacheTime = DateTime.MinValue; + + private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(5.0); + + public string? Prefix => null; + + public PluginMetadata Metadata => new PluginMetadata("Bookmarks", "Chrome / Edge 북마크 검색", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(query)) + { + return Task.FromResult((IEnumerable)Array.Empty()); + } + RefreshCacheIfNeeded(); + if (_cache == null || _cache.Count == 0) + { + return Task.FromResult((IEnumerable)Array.Empty()); + } + string q = query.Trim().ToLowerInvariant(); + List result = (from b in _cache.Where((BookmarkEntry b) => b.Name.ToLowerInvariant().Contains(q) || (b.Url?.ToLowerInvariant().Contains(q) ?? false)).Take(8) + select new LauncherItem(b.Name, b.Url ?? "", null, b.Url, null, "\ue774")).ToList(); + return Task.FromResult((IEnumerable)result); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is string text && !string.IsNullOrWhiteSpace(text)) + { + try + { + Process.Start(new ProcessStartInfo(text) + { + UseShellExecute = true + }); + } + catch (Exception ex) + { + LogService.Warn("북마크 열기 실패: " + ex.Message); + } + } + return Task.CompletedTask; + } + + private void RefreshCacheIfNeeded() + { + if (_cache != null && DateTime.Now - _cacheTime < CacheTtl) + { + return; + } + List list = new List(); + foreach (string bookmarkFile in GetBookmarkFiles()) + { + try + { + string json = File.ReadAllText(bookmarkFile); + ParseChromeBookmarks(json, list); + } + catch (Exception ex) + { + LogService.Warn("북마크 파일 읽기 실패: " + bookmarkFile + " — " + ex.Message); + } + } + _cache = list; + _cacheTime = DateTime.Now; + LogService.Info($"북마크 로드 완료: {list.Count}개"); + } + + private static IEnumerable GetBookmarkFiles() + { + string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + string[] chromePaths = new string[4] + { + Path.Combine(localAppData, "Google", "Chrome", "User Data"), + Path.Combine(localAppData, "Google", "Chrome Beta", "User Data"), + Path.Combine(localAppData, "Google", "Chrome Dev", "User Data"), + Path.Combine(localAppData, "Google", "Chrome SxS", "User Data") + }; + string[] edgePaths = new string[4] + { + Path.Combine(localAppData, "Microsoft", "Edge", "User Data"), + Path.Combine(localAppData, "Microsoft", "Edge Beta", "User Data"), + Path.Combine(localAppData, "Microsoft", "Edge Dev", "User Data"), + Path.Combine(localAppData, "Microsoft", "Edge Canary", "User Data") + }; + foreach (string profileRoot in chromePaths.Concat(edgePaths)) + { + if (!Directory.Exists(profileRoot)) + { + continue; + } + string defaultBookmark = Path.Combine(profileRoot, "Default", "Bookmarks"); + if (File.Exists(defaultBookmark)) + { + yield return defaultBookmark; + } + string[] directories = Directory.GetDirectories(profileRoot, "Profile *"); + foreach (string dir in directories) + { + string f = Path.Combine(dir, "Bookmarks"); + if (File.Exists(f)) + { + yield return f; + } + } + } + } + + private static void ParseChromeBookmarks(string json, List result) + { + JsonNode jsonNode = JsonNode.Parse(json)?["roots"]; + if (jsonNode == null) + { + return; + } + string[] array = new string[3] { "bookmark_bar", "other", "synced" }; + foreach (string propertyName in array) + { + JsonNode jsonNode2 = jsonNode[propertyName]; + if (jsonNode2 != null) + { + WalkNode(jsonNode2, result); + } + } + } + + private static void WalkNode(JsonNode node, List result) + { + string text = node["type"]?.GetValue(); + if (text == "url") + { + string text2 = node["name"]?.GetValue() ?? ""; + string text3 = node["url"]?.GetValue() ?? ""; + if (!string.IsNullOrWhiteSpace(text2) && !string.IsNullOrWhiteSpace(text3)) + { + result.Add(new BookmarkEntry(text2, text3)); + } + } + else + { + if (!(text == "folder")) + { + return; + } + JsonArray jsonArray = node["children"]?.AsArray(); + if (jsonArray == null) + { + return; + } + foreach (JsonNode item in jsonArray) + { + if (item != null) + { + WalkNode(item, result); + } + } + } + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/CalculatorHandler.cs b/.decompiledproj/AxCopilot/Handlers/CalculatorHandler.cs new file mode 100644 index 0000000..6ae12c6 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/CalculatorHandler.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using AxCopilot.SDK; + +namespace AxCopilot.Handlers; + +public class CalculatorHandler : IActionHandler +{ + public string? Prefix => "="; + + public PluginMetadata Metadata => new PluginMetadata("Calculator", "수식 계산기 — = 뒤에 수식 입력", "1.0", "AX"); + + public async Task> GetItemsAsync(string query, CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(query)) + { + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("수식을 입력하세요", "예: 1+2*3 · sqrt(16) · 100km in miles · 100 USD to KRW", null, null, null, "\ue8ef")); + } + string trimmed = query.Trim(); + if (CurrencyConverter.IsCurrencyQuery(trimmed)) + { + return await CurrencyConverter.ConvertAsync(trimmed, ct); + } + if (UnitConverter.TryConvert(trimmed, out string convertResult)) + { + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem(convertResult, trimmed + " · Enter로 클립보드에 복사", null, convertResult, null, "\ue8ef")); + } + try + { + double value = MathEvaluator.Evaluate(trimmed); + string result = FormatResult(value); + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem(result, trimmed + " = " + result + " · Enter로 클립보드에 복사", null, result, null, "\ue8ef")); + } + catch (Exception ex) + { + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("계산할 수 없습니다", ex.Message, null, null, null, "\uea39")); + } + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is string text) + { + try + { + Clipboard.SetText(text); + } + catch + { + } + } + return Task.CompletedTask; + } + + private static string FormatResult(double value) + { + if (double.IsNaN(value)) + { + return "NaN"; + } + if (double.IsPositiveInfinity(value)) + { + return "∞"; + } + if (double.IsNegativeInfinity(value)) + { + return "-∞"; + } + if (value == Math.Floor(value) && Math.Abs(value) < 1000000000000000.0) + { + return ((long)value).ToString(); + } + return value.ToString("G10", CultureInfo.InvariantCulture); + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/ChatHandler.cs b/.decompiledproj/AxCopilot/Handlers/ChatHandler.cs new file mode 100644 index 0000000..89d7062 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/ChatHandler.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using AxCopilot.Models; +using AxCopilot.SDK; +using AxCopilot.Services; +using AxCopilot.Views; + +namespace AxCopilot.Handlers; + +public class ChatHandler : IActionHandler +{ + private const bool DEPLOY_STUB = false; + + private readonly SettingsService _settings; + + private readonly object _windowLock = new object(); + + private ChatWindow? _chatWindow; + + public string? Prefix => "!"; + + public PluginMetadata Metadata => new PluginMetadata("ax.agent", "AX Agent", "1.0", "AX Agent — AI 어시스턴트"); + + public ChatHandler(SettingsService settings) + { + _settings = settings; + } + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + bool flag = false; + AppSettings appSettings = (Application.Current as App)?.SettingsService?.Settings; + if (appSettings != null && !appSettings.AiEnabled) + { + return Task.FromResult((IEnumerable)Array.Empty()); + } + List list = new List(); + string text = query.Trim(); + if (string.IsNullOrEmpty(text)) + { + list.Add(new LauncherItem("AX Agent 대화하기", "AI 비서와 대화를 시작합니다", null, "open_chat", null, "\ue8bd")); + try + { + ChatStorageService chatStorageService = new ChatStorageService(); + List source = chatStorageService.LoadAllMeta(); + foreach (ChatConversation item in source.Take(5)) + { + string value = FormatTimeAgo(item.UpdatedAt); + string symbol = ChatCategory.GetSymbol(item.Category); + list.Add(new LauncherItem(item.Title, $"{value} · 메시지 {item.Messages.Count}개", null, "resume:" + item.Id, null, symbol)); + } + if (source.Any()) + { + list.Add(new LauncherItem("새 대화 시작", "이전 대화와 별개의 새 대화를 시작합니다", null, "new_chat", null, "\ue710")); + } + } + catch + { + } + } + else + { + list.Add(new LauncherItem("AI에게 물어보기: " + ((text.Length > 40) ? (text.Substring(0, 40) + "…") : text), "Enter를 누르면 AX Agent이 열리고 질문이 전송됩니다", null, "ask:" + text, null, "\ue8bd")); + list.Add(new LauncherItem("AX Agent 대화하기", "질문 없이 Agent 창만 엽니다", null, "open_chat", null, "\ue8bd")); + } + return Task.FromResult((IEnumerable)list); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + bool flag = false; + AppSettings appSettings = (Application.Current as App)?.SettingsService?.Settings; + if (appSettings != null && !appSettings.AiEnabled) + { + return Task.CompletedTask; + } + string data = (item.Data as string) ?? "open_chat"; + ((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate + { + EnsureChatWindow(); + if (data.StartsWith("ask:")) + { + string text = data; + string message = text.Substring(4, text.Length - 4); + _chatWindow.Show(); + _chatWindow.Activate(); + _chatWindow.SendInitialMessage(message); + } + else if (data.StartsWith("resume:")) + { + string text = data; + string conversationId = text.Substring(7, text.Length - 7); + _chatWindow.Show(); + _chatWindow.Activate(); + _chatWindow.ResumeConversation(conversationId); + } + else if (data == "new_chat") + { + _chatWindow.Show(); + _chatWindow.Activate(); + _chatWindow.StartNewAndFocus(); + } + else + { + _chatWindow.Show(); + _chatWindow.Activate(); + } + }); + return Task.CompletedTask; + } + + private void EnsureChatWindow() + { + lock (_windowLock) + { + if (_chatWindow != null && _chatWindow.IsLoaded) + { + return; + } + _chatWindow = new ChatWindow(_settings); + _chatWindow.Closed += delegate + { + lock (_windowLock) + { + _chatWindow = null; + } + }; + } + } + + private static string FormatTimeAgo(DateTime dt) + { + TimeSpan timeSpan = DateTime.Now - dt; + if (timeSpan.TotalMinutes < 1.0) + { + return "방금 전"; + } + if (timeSpan.TotalHours < 1.0) + { + return $"{(int)timeSpan.TotalMinutes}분 전"; + } + if (timeSpan.TotalDays < 1.0) + { + return $"{(int)timeSpan.TotalHours}시간 전"; + } + if (timeSpan.TotalDays < 7.0) + { + return $"{(int)timeSpan.TotalDays}일 전"; + } + return dt.ToString("yyyy-MM-dd"); + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/ClipboardHandler.cs b/.decompiledproj/AxCopilot/Handlers/ClipboardHandler.cs new file mode 100644 index 0000000..b63232a --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/ClipboardHandler.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Forms; +using System.Windows.Threading; +using AxCopilot.Models; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class ClipboardHandler : IActionHandler +{ + private readonly SettingsService _settings; + + public string? Prefix => "$"; + + public PluginMetadata Metadata => new PluginMetadata("clipboard", "클립보드 변환", "1.0", "AX"); + + public ClipboardHandler(SettingsService settings) + { + _settings = settings; + } + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + List list = new List(); + IEnumerable enumerable = from t in GetBuiltinTransformers() + where string.IsNullOrEmpty(query) || t.Key.Contains(query, StringComparison.OrdinalIgnoreCase) + select t; + foreach (ClipboardTransformer item in enumerable) + { + list.Add(new LauncherItem(item.Key, item.Description ?? "", null, item, null, "\ue77f")); + } + IEnumerable collection = from t in _settings.Settings.ClipboardTransformers + where string.IsNullOrEmpty(query) || t.Key.Contains(query, StringComparison.OrdinalIgnoreCase) + select new LauncherItem(t.Key, t.Description ?? t.Type, null, t, null, "\ue77f"); + list.AddRange(collection); + return Task.FromResult((IEnumerable)list); + } + + public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + if (!(data is ClipboardTransformer transformer)) + { + return; + } + string input = null; + ((DispatcherObject)System.Windows.Application.Current).Dispatcher.Invoke((Action)delegate + { + input = (System.Windows.Clipboard.ContainsText() ? System.Windows.Clipboard.GetText() : null); + }); + if (input == null) + { + return; + } + string result = await TransformAsync(transformer, input, ct); + if (result != null) + { + ((DispatcherObject)System.Windows.Application.Current).Dispatcher.Invoke((Action)delegate + { + System.Windows.Clipboard.SetText(result); + }); + nint prevHwnd = WindowTracker.PreviousWindow; + if (prevHwnd != IntPtr.Zero) + { + SetForegroundWindow(prevHwnd); + } + await Task.Delay(120, ct); + SendKeys.SendWait("^v"); + LogService.Info("클립보드 변환: '" + transformer.Key + "' 적용"); + } + } + + private static async Task TransformAsync(ClipboardTransformer t, string input, CancellationToken ct) + { + try + { + string type = t.Type; + if (1 == 0) + { + } + string text = type; + string result; + if (!(text == "regex")) + { + if (!(text == "script") || t.Command == null) + { + goto IL_016f; + } + result = await RunScriptAsync(t.Command, input, t.Timeout, ct); + } + else + { + if (t.Pattern == null || t.Replace == null) + { + goto IL_016f; + } + result = Regex.Replace(input, t.Pattern, t.Replace, RegexOptions.None, TimeSpan.FromMilliseconds((t.Timeout > 0) ? t.Timeout : 5000)); + } + goto IL_0188; + IL_016f: + result = ExecuteBuiltin(t.Key, input); + goto IL_0188; + IL_0188: + if (1 == 0) + { + } + return result; + } + catch (Exception ex) + { + Exception ex2 = ex; + LogService.Error("변환 실패 (" + t.Key + "): " + ex2.Message); + return null; + } + } + + private static async Task RunScriptAsync(string command, string input, int timeoutMs, CancellationToken ct) + { + using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + cts.CancelAfter(timeoutMs); + string[] parts = command.Split(' ', 2); + ProcessStartInfo psi = new ProcessStartInfo(parts[0]) + { + Arguments = ((parts.Length > 1) ? parts[1] : ""), + RedirectStandardInput = true, + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + StandardInputEncoding = Encoding.UTF8, + StandardOutputEncoding = Encoding.UTF8 + }; + using Process proc = Process.Start(psi); + await proc.StandardInput.WriteAsync(input); + proc.StandardInput.Close(); + return await proc.StandardOutput.ReadToEndAsync(cts.Token); + } + + internal static string? ExecuteBuiltin(string key, string input) + { + if (1 == 0) + { + } + string result = key switch + { + "$json" => FormatJson(input), + "$upper" => input.ToUpperInvariant(), + "$lower" => input.ToLowerInvariant(), + "$ts" => TryParseTimestamp(input), + "$epoch" => TryParseDate(input), + "$urle" => Uri.EscapeDataString(input), + "$urld" => Uri.UnescapeDataString(input), + "$b64e" => Convert.ToBase64String(Encoding.UTF8.GetBytes(input)), + "$b64d" => Encoding.UTF8.GetString(Convert.FromBase64String(input)), + "$md" => StripMarkdown(input), + "$trim" => input.Trim(), + "$lines" => string.Join(Environment.NewLine, from l in input.Split('\n') + select l.Trim() into l + where l.Length > 0 + select l), + _ => null, + }; + if (1 == 0) + { + } + return result; + } + + private static string FormatJson(string input) + { + try + { + JsonDocument value = JsonDocument.Parse(input); + return JsonSerializer.Serialize(value, new JsonSerializerOptions + { + WriteIndented = true + }); + } + catch + { + return input; + } + } + + private static string? TryParseTimestamp(string input) + { + if (long.TryParse(input.Trim(), out var result)) + { + return DateTimeOffset.FromUnixTimeSeconds(result).LocalDateTime.ToString("yyyy-MM-dd HH:mm:ss"); + } + return null; + } + + private static string? TryParseDate(string input) + { + if (DateTime.TryParse(input.Trim(), out var result)) + { + return new DateTimeOffset(result).ToUnixTimeSeconds().ToString(); + } + return null; + } + + private static string StripMarkdown(string input) + { + return Regex.Replace(input, "(\\*\\*|__)(.*?)\\1|(\\*|_)(.*?)\\3|`(.+?)`|#{1,6}\\s*", "$2$4$5"); + } + + [DllImport("user32.dll")] + private static extern bool SetForegroundWindow(nint hWnd); + + private static IEnumerable GetBuiltinTransformers() + { + return new _003C_003Ez__ReadOnlyArray(new ClipboardTransformer[12] + { + new ClipboardTransformer + { + Key = "$json", + Type = "builtin", + Description = "JSON 포맷팅 (들여쓰기 적용)" + }, + new ClipboardTransformer + { + Key = "$upper", + Type = "builtin", + Description = "대문자 변환" + }, + new ClipboardTransformer + { + Key = "$lower", + Type = "builtin", + Description = "소문자 변환" + }, + new ClipboardTransformer + { + Key = "$ts", + Type = "builtin", + Description = "유닉스 타임스탬프 → 날짜 문자열" + }, + new ClipboardTransformer + { + Key = "$epoch", + Type = "builtin", + Description = "날짜 문자열 → 유닉스 타임스탬프" + }, + new ClipboardTransformer + { + Key = "$urle", + Type = "builtin", + Description = "URL 인코딩" + }, + new ClipboardTransformer + { + Key = "$urld", + Type = "builtin", + Description = "URL 디코딩" + }, + new ClipboardTransformer + { + Key = "$b64e", + Type = "builtin", + Description = "Base64 인코딩" + }, + new ClipboardTransformer + { + Key = "$b64d", + Type = "builtin", + Description = "Base64 디코딩" + }, + new ClipboardTransformer + { + Key = "$md", + Type = "builtin", + Description = "마크다운 문법 제거" + }, + new ClipboardTransformer + { + Key = "$trim", + Type = "builtin", + Description = "앞뒤 공백 제거" + }, + new ClipboardTransformer + { + Key = "$lines", + Type = "builtin", + Description = "빈 줄 제거 및 각 줄 공백 정리" + } + }); + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/ClipboardHistoryHandler.cs b/.decompiledproj/AxCopilot/Handlers/ClipboardHistoryHandler.cs new file mode 100644 index 0000000..f6940e3 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/ClipboardHistoryHandler.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media.Imaging; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class ClipboardHistoryHandler : IActionHandler +{ + [StructLayout(LayoutKind.Explicit, Size = 40)] + private struct INPUT + { + [FieldOffset(0)] + public uint Type; + + [FieldOffset(8)] + public KEYBDINPUT ki; + } + + private struct KEYBDINPUT + { + public ushort wVk; + + public ushort wScan; + + public uint dwFlags; + + public uint time; + + public nint dwExtraInfo; + } + + private readonly ClipboardHistoryService _historyService; + + private static readonly Dictionary CategoryFilters = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "url", "URL" }, + { "코드", "코드" }, + { "code", "코드" }, + { "경로", "경로" }, + { "path", "경로" }, + { "핀", "핀" }, + { "pin", "핀" } + }; + + public string? Prefix => "#"; + + public PluginMetadata Metadata => new PluginMetadata("ClipboardHistory", "클립보드 히스토리 — # 뒤에 검색어 (또는 빈 입력으로 전체 보기)", "1.0", "AX"); + + public ClipboardHistoryHandler(ClipboardHistoryService historyService) + { + _historyService = historyService; + } + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + IReadOnlyList history = _historyService.History; + if (history.Count == 0) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("클립보드 히스토리가 없습니다", "텍스트를 복사하면 이 곳에 기록됩니다", null, null, null, "\ue81c"))); + } + string q = query.Trim().ToLowerInvariant(); + string catFilter = null; + foreach (var (text3, text4) in CategoryFilters) + { + if (q == text3 || q.StartsWith(text3 + " ")) + { + catFilter = text4; + object obj; + if (q.Length <= text3.Length) + { + obj = ""; + } + else + { + string text5 = q; + int num = text3.Length + 1; + obj = text5.Substring(num, text5.Length - num).Trim(); + } + q = (string)obj; + break; + } + } + IEnumerable source = history.AsEnumerable(); + if (catFilter == "핀") + { + source = source.Where((ClipboardEntry e) => e.IsPinned); + } + else if (catFilter != null) + { + source = source.Where((ClipboardEntry e) => e.Category == catFilter); + } + if (!string.IsNullOrEmpty(q)) + { + source = source.Where((ClipboardEntry e) => e.Preview.ToLowerInvariant().Contains(q)); + } + IOrderedEnumerable source2 = from e in source + orderby e.IsPinned descending, e.CopiedAt descending + select e; + List list = source2.Select(delegate(ClipboardEntry e) + { + string text6 = (e.IsPinned ? "\ud83d\udccc " : ""); + string value = ((e.Category != "일반") ? ("[" + e.Category + "] ") : ""); + return new LauncherItem(text6 + e.Preview, $"{value}{e.RelativeTime} · {e.CopiedAt:MM/dd HH:mm}", null, e, null, e.IsPinned ? "\ue728" : (e.IsText ? "\ue81c" : "\ueb9f")); + }).ToList(); + if (list.Count == 0) + { + list.Add(new LauncherItem("'" + query + "'에 해당하는 항목 없음", "#pin #url #코드 #경로 로 필터링 가능", null, null, null, "\ue81c")); + } + return Task.FromResult((IEnumerable)list); + } + + public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + if (!(data is ClipboardEntry entry)) + { + return; + } + try + { + _historyService.SuppressNextCapture(); + _historyService.PromoteEntry(entry); + if (!entry.IsText && entry.Image != null) + { + BitmapSource originalImg = ClipboardHistoryService.LoadOriginalImage(entry.OriginalImagePath); + Clipboard.SetImage(originalImg ?? entry.Image); + } + else if (!string.IsNullOrEmpty(entry.Text)) + { + Clipboard.SetText(entry.Text); + nint prevWindow = WindowTracker.PreviousWindow; + if (prevWindow != IntPtr.Zero) + { + await Task.Delay(300, ct); + uint lpdwProcessId; + uint targetThread = GetWindowThreadProcessId(prevWindow, out lpdwProcessId); + uint currentThread = GetCurrentThreadId(); + AttachThreadInput(currentThread, targetThread, fAttach: true); + SetForegroundWindow(prevWindow); + AttachThreadInput(currentThread, targetThread, fAttach: false); + await Task.Delay(100, ct); + SendCtrlV(); + } + } + } + catch (OperationCanceledException) + { + } + catch (Exception ex2) + { + LogService.Warn("클립보드 히스토리 붙여넣기 실패: " + ex2.Message); + } + } + + private static void SendCtrlV() + { + INPUT[] array = new INPUT[4]; + array[0].Type = 1u; + array[0].ki.wVk = 17; + array[1].Type = 1u; + array[1].ki.wVk = 86; + array[2].Type = 1u; + array[2].ki.wVk = 86; + array[2].ki.dwFlags = 2u; + array[3].Type = 1u; + array[3].ki.wVk = 17; + array[3].ki.dwFlags = 2u; + SendInput((uint)array.Length, array, Marshal.SizeOf()); + } + + [DllImport("user32.dll")] + private static extern bool SetForegroundWindow(nint hWnd); + + [DllImport("user32.dll")] + private static extern uint GetWindowThreadProcessId(nint hWnd, out uint lpdwProcessId); + + [DllImport("user32.dll")] + private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, [MarshalAs(UnmanagedType.Bool)] bool fAttach); + + [DllImport("kernel32.dll")] + private static extern uint GetCurrentThreadId(); + + [DllImport("user32.dll")] + private static extern uint SendInput(uint nInputs, [MarshalAs(UnmanagedType.LPArray)] INPUT[] pInputs, int cbSize); +} diff --git a/.decompiledproj/AxCopilot/Handlers/ClipboardPipeHandler.cs b/.decompiledproj/AxCopilot/Handlers/ClipboardPipeHandler.cs new file mode 100644 index 0000000..efedf94 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/ClipboardPipeHandler.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class ClipboardPipeHandler : IActionHandler +{ + private static readonly Dictionary Fn)> Filters = new Dictionary)>(StringComparer.OrdinalIgnoreCase) + { + ["upper"] = ("대문자 변환", (string s) => s.ToUpperInvariant()), + ["lower"] = ("소문자 변환", (string s) => s.ToLowerInvariant()), + ["trim"] = ("앞뒤 공백 제거", (string s) => s.Trim()), + ["trimall"] = ("모든 공백 제거", (string s) => Regex.Replace(s, "\\s+", "", RegexOptions.None, TimeSpan.FromSeconds(1.0))), + ["sort"] = ("줄 정렬 (오름차순)", (string s) => string.Join("\n", s.Split('\n').Order())), + ["sortd"] = ("줄 정렬 (내림차순)", (string s) => string.Join("\n", s.Split('\n').OrderDescending())), + ["unique"] = ("중복 줄 제거", (string s) => string.Join("\n", s.Split('\n').Distinct())), + ["reverse"] = ("줄 순서 뒤집기", (string s) => string.Join("\n", s.Split('\n').Reverse())), + ["number"] = ("줄번호 추가", (string s) => string.Join("\n", s.Split('\n').Select((string l, int i) => $"{i + 1}. {l}"))), + ["quote"] = ("각 줄 따옴표 감싸기", (string s) => string.Join("\n", from l in s.Split('\n') + select "\"" + l + "\"")), + ["b64e"] = ("Base64 인코딩", (string s) => Convert.ToBase64String(Encoding.UTF8.GetBytes(s))), + ["b64d"] = ("Base64 디코딩", (string s) => Encoding.UTF8.GetString(Convert.FromBase64String(s.Trim()))), + ["urle"] = ("URL 인코딩", (string s) => Uri.EscapeDataString(s)), + ["urld"] = ("URL 디코딩", (string s) => Uri.UnescapeDataString(s)), + ["md"] = ("마크다운 제거", (string s) => Regex.Replace(s, "[#*_`~\\[\\]()]", "", RegexOptions.None, TimeSpan.FromSeconds(1.0))), + ["lines"] = ("빈 줄 제거", (string s) => string.Join("\n", from l in s.Split('\n') + where !string.IsNullOrWhiteSpace(l) + select l)), + ["count"] = ("글자/단어/줄 수", (string s) => $"글자: {s.Length} 단어: {s.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries).Length} 줄: {s.Split('\n').Length}"), + ["csv"] = ("CSV → 탭 변환", (string s) => s.Replace(',', '\t')), + ["tab"] = ("탭 → CSV 변환", (string s) => s.Replace('\t', ',')) + }; + + public string? Prefix => "pipe"; + + public PluginMetadata Metadata => new PluginMetadata("ClipboardPipe", "클립보드 파이프라인 — pipe", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string text = query.Trim(); + if (string.IsNullOrWhiteSpace(text)) + { + List list = Filters.Select)>, LauncherItem>((KeyValuePair Fn)> kv) => new LauncherItem(kv.Key, kv.Value.Desc, null, null, null, "\ue77f")).ToList(); + list.Insert(0, new LauncherItem("클립보드 파이프라인", "필터를 > 로 연결: pipe upper > trim > b64e", null, null, null, "\ue946")); + return Task.FromResult((IEnumerable)list); + } + string[] array = (from s in text.Split('>') + select s.Trim() into s + where !string.IsNullOrEmpty(s) + select s).ToArray(); + string[] array2 = array.Where((string s) => !Filters.ContainsKey(s)).ToArray(); + if (array2.Length != 0) + { + return Task.FromResult(new LauncherItem[1] + { + new LauncherItem("알 수 없는 필터: " + string.Join(", ", array2), "사용 가능: " + string.Join(", ", Filters.Keys.Take(10)) + " ...", null, null, null, "\ue7ba") + }.AsEnumerable()); + } + string text2 = null; + try + { + Application current = Application.Current; + if (current != null && ((DispatcherObject)current).Dispatcher.Invoke((Func)(() => Clipboard.ContainsText()))) + { + text2 = ((DispatcherObject)Application.Current).Dispatcher.Invoke((Func)(() => Clipboard.GetText())); + } + } + catch + { + } + if (string.IsNullOrEmpty(text2)) + { + return Task.FromResult(new LauncherItem[1] + { + new LauncherItem("클립보드에 텍스트가 없습니다", "텍스트를 복사한 후 시도하세요", null, null, null, "\ue7ba") + }.AsEnumerable()); + } + string text3 = text2; + List list2 = new List(); + try + { + string[] array3 = array; + foreach (string key in array3) + { + text3 = Filters[key].Fn(text3); + list2.Add(Filters[key].Desc); + } + } + catch (Exception ex) + { + return Task.FromResult(new LauncherItem[1] + { + new LauncherItem("파이프라인 실행 오류: " + ex.Message, "입력 데이터를 확인하세요", null, null, null, "\uea39") + }.AsEnumerable()); + } + string text4 = ((text3.Length > 100) ? (text3.Substring(0, 97) + "…") : text3); + text4 = text4.Replace("\r\n", "↵ ").Replace("\n", "↵ "); + return Task.FromResult(new LauncherItem[1] + { + new LauncherItem("[" + string.Join(" → ", array) + "] 결과 적용", text4 + " · Enter로 클립보드 복사", null, text3, null, "\ue77f") + }.AsEnumerable()); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + string text = data as string; + if (text != null) + { + try + { + Application current = Application.Current; + if (current != null) + { + ((DispatcherObject)current).Dispatcher.Invoke((Action)delegate + { + Clipboard.SetText(text); + }); + } + } + catch + { + } + NotificationService.Notify("파이프라인 완료", "변환 결과가 클립보드에 복사되었습니다"); + } + return Task.CompletedTask; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/ColorHandler.cs b/.decompiledproj/AxCopilot/Handlers/ColorHandler.cs new file mode 100644 index 0000000..ed2a6d4 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/ColorHandler.cs @@ -0,0 +1,333 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using AxCopilot.SDK; + +namespace AxCopilot.Handlers; + +public class ColorHandler : IActionHandler +{ + private static readonly Dictionary _namedColors = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["red"] = "#FF0000", + ["빨강"] = "#FF0000", + ["빨간색"] = "#FF0000", + ["green"] = "#008000", + ["초록"] = "#008000", + ["초록색"] = "#008000", + ["blue"] = "#0000FF", + ["파랑"] = "#0000FF", + ["파란색"] = "#0000FF", + ["white"] = "#FFFFFF", + ["흰색"] = "#FFFFFF", + ["하양"] = "#FFFFFF", + ["black"] = "#000000", + ["검정"] = "#000000", + ["검은색"] = "#000000", + ["yellow"] = "#FFFF00", + ["노랑"] = "#FFFF00", + ["노란색"] = "#FFFF00", + ["orange"] = "#FFA500", + ["주황"] = "#FFA500", + ["주황색"] = "#FFA500", + ["purple"] = "#800080", + ["보라"] = "#800080", + ["보라색"] = "#800080", + ["pink"] = "#FFC0CB", + ["분홍"] = "#FFC0CB", + ["분홍색"] = "#FFC0CB", + ["hotpink"] = "#FF69B4", + ["핫핑크"] = "#FF69B4", + ["cyan"] = "#00FFFF", + ["시안"] = "#00FFFF", + ["magenta"] = "#FF00FF", + ["마젠타"] = "#FF00FF", + ["brown"] = "#A52A2A", + ["갈색"] = "#A52A2A", + ["gray"] = "#808080", + ["grey"] = "#808080", + ["회색"] = "#808080", + ["회"] = "#808080", + ["silver"] = "#C0C0C0", + ["은색"] = "#C0C0C0", + ["gold"] = "#FFD700", + ["금색"] = "#FFD700", + ["navy"] = "#000080", + ["남색"] = "#000080", + ["teal"] = "#008080", + ["틸"] = "#008080", + ["lime"] = "#00FF00", + ["라임"] = "#00FF00", + ["maroon"] = "#800000", + ["밤색"] = "#800000", + ["olive"] = "#808000", + ["올리브"] = "#808000", + ["coral"] = "#FF7F50", + ["코랄"] = "#FF7F50", + ["salmon"] = "#FA8072", + ["연어색"] = "#FA8072", + ["skyblue"] = "#87CEEB", + ["하늘색"] = "#87CEEB", + ["lightblue"] = "#ADD8E6", + ["연파랑"] = "#ADD8E6", + ["darkblue"] = "#00008B", + ["진파랑"] = "#00008B", + ["darkgreen"] = "#006400", + ["진초록"] = "#006400", + ["lightgreen"] = "#90EE90", + ["연초록"] = "#90EE90", + ["indigo"] = "#4B0082", + ["인디고"] = "#4B0082", + ["violet"] = "#EE82EE", + ["바이올렛"] = "#EE82EE", + ["beige"] = "#F5F5DC", + ["베이지"] = "#F5F5DC", + ["ivory"] = "#FFFFF0", + ["아이보리"] = "#FFFFF0", + ["khaki"] = "#F0E68C", + ["카키"] = "#F0E68C", + ["lavender"] = "#E6E6FA", + ["라벤더"] = "#E6E6FA", + ["turquoise"] = "#40E0D0", + ["터키옥"] = "#40E0D0", + ["chocolate"] = "#D2691E", + ["초콜릿"] = "#D2691E", + ["crimson"] = "#DC143C", + ["크림슨"] = "#DC143C", + ["transparent"] = "#00000000" + }; + + private static readonly Regex _hexRe = new Regex("^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$"); + + private static readonly Regex _rgbRe = new Regex("^(?:rgb\\s*\\()?\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*\\)?$", RegexOptions.IgnoreCase); + + private static readonly Regex _rgbaRe = new Regex("^rgba\\s*\\(\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*([\\d.]+)\\s*\\)$", RegexOptions.IgnoreCase); + + private static readonly Regex _hslRe = new Regex("^hsl\\s*\\(\\s*([\\d.]+)\\s*,\\s*([\\d.]+)%?\\s*,\\s*([\\d.]+)%?\\s*\\)$", RegexOptions.IgnoreCase); + + public string? Prefix => "color"; + + public PluginMetadata Metadata => new PluginMetadata("Color", "색상 변환기 — color #FF5500 · color 255,85,0 · color red", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(query)) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("색상 코드를 입력하세요", "예: #FF5500 · 255,85,0 · red · hsl(20,100%,50%)", null, null, null, "\ue771"))); + } + string text = query.Trim(); + List list = new List(); + if (_hexRe.IsMatch(text)) + { + string text2 = text.TrimStart('#'); + if (text2.Length == 3) + { + text2 = $"{text2[0]}{text2[0]}{text2[1]}{text2[1]}{text2[2]}{text2[2]}"; + } + byte a = byte.MaxValue; + int r; + int g; + int b; + if (text2.Length == 8) + { + a = Convert.ToByte(text2.Substring(0, 2), 16); + r = Convert.ToInt32(text2.Substring(2, 2), 16); + g = Convert.ToInt32(text2.Substring(4, 2), 16); + b = Convert.ToInt32(text2.Substring(6, 2), 16); + } + else + { + r = Convert.ToInt32(text2.Substring(0, 2), 16); + g = Convert.ToInt32(text2.Substring(2, 2), 16); + b = Convert.ToInt32(text2.Substring(4, 2), 16); + } + AddColorItems(list, r, g, b, a, "#" + text2.ToUpperInvariant()); + return Task.FromResult((IEnumerable)list); + } + Match match = _rgbRe.Match(text); + if (match.Success) + { + int num = int.Parse(match.Groups[1].Value); + int num2 = int.Parse(match.Groups[2].Value); + int num3 = int.Parse(match.Groups[3].Value); + if (num <= 255 && num2 <= 255 && num3 <= 255) + { + AddColorItems(list, num, num2, num3, byte.MaxValue, $"rgb({num},{num2},{num3})"); + return Task.FromResult((IEnumerable)list); + } + } + Match match2 = _rgbaRe.Match(text); + if (match2.Success) + { + int num4 = int.Parse(match2.Groups[1].Value); + int num5 = int.Parse(match2.Groups[2].Value); + int num6 = int.Parse(match2.Groups[3].Value); + double num7 = double.Parse(match2.Groups[4].Value, CultureInfo.InvariantCulture); + byte a2 = (byte)Math.Clamp((int)(num7 * 255.0), 0, 255); + if (num4 <= 255 && num5 <= 255 && num6 <= 255) + { + AddColorItems(list, num4, num5, num6, a2, $"rgba({num4},{num5},{num6},{num7:G3})"); + return Task.FromResult((IEnumerable)list); + } + } + Match match3 = _hslRe.Match(text); + if (match3.Success) + { + double num8 = double.Parse(match3.Groups[1].Value, CultureInfo.InvariantCulture); + double num9 = double.Parse(match3.Groups[2].Value, CultureInfo.InvariantCulture) / 100.0; + double num10 = double.Parse(match3.Groups[3].Value, CultureInfo.InvariantCulture) / 100.0; + var (r2, g2, b2) = HslToRgb(num8, num9, num10); + AddColorItems(list, r2, g2, b2, byte.MaxValue, $"hsl({num8:G},{num9 * 100.0:G}%,{num10 * 100.0:G}%)"); + return Task.FromResult((IEnumerable)list); + } + if (_namedColors.TryGetValue(text, out string value)) + { + string text3 = value.TrimStart('#'); + int r3 = Convert.ToInt32(text3.Substring(0, 2), 16); + int g3 = Convert.ToInt32(text3.Substring(2, 2), 16); + int b3 = Convert.ToInt32(text3.Substring(4, 2), 16); + AddColorItems(list, r3, g3, b3, byte.MaxValue, value.ToUpperInvariant()); + return Task.FromResult((IEnumerable)list); + } + string searchQ = text.ToLowerInvariant(); + IEnumerable> enumerable = _namedColors.Where>((KeyValuePair kv) => kv.Key.Contains(searchQ, StringComparison.OrdinalIgnoreCase)).Take(8); + foreach (KeyValuePair item in enumerable) + { + list.Add(new LauncherItem(item.Key + " → " + item.Value.ToUpperInvariant(), "Enter로 HEX 복사", null, item.Value.ToUpperInvariant(), null, "\ue771")); + } + if (!list.Any()) + { + list.Add(new LauncherItem("인식할 수 없는 색상", "#RRGGBB · RGB(r,g,b) · hsl(h,s%,l%) · red 등 색상 이름", null, null, null, "\uea39")); + } + return Task.FromResult((IEnumerable)list); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is string text) + { + try + { + Clipboard.SetText(text); + } + catch + { + } + } + return Task.CompletedTask; + } + + private static void AddColorItems(List items, int r, int g, int b, byte a, string source) + { + string text = ((a == byte.MaxValue) ? $"#{r:X2}{g:X2}{b:X2}" : $"#{a:X2}{r:X2}{g:X2}{b:X2}"); + string text2 = ((a == byte.MaxValue) ? $"rgb({r}, {g}, {b})" : $"rgba({r}, {g}, {b}, {(double)(int)a / 255.0:G3})"); + (double h, double s, double l) tuple = RgbToHsl(r, g, b); + double item = tuple.h; + double item2 = tuple.s; + double item3 = tuple.l; + string text3 = $"hsl({item:G3}, {item2 * 100.0:G3}%, {item3 * 100.0:G3}%)"; + (double h, double s, double v) tuple2 = RgbToHsv(r, g, b); + double item4 = tuple2.h; + double item5 = tuple2.s; + double item6 = tuple2.v; + string text4 = $"hsv({item4:G3}, {item5 * 100.0:G3}%, {item6 * 100.0:G3}%)"; + items.Add(new LauncherItem("HEX → " + text.ToUpperInvariant(), source + " · Enter로 복사", null, text.ToUpperInvariant(), null, "\ue771")); + items.Add(new LauncherItem("RGB → " + text2, source + " · Enter로 복사", null, text2, null, "\ue771")); + items.Add(new LauncherItem("HSL → " + text3, source + " · Enter로 복사", null, text3, null, "\ue771")); + items.Add(new LauncherItem("HSV → " + text4, source + " · Enter로 복사", null, text4, null, "\ue771")); + } + + private static (double h, double s, double l) RgbToHsl(int r, int g, int b) + { + double num = (double)r / 255.0; + double num2 = (double)g / 255.0; + double num3 = (double)b / 255.0; + double num4 = Math.Max(num, Math.Max(num2, num3)); + double num5 = Math.Min(num, Math.Min(num2, num3)); + double num6 = (num4 + num5) / 2.0; + double num7 = 0.0; + double value = 0.0; + if (num4 != num5) + { + double num8 = num4 - num5; + value = ((num6 > 0.5) ? (num8 / (2.0 - num4 - num5)) : (num8 / (num4 + num5))); + num7 = ((num4 == num) ? ((num2 - num3) / num8 + (double)((num2 < num3) ? 6 : 0)) : ((num4 == num2) ? ((num3 - num) / num8 + 2.0) : ((num - num2) / num8 + 4.0))); + num7 /= 6.0; + } + return (h: Math.Round(num7 * 360.0, 1), s: Math.Round(value, 4), l: Math.Round(num6, 4)); + } + + private static (double h, double s, double v) RgbToHsv(int r, int g, int b) + { + double num = (double)r / 255.0; + double num2 = (double)g / 255.0; + double num3 = (double)b / 255.0; + double num4 = Math.Max(num, Math.Max(num2, num3)); + double num5 = Math.Min(num, Math.Min(num2, num3)); + double value = num4; + double value2 = 0.0; + double num6 = 0.0; + double num7 = num4 - num5; + if (num4 != 0.0) + { + value2 = num7 / num4; + } + if (num7 != 0.0) + { + num6 = ((num4 == num) ? ((num2 - num3) / num7 + (double)((num2 < num3) ? 6 : 0)) : ((num4 == num2) ? ((num3 - num) / num7 + 2.0) : ((num - num2) / num7 + 4.0))); + num6 /= 6.0; + } + return (h: Math.Round(num6 * 360.0, 1), s: Math.Round(value2, 4), v: Math.Round(value, 4)); + } + + private static (int r, int g, int b) HslToRgb(double h, double s, double l) + { + double num; + double num2; + double num3; + if (s == 0.0) + { + num = (num2 = (num3 = l)); + } + else + { + double num4 = ((l < 0.5) ? (l * (1.0 + s)) : (l + s - l * s)); + double p = 2.0 * l - num4; + h /= 360.0; + num = Hue2Rgb(p, num4, h + 1.0 / 3.0); + num2 = Hue2Rgb(p, num4, h); + num3 = Hue2Rgb(p, num4, h - 1.0 / 3.0); + } + return (r: (int)Math.Round(num * 255.0), g: (int)Math.Round(num2 * 255.0), b: (int)Math.Round(num3 * 255.0)); + } + + private static double Hue2Rgb(double p, double q, double t) + { + if (t < 0.0) + { + t += 1.0; + } + if (t > 1.0) + { + t -= 1.0; + } + if (t < 1.0 / 6.0) + { + return p + (q - p) * 6.0 * t; + } + if (t < 0.5) + { + return q; + } + if (t < 2.0 / 3.0) + { + return p + (q - p) * (2.0 / 3.0 - t) * 6.0; + } + return p; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/ColorPickHandler.cs b/.decompiledproj/AxCopilot/Handlers/ColorPickHandler.cs new file mode 100644 index 0000000..e3fbdd3 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/ColorPickHandler.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using AxCopilot.SDK; +using AxCopilot.Views; + +namespace AxCopilot.Handlers; + +public class ColorPickHandler : IActionHandler +{ + public string? Prefix => "pick"; + + public PluginMetadata Metadata => new PluginMetadata("ColorPick", "스포이드 색상 추출 — pick", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("스포이드로 화면 색상 추출", "Enter → 스포이드 모드 진입 · 화면 아무 곳을 클릭하면 색상 코드 추출 · Esc 취소", null, "__PICK__", null, "\ue771"))); + } + + public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + if (!(data is string s) || s != "__PICK__") + { + return; + } + await Task.Delay(200, ct); + Application current = Application.Current; + if (current == null) + { + return; + } + ((DispatcherObject)current).Dispatcher.Invoke((Action)delegate + { + EyeDropperWindow eyeDropperWindow = new EyeDropperWindow(); + eyeDropperWindow.ShowDialog(); + if (eyeDropperWindow.PickedColor.HasValue) + { + ColorPickResultWindow colorPickResultWindow = new ColorPickResultWindow(eyeDropperWindow.PickedColor.Value, eyeDropperWindow.PickX, eyeDropperWindow.PickY); + colorPickResultWindow.Show(); + } + }); + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/CredentialManager.cs b/.decompiledproj/AxCopilot/Handlers/CredentialManager.cs new file mode 100644 index 0000000..f51dd37 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/CredentialManager.cs @@ -0,0 +1,135 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public static class CredentialManager +{ + private struct FILETIME + { + public uint dwLowDateTime; + + public uint dwHighDateTime; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct CREDENTIAL + { + public uint Flags; + + public uint Type; + + public nint TargetName; + + public nint Comment; + + public FILETIME LastWritten; + + public uint CredentialBlobSize; + + public nint CredentialBlob; + + public uint Persist; + + public uint AttributeCount; + + public nint Attributes; + + public nint TargetAlias; + + public nint UserName; + } + + private const uint CRED_TYPE_GENERIC = 1u; + + private const uint CRED_PERSIST_LOCAL_MACHINE = 2u; + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern bool CredRead(string target, uint type, uint flags, out nint credential); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern bool CredWrite([In] ref CREDENTIAL userCredential, uint flags); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern void CredFree(nint cred); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern bool CredDelete(string target, uint type, uint flags); + + public static string? GetToken(string key) + { + if (string.IsNullOrEmpty(key)) + { + return null; + } + try + { + if (CredRead(key, 1u, 0u, out var credential)) + { + try + { + CREDENTIAL cREDENTIAL = Marshal.PtrToStructure(credential); + if (cREDENTIAL.CredentialBlobSize != 0 && cREDENTIAL.CredentialBlob != IntPtr.Zero) + { + return Marshal.PtrToStringUni(cREDENTIAL.CredentialBlob, (int)cREDENTIAL.CredentialBlobSize / 2); + } + } + finally + { + CredFree(credential); + } + } + } + catch (Exception ex) + { + LogService.Warn("Windows Credential Manager 읽기 실패 (" + key + "): " + ex.Message); + } + return Environment.GetEnvironmentVariable(key.ToUpperInvariant()); + } + + public static void SetToken(string key, string token) + { + if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(token)) + { + return; + } + byte[] bytes = Encoding.Unicode.GetBytes(token); + nint num = Marshal.AllocHGlobal(bytes.Length); + nint num2 = Marshal.StringToCoTaskMemUni(key); + nint num3 = Marshal.StringToCoTaskMemUni(Environment.UserName); + try + { + Marshal.Copy(bytes, 0, num, bytes.Length); + CREDENTIAL userCredential = new CREDENTIAL + { + Type = 1u, + TargetName = num2, + UserName = num3, + CredentialBlobSize = (uint)bytes.Length, + CredentialBlob = num, + Persist = 2u + }; + if (!CredWrite(ref userCredential, 0u)) + { + LogService.Error($"토큰 저장 실패: {key}, 오류 코드: {Marshal.GetLastWin32Error()}"); + } + else + { + LogService.Info("토큰 저장 완료: " + key); + } + } + finally + { + Marshal.FreeHGlobal(num); + Marshal.FreeCoTaskMem(num2); + Marshal.FreeCoTaskMem(num3); + } + } + + public static bool DeleteToken(string key) + { + return !string.IsNullOrEmpty(key) && CredDelete(key, 1u, 0u); + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/CurrencyConverter.cs b/.decompiledproj/AxCopilot/Handlers/CurrencyConverter.cs new file mode 100644 index 0000000..b1c7bf0 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/CurrencyConverter.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +internal static class CurrencyConverter +{ + private static readonly Regex _pattern = new Regex("^(\\d+(?:\\.\\d+)?)\\s+([A-Za-z]{3})\\s+(?:to|in)\\s+([A-Za-z]{3})$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly Dictionary Rates)> _cache = new Dictionary)>(); + + private static readonly HttpClient _http = new HttpClient + { + Timeout = TimeSpan.FromSeconds(5.0) + }; + + private static readonly TimeSpan CacheTtl = TimeSpan.FromHours(1.0); + + private static readonly Dictionary _names = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["KRW"] = "원", + ["USD"] = "달러", + ["EUR"] = "유로", + ["JPY"] = "엔", + ["GBP"] = "파운드", + ["CNY"] = "위안", + ["HKD"] = "홍콩달러", + ["SGD"] = "싱가포르달러", + ["CAD"] = "캐나다달러", + ["AUD"] = "호주달러", + ["CHF"] = "스위스프랑", + ["TWD"] = "대만달러", + ["MXN"] = "멕시코페소", + ["BRL"] = "브라질헤알", + ["INR"] = "인도루피", + ["RUB"] = "루블", + ["THB"] = "바트", + ["VND"] = "동", + ["IDR"] = "루피아", + ["MYR"] = "링깃", + ["PHP"] = "페소", + ["NZD"] = "뉴질랜드달러", + ["SEK"] = "크로나", + ["NOK"] = "크로나(노르)", + ["DKK"] = "크로나(덴)", + ["AED"] = "디르함", + ["SAR"] = "리얄" + }; + + public static bool IsCurrencyQuery(string input) + { + return _pattern.IsMatch(input.Trim()); + } + + public static async Task> ConvertAsync(string input, CancellationToken ct) + { + Match m = _pattern.Match(input.Trim()); + if (!m.Success) + { + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("통화 형식 오류", "예: 100 USD to KRW", null, null, null, "\uea39")); + } + if (!double.TryParse(m.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var amount)) + { + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("숫자 형식 오류", "", null, null, null, "\uea39")); + } + string from = m.Groups[2].Value.ToUpperInvariant(); + string to = m.Groups[3].Value.ToUpperInvariant(); + try + { + Dictionary rates = await GetRatesAsync(from, ct); + if (rates == null) + { + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("환율 조회 실패", "네트워크 연결을 확인하세요", null, null, null, "\ue7ba")); + } + if (!rates.TryGetValue(to, out var rate)) + { + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("지원하지 않는 통화: " + to, "3자리 ISO 4217 코드를 입력하세요", null, null, null, "\uea39")); + } + double result = amount * rate; + string fn; + string fromName = (_names.TryGetValue(from, out fn) ? fn : from); + string tn; + string toName = (_names.TryGetValue(to, out tn) ? tn : to); + string text; + switch (to) + { + default: + text = result.ToString("N2", CultureInfo.CurrentCulture); + break; + case "KRW": + case "JPY": + case "IDR": + case "VND": + text = result.ToString("N0", CultureInfo.CurrentCulture); + break; + } + string resultStr = text; + string rateStr = ((rate < 0.01) ? rate.ToString("G4", CultureInfo.InvariantCulture) : rate.ToString("N4", CultureInfo.CurrentCulture)); + string display = resultStr + " " + to; + return new _003C_003Ez__ReadOnlyArray(new LauncherItem[2] + { + new LauncherItem($"{amount:N2} {from} = {display}", $"1 {from}({fromName}) = {rateStr} {to}({toName}) · Enter로 복사", null, display, null, "\ue8ef"), + new LauncherItem("숫자만: " + resultStr, "Enter로 숫자만 복사", null, resultStr, null, "\ue8ef") + }); + } + catch (OperationCanceledException) + { + return Array.Empty(); + } + catch (Exception ex2) + { + LogService.Warn("환율 조회 오류: " + ex2.Message); + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("환율 조회 실패", ex2.Message, null, null, null, "\ue7ba")); + } + } + + private static async Task?> GetRatesAsync(string baseCurrency, CancellationToken ct) + { + if (_cache.TryGetValue(baseCurrency, out var cached) && DateTime.Now - cached.At < CacheTtl) + { + return cached.Rates; + } + string url = "https://open.er-api.com/v6/latest/" + baseCurrency; + using JsonDocument doc = JsonDocument.Parse(await _http.GetStringAsync(url, ct)); + JsonElement root = doc.RootElement; + if (!root.TryGetProperty("result", out var resultEl) || resultEl.GetString() != "success") + { + return null; + } + if (!root.TryGetProperty("rates", out var ratesEl)) + { + return null; + } + Dictionary rates = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (JsonProperty prop in ratesEl.EnumerateObject()) + { + rates[prop.Name] = prop.Value.GetDouble(); + } + _cache[baseCurrency] = (DateTime.Now, rates); + return rates; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/DateCalcHandler.cs b/.decompiledproj/AxCopilot/Handlers/DateCalcHandler.cs new file mode 100644 index 0000000..7e7d933 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/DateCalcHandler.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using AxCopilot.SDK; + +namespace AxCopilot.Handlers; + +public class DateCalcHandler : IActionHandler +{ + private static readonly string[] DateFormats = new string[5] { "yyyy-MM-dd", "yyyy/MM/dd", "yyyyMMdd", "MM/dd/yyyy", "dd-MM-yyyy" }; + + public string? Prefix => "date"; + + public PluginMetadata Metadata => new PluginMetadata("DateCalc", "날짜 계산 · D-day · 타임스탬프 변환", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string text = query.Trim(); + DateTime now = DateTime.Now; + List list = new List(); + if (string.IsNullOrWhiteSpace(text)) + { + string value = now.ToString("dddd", new CultureInfo("ko-KR")); + list.Add(Item($"{now:yyyy-MM-dd} ({value})", $"{now:HH:mm:ss} · {now:yyyy-MM-dd}")); + list.Add(Item($"유닉스 타임스탬프: {new DateTimeOffset(now).ToUnixTimeSeconds()}", "현재 시각의 Unix epoch")); + list.Add(Item($"올해 {now.DayOfYear}일째 / 남은 일: {(new DateTime(now.Year, 12, 31) - now).Days}일", $"ISO 주차: {ISOWeek.GetWeekOfYear(now)}주")); + return Task.FromResult((IEnumerable)list); + } + Match match = Regex.Match(text, "^([+-])(\\d+)([dDwWmMyY])$"); + if (match.Success) + { + int num = ((match.Groups[1].Value == "+") ? 1 : (-1)); + int num2 = int.Parse(match.Groups[2].Value) * num; + string text2 = match.Groups[3].Value.ToLowerInvariant(); + if (1 == 0) + { + } + DateTime dateTime = text2 switch + { + "d" => now.AddDays(num2), + "w" => now.AddDays(num2 * 7), + "m" => now.AddMonths(num2), + "y" => now.AddYears(num2), + _ => now, + }; + if (1 == 0) + { + } + DateTime value2 = dateTime; + string value3 = value2.ToString("dddd", new CultureInfo("ko-KR")); + int days = (value2.Date - now.Date).Days; + string subtitle = ((days >= 0) ? $"오늘로부터 {days}일 후" : $"오늘로부터 {Math.Abs(days)}일 전"); + list.Add(Item($"{value2:yyyy-MM-dd} ({value3})", subtitle)); + return Task.FromResult((IEnumerable)list); + } + if (Regex.IsMatch(text, "^\\d{10,13}$") && long.TryParse(text, out var result)) + { + long seconds = ((result > 9999999999L) ? (result / 1000) : result); + DateTime localDateTime = DateTimeOffset.FromUnixTimeSeconds(seconds).LocalDateTime; + string value4 = localDateTime.ToString("dddd", new CultureInfo("ko-KR")); + list.Add(Item($"{localDateTime:yyyy-MM-dd HH:mm:ss} ({value4})", $"Unix {result} → 로컬 시간")); + return Task.FromResult((IEnumerable)list); + } + if (text.Equals("unix", StringComparison.OrdinalIgnoreCase) || text.Equals("to unix", StringComparison.OrdinalIgnoreCase)) + { + long value5 = new DateTimeOffset(now).ToUnixTimeSeconds(); + list.Add(Item($"{value5}", $"현재 시각 ({now:yyyy-MM-dd HH:mm:ss}) → Unix 타임스탬프")); + return Task.FromResult((IEnumerable)list); + } + if (DateTime.TryParseExact(text, DateFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result2)) + { + string value6 = result2.ToString("dddd", new CultureInfo("ko-KR")); + int days2 = (result2.Date - now.Date).Days; + if (1 == 0) + { + } + string text3 = ((days2 > 0) ? $"D-{days2} (앞으로 {days2}일)" : ((days2 != 0) ? $"D+{Math.Abs(days2)} ({Math.Abs(days2)}일 지남)" : "오늘")); + if (1 == 0) + { + } + string subtitle2 = text3; + list.Add(Item($"{result2:yyyy-MM-dd} ({value6})", subtitle2)); + return Task.FromResult((IEnumerable)list); + } + list.Add(new LauncherItem("날짜 형식을 인식할 수 없습니다", "예: +30d, -100d, 2026-12-25, 1711584000, unix", null, null, null, "\ue7ba")); + return Task.FromResult((IEnumerable)list); + } + + private static LauncherItem Item(string title, string subtitle) + { + return new LauncherItem(title, subtitle + " · Enter로 복사", null, title, null, "\ue823"); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + string text = data as string; + if (text != null && !string.IsNullOrWhiteSpace(text)) + { + try + { + Application current = Application.Current; + if (current != null) + { + ((DispatcherObject)current).Dispatcher.Invoke((Action)delegate + { + Clipboard.SetText(text); + }); + } + } + catch + { + } + } + return Task.CompletedTask; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/DiffHandler.cs b/.decompiledproj/AxCopilot/Handlers/DiffHandler.cs new file mode 100644 index 0000000..ba04f8b --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/DiffHandler.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using AxCopilot.SDK; +using AxCopilot.Services; +using Microsoft.Win32; + +namespace AxCopilot.Handlers; + +public class DiffHandler : IActionHandler +{ + private record DiffResult(string Text, int Added, int Removed, int Same); + + private readonly ClipboardHistoryService? _clipHistory; + + public string? Prefix => "diff"; + + public PluginMetadata Metadata => new PluginMetadata("Diff", "텍스트/파일 비교 — diff", "1.0", "AX"); + + public DiffHandler(ClipboardHistoryService? clipHistory = null) + { + _clipHistory = clipHistory; + } + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string text = query.Trim(); + if (!string.IsNullOrWhiteSpace(text)) + { + string[] array = text.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (array.Length == 2 && File.Exists(array[0]) && File.Exists(array[1])) + { + try + { + string textA = File.ReadAllText(array[0]); + string textB = File.ReadAllText(array[1]); + DiffResult diffResult = BuildDiff(textA, textB, Path.GetFileName(array[0]), Path.GetFileName(array[1])); + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("파일 비교: " + Path.GetFileName(array[0]) + " ↔ " + Path.GetFileName(array[1]), $"{diffResult.Added}줄 추가, {diffResult.Removed}줄 삭제, {diffResult.Same}줄 동일 · Enter로 결과 복사", null, diffResult.Text, null, "\ue8a5"))); + } + catch (Exception ex) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("파일 읽기 실패: " + ex.Message, "", null, null, null, "\uea39"))); + } + } + if (array.Length >= 1 && (File.Exists(array[0]) || Directory.Exists(Path.GetDirectoryName(array[0]) ?? ""))) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("비교할 파일 2개의 경로를 입력하세요", "예: diff C:\\a.txt C:\\b.txt", null, null, null, "\ue946"))); + } + } + IReadOnlyList readOnlyList = _clipHistory?.History; + if (readOnlyList == null || readOnlyList.Count < 2) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("비교할 텍스트가 부족합니다", "클립보드에 2개 이상의 텍스트를 복사하거나, diff [파일A] [파일B]로 파일 비교", null, null, null, "\ue946"))); + } + List list = readOnlyList.Where((ClipboardEntry e) => e.IsText).Take(2).ToList(); + if (list.Count < 2) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("텍스트 히스토리가 2개 미만입니다", "텍스트를 2번 이상 복사하세요", null, null, null, "\ue946"))); + } + ClipboardEntry clipboardEntry = list[1]; + ClipboardEntry clipboardEntry2 = list[0]; + DiffResult diffResult2 = BuildDiff(clipboardEntry.Text, clipboardEntry2.Text, "이전 (" + clipboardEntry.RelativeTime + ")", "최근 (" + clipboardEntry2.RelativeTime + ")"); + List list2 = new List + { + new LauncherItem($"클립보드 비교: +{diffResult2.Added} -{diffResult2.Removed} ={diffResult2.Same}", "이전 복사 ↔ 최근 복사 · Enter로 결과 복사", null, diffResult2.Text, null, "\ue81c") + }; + IEnumerable enumerable = (from l in diffResult2.Text.Split('\n') + where l.StartsWith("+ ") || l.StartsWith("- ") + select l).Take(5); + foreach (string item in enumerable) + { + string symbol = (item.StartsWith("+ ") ? "\ue710" : "\ue711"); + list2.Add(new LauncherItem(item, "", null, null, null, symbol)); + } + list2.Add(new LauncherItem("파일 선택하여 비교", "파일 선택 대화 상자에서 2개의 파일을 골라 비교합니다", null, "__FILE_DIALOG__", null, "\ue8a5")); + return Task.FromResult((IEnumerable)list2); + } + + public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + if (data is string s && s == "__FILE_DIALOG__") + { + await Task.Delay(200, ct); + Application current = Application.Current; + if (current == null) + { + return; + } + ((DispatcherObject)current).Dispatcher.Invoke((Action)delegate + { + OpenFileDialog openFileDialog = new OpenFileDialog + { + Title = "비교할 첫 번째 파일 선택", + Filter = "텍스트 파일|*.txt;*.cs;*.json;*.xml;*.md;*.csv;*.log|모든 파일|*.*" + }; + if (openFileDialog.ShowDialog() == true) + { + string fileName = openFileDialog.FileName; + openFileDialog.Title = "비교할 두 번째 파일 선택"; + if (openFileDialog.ShowDialog() == true) + { + string fileName2 = openFileDialog.FileName; + try + { + string textA = File.ReadAllText(fileName); + string textB = File.ReadAllText(fileName2); + DiffResult diffResult = BuildDiff(textA, textB, Path.GetFileName(fileName), Path.GetFileName(fileName2)); + Clipboard.SetText(diffResult.Text); + NotificationService.Notify("파일 비교 완료", $"+{diffResult.Added} -{diffResult.Removed} ={diffResult.Same} · 결과 클립보드 복사됨"); + } + catch (Exception ex) + { + NotificationService.Notify("비교 실패", ex.Message); + } + } + } + }); + return; + } + data = item.Data; + string text = data as string; + if (text == null || string.IsNullOrWhiteSpace(text)) + { + return; + } + try + { + Application current2 = Application.Current; + if (current2 != null) + { + ((DispatcherObject)current2).Dispatcher.Invoke((Action)delegate + { + Clipboard.SetText(text); + }); + } + } + catch + { + } + NotificationService.Notify("비교 결과", "클립보드에 복사되었습니다"); + } + + private static DiffResult BuildDiff(string textA, string textB, string labelA, string labelB) + { + string[] array = (from l in textA.Split('\n') + select l.TrimEnd('\r')).ToArray(); + string[] array2 = (from l in textB.Split('\n') + select l.TrimEnd('\r')).ToArray(); + HashSet hashSet = new HashSet(array); + HashSet hashSet2 = new HashSet(array2); + StringBuilder stringBuilder = new StringBuilder(); + StringBuilder stringBuilder2 = stringBuilder; + StringBuilder stringBuilder3 = stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(4, 1, stringBuilder2); + handler.AppendLiteral("--- "); + handler.AppendFormatted(labelA); + stringBuilder3.AppendLine(ref handler); + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder4 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(4, 1, stringBuilder2); + handler.AppendLiteral("+++ "); + handler.AppendFormatted(labelB); + stringBuilder4.AppendLine(ref handler); + stringBuilder.AppendLine(); + int num = 0; + int num2 = 0; + int num3 = 0; + int num4 = Math.Max(array.Length, array2.Length); + for (int num5 = 0; num5 < num4; num5++) + { + string text = ((num5 < array.Length) ? array[num5] : null); + string text2 = ((num5 < array2.Length) ? array2[num5] : null); + if (text == text2) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder5 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(2, 1, stringBuilder2); + handler.AppendLiteral(" "); + handler.AppendFormatted(text); + stringBuilder5.AppendLine(ref handler); + num3++; + continue; + } + if (text != null && !hashSet2.Contains(text)) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder6 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(2, 1, stringBuilder2); + handler.AppendLiteral("- "); + handler.AppendFormatted(text); + stringBuilder6.AppendLine(ref handler); + num2++; + } + if (text2 != null && !hashSet.Contains(text2)) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder7 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(2, 1, stringBuilder2); + handler.AppendLiteral("+ "); + handler.AppendFormatted(text2); + stringBuilder7.AppendLine(ref handler); + num++; + } + if (text != null && hashSet2.Contains(text) && text != text2) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder8 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(2, 1, stringBuilder2); + handler.AppendLiteral(" "); + handler.AppendFormatted(text); + stringBuilder8.AppendLine(ref handler); + num3++; + } + } + return new DiffResult(stringBuilder.ToString(), num, num2, num3); + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/EmojiHandler.cs b/.decompiledproj/AxCopilot/Handlers/EmojiHandler.cs new file mode 100644 index 0000000..e9b4cb3 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/EmojiHandler.cs @@ -0,0 +1,517 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using AxCopilot.SDK; + +namespace AxCopilot.Handlers; + +public class EmojiHandler : IActionHandler +{ + private static readonly (string Emoji, string Name, string Tags)[] _emojis = new(string, string, string)[462] + { + ("\ud83d\ude00", "크게 웃는 얼굴", "smile happy grin 웃음 행복"), + ("\ud83d\ude03", "웃는 얼굴", "smile happy joy 웃음"), + ("\ud83d\ude04", "눈 웃음", "smile laugh 웃음 기쁨"), + ("\ud83d\ude01", "히죽 웃음", "grin beam 씩 웃다"), + ("\ud83d\ude06", "크게 웃음", "laughing 폭소"), + ("\ud83d\ude05", "식은땀 웃음", "sweat smile 안도"), + ("\ud83e\udd23", "바닥 구르며 웃음", "rofl lol 빵 웃음"), + ("\ud83d\ude02", "눈물 나게 웃음", "joy tears laugh 폭소"), + ("\ud83d\ude42", "살짝 웃음", "slightly smiling 미소"), + ("\ud83d\ude43", "거꾸로 웃음", "upside down 뒤집힌"), + ("\ud83d\ude09", "윙크", "wink 윙크"), + ("\ud83d\ude0a", "볼 빨개진 웃음", "blush 부끄러움 미소"), + ("\ud83d\ude07", "천사", "angel halo 천사 선량"), + ("\ud83e\udd70", "사랑스러운 얼굴", "love hearts 사랑 하트"), + ("\ud83d\ude0d", "하트 눈", "heart eyes 사랑 반함"), + ("\ud83e\udd29", "별 눈", "star struck 감동 황홀"), + ("\ud83d\ude18", "뽀뽀", "kiss blow 키스 뽀뽀"), + ("\ud83d\ude17", "오므린 입", "kiss whistle 키스"), + ("\ud83d\ude1a", "눈 감고 뽀뽀", "kiss 키스"), + ("\ud83d\ude19", "볼 뽀뽀", "kiss 키스"), + ("\ud83d\ude0b", "맛있다", "yum delicious 맛 음식"), + ("\ud83d\ude1b", "혀 내밀기", "tongue out 혀 놀림"), + ("\ud83d\ude1c", "윙크하며 혀", "wink tongue 장난"), + ("\ud83e\udd2a", "미친 표정", "zany crazy 정신없음"), + ("\ud83d\ude1d", "눈 감고 혀", "tongue 혀"), + ("\ud83e\udd11", "돈 눈", "money face 돈 부자"), + ("\ud83e\udd17", "포옹", "hugging hug 안아줘 포옹"), + ("\ud83e\udd2d", "입 가리고", "hand over mouth 헉 깜짝"), + ("\ud83e\udd2b", "쉿", "shushing quiet 조용 쉿"), + ("\ud83e\udd14", "생각 중", "thinking 고민 생각"), + ("\ud83e\udd10", "입 막음", "zipper mouth 비밀"), + ("\ud83e\udd28", "의심", "raised eyebrow 의심 의아"), + ("\ud83d\ude10", "무표정", "neutral 무감각 무표정"), + ("\ud83d\ude11", "표정 없음", "expressionless 냉담"), + ("\ud83d\ude36", "입 없는 얼굴", "no mouth 침묵"), + ("\ud83d\ude0f", "비웃음", "smirk 비웃 냉소"), + ("\ud83d\ude12", "불만", "unamused 불만 짜증"), + ("\ud83d\ude44", "눈 굴리기", "eye roll 어이없음"), + ("\ud83d\ude2c", "이 드러냄", "grimace 으 민망"), + ("\ud83e\udd25", "거짓말", "lying pinocchio 거짓말"), + ("\ud83d\ude0c", "안도/평온", "relieved 안도 평온"), + ("\ud83d\ude14", "슬픔", "pensive sad 슬픔 우울"), + ("\ud83d\ude2a", "졸림", "sleepy 졸음"), + ("\ud83e\udd24", "침 흘림", "drooling 군침 식욕"), + ("\ud83d\ude34", "잠", "sleeping sleep 수면 잠"), + ("\ud83d\ude37", "마스크", "mask sick 마스크 아픔"), + ("\ud83e\udd12", "열 나는", "sick fever 열 아픔"), + ("\ud83e\udd15", "머리 붕대", "injured hurt 부상"), + ("\ud83e\udd22", "구역질", "nauseated sick 구역 메스꺼움"), + ("\ud83e\udd2e", "토하는", "vomit 구토"), + ("\ud83e\udd27", "재채기", "sneezing sick 재채기 감기"), + ("\ud83e\udd75", "더운", "hot overheated 더움 열"), + ("\ud83e\udd76", "추운", "cold freezing 추움 냉기"), + ("\ud83e\udd74", "어지러운", "woozy 어지럼 취함"), + ("\ud83d\ude35", "어질어질", "dizzy 어지럼 충격"), + ("\ud83e\udd2f", "머리 폭발", "exploding head 충격 대박"), + ("\ud83e\udd20", "카우보이", "cowboy hat 카우보이"), + ("\ud83e\udd78", "변장", "disguise 변장 선글라스"), + ("\ud83d\ude0e", "쿨한", "cool sunglasses 선글라스 쿨"), + ("\ud83e\udd13", "공부벌레", "nerd glasses 공부 안경"), + ("\ud83e\uddd0", "모노클", "monocle curious 고상 탐정"), + ("\ud83d\ude15", "당황", "confused 당황 모호"), + ("\ud83d\ude1f", "걱정", "worried concern 걱정"), + ("\ud83d\ude41", "살짝 찡그림", "frown 슬픔"), + ("☹\ufe0f", "찡그린 얼굴", "frown sad 슬픔"), + ("\ud83d\ude2e", "입 벌림", "open mouth surprised 놀람"), + ("\ud83d\ude2f", "놀람", "hushed surprised 깜짝"), + ("\ud83d\ude32", "충격", "astonished 충격 놀람"), + ("\ud83d\ude33", "얼굴 빨개짐", "flushed embarrassed 부끄럼 당황"), + ("\ud83e\udd7a", "애원", "pleading eyes 부탁 눈빛"), + ("\ud83d\ude26", "찡그리며 벌린 입", "frowning 불안"), + ("\ud83d\ude27", "고통", "anguished 고통"), + ("\ud83d\ude28", "무서움", "fearful scared 무서움 공포"), + ("\ud83d\ude30", "식은땀", "anxious sweat 불안 걱정"), + ("\ud83d\ude25", "눈물 조금", "sad disappointed 실망 눈물"), + ("\ud83d\ude22", "울음", "cry sad 슬픔 눈물"), + ("\ud83d\ude2d", "엉엉 울음", "loudly crying sob 통곡"), + ("\ud83d\ude31", "공포에 질림", "screaming fear 비명 공포"), + ("\ud83d\ude16", "혼란", "confounded 혼란"), + ("\ud83d\ude23", "힘듦", "persevering 고생"), + ("\ud83d\ude1e", "실망", "disappointed 실망"), + ("\ud83d\ude13", "땀", "downcast sweat 땀 힘듦"), + ("\ud83d\ude29", "피곤", "weary tired 지침 피곤"), + ("\ud83d\ude2b", "극도로 지침", "tired exhausted 탈진"), + ("\ud83e\udd71", "하품", "yawning bored 하품 지루함"), + ("\ud83d\ude24", "콧김", "triumph snort 분노 콧김"), + ("\ud83d\ude21", "화남", "angry mad 화남 분노"), + ("\ud83d\ude20", "성남", "angry 화 성남"), + ("\ud83e\udd2c", "욕", "cursing swearing 욕 분노"), + ("\ud83d\ude08", "나쁜 미소", "smiling devil 악마 장난"), + ("\ud83d\udc7f", "화난 악마", "angry devil 악마"), + ("\ud83d\udc80", "해골", "skull death 해골 죽음"), + ("☠\ufe0f", "해골 십자", "skull crossbones 독"), + ("\ud83d\udca9", "응가", "poop 똥 응가"), + ("\ud83e\udd21", "피에로", "clown 광대"), + ("\ud83d\udc79", "도깨비", "ogre 도깨비 귀신"), + ("\ud83d\udc7a", "텐구", "goblin 텐구"), + ("\ud83d\udc7b", "유령", "ghost 유령 귀신"), + ("\ud83d\udc7e", "우주인", "alien monster 외계인 게임"), + ("\ud83e\udd16", "로봇", "robot 로봇"), + ("\ud83d\udc4b", "손 흔들기", "wave waving hi bye 안녕"), + ("\ud83e\udd1a", "손 뒤", "raised back hand 손"), + ("\ud83d\udd90\ufe0f", "손바닥", "hand palm 다섯 손가락"), + ("✋", "손 들기", "raised hand 손 들기 멈춤"), + ("\ud83d\udd96", "스팍 손인사", "vulcan salute 스타트렉"), + ("\ud83d\udc4c", "오케이", "ok perfect 오케이 좋아"), + ("\ud83e\udd0c", "손가락 모아", "pinched fingers 이탈리아"), + ("✌\ufe0f", "브이", "victory peace v 브이 평화"), + ("\ud83e\udd1e", "행운 손가락", "crossed fingers lucky 행운 기도"), + ("\ud83e\udd1f", "아이 러브 유", "love you 사랑해"), + ("\ud83e\udd18", "록 손", "rock on metal 록"), + ("\ud83e\udd19", "전화해", "call me shaka 전화 샤카"), + ("\ud83d\udc48", "왼쪽 가리킴", "backhand left 왼쪽"), + ("\ud83d\udc49", "오른쪽 가리킴", "backhand right 오른쪽"), + ("\ud83d\udc46", "위 가리킴", "backhand up 위"), + ("\ud83d\udd95", "욕", "middle finger 욕"), + ("\ud83d\udc47", "아래 가리킴", "backhand down 아래"), + ("☝\ufe0f", "검지 들기", "index pointing up 하나 포인트"), + ("\ud83d\udc4d", "좋아요", "thumbs up like good 좋아 최고"), + ("\ud83d\udc4e", "싫어요", "thumbs down dislike 싫어 별로"), + ("✊", "주먹", "fist punch 주먹"), + ("\ud83d\udc4a", "주먹 치기", "punch fist 주먹"), + ("\ud83e\udd1b", "왼 주먹", "left fist 주먹"), + ("\ud83e\udd1c", "오른 주먹", "right fist 주먹"), + ("\ud83d\udc4f", "박수", "clapping applause 박수 응원"), + ("\ud83d\ude4c", "만세", "raising hands celebrate 만세"), + ("\ud83d\udc50", "양손 펼침", "open hands 환영"), + ("\ud83e\udd32", "두 손 모음", "palms up together 기도 바람"), + ("\ud83d\ude4f", "두 손 합장", "pray please thanks 감사 부탁 기도"), + ("✍\ufe0f", "글쓰기", "writing pen 글쓰기"), + ("\ud83d\udc85", "네일", "nail polish manicure 네일 손톱"), + ("\ud83e\udd33", "셀카", "selfie 셀카"), + ("\ud83d\udcaa", "근육", "muscle strong 근육 힘"), + ("\ud83e\uddbe", "기계 팔", "mechanical arm 로봇 팔"), + ("\ud83e\uddbf", "기계 다리", "mechanical leg 로봇 다리"), + ("\ud83e\uddb5", "다리", "leg kick 다리"), + ("\ud83e\uddb6", "발", "foot kick 발"), + ("\ud83d\udc42", "귀", "ear hear 귀"), + ("\ud83e\uddbb", "보청기 귀", "ear hearing aid 보청기"), + ("\ud83d\udc43", "코", "nose smell 코"), + ("\ud83e\udec0", "심장", "heart anatomical 심장"), + ("\ud83e\udec1", "폐", "lungs 폐"), + ("\ud83e\udde0", "뇌", "brain mind 뇌 지능"), + ("\ud83e\uddb7", "치아", "tooth dental 치아"), + ("\ud83e\uddb4", "뼈", "bone 뼈"), + ("\ud83d\udc40", "눈", "eyes look see 눈 보기"), + ("\ud83d\udc41\ufe0f", "한쪽 눈", "eye 눈"), + ("\ud83d\udc45", "혀", "tongue 혀"), + ("\ud83d\udc44", "입술", "lips mouth 입술"), + ("\ud83d\udc8b", "입맞춤", "kiss lips 키스 입술"), + ("\ud83e\ude78", "피", "blood drop 피 혈액"), + ("❤\ufe0f", "빨간 하트", "red heart love 사랑 빨강"), + ("\ud83e\udde1", "주황 하트", "orange heart 사랑"), + ("\ud83d\udc9b", "노란 하트", "yellow heart 사랑"), + ("\ud83d\udc9a", "초록 하트", "green heart 사랑"), + ("\ud83d\udc99", "파란 하트", "blue heart 사랑"), + ("\ud83d\udc9c", "보라 하트", "purple heart 사랑"), + ("\ud83d\udda4", "검은 하트", "black heart 사랑 다크"), + ("\ud83e\udd0d", "흰 하트", "white heart 사랑"), + ("\ud83e\udd0e", "갈색 하트", "brown heart 사랑"), + ("\ud83d\udc94", "깨진 하트", "broken heart 이별 상처"), + ("❣\ufe0f", "느낌표 하트", "heart exclamation 사랑"), + ("\ud83d\udc95", "두 하트", "two hearts 사랑"), + ("\ud83d\udc9e", "회전 하트", "revolving hearts 사랑"), + ("\ud83d\udc93", "뛰는 하트", "beating heart 설렘"), + ("\ud83d\udc97", "성장 하트", "growing heart 사랑"), + ("\ud83d\udc96", "반짝 하트", "sparkling heart 사랑"), + ("\ud83d\udc98", "화살 하트", "heart arrow 큐피드"), + ("\ud83d\udc9d", "리본 하트", "heart ribbon 선물 사랑"), + ("\ud83d\udc9f", "하트 장식", "heart decoration 사랑"), + ("☮\ufe0f", "평화", "peace 평화"), + ("✝\ufe0f", "십자가", "cross 기독교"), + ("☯\ufe0f", "음양", "yin yang 음양 균형"), + ("\ud83d\udd2e", "수정구", "crystal ball magic 마법 점"), + ("✨", "반짝임", "sparkles glitter 빛 반짝"), + ("⭐", "별", "star 별"), + ("\ud83c\udf1f", "빛나는 별", "glowing star 별빛"), + ("\ud83d\udcab", "현기증", "dizzy star 빙글"), + ("⚡", "번개", "lightning bolt 번개 전기"), + ("\ud83d\udd25", "불", "fire hot 불 열정"), + ("\ud83d\udca5", "폭발", "explosion boom 폭발"), + ("❄\ufe0f", "눈송이", "snowflake cold 눈 추위"), + ("\ud83c\udf08", "무지개", "rainbow 무지개"), + ("☀\ufe0f", "태양", "sun sunny 태양 맑음"), + ("\ud83c\udf19", "달", "moon crescent 달"), + ("\ud83c\udf0a", "파도", "wave ocean 파도 바다"), + ("\ud83d\udca8", "바람", "wind dash 바람"), + ("\ud83d\udca6", "물방울", "sweat droplets water 물"), + ("\ud83c\udf38", "벚꽃", "cherry blossom 벚꽃 봄"), + ("\ud83c\udf39", "장미", "rose 장미 꽃"), + ("\ud83c\udf3a", "히비스커스", "hibiscus 꽃"), + ("\ud83c\udf3b", "해바라기", "sunflower 해바라기"), + ("\ud83c\udf3c", "꽃", "blossom flower 꽃"), + ("\ud83c\udf37", "튤립", "tulip 튤립"), + ("\ud83d\udc90", "꽃다발", "bouquet flowers 꽃다발"), + ("\ud83c\udf40", "네잎클로버", "four leaf clover lucky 행운"), + ("\ud83c\udf3f", "허브", "herb green 풀 허브"), + ("\ud83c\udf43", "잎사귀", "leaf 잎"), + ("\ud83c\udf55", "피자", "pizza 피자"), + ("\ud83c\udf54", "햄버거", "hamburger burger 버거"), + ("\ud83c\udf2e", "타코", "taco 타코"), + ("\ud83c\udf5c", "라면", "ramen noodles 라면 국수"), + ("\ud83c\udf71", "도시락", "bento box 도시락"), + ("\ud83c\udf63", "초밥", "sushi 초밥"), + ("\ud83c\udf5a", "밥", "rice 밥"), + ("\ud83c\udf5b", "카레", "curry rice 카레"), + ("\ud83c\udf5d", "파스타", "pasta spaghetti 파스타"), + ("\ud83c\udf66", "소프트 아이스크림", "ice cream soft serve 아이스크림"), + ("\ud83c\udf82", "생일 케이크", "cake birthday 생일 케이크"), + ("\ud83c\udf70", "케이크 조각", "cake slice 케이크"), + ("\ud83e\uddc1", "컵케이크", "cupcake 컵케이크"), + ("\ud83c\udf69", "도넛", "donut 도넛"), + ("\ud83c\udf6a", "쿠키", "cookie 쿠키"), + ("\ud83c\udf6b", "초콜릿", "chocolate bar 초콜릿"), + ("\ud83c\udf6c", "사탕", "candy 사탕"), + ("\ud83c\udf6d", "막대 사탕", "lollipop 막대사탕"), + ("\ud83c\udf7a", "맥주", "beer mug 맥주"), + ("\ud83c\udf7b", "건배", "clinking beer 건배"), + ("\ud83e\udd42", "샴페인 건배", "champagne 샴페인 건배"), + ("\ud83c\udf77", "와인", "wine 와인"), + ("☕", "커피", "coffee hot 커피"), + ("\ud83e\uddc3", "주스", "juice 주스"), + ("\ud83e\udd64", "음료", "drink cup 음료 컵"), + ("\ud83e\uddcb", "버블티", "bubble tea boba 버블티"), + ("\ud83c\udf75", "녹차", "tea matcha 차 녹차"), + ("\ud83d\udc36", "강아지", "dog puppy 강아지 개"), + ("\ud83d\udc31", "고양이", "cat kitten 고양이"), + ("\ud83d\udc2d", "쥐", "mouse 쥐"), + ("\ud83d\udc39", "햄스터", "hamster 햄스터"), + ("\ud83d\udc30", "토끼", "rabbit bunny 토끼"), + ("\ud83e\udd8a", "여우", "fox 여우"), + ("\ud83d\udc3b", "곰", "bear 곰"), + ("\ud83d\udc3c", "판다", "panda 판다"), + ("\ud83d\udc28", "코알라", "koala 코알라"), + ("\ud83d\udc2f", "호랑이", "tiger 호랑이"), + ("\ud83e\udd81", "사자", "lion 사자"), + ("\ud83d\udc2e", "소", "cow 소"), + ("\ud83d\udc37", "돼지", "pig 돼지"), + ("\ud83d\udc38", "개구리", "frog 개구리"), + ("\ud83d\udc35", "원숭이", "monkey 원숭이"), + ("\ud83d\ude48", "눈 가린 원숭이", "see no evil monkey 안 봐"), + ("\ud83d\ude49", "귀 가린 원숭이", "hear no evil monkey 안 들어"), + ("\ud83d\ude4a", "입 가린 원숭이", "speak no evil monkey 안 말해"), + ("\ud83d\udc14", "닭", "chicken 닭"), + ("\ud83d\udc27", "펭귄", "penguin 펭귄"), + ("\ud83d\udc26", "새", "bird 새"), + ("\ud83e\udd86", "오리", "duck 오리"), + ("\ud83e\udd85", "독수리", "eagle 독수리"), + ("\ud83e\udd89", "부엉이", "owl 부엉이"), + ("\ud83d\udc0d", "뱀", "snake 뱀"), + ("\ud83d\udc22", "거북이", "turtle 거북이"), + ("\ud83e\udd8b", "나비", "butterfly 나비"), + ("\ud83d\udc0c", "달팽이", "snail 달팽이"), + ("\ud83d\udc1b", "애벌레", "bug caterpillar 애벌레"), + ("\ud83d\udc1d", "꿀벌", "bee honeybee 벌"), + ("\ud83e\udd91", "오징어", "squid 오징어"), + ("\ud83d\udc19", "문어", "octopus 문어"), + ("\ud83d\udc20", "열대어", "tropical fish 열대어"), + ("\ud83d\udc21", "복어", "blowfish puffer 복어"), + ("\ud83e\udd88", "상어", "shark 상어"), + ("\ud83d\udc2c", "돌고래", "dolphin 돌고래"), + ("\ud83d\udc33", "고래", "whale 고래"), + ("\ud83d\udc32", "용", "dragon 용"), + ("\ud83e\udd84", "유니콘", "unicorn 유니콘"), + ("\ud83d\udcf1", "스마트폰", "phone mobile smartphone 폰"), + ("\ud83d\udcbb", "노트북", "laptop computer 노트북"), + ("\ud83d\udda5\ufe0f", "데스크톱", "desktop computer 컴퓨터"), + ("⌨\ufe0f", "키보드", "keyboard 키보드"), + ("\ud83d\uddb1\ufe0f", "마우스", "mouse 마우스"), + ("\ud83d\udda8\ufe0f", "프린터", "printer 프린터"), + ("\ud83d\udcf7", "카메라", "camera 카메라"), + ("\ud83d\udcf8", "플래시 카메라", "camera flash 사진"), + ("\ud83d\udcf9", "비디오 카메라", "video camera 동영상"), + ("\ud83c\udfa5", "영화 카메라", "movie camera film 영화"), + ("\ud83d\udcfa", "TV", "television tv 텔레비전"), + ("\ud83d\udcfb", "라디오", "radio 라디오"), + ("\ud83c\udf99\ufe0f", "마이크", "microphone studio 마이크"), + ("\ud83c\udfa4", "마이크 핸드헬드", "microphone karaoke 마이크"), + ("\ud83c\udfa7", "헤드폰", "headphones 헤드폰"), + ("\ud83d\udce1", "안테나", "satellite antenna 안테나"), + ("\ud83d\udd0b", "배터리", "battery 배터리"), + ("\ud83d\udd0c", "전원 플러그", "plug electric 플러그"), + ("\ud83d\udca1", "전구", "bulb idea light 전구 아이디어"), + ("\ud83d\udd26", "손전등", "flashlight torch 손전등"), + ("\ud83d\udd6f\ufe0f", "양초", "candle 양초"), + ("\ud83d\udcda", "책", "books stack 책"), + ("\ud83d\udcd6", "열린 책", "open book read 독서"), + ("\ud83d\udcdd", "메모", "memo note pencil 메모 노트"), + ("✏\ufe0f", "연필", "pencil 연필"), + ("\ud83d\udd8a\ufe0f", "펜", "pen 펜"), + ("\ud83d\udccc", "압정", "pushpin pin 압정"), + ("\ud83d\udcce", "클립", "paperclip 클립"), + ("✂\ufe0f", "가위", "scissors cut 가위"), + ("\ud83d\uddc2\ufe0f", "파일 폴더", "card index dividers folder 파일"), + ("\ud83d\udcc1", "폴더", "folder 폴더"), + ("\ud83d\udcc2", "열린 폴더", "open folder 폴더"), + ("\ud83d\uddc3\ufe0f", "파일 박스", "card file box 서류함"), + ("\ud83d\uddd1\ufe0f", "휴지통", "wastebasket trash 휴지통"), + ("\ud83d\udd12", "잠금", "locked lock 잠금"), + ("\ud83d\udd13", "열림", "unlocked 열림"), + ("\ud83d\udd11", "열쇠", "key 열쇠"), + ("\ud83d\udddd\ufe0f", "구식 열쇠", "old key 열쇠"), + ("\ud83d\udd28", "망치", "hammer 망치"), + ("\ud83d\udd27", "렌치", "wrench tool 렌치"), + ("\ud83d\udd29", "나사", "nut bolt 나사"), + ("⚙\ufe0f", "톱니바퀴", "gear settings 설정 톱니"), + ("\ud83d\udee0\ufe0f", "도구", "tools hammer wrench 도구"), + ("\ud83d\udc8a", "알약", "pill medicine 약 알약"), + ("\ud83d\udc89", "주사기", "syringe injection 주사"), + ("\ud83e\ude7a", "청진기", "stethoscope doctor 청진기"), + ("\ud83c\udfc6", "트로피", "trophy award 트로피 우승"), + ("\ud83e\udd47", "금메달", "first gold medal 금메달"), + ("\ud83e\udd48", "은메달", "second silver 은메달"), + ("\ud83e\udd49", "동메달", "third bronze 동메달"), + ("\ud83c\udf96\ufe0f", "훈장", "medal military 훈장"), + ("\ud83c\udf97\ufe0f", "리본", "ribbon awareness 리본"), + ("\ud83c\udfab", "티켓", "ticket admission 티켓"), + ("\ud83c\udf9f\ufe0f", "입장권", "admission tickets 티켓"), + ("\ud83c\udfaa", "서커스", "circus tent 서커스"), + ("\ud83c\udfa8", "팔레트", "art palette paint 그림 예술"), + ("\ud83c\udfad", "연극", "performing arts theater 연극"), + ("\ud83c\udfac", "클래퍼보드", "clapper film 영화 촬영"), + ("\ud83c\udfae", "게임 컨트롤러", "video game controller 게임"), + ("\ud83c\udfb2", "주사위", "dice game 주사위"), + ("\ud83c\udfaf", "다트", "bullseye target dart 다트 목표"), + ("\ud83c\udfb3", "볼링", "bowling 볼링"), + ("⚽", "축구", "soccer football 축구"), + ("\ud83c\udfc0", "농구", "basketball 농구"), + ("\ud83c\udfc8", "미식축구", "american football 미식축구"), + ("⚾", "야구", "baseball 야구"), + ("\ud83c\udfbe", "테니스", "tennis 테니스"), + ("\ud83c\udfd0", "배구", "volleyball 배구"), + ("\ud83c\udfc9", "럭비", "rugby 럭비"), + ("\ud83c\udfb1", "당구", "billiards pool 당구"), + ("\ud83c\udfd3", "탁구", "ping pong table tennis 탁구"), + ("\ud83c\udff8", "배드민턴", "badminton 배드민턴"), + ("\ud83e\udd4a", "권투 장갑", "boxing glove 권투"), + ("\ud83c\udfa3", "낚시", "fishing 낚시"), + ("\ud83c\udfcb\ufe0f", "역도", "weightlifting gym 헬스 역도"), + ("\ud83e\uddd8", "명상", "yoga meditation 명상 요가"), + ("\ud83d\ude97", "자동차", "car automobile 자동차"), + ("\ud83d\ude95", "택시", "taxi cab 택시"), + ("\ud83d\ude99", "SUV", "suv car 차"), + ("\ud83d\ude8c", "버스", "bus 버스"), + ("\ud83d\ude8e", "무궤도 전차", "trolleybus 버스"), + ("\ud83c\udfce\ufe0f", "레이싱카", "racing car 레이싱"), + ("\ud83d\ude93", "경찰차", "police car 경찰"), + ("\ud83d\ude91", "구급차", "ambulance 구급차"), + ("\ud83d\ude92", "소방차", "fire truck 소방차"), + ("\ud83d\ude90", "미니밴", "minibus van 밴"), + ("\ud83d\ude9a", "트럭", "truck delivery 트럭"), + ("✈\ufe0f", "비행기", "airplane flight plane 비행기"), + ("\ud83d\ude80", "로켓", "rocket space launch 로켓"), + ("\ud83d\udef8", "UFO", "flying saucer ufo 유에프오"), + ("\ud83d\ude81", "헬리콥터", "helicopter 헬리콥터"), + ("\ud83d\ude82", "기차", "train locomotive 기차"), + ("\ud83d\ude86", "고속열차", "train 기차"), + ("\ud83d\ude87", "지하철", "metro subway 지하철"), + ("⛵", "돛단배", "sailboat 요트"), + ("\ud83d\udea2", "배", "ship cruise 배"), + ("\ud83d\udeb2", "자전거", "bicycle bike 자전거"), + ("\ud83d\udef5", "스쿠터", "scooter moped 스쿠터"), + ("\ud83c\udfcd\ufe0f", "오토바이", "motorcycle 오토바이"), + ("\ud83c\udfe0", "집", "house home 집"), + ("\ud83c\udfe1", "마당 있는 집", "house garden 집"), + ("\ud83c\udfe2", "빌딩", "office building 빌딩"), + ("\ud83c\udfe3", "우체국", "post office 우체국"), + ("\ud83c\udfe5", "병원", "hospital 병원"), + ("\ud83c\udfe6", "은행", "bank 은행"), + ("\ud83c\udfe8", "호텔", "hotel 호텔"), + ("\ud83c\udfeb", "학교", "school 학교"), + ("\ud83c\udfea", "편의점", "convenience store shop 편의점"), + ("\ud83c\udfec", "백화점", "department store 백화점"), + ("\ud83c\udff0", "성", "castle 성"), + ("⛪", "교회", "church 교회"), + ("\ud83d\udd4c", "모스크", "mosque 모스크"), + ("\ud83d\uddfc", "에펠탑", "eiffel tower paris 파리"), + ("\ud83d\uddfd", "자유의 여신상", "statue of liberty new york 뉴욕"), + ("\ud83c\udfd4\ufe0f", "산", "mountain snow 산"), + ("\ud83c\udf0b", "화산", "volcano 화산"), + ("\ud83d\uddfb", "후지산", "mount fuji japan 후지산"), + ("\ud83c\udfd5\ufe0f", "캠핑", "camping tent 캠핑"), + ("\ud83c\udfd6\ufe0f", "해변", "beach summer 해변 해수욕"), + ("\ud83c\udf0f", "지구", "earth globe asia 지구"), + ("\ud83d\udcaf", "100점", "hundred percent perfect 완벽 100"), + ("\ud83d\udd22", "숫자", "numbers 숫자"), + ("\ud83c\udd97", "OK", "ok button 오케이"), + ("\ud83c\udd99", "업", "up button 업"), + ("\ud83c\udd92", "쿨", "cool button 쿨"), + ("\ud83c\udd95", "새것", "new button 새"), + ("\ud83c\udd93", "무료", "free button 무료"), + ("\ud83c\udd98", "SOS", "sos emergency 긴급 구조"), + ("⚠\ufe0f", "경고", "warning caution 경고 주의"), + ("\ud83d\udeab", "금지", "prohibited no 금지"), + ("✅", "체크", "check mark done 완료 확인"), + ("❌", "엑스", "x cross error 실패 오류"), + ("❓", "물음표", "question mark 물음표"), + ("❗", "느낌표", "exclamation mark 느낌표"), + ("➕", "더하기", "plus add 더하기"), + ("➖", "빼기", "minus subtract 빼기"), + ("➗", "나누기", "divide 나누기"), + ("✖\ufe0f", "곱하기", "multiply times 곱하기"), + ("♾\ufe0f", "무한대", "infinity 무한"), + ("\ud83d\udd01", "반복", "repeat loop 반복"), + ("\ud83d\udd00", "셔플", "shuffle random 랜덤"), + ("▶\ufe0f", "재생", "play 재생"), + ("⏸\ufe0f", "일시정지", "pause 일시정지"), + ("⏹\ufe0f", "정지", "stop 정지"), + ("⏩", "빨리 감기", "fast forward 빨리감기"), + ("⏪", "되감기", "rewind 되감기"), + ("\ud83d\udd14", "알림", "bell notification 알림 벨"), + ("\ud83d\udd15", "알림 끔", "bell off 알림끔"), + ("\ud83d\udd0a", "볼륨 크게", "loud speaker volume up 볼륨"), + ("\ud83d\udd07", "음소거", "muted speaker 음소거"), + ("\ud83d\udce3", "메가폰", "megaphone loud 확성기"), + ("\ud83d\udce2", "스피커", "loudspeaker 스피커"), + ("\ud83d\udcac", "말풍선", "speech bubble chat 대화"), + ("\ud83d\udcad", "생각 말풍선", "thought bubble thinking 생각"), + ("\ud83d\udce7", "이메일", "email mail 이메일 메일"), + ("\ud83d\udce8", "수신 봉투", "incoming envelope 수신"), + ("\ud83d\udce9", "발신 봉투", "envelope outbox 발신"), + ("\ud83d\udcec", "우편함", "mailbox 우편함"), + ("\ud83d\udce6", "택배 박스", "package box parcel 택배 상자"), + ("\ud83c\udf81", "선물", "gift present 선물"), + ("\ud83c\udf80", "리본 묶음", "ribbon bow 리본"), + ("\ud83c\udf8a", "색종이", "confetti 파티 축하"), + ("\ud83c\udf89", "파티 폭죽", "party popper celebrate 파티 축하"), + ("\ud83c\udf88", "풍선", "balloon party 풍선"), + ("\ud83d\udd50", "1시", "one o'clock 1시 시간"), + ("\ud83d\udd52", "3시", "three o'clock 3시 시간"), + ("\ud83d\udd54", "4시", "four o'clock 4시 시간"), + ("⏰", "알람 시계", "alarm clock 알람 시계"), + ("⏱\ufe0f", "스톱워치", "stopwatch timer 스톱워치 타이머"), + ("\ud83d\udcc5", "달력", "calendar date 달력 날짜"), + ("\ud83d\udcc6", "찢는 달력", "tear-off calendar 달력"), + ("\ud83d\udcb0", "돈 가방", "money bag 돈 부자"), + ("\ud83d\udcb3", "신용카드", "credit card payment 카드 결제"), + ("\ud83d\udcb5", "달러", "dollar banknote 달러"), + ("\ud83d\udcb4", "엔화", "yen banknote 엔"), + ("\ud83d\udcb6", "유로", "euro banknote 유로"), + ("\ud83d\udcb7", "파운드", "pound banknote 파운드"), + ("\ud83d\udcca", "막대 그래프", "bar chart graph 그래프"), + ("\ud83d\udcc8", "상승 그래프", "chart increasing trend 상승 트렌드"), + ("\ud83d\udcc9", "하락 그래프", "chart decreasing trend 하락"), + ("\ud83d\udd0d", "돋보기", "magnifying glass search 검색 돋보기"), + ("\ud83d\udd0e", "오른쪽 돋보기", "magnifying glass right search 검색"), + ("\ud83c\udff3\ufe0f", "흰 깃발", "white flag 항복"), + ("\ud83c\udff4", "검은 깃발", "black flag 해적"), + ("\ud83d\udea9", "빨간 삼각기", "triangular flag 경고 깃발"), + ("\ud83c\udfc1", "체크무늬 깃발", "chequered flag finish race 결승"), + ("\ud83c\udf10", "지구본", "globe internet web 인터넷 웹"), + ("⚓", "닻", "anchor 닻"), + ("\ud83c\udfb5", "음표", "music note 음악 음표"), + ("\ud83c\udfb6", "음표들", "musical notes 음악"), + ("\ud83c\udfbc", "악보", "musical score 악보"), + ("\ud83c\udfb9", "피아노", "piano keyboard 피아노"), + ("\ud83c\udfb8", "기타", "guitar 기타"), + ("\ud83e\udd41", "드럼", "drum 드럼"), + ("\ud83e\ude97", "아코디언", "accordion 아코디언"), + ("\ud83c\udfb7", "색소폰", "saxophone 색소폰"), + ("\ud83c\udfba", "트럼펫", "trumpet 트럼펫"), + ("\ud83c\udfbb", "바이올린", "violin 바이올린") + }; + + public string? Prefix => "emoji"; + + public PluginMetadata Metadata => new PluginMetadata("Emoji", "이모지 피커 — emoji 뒤에 이름 입력", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + IEnumerable<(string, string, string)> source; + if (string.IsNullOrWhiteSpace(query)) + { + source = _emojis.Take(30); + } + else + { + string q = query.Trim().ToLowerInvariant(); + source = _emojis.Where(((string Emoji, string Name, string Tags) e) => e.Name.Contains(q, StringComparison.OrdinalIgnoreCase) || e.Tags.Contains(q, StringComparison.OrdinalIgnoreCase) || e.Emoji.Contains(q)).Take(20); + } + List list = source.Select<(string, string, string), LauncherItem>(((string Emoji, string Name, string Tags) e) => new LauncherItem(e.Emoji + " " + e.Name, "Enter로 클립보드에 복사", null, e.Emoji, null, "\ue76e")).ToList(); + if (!list.Any() && !string.IsNullOrWhiteSpace(query)) + { + list.Add(new LauncherItem("검색 결과 없음", "'" + query + "'에 해당하는 이모지가 없습니다", null, null, null, "\ue946")); + } + return Task.FromResult((IEnumerable)list); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is string text) + { + try + { + Clipboard.SetText(text); + } + catch + { + } + } + return Task.CompletedTask; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/EncodeHandler.cs b/.decompiledproj/AxCopilot/Handlers/EncodeHandler.cs new file mode 100644 index 0000000..d29e143 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/EncodeHandler.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using AxCopilot.SDK; + +namespace AxCopilot.Handlers; + +public class EncodeHandler : IActionHandler +{ + public string? Prefix => "encode "; + + public PluginMetadata Metadata => new PluginMetadata("Encoder", "인코딩/해싱 — encode base64/url/hex/md5/sha256 텍스트", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string text = query.Trim(); + if (string.IsNullOrWhiteSpace(text)) + { + return Task.FromResult(HelpItems()); + } + string text2; + if (text.StartsWith("decode ", StringComparison.OrdinalIgnoreCase)) + { + text2 = text; + string rest = text2.Substring(7, text2.Length - 7).Trim(); + return Task.FromResult(HandleDecode(rest)); + } + int num = text.IndexOf(' '); + if (num < 0) + { + return Task.FromResult(HelpItems()); + } + string type = text.Substring(0, num).Trim().ToLowerInvariant(); + text2 = text; + int num2 = num + 1; + string input = text2.Substring(num2, text2.Length - num2); + return Task.FromResult(HandleEncode(type, input)); + } + + private static IEnumerable HandleEncode(string type, string input) + { + try + { + if (1 == 0) + { + } + IEnumerable result; + switch (type) + { + case "base64": + case "b64": + result = SingleResult("Base64 인코딩", Convert.ToBase64String(Encoding.UTF8.GetBytes(input))); + break; + case "url": + result = SingleResult("URL 인코딩", Uri.EscapeDataString(input)); + break; + case "hex": + result = SingleResult("HEX 인코딩", Convert.ToHexString(Encoding.UTF8.GetBytes(input)).ToLowerInvariant()); + break; + case "md5": + result = SingleResult("MD5 해시", MD5Hash(input)); + break; + case "sha1": + result = SingleResult("SHA-1 해시", SHA1Hash(input)); + break; + case "sha256": + result = SingleResult("SHA-256 해시", SHA256Hash(input)); + break; + case "sha512": + result = SingleResult("SHA-512 해시", SHA512Hash(input)); + break; + case "html": + result = SingleResult("HTML 엔티티 인코딩", WebUtility.HtmlEncode(input)); + break; + default: + result = new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("알 수 없는 타입: " + type, "base64, url, hex, md5, sha1, sha256, sha512, html 중 선택", null, null, null, "\ue7ba")); + break; + } + if (1 == 0) + { + } + return result; + } + catch (Exception ex) + { + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("변환 오류", ex.Message, null, null, null, "\uea39")); + } + } + + private static IEnumerable HandleDecode(string rest) + { + int num = rest.IndexOf(' '); + if (num < 0) + { + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("사용법: encode decode <타입> <값>", "타입: base64, url, hex, html", null, null, null, "\ue946")); + } + string text = rest.Substring(0, num).Trim().ToLowerInvariant(); + int num2 = num + 1; + string text2 = rest.Substring(num2, rest.Length - num2); + try + { + if (1 == 0) + { + } + IEnumerable result; + switch (text) + { + case "base64": + case "b64": + result = SingleResult("Base64 디코딩", Encoding.UTF8.GetString(Convert.FromBase64String(text2))); + break; + case "url": + result = SingleResult("URL 디코딩", Uri.UnescapeDataString(text2)); + break; + case "hex": + result = SingleResult("HEX 디코딩", Encoding.UTF8.GetString(Convert.FromHexString(text2))); + break; + case "html": + result = SingleResult("HTML 엔티티 디코딩", WebUtility.HtmlDecode(text2)); + break; + default: + result = new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("디코딩 불가 타입: " + text, "디코딩 지원: base64, url, hex, html", null, null, null, "\ue7ba")); + break; + } + if (1 == 0) + { + } + return result; + } + catch (Exception ex) + { + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("디코딩 오류", ex.Message, null, null, null, "\uea39")); + } + } + + private static IEnumerable SingleResult(string title, string result) + { + string text = ((result.Length > 80) ? (result.Substring(0, 77) + "…") : result); + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem(title, text + " · Enter로 복사", null, result, null, "\ue8cb")); + } + + private static IEnumerable HelpItems() + { + return new _003C_003Ez__ReadOnlyArray(new LauncherItem[7] + { + new LauncherItem("encode base64 <텍스트>", "Base64 인코딩/디코딩", null, null, null, "\ue8cb"), + new LauncherItem("encode url <텍스트>", "URL 퍼센트 인코딩/디코딩", null, null, null, "\ue8cb"), + new LauncherItem("encode hex <텍스트>", "16진수 인코딩/디코딩", null, null, null, "\ue8cb"), + new LauncherItem("encode md5 <텍스트>", "MD5 해시 (단방향)", null, null, null, "\ue8cb"), + new LauncherItem("encode sha256 <텍스트>", "SHA-256 해시 (단방향)", null, null, null, "\ue8cb"), + new LauncherItem("encode html <텍스트>", "HTML 엔티티 인코딩/디코딩", null, null, null, "\ue8cb"), + new LauncherItem("encode decode base64 <값>", "Base64 디코딩 예시", null, null, null, "\ue8cb") + }); + } + + private static string MD5Hash(string input) + { + byte[] inArray = MD5.HashData(Encoding.UTF8.GetBytes(input)); + return Convert.ToHexString(inArray).ToLowerInvariant(); + } + + private static string SHA1Hash(string input) + { + byte[] inArray = SHA1.HashData(Encoding.UTF8.GetBytes(input)); + return Convert.ToHexString(inArray).ToLowerInvariant(); + } + + private static string SHA256Hash(string input) + { + byte[] inArray = SHA256.HashData(Encoding.UTF8.GetBytes(input)); + return Convert.ToHexString(inArray).ToLowerInvariant(); + } + + private static string SHA512Hash(string input) + { + byte[] inArray = SHA512.HashData(Encoding.UTF8.GetBytes(input)); + return Convert.ToHexString(inArray).ToLowerInvariant(); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is string text) + { + try + { + Clipboard.SetText(text); + } + catch + { + } + } + return Task.CompletedTask; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/EnvHandler.cs b/.decompiledproj/AxCopilot/Handlers/EnvHandler.cs new file mode 100644 index 0000000..04ae8c8 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/EnvHandler.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using AxCopilot.SDK; + +namespace AxCopilot.Handlers; + +public class EnvHandler : IActionHandler +{ + private static readonly string[] _priorityKeys = new string[22] + { + "PATH", "JAVA_HOME", "PYTHON_HOME", "NODE_HOME", "GOPATH", "GOROOT", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "TEMP", + "TMP", "COMPUTERNAME", "USERNAME", "USERDOMAIN", "OS", "PROCESSOR_ARCHITECTURE", "SYSTEMROOT", "WINDIR", "PROGRAMFILES", "PROGRAMFILES(X86)", + "COMMONPROGRAMFILES", "NUMBER_OF_PROCESSORS" + }; + + public string? Prefix => "env"; + + public PluginMetadata Metadata => new PluginMetadata("EnvVars", "환경변수 조회 — env 뒤에 변수명 입력", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string q = query.Trim(); + List<(string Key, string Value)> allVars = (from DictionaryEntry e in Environment.GetEnvironmentVariables() + select (Key: e.Key?.ToString() ?? "", Value: e.Value?.ToString() ?? "") into x + where !string.IsNullOrEmpty(x.Key) + select x).ToList(); + IEnumerable<(string, string)> source; + if (string.IsNullOrWhiteSpace(q)) + { + HashSet prioritySet = new HashSet(_priorityKeys, StringComparer.OrdinalIgnoreCase); + IEnumerable<(string, string)> first = from k in _priorityKeys + where allVars.Any(((string Key, string Value) v) => string.Equals(v.Key, k, StringComparison.OrdinalIgnoreCase)) + select allVars.First(((string Key, string Value) v) => string.Equals(v.Key, k, StringComparison.OrdinalIgnoreCase)); + IOrderedEnumerable<(string, string)> second = from v in allVars + where !prioritySet.Contains(v.Key) + orderby v.Key + select v; + source = first.Concat(second).Take(20); + } + else + { + source = (from v in allVars + where v.Key.Contains(q, StringComparison.OrdinalIgnoreCase) || v.Value.Contains(q, StringComparison.OrdinalIgnoreCase) + orderby (!v.Key.StartsWith(q, StringComparison.OrdinalIgnoreCase)) ? 1 : 0, v.Key + select v).Take(20); + } + List list = source.Select<(string, string), LauncherItem>(delegate((string Key, string Value) v) + { + string text = ((v.Value.Length > 80) ? (v.Value.Substring(0, 77) + "…") : v.Value); + if (v.Key.Equals("PATH", StringComparison.OrdinalIgnoreCase) && v.Value.Contains(';')) + { + text = v.Value.Split(';')[0] + $" (외 {v.Value.Split(';').Length - 1}개)"; + } + return new LauncherItem(v.Key, text + " · Enter로 값 복사", null, v.Value, null, "\ue8d7"); + }).ToList(); + if (!list.Any()) + { + list.Add(new LauncherItem("'" + q + "' — 환경변수 없음", "해당 이름의 환경변수를 찾을 수 없습니다", null, null, null, "\ue7ba")); + } + return Task.FromResult((IEnumerable)list); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is string text) + { + try + { + Clipboard.SetText(text); + } + catch + { + } + } + return Task.CompletedTask; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/EverythingHandler.cs b/.decompiledproj/AxCopilot/Handlers/EverythingHandler.cs new file mode 100644 index 0000000..18b21de --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/EverythingHandler.cs @@ -0,0 +1,289 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class EverythingHandler : IActionHandler +{ + private const int EVERYTHING_OK = 0; + + private const int EVERYTHING_REQUEST_FILE_NAME = 1; + + private const int EVERYTHING_REQUEST_PATH = 2; + + private const int EVERYTHING_REQUEST_SIZE = 16; + + private bool? _isAvailable; + + public string? Prefix => "es"; + + public PluginMetadata Metadata => new PluginMetadata("EverythingSearch", "Everything 초고속 파일 검색 — es [키워드]", "1.0", "AX"); + + private bool IsAvailable + { + get + { + if (_isAvailable.HasValue) + { + return _isAvailable.Value; + } + try + { + Everything_GetMajorVersion(); + _isAvailable = true; + } + catch + { + _isAvailable = false; + } + return _isAvailable.Value; + } + } + + [DllImport("Everything64.dll", CharSet = CharSet.Unicode)] + private static extern uint Everything_SetSearchW(string lpSearchString); + + [DllImport("Everything64.dll")] + private static extern void Everything_SetMax(uint dwMax); + + [DllImport("Everything64.dll")] + private static extern void Everything_SetRequestFlags(uint dwRequestFlags); + + [DllImport("Everything64.dll")] + private static extern bool Everything_QueryW(bool bWait); + + [DllImport("Everything64.dll")] + private static extern uint Everything_GetNumResults(); + + [DllImport("Everything64.dll")] + private static extern uint Everything_GetLastError(); + + [DllImport("Everything64.dll", CharSet = CharSet.Unicode)] + private static extern nint Everything_GetResultFullPathNameW(uint nIndex, nint lpString, uint nMaxCount); + + [DllImport("Everything64.dll", CharSet = CharSet.Unicode)] + private static extern void Everything_GetResultSize(uint nIndex, out long lpFileSize); + + [DllImport("Everything64.dll")] + private static extern uint Everything_GetMajorVersion(); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + if (!IsAvailable) + { + return Task.FromResult((IEnumerable)new LauncherItem[1] + { + new LauncherItem("Everything이 설치되어 있지 않습니다", "voidtools.com에서 Everything을 설치하면 초고속 파일 검색을 사용할 수 있습니다", null, null, null, "\ue7ba") + }); + } + string text = query.Trim(); + if (string.IsNullOrWhiteSpace(text)) + { + return Task.FromResult((IEnumerable)new LauncherItem[1] + { + new LauncherItem("Everything 파일 검색", "검색어를 입력하세요 — 파일명, 확장자(*.xlsx), 경로 일부 등", null, null, null, "\ue721") + }); + } + try + { + Everything_SetSearchW(text); + Everything_SetMax(30u); + Everything_SetRequestFlags(19u); + if (!Everything_QueryW(bWait: true)) + { + return Task.FromResult((IEnumerable)new LauncherItem[1] + { + new LauncherItem("Everything 검색 실패", $"오류 코드: {Everything_GetLastError()} — Everything 서비스가 실행 중인지 확인하세요", null, null, null, "\ue7ba") + }); + } + uint num = Everything_GetNumResults(); + if (num == 0) + { + return Task.FromResult((IEnumerable)new LauncherItem[1] + { + new LauncherItem("검색 결과 없음: " + text, "다른 키워드나 와일드카드(*.xlsx)를 시도해 보세요", null, null, null, "\ue946") + }); + } + List list = new List(); + nint num2 = Marshal.AllocHGlobal(1040); + try + { + for (uint num3 = 0u; num3 < num && num3 < 30; num3++) + { + Everything_GetResultFullPathNameW(num3, num2, 520u); + string text2 = Marshal.PtrToStringUni(num2) ?? ""; + Everything_GetResultSize(num3, out var lpFileSize); + string fileName = Path.GetFileName(text2); + string text3 = Path.GetDirectoryName(text2) ?? ""; + string text4 = ((lpFileSize > 0) ? FormatSize(lpFileSize) : ""); + string subtitle = (string.IsNullOrEmpty(text4) ? text3 : (text4 + " · " + text3)); + string symbol = (Directory.Exists(text2) ? "\ue8b7" : GetFileSymbol(text2)); + list.Add(new LauncherItem(fileName, subtitle, null, text2, null, symbol)); + } + } + finally + { + Marshal.FreeHGlobal(num2); + } + return Task.FromResult((IEnumerable)list); + } + catch (Exception ex) + { + LogService.Warn("Everything 검색 오류: " + ex.Message); + return Task.FromResult((IEnumerable)new LauncherItem[1] + { + new LauncherItem("Everything 검색 오류", ex.Message, null, null, null, "\ue7ba") + }); + } + } + + public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + string path = data as string; + if (path == null || string.IsNullOrEmpty(path)) + { + return; + } + try + { + if (Directory.Exists(path)) + { + Process.Start(new ProcessStartInfo + { + FileName = path, + UseShellExecute = true + }); + } + else if (File.Exists(path)) + { + Process.Start(new ProcessStartInfo + { + FileName = path, + UseShellExecute = true + }); + } + } + catch (Exception ex) + { + Exception ex2 = ex; + LogService.Warn("Everything 결과 열기 실패: " + ex2.Message); + } + await Task.CompletedTask; + } + + private static string FormatSize(long bytes) + { + if (bytes >= 1024) + { + if (bytes >= 1048576) + { + if (bytes >= 1073741824) + { + return $"{(double)bytes / 1073741824.0:F2} GB"; + } + return $"{(double)bytes / 1048576.0:F1} MB"; + } + return $"{(double)bytes / 1024.0:F1} KB"; + } + return $"{bytes} B"; + } + + private static string GetFileSymbol(string path) + { + string text = Path.GetExtension(path).ToLowerInvariant(); + if (1 == 0) + { + } + string result; + switch (text) + { + case ".xlsx": + case ".xls": + case ".csv": + result = "\ue9f9"; + break; + case ".docx": + case ".doc": + result = "\ue8a5"; + break; + case ".pptx": + case ".ppt": + result = "\uee71"; + break; + case ".pdf": + result = "\uea90"; + break; + case ".png": + case ".jpg": + case ".jpeg": + case ".gif": + case ".bmp": + case ".svg": + result = "\ueb9f"; + break; + case ".mp4": + case ".avi": + case ".mkv": + case ".mov": + result = "\ue714"; + break; + case ".mp3": + case ".wav": + case ".flac": + result = "\uec4f"; + break; + case ".zip": + case ".rar": + case ".7z": + case ".tar": + case ".gz": + result = "\ue8c6"; + break; + case ".exe": + case ".msi": + result = "\uecaa"; + break; + case ".cs": + case ".py": + case ".js": + case ".ts": + case ".java": + case ".cpp": + case ".c": + case ".go": + result = "\ue943"; + break; + case ".json": + case ".xml": + case ".yaml": + case ".yml": + result = "\ue713"; + break; + case ".txt": + case ".md": + case ".log": + result = "\ue8d2"; + break; + case ".html": + case ".htm": + case ".css": + result = "\ue774"; + break; + default: + result = "\ue8a5"; + break; + } + if (1 == 0) + { + } + return result; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/FavoriteHandler.cs b/.decompiledproj/AxCopilot/Handlers/FavoriteHandler.cs new file mode 100644 index 0000000..0bf1fe6 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/FavoriteHandler.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class FavoriteHandler : IActionHandler +{ + private class FavEntry + { + [JsonPropertyName("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("path")] + public string Path { get; set; } = ""; + } + + private static readonly string FavFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "favorites.json"); + + private static readonly JsonSerializerOptions JsonOpts = new JsonSerializerOptions + { + WriteIndented = true, + PropertyNameCaseInsensitive = true + }; + + private List _cache = new List(); + + private bool _loaded; + + public string? Prefix => "fav"; + + public PluginMetadata Metadata => new PluginMetadata("Favorite", "즐겨찾기 관리 — fav", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + EnsureLoaded(); + string q = query.Trim(); + if (q.StartsWith("add ", StringComparison.OrdinalIgnoreCase)) + { + string text = q; + string text2 = text.Substring(4, text.Length - 4).Trim(); + int num = text2.IndexOf(' '); + if (num > 0) + { + string text3 = text2.Substring(0, num).Trim(); + text = text2; + int num2 = num + 1; + string text4 = text.Substring(num2, text.Length - num2).Trim(); + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("즐겨찾기 추가: " + text3, "경로: " + text4 + " · Enter로 추가", null, ValueTuple.Create("__ADD__", text3, text4), null, "\ue74e"))); + } + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("사용법: fav add [이름] [경로]", "예: fav add 보고서 C:\\work\\report.xlsx", null, null, null, "\ue946"))); + } + if (q.StartsWith("del ", StringComparison.OrdinalIgnoreCase)) + { + string text = q; + string name = text.Substring(4, text.Length - 4).Trim(); + FavEntry favEntry = _cache.FirstOrDefault((FavEntry b) => b.Name.Contains(name, StringComparison.OrdinalIgnoreCase)); + if (favEntry != null) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("즐겨찾기 삭제: " + favEntry.Name, "경로: " + favEntry.Path + " · Enter로 삭제", null, ValueTuple.Create("__DEL__", favEntry.Name), null, "\ue74d"))); + } + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("'" + name + "'에 해당하는 즐겨찾기 없음", "fav으로 전체 목록 확인", null, null, null, "\ue7ba"))); + } + List source = (string.IsNullOrWhiteSpace(q) ? _cache : _cache.Where((FavEntry b) => b.Name.Contains(q, StringComparison.OrdinalIgnoreCase) || b.Path.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList()); + if (!source.Any()) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem((_cache.Count == 0) ? "즐겨찾기가 없습니다" : ("'" + q + "'에 해당하는 즐겨찾기 없음"), "fav add [이름] [경로]로 추가하세요", null, null, null, "\ue946"))); + } + List result = source.Select(delegate(FavEntry b) + { + bool flag = Directory.Exists(b.Path); + bool flag2 = File.Exists(b.Path); + string symbol = (flag ? "\ue8b7" : (flag2 ? "\ue8a5" : "\ue7ba")); + string text5 = (flag ? "폴더 열기" : (flag2 ? "파일 열기" : "경로를 찾을 수 없음")); + return new LauncherItem(b.Name, b.Path + " · " + text5, null, b.Path, null, symbol); + }).ToList(); + return Task.FromResult((IEnumerable)result); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is (string, string, string) { Item1: "__ADD__" } tuple) + { + AddFav(tuple.Item2, tuple.Item3); + NotificationService.Notify("AX Copilot", "즐겨찾기 추가: " + tuple.Item2); + return Task.CompletedTask; + } + if (item.Data is (string, string) { Item1: "__DEL__" } tuple2) + { + RemoveFav(tuple2.Item2); + NotificationService.Notify("AX Copilot", "즐겨찾기 삭제: " + tuple2.Item2); + return Task.CompletedTask; + } + object data = item.Data; + string path = data as string; + if (path != null) + { + if (Directory.Exists(path) || File.Exists(path)) + { + try + { + Process.Start(new ProcessStartInfo(path) + { + UseShellExecute = true + }); + } + catch (Exception ex) + { + LogService.Warn("즐겨찾기 열기 실패: " + ex.Message); + } + } + else + { + try + { + Application current = Application.Current; + if (current != null) + { + ((DispatcherObject)current).Dispatcher.Invoke((Action)delegate + { + Clipboard.SetText(path); + }); + } + } + catch + { + } + } + } + return Task.CompletedTask; + } + + private void EnsureLoaded() + { + if (_loaded) + { + return; + } + _loaded = true; + try + { + if (File.Exists(FavFile)) + { + _cache = JsonSerializer.Deserialize>(File.ReadAllText(FavFile), JsonOpts) ?? new List(); + } + } + catch (Exception ex) + { + LogService.Warn("즐겨찾기 로드 실패: " + ex.Message); + } + } + + private void AddFav(string name, string path) + { + EnsureLoaded(); + _cache.RemoveAll((FavEntry b) => b.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + _cache.Insert(0, new FavEntry + { + Name = name, + Path = path + }); + Save(); + } + + private void RemoveFav(string name) + { + EnsureLoaded(); + _cache.RemoveAll((FavEntry b) => b.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + Save(); + } + + private void Save() + { + try + { + Directory.CreateDirectory(Path.GetDirectoryName(FavFile)); + File.WriteAllText(FavFile, JsonSerializer.Serialize(_cache, JsonOpts)); + } + catch (Exception ex) + { + LogService.Warn("즐겨찾기 저장 실패: " + ex.Message); + } + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/FolderAliasHandler.cs b/.decompiledproj/AxCopilot/Handlers/FolderAliasHandler.cs new file mode 100644 index 0000000..fe2f370 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/FolderAliasHandler.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.Models; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class FolderAliasHandler : IActionHandler +{ + private readonly SettingsService _settings; + + public string? Prefix => "cd"; + + public PluginMetadata Metadata => new PluginMetadata("folder-alias", "폴더 별칭", "1.0", "AX"); + + public FolderAliasHandler(SettingsService settings) + { + _settings = settings; + } + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + IEnumerable result = from a in _settings.Settings.Aliases + where a.Type == "folder" && (string.IsNullOrEmpty(query) || a.Key.Contains(query, StringComparison.OrdinalIgnoreCase)) + select new LauncherItem(a.Key, Environment.ExpandEnvironmentVariables(a.Target), null, a, null, "\ue8b7"); + return Task.FromResult(result); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is AliasEntry aliasEntry) + { + string arguments = Environment.ExpandEnvironmentVariables(aliasEntry.Target); + Process.Start(new ProcessStartInfo("explorer.exe", arguments) + { + UseShellExecute = true + }); + } + return Task.CompletedTask; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/HelpHandler.cs b/.decompiledproj/AxCopilot/Handlers/HelpHandler.cs new file mode 100644 index 0000000..d4d1f57 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/HelpHandler.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; +using System.Windows.Threading; +using AxCopilot.SDK; +using AxCopilot.Services; +using AxCopilot.Views; + +namespace AxCopilot.Handlers; + +public class HelpHandler : IActionHandler +{ + private record HelpEntry(string Category, string Command, string Title, string Description, string Example, string Symbol, string ColorHex); + + private readonly SettingsService? _settings; + + private static readonly HelpEntry[] _baseEntries = new HelpEntry[47] + { + new HelpEntry("검색", "", "앱 · 파일 · 북마크 퍼지 검색", "앱 이름, 파일명, 한국어 초성, Chrome·Edge 북마크를 통합 검색", "chrome / 보고서 / ㅅㄷ (설정) / 북마크 제목", "\ue721", "#0078D4"), + new HelpEntry("계산", "=", "계산기", "수식 계산 · 단위 변환 · 실시간 환율", "= sqrt(144) / = 100 USD to KRW / = 15km to miles", "\ue8ef", "#4B5EFC"), + new HelpEntry("검색", "?", "웹 검색 (10개 엔진)", "?n 네이버 · ?g 구글 · ?y 유튜브 · ?gh 깃허브 · ?d DuckDuckGo · ?w 위키피디아 · ?nw 나무위키 · ?nm 네이버지도 · ?ni 네이버이미지 · ?gi 구글이미지", "? 오늘 날씨 / ?n 뉴스 / ?g python / ?nw 검색어 / ?y 음악 / ?gh axios", "\ue774", "#006EAF"), + new HelpEntry("클립보드", "#", "클립보드 히스토리", "복사 이력 검색 & 재사용 · Shift+↑↓로 여러 항목 병합", "# / # 회의록 / (Shift+↑↓ 선택 후 Shift+Enter 병합)", "\ue81c", "#B7791F"), + new HelpEntry("클립보드", "$", "클립보드 텍스트 변환 (12종)", "현재 클립보드 내용을 즉시 변환", "$json / $b64e / $upper / $url / $md5 / $trim", "\ue77f", "#8764B8"), + new HelpEntry("텍스트", ";", "텍스트 스니펫", "저장된 템플릿 불러오기 · 변수 치환 지원", ";addr / ;sig / ;greet", "\ue70b", "#0F6CBD"), + new HelpEntry("단축키", "@", "URL 단축키", "저장해둔 URL을 키워드로 바로 열기", "@gh / @notion / @jira", "\ue774", "#0078D4"), + new HelpEntry("단축키", "cd", "폴더 단축키", "저장해둔 폴더를 키워드로 바로 열기", "cd dl / cd work / cd desktop", "\ue8b7", "#107C10"), + new HelpEntry("단축키", ">", "터미널 명령 실행", "PowerShell 명령을 AX Commander에서 직접 실행", "> git status / > ipconfig / > cls", "\ue756", "#323130"), + new HelpEntry("창관리", "~", "워크스페이스 저장·복원", "현재 창 배치를 스냅샷으로 저장하고 언제든 복원", "~save 업무 / ~restore 업무 / ~list", "\ue8a1", "#C50F1F"), + new HelpEntry("창관리", "snap", "창 분할 레이아웃", "창을 화면의 특정 영역에 즉시 스냅", "snap left / snap right / snap tl / snap full", "\ue8a0", "#B45309"), + new HelpEntry("창관리", "cap", "스크린샷 캡처 (예약어 변경 가능)", "영역 선택 · 활성 창 · 스크롤 · 전체 화면. Shift+Enter로 지연 캡처(3/5/10초 타이머). 결과는 클립보드에 복사. 글로벌 단축키(PrintScreen 등)로 바로 캡처 가능. 설정 → 캡처 탭에서 예약어·단축키·스크롤 속도 변경", "cap region / cap window / cap scroll / cap screen / Shift+Enter → 지연 캡처", "\ue722", "#BE185D"), + new HelpEntry("시스템", "/", "시스템 명령", "잠금·절전·재시작·종료·타이머·알람", "/lock / /sleep / /shutdown / /timer 5m / /alarm 14:30", "\ue7e8", "#4A4A4A"), + new HelpEntry("시스템", "info · *", "시스템 정보", "IP · 배터리 · 볼륨 · 가동시간 · CPU · 디스크. `*` 입력으로도 동일하게 사용 가능", "info / info ip / info battery / * / * ip", "\ue7f4", "#5B4E7E"), + new HelpEntry("알림", "", "잠금 해제 사용시간 알림", "PC 잠금 해제 시 오늘 누적 사용 시간과 격려 문구·명언 팝업 표시. 설정 → 알림 탭에서 활성화", "설정 → 알림 탭: 활성화 토글 / 표시 위치(4방향) / 표시 간격(30분~4시간) / 자동 닫힘(5초~3분)", "\uea8f", "#EA8F00"), + new HelpEntry("시스템", "kill", "프로세스 종료", "프로세스 이름으로 검색 후 강제 종료", "kill chrome / kill node / kill teams", "\uea39", "#CC2222"), + new HelpEntry("시스템", "media", "미디어 제어", "재생·일시정지·이전·다음·볼륨 조절", "media play / media next / media vol+ / media mute", "\ue768", "#1A6B3C"), + new HelpEntry("개발", "json", "JSON 포맷 · 검증", "클립보드의 JSON을 정렬·압축·유효성 검사", "json → format / minify / validate", "\ue930", "#D97706"), + new HelpEntry("개발", "encode", "인코딩 · 해싱", "Base64 · URL · HTML · UTF-8 · MD5 · SHA256", "encode base64 / encode url / encode sha256", "\ue8cb", "#6366F1"), + new HelpEntry("개발", "color", "색상 변환", "HEX ↔ RGB ↔ HSL ↔ HSV 변환 및 색상명 지원", "color #FF5733 / color 255,87,51 / color red", "\ue771", "#EC4899"), + new HelpEntry("개발", "port", "포트 · 프로세스 조회", "포트 번호로 점유 프로세스 확인", "port 3000 / port 8080 / port 443", "\ue968", "#006699"), + new HelpEntry("개발", "env", "환경변수 조회", "시스템 환경변수 검색 및 클립보드 복사", "env / env PATH / env JAVA", "\ue8d7", "#0D9488"), + new HelpEntry("앱", "emoji", "이모지 피커", "300개+ 이모지 검색 · Enter로 클립보드 복사", "emoji / emoji 하트 / emoji wave / emoji 불꽃", "\ue76e", "#F59E0B"), + new HelpEntry("앱", "recent", "최근 파일", "Windows 최근 파일 목록 검색 & 바로 열기", "recent / recent 보고서 / recent xlsx", "\ue81c", "#059669"), + new HelpEntry("앱", "note", "빠른 메모", "간단한 메모를 저장하고 불러오기", "note 내일 회의 10시 / note", "\ue70b", "#7C3AED"), + new HelpEntry("앱", "uninstall", "앱 제거", "설치된 앱 검색 후 제거", "uninstall / uninstall kakao / uninstall zoom", "\ue74d", "#DC2626"), + new HelpEntry("유틸", "pick", "스포이드 색상 추출", "화면 아무 곳을 클릭하여 HEX 색상 코드 추출 · 돋보기로 실시간 미리보기 · 결과 반투명 창 5초 표시", "pick → 스포이드 모드 진입 → 클릭으로 색상 추출", "\ue771", "#EC4899"), + new HelpEntry("유틸", "date", "날짜 계산 · D-day · 타임스탬프", "날짜 가감(+30d), D-day 계산, Unix ↔ 날짜 변환, 요일·ISO 주차 조회", "date / date +30d / date 2026-12-25 / date 1711584000 / date unix", "\ue787", "#0EA5E9"), + new HelpEntry("시스템", "svc", "서비스 관리", "Windows 서비스 검색·시작·중지·재시작 + AX 클립보드 서비스 강제 재시작", "svc / svc spooler / svc restart clipboard", "\ue912", "#6366F1"), + new HelpEntry("유틸", "pipe", "클립보드 파이프라인", "변환을 > 로 체이닝: 대문자→공백제거→Base64 등 19종 필터 한 번에 적용", "pipe upper > trim > b64e / pipe sort > unique > number", "\ue8c8", "#8B5CF6"), + new HelpEntry("유틸", "journal", "업무 일지 자동 생성", "오늘 사용한 앱·명령어·활성 시간을 마크다운 요약으로 자동 생성. 스탠드업/일일 보고에 바로 사용", "journal / journal 2026-03-25", "\ue70b", "#0EA5E9"), + new HelpEntry("유틸", "routine", "루틴 자동화", "등록된 루틴(앱·폴더·URL 조합)을 한 번에 순서대로 실행. 출근/퇴근/회의 전 세팅", "routine / routine morning / routine endofday", "\ue82f", "#F59E0B"), + new HelpEntry("유틸", "batch", "텍스트 일괄 처리", "클립보드 각 줄에 동시 적용: 접두사·접미사·줄번호·정렬·중복제거·따옴표·치환·CSV 변환", "batch number / batch prefix >> / batch sort / batch replace A B", "\ue8c6", "#10B981"), + new HelpEntry("유틸", "diff", "텍스트/파일 비교", "클립보드 최근 2개 텍스트 또는 파일 2개를 줄 단위 비교. 추가·삭제·동일 줄 하이라이트", "diff / diff C:\\a.txt C:\\b.txt", "\ue89a", "#EF4444"), + new HelpEntry("유틸", "win", "윈도우 포커스 스위처", "열린 창을 타이틀·프로세스명으로 검색하여 Alt+Tab 없이 즉시 전환", "win / win chrome / win 보고서", "\ue8a7", "#6366F1"), + new HelpEntry("시스템", "^", "Windows 실행 명령", "Win+R 실행 창과 동일하게 명령어 실행. notepad, calc, cmd, control, mstsc 등 모든 Windows 실행 명령 지원", "^ notepad / ^ cmd / ^ calc / ^ control / ^ mspaint", "\ue7c4", "#E08850"), + new HelpEntry("유틸", "stats", "텍스트 통계 분석", "클립보드 텍스트 글자·단어·줄 수, 키워드 빈도, 읽기 시간 추정", "stats / stats 키워드 (클립보드 텍스트 분석)", "\ue8d2", "#6366F1"), + new HelpEntry("유틸", "fav", "즐겨찾기 (파일·폴더)", "자주 쓰는 경로를 등록하고 빠르게 열기", "fav / fav add 보고서 C:\\work\\report.xlsx / fav del 보고서", "\ue728", "#F59E0B"), + new HelpEntry("유틸", "rename", "파일 일괄 이름변경", "폴더 내 파일을 패턴·변수로 일괄 이름변경. {n}순번 {date}날짜 {orig}원본명", "rename C:\\work\\*.xlsx 보고서_{n}", "\ue8ac", "#8B5CF6"), + new HelpEntry("유틸", "monitor", "시스템 리소스 모니터", "CPU·메모리·디스크·프로세스 실시간 현황 조회", "monitor / monitor cpu / monitor mem / monitor disk", "\ue7f4", "#10B981"), + new HelpEntry("유틸", "scaffold", "프로젝트 스캐폴딩", "내장/사용자 템플릿으로 프로젝트 폴더 구조 일괄 생성", "scaffold / scaffold webapi / scaffold C:\\new-project webapi", "\ue8f1", "#0EA5E9"), + new HelpEntry("키보드", "Alt+Space", "AX Commander 열기 / 닫기", "어디서든 AX Commander를 토글", "설정 → 핫키에서 변경 가능", "\ue946", "#6B7280"), + new HelpEntry("키보드", "Enter", "실행", "선택된 항목 실행", "", "\ue946", "#6B7280"), + new HelpEntry("키보드", "Shift+Enter", "Large Type / 병합 실행 / 지연 캡처", "텍스트를 전체화면으로 표시 · 클립보드 항목 병합 · 캡처 모드에서는 지연 캡처(3초/5초/10초) 타이머 선택", "(#모드) Shift+↑↓ 선택 후 Shift+Enter 병합 / (cap모드) Shift+Enter → 타이머 선택", "\ue946", "#6B7280"), + new HelpEntry("키보드", "Tab", "자동완성", "선택 항목으로 입력 자동완성", "", "\ue946", "#6B7280"), + new HelpEntry("키보드", "→ (커서 끝)", "파일 액션 메뉴", "앱·파일 선택 후 → 키: 경로복사 · 탐색기 · 관리자 · 터미널", "", "\ue946", "#6B7280"), + new HelpEntry("키보드", "Ctrl+,", "설정 열기", "AX Copilot 설정 창", "", "\ue946", "#6B7280") + }; + + public string? Prefix => "help"; + + public PluginMetadata Metadata => new PluginMetadata("Help", "AX Commander 사용 설명서", "1.0", "AX"); + + public HelpHandler(SettingsService? settings = null) + { + _settings = settings; + } + + private HelpEntry[] GetEntries() + { + string text = _settings?.Settings.ScreenCapture.Prefix ?? "cap"; + if (string.IsNullOrWhiteSpace(text)) + { + text = "cap"; + } + string command = _settings?.Settings.Hotkey ?? "Alt+Space"; + HelpEntry[] array = (HelpEntry[])_baseEntries.Clone(); + for (int i = 0; i < array.Length; i++) + { + if (array[i].Title == "스크린샷 캡처 (예약어 변경 가능)") + { + array[i] = array[i]with + { + Command = text, + Example = $"{text} screen / {text} window / {text} region / {text} scroll / (설정 → 캡처 탭)" + }; + } + if (array[i].Title == "AX Commander 열기 / 닫기") + { + array[i] = array[i]with + { + Command = command, + Example = "설정 → 핫키에서 변경 가능" + }; + } + } + return array; + } + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string q = query.Trim(); + HelpEntry[] entries = GetEntries(); + if (string.IsNullOrEmpty(q)) + { + int value = entries.Count((HelpEntry e) => e.Category != "키보드"); + int value2 = entries.Count((HelpEntry e) => e.Category == "키보드"); + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("AX Commander — 전체 기능 목록 보기", $"총 {value}개 명령어 · {value2}개 단축키 · Enter → 기능 설명 창 열기", null, "__HELP_OVERVIEW__", null, "\ue946"))); + } + IEnumerable source = entries.Where((HelpEntry e) => e.Category.Contains(q, StringComparison.OrdinalIgnoreCase) || e.Command.Contains(q, StringComparison.OrdinalIgnoreCase) || e.Title.Contains(q, StringComparison.OrdinalIgnoreCase) || e.Description.Contains(q, StringComparison.OrdinalIgnoreCase)); + List list = source.Select((HelpEntry e) => new LauncherItem(FormatTitle(e), FormatSubtitle(e), null, e.Example, null, e.Symbol)).ToList(); + if (list.Count == 0) + { + list.Add(new LauncherItem("'" + q + "'에 해당하는 명령어가 없습니다", "help만 입력하면 전체 기능 창을 열 수 있어요", null, null, null, "\ue946")); + } + return Task.FromResult((IEnumerable)list); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + string s = data as string; + if (s == null) + { + return Task.CompletedTask; + } + if (s == "__HELP_OVERVIEW__") + { + ((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate + { + HelpEntry[] entries = GetEntries(); + IEnumerable items = entries.Select((HelpEntry e) => new HelpItemModel + { + Category = e.Category, + Command = (string.IsNullOrEmpty(e.Command) ? "(퍼지 검색)" : e.Command), + Title = e.Title, + Description = e.Description, + Example = e.Example, + Symbol = e.Symbol, + ColorBrush = ParseColor(e.ColorHex) + }); + string globalHotkey = _settings?.Settings.Hotkey ?? "Alt+Space"; + new HelpDetailWindow(items, entries.Count((HelpEntry e) => e.Category != "키보드"), globalHotkey).Show(); + }); + return Task.CompletedTask; + } + if (!string.IsNullOrWhiteSpace(s)) + { + try + { + ((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate + { + Clipboard.SetText(s); + }); + } + catch + { + } + } + return Task.CompletedTask; + } + + private static SolidColorBrush ParseColor(string hex) + { + try + { + Color color = (Color)ColorConverter.ConvertFromString(hex); + return new SolidColorBrush(color); + } + catch + { + return new SolidColorBrush(Colors.Gray); + } + } + + private static string FormatTitle(HelpEntry e) + { + string text = (string.IsNullOrEmpty(e.Command) ? ("[" + e.Category + "]") : ("[" + e.Category + "] " + e.Command)); + return text + " — " + e.Title; + } + + private static string FormatSubtitle(HelpEntry e) + { + List list = new List { e.Description }; + if (!string.IsNullOrEmpty(e.Example)) + { + list.Add("예) " + e.Example); + } + return string.Join(" · ", list); + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/InstalledApp.cs b/.decompiledproj/AxCopilot/Handlers/InstalledApp.cs new file mode 100644 index 0000000..52b08f7 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/InstalledApp.cs @@ -0,0 +1,14 @@ +namespace AxCopilot.Handlers; + +internal sealed class InstalledApp +{ + public string Name { get; init; } = ""; + + public string? UninstallString { get; init; } + + public string? Publisher { get; init; } + + public string? Version { get; init; } + + public string? InstallDate { get; init; } +} diff --git a/.decompiledproj/AxCopilot/Handlers/JournalHandler.cs b/.decompiledproj/AxCopilot/Handlers/JournalHandler.cs new file mode 100644 index 0000000..30ef0cf --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/JournalHandler.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using AxCopilot.Models; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class JournalHandler : IActionHandler +{ + public string? Prefix => "journal"; + + public PluginMetadata Metadata => new PluginMetadata("Journal", "업무 일지 자동 생성 — journal", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string text = query.Trim(); + DateTime targetDate; + if (string.IsNullOrWhiteSpace(text)) + { + targetDate = DateTime.Today; + } + else + { + if (!DateTime.TryParse(text, out var result)) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("날짜 형식 오류", "예: journal 2026-03-25 또는 journal (오늘)", null, null, null, "\ue7ba"))); + } + targetDate = result.Date; + } + int days = (DateTime.Today - targetDate).Days; + List stats = UsageStatisticsService.GetStats(Math.Max(days + 1, 1)); + DailyUsageStats dailyUsageStats = stats.FirstOrDefault((DailyUsageStats s) => s.Date == targetDate.ToString("yyyy-MM-dd")); + List list = new List(); + if (dailyUsageStats == null) + { + list.Add(new LauncherItem($"{targetDate:yyyy-MM-dd} — 기록 없음", "해당 날짜의 사용 기록이 없습니다", null, null, null, "\ue946")); + return Task.FromResult((IEnumerable)list); + } + double value = (double)dailyUsageStats.ActiveSeconds / 3600.0; + StringBuilder stringBuilder = new StringBuilder(); + StringBuilder stringBuilder2 = stringBuilder; + StringBuilder stringBuilder3 = stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(14, 2, stringBuilder2); + handler.AppendLiteral("## 업무 일지 — "); + handler.AppendFormatted(targetDate, "yyyy-MM-dd"); + handler.AppendLiteral(" ("); + handler.AppendFormatted(targetDate, "dddd"); + handler.AppendLiteral(")"); + stringBuilder3.AppendLine(ref handler); + stringBuilder.AppendLine(); + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder4 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(18, 1, stringBuilder2); + handler.AppendLiteral("- **PC 활성 시간**: "); + handler.AppendFormatted(value, "F1"); + handler.AppendLiteral("시간"); + stringBuilder4.AppendLine(ref handler); + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder5 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(14, 1, stringBuilder2); + handler.AppendLiteral("- **런처 호출**: "); + handler.AppendFormatted(dailyUsageStats.LauncherOpens); + handler.AppendLiteral("회"); + stringBuilder5.AppendLine(ref handler); + if (dailyUsageStats.CommandUsage.Count > 0) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine("### 사용한 명령어"); + foreach (KeyValuePair item in dailyUsageStats.CommandUsage.OrderByDescending, int>((KeyValuePair x) => x.Value).Take(10)) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder6 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(8, 2, stringBuilder2); + handler.AppendLiteral("- `"); + handler.AppendFormatted(item.Key); + handler.AppendLiteral("` — "); + handler.AppendFormatted(item.Value); + handler.AppendLiteral("회"); + stringBuilder6.AppendLine(ref handler); + } + } + stringBuilder.AppendLine(); + stringBuilder.AppendLine("---"); + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder7 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(21, 1, stringBuilder2); + handler.AppendLiteral("*AX Copilot 자동 생성 · "); + handler.AppendFormatted(DateTime.Now, "HH:mm"); + handler.AppendLiteral("*"); + stringBuilder7.AppendLine(ref handler); + string data = stringBuilder.ToString(); + IEnumerable values = from x in dailyUsageStats.CommandUsage.OrderByDescending, int>((KeyValuePair x) => x.Value).Take(3) + select x.Key; + string value2 = ((dailyUsageStats.CommandUsage.Count > 0) ? ("주요 명령: " + string.Join(", ", values)) : "명령어 사용 기록 없음"); + list.Add(new LauncherItem($"{targetDate:yyyy-MM-dd} 업무 일지 — 클립보드로 복사", $"활성 {value:F1}h · 런처 {dailyUsageStats.LauncherOpens}회 · {value2}", null, data, null, "\ue70b")); + list.Add(new LauncherItem($"PC 활성 시간: {value:F1}시간", "잠금 해제 시간 기준 누적", null, $"PC 활성 시간: {value:F1}시간", null, "\ue823")); + list.Add(new LauncherItem($"런처 호출: {dailyUsageStats.LauncherOpens}회", "Alt+Space 또는 트레이 클릭", null, $"런처 호출: {dailyUsageStats.LauncherOpens}회", null, "\ue721")); + if (dailyUsageStats.CommandUsage.Count > 0) + { + foreach (KeyValuePair item2 in dailyUsageStats.CommandUsage.OrderByDescending, int>((KeyValuePair x) => x.Value).Take(5)) + { + list.Add(new LauncherItem($"{item2.Key} — {item2.Value}회", "Enter로 복사", null, $"{item2.Key}: {item2.Value}회", null, "\ue756")); + } + } + return Task.FromResult((IEnumerable)list); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + string text = data as string; + if (text != null && !string.IsNullOrWhiteSpace(text)) + { + try + { + Application current = Application.Current; + if (current != null) + { + ((DispatcherObject)current).Dispatcher.Invoke((Action)delegate + { + Clipboard.SetText(text); + }); + } + } + catch + { + } + NotificationService.Notify("업무 일지", "클립보드에 복사되었습니다"); + } + return Task.CompletedTask; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/JsonHandler.cs b/.decompiledproj/AxCopilot/Handlers/JsonHandler.cs new file mode 100644 index 0000000..84a3743 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/JsonHandler.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using AxCopilot.SDK; + +namespace AxCopilot.Handlers; + +public class JsonHandler : IActionHandler +{ + private static readonly JsonSerializerOptions _prettyOpts = new JsonSerializerOptions + { + WriteIndented = true + }; + + private static readonly JsonSerializerOptions _compactOpts = new JsonSerializerOptions + { + WriteIndented = false + }; + + public string? Prefix => "json"; + + public PluginMetadata Metadata => new PluginMetadata("JsonFormatter", "JSON 검증/포맷 — json 뒤에 내용 또는 명령 입력", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string text = query.Trim(); + if (string.IsNullOrWhiteSpace(text) || text.Equals("format", StringComparison.OrdinalIgnoreCase) || text.Equals("minify", StringComparison.OrdinalIgnoreCase) || text.Equals("min", StringComparison.OrdinalIgnoreCase)) + { + string text2 = ""; + try + { + text2 = Clipboard.GetText(); + } + catch + { + } + if (string.IsNullOrWhiteSpace(text2)) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("클립보드가 비어 있습니다", "클립보드에 JSON 텍스트를 복사한 뒤 실행하세요", null, null, null, "\ue77f"))); + } + return Task.FromResult(BuildItems(text2, text.StartsWith("min", StringComparison.OrdinalIgnoreCase))); + } + return Task.FromResult(BuildItems(text, isMinify: false)); + } + + private static IEnumerable BuildItems(string input, bool isMinify) + { + try + { + using JsonDocument jsonDocument = JsonDocument.Parse(input, new JsonDocumentOptions + { + AllowTrailingCommas = true, + CommentHandling = JsonCommentHandling.Skip + }); + string text = JsonSerializer.Serialize(jsonDocument.RootElement, _prettyOpts); + string text2 = JsonSerializer.Serialize(jsonDocument.RootElement, _compactOpts); + JsonValueKind valueKind = jsonDocument.RootElement.ValueKind; + if (1 == 0) + { + } + string text3 = valueKind switch + { + JsonValueKind.Object => $"Object ({jsonDocument.RootElement.EnumerateObject().Count()}개 키)", + JsonValueKind.Array => $"Array ({jsonDocument.RootElement.GetArrayLength()}개 항목)", + _ => jsonDocument.RootElement.ValueKind.ToString(), + }; + if (1 == 0) + { + } + string text4 = text3; + string data = (isMinify ? text2 : text); + string text5 = (isMinify ? "미니파이" : "포맷"); + string text6 = ((text2.Length > 100) ? (text2.Substring(0, 97) + "…") : text2); + return new _003C_003Ez__ReadOnlyArray(new LauncherItem[3] + { + new LauncherItem("✅ 유효한 JSON — " + text4, text6 + " · Enter로 " + text5 + " 결과 클립보드 복사", null, data, null, "\ue930"), + new LauncherItem("포맷 (Pretty Print) 복사", $"{text.Length}자 · 들여쓰기 2스페이스", null, text, null, "\ue8a4"), + new LauncherItem("미니파이 (Minify) 복사", $"{text2.Length}자 · 공백 제거", null, text2, null, "\ue8c6") + }); + } + catch (JsonException ex) + { + string subtitle = ((ex.Message.Length > 100) ? (ex.Message.Substring(0, 97) + "…") : ex.Message); + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("❌ JSON 오류", subtitle, null, null, null, "\uea39")); + } + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is string text) + { + try + { + Clipboard.SetText(text); + } + catch + { + } + } + return Task.CompletedTask; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/JsonSkillCache.cs b/.decompiledproj/AxCopilot/Handlers/JsonSkillCache.cs new file mode 100644 index 0000000..a49b943 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/JsonSkillCache.cs @@ -0,0 +1,6 @@ +namespace AxCopilot.Handlers; + +public class JsonSkillCache +{ + public int Ttl { get; set; } = 0; +} diff --git a/.decompiledproj/AxCopilot/Handlers/JsonSkillCredential.cs b/.decompiledproj/AxCopilot/Handlers/JsonSkillCredential.cs new file mode 100644 index 0000000..9fa0d35 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/JsonSkillCredential.cs @@ -0,0 +1,8 @@ +namespace AxCopilot.Handlers; + +public class JsonSkillCredential +{ + public string Type { get; set; } = "bearer_token"; + + public string CredentialKey { get; set; } = ""; +} diff --git a/.decompiledproj/AxCopilot/Handlers/JsonSkillDefinition.cs b/.decompiledproj/AxCopilot/Handlers/JsonSkillDefinition.cs new file mode 100644 index 0000000..30d48fb --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/JsonSkillDefinition.cs @@ -0,0 +1,20 @@ +namespace AxCopilot.Handlers; + +public class JsonSkillDefinition +{ + public string Id { get; set; } = ""; + + public string Name { get; set; } = ""; + + public string Version { get; set; } = "1.0"; + + public string Prefix { get; set; } = ""; + + public JsonSkillCredential? Credential { get; set; } + + public JsonSkillRequest? Request { get; set; } + + public JsonSkillResponse? Response { get; set; } + + public JsonSkillCache? Cache { get; set; } +} diff --git a/.decompiledproj/AxCopilot/Handlers/JsonSkillHandler.cs b/.decompiledproj/AxCopilot/Handlers/JsonSkillHandler.cs new file mode 100644 index 0000000..6981b1f --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/JsonSkillHandler.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class JsonSkillHandler : IActionHandler +{ + private readonly JsonSkillDefinition _def; + + private readonly HttpClient _http = new HttpClient(); + + private List? _cache; + + private DateTime _cacheExpiry = DateTime.MinValue; + + public string? Prefix => _def.Prefix; + + public PluginMetadata Metadata => new PluginMetadata(_def.Id, _def.Name, _def.Version, "JSON Skill"); + + public JsonSkillHandler(JsonSkillDefinition def) + { + _def = def; + _http.Timeout = TimeSpan.FromSeconds(3.0); + if (def.Credential?.Type == "bearer_token") + { + string token = CredentialManager.GetToken(def.Credential.CredentialKey); + if (!string.IsNullOrEmpty(token)) + { + _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + } + } + } + + public async Task> GetItemsAsync(string query, CancellationToken ct) + { + if (_cache != null && DateTime.Now < _cacheExpiry) + { + return _cache; + } + if (_def.Request == null || _def.Response == null) + { + return Enumerable.Empty(); + } + try + { + string url = _def.Request.Url.Replace("{{INPUT}}", Uri.EscapeDataString(query)); + if (!Uri.TryCreate(url, UriKind.Absolute, out Uri parsedUrl) || (parsedUrl.Scheme != Uri.UriSchemeHttp && parsedUrl.Scheme != Uri.UriSchemeHttps)) + { + LogService.Error("[" + _def.Name + "] 유효하지 않은 URL: " + url); + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("설정 오류", "스킬 URL이 유효하지 않습니다 (http/https만 허용)", null, null)); + } + string text = _def.Request.Method.ToUpper(); + if (1 == 0) + { + } + HttpResponseMessage httpResponseMessage = ((!(text == "POST")) ? (await _http.GetAsync(url, ct)) : (await _http.PostAsync(url, BuildBody(query), ct))); + if (1 == 0) + { + } + HttpResponseMessage response = httpResponseMessage; + response.EnsureSuccessStatusCode(); + List items = ParseResults(await response.Content.ReadAsStringAsync(ct)); + JsonSkillCache? cache = _def.Cache; + if (cache != null && cache.Ttl > 0) + { + _cache = items; + _cacheExpiry = DateTime.Now.AddSeconds(_def.Cache.Ttl); + } + return items; + } + catch (TaskCanceledException) + { + if (_cache != null) + { + LogService.Warn("[" + _def.Name + "] API 타임아웃, 캐시 반환"); + return _cache; + } + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("네트워크 오류", "연결을 확인하세요", null, null)); + } + catch (Exception ex2) + { + LogService.Error("[" + _def.Name + "] API 호출 실패: " + ex2.Message); + return new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("오류: " + ex2.Message, _def.Name, null, null)); + } + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.ActionUrl != null && Uri.TryCreate(item.ActionUrl, UriKind.Absolute, out Uri result) && (result.Scheme == Uri.UriSchemeHttp || result.Scheme == Uri.UriSchemeHttps)) + { + Process.Start(new ProcessStartInfo(item.ActionUrl) + { + UseShellExecute = true + }); + } + return Task.CompletedTask; + } + + private HttpContent BuildBody(string query) + { + string content = JsonSerializer.Serialize(_def.Request?.Body).Replace("\"{{INPUT}}\"", "\"" + query + "\""); + return new StringContent(content, Encoding.UTF8, "application/json"); + } + + private List ParseResults(string json) + { + List list = new List(); + try + { + JsonNode jsonNode = JsonNode.Parse(json); + if (jsonNode == null) + { + return list; + } + JsonNode jsonNode2 = NavigatePath(jsonNode, _def.Response.ResultsPath); + if (!(jsonNode2 is JsonArray source)) + { + return list; + } + foreach (JsonNode item in source.Take(10)) + { + if (item != null) + { + string title = NavigatePath(item, _def.Response.TitleField)?.ToString() ?? "(제목 없음)"; + string subtitle = ((_def.Response.SubtitleField == null) ? "" : (NavigatePath(item, _def.Response.SubtitleField)?.ToString() ?? "")); + string actionUrl = ((_def.Response.ActionUrl == null) ? null : NavigatePath(item, _def.Response.ActionUrl)?.ToString()); + list.Add(new LauncherItem(title, subtitle, null, item, actionUrl, "\ue82d")); + } + } + } + catch (Exception ex) + { + LogService.Error("[" + _def.Name + "] 응답 파싱 실패: " + ex.Message); + } + return list; + } + + private static JsonNode? NavigatePath(JsonNode root, string path) + { + string[] array = path.Split('.'); + JsonNode jsonNode = root; + string[] array2 = array; + foreach (string text in array2) + { + if (jsonNode == null) + { + return null; + } + int num = text.IndexOf('['); + if (num >= 0) + { + int num2 = text.IndexOf(']'); + if (num2 < 0) + { + return null; + } + string propertyName = text.Substring(0, num); + int num3 = num + 1; + int index = int.Parse(text.Substring(num3, num2 - num3)); + jsonNode = jsonNode[propertyName]?[index]; + } + else + { + jsonNode = jsonNode[text]; + } + } + return jsonNode; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/JsonSkillLoader.cs b/.decompiledproj/AxCopilot/Handlers/JsonSkillLoader.cs new file mode 100644 index 0000000..bb99ee8 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/JsonSkillLoader.cs @@ -0,0 +1,22 @@ +using System.IO; +using System.Text.Json; +using AxCopilot.SDK; + +namespace AxCopilot.Handlers; + +public static class JsonSkillLoader +{ + public static IActionHandler? Load(string filePath) + { + string json = File.ReadAllText(filePath); + JsonSkillDefinition jsonSkillDefinition = JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + if (jsonSkillDefinition == null) + { + return null; + } + return new JsonSkillHandler(jsonSkillDefinition); + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/JsonSkillRequest.cs b/.decompiledproj/AxCopilot/Handlers/JsonSkillRequest.cs new file mode 100644 index 0000000..e6d8c09 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/JsonSkillRequest.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace AxCopilot.Handlers; + +public class JsonSkillRequest +{ + public string Method { get; set; } = "GET"; + + public string Url { get; set; } = ""; + + public Dictionary? Headers { get; set; } + + public object? Body { get; set; } +} diff --git a/.decompiledproj/AxCopilot/Handlers/JsonSkillResponse.cs b/.decompiledproj/AxCopilot/Handlers/JsonSkillResponse.cs new file mode 100644 index 0000000..fff4371 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/JsonSkillResponse.cs @@ -0,0 +1,12 @@ +namespace AxCopilot.Handlers; + +public class JsonSkillResponse +{ + public string ResultsPath { get; set; } = "results"; + + public string TitleField { get; set; } = "title"; + + public string? SubtitleField { get; set; } + + public string? ActionUrl { get; set; } +} diff --git a/.decompiledproj/AxCopilot/Handlers/MathEvaluator.cs b/.decompiledproj/AxCopilot/Handlers/MathEvaluator.cs new file mode 100644 index 0000000..194cb6f --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/MathEvaluator.cs @@ -0,0 +1,256 @@ +using System; +using System.Globalization; + +namespace AxCopilot.Handlers; + +internal static class MathEvaluator +{ + private class Evaluator + { + private readonly string _s; + + private int _i; + + public Evaluator(string s) + { + _s = s; + _i = 0; + } + + public double Parse() + { + double result = ParseExpr(); + if (_i < _s.Length) + { + throw new InvalidOperationException($"예기치 않은 문자: '{_s[_i]}'"); + } + return result; + } + + private double ParseExpr() + { + double num = ParseTerm(); + while (_i < _s.Length && (_s[_i] == '+' || _s[_i] == '-')) + { + char c = _s[_i++]; + double num2 = ParseTerm(); + num = ((c == '+') ? (num + num2) : (num - num2)); + } + return num; + } + + private double ParseTerm() + { + double num = ParsePower(); + while (_i < _s.Length && (_s[_i] == '*' || _s[_i] == '/' || _s[_i] == '%')) + { + char c = _s[_i++]; + double num2 = ParsePower(); + num = c switch + { + '/' => num / num2, + '*' => num * num2, + _ => num % num2, + }; + } + return num; + } + + private double ParsePower() + { + double num = ParseUnary(); + if (_i < _s.Length && _s[_i] == '^') + { + _i++; + double y = ParseUnary(); + return Math.Pow(num, y); + } + return num; + } + + private double ParseUnary() + { + if (_i < _s.Length && _s[_i] == '-') + { + _i++; + return 0.0 - ParsePrimary(); + } + if (_i < _s.Length && _s[_i] == '+') + { + _i++; + return ParsePrimary(); + } + return ParsePrimary(); + } + + private double ParsePrimary() + { + if (_i >= _s.Length) + { + throw new InvalidOperationException("수식이 불완전합니다."); + } + if (_i + 1 < _s.Length && _s[_i] == '0' && _s[_i + 1] == 'x') + { + _i += 2; + int i = _i; + while (_i < _s.Length && "0123456789abcdef".Contains(_s[_i])) + { + _i++; + } + string s = _s; + int num = i; + return Convert.ToInt64(s.Substring(num, _i - num), 16); + } + if (char.IsDigit(_s[_i]) || _s[_i] == '.') + { + int i2 = _i; + while (_i < _s.Length && (char.IsDigit(_s[_i]) || _s[_i] == '.')) + { + _i++; + } + if (_i < _s.Length && _s[_i] == 'e') + { + _i++; + if (_i < _s.Length && (_s[_i] == '+' || _s[_i] == '-')) + { + _i++; + } + while (_i < _s.Length && char.IsDigit(_s[_i])) + { + _i++; + } + } + string s2 = _s; + int num = i2; + return double.Parse(s2.Substring(num, _i - num), NumberStyles.Float, CultureInfo.InvariantCulture); + } + if (_s[_i] == '(') + { + _i++; + double result = ParseExpr(); + if (_i < _s.Length && _s[_i] == ')') + { + _i++; + } + return result; + } + if (char.IsLetter(_s[_i])) + { + int i3 = _i; + while (_i < _s.Length && (char.IsLetterOrDigit(_s[_i]) || _s[_i] == '_')) + { + _i++; + } + string s3 = _s; + int num = i3; + string text = s3.Substring(num, _i - num); + switch (text) + { + case "pi": + return Math.PI; + case "e": + return Math.E; + case "inf": + return double.PositiveInfinity; + default: + if (_i < _s.Length && _s[_i] == '(') + { + _i++; + double num2 = ParseExpr(); + double? num3 = null; + if (_i < _s.Length && _s[_i] == ',') + { + _i++; + num3 = ParseExpr(); + } + if (_i < _s.Length && _s[_i] == ')') + { + _i++; + } + if (1 == 0) + { + } + double result2; + switch (text) + { + case "sqrt": + result2 = Math.Sqrt(num2); + break; + case "abs": + result2 = Math.Abs(num2); + break; + case "ceil": + result2 = Math.Ceiling(num2); + break; + case "floor": + result2 = Math.Floor(num2); + break; + case "round": + result2 = (num3.HasValue ? Math.Round(num2, (int)num3.Value) : Math.Round(num2)); + break; + case "sin": + result2 = Math.Sin(num2 * Math.PI / 180.0); + break; + case "cos": + result2 = Math.Cos(num2 * Math.PI / 180.0); + break; + case "tan": + result2 = Math.Tan(num2 * Math.PI / 180.0); + break; + case "asin": + result2 = Math.Asin(num2) * 180.0 / Math.PI; + break; + case "acos": + result2 = Math.Acos(num2) * 180.0 / Math.PI; + break; + case "atan": + result2 = Math.Atan(num2) * 180.0 / Math.PI; + break; + case "log": + result2 = (num3.HasValue ? Math.Log(num2, num3.Value) : Math.Log10(num2)); + break; + case "log2": + result2 = Math.Log2(num2); + break; + case "ln": + result2 = Math.Log(num2); + break; + case "exp": + result2 = Math.Exp(num2); + break; + case "pow": + if (!num3.HasValue) + { + throw new InvalidOperationException("pow(x,y) 형식으로 사용하세요."); + } + result2 = Math.Pow(num2, num3.Value); + break; + case "min": + result2 = (num3.HasValue ? Math.Min(num2, num3.Value) : num2); + break; + case "max": + result2 = (num3.HasValue ? Math.Max(num2, num3.Value) : num2); + break; + default: + throw new InvalidOperationException("알 수 없는 함수: " + text + "()"); + } + if (1 == 0) + { + } + return result2; + } + throw new InvalidOperationException("알 수 없는 식별자: " + text); + } + } + throw new InvalidOperationException($"예기치 않은 문자: '{_s[_i]}'"); + } + } + + public static double Evaluate(string expr) + { + Evaluator evaluator = new Evaluator(expr.Replace(" ", "").Replace("×", "*").Replace("÷", "/") + .Replace(",", ",") + .ToLowerInvariant()); + return evaluator.Parse(); + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/MediaHandler.cs b/.decompiledproj/AxCopilot/Handlers/MediaHandler.cs new file mode 100644 index 0000000..4ff33cc --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/MediaHandler.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class MediaHandler : IActionHandler +{ + private record MediaKeyData(byte Vk); + + private const byte VK_MEDIA_PLAY_PAUSE = 179; + + private const byte VK_MEDIA_NEXT_TRACK = 176; + + private const byte VK_MEDIA_PREV_TRACK = 177; + + private const byte VK_VOLUME_UP = 175; + + private const byte VK_VOLUME_DOWN = 174; + + private const byte VK_VOLUME_MUTE = 173; + + private const uint KEYEVENTF_EXTENDEDKEY = 1u; + + private const uint KEYEVENTF_KEYUP = 2u; + + private static readonly List<(string[] Keys, string Title, string Subtitle, byte Vk, string Symbol)> _commands; + + public string? Prefix => "media"; + + public PluginMetadata Metadata => new PluginMetadata("MediaControl", "미디어 컨트롤 — media 뒤에 명령어 입력", "1.0", "AX"); + + [DllImport("user32.dll")] + private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, nuint dwExtraInfo); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string q = query.Trim().ToLowerInvariant(); + IEnumerable<(string[], string, string, byte, string)> source = ((!string.IsNullOrEmpty(q)) ? _commands.Where<(string[], string, string, byte, string)>(((string[] Keys, string Title, string Subtitle, byte Vk, string Symbol) c) => c.Keys.Any((string k) => k.StartsWith(q, StringComparison.OrdinalIgnoreCase)) || c.Title.Contains(q, StringComparison.OrdinalIgnoreCase)) : _commands); + List list = source.Select<(string[], string, string, byte, string), LauncherItem>(((string[] Keys, string Title, string Subtitle, byte Vk, string Symbol) c) => new LauncherItem(c.Title, c.Subtitle, null, new MediaKeyData(c.Vk), null, c.Symbol)).ToList(); + if (list.Count == 0) + { + list.Add(new LauncherItem("알 수 없는 명령어", "play · next · prev · vol+ · vol- · mute 중 하나를 입력하세요", null, null, null, "\ue7ba")); + } + return Task.FromResult((IEnumerable)list); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (!(item.Data is MediaKeyData mediaKeyData)) + { + return Task.CompletedTask; + } + try + { + keybd_event(mediaKeyData.Vk, 0, 1u, 0u); + keybd_event(mediaKeyData.Vk, 0, 3u, 0u); + LogService.Info($"미디어 키 전송: VK=0x{mediaKeyData.Vk:X2}"); + } + catch (Exception ex) + { + LogService.Warn("미디어 키 전송 실패: " + ex.Message); + } + return Task.CompletedTask; + } + + static MediaHandler() + { + int num = 6; + List<(string[], string, string, byte, string)> list = new List<(string[], string, string, byte, string)>(num); + CollectionsMarshal.SetCount(list, num); + Span<(string[], string, string, byte, string)> span = CollectionsMarshal.AsSpan(list); + span[0] = (new string[3] { "play", "pause", "pp" }, "재생 / 일시정지", "현재 미디어 재생 또는 일시정지", 179, "\ue768"); + span[1] = (new string[2] { "next", ">>" }, "다음 트랙", "다음 곡으로 이동", 176, "\ue893"); + span[2] = (new string[3] { "prev", "previous", "<<" }, "이전 트랙", "이전 곡으로 이동", 177, "\ue892"); + span[3] = (new string[3] { "vol+", "volup", "up" }, "볼륨 올리기", "시스템 볼륨 증가", 175, "\ue995"); + span[4] = (new string[3] { "vol-", "voldown", "down" }, "볼륨 낮추기", "시스템 볼륨 감소", 174, "\ue993"); + span[5] = (new string[2] { "mute", "음소거" }, "음소거 토글", "볼륨 음소거 / 해제", 173, "\ue74f"); + _commands = list; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/MonitorHandler.cs b/.decompiledproj/AxCopilot/Handlers/MonitorHandler.cs new file mode 100644 index 0000000..b1ccb9b --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/MonitorHandler.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using AxCopilot.SDK; + +namespace AxCopilot.Handlers; + +public class MonitorHandler : IActionHandler +{ + private struct MEMORYSTATUSEX + { + public uint dwLength; + + public uint dwMemoryLoad; + + public ulong ullTotalPhys; + + public ulong ullAvailPhys; + + public ulong ullTotalPageFile; + + public ulong ullAvailPageFile; + + public ulong ullTotalVirtual; + + public ulong ullAvailVirtual; + + public ulong ullAvailExtendedVirtual; + } + + public string? Prefix => "monitor"; + + public PluginMetadata Metadata => new PluginMetadata("Monitor", "시스템 리소스 모니터 — monitor", "1.0", "AX"); + + [DllImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string text = query.Trim().ToLowerInvariant(); + List list = new List(); + bool flag = string.IsNullOrWhiteSpace(text); + if (flag || text.Contains("cpu") || text.Contains("프로세서")) + { + int processorCount = Environment.ProcessorCount; + int value = Process.GetProcesses().Length; + int value2 = 0; + try + { + value2 = Process.GetProcesses().Sum(delegate(Process p) + { + try + { + return p.Threads.Count; + } + catch + { + return 0; + } + }); + } + catch + { + } + list.Add(new LauncherItem($"CPU: {processorCount}코어 · 프로세스 {value}개 · 스레드 {value2:N0}개", "Enter로 클립보드 복사", null, $"CPU: {processorCount}코어, 프로세스 {value}개, 스레드 {value2:N0}개", null, "\ue950")); + } + if (flag || text.Contains("mem") || text.Contains("ram") || text.Contains("메모리")) + { + MEMORYSTATUSEX lpBuffer = new MEMORYSTATUSEX + { + dwLength = (uint)Marshal.SizeOf() + }; + GlobalMemoryStatusEx(ref lpBuffer); + double value3 = (double)lpBuffer.ullTotalPhys / 1073741824.0; + double value4 = (double)(lpBuffer.ullTotalPhys - lpBuffer.ullAvailPhys) / 1073741824.0; + uint dwMemoryLoad = lpBuffer.dwMemoryLoad; + list.Add(new LauncherItem($"메모리: {value4:F1}GB / {value3:F1}GB ({dwMemoryLoad}% 사용)", $"사용 가능: {(double)lpBuffer.ullAvailPhys / 1073741824.0:F1}GB · Enter로 복사", null, $"메모리: {value4:F1}GB / {value3:F1}GB ({dwMemoryLoad}% 사용)", null, "\ue950")); + } + if (flag || text.Contains("disk") || text.Contains("디스크") || text.Contains("저장")) + { + DriveInfo[] drives = DriveInfo.GetDrives(); + foreach (DriveInfo driveInfo in drives) + { + if (driveInfo.IsReady && driveInfo.DriveType == DriveType.Fixed) + { + double num2 = (double)driveInfo.TotalSize / 1073741824.0; + double num3 = (double)driveInfo.AvailableFreeSpace / 1073741824.0; + double num4 = num2 - num3; + int value5 = (int)(num4 / num2 * 100.0); + list.Add(new LauncherItem($"디스크 {driveInfo.Name.TrimEnd('\\')} {num4:F0}GB / {num2:F0}GB ({value5}%)", $"여유: {num3:F1}GB · {driveInfo.DriveFormat} · Enter로 복사", null, $"디스크 {driveInfo.Name}: {num4:F0}GB / {num2:F0}GB ({value5}%), 여유 {num3:F1}GB", null, "\ueda2")); + } + } + } + if (flag || text.Contains("uptime") || text.Contains("가동")) + { + TimeSpan timeSpan = TimeSpan.FromMilliseconds(Environment.TickCount64); + string text2 = ((timeSpan.Days > 0) ? $"{timeSpan.Days}일 {timeSpan.Hours}시간 {timeSpan.Minutes}분" : $"{timeSpan.Hours}시간 {timeSpan.Minutes}분"); + list.Add(new LauncherItem("가동 시간: " + text2, "마지막 재시작 이후 경과 · Enter로 복사", null, "가동 시간: " + text2, null, "\ue823")); + } + if (flag || text.Contains("top") || text.Contains("프로세스")) + { + try + { + IEnumerable values = Process.GetProcesses().Where(delegate(Process p) + { + try + { + return p.WorkingSet64 > 0; + } + catch + { + return false; + } + }).OrderByDescending(delegate(Process p) + { + try + { + return p.WorkingSet64; + } + catch + { + return 0L; + } + }) + .Take(5) + .Select(delegate(Process p) + { + try + { + return $"{p.ProcessName} ({p.WorkingSet64 / 1048576}MB)"; + } + catch + { + return p.ProcessName; + } + }); + list.Add(new LauncherItem("메모리 상위 프로세스", string.Join(", ", values), null, "메모리 상위: " + string.Join(", ", values), null, "\ue7f4")); + } + catch + { + } + } + if (list.Count == 0) + { + list.Add(new LauncherItem("'" + text + "'에 해당하는 리소스 항목 없음", "cpu / mem / disk / uptime / top", null, null, null, "\ue7ba")); + } + return Task.FromResult((IEnumerable)list); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + string text = data as string; + if (text != null && !string.IsNullOrWhiteSpace(text)) + { + try + { + Application current = Application.Current; + if (current != null) + { + ((DispatcherObject)current).Dispatcher.Invoke((Action)delegate + { + Clipboard.SetText(text); + }); + } + } + catch + { + } + } + return Task.CompletedTask; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/NoteEntry.cs b/.decompiledproj/AxCopilot/Handlers/NoteEntry.cs new file mode 100644 index 0000000..948dc3c --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/NoteEntry.cs @@ -0,0 +1,5 @@ +using System; + +namespace AxCopilot.Handlers; + +internal record NoteEntry(DateTime SavedAt, string Content); diff --git a/.decompiledproj/AxCopilot/Handlers/NoteHandler.cs b/.decompiledproj/AxCopilot/Handlers/NoteHandler.cs new file mode 100644 index 0000000..0634113 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/NoteHandler.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class NoteHandler : IActionHandler +{ + private static readonly string NotesFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "notes.txt"); + + public string? Prefix => "note"; + + public PluginMetadata Metadata => new PluginMetadata("Note", "빠른 메모 — note 뒤에 내용 입력", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string text = query.Trim(); + if (string.IsNullOrWhiteSpace(text)) + { + List list = ReadNotes(); + if (!list.Any()) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("메모가 없습니다", "note 뒤에 내용을 입력하면 저장됩니다", null, null, null, "\ue70b"))); + } + List list2 = (from n in list.Take(10) + select new LauncherItem((n.Content.Length > 60) ? (n.Content.Substring(0, 57) + "…") : n.Content, $"{n.SavedAt:yyyy-MM-dd HH:mm} · Enter 복사 · Delete 삭제", null, n.Content, null, "\ue70b")).ToList(); + list2.Add(new LauncherItem("전체 메모 삭제", $"총 {list.Count}개 메모 모두 삭제 · Enter로 실행", null, "__CLEAR__", null, "\ue74d")); + return Task.FromResult((IEnumerable)list2); + } + if (text.Equals("clear", StringComparison.OrdinalIgnoreCase)) + { + int count = ReadNotes().Count; + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem($"전체 메모 삭제 ({count}개)", "모든 메모를 삭제합니다 · Enter로 실행", null, "__CLEAR__", null, "\ue74d"))); + } + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("메모 저장: " + ((text.Length > 60) ? (text.Substring(0, 57) + "…") : text), $"{DateTime.Now:yyyy-MM-dd HH:mm} · Enter로 저장", null, ("__SAVE__", text), null, "\ue70b"))); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + object obj = data; + if (!(obj is (string, string) tuple)) + { + if (obj is string text) + { + if (text == "__CLEAR__") + { + ClearNotes(); + } + else + { + string text2 = text; + try + { + Clipboard.SetText(text2); + } + catch + { + } + } + } + } + else if (tuple.Item1 == "__SAVE__") + { + SaveNote(tuple.Item2); + NotificationService.Notify("AX Copilot", "메모 저장됨: " + ((tuple.Item2.Length > 30) ? (tuple.Item2.Substring(0, 27) + "…") : tuple.Item2)); + } + return Task.CompletedTask; + } + + private static void SaveNote(string content) + { + try + { + Directory.CreateDirectory(Path.GetDirectoryName(NotesFile)); + string contents = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {content}{Environment.NewLine}"; + File.AppendAllText(NotesFile, contents, Encoding.UTF8); + } + catch (Exception ex) + { + LogService.Warn("메모 저장 실패: " + ex.Message); + } + } + + private static List ReadNotes() + { + List list = new List(); + if (!File.Exists(NotesFile)) + { + return list; + } + try + { + string[] source = File.ReadAllLines(NotesFile, Encoding.UTF8); + foreach (string item in source.Reverse()) + { + if (!string.IsNullOrWhiteSpace(item)) + { + if (item.StartsWith('[') && item.Length > 21 && item[20] == ']' && DateTime.TryParse(item.Substring(1, 19), out var result)) + { + DateTime savedAt = result; + string text = item; + list.Add(new NoteEntry(savedAt, text.Substring(22, text.Length - 22).Trim())); + } + else + { + list.Add(new NoteEntry(DateTime.MinValue, item.Trim())); + } + } + } + } + catch (Exception ex) + { + LogService.Warn("메모 읽기 실패: " + ex.Message); + } + return list; + } + + private static void ClearNotes() + { + try + { + if (File.Exists(NotesFile)) + { + File.Delete(NotesFile); + } + } + catch (Exception ex) + { + LogService.Warn("메모 삭제 실패: " + ex.Message); + } + } + + public static bool DeleteNote(string content) + { + try + { + if (!File.Exists(NotesFile)) + { + return false; + } + List list = File.ReadAllLines(NotesFile, Encoding.UTF8).ToList(); + for (int num = list.Count - 1; num >= 0; num--) + { + string text = list[num]; + if (!string.IsNullOrWhiteSpace(text)) + { + string text3; + if (text.StartsWith('[') && text.Length > 21 && text[20] == ']') + { + string text2 = text; + text3 = text2.Substring(22, text2.Length - 22).Trim(); + } + else + { + text3 = text.Trim(); + } + if (text3 == content) + { + list.RemoveAt(num); + File.WriteAllLines(NotesFile, list, Encoding.UTF8); + return true; + } + } + } + } + catch (Exception ex) + { + LogService.Warn("메모 개별 삭제 실패: " + ex.Message); + } + return false; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/PortHandler.cs b/.decompiledproj/AxCopilot/Handlers/PortHandler.cs new file mode 100644 index 0000000..f2ecef0 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/PortHandler.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.NetworkInformation; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class PortHandler : IActionHandler +{ + private static readonly Dictionary _procCache = new Dictionary(); + + private static readonly Dictionary _pidMap = new Dictionary(); + + private static DateTime _cacheExpiry = DateTime.MinValue; + + public string? Prefix => "port"; + + public PluginMetadata Metadata => new PluginMetadata("PortChecker", "포트 & 프로세스 점검 — port 뒤에 포트번호 또는 프로세스명", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + RefreshProcessCache(); + TcpConnectionInformation[] activeTcpConnections; + try + { + IPGlobalProperties iPGlobalProperties = IPGlobalProperties.GetIPGlobalProperties(); + activeTcpConnections = iPGlobalProperties.GetActiveTcpConnections(); + } + catch (Exception ex) + { + LogService.Warn("포트 조회 실패: " + ex.Message); + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("네트워크 정보를 가져올 수 없습니다", ex.Message, null, null, null, "\ue7ba"))); + } + string text = query.Trim(); + if (string.IsNullOrWhiteSpace(text)) + { + List list = (from c in activeTcpConnections + where c.State == TcpState.Established || c.State == TcpState.Listen + orderby c.LocalEndPoint.Port + select c).Take(20).Select(delegate(TcpConnectionInformation c) + { + string processNameForPort = GetProcessNameForPort(c.LocalEndPoint.Port); + string text2 = ((c.State == TcpState.Listen) ? "LISTEN" : "ESTABLISHED"); + return new LauncherItem($":{c.LocalEndPoint.Port} → {c.RemoteEndPoint}", text2 + " · " + processNameForPort + " · Enter로 포트번호 복사", null, c.LocalEndPoint.Port.ToString(), null, "\ue968"); + }).ToList(); + if (!list.Any()) + { + list.Add(new LauncherItem("활성 연결 없음", "TCP 연결이 감지되지 않았습니다", null, null, null, "\ue968")); + } + return Task.FromResult((IEnumerable)list); + } + if (int.TryParse(text, out var portNum)) + { + List source = activeTcpConnections.Where((TcpConnectionInformation c) => c.LocalEndPoint.Port == portNum || c.RemoteEndPoint.Port == portNum).ToList(); + if (!source.Any()) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem($"포트 {portNum} — 사용 중 아님", "해당 포트를 사용하는 TCP 연결이 없습니다", null, portNum.ToString(), null, "\ue946"))); + } + List result = source.Select(delegate(TcpConnectionInformation c) + { + string processNameForPort = GetProcessNameForPort(c.LocalEndPoint.Port); + int pidForPort = GetPidForPort(c.LocalEndPoint.Port); + return new LauncherItem($":{c.LocalEndPoint.Port} ←→ {c.RemoteEndPoint}", $"{c.State} · {processNameForPort} (PID {pidForPort}) · Enter로 PID 복사", null, (pidForPort > 0) ? pidForPort.ToString() : portNum.ToString(), null, "\ue968"); + }).ToList(); + return Task.FromResult((IEnumerable)result); + } + string procLower = text.ToLowerInvariant(); + List list2 = activeTcpConnections.Where(delegate(TcpConnectionInformation c) + { + string text2 = GetProcessNameForPort(c.LocalEndPoint.Port).ToLowerInvariant(); + return text2.Contains(procLower); + }).Take(15).Select(delegate(TcpConnectionInformation c) + { + string processNameForPort = GetProcessNameForPort(c.LocalEndPoint.Port); + return new LauncherItem($"{processNameForPort} : {c.LocalEndPoint.Port} → {c.RemoteEndPoint}", $"{c.State} · Enter로 포트번호 복사", null, c.LocalEndPoint.Port.ToString(), null, "\ue968"); + }) + .ToList(); + if (!list2.Any()) + { + list2.Add(new LauncherItem("'" + text + "' — 연결 없음", "해당 프로세스의 TCP 연결이 없습니다", null, null, null, "\ue7ba")); + } + return Task.FromResult((IEnumerable)list2); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is string text) + { + try + { + Clipboard.SetText(text); + } + catch + { + } + } + return Task.CompletedTask; + } + + private static void RefreshProcessCache() + { + if (DateTime.Now < _cacheExpiry) + { + return; + } + _procCache.Clear(); + _pidMap.Clear(); + try + { + Process[] processes = Process.GetProcesses(); + foreach (Process process in processes) + { + try + { + _procCache[process.Id] = process.ProcessName; + } + catch + { + } + } + } + catch (Exception ex) + { + LogService.Warn("프로세스 목록 갱신 실패: " + ex.Message); + } + try + { + ProcessStartInfo startInfo = new ProcessStartInfo("netstat", "-ano") + { + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + using Process process2 = Process.Start(startInfo); + if (process2 != null) + { + string text = process2.StandardOutput.ReadToEnd(); + process2.WaitForExit(2000); + string[] array = text.Split('\n'); + foreach (string text2 in array) + { + string[] array2 = text2.Trim().Split(new char[2] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); + if (array2.Length < 5 || !int.TryParse(array2[^1], out var result)) + { + continue; + } + string text3 = array2[1]; + int num = text3.LastIndexOf(':'); + if (num >= 0) + { + string text4 = text3; + int num2 = num + 1; + if (int.TryParse(text4.Substring(num2, text4.Length - num2), out var result2)) + { + _pidMap.TryAdd(result2, result); + } + } + } + } + } + catch (Exception ex2) + { + LogService.Warn("netstat 실행 실패: " + ex2.Message); + } + _cacheExpiry = DateTime.Now.AddSeconds(5.0); + } + + private static string GetProcessNameForPort(int port) + { + int pidForPort = GetPidForPort(port); + string value; + return (pidForPort > 0 && _procCache.TryGetValue(pidForPort, out value)) ? value : "알 수 없음"; + } + + private static int GetPidForPort(int port) + { + int value; + return _pidMap.TryGetValue(port, out value) ? value : (-1); + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/ProcessHandler.cs b/.decompiledproj/AxCopilot/Handlers/ProcessHandler.cs new file mode 100644 index 0000000..1135812 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/ProcessHandler.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class ProcessHandler : IActionHandler +{ + private record ProcessKillData(string Name, List Pids); + + private static readonly HashSet ProtectedProcesses = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "system", "smss", "csrss", "wininit", "winlogon", "services", "lsass", "svchost", "explorer", "dwm", + "fontdrvhost", "spoolsv", "registry" + }; + + public string? Prefix => "kill "; + + public PluginMetadata Metadata => new PluginMetadata("ProcessKiller", "프로세스 종료 — kill 뒤에 프로세스명 입력", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(query)) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("종료할 프로세스명을 입력하세요", "예: kill chrome · kill notepad · kill explorer", null, null, null, "\ue7e8"))); + } + string q = query.Trim().ToLowerInvariant(); + try + { + List list = (from p in Process.GetProcesses() + where !ProtectedProcesses.Contains(p.ProcessName) && p.ProcessName.ToLowerInvariant().Contains(q) + orderby p.ProcessName + select p).Take(12).ToList(); + if (list.Count == 0) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("'" + query + "' 프로세스를 찾을 수 없습니다", "실행 중인 프로세스가 없거나 이름이 다릅니다", null, null, null, "\ue7ba"))); + } + List result = list.GroupBy((Process p) => p.ProcessName, StringComparer.OrdinalIgnoreCase).Select(delegate(IGrouping g) + { + List list2 = g.Select((Process p) => p.Id).ToList(); + long value = g.Sum(delegate(Process p) + { + try + { + return p.WorkingSet64 / 1024 / 1024; + } + catch + { + return 0L; + } + }); + string title = ((g.Count() > 1) ? $"{g.Key} ({g.Count()}개 인스턴스)" : g.Key); + string subtitle = $"PID: {string.Join(", ", list2)} · 메모리: {value} MB · Enter로 종료"; + return new LauncherItem(title, subtitle, null, new ProcessKillData(g.Key, list2), null, "\ue7e8"); + }).ToList(); + return Task.FromResult((IEnumerable)result); + } + catch (Exception ex) + { + LogService.Warn("프로세스 목록 조회 실패: " + ex.Message); + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("프로세스 목록 조회 실패", ex.Message, null, null, null, "\uea39"))); + } + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (!(item.Data is ProcessKillData processKillData)) + { + return Task.CompletedTask; + } + int num = 0; + int num2 = 0; + foreach (int pid in processKillData.Pids) + { + try + { + Process processById = Process.GetProcessById(pid); + processById.Kill(entireProcessTree: false); + num++; + } + catch + { + num2++; + } + } + LogService.Info($"프로세스 종료: {processKillData.Name} — {num}개 성공, {num2}개 실패"); + return Task.CompletedTask; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/RecentFilesHandler.cs b/.decompiledproj/AxCopilot/Handlers/RecentFilesHandler.cs new file mode 100644 index 0000000..0c62820 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/RecentFilesHandler.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class RecentFilesHandler : IActionHandler +{ + private static readonly string RecentFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft\\Windows\\Recent"); + + private static (DateTime At, List<(string Name, string LinkPath, DateTime Modified)> Files)? _cache; + + private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(10.0); + + public string? Prefix => "recent"; + + public PluginMetadata Metadata => new PluginMetadata("RecentFiles", "최근 파일 — recent 뒤에 검색어 입력", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string q = query.Trim(); + if (string.IsNullOrWhiteSpace(q)) + { + } + List<(string, string, DateTime)> recentFiles = GetRecentFiles(); + IEnumerable<(string, string, DateTime)> source = recentFiles; + if (!string.IsNullOrWhiteSpace(q)) + { + source = recentFiles.Where<(string, string, DateTime)>(((string Name, string LinkPath, DateTime Modified) f) => f.Name.Contains(q, StringComparison.OrdinalIgnoreCase)); + } + List list = (from f in source.Take(20) + select new LauncherItem(f.Name, $"{f.Modified:yyyy-MM-dd HH:mm} · Enter로 열기", null, f.LinkPath, null, GetSymbol(f.Name))).ToList(); + if (!list.Any()) + { + list.Add(new LauncherItem(string.IsNullOrWhiteSpace(q) ? "최근 파일 없음" : "검색 결과 없음", string.IsNullOrWhiteSpace(q) ? "Windows Recent 폴더가 비어 있습니다" : ("'" + q + "' 파일을 최근 목록에서 찾을 수 없습니다"), null, null, null, "\ue946")); + } + return Task.FromResult((IEnumerable)list); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is string text && File.Exists(text)) + { + try + { + Process.Start(new ProcessStartInfo(text) + { + UseShellExecute = true + }); + } + catch (Exception ex) + { + LogService.Warn("최근 파일 열기 실패: " + ex.Message); + } + } + return Task.CompletedTask; + } + + private static List<(string Name, string LinkPath, DateTime Modified)> GetRecentFiles() + { + if (_cache.HasValue && DateTime.Now - _cache.Value.At < CacheTtl) + { + return _cache.Value.Files; + } + List<(string, string, DateTime)> list = new List<(string, string, DateTime)>(); + try + { + if (!Directory.Exists(RecentFolder)) + { + return list; + } + List<(string, FileInfo)> list2 = (from p in Directory.GetFiles(RecentFolder, "*.lnk") + select (Path: p, Info: new FileInfo(p)) into f + orderby f.Info.LastWriteTime descending + select f).Take(100).ToList(); + foreach (var item3 in list2) + { + string item = item3.Item1; + FileInfo item2 = item3.Item2; + string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(item2.Name); + list.Add((fileNameWithoutExtension, item, item2.LastWriteTime)); + } + } + catch (Exception ex) + { + LogService.Warn("최근 파일 목록 읽기 실패: " + ex.Message); + } + _cache = (DateTime.Now, list); + return list; + } + + private static string GetSymbol(string name) + { + string text = Path.GetExtension(name).ToLowerInvariant(); + if (1 == 0) + { + } + string result; + switch (text) + { + case ".exe": + case ".msi": + result = "\uecaa"; + break; + case ".xlsx": + case ".xls": + case ".csv": + result = "\ue8a5"; + break; + case ".docx": + case ".doc": + result = "\ue8a5"; + break; + case ".pptx": + case ".ppt": + result = "\ue8a5"; + break; + case ".pdf": + result = "\ue8a5"; + break; + case ".txt": + case ".md": + case ".log": + result = "\ue8d2"; + break; + case ".jpg": + case ".jpeg": + case ".png": + case ".gif": + case ".webp": + case ".bmp": + result = "\ueb9f"; + break; + default: + result = "\ue8a5"; + break; + } + if (1 == 0) + { + } + return result; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/RenameHandler.cs b/.decompiledproj/AxCopilot/Handlers/RenameHandler.cs new file mode 100644 index 0000000..449fe3b --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/RenameHandler.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class RenameHandler : IActionHandler +{ + public string? Prefix => "rename"; + + public PluginMetadata Metadata => new PluginMetadata("Rename", "파일 일괄 이름변경 — rename", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string text = query.Trim(); + if (string.IsNullOrWhiteSpace(text)) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlyArray(new LauncherItem[3] + { + new LauncherItem("파일 일괄 이름변경", "rename [폴더\\패턴] [새이름 템플릿]", null, null, null, "\ue8ac"), + new LauncherItem("사용 예시", "rename C:\\work\\*.xlsx 보고서_{n} → 보고서_1.xlsx, 보고서_2.xlsx ...", null, null, null, "\ue946"), + new LauncherItem("변수: {n} 순번, {date} 날짜, {orig} 원본명", "rename D:\\photos\\*.jpg {date}_{n} → 2026-03-27_1.jpg ...", null, null, null, "\ue946") + })); + } + string[] array = text.Split(' ', 2, StringSplitOptions.TrimEntries); + string path = array[0]; + string text2 = ((array.Length > 1) ? array[1] : null); + string dir = Path.GetDirectoryName(path); + string fileName = Path.GetFileName(path); + if (string.IsNullOrWhiteSpace(dir) || !Directory.Exists(dir)) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("폴더를 찾을 수 없습니다", "경로: " + (dir ?? "(비어 있음)"), null, null, null, "\ue7ba"))); + } + string[] array2; + try + { + array2 = Directory.GetFiles(dir, fileName); + } + catch + { + array2 = Array.Empty(); + } + if (array2.Length == 0) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("일치하는 파일이 없습니다", "패턴: " + fileName + " · 폴더: " + dir, null, null, null, "\ue7ba"))); + } + Array.Sort(array2); + if (string.IsNullOrWhiteSpace(text2)) + { + List list = array2.Take(10).Select((string f, int i) => new LauncherItem(Path.GetFileName(f), dir, null, null, null, "\ue8a5")).ToList(); + list.Insert(0, new LauncherItem($"총 {array2.Length}개 파일 발견", "뒤에 새 이름 템플릿을 추가하세요 (예: 보고서_{n})", null, null, null, "\ue946")); + return Task.FromResult((IEnumerable)list); + } + string newValue = DateTime.Today.ToString("yyyy-MM-dd"); + List<(string, string)> list2 = new List<(string, string)>(); + for (int num = 0; num < array2.Length; num++) + { + string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(array2[num]); + string extension = Path.GetExtension(array2[num]); + string text3 = text2.Replace("{n}", (num + 1).ToString()).Replace("{date}", newValue).Replace("{orig}", fileNameWithoutExtension); + if (!Path.HasExtension(text3)) + { + text3 += extension; + } + list2.Add((Path.GetFileName(array2[num]), text3)); + } + List list3 = new List(); + list3.Add(new LauncherItem($"총 {array2.Length}개 파일 이름변경 실행", $"Enter로 실행 · {list2[0].Item1} → {list2[0].Item2} ...", null, ValueTuple.Create(dir, array2, list2.Select<(string, string), string>(((string From, string To) p) => p.To).ToArray()), null, "\ue8ac")); + foreach (var (text4, text5) in list2.Take(8)) + { + list3.Add(new LauncherItem(text4 + " → " + text5, "미리보기", null, null, null, "\ue8a5")); + } + if (array2.Length > 8) + { + list3.Add(new LauncherItem($"... 외 {array2.Length - 8}개", "", null, null, null, "\ue946")); + } + return Task.FromResult((IEnumerable)list3); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (!(item.Data is (string, string[], string[]) tuple) || 1 == 0) + { + return Task.CompletedTask; + } + (string, string[], string[]) tuple2 = tuple; + string item2 = tuple2.Item1; + string[] item3 = tuple2.Item2; + string[] item4 = tuple2.Item3; + int num = 0; + int num2 = 0; + for (int i = 0; i < item3.Length && i < item4.Length; i++) + { + try + { + string text = Path.Combine(item2, item4[i]); + if (File.Exists(text)) + { + num2++; + continue; + } + File.Move(item3[i], text); + num++; + } + catch + { + num2++; + } + } + string message = ((num2 > 0) ? $"{num}개 이름변경 완료, {num2}개 실패 (이미 존재하거나 접근 불가)" : $"{num}개 파일 이름변경 완료"); + NotificationService.Notify("AX Copilot", message); + return Task.CompletedTask; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/RoutineHandler.cs b/.decompiledproj/AxCopilot/Handlers/RoutineHandler.cs new file mode 100644 index 0000000..c506357 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/RoutineHandler.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class RoutineHandler : IActionHandler +{ + internal record RoutineDefinition([property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("description")] string Description, [property: JsonPropertyName("steps")] RoutineStep[] Steps); + + internal record RoutineStep([property: JsonPropertyName("type")] string Type, [property: JsonPropertyName("target")] string Target, [property: JsonPropertyName("label")] string Label); + + private static readonly string RoutineFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "routines.json"); + + private static readonly JsonSerializerOptions JsonOpts = new JsonSerializerOptions + { + WriteIndented = true, + PropertyNameCaseInsensitive = true + }; + + private static readonly RoutineDefinition[] BuiltInRoutines = new RoutineDefinition[2] + { + new RoutineDefinition("morning", "출근 루틴", new RoutineStep[2] + { + new RoutineStep("app", "explorer.exe", "파일 탐색기"), + new RoutineStep("info", "info", "시스템 정보 표시") + }), + new RoutineDefinition("endofday", "퇴근 루틴", new RoutineStep[1] + { + new RoutineStep("cmd", "journal", "오늘 업무 일지 생성") + }) + }; + + public string? Prefix => "routine"; + + public PluginMetadata Metadata => new PluginMetadata("Routine", "루틴 자동화 — routine", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string q = query.Trim(); + List list = LoadRoutines(); + if (string.IsNullOrWhiteSpace(q)) + { + List list2 = new List + { + new LauncherItem("루틴 자동화", $"총 {list.Count}개 루틴 · 이름 입력 시 실행 · routines.json에서 편집", null, null, null, "\ue946") + }; + foreach (RoutineDefinition item in list) + { + string value = string.Join(" → ", item.Steps.Select((RoutineStep s) => s.Label)); + list2.Add(new LauncherItem("[" + item.Name + "] " + item.Description, $"{item.Steps.Length}단계: {value} · Enter로 실행", null, item, null, "\ue82f")); + } + return Task.FromResult((IEnumerable)list2); + } + RoutineDefinition routineDefinition = list.FirstOrDefault((RoutineDefinition r) => r.Name.Equals(q, StringComparison.OrdinalIgnoreCase)); + if (routineDefinition != null) + { + string text = string.Join(" → ", routineDefinition.Steps.Select((RoutineStep s) => s.Label)); + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("[" + routineDefinition.Name + "] 루틴 실행", routineDefinition.Description + " · " + text, null, routineDefinition, null, "\ue82f"))); + } + IEnumerable source = list.Where((RoutineDefinition r) => r.Name.Contains(q, StringComparison.OrdinalIgnoreCase) || r.Description.Contains(q, StringComparison.OrdinalIgnoreCase)); + List list3 = source.Select((RoutineDefinition r) => new LauncherItem("[" + r.Name + "] " + r.Description, $"{r.Steps.Length}단계 · Enter로 실행", null, r, null, "\ue82f")).ToList(); + if (!list3.Any()) + { + list3.Add(new LauncherItem("'" + q + "' 루틴 없음", "routines.json에서 직접 추가하거나 routine으로 목록 확인", null, null, null, "\ue7ba")); + } + return Task.FromResult((IEnumerable)list3); + } + + public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + if (!(data is RoutineDefinition routine)) + { + return; + } + int executed = 0; + RoutineStep[] steps = routine.Steps; + foreach (RoutineStep step in steps) + { + try + { + switch (step.Type.ToLowerInvariant()) + { + case "app": + case "url": + case "folder": + Process.Start(new ProcessStartInfo(step.Target) + { + UseShellExecute = true + }); + break; + case "cmd": + Process.Start(new ProcessStartInfo("powershell.exe", "-Command \"" + step.Target + "\"") + { + UseShellExecute = false, + CreateNoWindow = true + }); + break; + case "info": + NotificationService.Notify("루틴", step.Label); + break; + } + executed++; + await Task.Delay(300, ct); + } + catch (Exception ex) + { + LogService.Warn("루틴 단계 실행 실패: " + step.Label + " — " + ex.Message); + } + } + NotificationService.Notify("루틴 완료", $"[{routine.Name}] {executed}/{routine.Steps.Length}단계 실행 완료"); + } + + private List LoadRoutines() + { + List list = new List(BuiltInRoutines); + try + { + if (File.Exists(RoutineFile)) + { + string json = File.ReadAllText(RoutineFile); + List list2 = JsonSerializer.Deserialize>(json, JsonOpts); + if (list2 != null) + { + foreach (RoutineDefinition r in list2) + { + list.RemoveAll((RoutineDefinition x) => x.Name.Equals(r.Name, StringComparison.OrdinalIgnoreCase)); + list.Add(r); + } + } + } + } + catch (Exception ex) + { + LogService.Warn("루틴 로드 실패: " + ex.Message); + } + return list; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/RunHandler.cs b/.decompiledproj/AxCopilot/Handlers/RunHandler.cs new file mode 100644 index 0000000..62d30ec --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/RunHandler.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using AxCopilot.SDK; +using AxCopilot.Views; + +namespace AxCopilot.Handlers; + +public class RunHandler : IActionHandler +{ + public string? Prefix => "^"; + + public PluginMetadata Metadata => new PluginMetadata("Run", "Windows 실행 명령", "1.0", "AX Copilot"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string text = query.Trim(); + if (string.IsNullOrEmpty(text)) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlyArray(new LauncherItem[5] + { + new LauncherItem("Windows 실행 명령", "Win+R 실행 창과 동일 · 명령어 입력 후 Enter", null, null, null, "\ue7c4"), + new LauncherItem("^ notepad", "메모장 실행", null, null, null, "\ue946"), + new LauncherItem("^ cmd", "명령 프롬프트", null, null, null, "\ue946"), + new LauncherItem("^ control", "제어판", null, null, null, "\ue946"), + new LauncherItem("^ calc", "계산기", null, null, null, "\ue946") + })); + } + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("실행: " + text, "Enter → Windows 실행 명령으로 실행", null, "__RUN__" + text, null, "\ue7c4"))); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (!(item.Data is string text) || !text.StartsWith("__RUN__")) + { + return Task.CompletedTask; + } + string text2 = text; + int length = "__RUN__".Length; + string text3 = text2.Substring(length, text2.Length - length).Trim(); + if (string.IsNullOrEmpty(text3)) + { + return Task.CompletedTask; + } + try + { + Process.Start(new ProcessStartInfo(text3) + { + UseShellExecute = true + })?.Dispose(); + } + catch (Exception ex) + { + CustomMessageBox.Show("실행 실패: " + ex.Message, "AX Copilot", MessageBoxButton.OK, MessageBoxImage.Hand); + } + return Task.CompletedTask; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/ScaffoldHandler.cs b/.decompiledproj/AxCopilot/Handlers/ScaffoldHandler.cs new file mode 100644 index 0000000..1c5298f --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/ScaffoldHandler.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class ScaffoldHandler : IActionHandler +{ + internal record ScaffoldTemplate([property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("description")] string Description, [property: JsonPropertyName("paths")] string[] Paths); + + private static readonly string TemplateDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "templates"); + + private static readonly ScaffoldTemplate[] BuiltInTemplates = new ScaffoldTemplate[5] + { + new ScaffoldTemplate("webapi", "Web API 프로젝트", new string[8] { "src/Controllers/", "src/Models/", "src/Services/", "src/Middleware/", "tests/", "docs/", "README.md", ".gitignore" }), + new ScaffoldTemplate("console", "콘솔 애플리케이션", new string[5] { "src/", "src/Core/", "src/Services/", "tests/", "README.md" }), + new ScaffoldTemplate("wpf", "WPF 데스크톱 앱", new string[8] { "src/Views/", "src/ViewModels/", "src/Models/", "src/Services/", "src/Themes/", "src/Assets/", "tests/", "docs/" }), + new ScaffoldTemplate("data", "데이터 파이프라인", new string[9] { "src/Extractors/", "src/Transformers/", "src/Loaders/", "config/", "scripts/", "tests/", "data/input/", "data/output/", "README.md" }), + new ScaffoldTemplate("docs", "문서 프로젝트", new string[5] { "docs/", "images/", "templates/", "README.md", "CHANGELOG.md" }) + }; + + public string? Prefix => "scaffold"; + + public PluginMetadata Metadata => new PluginMetadata("Scaffold", "프로젝트 스캐폴딩 — scaffold", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string q = query.Trim(); + IEnumerable second = LoadUserTemplates(); + List list = BuiltInTemplates.Concat(second).ToList(); + if (string.IsNullOrWhiteSpace(q)) + { + List list2 = list.Select((ScaffoldTemplate t) => new LauncherItem("[" + t.Name + "] " + t.Description, $"{t.Paths.Length}개 폴더/파일 · Enter → 대상 경로 입력 후 생성", null, t, null, "\ue8b7")).ToList(); + list2.Insert(0, new LauncherItem("프로젝트 스캐폴딩", $"총 {list.Count}개 템플릿 · 이름을 입력해 필터링", null, null, null, "\ue946")); + return Task.FromResult((IEnumerable)list2); + } + if (q.Contains('\\') || q.Contains('/')) + { + int num = q.LastIndexOf(' '); + if (num > 0) + { + string text = q.Substring(0, num).Trim(); + string text2 = q; + int num2 = num + 1; + string templateName = text2.Substring(num2, text2.Length - num2).Trim(); + ScaffoldTemplate scaffoldTemplate = list.FirstOrDefault((ScaffoldTemplate t) => t.Name.Equals(templateName, StringComparison.OrdinalIgnoreCase)); + if (scaffoldTemplate != null) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("[" + scaffoldTemplate.Name + "] → " + text, $"{scaffoldTemplate.Paths.Length}개 폴더/파일 생성 · Enter로 실행", null, ValueTuple.Create(text, scaffoldTemplate), null, "\ue74e"))); + } + } + } + IEnumerable source = list.Where((ScaffoldTemplate t) => t.Name.Contains(q, StringComparison.OrdinalIgnoreCase) || t.Description.Contains(q, StringComparison.OrdinalIgnoreCase)); + List list3 = source.Select(delegate(ScaffoldTemplate t) + { + string text3 = string.Join(", ", t.Paths.Take(4)); + if (t.Paths.Length > 4) + { + text3 += $" ... (+{t.Paths.Length - 4})"; + } + return new LauncherItem("[" + t.Name + "] " + t.Description, text3 + " · 사용법: scaffold [대상경로] " + t.Name, null, t, null, "\ue8b7"); + }).ToList(); + if (!list3.Any()) + { + list3.Add(new LauncherItem("'" + q + "'에 해당하는 템플릿 없음", "scaffold 으로 전체 목록 확인", null, null, null, "\ue7ba")); + } + return Task.FromResult((IEnumerable)list3); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (!(item.Data is (string, ScaffoldTemplate) tuple)) + { + if (item.Data is ScaffoldTemplate scaffoldTemplate) + { + string usage = "scaffold [대상경로] " + scaffoldTemplate.Name; + try + { + Application current = Application.Current; + if (current != null) + { + ((DispatcherObject)current).Dispatcher.Invoke((Action)delegate + { + Clipboard.SetText(usage); + }); + } + } + catch + { + } + } + return Task.CompletedTask; + } + var (basePath, template) = tuple; + return CreateStructure(basePath, template); + } + + private static Task CreateStructure(string basePath, ScaffoldTemplate template) + { + try + { + int num = 0; + string[] paths = template.Paths; + foreach (string text in paths) + { + string path = Path.Combine(basePath, text.Replace('/', Path.DirectorySeparatorChar)); + if (text.EndsWith('/') || text.EndsWith('\\') || !Path.HasExtension(text)) + { + Directory.CreateDirectory(path); + } + else + { + Directory.CreateDirectory(Path.GetDirectoryName(path)); + if (!File.Exists(path)) + { + File.WriteAllText(path, ""); + } + } + num++; + } + NotificationService.Notify("스캐폴딩 완료", $"[{template.Name}] {num}개 항목 생성 → {basePath}"); + } + catch (Exception ex) + { + LogService.Error("스캐폴딩 실패: " + ex.Message); + NotificationService.Notify("AX Copilot", "스캐폴딩 실패: " + ex.Message); + } + return Task.CompletedTask; + } + + private static IEnumerable LoadUserTemplates() + { + if (!Directory.Exists(TemplateDir)) + { + yield break; + } + string[] files = Directory.GetFiles(TemplateDir, "*.json"); + foreach (string file in files) + { + ScaffoldTemplate tmpl = null; + try + { + string json = File.ReadAllText(file); + tmpl = JsonSerializer.Deserialize(json); + } + catch + { + } + if (tmpl != null) + { + yield return tmpl; + } + } + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/ScreenCaptureHandler.cs b/.decompiledproj/AxCopilot/Handlers/ScreenCaptureHandler.cs new file mode 100644 index 0000000..e5b52cf --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/ScreenCaptureHandler.cs @@ -0,0 +1,612 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Forms; +using System.Windows.Interop; +using System.Windows.Media.Imaging; +using System.Windows.Threading; +using AxCopilot.SDK; +using AxCopilot.Services; +using AxCopilot.Views; + +namespace AxCopilot.Handlers; + +public class ScreenCaptureHandler : IActionHandler +{ + private struct RECT + { + public int left; + + public int top; + + public int right; + + public int bottom; + } + + private readonly SettingsService _settings; + + private const int SW_RESTORE = 9; + + private const byte VK_NEXT = 34; + + private const uint WM_VSCROLL = 277u; + + private const uint WM_KEYDOWN = 256u; + + private const uint WM_KEYUP = 257u; + + private const int SB_PAGEDOWN = 3; + + private static readonly (string Key, string Label, string Desc)[] _options = new(string, string, string)[4] + { + ("region", "영역 선택 캡처", "마우스로 드래그하여 원하는 영역만 캡처 · Shift+Enter: 타이머 캡처"), + ("window", "활성 창 캡처", "런처 호출 전 활성 창만 캡처 · Shift+Enter: 타이머 캡처"), + ("scroll", "스크롤 캡처", "활성 창을 끝까지 스크롤하며 페이지 전체 캡처 · Shift+Enter: 타이머 캡처"), + ("screen", "전체 화면 캡처", "모든 모니터를 포함한 전체 화면 · Shift+Enter: 타이머 캡처") + }; + + public string? Prefix => string.IsNullOrWhiteSpace(_settings.Settings.ScreenCapture.Prefix) ? "cap" : _settings.Settings.ScreenCapture.Prefix.Trim(); + + public PluginMetadata Metadata => new PluginMetadata("ScreenCapture", "화면 캡처 — cap screen/window/scroll/region", "1.0", "AX"); + + internal int ScrollDelayMs => Math.Max(50, _settings.Settings.ScreenCapture.ScrollDelayMs); + + public ScreenCaptureHandler(SettingsService settings) + { + _settings = settings; + } + + [DllImport("user32.dll")] + private static extern bool GetWindowRect(nint hWnd, out RECT lpRect); + + [DllImport("user32.dll")] + private static extern bool IsWindow(nint hWnd); + + [DllImport("user32.dll")] + private static extern bool SetForegroundWindow(nint hWnd); + + [DllImport("user32.dll")] + private static extern bool ShowWindow(nint hWnd, int nCmdShow); + + [DllImport("user32.dll")] + private static extern bool PrintWindow(nint hwnd, nint hdcBlt, uint nFlags); + + [DllImport("user32.dll")] + private static extern bool IsWindowVisible(nint hWnd); + + [DllImport("user32.dll")] + private static extern nint SendMessage(nint hWnd, uint Msg, nint wParam, nint lParam); + + [DllImport("user32.dll")] + private static extern nint FindWindowEx(nint parent, nint child, string? className, string? windowText); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string q = query.Trim().ToLowerInvariant(); + string saveHint = "클립보드에 복사"; + IEnumerable<(string, string, string)> enumerable; + if (!string.IsNullOrWhiteSpace(q)) + { + enumerable = _options.Where(((string Key, string Label, string Desc) o) => o.Key.StartsWith(q) || o.Label.Contains(q)); + } + else + { + IEnumerable<(string, string, string)> options = _options; + enumerable = options; + } + IEnumerable<(string, string, string)> source = enumerable; + List list = source.Select<(string, string, string), LauncherItem>(((string Key, string Label, string Desc) o) => new LauncherItem(o.Label, o.Desc + " · " + saveHint, null, o.Key, null, "\ue722")).ToList(); + if (!list.Any()) + { + list.Add(new LauncherItem("알 수 없는 캡처 모드: " + q, "screen / window / scroll / region", null, null, null, "\ue7ba")); + } + return Task.FromResult((IEnumerable)list); + } + + public IEnumerable GetDelayItems(string mode) + { + string text = _options.FirstOrDefault(((string Key, string Label, string Desc) o) => o.Key == mode).Label ?? mode; + return new LauncherItem[3] + { + new LauncherItem("3초 후 " + text, "Shift+Enter로 지연 캡처", null, "delay:" + mode + ":3", null, "\ue916"), + new LauncherItem("5초 후 " + text, "Shift+Enter로 지연 캡처", null, "delay:" + mode + ":5", null, "\ue916"), + new LauncherItem("10초 후 " + text, "Shift+Enter로 지연 캡처", null, "delay:" + mode + ":10", null, "\ue916") + }; + } + + public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + if (!(data is string data2)) + { + return; + } + if (data2.StartsWith("delay:")) + { + string[] parts = data2.Split(':'); + if (parts.Length == 3 && int.TryParse(parts[2], out var delaySec)) + { + await ExecuteDelayedCaptureAsync(parts[1], delaySec, ct); + return; + } + } + await Task.Delay(150, ct); + await CaptureDirectAsync(data2, ct); + } + + private async Task ExecuteDelayedCaptureAsync(string mode, int delaySec, CancellationToken ct) + { + await Task.Delay(200, ct); + for (int i = delaySec; i > 0; i--) + { + ct.ThrowIfCancellationRequested(); + await Task.Delay(1000, ct); + } + await CaptureDirectAsync(mode, ct); + } + + public async Task CaptureDirectAsync(string mode, CancellationToken ct = default(CancellationToken)) + { + try + { + string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + switch (mode) + { + case "screen": + await CaptureScreenAsync(timestamp); + break; + case "window": + await CaptureWindowAsync(timestamp); + break; + case "scroll": + await CaptureScrollAsync(timestamp, ct); + break; + case "region": + await CaptureRegionAsync(timestamp, ct); + break; + } + } + catch (Exception ex) + { + Exception ex2 = ex; + LogService.Error("캡처 실패: " + ex2.Message); + NotificationService.Notify("AX Copilot", "캡처 실패: " + ex2.Message); + } + } + + private async Task CaptureScreenAsync(string timestamp) + { + Rectangle bounds = GetAllScreenBounds(); + using Bitmap bmp = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb); + using Graphics g = Graphics.FromImage(bmp); + g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size, CopyPixelOperation.SourceCopy); + CopyToClipboard(bmp); + await Task.Delay(10); + NotificationService.Notify("화면 캡처 완료", "클립보드에 복사되었습니다"); + } + + private async Task CaptureWindowAsync(string timestamp) + { + nint hwnd = WindowTracker.PreviousWindow; + if (hwnd == IntPtr.Zero || !IsWindow(hwnd)) + { + NotificationService.Notify("AX Copilot", "캡처할 창이 없습니다. 런처 호출 전 창을 확인하세요."); + return; + } + nint launcherHwnd = GetLauncherHwnd(); + if (launcherHwnd != IntPtr.Zero) + { + for (int i = 0; i < 10; i++) + { + if (!IsWindowVisible(launcherHwnd)) + { + break; + } + await Task.Delay(50); + } + } + ShowWindow(hwnd, 9); + SetForegroundWindow(hwnd); + await Task.Delay(150); + if (!GetWindowRect(hwnd, out var rect)) + { + return; + } + int w = rect.right - rect.left; + int h = rect.bottom - rect.top; + if (w <= 0 || h <= 0) + { + return; + } + using Bitmap bmp = CaptureWindow(hwnd, w, h, rect); + CopyToClipboard(bmp); + NotificationService.Notify("창 캡처 완료", "클립보드에 복사되었습니다"); + } + + private async Task CaptureScrollAsync(string timestamp, CancellationToken ct) + { + nint hwnd = WindowTracker.PreviousWindow; + if (hwnd == IntPtr.Zero || !IsWindow(hwnd)) + { + NotificationService.Notify("AX Copilot", "캡처할 창이 없습니다."); + return; + } + nint launcherHwnd = GetLauncherHwnd(); + if (launcherHwnd != IntPtr.Zero) + { + for (int i = 0; i < 10; i++) + { + if (!IsWindowVisible(launcherHwnd)) + { + break; + } + await Task.Delay(50, ct); + } + } + ShowWindow(hwnd, 9); + SetForegroundWindow(hwnd); + await Task.Delay(200, ct); + if (!GetWindowRect(hwnd, out var rect)) + { + return; + } + int w = rect.right - rect.left; + int h = rect.bottom - rect.top; + if (w <= 0 || h <= 0) + { + return; + } + nint scrollTarget = FindScrollableChild(hwnd); + List frames = new List { CaptureWindow(hwnd, w, h, rect) }; + for (int j = 0; j < 14; j++) + { + ct.ThrowIfCancellationRequested(); + if (scrollTarget != IntPtr.Zero) + { + SendMessage(scrollTarget, 277u, new IntPtr(3), IntPtr.Zero); + } + else + { + SendPageDown(hwnd); + } + await Task.Delay(ScrollDelayMs, ct); + if (!GetWindowRect(hwnd, out var newRect)) + { + break; + } + Bitmap frame = CaptureWindow(hwnd, w, h, newRect); + int num; + if (frames.Count > 0) + { + num = (AreSimilar(frames[frames.Count - 1], frame) ? 1 : 0); + } + else + { + num = 0; + } + if (num != 0) + { + frame.Dispose(); + break; + } + frames.Add(frame); + } + using Bitmap stitched = StitchFrames(frames, h); + foreach (Bitmap f in frames) + { + f.Dispose(); + } + CopyToClipboard(stitched); + NotificationService.Notify("스크롤 캡처 완료", $"{stitched.Height}px · 클립보드에 복사되었습니다"); + } + + private static Bitmap CaptureWindow(nint hwnd, int w, int h, RECT rect) + { + Bitmap bitmap = new Bitmap(w, h, PixelFormat.Format32bppArgb); + using Graphics graphics = Graphics.FromImage(bitmap); + nint hdc = graphics.GetHdc(); + bool flag = PrintWindow(hwnd, hdc, 2u); + graphics.ReleaseHdc(hdc); + if (!flag) + { + graphics.CopyFromScreen(rect.left, rect.top, 0, 0, new Size(w, h), CopyPixelOperation.SourceCopy); + } + return bitmap; + } + + private static Rectangle GetAllScreenBounds() + { + Screen[] allScreens = Screen.AllScreens; + int num = allScreens.Min((Screen s) => s.Bounds.X); + int num2 = allScreens.Min((Screen s) => s.Bounds.Y); + int num3 = allScreens.Max((Screen s) => s.Bounds.Right); + int num4 = allScreens.Max((Screen s) => s.Bounds.Bottom); + return new Rectangle(num, num2, num3 - num, num4 - num2); + } + + private static nint FindScrollableChild(nint hwnd) + { + string[] array = new string[7] { "Internet Explorer_Server", "Chrome_RenderWidgetHostHWND", "MozillaWindowClass", "RichEdit20W", "RICHEDIT50W", "TextBox", "EDIT" }; + foreach (string className in array) + { + nint num = FindWindowEx(hwnd, IntPtr.Zero, className, null); + if (num != IntPtr.Zero) + { + return num; + } + } + return IntPtr.Zero; + } + + private static void SendPageDown(nint hwnd) + { + SendMessage(hwnd, 256u, new IntPtr(34), IntPtr.Zero); + SendMessage(hwnd, 257u, new IntPtr(34), IntPtr.Zero); + } + + private unsafe static bool AreSimilar(Bitmap a, Bitmap b) + { + if (a.Width != b.Width || a.Height != b.Height) + { + return false; + } + int num = (int)((double)a.Height * 0.8); + int width = a.Width; + int height = a.Height; + Rectangle rect = new Rectangle(0, num, width, height - num); + Rectangle rect2 = new Rectangle(0, num, width, height - num); + BitmapData bitmapData = a.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + BitmapData bitmapData2 = b.LockBits(rect2, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + try + { + int num2 = 0; + int num3 = 0; + int stride = bitmapData.Stride; + int num4 = width / 16 + 1; + int num5 = (height - num) / 8 + 1; + byte* ptr = (byte*)((IntPtr)bitmapData.Scan0).ToPointer(); + byte* ptr2 = (byte*)((IntPtr)bitmapData2.Scan0).ToPointer(); + for (int i = 0; i < num5; i++) + { + int num6 = i * 8; + if (num6 >= height - num) + { + break; + } + for (int j = 0; j < num4; j++) + { + int num7 = j * 16; + if (num7 >= width) + { + break; + } + int num8 = num6 * stride + num7 * 4; + if (Math.Abs(ptr[num8] - ptr2[num8]) < 5 && Math.Abs(ptr[num8 + 1] - ptr2[num8 + 1]) < 5 && Math.Abs(ptr[num8 + 2] - ptr2[num8 + 2]) < 5) + { + num2++; + } + num3++; + } + } + return num3 > 0 && (double)num2 / (double)num3 > 0.97; + } + finally + { + a.UnlockBits(bitmapData); + b.UnlockBits(bitmapData2); + } + } + + private static Bitmap StitchFrames(List frames, int windowHeight) + { + if (frames.Count == 0) + { + return new Bitmap(1, 1); + } + if (frames.Count == 1) + { + return new Bitmap(frames[0]); + } + int width = frames[0].Width; + List list = new List(); + List list2 = new List(); + int num = windowHeight; + for (int i = 1; i < frames.Count; i++) + { + int num2 = FindOverlap(frames[i - 1], frames[i]); + int num3 = ((num2 > 0) ? num2 : (windowHeight / 5)); + int num4 = windowHeight - num3; + if (num4 <= 0) + { + num4 = windowHeight / 4; + num3 = windowHeight - num4; + } + list.Add(num3); + list2.Add(num4); + num += num4; + } + Bitmap bitmap = new Bitmap(width, num, PixelFormat.Format32bppArgb); + using Graphics graphics = Graphics.FromImage(bitmap); + graphics.DrawImage(frames[0], 0, 0, width, windowHeight); + int num5 = windowHeight; + for (int j = 1; j < frames.Count; j++) + { + int y = list[j - 1]; + int num6 = list2[j - 1]; + graphics.DrawImage(srcRect: new Rectangle(0, y, width, num6), destRect: new Rectangle(0, num5, width, num6), image: frames[j], srcUnit: GraphicsUnit.Pixel); + num5 += num6; + } + return bitmap; + } + + private unsafe static int FindOverlap(Bitmap prev, Bitmap next) + { + int num = Math.Min(prev.Width, next.Width); + int height = prev.Height; + if (height < 16 || num < 16) + { + return 0; + } + int num2 = (int)((double)height * 0.7); + BitmapData bitmapData = prev.LockBits(new Rectangle(0, 0, prev.Width, prev.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + BitmapData bitmapData2 = next.LockBits(new Rectangle(0, 0, next.Width, next.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + try + { + int stride = bitmapData.Stride; + int stride2 = bitmapData2.Stride; + int result = 0; + byte* ptr = (byte*)((IntPtr)bitmapData.Scan0).ToPointer(); + byte* ptr2 = (byte*)((IntPtr)bitmapData2.Scan0).ToPointer(); + for (int num3 = num2; num3 > 8; num3 -= 2) + { + int num4 = height - num3; + if (num4 >= 0) + { + int num5 = 0; + int num6 = 0; + for (int i = 0; i < 8; i++) + { + int num7 = i * (num3 / 8); + int num8 = num4 + num7; + int num9 = num7; + if (num8 >= height || num9 >= next.Height) + { + continue; + } + for (int j = 4; j < num - 4; j += 12) + { + int num10 = num8 * stride + j * 4; + int num11 = num9 * stride2 + j * 4; + if (num10 + 2 < bitmapData.Height * stride && num11 + 2 < bitmapData2.Height * stride2) + { + if (Math.Abs(ptr[num10] - ptr2[num11]) < 10 && Math.Abs(ptr[num10 + 1] - ptr2[num11 + 1]) < 10 && Math.Abs(ptr[num10 + 2] - ptr2[num11 + 2]) < 10) + { + num5++; + } + num6++; + } + } + } + if (num6 > 0 && (double)num5 / (double)num6 > 0.8) + { + result = num3; + break; + } + } + } + return result; + } + finally + { + prev.UnlockBits(bitmapData); + next.UnlockBits(bitmapData2); + } + } + + private async Task CaptureRegionAsync(string timestamp, CancellationToken ct) + { + Rectangle bounds = GetAllScreenBounds(); + Bitmap fullBmp = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb); + try + { + using (Graphics g = Graphics.FromImage(fullBmp)) + { + g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size, CopyPixelOperation.SourceCopy); + } + Rectangle? selected = null; + System.Windows.Application current = System.Windows.Application.Current; + if (current != null) + { + ((DispatcherObject)current).Dispatcher.Invoke((Action)delegate + { + RegionSelectWindow regionSelectWindow = new RegionSelectWindow(fullBmp, bounds); + regionSelectWindow.ShowDialog(); + selected = regionSelectWindow.SelectedRect; + }); + } + if (!selected.HasValue || selected.Value.Width < 4 || selected.Value.Height < 4) + { + NotificationService.Notify("AX Copilot", "영역 선택이 취소되었습니다."); + return; + } + Rectangle r = selected.Value; + using Bitmap crop = new Bitmap(r.Width, r.Height, PixelFormat.Format32bppArgb); + using (Graphics g2 = Graphics.FromImage(crop)) + { + g2.DrawImage(fullBmp, new Rectangle(0, 0, r.Width, r.Height), r, GraphicsUnit.Pixel); + } + CopyToClipboard(crop); + NotificationService.Notify("영역 캡처 완료", $"{r.Width}×{r.Height} · 클립보드에 복사되었습니다"); + await Task.CompletedTask; + } + finally + { + if (fullBmp != null) + { + ((IDisposable)fullBmp).Dispose(); + } + } + } + + private static void CopyToClipboard(Bitmap bmp) + { + try + { + System.Windows.Application current = System.Windows.Application.Current; + if (current == null) + { + return; + } + ((DispatcherObject)current).Dispatcher.Invoke((Action)delegate + { + using MemoryStream memoryStream = new MemoryStream(); + bmp.Save(memoryStream, ImageFormat.Bmp); + memoryStream.Position = 0L; + BitmapImage bitmapImage = new BitmapImage(); + bitmapImage.BeginInit(); + bitmapImage.StreamSource = memoryStream; + bitmapImage.CacheOption = BitmapCacheOption.OnLoad; + bitmapImage.EndInit(); + ((Freezable)bitmapImage).Freeze(); + System.Windows.Clipboard.SetImage(bitmapImage); + }); + } + catch (Exception ex) + { + LogService.Warn("클립보드 이미지 복사 실패: " + ex.Message); + } + } + + private static nint GetLauncherHwnd() + { + try + { + nint hwnd = IntPtr.Zero; + System.Windows.Application current = System.Windows.Application.Current; + if (current != null) + { + ((DispatcherObject)current).Dispatcher.Invoke((Action)delegate + { + Window window = System.Windows.Application.Current.Windows.OfType().FirstOrDefault((Window w) => ((object)w).GetType().Name == "LauncherWindow"); + if (window != null) + { + hwnd = new WindowInteropHelper(window).Handle; + } + }); + } + return hwnd; + } + catch + { + return IntPtr.Zero; + } + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/ServiceHandler.cs b/.decompiledproj/AxCopilot/Handlers/ServiceHandler.cs new file mode 100644 index 0000000..93eeb97 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/ServiceHandler.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.ServiceProcess; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class ServiceHandler : IActionHandler +{ + private readonly ClipboardHistoryService? _clipboardService; + + public string? Prefix => "svc"; + + public PluginMetadata Metadata => new PluginMetadata("Service", "Windows 서비스 관리 — svc", "1.0", "AX"); + + public ServiceHandler(ClipboardHistoryService? clipboardService = null) + { + _clipboardService = clipboardService; + } + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string q = query.Trim(); + List list = new List(); + if (q.Equals("restart clipboard", StringComparison.OrdinalIgnoreCase) || q.Equals("클립보드 재시작", StringComparison.OrdinalIgnoreCase)) + { + list.Add(new LauncherItem("AX 클립보드 히스토리 서비스 재시작", "클립보드 감지가 작동하지 않을 때 사용 · Enter로 실행", null, "__RESTART_CLIPBOARD__", null, "\ue777")); + return Task.FromResult((IEnumerable)list); + } + if (q.StartsWith("start ", StringComparison.OrdinalIgnoreCase) || q.StartsWith("stop ", StringComparison.OrdinalIgnoreCase) || q.StartsWith("restart ", StringComparison.OrdinalIgnoreCase)) + { + string[] array = q.Split(' ', 2, StringSplitOptions.TrimEntries); + string text = array[0].ToLowerInvariant(); + string svcName = ((array.Length > 1) ? array[1] : ""); + if (!string.IsNullOrWhiteSpace(svcName)) + { + try + { + ServiceController serviceController = ServiceController.GetServices().FirstOrDefault((ServiceController s) => s.ServiceName.Contains(svcName, StringComparison.OrdinalIgnoreCase) || s.DisplayName.Contains(svcName, StringComparison.OrdinalIgnoreCase)); + if (serviceController != null) + { + if (1 == 0) + { + } + string text2 = text switch + { + "start" => "시작", + "stop" => "중지", + "restart" => "재시작", + _ => text, + }; + if (1 == 0) + { + } + string text3 = text2; + list.Add(new LauncherItem("[" + text3 + "] " + serviceController.DisplayName, $"서비스명: {serviceController.ServiceName} · 현재 상태: {StatusText(serviceController.Status)} · Enter로 실행", null, ("__" + text.ToUpperInvariant() + "__", serviceController.ServiceName), null, (text == "stop") ? "\uea39" : "\ue777")); + } + else + { + list.Add(new LauncherItem("'" + svcName + "' 서비스를 찾을 수 없습니다", "서비스 이름 또는 표시 이름으로 검색", null, null, null, "\ue7ba")); + } + } + catch (Exception ex) + { + list.Add(new LauncherItem("서비스 조회 실패", ex.Message, null, null, null, "\uea39")); + } + return Task.FromResult((IEnumerable)list); + } + } + try + { + ServiceController[] services = ServiceController.GetServices(); + IEnumerable enumerable = (string.IsNullOrWhiteSpace(q) ? (from s in services + where s.Status == ServiceControllerStatus.Running + orderby s.DisplayName + select s).Take(20) : (from s in services + where s.ServiceName.Contains(q, StringComparison.OrdinalIgnoreCase) || s.DisplayName.Contains(q, StringComparison.OrdinalIgnoreCase) + orderby s.DisplayName + select s).Take(20)); + list.Add(new LauncherItem("AX 클립보드 히스토리 서비스 재시작", "svc restart clipboard · 클립보드 감지 문제 시 사용", null, "__RESTART_CLIPBOARD__", null, "\ue777")); + foreach (ServiceController item in enumerable) + { + string text4 = StatusText(item.Status); + string symbol = ((item.Status == ServiceControllerStatus.Running) ? "\ue73e" : "\ue711"); + list.Add(new LauncherItem("[" + text4 + "] " + item.DisplayName, item.ServiceName + " · svc start/stop/restart " + item.ServiceName, null, item.ServiceName, null, symbol)); + } + } + catch (Exception ex2) + { + list.Add(new LauncherItem("서비스 조회 실패", ex2.Message, null, null, null, "\uea39")); + } + return Task.FromResult((IEnumerable)list); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is string text && text == "__RESTART_CLIPBOARD__") + { + RestartClipboardService(); + return Task.CompletedTask; + } + if (!(item.Data is (string, string) tuple)) + { + object data = item.Data; + string name = data as string; + if (name != null) + { + try + { + Application current = Application.Current; + if (current != null) + { + ((DispatcherObject)current).Dispatcher.Invoke((Action)delegate + { + Clipboard.SetText(name); + }); + } + } + catch + { + } + } + return Task.CompletedTask; + } + var (action, svcName) = tuple; + return ExecuteServiceAction(action, svcName); + } + + private void RestartClipboardService() + { + try + { + if (_clipboardService == null) + { + NotificationService.Notify("AX Copilot", "클립보드 서비스 참조를 찾을 수 없습니다."); + return; + } + _clipboardService.Dispose(); + Application current = Application.Current; + if (current == null) + { + return; + } + ((DispatcherObject)current).Dispatcher.BeginInvoke((Delegate)(Action)delegate + { + try + { + _clipboardService.Reinitialize(); + NotificationService.Notify("AX Copilot", "클립보드 히스토리 서비스가 재시작되었습니다."); + LogService.Info("클립보드 히스토리 서비스 강제 재시작 완료"); + } + catch (Exception ex2) + { + NotificationService.Notify("AX Copilot", "클립보드 재시작 실패: " + ex2.Message); + LogService.Error("클립보드 재시작 실패: " + ex2.Message); + } + }, (DispatcherPriority)4, Array.Empty()); + } + catch (Exception ex) + { + NotificationService.Notify("AX Copilot", "클립보드 재시작 실패: " + ex.Message); + } + } + + private static async Task ExecuteServiceAction(string action, string svcName) + { + try + { + if (1 == 0) + { + } + string text = action switch + { + "__START__" => "start", + "__STOP__" => "stop", + "__RESTART__" => "stop", + _ => null, + }; + if (1 == 0) + { + } + string verb = text; + if (verb != null) + { + ProcessStartInfo psi = new ProcessStartInfo("sc.exe", verb + " \"" + svcName + "\"") + { + Verb = "runas", + UseShellExecute = true, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + }; + Process.Start(psi)?.WaitForExit(5000); + if (action == "__RESTART__") + { + await Task.Delay(1000); + ProcessStartInfo startPsi = new ProcessStartInfo("sc.exe", "start \"" + svcName + "\"") + { + Verb = "runas", + UseShellExecute = true, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + }; + Process.Start(startPsi)?.WaitForExit(5000); + } + if (1 == 0) + { + } + text = action switch + { + "__START__" => "시작", + "__STOP__" => "중지", + "__RESTART__" => "재시작", + _ => action, + }; + if (1 == 0) + { + } + string label = text; + NotificationService.Notify("서비스 관리", svcName + " " + label + " 요청 완료"); + } + } + catch (Exception ex) + { + Exception ex2 = ex; + NotificationService.Notify("AX Copilot", "서비스 제어 실패: " + ex2.Message); + } + } + + private static string StatusText(ServiceControllerStatus status) + { + if (1 == 0) + { + } + string result = status switch + { + ServiceControllerStatus.Running => "실행 중", + ServiceControllerStatus.Stopped => "중지됨", + ServiceControllerStatus.StartPending => "시작 중", + ServiceControllerStatus.StopPending => "중지 중", + ServiceControllerStatus.Paused => "일시 중지", + ServiceControllerStatus.ContinuePending => "재개 중", + ServiceControllerStatus.PausePending => "일시 중지 중", + _ => status.ToString(), + }; + if (1 == 0) + { + } + return result; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/SnapHandler.cs b/.decompiledproj/AxCopilot/Handlers/SnapHandler.cs new file mode 100644 index 0000000..97e62bf --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/SnapHandler.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class SnapHandler : IActionHandler +{ + private struct RECT + { + public int left; + + public int top; + + public int right; + + public int bottom; + } + + private struct MONITORINFO + { + public int cbSize; + + public RECT rcMonitor; + + public RECT rcWork; + + public uint dwFlags; + } + + private const uint SWP_SHOWWINDOW = 64u; + + private const uint SWP_NOZORDER = 4u; + + private const uint MONITOR_DEFAULTTONEAREST = 2u; + + private const int SW_RESTORE = 9; + + private const int SW_MAXIMIZE = 3; + + private static readonly (string Key, string Label, string Desc)[] _snapOptions = new(string, string, string)[20] + { + ("left", "왼쪽 절반", "화면 왼쪽 50% 영역에 배치"), + ("right", "오른쪽 절반", "화면 오른쪽 50% 영역에 배치"), + ("top", "위쪽 절반", "화면 위쪽 50% 영역에 배치"), + ("bottom", "아래쪽 절반", "화면 아래쪽 50% 영역에 배치"), + ("tl", "좌상단 1/4", "화면 좌상단 25% 영역에 배치"), + ("tr", "우상단 1/4", "화면 우상단 25% 영역에 배치"), + ("bl", "좌하단 1/4", "화면 좌하단 25% 영역에 배치"), + ("br", "우하단 1/4", "화면 우하단 25% 영역에 배치"), + ("l-rt", "좌반 + 우상", "왼쪽 50% + 오른쪽 상단 25% (2창용)"), + ("l-rb", "좌반 + 우하", "왼쪽 50% + 오른쪽 하단 25% (2창용)"), + ("r-lt", "우반 + 좌상", "오른쪽 50% + 왼쪽 상단 25% (2창용)"), + ("r-lb", "우반 + 좌하", "오른쪽 50% + 왼쪽 하단 25% (2창용)"), + ("third-l", "좌측 1/3", "화면 왼쪽 33% 영역에 배치"), + ("third-c", "중앙 1/3", "화면 가운데 33% 영역에 배치"), + ("third-r", "우측 1/3", "화면 오른쪽 33% 영역에 배치"), + ("two3-l", "좌측 2/3", "화면 왼쪽 66% 영역에 배치"), + ("two3-r", "우측 2/3", "화면 오른쪽 66% 영역에 배치"), + ("full", "전체 화면", "최대화"), + ("center", "화면 중앙", "화면 중앙 80% 크기로 배치"), + ("restore", "원래 크기 복원", "창을 이전 크기로 복원") + }; + + public string? Prefix => "snap"; + + public PluginMetadata Metadata => new PluginMetadata("WindowSnap", "창 배치 — 2/3/4분할, 1/3·2/3, 전체화면, 중앙, 복원", "1.1", "AX"); + + [DllImport("user32.dll")] + private static extern bool SetWindowPos(nint hWnd, nint hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags); + + [DllImport("user32.dll")] + private static extern bool ShowWindow(nint hWnd, int nCmdShow); + + [DllImport("user32.dll")] + private static extern nint MonitorFromWindow(nint hwnd, uint dwFlags); + + [DllImport("user32.dll")] + private static extern bool GetMonitorInfo(nint hMonitor, ref MONITORINFO lpmi); + + [DllImport("user32.dll")] + private static extern bool IsWindow(nint hWnd); + + [DllImport("user32.dll")] + private static extern bool IsIconic(nint hWnd); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string q = query.Trim().ToLowerInvariant(); + nint previousWindow = WindowTracker.PreviousWindow; + bool flag = previousWindow != IntPtr.Zero && IsWindow(previousWindow); + string hint = (flag ? "Enter로 현재 활성 창에 적용" : "대상 창 없음 — 런처를 열기 전 창에 적용됩니다"); + IEnumerable<(string, string, string)> enumerable; + if (!string.IsNullOrWhiteSpace(q)) + { + enumerable = _snapOptions.Where(((string Key, string Label, string Desc) o) => o.Key.StartsWith(q) || o.Label.Contains(q)); + } + else + { + IEnumerable<(string, string, string)> snapOptions = _snapOptions; + enumerable = snapOptions; + } + IEnumerable<(string, string, string)> source = enumerable; + List list = source.Select<(string, string, string), LauncherItem>(((string Key, string Label, string Desc) o) => new LauncherItem(o.Label, o.Desc + " · " + hint, null, o.Key, null, "\ue8a0")).ToList(); + if (!list.Any()) + { + list.Add(new LauncherItem("알 수 없는 스냅 방향: " + q, "left / right / tl / tr / bl / br / third-l/c/r / two3-l/r / full / center / restore", null, null, null, "\ue7ba")); + } + return Task.FromResult((IEnumerable)list); + } + + public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + if (!(data is string snapKey)) + { + return; + } + nint hwnd = WindowTracker.PreviousWindow; + if (hwnd != IntPtr.Zero && IsWindow(hwnd)) + { + if (IsIconic(hwnd)) + { + ShowWindow(hwnd, 9); + } + await Task.Delay(80, ct); + ApplySnap(hwnd, snapKey); + } + } + + private static void ApplySnap(nint hwnd, string key) + { + nint hMonitor = MonitorFromWindow(hwnd, 2u); + MONITORINFO lpmi = new MONITORINFO + { + cbSize = Marshal.SizeOf() + }; + if (!GetMonitorInfo(hMonitor, ref lpmi)) + { + return; + } + RECT rcWork = lpmi.rcWork; + int num = rcWork.right - rcWork.left; + int num2 = rcWork.bottom - rcWork.top; + int left = rcWork.left; + int top = rcWork.top; + if (key == "full") + { + ShowWindow(hwnd, 3); + return; + } + if (key == "restore") + { + ShowWindow(hwnd, 9); + return; + } + if (1 == 0) + { + } + (int, int, int, int) tuple = key switch + { + "left" => (left, top, num / 2, num2), + "right" => (left + num / 2, top, num / 2, num2), + "top" => (left, top, num, num2 / 2), + "bottom" => (left, top + num2 / 2, num, num2 / 2), + "tl" => (left, top, num / 2, num2 / 2), + "tr" => (left + num / 2, top, num / 2, num2 / 2), + "bl" => (left, top + num2 / 2, num / 2, num2 / 2), + "br" => (left + num / 2, top + num2 / 2, num / 2, num2 / 2), + "l-rt" => (left + num / 2, top, num / 2, num2 / 2), + "l-rb" => (left + num / 2, top + num2 / 2, num / 2, num2 / 2), + "r-lt" => (left, top, num / 2, num2 / 2), + "r-lb" => (left, top + num2 / 2, num / 2, num2 / 2), + "third-l" => (left, top, num / 3, num2), + "third-c" => (left + num / 3, top, num / 3, num2), + "third-r" => (left + num * 2 / 3, top, num / 3, num2), + "two3-l" => (left, top, num * 2 / 3, num2), + "two3-r" => (left + num / 3, top, num * 2 / 3, num2), + "center" => (left + num / 10, top + num2 / 10, num * 8 / 10, num2 * 8 / 10), + _ => (left, top, num, num2), + }; + if (1 == 0) + { + } + var (x, y, cx, cy) = tuple; + ShowWindow(hwnd, 9); + SetWindowPos(hwnd, IntPtr.Zero, x, y, cx, cy, 68u); + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/SnippetHandler.cs b/.decompiledproj/AxCopilot/Handlers/SnippetHandler.cs new file mode 100644 index 0000000..ed41230 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/SnippetHandler.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using AxCopilot.Models; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class SnippetHandler : IActionHandler +{ + private struct INPUT + { + public uint type; + + public InputUnion u; + } + + [StructLayout(LayoutKind.Explicit)] + private struct InputUnion + { + [FieldOffset(0)] + public KEYBDINPUT ki; + } + + private struct KEYBDINPUT + { + public ushort wVk; + + public ushort wScan; + + public uint dwFlags; + + public uint time; + + public nint dwExtraInfo; + } + + private readonly SettingsService _settings; + + public string? Prefix => ";"; + + public PluginMetadata Metadata => new PluginMetadata("Snippets", "텍스트 스니펫 — ; 뒤에 키워드 입력", "1.0", "AX"); + + public SnippetHandler(SettingsService settings) + { + _settings = settings; + } + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + List snippets = _settings.Settings.Snippets; + if (!snippets.Any()) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("등록된 스니펫이 없습니다", "설정 → 스니펫 탭에서 추가하세요", null, null, null, "\ue70b"))); + } + string q = query.Trim().ToLowerInvariant(); + List list = (from s in snippets + where string.IsNullOrEmpty(q) || s.Key.ToLowerInvariant().Contains(q) || s.Name.ToLowerInvariant().Contains(q) + select new LauncherItem((s.Name.Length > 0) ? s.Name : s.Key, TruncatePreview(s.Content), null, s, null, "\ue70b")).ToList(); + if (list.Count == 0) + { + list.Add(new LauncherItem("'" + query + "'에 해당하는 스니펫 없음", "설정에서 새 스니펫을 추가하세요", null, null, null, "\ue70b")); + } + return Task.FromResult((IEnumerable)list); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (!(item.Data is SnippetEntry snippetEntry)) + { + return Task.CompletedTask; + } + try + { + string text = ExpandVariables(snippetEntry.Content); + Clipboard.SetText(text); + Task.Delay(100, ct).ContinueWith(delegate + { + ((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate + { + INPUT[] array = new INPUT[4] + { + new INPUT + { + type = 1u, + u = new InputUnion + { + ki = new KEYBDINPUT + { + wVk = 17 + } + } + }, + new INPUT + { + type = 1u, + u = new InputUnion + { + ki = new KEYBDINPUT + { + wVk = 86 + } + } + }, + new INPUT + { + type = 1u, + u = new InputUnion + { + ki = new KEYBDINPUT + { + wVk = 86, + dwFlags = 2u + } + } + }, + new INPUT + { + type = 1u, + u = new InputUnion + { + ki = new KEYBDINPUT + { + wVk = 17, + dwFlags = 2u + } + } + } + }; + SendInput((uint)array.Length, array, Marshal.SizeOf()); + }); + }, ct, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default); + } + catch (Exception ex) + { + LogService.Warn("스니펫 실행 실패: " + ex.Message); + } + return Task.CompletedTask; + } + + private static string ExpandVariables(string content) + { + DateTime now = DateTime.Now; + return content.Replace("{date}", now.ToString("yyyy-MM-dd")).Replace("{time}", now.ToString("HH:mm:ss")).Replace("{datetime}", now.ToString("yyyy-MM-dd HH:mm:ss")) + .Replace("{year}", now.Year.ToString()) + .Replace("{month}", now.Month.ToString("D2")) + .Replace("{day}", now.Day.ToString("D2")); + } + + private static string TruncatePreview(string content) + { + string text = content.Replace("\r\n", " ").Replace("\n", " ").Replace("\r", " "); + return (text.Length > 60) ? (text.Substring(0, 57) + "…") : text; + } + + [DllImport("user32.dll")] + private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); +} diff --git a/.decompiledproj/AxCopilot/Handlers/StarInfoHandler.cs b/.decompiledproj/AxCopilot/Handlers/StarInfoHandler.cs new file mode 100644 index 0000000..3bd2d78 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/StarInfoHandler.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.SDK; + +namespace AxCopilot.Handlers; + +public class StarInfoHandler : IActionHandler +{ + private readonly SystemInfoHandler _inner = new SystemInfoHandler(); + + public string? Prefix => "*"; + + public PluginMetadata Metadata => new PluginMetadata("StarInfo", "시스템 정보 빠른 조회 — * 단축키 (info와 동일)", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + return _inner.GetItemsAsync(query, ct); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + return _inner.ExecuteAsync(item, ct); + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/SystemCommandHandler.cs b/.decompiledproj/AxCopilot/Handlers/SystemCommandHandler.cs new file mode 100644 index 0000000..593f1e7 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/SystemCommandHandler.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Forms; +using System.Windows.Threading; +using AxCopilot.Models; +using AxCopilot.SDK; +using AxCopilot.Services; +using AxCopilot.Views; + +namespace AxCopilot.Handlers; + +public class SystemCommandHandler : IActionHandler +{ + private readonly SettingsService _settings; + + private static readonly Regex _timerRe = new Regex("^(?:(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?)$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly Regex _alarmRe = new Regex("^(\\d{1,2}):(\\d{2})$", RegexOptions.Compiled); + + public string? Prefix => "/"; + + public PluginMetadata Metadata => new PluginMetadata("SystemCommands", "시스템 명령 — / 뒤에 명령 입력", "1.0", "AX"); + + public SystemCommandHandler(SettingsService settings) + { + _settings = settings; + } + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + SystemCommandSettings cfg = _settings.Settings.SystemCommands; + string q = query.Trim().ToLowerInvariant(); + if (q.StartsWith("timer")) + { + string text = q; + string text2 = text.Substring(5, text.Length - 5).Trim(); + List list = new List(); + if (string.IsNullOrEmpty(text2)) + { + list.Add(new LauncherItem("타이머 — 시간을 입력하세요", "예: /timer 5m · /timer 1h30m · /timer 30s 회의", null, null, null, "\ue916")); + return Task.FromResult((IEnumerable)list); + } + string[] array = text2.Split(' ', 2); + string s = array[0]; + string label = ((array.Length > 1) ? array[1].Trim() : ""); + if (TryParseTimer(s, out var seconds) && seconds > 0) + { + string display = FormatDuration(seconds); + string title = (string.IsNullOrEmpty(label) ? ("타이머 " + display) : ("타이머 " + display + " — " + label)); + list.Add(new LauncherItem(title, display + " 후 알림 · Enter로 시작", null, (Func)(() => StartTimerAsync(seconds, (label.Length > 0) ? label : display)), null, "\ue916")); + } + else + { + list.Add(new LauncherItem("형식 오류", "예: /timer 5m · /timer 1h30m · /timer 90s", null, null, null, "\uea39")); + } + return Task.FromResult((IEnumerable)list); + } + if (q.StartsWith("alarm")) + { + string text = q; + string text3 = text.Substring(5, text.Length - 5).Trim(); + List list2 = new List(); + if (string.IsNullOrEmpty(text3)) + { + list2.Add(new LauncherItem("알람 — 시각을 입력하세요", "예: /alarm 14:30 · /alarm 9:00", null, null, null, "\ue916")); + return Task.FromResult((IEnumerable)list2); + } + Match match = _alarmRe.Match(text3.Split(' ')[0]); + if (match.Success) + { + int num = int.Parse(match.Groups[1].Value); + int num2 = int.Parse(match.Groups[2].Value); + object obj; + if (!text3.Contains(' ')) + { + obj = ""; + } + else + { + text = text3; + int num3 = text3.IndexOf(' ') + 1; + obj = text.Substring(num3, text.Length - num3); + } + string label2 = (string)obj; + if (num >= 0 && num <= 23 && num2 >= 0 && num2 <= 59) + { + DateTime dateTime = DateTime.Today.AddHours(num).AddMinutes(num2); + if (dateTime <= DateTime.Now) + { + dateTime = dateTime.AddDays(1.0); + } + int diff = (int)(dateTime - DateTime.Now).TotalSeconds; + string timeStr = dateTime.ToString("HH:mm"); + list2.Add(new LauncherItem("알람 " + timeStr + " " + ((dateTime.Date > DateTime.Today) ? "(내일)" : ""), (FormatDuration(diff) + " 후 알림 · Enter로 시작 " + ((label2.Length > 0) ? ("— " + label2) : "")).Trim(), null, (Func)(() => StartTimerAsync(diff, (label2.Length > 0) ? label2 : (timeStr + " 알람"))), null, "\ue916")); + } + else + { + list2.Add(new LauncherItem("시각 범위 오류", "00:00 ~ 23:59 범위로 입력하세요", null, null, null, "\uea39")); + } + } + else + { + list2.Add(new LauncherItem("형식 오류", "예: /alarm 14:30 · /alarm 09:00", null, null, null, "\uea39")); + } + return Task.FromResult((IEnumerable)list2); + } + List<(string, string, string, string, bool, Func)> list3 = new List<(string, string, string, string, bool, Func)>(); + list3.Add(("lock", "화면 잠금", "현재 세션을 잠급니다", "\ue72e", cfg.ShowLock, LockAsync)); + list3.Add(("sleep", "절전 모드", "시스템을 절전 상태로 전환합니다", "\uec46", cfg.ShowSleep, SleepAsync)); + list3.Add(("restart", "재시작", "컴퓨터를 재시작합니다", "\ue777", cfg.ShowRestart, RestartAsync)); + list3.Add(("shutdown", "시스템 종료", "컴퓨터를 종료합니다", "\ue7e8", cfg.ShowShutdown, ShutdownAsync)); + list3.Add(("hibernate", "최대 절전", "최대 절전 모드로 전환합니다", "\uec46", cfg.ShowHibernate, HibernateAsync)); + list3.Add(("logout", "로그아웃", "현재 사용자 세션에서 로그아웃합니다", "\uf3b1", cfg.ShowLogout, LogoutAsync)); + list3.Add(("recycle", "휴지통 비우기", "휴지통의 모든 파일을 영구 삭제합니다", "\ue74d", cfg.ShowRecycleBin, EmptyRecycleBinAsync)); + list3.Add(("dock", "독 바 표시/숨기기", "화면 하단 독 바를 표시하거나 숨깁니다", "\ue8a0", true, ToggleDockAsync)); + List<(string, string, string, string, bool, Func)> source = list3; + List value; + IEnumerable source2 = from c in source + where c.Enabled + where string.IsNullOrEmpty(q) || c.Key.StartsWith(q) || c.Name.Contains(q) || (cfg.CommandAliases.TryGetValue(c.Key, out value) && value.Any((string a) => a.StartsWith(q, StringComparison.OrdinalIgnoreCase))) + select new LauncherItem(c.Name, c.Hint, null, c.Action, null, c.Symbol); + return Task.FromResult((IEnumerable)source2.ToList()); + } + + public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + Func action = data as Func; + if (action != null) + { + await action(); + } + } + + private static async Task StartTimerAsync(int totalSeconds, string label) + { + await Task.Delay(TimeSpan.FromSeconds(totalSeconds)); + NotificationService.Notify("⏰ 타이머 완료", label); + } + + private static bool TryParseTimer(string s, out int totalSeconds) + { + totalSeconds = 0; + Match match = _timerRe.Match(s.ToLowerInvariant()); + if (!match.Success) + { + return false; + } + int num = (match.Groups[1].Success ? int.Parse(match.Groups[1].Value) : 0); + int num2 = (match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : 0); + int num3 = (match.Groups[3].Success ? int.Parse(match.Groups[3].Value) : 0); + totalSeconds = num * 3600 + num2 * 60 + num3; + return true; + } + + private static string FormatDuration(int totalSeconds) + { + int num = totalSeconds / 3600; + int num2 = totalSeconds % 3600 / 60; + int num3 = totalSeconds % 60; + if (num > 0 && num2 > 0) + { + return $"{num}시간 {num2}분"; + } + if (num <= 0) + { + if (num2 > 0 && num3 > 0) + { + return $"{num2}분 {num3}초"; + } + if (num2 <= 0) + { + return $"{num3}초"; + } + return $"{num2}분"; + } + return $"{num}시간"; + } + + private static Task LockAsync() + { + LockWorkStation(); + return Task.CompletedTask; + } + + private static Task SleepAsync() + { + System.Windows.Forms.Application.SetSuspendState(PowerState.Suspend, force: false, disableWakeEvent: false); + return Task.CompletedTask; + } + + private static Task HibernateAsync() + { + System.Windows.Forms.Application.SetSuspendState(PowerState.Hibernate, force: false, disableWakeEvent: false); + return Task.CompletedTask; + } + + private static Task RestartAsync() + { + MessageBoxResult messageBoxResult = CustomMessageBox.Show("컴퓨터를 재시작하시겠습니까?\n저장되지 않은 작업이 있으면 먼저 저장하세요.", "재시작 확인", MessageBoxButton.YesNo, MessageBoxImage.Question); + if (messageBoxResult == MessageBoxResult.Yes) + { + Process.Start(new ProcessStartInfo("shutdown", "/r /t 5 /c \"AX Copilot에 의해 재시작됩니다.\"") + { + UseShellExecute = true, + CreateNoWindow = true + }); + } + return Task.CompletedTask; + } + + private static Task ShutdownAsync() + { + MessageBoxResult messageBoxResult = CustomMessageBox.Show("컴퓨터를 종료하시겠습니까?\n저장되지 않은 작업이 있으면 먼저 저장하세요.", "종료 확인", MessageBoxButton.YesNo, MessageBoxImage.Question); + if (messageBoxResult == MessageBoxResult.Yes) + { + Process.Start(new ProcessStartInfo("shutdown", "/s /t 5 /c \"AX Copilot에 의해 종료됩니다.\"") + { + UseShellExecute = true, + CreateNoWindow = true + }); + } + return Task.CompletedTask; + } + + private static Task LogoutAsync() + { + MessageBoxResult messageBoxResult = CustomMessageBox.Show("로그아웃하시겠습니까?", "로그아웃 확인", MessageBoxButton.YesNo, MessageBoxImage.Question); + if (messageBoxResult == MessageBoxResult.Yes) + { + ExitWindowsEx(0u, 0u); + } + return Task.CompletedTask; + } + + private static Task EmptyRecycleBinAsync() + { + MessageBoxResult messageBoxResult = CustomMessageBox.Show("휴지통을 비우시겠습니까?\n삭제된 파일은 복구할 수 없습니다.", "휴지통 비우기", MessageBoxButton.YesNo, MessageBoxImage.Exclamation); + if (messageBoxResult == MessageBoxResult.Yes) + { + SHEmptyRecycleBin(IntPtr.Zero, null, 7u); + } + return Task.CompletedTask; + } + + private static Task ToggleDockAsync() + { + ((DispatcherObject)System.Windows.Application.Current).Dispatcher.Invoke((Action)delegate + { + (System.Windows.Application.Current as App)?.ToggleDockBar(); + }); + return Task.CompletedTask; + } + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool LockWorkStation(); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool ExitWindowsEx(uint uFlags, uint dwReason); + + [DllImport("shell32.dll", CharSet = CharSet.Unicode)] + private static extern uint SHEmptyRecycleBin(nint hwnd, string? pszRootPath, uint dwFlags); +} diff --git a/.decompiledproj/AxCopilot/Handlers/SystemInfoHandler.cs b/.decompiledproj/AxCopilot/Handlers/SystemInfoHandler.cs new file mode 100644 index 0000000..d5d34de --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/SystemInfoHandler.cs @@ -0,0 +1,462 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Forms; +using System.Windows.Threading; +using AxCopilot.SDK; +using AxCopilot.Services; +using AxCopilot.Views; +using Microsoft.Win32; + +namespace AxCopilot.Handlers; + +public class SystemInfoHandler : IActionHandler +{ + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + private struct MEMORYSTATUSEX + { + public uint dwLength; + + public uint dwMemoryLoad; + + public ulong ullTotalPhys; + + public ulong ullAvailPhys; + + public ulong ullTotalPageFile; + + public ulong ullAvailPageFile; + + public ulong ullTotalVirtual; + + public ulong ullAvailVirtual; + + public ulong ullAvailExtendedVirtual; + } + + internal sealed record InfoAction(string Type, string? Payload = null); + + private static volatile PerformanceCounter? _cpuCounter; + + private static float _cpuCached; + + private static DateTime _cpuUpdated; + + private static readonly object _cpuLock; + + public string? Prefix => "info"; + + public PluginMetadata Metadata => new PluginMetadata("SystemInfo", "시스템 정보 — IP, 배터리, 볼륨, 가동시간 등", "1.0", "AX"); + + [DllImport("winmm.dll")] + private static extern int waveOutGetVolume(nint hwo, out uint dwVolume); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer); + + static SystemInfoHandler() + { + _cpuUpdated = DateTime.MinValue; + _cpuLock = new object(); + Task.Run(delegate + { + try + { + PerformanceCounter performanceCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total"); + performanceCounter.NextValue(); + _cpuCounter = performanceCounter; + } + catch + { + } + }); + } + + private static float GetCpuUsage() + { + try + { + PerformanceCounter cpuCounter = _cpuCounter; + if (cpuCounter == null) + { + return -1f; + } + lock (_cpuLock) + { + if ((DateTime.Now - _cpuUpdated).TotalMilliseconds > 800.0) + { + _cpuCached = cpuCounter.NextValue(); + _cpuUpdated = DateTime.Now; + } + return _cpuCached; + } + } + catch + { + return -1f; + } + } + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string text = query.Trim().ToLowerInvariant(); + List list = new List(); + try + { + bool flag = string.IsNullOrEmpty(text); + bool flag2 = flag || text.Contains("ip") || text.Contains("네트워크") || text.Contains("network"); + bool flag3 = flag || text.Contains("bat") || text.Contains("배터리") || text.Contains("battery"); + bool flag4 = flag || text.Contains("vol") || text.Contains("볼륨") || text.Contains("volume"); + bool flag5 = flag || text.Contains("up") || text.Contains("가동") || text.Contains("uptime"); + bool flag6 = flag || text.Contains("sys") || text.Contains("시스템") || text.Contains("system") || text.Contains("host") || text.Contains("호스트") || text.Contains("os"); + bool flag7 = flag || text.Contains("user") || text.Contains("사용자"); + bool flag8 = flag || text.Contains("cpu") || text.Contains("프로세서") || text.Contains("processor"); + bool flag9 = flag || text.Contains("ram") || text.Contains("메모리") || text.Contains("memory"); + bool flag10 = flag || text.Contains("disk") || text.Contains("디스크") || text.Contains("storage") || text.Contains("저장"); + bool flag11 = flag || text.Contains("screen") || text.Contains("화면") || text.Contains("resolution") || text.Contains("display"); + if (flag6) + { + list.Add(new LauncherItem("컴퓨터: " + Environment.MachineName, "OS: " + GetOsVersion() + " · Enter로 시스템 정보 열기", null, new InfoAction("shell", "msinfo32"), null, "\ue7f4")); + } + if (flag7) + { + string text2 = Environment.UserDomainName + "\\" + Environment.UserName; + list.Add(new LauncherItem("사용자: " + Environment.UserName, text2 + " · Enter로 사용자 계정 설정 열기", null, new InfoAction("shell", "netplwiz"), null, "\ue77b")); + } + if (flag2) + { + string localIpAddress = GetLocalIpAddress(); + if (localIpAddress != null) + { + list.Add(new LauncherItem("로컬 IP: " + localIpAddress, "LAN / Wi-Fi 주소 · Enter로 네트워크 설정 열기", null, new InfoAction("ms_settings", "ms-settings:network"), null, "\ue968")); + } + string defaultGateway = GetDefaultGateway(); + if (defaultGateway != null) + { + list.Add(new LauncherItem("게이트웨이: " + defaultGateway, "기본 게이트웨이 · Enter로 네트워크 설정 열기", null, new InfoAction("ms_settings", "ms-settings:network"), null, "\ue968")); + } + } + if (flag3) + { + LauncherItem batteryItem = GetBatteryItem(); + if (batteryItem != null) + { + list.Add(batteryItem); + } + } + if (flag4) + { + LauncherItem volumeItem = GetVolumeItem(); + if (volumeItem != null) + { + list.Add(volumeItem); + } + } + if (flag5) + { + TimeSpan timeSpan = TimeSpan.FromMilliseconds(Environment.TickCount64); + string text3 = FormatUptime(timeSpan); + DateTime value = DateTime.Now - timeSpan; + list.Add(MakeItem("가동 시간: " + text3, $"마지막 재시작: {value:MM/dd HH:mm}", text3, "\ue823")); + } + if (flag8) + { + float cpuUsage = GetCpuUsage(); + string title = ((cpuUsage < 0f) ? "CPU: 측정 중…" : $"CPU: {(int)cpuUsage}%"); + string processorName = GetProcessorName(); + list.Add(new LauncherItem(title, (string.IsNullOrEmpty(processorName) ? "전체 CPU 사용률" : processorName) + " · Enter로 리소스 모니터 열기", null, new InfoAction("resource_mon"), null, "\ue950")); + } + if (flag9) + { + MEMORYSTATUSEX lpBuffer = new MEMORYSTATUSEX + { + dwLength = (uint)Marshal.SizeOf() + }; + if (GlobalMemoryStatusEx(ref lpBuffer)) + { + double value2 = (double)lpBuffer.ullTotalPhys / 1024.0 / 1024.0 / 1024.0; + double value3 = (double)(lpBuffer.ullTotalPhys - lpBuffer.ullAvailPhys) / 1024.0 / 1024.0 / 1024.0; + uint dwMemoryLoad = lpBuffer.dwMemoryLoad; + list.Add(new LauncherItem($"RAM: {value3:F1} / {value2:F1} GB ({dwMemoryLoad}%)", $"사용 중: {value3:F1} GB · 여유: {(double)lpBuffer.ullAvailPhys / 1024.0 / 1024.0 / 1024.0:F1} GB · Enter로 리소스 모니터 열기", null, new InfoAction("resource_mon"), null, "\ue950")); + } + } + if (flag10) + { + foreach (DriveInfo item in from d in DriveInfo.GetDrives() + where d.IsReady && d.DriveType == DriveType.Fixed + select d) + { + double num = (double)item.TotalSize / 1024.0 / 1024.0 / 1024.0; + double num2 = (double)item.AvailableFreeSpace / 1024.0 / 1024.0 / 1024.0; + double num3 = num - num2; + int value4 = (int)(num3 / num * 100.0); + string value5 = (string.IsNullOrWhiteSpace(item.VolumeLabel) ? item.Name.TrimEnd('\\') : item.VolumeLabel); + list.Add(new LauncherItem($"드라이브 {item.Name.TrimEnd('\\')} ({value5}) — {num3:F0} / {num:F0} GB ({value4}%)", $"여유 공간: {num2:F1} GB · Enter로 탐색기 열기", null, new InfoAction("open_drive", item.RootDirectory.FullName), null, "\ueda2")); + } + } + if (flag11) + { + double primaryScreenWidth = SystemParameters.PrimaryScreenWidth; + double primaryScreenHeight = SystemParameters.PrimaryScreenHeight; + list.Add(new LauncherItem($"화면: {(int)primaryScreenWidth} × {(int)primaryScreenHeight}", "기본 모니터 해상도 · Enter로 디스플레이 설정 열기", null, new InfoAction("ms_settings", "ms-settings:display"), null, "\ue7f4")); + } + } + catch (Exception ex) + { + LogService.Warn("시스템 정보 조회 오류: " + ex.Message); + list.Add(new LauncherItem("시스템 정보 조회 실패", ex.Message, null, null, null, "\uea39")); + } + if (list.Count == 0) + { + list.Add(new LauncherItem("시스템 정보 없음", "ip · battery · volume · uptime · system 키워드로 검색하세요", null, null, null, "\ue946")); + } + return Task.FromResult((IEnumerable)list); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + InfoAction action = data as InfoAction; + if ((object)action == null) + { + return Task.CompletedTask; + } + switch (action.Type) + { + case "copy": + if (string.IsNullOrWhiteSpace(action.Payload)) + { + break; + } + try + { + ((DispatcherObject)System.Windows.Application.Current).Dispatcher.Invoke((Action)delegate + { + System.Windows.Clipboard.SetText(action.Payload); + }); + LogService.Info("클립보드 복사: " + action.Payload); + } + catch (Exception ex2) + { + LogService.Warn("클립보드 복사 실패: " + ex2.Message); + } + break; + case "open_drive": + if (!string.IsNullOrWhiteSpace(action.Payload)) + { + try + { + Process.Start(new ProcessStartInfo("explorer.exe", action.Payload) + { + UseShellExecute = true + }); + } + catch (Exception ex4) + { + LogService.Warn("드라이브 열기 실패: " + ex4.Message); + } + } + break; + case "resource_mon": + ((DispatcherObject)System.Windows.Application.Current).Dispatcher.Invoke((Action)delegate + { + ResourceMonitorWindow resourceMonitorWindow = System.Windows.Application.Current.Windows.OfType().FirstOrDefault(); + if (resourceMonitorWindow != null) + { + resourceMonitorWindow.Activate(); + } + else + { + new ResourceMonitorWindow().Show(); + } + }); + break; + case "shell": + if (!string.IsNullOrWhiteSpace(action.Payload)) + { + try + { + Process.Start(new ProcessStartInfo(action.Payload) + { + UseShellExecute = true + }); + } + catch (Exception ex3) + { + LogService.Warn("셸 명령 실행 실패: " + ex3.Message); + } + } + break; + case "ms_settings": + if (!string.IsNullOrWhiteSpace(action.Payload)) + { + try + { + Process.Start(new ProcessStartInfo(action.Payload) + { + UseShellExecute = true + }); + } + catch (Exception ex) + { + LogService.Warn("설정 열기 실패: " + ex.Message); + } + } + break; + } + return Task.CompletedTask; + } + + private static LauncherItem MakeItem(string title, string subtitle, string? copyValue, string symbol) + { + return new LauncherItem(title, subtitle, null, (copyValue != null) ? new InfoAction("copy", copyValue) : null, null, symbol); + } + + private static string? GetLocalIpAddress() + { + try + { + NetworkInterface[] allNetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); + foreach (NetworkInterface networkInterface in allNetworkInterfaces) + { + if (networkInterface.OperationalStatus != OperationalStatus.Up) + { + continue; + } + NetworkInterfaceType networkInterfaceType = networkInterface.NetworkInterfaceType; + if ((networkInterfaceType == NetworkInterfaceType.Loopback || networkInterfaceType == NetworkInterfaceType.Tunnel) ? true : false) + { + continue; + } + foreach (UnicastIPAddressInformation unicastAddress in networkInterface.GetIPProperties().UnicastAddresses) + { + if (unicastAddress.Address.AddressFamily == AddressFamily.InterNetwork) + { + return unicastAddress.Address.ToString(); + } + } + } + } + catch + { + } + return null; + } + + private static string? GetDefaultGateway() + { + try + { + NetworkInterface[] allNetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); + foreach (NetworkInterface networkInterface in allNetworkInterfaces) + { + if (networkInterface.OperationalStatus == OperationalStatus.Up) + { + GatewayIPAddressInformation gatewayIPAddressInformation = networkInterface.GetIPProperties().GatewayAddresses.FirstOrDefault((GatewayIPAddressInformation g) => g.Address.AddressFamily == AddressFamily.InterNetwork); + if (gatewayIPAddressInformation != null) + { + return gatewayIPAddressInformation.Address.ToString(); + } + } + } + } + catch + { + } + return null; + } + + private static LauncherItem? GetBatteryItem() + { + try + { + PowerStatus powerStatus = SystemInformation.PowerStatus; + float batteryLifePercent = powerStatus.BatteryLifePercent; + if (batteryLifePercent < 0f) + { + return new LauncherItem("배터리: 해당 없음", "데스크톱 PC 또는 항상 연결됨 · Enter로 전원 설정 열기", null, new InfoAction("ms_settings", "ms-settings:powersleep"), null, "\ue83f"); + } + int num = (int)(batteryLifePercent * 100f); + bool flag = powerStatus.PowerLineStatus == System.Windows.Forms.PowerLineStatus.Online; + string text = ((powerStatus.BatteryLifeRemaining >= 0) ? (" · 잔여: " + FormatUptime(TimeSpan.FromSeconds(powerStatus.BatteryLifeRemaining))) : ""); + string symbol = (flag ? "\ue83e" : ((num > 50) ? "\ue83f" : "\ueba0")); + return new LauncherItem($"배터리: {num}%{(flag ? " ⚡ 충전 중" : "")}", "전원: " + (flag ? "AC 연결됨" : "배터리 사용 중") + text + " · Enter로 전원 설정 열기", null, new InfoAction("ms_settings", "ms-settings:powersleep"), null, symbol); + } + catch + { + return null; + } + } + + private static LauncherItem? GetVolumeItem() + { + try + { + if (waveOutGetVolume(IntPtr.Zero, out var dwVolume) == 0) + { + int num = (int)((double)(dwVolume & 0xFFFF) / 655.35); + int num2 = (int)((double)((dwVolume >> 16) & 0xFFFF) / 655.35); + int num3 = (num + num2) / 2; + return new LauncherItem($"볼륨: {num3}%", $"L: {num}% · R: {num2}% · Enter로 사운드 설정 열기", null, new InfoAction("ms_settings", "ms-settings:sound"), null, (num3 == 0) ? "\ue74f" : "\ue995"); + } + } + catch + { + } + return null; + } + + private static string GetOsVersion() + { + try + { + using RegistryKey registryKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"); + if (registryKey != null) + { + string text = (registryKey.GetValue("ProductName") as string) ?? "Windows"; + string text2 = (registryKey.GetValue("CurrentBuildNumber") as string) ?? ""; + object value = registryKey.GetValue("UBR"); + return (value != null) ? $"{text} (빌드 {text2}.{value})" : (text + " (빌드 " + text2 + ")"); + } + } + catch + { + } + return Environment.OSVersion.ToString(); + } + + private static string GetProcessorName() + { + try + { + using RegistryKey registryKey = Registry.LocalMachine.OpenSubKey("HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0"); + return (registryKey?.GetValue("ProcessorNameString") as string) ?? ""; + } + catch + { + return ""; + } + } + + private static string FormatUptime(TimeSpan t) + { + if (!(t.TotalDays >= 1.0)) + { + if (!(t.TotalHours >= 1.0)) + { + return $"{t.Minutes}분 {t.Seconds}초"; + } + return $"{t.Hours}시간 {t.Minutes}분"; + } + return $"{(int)t.TotalDays}일 {t.Hours}시간 {t.Minutes}분"; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/TextStatsHandler.cs b/.decompiledproj/AxCopilot/Handlers/TextStatsHandler.cs new file mode 100644 index 0000000..b505e2a --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/TextStatsHandler.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using AxCopilot.SDK; + +namespace AxCopilot.Handlers; + +public class TextStatsHandler : IActionHandler +{ + public string? Prefix => "stats"; + + public PluginMetadata Metadata => new PluginMetadata("TextStats", "텍스트 통계 분석", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string text = null; + try + { + Application current = Application.Current; + if (current != null && ((DispatcherObject)current).Dispatcher.Invoke((Func)(() => Clipboard.ContainsText()))) + { + text = ((DispatcherObject)Application.Current).Dispatcher.Invoke((Func)(() => Clipboard.GetText())); + } + } + catch + { + } + if (string.IsNullOrEmpty(text)) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("클립보드에 텍스트가 없습니다", "텍스트를 복사한 후 다시 시도하세요", null, null, null, "\ue946"))); + } + string text2 = query.Trim(); + List list = new List(); + int length = text.Length; + int length2 = text.Replace(" ", "").Replace("\t", "").Replace("\r", "") + .Replace("\n", "") + .Length; + string[] array = (from w in Regex.Split(text, "\\s+", RegexOptions.None, TimeSpan.FromSeconds(1.0)) + where !string.IsNullOrWhiteSpace(w) + select w).ToArray(); + int num = array.Length; + string[] array2 = text.Split('\n'); + int value = array2.Length; + int value2 = array2.Count((string l) => !string.IsNullOrWhiteSpace(l)); + int byteCount = Encoding.UTF8.GetByteCount(text); + string text3 = $"글자 {length:N0} · 공백 제외 {length2:N0} · 단어 {num:N0} · 줄 {value:N0}"; + list.Add(new LauncherItem($"글자 수: {length:N0} ({length2:N0} 공백 제외)", $"UTF-8: {byteCount:N0} bytes · Enter로 결과 복사", null, $"글자 수: {length:N0} (공백 제외: {length2:N0})", null, "\ue8d2")); + list.Add(new LauncherItem($"단어 수: {num:N0}", "공백·줄바꿈 기준 분리 · Enter로 결과 복사", null, $"단어 수: {num:N0}", null, "\ue8d2")); + list.Add(new LauncherItem($"줄 수: {value:N0} (비어있지 않은 줄: {value2:N0})", "Enter로 결과 복사", null, $"줄 수: {value:N0} (비어있지 않은 줄: {value2:N0})", null, "\ue8d2")); + int num2 = text.Count((char c) => c >= '가' && c <= '\ud7af'); + double num3 = ((num2 > length / 2) ? ((double)length2 / 500.0) : ((double)num / 200.0)); + string text4 = ((num3 < 1.0) ? "1분 미만" : $"약 {Math.Ceiling(num3)}분"); + list.Add(new LauncherItem("예상 읽기 시간: " + text4, (num2 > length / 2) ? "한국어 기준 (500자/분)" : "영어 기준 (200단어/분)", null, "예상 읽기 시간: " + text4, null, "\ue823")); + if (string.IsNullOrWhiteSpace(text2)) + { + IEnumerable values = from g in (from w in array + where w.Length >= 2 + group w by w.ToLowerInvariant() into g + orderby g.Count() descending + select g).Take(5) + select $"{g.Key}({g.Count()})"; + string text5 = string.Join(", ", values); + if (!string.IsNullOrEmpty(text5)) + { + list.Add(new LauncherItem("상위 키워드", text5, null, "상위 키워드: " + text5, null, "\ue721")); + } + } + else + { + try + { + int count = Regex.Matches(text, Regex.Escape(text2), RegexOptions.IgnoreCase, TimeSpan.FromSeconds(1.0)).Count; + list.Insert(0, new LauncherItem($"'{text2}' 출현 횟수: {count}회", "대소문자 무시 검색 · Enter로 결과 복사", null, $"'{text2}' 출현 횟수: {count}회", null, "\ue721")); + } + catch + { + } + } + list.Add(new LauncherItem("전체 요약 복사", text3, null, $"[텍스트 통계]\n{text3}\nUTF-8: {byteCount:N0} bytes\n읽기 시간: {text4}", null, "\ue77f")); + return Task.FromResult((IEnumerable)list); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + string text = data as string; + if (text != null && !string.IsNullOrWhiteSpace(text)) + { + try + { + Application current = Application.Current; + if (current != null) + { + ((DispatcherObject)current).Dispatcher.Invoke((Action)delegate + { + Clipboard.SetText(text); + }); + } + } + catch + { + } + } + return Task.CompletedTask; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/UninstallHandler.cs b/.decompiledproj/AxCopilot/Handlers/UninstallHandler.cs new file mode 100644 index 0000000..0512716 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/UninstallHandler.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.SDK; +using AxCopilot.Services; +using Microsoft.Win32; + +namespace AxCopilot.Handlers; + +public class UninstallHandler : IActionHandler +{ + private static readonly string[] _regPaths = new string[2] { "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall" }; + + private static (DateTime At, List Apps)? _cache; + + private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(30.0); + + public string? Prefix => "uninstall"; + + public PluginMetadata Metadata => new PluginMetadata("Uninstall", "앱 제거 — uninstall 뒤에 앱 이름 입력", "1.0", "AX"); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string q = query.Trim(); + if (string.IsNullOrWhiteSpace(q)) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("제거할 앱 이름을 입력하세요", "예: uninstall chrome · uninstall office", null, null, null, "\ue74d"))); + } + List installedApps = GetInstalledApps(); + List source = (from a in installedApps + where a.Name.Contains(q, StringComparison.OrdinalIgnoreCase) || (a.Publisher?.Contains(q, StringComparison.OrdinalIgnoreCase) ?? false) + orderby a.Name + select a).Take(12).ToList(); + if (!source.Any()) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("검색 결과 없음", "'" + q + "'에 해당하는 설치된 앱이 없습니다", null, null, null, "\ue946"))); + } + List result = source.Select((InstalledApp a) => new LauncherItem(a.Name, BuildSubtitle(a), null, a, null, "\ue74d")).ToList(); + return Task.FromResult((IEnumerable)result); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (!(item.Data is InstalledApp installedApp)) + { + return Task.CompletedTask; + } + if (string.IsNullOrWhiteSpace(installedApp.UninstallString)) + { + LogService.Warn("제거 문자열 없음: " + installedApp.Name); + return Task.CompletedTask; + } + try + { + string text = installedApp.UninstallString.Trim(); + if (text.StartsWith("msiexec", StringComparison.OrdinalIgnoreCase)) + { + string arguments = text.Substring("msiexec".Length).Trim(); + Process.Start(new ProcessStartInfo("msiexec.exe", arguments) + { + UseShellExecute = true + }); + } + else + { + string fileName; + string arguments2; + if (text.StartsWith('"')) + { + int num = text.IndexOf('"', 1); + fileName = ((num > 0) ? text.Substring(1, num - 1) : text); + object obj; + if (num <= 0) + { + obj = ""; + } + else + { + string text2 = text; + int num2 = num + 1; + obj = text2.Substring(num2, text2.Length - num2).Trim(); + } + arguments2 = (string)obj; + } + else + { + int num3 = text.IndexOf(' '); + if (num3 > 0) + { + fileName = text.Substring(0, num3); + string text2 = text; + int num2 = num3 + 1; + arguments2 = text2.Substring(num2, text2.Length - num2); + } + else + { + fileName = text; + arguments2 = ""; + } + } + ProcessStartInfo startInfo = new ProcessStartInfo(fileName, arguments2) + { + UseShellExecute = true + }; + Process.Start(startInfo); + } + } + catch (Exception ex) + { + LogService.Warn("제거 실행 실패 [" + installedApp.Name + "]: " + ex.Message); + } + return Task.CompletedTask; + } + + private static string BuildSubtitle(InstalledApp a) + { + List list = new List(); + if (!string.IsNullOrWhiteSpace(a.Publisher)) + { + list.Add(a.Publisher); + } + if (!string.IsNullOrWhiteSpace(a.Version)) + { + list.Add("v" + a.Version); + } + if (!string.IsNullOrWhiteSpace(a.InstallDate)) + { + list.Add(a.InstallDate); + } + list.Add("Enter로 제거"); + return string.Join(" · ", list); + } + + private static List GetInstalledApps() + { + if (_cache.HasValue && DateTime.Now - _cache.Value.At < CacheTtl) + { + return _cache.Value.Apps; + } + List list = new List(); + HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); + RegistryKey[] array = new RegistryKey[2] + { + Registry.LocalMachine, + Registry.CurrentUser + }; + foreach (RegistryKey registryKey in array) + { + string[] regPaths = _regPaths; + foreach (string name in regPaths) + { + try + { + using RegistryKey registryKey2 = registryKey.OpenSubKey(name); + if (registryKey2 == null) + { + continue; + } + string[] subKeyNames = registryKey2.GetSubKeyNames(); + foreach (string name2 in subKeyNames) + { + try + { + using RegistryKey registryKey3 = registryKey2.OpenSubKey(name2); + if (registryKey3 != null) + { + string text = registryKey3.GetValue("DisplayName") as string; + string text2 = registryKey3.GetValue("UninstallString") as string; + if (!string.IsNullOrWhiteSpace(text) && !string.IsNullOrWhiteSpace(text2) && (!text.StartsWith("KB", StringComparison.OrdinalIgnoreCase) || text.Length >= 12) && hashSet.Add(text) && (!(registryKey3.GetValue("SystemComponent") is int num) || num != 1)) + { + list.Add(new InstalledApp + { + Name = text, + UninstallString = text2, + Publisher = (registryKey3.GetValue("Publisher") as string), + Version = (registryKey3.GetValue("DisplayVersion") as string), + InstallDate = FormatDate(registryKey3.GetValue("InstallDate") as string) + }); + } + } + } + catch + { + } + } + } + catch + { + } + } + } + list.Sort((InstalledApp a, InstalledApp b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); + _cache = (DateTime.Now, list); + return list; + } + + private static string? FormatDate(string? raw) + { + if (raw != null && raw.Length == 8 && int.TryParse(raw, out var _)) + { + return $"{raw.Substring(0, 4)}-{raw.Substring(4, 2)}-{raw.Substring(6, 2)}"; + } + return null; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/UnitConverter.cs b/.decompiledproj/AxCopilot/Handlers/UnitConverter.cs new file mode 100644 index 0000000..ec0e1a1 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/UnitConverter.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace AxCopilot.Handlers; + +internal static class UnitConverter +{ + private static readonly Regex Pattern = new Regex("^(-?\\d+(?:\\.\\d+)?)\\s*([a-z°/²³µ]+)\\s+(?:in|to)\\s+([a-z°/²³µ]+)$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly Dictionary _length = new Dictionary + { + ["km"] = 1000.0, + ["m"] = 1.0, + ["cm"] = 0.01, + ["mm"] = 0.001, + ["mi"] = 1609.344, + ["mile"] = 1609.344, + ["miles"] = 1609.344, + ["ft"] = 0.3048, + ["feet"] = 0.3048, + ["foot"] = 0.3048, + ["in"] = 0.0254, + ["inch"] = 0.0254, + ["inches"] = 0.0254, + ["yd"] = 0.9144, + ["yard"] = 0.9144, + ["yards"] = 0.9144, + ["nm"] = 1E-09 + }; + + private static readonly Dictionary _weight = new Dictionary + { + ["t"] = 1000.0, + ["ton"] = 1000.0, + ["tonnes"] = 1000.0, + ["kg"] = 1.0, + ["g"] = 0.001, + ["mg"] = 1E-06, + ["lb"] = 0.453592, + ["lbs"] = 0.453592, + ["pound"] = 0.453592, + ["pounds"] = 0.453592, + ["oz"] = 0.0283495, + ["ounce"] = 0.0283495, + ["ounces"] = 0.0283495 + }; + + private static readonly Dictionary _speed = new Dictionary + { + ["m/s"] = 1.0, + ["mps"] = 1.0, + ["km/h"] = 5.0 / 18.0, + ["kmh"] = 5.0 / 18.0, + ["kph"] = 5.0 / 18.0, + ["mph"] = 0.44704, + ["kn"] = 0.514444, + ["knot"] = 0.514444, + ["knots"] = 0.514444 + }; + + private static readonly Dictionary _data = new Dictionary + { + ["b"] = 1.0, + ["byte"] = 1.0, + ["bytes"] = 1.0, + ["kb"] = 1024.0, + ["kib"] = 1024.0, + ["mb"] = 1048576.0, + ["mib"] = 1048576.0, + ["gb"] = 1073741824.0, + ["gib"] = 1073741824.0, + ["tb"] = 1099511627776.0, + ["tib"] = 1099511627776.0, + ["pb"] = 1125899906842624.0 + }; + + private static readonly Dictionary _area = new Dictionary + { + ["m²"] = 1.0, + ["m2"] = 1.0, + ["km²"] = 1000000.0, + ["km2"] = 1000000.0, + ["cm²"] = 0.0001, + ["cm2"] = 0.0001, + ["ha"] = 10000.0, + ["acre"] = 4046.86, + ["acres"] = 4046.86, + ["ft²"] = 0.092903, + ["ft2"] = 0.092903 + }; + + private static readonly List> _tables = new List> { _length, _weight, _speed, _data, _area }; + + public static bool TryConvert(string input, out string? result) + { + result = null; + Match match = Pattern.Match(input.Trim()); + if (!match.Success) + { + return false; + } + if (!double.TryParse(match.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var result2)) + { + return false; + } + string text = match.Groups[2].Value.ToLowerInvariant(); + string text2 = match.Groups[3].Value.ToLowerInvariant(); + if (TryConvertTemperature(result2, text, text2, out var r)) + { + result = FormatNum(r) + " " + TemperatureLabel(text2); + return true; + } + foreach (Dictionary table in _tables) + { + if (table.TryGetValue(text, out var value) && table.TryGetValue(text2, out var value2)) + { + double v = result2 * value / value2; + result = FormatNum(v) + " " + text2; + return true; + } + } + return false; + } + + private static bool TryConvertTemperature(double v, string from, string to, out double r) + { + r = 0.0; + double num; + switch (from) + { + case "c": + case "°c": + case "celsius": + num = v; + break; + case "f": + case "°f": + case "fahrenheit": + num = (v - 32.0) * 5.0 / 9.0; + break; + case "k": + case "kelvin": + num = v - 273.15; + break; + default: + return false; + } + switch (to) + { + case "c": + case "°c": + case "celsius": + r = num; + break; + case "f": + case "°f": + case "fahrenheit": + r = num * 9.0 / 5.0 + 32.0; + break; + case "k": + case "kelvin": + r = num + 273.15; + break; + default: + return false; + } + return true; + } + + private static string TemperatureLabel(string unit) + { + if (1 == 0) + { + } + string result; + switch (unit) + { + case "c": + case "°c": + case "celsius": + result = "°C"; + break; + case "f": + case "°f": + case "fahrenheit": + result = "°F"; + break; + case "k": + case "kelvin": + result = "K"; + break; + default: + result = unit; + break; + } + if (1 == 0) + { + } + return result; + } + + private static string FormatNum(double v) + { + if (v == Math.Floor(v) && Math.Abs(v) < 1000000000000.0) + { + return ((long)v).ToString("N0", CultureInfo.CurrentCulture); + } + return v.ToString("G6", CultureInfo.InvariantCulture); + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/UrlAliasHandler.cs b/.decompiledproj/AxCopilot/Handlers/UrlAliasHandler.cs new file mode 100644 index 0000000..1e6ddf1 --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/UrlAliasHandler.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.Models; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class UrlAliasHandler : IActionHandler +{ + private readonly SettingsService _settings; + + private static readonly HashSet AllowedSchemes = new HashSet(StringComparer.OrdinalIgnoreCase) { "http", "https", "ftp", "ftps", "ms-settings", "mailto", "file" }; + + public string? Prefix => "@"; + + public PluginMetadata Metadata => new PluginMetadata("url-alias", "URL 별칭", "1.0", "AX"); + + public UrlAliasHandler(SettingsService settings) + { + _settings = settings; + } + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + IEnumerable result = _settings.Settings.Aliases.Where((AliasEntry a) => a.Type == "url" && (string.IsNullOrEmpty(query) || a.Key.Contains(query, StringComparison.OrdinalIgnoreCase))).Select(delegate(AliasEntry a) + { + string faviconPath = GetFaviconPath(a.Target); + return new LauncherItem(a.Key, a.Description ?? a.Target, faviconPath, a, a.Target, "\ue774"); + }); + return Task.FromResult(result); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is AliasEntry aliasEntry) + { + string text = Environment.ExpandEnvironmentVariables(aliasEntry.Target); + if (!IsAllowedUrl(text)) + { + LogService.Warn("허용되지 않는 URL 스킴: " + text); + return Task.CompletedTask; + } + Process.Start(new ProcessStartInfo(text) + { + UseShellExecute = true + }); + } + return Task.CompletedTask; + } + + private static bool IsAllowedUrl(string url) + { + if (!Uri.TryCreate(url, UriKind.Absolute, out Uri result)) + { + return false; + } + return AllowedSchemes.Contains(result.Scheme); + } + + private static string? GetFaviconPath(string url) + { + try + { + if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + url = "https://" + url; + } + string text = new Uri(url).Host.ToLowerInvariant(); + string text2 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "favicons", text + ".png"); + if (File.Exists(text2)) + { + return text2; + } + FaviconService.GetFavicon(url); + return null; + } + catch + { + return null; + } + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/WebSearchHandler.cs b/.decompiledproj/AxCopilot/Handlers/WebSearchHandler.cs new file mode 100644 index 0000000..2b0519f --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/WebSearchHandler.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class WebSearchHandler : IActionHandler +{ + private readonly SettingsService _settings; + + private static readonly string _iconDir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "Assets", "SearchEngines"); + + private static readonly Dictionary _engines = new Dictionary + { + ["g"] = ("Google", "https://www.google.com/search?q={0}", "google"), + ["n"] = ("Naver", "https://search.naver.com/search.naver?query={0}", "naver"), + ["y"] = ("YouTube", "https://www.youtube.com/results?search_query={0}", "youtube"), + ["gh"] = ("GitHub", "https://github.com/search?q={0}", "github"), + ["d"] = ("DuckDuckGo", "https://duckduckgo.com/?q={0}", "duckduckgo"), + ["w"] = ("Wikipedia", "https://ko.wikipedia.org/w/index.php?search={0}", "wikipedia"), + ["nm"] = ("Naver Map", "https://map.naver.com/p/search/{0}", "navermap"), + ["nw"] = ("나무위키", "https://namu.wiki/Special:Search?query={0}", "namuwiki"), + ["ni"] = ("Naver 이미지", "https://search.naver.com/search.naver?where=image&query={0}", "naver"), + ["gi"] = ("Google 이미지", "https://www.google.com/search?tbm=isch&q={0}", "google") + }; + + private const string SecurityWarn = "⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지"; + + public string? Prefix => "?"; + + public PluginMetadata Metadata => new PluginMetadata("WebSearch", "웹 검색 — ? 뒤에 검색어 또는 URL 입력", "1.0", "AX"); + + private string DefaultEngineKey => _settings.Settings.Launcher.WebSearchEngine ?? "g"; + + private (string Name, string UrlTemplate, string Icon) DefaultEngine + { + get + { + (string, string, string) value; + return _engines.TryGetValue(DefaultEngineKey, out value) ? value : _engines["g"]; + } + } + + public WebSearchHandler(SettingsService settings) + { + _settings = settings; + } + + private static string? GetIconPath(string engineKey) + { + if (!_engines.TryGetValue(engineKey, out (string, string, string) value)) + { + return null; + } + string text = Path.Combine(_iconDir, value.Item3 + ".png"); + return File.Exists(text) ? text : null; + } + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(query)) + { + string iconPath = GetIconPath(DefaultEngineKey); + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("검색어를 입력하세요", "예: ? 오늘 날씨 · ?g python · ?y 음악 · ?n 뉴스 | ⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지", iconPath, null, null, "\ue774"))); + } + List list = new List(); + string text = query.Trim(); + if (IsUrl(text)) + { + string text2 = (text.StartsWith("http", StringComparison.OrdinalIgnoreCase) ? text : ("https://" + text)); + string faviconPathForUrl = GetFaviconPathForUrl(text2); + list.Add(new LauncherItem("열기: " + text, text2 + " | ⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지", faviconPathForUrl, text2, null, "\ue774")); + return Task.FromResult((IEnumerable)list); + } + string[] array = text.Split(' ', 2); + string text3 = array[0].ToLowerInvariant(); + if (array.Length == 2 && _engines.TryGetValue(text3, out (string, string, string) value)) + { + string text4 = array[1].Trim(); + if (!string.IsNullOrWhiteSpace(text4)) + { + string data = string.Format(value.Item2, Uri.EscapeDataString(text4)); + list.Add(new LauncherItem(value.Item1 + "에서 '" + text4 + "' 검색", "⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지", GetIconPath(text3), data, null, "\ue774")); + } + } + string defaultEngineKey = DefaultEngineKey; + (string, string, string) defaultEngine = DefaultEngine; + string data2 = string.Format(defaultEngine.Item2, Uri.EscapeDataString(text)); + list.Add(new LauncherItem(defaultEngine.Item1 + "에서 '" + text + "' 검색", "⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지", GetIconPath(defaultEngineKey), data2, null, "\ue774")); + if (text.Length <= 2) + { + foreach (var (text6, tuple2) in _engines) + { + list.Add(new LauncherItem("?" + text6 + " — " + tuple2.Item1, $"예: ?{text6} {text} | {"⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지"}", GetIconPath(text6), null, null, "\ue774")); + } + } + return Task.FromResult((IEnumerable)list); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is string text && !string.IsNullOrWhiteSpace(text)) + { + try + { + Process.Start(new ProcessStartInfo(text) + { + UseShellExecute = true + }); + } + catch (Exception ex) + { + LogService.Warn("웹 검색 열기 실패: " + ex.Message); + } + } + return Task.CompletedTask; + } + + private static bool IsUrl(string input) + { + if (input.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || input.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + if (!input.Contains(' ') && input.Contains('.') && input.Length > 3) + { + string text = input.Split('/')[0]; + string[] array = text.Split('.'); + return array.Length >= 2 && array[^1].Length >= 2 && array[^1].Length <= 6 && array[^1].All(char.IsLetter); + } + return false; + } + + private static string? GetFaviconPathForUrl(string url) + { + try + { + if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + url = "https://" + url; + } + string text = new Uri(url).Host.ToLowerInvariant(); + string text2 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "favicons", text + ".png"); + if (File.Exists(text2)) + { + return text2; + } + FaviconService.GetFavicon(url); + return null; + } + catch + { + return null; + } + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/WindowSwitchHandler.cs b/.decompiledproj/AxCopilot/Handlers/WindowSwitchHandler.cs new file mode 100644 index 0000000..2a9ebba --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/WindowSwitchHandler.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.SDK; + +namespace AxCopilot.Handlers; + +public class WindowSwitchHandler : IActionHandler +{ + private delegate bool EnumWindowsProc(nint hWnd, nint lParam); + + private record WindowInfo(nint Handle, string Title, string ProcessName, bool IsMinimized); + + private const int SW_RESTORE = 9; + + public string? Prefix => "win"; + + public PluginMetadata Metadata => new PluginMetadata("WindowSwitch", "윈도우 포커스 스위처 — win", "1.0", "AX"); + + [DllImport("user32.dll")] + private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, nint lParam); + + [DllImport("user32.dll")] + private static extern bool IsWindowVisible(nint hWnd); + + [DllImport("user32.dll")] + private static extern int GetWindowTextLength(nint hWnd); + + [DllImport("user32.dll")] + private static extern int GetWindowText(nint hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll")] + private static extern uint GetWindowThreadProcessId(nint hWnd, out uint lpdwProcessId); + + [DllImport("user32.dll")] + private static extern bool SetForegroundWindow(nint hWnd); + + [DllImport("user32.dll")] + private static extern bool ShowWindow(nint hWnd, int nCmdShow); + + [DllImport("user32.dll")] + private static extern bool IsIconic(nint hWnd); + + [DllImport("user32.dll")] + private static extern nint GetShellWindow(); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string q = query.Trim(); + List list = GetOpenWindows(); + if (!string.IsNullOrWhiteSpace(q)) + { + list = list.Where((WindowInfo w) => w.Title.Contains(q, StringComparison.OrdinalIgnoreCase) || w.ProcessName.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList(); + } + if (!list.Any()) + { + return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem(string.IsNullOrWhiteSpace(q) ? "열린 창이 없습니다" : ("'" + q + "'에 해당하는 창 없음"), "win 뒤에 프로세스명 또는 창 제목 입력", null, null, null, "\ue946"))); + } + List list2 = (from w in list.Take(15) + select new LauncherItem((w.Title.Length > 60) ? (w.Title.Substring(0, 57) + "…") : w.Title, w.ProcessName + " · " + (w.IsMinimized ? "[최소화됨] · " : "") + "Enter → 전환", null, w.Handle, null, "\ue7f4")).ToList(); + list2.Insert(0, new LauncherItem($"열린 창 {list.Count}개", "Enter → 해당 창으로 즉시 전환", null, null, null, "\ue946")); + return Task.FromResult((IEnumerable)list2); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is nint num && num != IntPtr.Zero) + { + if (IsIconic(num)) + { + ShowWindow(num, 9); + } + SetForegroundWindow(num); + } + return Task.CompletedTask; + } + + private static List GetOpenWindows() + { + List list = new List(); + nint shellWnd = GetShellWindow(); + string currentProcessName = Process.GetCurrentProcess().ProcessName; + EnumWindows(delegate(nint hWnd, nint _) + { + if (hWnd == shellWnd) + { + return true; + } + if (!IsWindowVisible(hWnd)) + { + return true; + } + int windowTextLength = GetWindowTextLength(hWnd); + if (windowTextLength == 0) + { + return true; + } + StringBuilder stringBuilder = new StringBuilder(windowTextLength + 1); + GetWindowText(hWnd, stringBuilder, stringBuilder.Capacity); + string title = stringBuilder.ToString(); + string text = ""; + try + { + GetWindowThreadProcessId(hWnd, out var lpdwProcessId); + text = Process.GetProcessById((int)lpdwProcessId).ProcessName; + } + catch + { + } + if (text == currentProcessName) + { + return true; + } + bool flag; + switch (text) + { + case "ApplicationFrameHost": + case "ShellExperienceHost": + case "SearchHost": + case "StartMenuExperienceHost": + case "TextInputHost": + flag = true; + break; + default: + flag = false; + break; + } + if (flag) + { + return true; + } + list.Add(new WindowInfo(hWnd, title, text, IsIconic(hWnd))); + return true; + }, IntPtr.Zero); + return list; + } +} diff --git a/.decompiledproj/AxCopilot/Handlers/WorkspaceAction.cs b/.decompiledproj/AxCopilot/Handlers/WorkspaceAction.cs new file mode 100644 index 0000000..0a3f20c --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/WorkspaceAction.cs @@ -0,0 +1,3 @@ +namespace AxCopilot.Handlers; + +public record WorkspaceAction(WorkspaceActionType Type, string Name, string? NewName = null); diff --git a/.decompiledproj/AxCopilot/Handlers/WorkspaceActionType.cs b/.decompiledproj/AxCopilot/Handlers/WorkspaceActionType.cs new file mode 100644 index 0000000..775e67f --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/WorkspaceActionType.cs @@ -0,0 +1,9 @@ +namespace AxCopilot.Handlers; + +public enum WorkspaceActionType +{ + Restore, + Save, + Delete, + Rename +} diff --git a/.decompiledproj/AxCopilot/Handlers/WorkspaceHandler.cs b/.decompiledproj/AxCopilot/Handlers/WorkspaceHandler.cs new file mode 100644 index 0000000..290dead --- /dev/null +++ b/.decompiledproj/AxCopilot/Handlers/WorkspaceHandler.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.Core; +using AxCopilot.Models; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +public class WorkspaceHandler : IActionHandler +{ + private readonly ContextManager _context; + + private readonly SettingsService _settings; + + public string? Prefix => "~"; + + public PluginMetadata Metadata => new PluginMetadata("workspace", "워크스페이스", "1.0", "AX"); + + public WorkspaceHandler(ContextManager context, SettingsService settings) + { + _context = context; + _settings = settings; + } + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + string[] array = query.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (array.Length == 0 || string.IsNullOrEmpty(query)) + { + List list = _settings.Settings.Profiles.Select((WorkspaceProfile p) => new LauncherItem("~" + p.Name, $"{p.Windows.Count}개 창 | {p.CreatedAt:MM/dd HH:mm}", null, p, null, "\ue8a1")).ToList(); + if (list.Count == 0) + { + list.Add(new LauncherItem("저장된 프로필 없음", "~save <이름> 으로 현재 배치를 저장하세요", null, null, null, "\ue946")); + } + return Task.FromResult((IEnumerable)list); + } + string text = array[0].ToLowerInvariant(); + if (text == "save") + { + string text2 = ((array.Length > 1) ? array[1] : "default"); + return Task.FromResult((IEnumerable)new LauncherItem[1] + { + new LauncherItem("현재 창 배치를 '" + text2 + "'으로 저장", "Enter로 확인", null, new WorkspaceAction(WorkspaceActionType.Save, text2), null, "\ue74e") + }); + } + if (text == "delete" && array.Length > 1) + { + return Task.FromResult((IEnumerable)new LauncherItem[1] + { + new LauncherItem("프로필 '" + array[1] + "' 삭제", "Enter로 확인 (되돌릴 수 없습니다)", null, new WorkspaceAction(WorkspaceActionType.Delete, array[1]), null, "\ue74d") + }); + } + if (text == "rename" && array.Length > 2) + { + return Task.FromResult((IEnumerable)new LauncherItem[1] + { + new LauncherItem($"프로필 '{array[1]}' → '{array[2]}'로 이름 변경", "Enter로 확인", null, new WorkspaceAction(WorkspaceActionType.Rename, array[1], array[2]), null, "\ue8ac") + }); + } + IEnumerable result = from p in _settings.Settings.Profiles + where p.Name.Contains(query, StringComparison.OrdinalIgnoreCase) + select new LauncherItem("~" + p.Name + " 복원", $"{p.Windows.Count}개 창 | {p.CreatedAt:MM/dd HH:mm}", null, new WorkspaceAction(WorkspaceActionType.Restore, p.Name), null, "\ue72c"); + return Task.FromResult(result); + } + + public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + object data = item.Data; + if (!(data is WorkspaceAction action)) + { + return; + } + switch (action.Type) + { + case WorkspaceActionType.Restore: + LogService.Info("복원 결과: " + (await _context.RestoreProfileAsync(action.Name, ct)).Message); + break; + case WorkspaceActionType.Save: + _context.CaptureProfile(action.Name); + break; + case WorkspaceActionType.Delete: + _context.DeleteProfile(action.Name); + break; + case WorkspaceActionType.Rename: + if (action.NewName != null) + { + _context.RenameProfile(action.Name, action.NewName); + } + break; + } + } +} diff --git a/.decompiledproj/AxCopilot/Models/AgentHookEntry.cs b/.decompiledproj/AxCopilot/Models/AgentHookEntry.cs new file mode 100644 index 0000000..a3c278d --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/AgentHookEntry.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class AgentHookEntry +{ + [JsonPropertyName("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("toolName")] + public string ToolName { get; set; } = "*"; + + [JsonPropertyName("timing")] + public string Timing { get; set; } = "post"; + + [JsonPropertyName("scriptPath")] + public string ScriptPath { get; set; } = ""; + + [JsonPropertyName("arguments")] + public string Arguments { get; set; } = ""; + + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } = true; +} diff --git a/.decompiledproj/AxCopilot/Models/AliasEntry.cs b/.decompiledproj/AxCopilot/Models/AliasEntry.cs new file mode 100644 index 0000000..61226e7 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/AliasEntry.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class AliasEntry +{ + [JsonPropertyName("key")] + public string Key { get; set; } = ""; + + [JsonPropertyName("type")] + public string Type { get; set; } = "url"; + + [JsonPropertyName("target")] + public string Target { get; set; } = ""; + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("showWindow")] + public bool ShowWindow { get; set; } = false; + + [JsonPropertyName("adapter")] + public string? Adapter { get; set; } + + [JsonPropertyName("query")] + public string? Query { get; set; } +} diff --git a/.decompiledproj/AxCopilot/Models/ApiAdapter.cs b/.decompiledproj/AxCopilot/Models/ApiAdapter.cs new file mode 100644 index 0000000..b3be7a5 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/ApiAdapter.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class ApiAdapter +{ + [JsonPropertyName("id")] + public string Id { get; set; } = ""; + + [JsonPropertyName("baseUrl")] + public string BaseUrl { get; set; } = ""; + + [JsonPropertyName("credentialKey")] + public string CredentialKey { get; set; } = ""; +} diff --git a/.decompiledproj/AxCopilot/Models/AppSettings.cs b/.decompiledproj/AxCopilot/Models/AppSettings.cs new file mode 100644 index 0000000..53f1bc2 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/AppSettings.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class AppSettings +{ + [JsonPropertyName("version")] + public string Version { get; set; } = "1.0"; + + [JsonPropertyName("ai_enabled")] + public bool AiEnabled { get; set; } = false; + + [JsonPropertyName("hotkey")] + public string Hotkey { get; set; } = "Alt+Space"; + + [JsonPropertyName("launcher")] + public LauncherSettings Launcher { get; set; } = new LauncherSettings(); + + [JsonPropertyName("indexPaths")] + public List IndexPaths { get; set; } = new List { "%USERPROFILE%\\Desktop", "%APPDATA%\\Microsoft\\Windows\\Start Menu" }; + + [JsonPropertyName("indexExtensions")] + public List IndexExtensions { get; set; } = new List + { + ".exe", ".lnk", ".bat", ".ps1", ".url", ".cmd", ".msi", ".pdf", ".doc", ".docx", + ".xls", ".xlsx", ".ppt", ".pptx", ".hwp", ".hwpx", ".txt", ".md", ".csv", ".json", + ".xml", ".yaml", ".yml", ".log", ".ini", ".cfg", ".conf", ".png", ".jpg", ".jpeg", + ".gif", ".bmp", ".svg", ".webp", ".ico", ".tiff", ".zip", ".7z", ".rar" + }; + + [JsonPropertyName("indexSpeed")] + public string IndexSpeed { get; set; } = "normal"; + + [JsonPropertyName("monitorMismatch")] + public string MonitorMismatch { get; set; } = "warn"; + + [JsonPropertyName("profiles")] + public List Profiles { get; set; } = new List(); + + [JsonPropertyName("aliases")] + public List Aliases { get; set; } = new List(); + + [JsonPropertyName("clipboardTransformers")] + public List ClipboardTransformers { get; set; } = new List(); + + [JsonPropertyName("apiAdapters")] + public List ApiAdapters { get; set; } = new List(); + + [JsonPropertyName("plugins")] + public List Plugins { get; set; } = new List(); + + [JsonPropertyName("snippets")] + public List Snippets { get; set; } = new List(); + + [JsonPropertyName("clipboardHistory")] + public ClipboardHistorySettings ClipboardHistory { get; set; } = new ClipboardHistorySettings(); + + [JsonPropertyName("systemCommands")] + public SystemCommandSettings SystemCommands { get; set; } = new SystemCommandSettings(); + + [JsonPropertyName("screenCapture")] + public ScreenCaptureSettings ScreenCapture { get; set; } = new ScreenCaptureSettings(); + + [JsonPropertyName("reminder")] + public ReminderSettings Reminder { get; set; } = new ReminderSettings(); + + [JsonPropertyName("llm")] + public LlmSettings Llm { get; set; } = new LlmSettings(); +} diff --git a/.decompiledproj/AxCopilot/Models/ChatCategory.cs b/.decompiledproj/AxCopilot/Models/ChatCategory.cs new file mode 100644 index 0000000..2f7d78c --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/ChatCategory.cs @@ -0,0 +1,71 @@ +namespace AxCopilot.Models; + +public static class ChatCategory +{ + public const string General = "일반"; + + public const string Management = "경영"; + + public const string HR = "인사"; + + public const string Finance = "재무"; + + public const string RnD = "연구개발"; + + public const string Product = "제품분석"; + + public const string Yield = "수율분석"; + + public const string MfgTech = "제조기술"; + + public const string System = "시스템"; + + public static readonly (string Key, string Label, string Symbol, string Color)[] All = new(string, string, string, string)[9] + { + ("일반", "일반", "\ue8bd", "#6B7280"), + ("경영", "경영", "\ue902", "#8B5CF6"), + ("인사", "인사", "\ue716", "#0EA5E9"), + ("재무", "재무", "\ue8c7", "#D97706"), + ("연구개발", "연구개발", "\ue9a8", "#3B82F6"), + ("제품분석", "제품분석", "\ue9d9", "#EC4899"), + ("수율분석", "수율분석", "\ue9f9", "#F59E0B"), + ("제조기술", "제조기술", "\ue90f", "#10B981"), + ("시스템", "시스템", "\ue770", "#EF4444") + }; + + public static string GetSymbol(string? category) + { + if (string.IsNullOrEmpty(category)) + { + return "\ue8bd"; + } + (string, string, string, string)[] all = All; + for (int i = 0; i < all.Length; i++) + { + var (text, _, result, _) = all[i]; + if (text == category) + { + return result; + } + } + return "\ue8bd"; + } + + public static string GetColor(string? category) + { + if (string.IsNullOrEmpty(category)) + { + return "#6B7280"; + } + (string, string, string, string)[] all = All; + for (int i = 0; i < all.Length; i++) + { + var (text, _, _, result) = all[i]; + if (text == category) + { + return result; + } + } + return "#6B7280"; + } +} diff --git a/.decompiledproj/AxCopilot/Models/ChatConversation.cs b/.decompiledproj/AxCopilot/Models/ChatConversation.cs new file mode 100644 index 0000000..0657840 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/ChatConversation.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class ChatConversation +{ + [JsonPropertyName("id")] + public string Id { get; set; } = Guid.NewGuid().ToString("N"); + + [JsonPropertyName("title")] + public string Title { get; set; } = "새 대화"; + + [JsonPropertyName("createdAt")] + public DateTime CreatedAt { get; set; } = DateTime.Now; + + [JsonPropertyName("updatedAt")] + public DateTime UpdatedAt { get; set; } = DateTime.Now; + + [JsonPropertyName("pinned")] + public bool Pinned { get; set; } = false; + + [JsonPropertyName("tab")] + public string Tab { get; set; } = "Chat"; + + [JsonPropertyName("category")] + public string Category { get; set; } = "일반"; + + [JsonPropertyName("systemCommand")] + public string SystemCommand { get; set; } = ""; + + [JsonPropertyName("workFolder")] + public string WorkFolder { get; set; } = ""; + + [JsonPropertyName("preview")] + public string Preview { get; set; } = ""; + + [JsonPropertyName("parentId")] + public string? ParentId { get; set; } + + [JsonPropertyName("branchLabel")] + public string? BranchLabel { get; set; } + + [JsonPropertyName("branchAtIndex")] + public int? BranchAtIndex { get; set; } + + [JsonPropertyName("permission")] + public string? Permission { get; set; } + + [JsonPropertyName("dataUsage")] + public string? DataUsage { get; set; } + + [JsonPropertyName("outputFormat")] + public string? OutputFormat { get; set; } + + [JsonPropertyName("mood")] + public string? Mood { get; set; } + + [JsonPropertyName("messages")] + public List Messages { get; set; } = new List(); + + [JsonPropertyName("executionEvents")] + public List ExecutionEvents { get; set; } = new List(); +} diff --git a/.decompiledproj/AxCopilot/Models/ChatExecutionEvent.cs b/.decompiledproj/AxCopilot/Models/ChatExecutionEvent.cs new file mode 100644 index 0000000..20af140 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/ChatExecutionEvent.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class ChatExecutionEvent +{ + [JsonPropertyName("timestamp")] + public DateTime Timestamp { get; set; } = DateTime.Now; + + [JsonPropertyName("type")] + public string Type { get; set; } = "Thinking"; + + [JsonPropertyName("toolName")] + public string ToolName { get; set; } = ""; + + [JsonPropertyName("summary")] + public string Summary { get; set; } = ""; + + [JsonPropertyName("filePath")] + public string? FilePath { get; set; } + + [JsonPropertyName("success")] + public bool Success { get; set; } = true; + + [JsonPropertyName("stepCurrent")] + public int StepCurrent { get; set; } + + [JsonPropertyName("stepTotal")] + public int StepTotal { get; set; } + + [JsonPropertyName("steps")] + public List? Steps { get; set; } + + [JsonPropertyName("elapsedMs")] + public long ElapsedMs { get; set; } + + [JsonPropertyName("inputTokens")] + public int InputTokens { get; set; } + + [JsonPropertyName("outputTokens")] + public int OutputTokens { get; set; } + + [JsonPropertyName("toolInput")] + public string? ToolInput { get; set; } + + [JsonPropertyName("iteration")] + public int Iteration { get; set; } +} diff --git a/.decompiledproj/AxCopilot/Models/ChatMessage.cs b/.decompiledproj/AxCopilot/Models/ChatMessage.cs new file mode 100644 index 0000000..258a630 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/ChatMessage.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class ChatMessage +{ + [JsonPropertyName("role")] + public string Role { get; set; } = "user"; + + [JsonPropertyName("content")] + public string Content { get; set; } = ""; + + [JsonPropertyName("timestamp")] + public DateTime Timestamp { get; set; } = DateTime.Now; + + [JsonPropertyName("feedback")] + public string? Feedback { get; set; } + + [JsonPropertyName("attachedFiles")] + public List? AttachedFiles { get; set; } + + [JsonPropertyName("images")] + public List? Images { get; set; } +} diff --git a/.decompiledproj/AxCopilot/Models/ClipboardHistorySettings.cs b/.decompiledproj/AxCopilot/Models/ClipboardHistorySettings.cs new file mode 100644 index 0000000..27c26cb --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/ClipboardHistorySettings.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class ClipboardHistorySettings +{ + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } = true; + + [JsonPropertyName("maxItems")] + public int MaxItems { get; set; } = 50; + + [JsonPropertyName("excludePatterns")] + public List ExcludePatterns { get; set; } = new List { "^\\d{4}[\\s\\-]?\\d{4}[\\s\\-]?\\d{4}[\\s\\-]?\\d{4}$", "^(?:\\d{1,3}\\.){3}\\d{1,3}$" }; +} diff --git a/.decompiledproj/AxCopilot/Models/ClipboardTransformer.cs b/.decompiledproj/AxCopilot/Models/ClipboardTransformer.cs new file mode 100644 index 0000000..9e92ec0 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/ClipboardTransformer.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class ClipboardTransformer +{ + [JsonPropertyName("key")] + public string Key { get; set; } = ""; + + [JsonPropertyName("type")] + public string Type { get; set; } = "regex"; + + [JsonPropertyName("pattern")] + public string? Pattern { get; set; } + + [JsonPropertyName("replace")] + public string? Replace { get; set; } + + [JsonPropertyName("command")] + public string? Command { get; set; } + + [JsonPropertyName("timeout")] + public int Timeout { get; set; } = 5000; + + [JsonPropertyName("description")] + public string? Description { get; set; } +} diff --git a/.decompiledproj/AxCopilot/Models/CodeSettings.cs b/.decompiledproj/AxCopilot/Models/CodeSettings.cs new file mode 100644 index 0000000..64c44c9 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/CodeSettings.cs @@ -0,0 +1,48 @@ +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class CodeSettings +{ + [JsonPropertyName("nexusBaseUrl")] + public string NexusBaseUrl { get; set; } = ""; + + [JsonPropertyName("nugetSource")] + public string NugetSource { get; set; } = "https://api.nuget.org/v3/index.json"; + + [JsonPropertyName("pypiSource")] + public string PypiSource { get; set; } = "https://conda.anaconda.org/conda-forge"; + + [JsonPropertyName("mavenSource")] + public string MavenSource { get; set; } = "https://repo1.maven.org/maven2"; + + [JsonPropertyName("npmSource")] + public string NpmSource { get; set; } = "https://registry.npmjs.org"; + + [JsonPropertyName("preferredIdePath")] + public string PreferredIdePath { get; set; } = ""; + + [JsonPropertyName("buildTimeout")] + public int BuildTimeout { get; set; } = 120; + + [JsonPropertyName("enableLsp")] + public bool EnableLsp { get; set; } = true; + + [JsonPropertyName("enableCodeIndex")] + public bool EnableCodeIndex { get; set; } = true; + + [JsonPropertyName("codeIndexMaxFileKb")] + public int CodeIndexMaxFileKb { get; set; } = 500; + + [JsonPropertyName("enableAutoDiff")] + public bool EnableAutoDiff { get; set; } = true; + + [JsonPropertyName("enableCodeReview")] + public bool EnableCodeReview { get; set; } = true; + + [JsonPropertyName("enableSnippetRunner")] + public bool EnableSnippetRunner { get; set; } = true; + + [JsonPropertyName("enableCodeVerification")] + public bool EnableCodeVerification { get; set; } = false; +} diff --git a/.decompiledproj/AxCopilot/Models/CustomMoodEntry.cs b/.decompiledproj/AxCopilot/Models/CustomMoodEntry.cs new file mode 100644 index 0000000..606e3d3 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/CustomMoodEntry.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class CustomMoodEntry +{ + [JsonPropertyName("key")] + public string Key { get; set; } = ""; + + [JsonPropertyName("label")] + public string Label { get; set; } = ""; + + [JsonPropertyName("icon")] + public string Icon { get; set; } = "\ud83c\udfaf"; + + [JsonPropertyName("description")] + public string Description { get; set; } = ""; + + [JsonPropertyName("css")] + public string Css { get; set; } = ""; +} diff --git a/.decompiledproj/AxCopilot/Models/CustomPresetEntry.cs b/.decompiledproj/AxCopilot/Models/CustomPresetEntry.cs new file mode 100644 index 0000000..df47f02 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/CustomPresetEntry.cs @@ -0,0 +1,28 @@ +using System; +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class CustomPresetEntry +{ + [JsonPropertyName("id")] + public string Id { get; set; } = Guid.NewGuid().ToString("N").Substring(0, 8); + + [JsonPropertyName("label")] + public string Label { get; set; } = ""; + + [JsonPropertyName("description")] + public string Description { get; set; } = ""; + + [JsonPropertyName("systemPrompt")] + public string SystemPrompt { get; set; } = ""; + + [JsonPropertyName("color")] + public string Color { get; set; } = "#6366F1"; + + [JsonPropertyName("symbol")] + public string Symbol { get; set; } = "\ue713"; + + [JsonPropertyName("tab")] + public string Tab { get; set; } = "Chat"; +} diff --git a/.decompiledproj/AxCopilot/Models/CustomThemeColors.cs b/.decompiledproj/AxCopilot/Models/CustomThemeColors.cs new file mode 100644 index 0000000..214c7df --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/CustomThemeColors.cs @@ -0,0 +1,54 @@ +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class CustomThemeColors +{ + [JsonPropertyName("launcherBackground")] + public string LauncherBackground { get; set; } = "#1A1B2E"; + + [JsonPropertyName("itemBackground")] + public string ItemBackground { get; set; } = "#252637"; + + [JsonPropertyName("itemSelectedBackground")] + public string ItemSelectedBackground { get; set; } = "#3B4BDB"; + + [JsonPropertyName("itemHoverBackground")] + public string ItemHoverBackground { get; set; } = "#22233A"; + + [JsonPropertyName("primaryText")] + public string PrimaryText { get; set; } = "#F0F0FF"; + + [JsonPropertyName("secondaryText")] + public string SecondaryText { get; set; } = "#7A7D9C"; + + [JsonPropertyName("placeholderText")] + public string PlaceholderText { get; set; } = "#464868"; + + [JsonPropertyName("accentColor")] + public string AccentColor { get; set; } = "#4B5EFC"; + + [JsonPropertyName("separatorColor")] + public string SeparatorColor { get; set; } = "#252637"; + + [JsonPropertyName("hintBackground")] + public string HintBackground { get; set; } = "#252637"; + + [JsonPropertyName("hintText")] + public string HintText { get; set; } = "#4B5070"; + + [JsonPropertyName("borderColor")] + public string BorderColor { get; set; } = "#2E2F4A"; + + [JsonPropertyName("scrollbarThumb")] + public string ScrollbarThumb { get; set; } = "#3A3B5A"; + + [JsonPropertyName("shadowColor")] + public string ShadowColor { get; set; } = "#000000"; + + [JsonPropertyName("windowCornerRadius")] + public int WindowCornerRadius { get; set; } = 20; + + [JsonPropertyName("itemCornerRadius")] + public int ItemCornerRadius { get; set; } = 10; +} diff --git a/.decompiledproj/AxCopilot/Models/DailyUsageStats.cs b/.decompiledproj/AxCopilot/Models/DailyUsageStats.cs new file mode 100644 index 0000000..3b9ede1 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/DailyUsageStats.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class DailyUsageStats +{ + [JsonPropertyName("date")] + public string Date { get; set; } = ""; + + [JsonPropertyName("launcherOpens")] + public int LauncherOpens { get; set; } + + [JsonPropertyName("commandUsage")] + public Dictionary CommandUsage { get; set; } = new Dictionary(); + + [JsonPropertyName("activeSeconds")] + public int ActiveSeconds { get; set; } + + [JsonPropertyName("chatCounts")] + public Dictionary ChatCounts { get; set; } = new Dictionary(); + + [JsonPropertyName("totalTokens")] + public long TotalTokens { get; set; } + + [JsonPropertyName("promptTokens")] + public long PromptTokens { get; set; } + + [JsonPropertyName("completionTokens")] + public long CompletionTokens { get; set; } +} diff --git a/.decompiledproj/AxCopilot/Models/ImageAttachment.cs b/.decompiledproj/AxCopilot/Models/ImageAttachment.cs new file mode 100644 index 0000000..06ee298 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/ImageAttachment.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class ImageAttachment +{ + [JsonPropertyName("base64")] + public string Base64 { get; set; } = ""; + + [JsonPropertyName("mimeType")] + public string MimeType { get; set; } = "image/png"; + + [JsonPropertyName("fileName")] + public string FileName { get; set; } = ""; +} diff --git a/.decompiledproj/AxCopilot/Models/LauncherSettings.cs b/.decompiledproj/AxCopilot/Models/LauncherSettings.cs new file mode 100644 index 0000000..27cf3aa --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/LauncherSettings.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class LauncherSettings +{ + [JsonPropertyName("opacity")] + public double Opacity { get; set; } = 0.96; + + [JsonPropertyName("maxResults")] + public int MaxResults { get; set; } = 7; + + [JsonPropertyName("theme")] + public string Theme { get; set; } = "system"; + + [JsonPropertyName("position")] + public string Position { get; set; } = "center-top"; + + [JsonPropertyName("width")] + public double Width { get; set; } = 680.0; + + [JsonPropertyName("webSearchEngine")] + public string WebSearchEngine { get; set; } = "g"; + + [JsonPropertyName("snippetAutoExpand")] + public bool SnippetAutoExpand { get; set; } = true; + + [JsonPropertyName("language")] + public string Language { get; set; } = "ko"; + + [JsonPropertyName("customTheme")] + public CustomThemeColors? CustomTheme { get; set; } + + [JsonPropertyName("showNumberBadges")] + public bool ShowNumberBadges { get; set; } = true; + + [JsonPropertyName("enableFavorites")] + public bool EnableFavorites { get; set; } = true; + + [JsonPropertyName("enableRecent")] + public bool EnableRecent { get; set; } = true; + + [JsonPropertyName("enableActionMode")] + public bool EnableActionMode { get; set; } = true; + + [JsonPropertyName("closeOnFocusLost")] + public bool CloseOnFocusLost { get; set; } = true; + + [JsonPropertyName("showPrefixBadge")] + public bool ShowPrefixBadge { get; set; } = true; + + [JsonPropertyName("enableIconAnimation")] + public bool EnableIconAnimation { get; set; } = true; + + [JsonPropertyName("enableRandomPlaceholder")] + public bool EnableRandomPlaceholder { get; set; } = true; + + [JsonPropertyName("enableRainbowGlow")] + public bool EnableRainbowGlow { get; set; } = false; + + [JsonPropertyName("enableSelectionGlow")] + public bool EnableSelectionGlow { get; set; } = false; + + [JsonPropertyName("showLauncherBorder")] + public bool ShowLauncherBorder { get; set; } = true; + + [JsonPropertyName("shortcutHelpUseThemeColor")] + public bool ShortcutHelpUseThemeColor { get; set; } = true; + + [JsonPropertyName("enableTextAction")] + public bool EnableTextAction { get; set; } = true; + + [JsonPropertyName("textActionCommands")] + public List TextActionCommands { get; set; } = new List { "translate", "summarize", "grammar", "explain", "rewrite" }; + + [JsonPropertyName("textActionTranslateLanguage")] + public string TextActionTranslateLanguage { get; set; } = "auto"; + + [JsonPropertyName("enableFileDialogIntegration")] + public bool EnableFileDialogIntegration { get; set; } = false; + + [JsonPropertyName("enableClipboardAutoCategory")] + public bool EnableClipboardAutoCategory { get; set; } = true; + + [JsonPropertyName("maxPinnedClipboardItems")] + public int MaxPinnedClipboardItems { get; set; } = 20; + + [JsonPropertyName("dockBarItems")] + public List DockBarItems { get; set; } = new List { "launcher", "clipboard", "capture", "agent", "clock", "cpu" }; + + [JsonPropertyName("dockBarAutoShow")] + public bool DockBarAutoShow { get; set; } = false; + + [JsonPropertyName("dockBarOpacity")] + public double DockBarOpacity { get; set; } = 0.92; + + [JsonPropertyName("dockBarRainbowGlow")] + public bool DockBarRainbowGlow { get; set; } = false; + + [JsonPropertyName("dockBarLeft")] + public double DockBarLeft { get; set; } = -1.0; + + [JsonPropertyName("dockBarTop")] + public double DockBarTop { get; set; } = -1.0; +} diff --git a/.decompiledproj/AxCopilot/Models/LlmSettings.cs b/.decompiledproj/AxCopilot/Models/LlmSettings.cs new file mode 100644 index 0000000..95c418f --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/LlmSettings.cs @@ -0,0 +1,256 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class LlmSettings +{ + [JsonPropertyName("service")] + public string Service { get; set; } = "ollama"; + + [JsonPropertyName("endpoint")] + public string Endpoint { get; set; } = "http://localhost:11434"; + + [JsonPropertyName("model")] + public string Model { get; set; } = ""; + + [JsonPropertyName("encryptedApiKey")] + public string EncryptedApiKey { get; set; } = ""; + + [JsonPropertyName("apiKey")] + public string ApiKey { get; set; } = ""; + + [JsonPropertyName("streaming")] + public bool Streaming { get; set; } = true; + + [JsonPropertyName("maxContextTokens")] + public int MaxContextTokens { get; set; } = 4096; + + [JsonPropertyName("retentionDays")] + public int RetentionDays { get; set; } = 30; + + [JsonPropertyName("temperature")] + public double Temperature { get; set; } = 0.7; + + [JsonPropertyName("registeredModels")] + public List RegisteredModels { get; set; } = new List(); + + [JsonPropertyName("encryptionEnabled")] + public bool EncryptionEnabled { get; set; } = false; + + [JsonPropertyName("promptTemplates")] + public List PromptTemplates { get; set; } = new List(); + + [JsonPropertyName("workFolder")] + public string WorkFolder { get; set; } = ""; + + [JsonPropertyName("recentWorkFolders")] + public List RecentWorkFolders { get; set; } = new List(); + + [JsonPropertyName("maxRecentFolders")] + public int MaxRecentFolders { get; set; } = 10; + + [JsonPropertyName("showFileBrowser")] + public bool ShowFileBrowser { get; set; } = false; + + [JsonPropertyName("filePermission")] + public string FilePermission { get; set; } = "Ask"; + + [JsonPropertyName("defaultAgentPermission")] + public string DefaultAgentPermission { get; set; } = "Ask"; + + [JsonPropertyName("ollamaEndpoint")] + public string OllamaEndpoint { get; set; } = "http://localhost:11434"; + + [JsonPropertyName("ollamaApiKey")] + public string OllamaApiKey { get; set; } = ""; + + [JsonPropertyName("ollamaModel")] + public string OllamaModel { get; set; } = ""; + + [JsonPropertyName("vllmEndpoint")] + public string VllmEndpoint { get; set; } = ""; + + [JsonPropertyName("vllmApiKey")] + public string VllmApiKey { get; set; } = ""; + + [JsonPropertyName("vllmModel")] + public string VllmModel { get; set; } = ""; + + [JsonPropertyName("geminiApiKey")] + public string GeminiApiKey { get; set; } = ""; + + [JsonPropertyName("geminiModel")] + public string GeminiModel { get; set; } = "gemini-2.5-flash"; + + [JsonPropertyName("claudeApiKey")] + public string ClaudeApiKey { get; set; } = ""; + + [JsonPropertyName("claudeModel")] + public string ClaudeModel { get; set; } = "claude-sonnet-4-6"; + + [JsonPropertyName("defaultOutputFormat")] + public string DefaultOutputFormat { get; set; } = "auto"; + + [JsonPropertyName("defaultMood")] + public string DefaultMood { get; set; } = "modern"; + + [JsonPropertyName("autoPreview")] + public string AutoPreview { get; set; } = "off"; + + [JsonPropertyName("maxAgentIterations")] + public int MaxAgentIterations { get; set; } = 25; + + [JsonPropertyName("maxRetryOnError")] + public int MaxRetryOnError { get; set; } = 3; + + [JsonPropertyName("maxTestFixIterations")] + public int MaxTestFixIterations { get; set; } = 5; + + [JsonPropertyName("agentLogLevel")] + public string AgentLogLevel { get; set; } = "simple"; + + [JsonPropertyName("enableParallelTools")] + public bool EnableParallelTools { get; set; } = false; + + [JsonPropertyName("enableMultiPassDocument")] + public bool EnableMultiPassDocument { get; set; } = false; + + [JsonPropertyName("enableCoworkVerification")] + public bool EnableCoworkVerification { get; set; } = false; + + [JsonPropertyName("enableFilePathHighlight")] + public bool EnableFilePathHighlight { get; set; } = true; + + [JsonPropertyName("multiPassThresholdPages")] + public int MultiPassThresholdPages { get; set; } = 3; + + [JsonPropertyName("lastConversationIds")] + public Dictionary LastConversationIds { get; set; } = new Dictionary(); + + [JsonPropertyName("folderDataUsage")] + public string FolderDataUsage { get; set; } = "active"; + + [JsonPropertyName("agentDecisionLevel")] + public string AgentDecisionLevel { get; set; } = "detailed"; + + [JsonPropertyName("planMode")] + public string PlanMode { get; set; } = "off"; + + [JsonPropertyName("enableProjectRules")] + public bool EnableProjectRules { get; set; } = true; + + [JsonPropertyName("fallbackModels")] + public List FallbackModels { get; set; } = new List(); + + [JsonPropertyName("maxSubAgents")] + public int MaxSubAgents { get; set; } = 3; + + [JsonPropertyName("pdfExportPath")] + public string PdfExportPath { get; set; } = ""; + + [JsonPropertyName("enableAuditLog")] + public bool EnableAuditLog { get; set; } = true; + + [JsonPropertyName("enableAgentMemory")] + public bool EnableAgentMemory { get; set; } = true; + + [JsonPropertyName("maxMemoryEntries")] + public int MaxMemoryEntries { get; set; } = 100; + + [JsonPropertyName("enableImageInput")] + public bool EnableImageInput { get; set; } = true; + + [JsonPropertyName("maxImageSizeKb")] + public int MaxImageSizeKb { get; set; } = 5120; + + [JsonPropertyName("enableAutoRouter")] + public bool EnableAutoRouter { get; set; } = false; + + [JsonPropertyName("autoRouterConfidence")] + public double AutoRouterConfidence { get; set; } = 0.7; + + [JsonPropertyName("modelCapabilities")] + public List ModelCapabilities { get; set; } = new List(); + + [JsonPropertyName("mcpServers")] + public List McpServers { get; set; } = new List(); + + [JsonPropertyName("enableChatRainbowGlow")] + public bool EnableChatRainbowGlow { get; set; } = false; + + [JsonPropertyName("notifyOnComplete")] + public bool NotifyOnComplete { get; set; } = false; + + [JsonPropertyName("showTips")] + public bool ShowTips { get; set; } = false; + + [JsonPropertyName("tipDurationSeconds")] + public int TipDurationSeconds { get; set; } = 5; + + [JsonPropertyName("devMode")] + public bool DevMode { get; set; } = false; + + [JsonPropertyName("devModeStepApproval")] + public bool DevModeStepApproval { get; set; } = false; + + [JsonPropertyName("workflowVisualizer")] + public bool WorkflowVisualizer { get; set; } = false; + + [JsonPropertyName("freeTierMode")] + public bool FreeTierMode { get; set; } = false; + + [JsonPropertyName("freeTierDelaySeconds")] + public int FreeTierDelaySeconds { get; set; } = 4; + + [JsonPropertyName("showTotalCallStats")] + public bool ShowTotalCallStats { get; set; } = false; + + [JsonPropertyName("blockedPaths")] + public List BlockedPaths { get; set; } = new List { "*\\Windows\\*", "*\\Program Files\\*", "*\\Program Files (x86)\\*", "*\\System32\\*", "*\\AppData\\Local\\*", "*Documents*" }; + + [JsonPropertyName("blockedExtensions")] + public List BlockedExtensions { get; set; } = new List { ".exe", ".dll", ".sys", ".msi", ".reg", ".vbs", ".com", ".scr", ".pif" }; + + [JsonPropertyName("customPresets")] + public List CustomPresets { get; set; } = new List(); + + [JsonPropertyName("customMoods")] + public List CustomMoods { get; set; } = new List(); + + [JsonPropertyName("disabledTools")] + public List DisabledTools { get; set; } = new List(); + + [JsonPropertyName("toolPermissions")] + public Dictionary ToolPermissions { get; set; } = new Dictionary(); + + [JsonPropertyName("enableToolHooks")] + public bool EnableToolHooks { get; set; } = true; + + [JsonPropertyName("toolHookTimeoutMs")] + public int ToolHookTimeoutMs { get; set; } = 10000; + + [JsonPropertyName("agentHooks")] + public List AgentHooks { get; set; } = new List(); + + [JsonPropertyName("enableSkillSystem")] + public bool EnableSkillSystem { get; set; } = true; + + [JsonPropertyName("skillsFolderPath")] + public string SkillsFolderPath { get; set; } = ""; + + [JsonPropertyName("slashPopupPageSize")] + public int SlashPopupPageSize { get; set; } = 7; + + [JsonPropertyName("favoriteSlashCommands")] + public List FavoriteSlashCommands { get; set; } = new List(); + + [JsonPropertyName("enableDragDropAiActions")] + public bool EnableDragDropAiActions { get; set; } = true; + + [JsonPropertyName("dragDropAutoSend")] + public bool DragDropAutoSend { get; set; } = false; + + [JsonPropertyName("code")] + public CodeSettings Code { get; set; } = new CodeSettings(); +} diff --git a/.decompiledproj/AxCopilot/Models/McpParameterDef.cs b/.decompiledproj/AxCopilot/Models/McpParameterDef.cs new file mode 100644 index 0000000..5cabea8 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/McpParameterDef.cs @@ -0,0 +1,10 @@ +namespace AxCopilot.Models; + +public class McpParameterDef +{ + public string Type { get; set; } = "string"; + + public string Description { get; set; } = ""; + + public bool Required { get; set; } = false; +} diff --git a/.decompiledproj/AxCopilot/Models/McpServerEntry.cs b/.decompiledproj/AxCopilot/Models/McpServerEntry.cs new file mode 100644 index 0000000..24e6561 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/McpServerEntry.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class McpServerEntry +{ + [JsonPropertyName("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("command")] + public string Command { get; set; } = ""; + + [JsonPropertyName("args")] + public List Args { get; set; } = new List(); + + [JsonPropertyName("env")] + public Dictionary Env { get; set; } = new Dictionary(); + + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } = true; + + [JsonPropertyName("transport")] + public string Transport { get; set; } = "stdio"; + + [JsonPropertyName("url")] + public string? Url { get; set; } +} diff --git a/.decompiledproj/AxCopilot/Models/McpToolDefinition.cs b/.decompiledproj/AxCopilot/Models/McpToolDefinition.cs new file mode 100644 index 0000000..6371d59 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/McpToolDefinition.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace AxCopilot.Models; + +public class McpToolDefinition +{ + public string Name { get; set; } = ""; + + public string Description { get; set; } = ""; + + public string ServerName { get; set; } = ""; + + public Dictionary Parameters { get; set; } = new Dictionary(); +} diff --git a/.decompiledproj/AxCopilot/Models/ModelCapability.cs b/.decompiledproj/AxCopilot/Models/ModelCapability.cs new file mode 100644 index 0000000..343d5cc --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/ModelCapability.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class ModelCapability +{ + [JsonPropertyName("service")] + public string Service { get; set; } = ""; + + [JsonPropertyName("model")] + public string Model { get; set; } = ""; + + [JsonPropertyName("alias")] + public string Alias { get; set; } = ""; + + [JsonPropertyName("scores")] + public Dictionary Scores { get; set; } = new Dictionary(); + + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } = true; +} diff --git a/.decompiledproj/AxCopilot/Models/PluginEntry.cs b/.decompiledproj/AxCopilot/Models/PluginEntry.cs new file mode 100644 index 0000000..32466cc --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/PluginEntry.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class PluginEntry +{ + [JsonPropertyName("path")] + public string Path { get; set; } = ""; + + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } = true; +} diff --git a/.decompiledproj/AxCopilot/Models/PromptTemplate.cs b/.decompiledproj/AxCopilot/Models/PromptTemplate.cs new file mode 100644 index 0000000..22169f8 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/PromptTemplate.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class PromptTemplate +{ + [JsonPropertyName("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("content")] + public string Content { get; set; } = ""; + + [JsonPropertyName("icon")] + public string Icon { get; set; } = "\ue8bd"; +} diff --git a/.decompiledproj/AxCopilot/Models/RegisteredModel.cs b/.decompiledproj/AxCopilot/Models/RegisteredModel.cs new file mode 100644 index 0000000..e194802 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/RegisteredModel.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class RegisteredModel +{ + [JsonPropertyName("alias")] + public string Alias { get; set; } = ""; + + [JsonPropertyName("encryptedModelName")] + public string EncryptedModelName { get; set; } = ""; + + [JsonPropertyName("service")] + public string Service { get; set; } = "ollama"; + + [JsonPropertyName("endpoint")] + public string Endpoint { get; set; } = ""; + + [JsonPropertyName("apiKey")] + public string ApiKey { get; set; } = ""; + + [JsonPropertyName("authType")] + public string AuthType { get; set; } = "bearer"; + + [JsonPropertyName("cp4dUrl")] + public string Cp4dUrl { get; set; } = ""; + + [JsonPropertyName("cp4dUsername")] + public string Cp4dUsername { get; set; } = ""; + + [JsonPropertyName("cp4dPassword")] + public string Cp4dPassword { get; set; } = ""; +} diff --git a/.decompiledproj/AxCopilot/Models/ReminderSettings.cs b/.decompiledproj/AxCopilot/Models/ReminderSettings.cs new file mode 100644 index 0000000..34c86ca --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/ReminderSettings.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class ReminderSettings +{ + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } = false; + + [JsonPropertyName("corner")] + public string Corner { get; set; } = "bottom-right"; + + [JsonPropertyName("intervalMinutes")] + public int IntervalMinutes { get; set; } = 60; + + [JsonPropertyName("displaySeconds")] + public int DisplaySeconds { get; set; } = 15; + + [JsonPropertyName("enabledCategories")] + public List EnabledCategories { get; set; } = new List { "motivational" }; +} diff --git a/.decompiledproj/AxCopilot/Models/ScreenCaptureSettings.cs b/.decompiledproj/AxCopilot/Models/ScreenCaptureSettings.cs new file mode 100644 index 0000000..6dd771c --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/ScreenCaptureSettings.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class ScreenCaptureSettings +{ + [JsonPropertyName("prefix")] + public string Prefix { get; set; } = "cap"; + + [JsonPropertyName("globalHotkeyEnabled")] + public bool GlobalHotkeyEnabled { get; set; } = false; + + [JsonPropertyName("globalHotkey")] + public string GlobalHotkey { get; set; } = "PrintScreen"; + + [JsonPropertyName("globalHotkeyMode")] + public string GlobalHotkeyMode { get; set; } = "screen"; + + [JsonPropertyName("scrollDelayMs")] + public int ScrollDelayMs { get; set; } = 120; +} diff --git a/.decompiledproj/AxCopilot/Models/SnippetEntry.cs b/.decompiledproj/AxCopilot/Models/SnippetEntry.cs new file mode 100644 index 0000000..1b296ab --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/SnippetEntry.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class SnippetEntry +{ + [JsonPropertyName("key")] + public string Key { get; set; } = ""; + + [JsonPropertyName("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("content")] + public string Content { get; set; } = ""; +} diff --git a/.decompiledproj/AxCopilot/Models/SystemCommandSettings.cs b/.decompiledproj/AxCopilot/Models/SystemCommandSettings.cs new file mode 100644 index 0000000..63837cd --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/SystemCommandSettings.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class SystemCommandSettings +{ + [JsonPropertyName("showLock")] + public bool ShowLock { get; set; } = true; + + [JsonPropertyName("showSleep")] + public bool ShowSleep { get; set; } = true; + + [JsonPropertyName("showRestart")] + public bool ShowRestart { get; set; } = true; + + [JsonPropertyName("showShutdown")] + public bool ShowShutdown { get; set; } = true; + + [JsonPropertyName("showHibernate")] + public bool ShowHibernate { get; set; } = false; + + [JsonPropertyName("showLogout")] + public bool ShowLogout { get; set; } = true; + + [JsonPropertyName("showRecycleBin")] + public bool ShowRecycleBin { get; set; } = true; + + [JsonPropertyName("commandAliases")] + public Dictionary> CommandAliases { get; set; } = new Dictionary>(); +} diff --git a/.decompiledproj/AxCopilot/Models/WindowRect.cs b/.decompiledproj/AxCopilot/Models/WindowRect.cs new file mode 100644 index 0000000..934085e --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/WindowRect.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class WindowRect +{ + [JsonPropertyName("x")] + public int X { get; set; } + + [JsonPropertyName("y")] + public int Y { get; set; } + + [JsonPropertyName("width")] + public int Width { get; set; } + + [JsonPropertyName("height")] + public int Height { get; set; } +} diff --git a/.decompiledproj/AxCopilot/Models/WindowSnapshot.cs b/.decompiledproj/AxCopilot/Models/WindowSnapshot.cs new file mode 100644 index 0000000..ff74e1e --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/WindowSnapshot.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class WindowSnapshot +{ + [JsonPropertyName("exe")] + public string Exe { get; set; } = ""; + + [JsonPropertyName("title")] + public string Title { get; set; } = ""; + + [JsonPropertyName("rect")] + public WindowRect Rect { get; set; } = new WindowRect(); + + [JsonPropertyName("showCmd")] + public string ShowCmd { get; set; } = "Normal"; + + [JsonPropertyName("monitor")] + public int Monitor { get; set; } = 0; +} diff --git a/.decompiledproj/AxCopilot/Models/WorkspaceProfile.cs b/.decompiledproj/AxCopilot/Models/WorkspaceProfile.cs new file mode 100644 index 0000000..4028cc0 --- /dev/null +++ b/.decompiledproj/AxCopilot/Models/WorkspaceProfile.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AxCopilot.Models; + +public class WorkspaceProfile +{ + [JsonPropertyName("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("windows")] + public List Windows { get; set; } = new List(); + + [JsonPropertyName("createdAt")] + public DateTime CreatedAt { get; set; } = DateTime.Now; +} diff --git a/.decompiledproj/AxCopilot/Security/AntiTamper.cs b/.decompiledproj/AxCopilot/Security/AntiTamper.cs new file mode 100644 index 0000000..082acab --- /dev/null +++ b/.decompiledproj/AxCopilot/Security/AntiTamper.cs @@ -0,0 +1,133 @@ +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace AxCopilot.Security; + +internal static class AntiTamper +{ + private static readonly string[] TrustedIpPrefixes = new string[6] { "11.99.", "11.90.", "11.96.", "12.25.", "12.23.", "12.24." }; + + private static readonly string[] KnownDebuggers = new string[16] + { + "dnspy", "dnspy-x86", "ilspy", "dotpeek", "de4dot", "ollydbg", "x64dbg", "x32dbg", "windbg", "ida", + "ida64", "ghidra", "cheatengine", "processhacker", "fiddler", "wireshark" + }; + + [DllImport("kernel32.dll")] + private static extern bool IsDebuggerPresent(); + + [DllImport("kernel32.dll")] + private static extern bool CheckRemoteDebuggerPresent(nint hProcess, ref bool isDebuggerPresent); + + public static void Check() + { + } + + private static bool IsBeingDebugged() + { + if (Debugger.IsAttached) + { + return true; + } + try + { + if (IsDebuggerPresent()) + { + return true; + } + } + catch + { + } + try + { + bool isDebuggerPresent = false; + CheckRemoteDebuggerPresent(Process.GetCurrentProcess().Handle, ref isDebuggerPresent); + if (isDebuggerPresent) + { + return true; + } + } + catch + { + } + return false; + } + + private static bool IsDecompilerRunning() + { + try + { + Process[] processes = Process.GetProcesses(); + Process[] array = processes; + foreach (Process process in array) + { + try + { + string text = process.ProcessName.ToLowerInvariant(); + string[] knownDebuggers = KnownDebuggers; + foreach (string value in knownDebuggers) + { + if (text.Contains(value)) + { + return true; + } + } + } + catch + { + } + } + } + catch + { + } + return false; + } + + private static bool IsTrustedNetwork() + { + try + { + IPAddress[] hostAddresses = Dns.GetHostAddresses(Dns.GetHostName()); + IPAddress[] array = hostAddresses; + foreach (IPAddress iPAddress in array) + { + if (iPAddress.AddressFamily != AddressFamily.InterNetwork) + { + continue; + } + string text = iPAddress.ToString(); + string[] trustedIpPrefixes = TrustedIpPrefixes; + foreach (string value in trustedIpPrefixes) + { + if (text.StartsWith(value)) + { + return true; + } + } + } + } + catch + { + } + return false; + } + + public static bool VerifyAssemblyIntegrity() + { + try + { + Assembly executingAssembly = Assembly.GetExecutingAssembly(); + AssemblyName name = executingAssembly.GetName(); + return name.Version != null && name.Name == "AxCopilot"; + } + catch + { + return false; + } + } +} diff --git a/.decompiledproj/AxCopilot/Services/Agent/AgentContext.cs b/.decompiledproj/AxCopilot/Services/Agent/AgentContext.cs new file mode 100644 index 0000000..6006e7e --- /dev/null +++ b/.decompiledproj/AxCopilot/Services/Agent/AgentContext.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace AxCopilot.Services.Agent; + +public class AgentContext +{ + public string WorkFolder { get; init; } = ""; + + public string Permission { get; init; } = "Ask"; + + public Dictionary ToolPermissions { get; init; } = new Dictionary(); + + public List BlockedPaths { get; init; } = new List(); + + public List BlockedExtensions { get; init; } = new List(); + + public string ActiveTab { get; init; } = "Chat"; + + public bool DevMode { get; init; } + + public bool DevModeStepApproval { get; init; } + + public Func>? AskPermission { get; init; } + + public Func, Task>? UserDecision { get; init; } + + public Func, string, Task>? UserAskCallback { get; init; } + + public bool IsPathAllowed(string path) + { + string fullPath = Path.GetFullPath(path); + string ext = Path.GetExtension(fullPath).ToLowerInvariant(); + if (BlockedExtensions.Any((string e) => string.Equals(e, ext, StringComparison.OrdinalIgnoreCase))) + { + return false; + } + foreach (string blockedPath in BlockedPaths) + { + string value = blockedPath.Replace("*", ""); + if (!string.IsNullOrEmpty(value) && fullPath.Contains(value, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + if (!string.IsNullOrEmpty(WorkFolder)) + { + string fullPath2 = Path.GetFullPath(WorkFolder); + if (!fullPath.StartsWith(fullPath2, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + return true; + } + + public static string EnsureTimestampedPath(string fullPath) + { + string path = Path.GetDirectoryName(fullPath) ?? ""; + string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fullPath); + string extension = Path.GetExtension(fullPath); + string text = DateTime.Now.ToString("yyyyMMdd_HHmm"); + if (Regex.IsMatch(fileNameWithoutExtension, "_\\d{8}_\\d{4}$")) + { + return fullPath; + } + return Path.Combine(path, fileNameWithoutExtension + "_" + text + extension); + } + + public async Task CheckWritePermissionAsync(string toolName, string filePath) + { + string effectivePerm = Permission; + if (ToolPermissions.TryGetValue(toolName, out string toolPerm)) + { + effectivePerm = toolPerm; + } + if (string.Equals(effectivePerm, "Deny", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + if (string.Equals(effectivePerm, "Auto", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + if (AskPermission != null) + { + return await AskPermission(toolName, filePath); + } + return false; + } +} diff --git a/.decompiledproj/AxCopilot/Services/Agent/AgentEvent.cs b/.decompiledproj/AxCopilot/Services/Agent/AgentEvent.cs new file mode 100644 index 0000000..e999074 --- /dev/null +++ b/.decompiledproj/AxCopilot/Services/Agent/AgentEvent.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace AxCopilot.Services.Agent; + +public class AgentEvent +{ + public DateTime Timestamp { get; init; } = DateTime.Now; + + public AgentEventType Type { get; init; } + + public string ToolName { get; init; } = ""; + + public string Summary { get; init; } = ""; + + public string? FilePath { get; init; } + + public bool Success { get; init; } = true; + + public int StepCurrent { get; init; } + + public int StepTotal { get; init; } + + public List? Steps { get; init; } + + public long ElapsedMs { get; init; } + + public int InputTokens { get; init; } + + public int OutputTokens { get; init; } + + public string? ToolInput { get; init; } + + public int Iteration { get; init; } +} diff --git a/.decompiledproj/AxCopilot/Services/Agent/AgentEventType.cs b/.decompiledproj/AxCopilot/Services/Agent/AgentEventType.cs new file mode 100644 index 0000000..c83a9be --- /dev/null +++ b/.decompiledproj/AxCopilot/Services/Agent/AgentEventType.cs @@ -0,0 +1,17 @@ +namespace AxCopilot.Services.Agent; + +public enum AgentEventType +{ + Thinking, + Planning, + StepStart, + StepDone, + ToolCall, + ToolResult, + SkillCall, + Error, + Complete, + Decision, + Paused, + Resumed +} diff --git a/.decompiledproj/AxCopilot/Services/Agent/AgentHookRunner.cs b/.decompiledproj/AxCopilot/Services/Agent/AgentHookRunner.cs new file mode 100644 index 0000000..fdb3c8f --- /dev/null +++ b/.decompiledproj/AxCopilot/Services/Agent/AgentHookRunner.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AxCopilot.Models; + +namespace AxCopilot.Services.Agent; + +public static class AgentHookRunner +{ + private const int MaxEnvValueLength = 4096; + + public static async Task> RunAsync(IReadOnlyList hooks, string toolName, string timing, string? toolInput = null, string? toolOutput = null, bool success = true, string? workFolder = null, int timeoutMs = 10000, CancellationToken ct = default(CancellationToken)) + { + List results = new List(); + if (hooks == null || hooks.Count == 0) + { + return results; + } + foreach (AgentHookEntry hook in hooks) + { + if (hook.Enabled && string.Equals(hook.Timing, timing, StringComparison.OrdinalIgnoreCase) && (!(hook.ToolName != "*") || string.Equals(hook.ToolName, toolName, StringComparison.OrdinalIgnoreCase))) + { + results.Add(await ExecuteHookAsync(hook, toolName, timing, toolInput, toolOutput, success, workFolder, timeoutMs, ct)); + } + } + return results; + } + + private static async Task ExecuteHookAsync(AgentHookEntry hook, string toolName, string timing, string? toolInput, string? toolOutput, bool success, string? workFolder, int timeoutMs, CancellationToken ct) + { + try + { + if (string.IsNullOrWhiteSpace(hook.ScriptPath)) + { + return new HookExecutionResult(hook.Name, Success: false, "스크립트 경로가 비어 있습니다."); + } + string scriptPath = Environment.ExpandEnvironmentVariables(hook.ScriptPath); + if (!File.Exists(scriptPath)) + { + return new HookExecutionResult(hook.Name, Success: false, "스크립트를 찾을 수 없습니다: " + scriptPath); + } + string ext = Path.GetExtension(scriptPath).ToLowerInvariant(); + string fileName; + string arguments; + switch (ext) + { + case ".ps1": + fileName = "powershell.exe"; + arguments = "-NoProfile -ExecutionPolicy Bypass -File \"" + scriptPath + "\""; + break; + case ".bat": + case ".cmd": + fileName = "cmd.exe"; + arguments = "/c \"" + scriptPath + "\""; + break; + default: + return new HookExecutionResult(hook.Name, Success: false, "지원하지 않는 스크립트 확장자: " + ext + " (.bat/.cmd/.ps1만 허용)"); + } + if (!string.IsNullOrWhiteSpace(hook.Arguments)) + { + arguments = arguments + " " + hook.Arguments; + } + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + WorkingDirectory = (workFolder ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)), + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + psi.EnvironmentVariables["AX_TOOL_NAME"] = toolName; + psi.EnvironmentVariables["AX_TOOL_TIMING"] = timing; + psi.EnvironmentVariables["AX_TOOL_INPUT"] = Truncate(toolInput, 4096); + psi.EnvironmentVariables["AX_WORK_FOLDER"] = workFolder ?? ""; + if (string.Equals(timing, "post", StringComparison.OrdinalIgnoreCase)) + { + psi.EnvironmentVariables["AX_TOOL_OUTPUT"] = Truncate(toolOutput, 4096); + psi.EnvironmentVariables["AX_TOOL_SUCCESS"] = (success ? "true" : "false"); + } + using Process process = new Process + { + StartInfo = psi + }; + StringBuilder stdOut = new StringBuilder(); + StringBuilder stdErr = new StringBuilder(); + process.OutputDataReceived += delegate(object _, DataReceivedEventArgs e) + { + if (e.Data != null) + { + stdOut.AppendLine(e.Data); + } + }; + process.ErrorDataReceived += delegate(object _, DataReceivedEventArgs e) + { + if (e.Data != null) + { + stdErr.AppendLine(e.Data); + } + }; + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + cts.CancelAfter(timeoutMs); + try + { + await process.WaitForExitAsync(cts.Token); + } + catch (OperationCanceledException) + { + try + { + process.Kill(entireProcessTree: true); + } + catch + { + } + return new HookExecutionResult(hook.Name, Success: false, $"타임아웃 ({timeoutMs}ms 초과)"); + } + int exitCode = process.ExitCode; + string output = stdOut.ToString().TrimEnd(); + string error = stdErr.ToString().TrimEnd(); + if (exitCode != 0) + { + return new HookExecutionResult(hook.Name, Success: false, $"종료 코드 {exitCode}: {(string.IsNullOrEmpty(error) ? output : error)}"); + } + return new HookExecutionResult(hook.Name, Success: true, string.IsNullOrEmpty(output) ? "(정상 완료)" : output); + } + catch (Exception ex2) + { + Exception ex3 = ex2; + return new HookExecutionResult(hook.Name, Success: false, "훅 실행 예외: " + ex3.Message); + } + } + + private static string Truncate(string? value, int maxLen) + { + return string.IsNullOrEmpty(value) ? "" : ((value.Length <= maxLen) ? value : value.Substring(0, maxLen)); + } +} diff --git a/.decompiledproj/AxCopilot/Services/Agent/AgentLoopService.cs b/.decompiledproj/AxCopilot/Services/Agent/AgentLoopService.cs new file mode 100644 index 0000000..e0f501e --- /dev/null +++ b/.decompiledproj/AxCopilot/Services/Agent/AgentLoopService.cs @@ -0,0 +1,1573 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using AxCopilot.Models; + +namespace AxCopilot.Services.Agent; + +public class AgentLoopService +{ + private class ParallelState + { + public int CurrentStep; + + public int TotalToolCalls; + + public int MaxIterations; + + public int ConsecutiveErrors; + + public int StatsSuccessCount; + + public int StatsFailCount; + + public int StatsInputTokens; + + public int StatsOutputTokens; + } + + private readonly LlmService _llm; + + private readonly ToolRegistry _tools; + + private readonly SettingsService _settings; + + private string _conversationId = ""; + + private bool _docFallbackAttempted; + + private readonly SemaphoreSlim _pauseSemaphore = new SemaphoreSlim(1, 1); + + private static readonly HashSet VerificationAllowedTools = new HashSet { "file_read", "directory_list" }; + + private static readonly HashSet ReadOnlyTools = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "file_read", "glob", "grep_tool", "folder_map", "document_read", "search_codebase", "code_search", "env_tool", "datetime_tool", "dev_env_detect", + "memory", "skill_manager", "json_tool", "regex_tool", "base64_tool", "hash_tool", "image_analyze" + }; + + public ObservableCollection Events { get; } = new ObservableCollection(); + + public bool IsRunning { get; private set; } + + public Action? Dispatcher { get; set; } + + public Func>? AskPermissionCallback { get; set; } + + public Func, string, Task>? UserAskCallback { get; set; } + + public string ActiveTab { get; set; } = "Chat"; + + public bool IsPaused { get; private set; } + + public Func, Task>? UserDecisionCallback { get; set; } + + public event Action? EventOccurred; + + public AgentLoopService(LlmService llm, ToolRegistry tools, SettingsService settings) + { + _llm = llm; + _tools = tools; + _settings = settings; + } + + public async Task PauseAsync() + { + if (!IsPaused && IsRunning) + { + await _pauseSemaphore.WaitAsync().ConfigureAwait(continueOnCapturedContext: false); + IsPaused = true; + EmitEvent(AgentEventType.Paused, "", "에이전트가 일시정지되었습니다", null, 0, 0, null, 0L); + } + } + + public void Resume() + { + if (IsPaused) + { + IsPaused = false; + try + { + _pauseSemaphore.Release(); + } + catch (SemaphoreFullException) + { + } + EmitEvent(AgentEventType.Resumed, "", "에이전트가 재개되었습니다", null, 0, 0, null, 0L); + } + } + + public async Task RunAsync(List messages, CancellationToken ct = default(CancellationToken)) + { + if (IsRunning) + { + throw new InvalidOperationException("에이전트가 이미 실행 중입니다."); + } + IsRunning = true; + _docFallbackAttempted = false; + LlmSettings llm = _settings.Settings.Llm; + int baseMax = ((llm.MaxAgentIterations > 0) ? llm.MaxAgentIterations : 25); + int maxIterations = baseMax; + int maxRetry = ((llm.MaxRetryOnError > 0) ? llm.MaxRetryOnError : 3); + int iteration = 0; + string userQuery = messages.LastOrDefault((ChatMessage m) => m.Role == "user")?.Content ?? ""; + int consecutiveErrors = 0; + int totalToolCalls = 0; + DateTime statsStart = DateTime.Now; + int statsSuccessCount = 0; + int statsFailCount = 0; + int statsInputTokens = 0; + int statsOutputTokens = 0; + List statsUsedTools = new List(); + List planSteps = new List(); + int currentStep = 0; + bool planExtracted = false; + int planExecutionRetry = 0; + bool documentPlanCalled = false; + int postDocumentPlanRetry = 0; + string documentPlanPath = null; + string documentPlanTitle = null; + string documentPlanScaffold = null; + string planMode = llm.PlanMode ?? "off"; + AgentContext context = BuildContext(); + try + { + if (planMode == "always") + { + iteration++; + EmitEvent(AgentEventType.Thinking, "", "실행 계획 생성 중...", null, 0, 0, null, 0L); + ChatMessage planInstruction = new ChatMessage + { + Role = "user", + Content = "[System] 도구를 호출하지 마세요. 먼저 실행 계획을 번호 매긴 단계로 작성하세요. 각 단계에 사용할 도구와 대상을 구체적으로 명시하세요. 계획만 제시하고 실행은 하지 마세요." + }; + messages.Add(planInstruction); + string planText; + try + { + planText = await _llm.SendAsync(messages, ct); + } + catch (Exception ex) + { + Exception ex2 = ex; + EmitEvent(AgentEventType.Error, "", "LLM 오류: " + ex2.Message, null, 0, 0, null, 0L); + return "⚠ LLM 오류: " + ex2.Message; + } + messages.Remove(planInstruction); + planSteps = TaskDecomposer.ExtractSteps(planText); + planExtracted = true; + if (planSteps.Count > 0) + { + EmitEvent(AgentEventType.Planning, "", $"작업 계획: {planSteps.Count}단계", null, 0, 0, planSteps, 0L); + if (UserDecisionCallback != null) + { + string decision = await UserDecisionCallback(planText, new List { "승인", "수정 요청", "취소" }); + if (decision == "취소") + { + EmitEvent(AgentEventType.Complete, "", "사용자가 작업을 취소했습니다", null, 0, 0, null, 0L); + return "작업이 취소되었습니다."; + } + if (decision != null && decision != "승인") + { + messages.Add(new ChatMessage + { + Role = "assistant", + Content = planText + }); + messages.Add(new ChatMessage + { + Role = "user", + Content = decision + "\n위 피드백을 반영하여 실행 계획을 다시 작성하세요." + }); + for (int retry = 0; retry < 3; retry++) + { + try + { + planText = await _llm.SendAsync(messages, ct); + } + catch + { + break; + } + planSteps = TaskDecomposer.ExtractSteps(planText); + if (planSteps.Count > 0) + { + EmitEvent(AgentEventType.Planning, "", $"수정된 계획: {planSteps.Count}단계", null, 0, 0, planSteps, 0L); + } + decision = await UserDecisionCallback(planText, new List { "승인", "수정 요청", "취소" }); + if (decision == "취소") + { + EmitEvent(AgentEventType.Complete, "", "사용자가 작업을 취소했습니다", null, 0, 0, null, 0L); + return "작업이 취소되었습니다."; + } + if (decision == null || decision == "승인") + { + break; + } + messages.Add(new ChatMessage + { + Role = "assistant", + Content = planText + }); + messages.Add(new ChatMessage + { + Role = "user", + Content = decision + "\n위 피드백을 반영하여 실행 계획을 다시 작성하세요." + }); + } + } + } + messages.Add(new ChatMessage + { + Role = "assistant", + Content = planText + }); + string planSectionsHint = ((planSteps.Count > 0) ? string.Join(", ", planSteps) : ""); + string sectionInstruction = ((!string.IsNullOrEmpty(planSectionsHint)) ? ("document_plan 도구를 호출할 때 sections_hint 파라미터에 위 계획의 섹션/단계를 그대로 넣으세요: \"" + planSectionsHint + "\"") : ""); + messages.Add(new ChatMessage + { + Role = "user", + Content = "계획이 승인되었습니다. 지금 즉시 1단계부터 도구(tool)를 호출하여 실행을 시작하세요. 텍스트로 설명하지 말고 반드시 도구를 호출하세요." + (string.IsNullOrEmpty(sectionInstruction) ? "" : ("\n" + sectionInstruction)) + }); + } + else if (!string.IsNullOrEmpty(planText)) + { + messages.Add(new ChatMessage + { + Role = "assistant", + Content = planText + }); + } + } + while (iteration < maxIterations && !ct.IsCancellationRequested) + { + iteration++; + await _pauseSemaphore.WaitAsync(ct).ConfigureAwait(continueOnCapturedContext: false); + try + { + _pauseSemaphore.Release(); + } + catch (SemaphoreFullException) + { + } + if (await ContextCondenser.CondenseIfNeededAsync(messages, _llm, llm.MaxContextTokens, ct)) + { + EmitEvent(AgentEventType.Thinking, "", "컨텍스트 압축 완료 — 입력 토큰을 절감했습니다", null, 0, 0, null, 0L); + } + EmitEvent(AgentEventType.Thinking, "", $"LLM에 요청 중... (반복 {iteration}/{maxIterations})", null, 0, 0, null, 0L); + if (llm.FreeTierMode && iteration > 1) + { + int delaySec = ((llm.FreeTierDelaySeconds > 0) ? llm.FreeTierDelaySeconds : 4); + EmitEvent(AgentEventType.Thinking, "", $"무료 티어 모드: {delaySec}초 대기 중...", null, 0, 0, null, 0L); + await Task.Delay(delaySec * 1000, ct); + } + List blocks; + try + { + IReadOnlyCollection activeTools = _tools.GetActiveTools(llm.DisabledTools); + blocks = await _llm.SendWithToolsAsync(messages, activeTools, ct); + } + catch (NotSupportedException) + { + return await _llm.SendAsync(messages, ct); + } + catch (ToolCallNotSupportedException ex5) + { + LogService.Warn("[AgentLoop] 도구 호출 거부됨, 일반 응답으로 폴백: " + ex5.Message); + EmitEvent(AgentEventType.Thinking, "", "도구 호출이 거부되어 일반 응답으로 전환합니다…", null, 0, 0, null, 0L); + if (documentPlanCalled && !string.IsNullOrEmpty(documentPlanScaffold) && !_docFallbackAttempted) + { + _docFallbackAttempted = true; + EmitEvent(AgentEventType.Thinking, "", "앱에서 직접 문서를 생성합니다...", null, 0, 0, null, 0L); + try + { + List bodyRequest = new List + { + new ChatMessage + { + Role = "user", + Content = $"아래 HTML 골격의 각 h2 섹션에 주석의 핵심 항목을 참고하여 풍부한 내용을 채워 완전한 HTML body를 출력하세요. 도구를 호출하지 말고 HTML 코드만 출력하세요.\n\n주제: {documentPlanTitle ?? userQuery}\n\n골격:\n{documentPlanScaffold}" + } + }; + string bodyText = await _llm.SendAsync(bodyRequest, ct); + if (!string.IsNullOrEmpty(bodyText)) + { + IAgentTool htmlTool = _tools.Get("html_create"); + if (htmlTool != null) + { + string fallbackPath = documentPlanPath; + if (string.IsNullOrEmpty(fallbackPath)) + { + string safe = ((userQuery.Length > 40) ? userQuery.Substring(0, 40) : userQuery); + char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); + foreach (char c in invalidFileNameChars) + { + safe = safe.Replace(c, '_'); + } + fallbackPath = safe.Trim() + ".html"; + } + JsonElement argsJson = JsonSerializer.SerializeToElement(new + { + path = fallbackPath, + title = (documentPlanTitle ?? userQuery), + body = bodyText, + toc = true, + numbered = true, + mood = "professional", + cover = new + { + title = (documentPlanTitle ?? userQuery), + author = "AX Copilot Agent" + } + }); + ToolResult htmlResult = await htmlTool.ExecuteAsync(argsJson, context, ct); + if (htmlResult.Success) + { + EmitEvent(AgentEventType.ToolResult, "html_create", "✅ 보고서 파일 생성: " + Path.GetFileName(htmlResult.FilePath ?? ""), htmlResult.FilePath, 0, 0, null, 0L); + EmitEvent(AgentEventType.Complete, "", "에이전트 작업 완료", null, 0, 0, null, 0L); + return htmlResult.Output; + } + } + } + } + catch (Exception ex) + { + Exception docEx = ex; + LogService.Warn("[AgentLoop] document_plan 직접 생성 실패: " + docEx.Message); + } + } + try + { + return await _llm.SendAsync(messages, ct); + } + catch (Exception ex6) + { + EmitEvent(AgentEventType.Error, "", "LLM 오류: " + ex6.Message, null, 0, 0, null, 0L); + return "⚠ LLM 오류 (도구 호출 실패 후 폴백도 실패): " + ex6.Message; + } + } + catch (Exception ex7) + { + EmitEvent(AgentEventType.Error, "", "LLM 오류: " + ex7.Message, null, 0, 0, null, 0L); + return "⚠ LLM 오류: " + ex7.Message; + } + List textParts = new List(); + List toolCalls = new List(); + foreach (LlmService.ContentBlock block in blocks) + { + if (block.Type == "text" && !string.IsNullOrWhiteSpace(block.Text)) + { + textParts.Add(block.Text); + } + else if (block.Type == "tool_use") + { + toolCalls.Add(block); + } + } + string textResponse = string.Join("\n", textParts); + if (!planExtracted && !string.IsNullOrEmpty(textResponse)) + { + planSteps = TaskDecomposer.ExtractSteps(textResponse); + planExtracted = true; + if (planSteps.Count > 0) + { + EmitEvent(AgentEventType.Planning, "", $"작업 계획: {planSteps.Count}단계", null, 0, 0, planSteps, 0L); + if (planMode == "auto" && toolCalls.Count == 0 && UserDecisionCallback != null) + { + string decision2 = await UserDecisionCallback(textResponse, new List { "승인", "수정 요청", "취소" }); + if (decision2 == "취소") + { + EmitEvent(AgentEventType.Complete, "", "사용자가 작업을 취소했습니다", null, 0, 0, null, 0L); + return "작업이 취소되었습니다."; + } + if (decision2 != null && decision2 != "승인") + { + messages.Add(new ChatMessage + { + Role = "user", + Content = decision2 + }); + EmitEvent(AgentEventType.Thinking, "", "사용자 피드백 반영 중...", null, 0, 0, null, 0L); + planExtracted = false; + continue; + } + } + } + } + if (!string.IsNullOrEmpty(textResponse) && toolCalls.Count > 0) + { + string thinkingSummary = ((textResponse.Length > 150) ? (textResponse.Substring(0, 150) + "…") : textResponse); + EmitEvent(AgentEventType.Thinking, "", thinkingSummary, null, 0, 0, null, 0L); + } + if (toolCalls.Count == 0) + { + if (planSteps.Count > 0 && totalToolCalls == 0 && planExecutionRetry < 2) + { + planExecutionRetry++; + if (!string.IsNullOrEmpty(textResponse)) + { + messages.Add(new ChatMessage + { + Role = "assistant", + Content = textResponse + }); + } + messages.Add(new ChatMessage + { + Role = "user", + Content = "도구를 호출하지 않았습니다. 계획 1단계를 지금 즉시 도구(tool call)로 실행하세요. 설명 없이 도구 호출만 하세요." + }); + EmitEvent(AgentEventType.Thinking, "", $"도구 미호출 감지 — 실행 재시도 {planExecutionRetry}/2...", null, 0, 0, null, 0L); + continue; + } + if (documentPlanCalled && postDocumentPlanRetry < 2) + { + postDocumentPlanRetry++; + if (!string.IsNullOrEmpty(textResponse)) + { + messages.Add(new ChatMessage + { + Role = "assistant", + Content = textResponse + }); + } + messages.Add(new ChatMessage + { + Role = "user", + Content = "html_create 도구를 호출하지 않았습니다. document_plan 결과의 body 골격을 바탕으로 각 섹션에 충분한 내용을 채워서 html_create 도구를 지금 즉시 호출하세요. 설명 없이 도구 호출만 하세요." + }); + EmitEvent(AgentEventType.Thinking, "", $"html_create 미호출 재시도 {postDocumentPlanRetry}/2...", null, 0, 0, null, 0L); + continue; + } + if (documentPlanCalled && !string.IsNullOrEmpty(documentPlanScaffold) && !_docFallbackAttempted) + { + _docFallbackAttempted = true; + EmitEvent(AgentEventType.Thinking, "", "LLM이 html_create를 호출하지 않아 앱에서 직접 문서를 생성합니다...", null, 0, 0, null, 0L); + try + { + List bodyRequest2 = new List + { + new ChatMessage + { + Role = "user", + Content = $"아래 HTML 골격의 각 h2 섹션에 주석()의 핵심 항목을 참고하여 풍부한 내용을 채워 완전한 HTML body를 출력하세요. 도구를 호출하지 말고 HTML 코드만 출력하세요.\n\n주제: {documentPlanTitle ?? userQuery}\n\n골격:\n{documentPlanScaffold}" + } + }; + string bodyText2 = await _llm.SendAsync(bodyRequest2, ct); + if (!string.IsNullOrEmpty(bodyText2)) + { + IAgentTool htmlTool2 = _tools.Get("html_create"); + if (htmlTool2 != null) + { + string fallbackPath2 = documentPlanPath; + if (string.IsNullOrEmpty(fallbackPath2)) + { + string safe2 = ((userQuery.Length > 40) ? userQuery.Substring(0, 40) : userQuery); + char[] invalidFileNameChars2 = Path.GetInvalidFileNameChars(); + foreach (char c2 in invalidFileNameChars2) + { + safe2 = safe2.Replace(c2, '_'); + } + fallbackPath2 = safe2.Trim() + ".html"; + } + JsonElement argsJson2 = JsonSerializer.SerializeToElement(new + { + path = fallbackPath2, + title = (documentPlanTitle ?? userQuery), + body = bodyText2, + toc = true, + numbered = true, + mood = "professional", + cover = new + { + title = (documentPlanTitle ?? userQuery), + author = "AX Copilot Agent" + } + }); + ToolResult htmlResult2 = await htmlTool2.ExecuteAsync(argsJson2, context, ct); + if (htmlResult2.Success) + { + EmitEvent(AgentEventType.ToolResult, "html_create", "✅ 보고서 파일 생성: " + Path.GetFileName(htmlResult2.FilePath ?? ""), htmlResult2.FilePath, 0, 0, null, 0L); + textResponse = htmlResult2.Output; + } + } + } + } + catch (Exception ex) + { + Exception ex8 = ex; + EmitEvent(AgentEventType.Thinking, "", "직접 생성 실패: " + ex8.Message, null, 0, 0, null, 0L); + } + } + if (!_docFallbackAttempted && totalToolCalls == 0 && !string.IsNullOrEmpty(textResponse) && IsDocumentCreationRequest(userQuery)) + { + _docFallbackAttempted = true; + string savedPath = AutoSaveAsHtml(textResponse, userQuery, context); + if (savedPath != null) + { + EmitEvent(AgentEventType.ToolResult, "html_create", "✅ 보고서 파일 자동 생성: " + Path.GetFileName(savedPath), savedPath, 0, 0, null, 0L); + textResponse = textResponse + "\n\n\ud83d\udcc4 파일이 저장되었습니다: " + savedPath; + } + } + if (!string.IsNullOrEmpty(textResponse)) + { + messages.Add(new ChatMessage + { + Role = "assistant", + Content = textResponse + }); + } + EmitEvent(AgentEventType.Complete, "", "에이전트 작업 완료", null, 0, 0, null, 0L); + return textResponse; + } + List contentBlocks = new List(); + if (!string.IsNullOrEmpty(textResponse)) + { + contentBlocks.Add(new + { + type = "text", + text = textResponse + }); + } + foreach (LlmService.ContentBlock tc in toolCalls) + { + contentBlocks.Add(new + { + type = "tool_use", + id = tc.ToolId, + name = tc.ToolName, + input = tc.ToolInput + }); + } + string assistantContent = JsonSerializer.Serialize(new + { + _tool_use_blocks = contentBlocks + }); + messages.Add(new ChatMessage + { + Role = "assistant", + Content = assistantContent + }); + if (llm.EnableParallelTools && toolCalls.Count > 1) + { + List parallelBatch; + List sequentialBatch; + (parallelBatch, sequentialBatch) = ClassifyToolCalls(toolCalls); + if (parallelBatch.Count > 1) + { + ParallelState pState = new ParallelState + { + CurrentStep = currentStep, + TotalToolCalls = totalToolCalls, + MaxIterations = maxIterations, + ConsecutiveErrors = consecutiveErrors, + StatsSuccessCount = statsSuccessCount, + StatsFailCount = statsFailCount, + StatsInputTokens = statsInputTokens, + StatsOutputTokens = statsOutputTokens + }; + await ExecuteToolsInParallelAsync(parallelBatch, messages, context, planSteps, pState, baseMax, maxRetry, llm, iteration, ct, statsUsedTools); + currentStep = pState.CurrentStep; + totalToolCalls = pState.TotalToolCalls; + maxIterations = pState.MaxIterations; + consecutiveErrors = pState.ConsecutiveErrors; + statsSuccessCount = pState.StatsSuccessCount; + statsFailCount = pState.StatsFailCount; + statsInputTokens = pState.StatsInputTokens; + statsOutputTokens = pState.StatsOutputTokens; + } + toolCalls = sequentialBatch; + if (toolCalls.Count == 0) + { + continue; + } + } + foreach (LlmService.ContentBlock call in toolCalls) + { + if (ct.IsCancellationRequested) + { + break; + } + IAgentTool tool = _tools.Get(call.ToolName); + if (tool == null) + { + string errResult = "알 수 없는 도구: " + call.ToolName; + EmitEvent(AgentEventType.Error, call.ToolName, errResult, null, 0, 0, null, 0L); + messages.Add(LlmService.CreateToolResultMessage(call.ToolId, call.ToolName, errResult)); + continue; + } + if (planSteps.Count > 0) + { + int newStep = TaskDecomposer.EstimateCurrentStep(toolSummary: FormatToolCallSummary(call), steps: planSteps, toolName: call.ToolName, lastStep: currentStep); + if (newStep != currentStep) + { + currentStep = newStep; + EmitEvent(AgentEventType.StepStart, "", planSteps[currentStep], null, currentStep + 1, planSteps.Count, null, 0L); + } + } + if (context.DevMode) + { + string paramJson = call.ToolInput?.ToString() ?? "{}"; + if (paramJson.Length > 500) + { + paramJson = paramJson.Substring(0, 500) + "..."; + } + EmitEvent(AgentEventType.Thinking, call.ToolName, "[DEV] 도구 호출: " + call.ToolName + "\n파라미터: " + paramJson, null, 0, 0, null, 0L); + } + EmitEvent(AgentEventType.ToolCall, call.ToolName, FormatToolCallSummary(call), null, 0, 0, null, 0L); + if (context.DevModeStepApproval && UserDecisionCallback != null) + { + string decision3 = await UserDecisionCallback("[DEV] 도구 '" + call.ToolName + "' 실행을 승인하시겠습니까?\n" + FormatToolCallSummary(call), new List { "승인", "건너뛰기", "중단" }); + if (decision3 == "중단") + { + EmitEvent(AgentEventType.Complete, "", "[DEV] 사용자가 실행을 중단했습니다", null, 0, 0, null, 0L); + return "사용자가 개발자 모드에서 실행을 중단했습니다."; + } + if (decision3 == "건너뛰기") + { + messages.Add(LlmService.CreateToolResultMessage(call.ToolId, call.ToolName, "[SKIPPED by developer] 사용자가 이 도구 실행을 건너뛰었습니다.")); + continue; + } + } + string decisionRequired = CheckDecisionRequired(call, context); + if (decisionRequired != null && UserDecisionCallback != null) + { + string decision4 = await UserDecisionCallback(decisionRequired, new List { "승인", "건너뛰기", "취소" }); + if (decision4 == "취소") + { + EmitEvent(AgentEventType.Complete, "", "사용자가 작업을 취소했습니다", null, 0, 0, null, 0L); + return "사용자가 작업을 취소했습니다."; + } + if (decision4 == "건너뛰기") + { + messages.Add(LlmService.CreateToolResultMessage(call.ToolId, call.ToolName, "[SKIPPED] 사용자가 이 작업을 건너뛰었습니다.")); + continue; + } + } + if (llm.EnableToolHooks && llm.AgentHooks.Count > 0) + { + try + { + foreach (HookExecutionResult pr in (await AgentHookRunner.RunAsync(llm.AgentHooks, call.ToolName, "pre", call.ToolInput.ToString(), null, success: true, context.WorkFolder, llm.ToolHookTimeoutMs, ct)).Where((HookExecutionResult r) => !r.Success)) + { + EmitEvent(AgentEventType.Error, call.ToolName, "[Hook:" + pr.HookName + "] " + pr.Output, null, 0, 0, null, 0L); + } + } + catch + { + } + } + Stopwatch sw = Stopwatch.StartNew(); + ToolResult result; + try + { + JsonElement input = call.ToolInput ?? JsonDocument.Parse("{}").RootElement; + result = await tool.ExecuteAsync(input, context, ct); + } + catch (OperationCanceledException) + { + EmitEvent(AgentEventType.Complete, "", "사용자가 작업을 취소했습니다.", null, 0, 0, null, 0L); + return "사용자가 작업을 취소했습니다."; + } + catch (Exception ex10) + { + result = ToolResult.Fail("도구 실행 오류: " + ex10.Message); + } + sw.Stop(); + if (llm.EnableToolHooks && llm.AgentHooks.Count > 0) + { + try + { + foreach (HookExecutionResult pr2 in (await AgentHookRunner.RunAsync(llm.AgentHooks, call.ToolName, "post", call.ToolInput.ToString(), TruncateOutput(result.Output, 2048), result.Success, context.WorkFolder, llm.ToolHookTimeoutMs, ct)).Where((HookExecutionResult r) => !r.Success)) + { + EmitEvent(AgentEventType.Error, call.ToolName, "[Hook:" + pr2.HookName + "] " + pr2.Output, null, 0, 0, null, 0L); + } + } + catch + { + } + } + if (context.DevMode) + { + EmitEvent(AgentEventType.Thinking, call.ToolName, "[DEV] 결과: " + (result.Success ? "성공" : "실패") + "\n" + TruncateOutput(result.Output, 500), null, 0, 0, null, 0L); + } + TokenUsage tokenUsage = _llm.LastTokenUsage; + EmitEvent(result.Success ? AgentEventType.ToolResult : AgentEventType.Error, call.ToolName, TruncateOutput(result.Output, 200), result.FilePath, 0, 0, null, sw.ElapsedMilliseconds, tokenUsage?.PromptTokens ?? 0, tokenUsage?.CompletionTokens ?? 0, call.ToolInput?.ToString(), iteration); + if (result.Success) + { + statsSuccessCount++; + } + else + { + statsFailCount++; + } + statsInputTokens += tokenUsage?.PromptTokens ?? 0; + statsOutputTokens += tokenUsage?.CompletionTokens ?? 0; + if (!statsUsedTools.Contains(call.ToolName)) + { + statsUsedTools.Add(call.ToolName); + } + if (llm.EnableAuditLog) + { + AuditLogService.LogToolCall(_conversationId, ActiveTab ?? "", call.ToolName, call.ToolInput.ToString() ?? "", TruncateOutput(result.Output, 500), result.FilePath, result.Success); + } + totalToolCalls++; + if (totalToolCalls > 15 && maxIterations < baseMax * 2) + { + maxIterations = Math.Min(baseMax * 2, 50); + } + if (call.ToolName == "test_loop" && result.Output.Contains("[AUTO_FIX:")) + { + int testFixMax = ((llm.MaxTestFixIterations > 0) ? llm.MaxTestFixIterations : 5); + int testFixBudget = baseMax + testFixMax * 3; + if (maxIterations < testFixBudget) + { + maxIterations = Math.Min(testFixBudget, 60); + } + } + await Task.Delay(80, ct); + if (!result.Success) + { + consecutiveErrors++; + if (consecutiveErrors <= maxRetry) + { + messages.Add(LlmService.CreateToolResultMessage(result: $"[Tool '{call.ToolName}' failed: {TruncateOutput(result.Output, 500)}]\nAnalyze why this failed. Consider: wrong parameters, wrong file path, missing prerequisites. Try a different approach. (Error {consecutiveErrors}/{maxRetry})", toolId: call.ToolId, toolName: call.ToolName)); + EmitEvent(AgentEventType.Thinking, "", $"Self-Reflection: 실패 분석 후 재시도 ({consecutiveErrors}/{maxRetry})", null, 0, 0, null, 0L); + continue; + } + messages.Add(LlmService.CreateToolResultMessage(call.ToolId, call.ToolName, $"[FAILED after {maxRetry} retries] {TruncateOutput(result.Output, 500)}\n" + "Stop retrying this tool. Explain the error to the user and suggest alternative approaches.")); + try + { + App app = Application.Current as App; + AgentMemoryService memSvc = app?.MemoryService; + if (memSvc != null && app?.SettingsService?.Settings.Llm.EnableAgentMemory == true) + { + memSvc.Add("correction", "도구 '" + call.ToolName + "' 반복 실패: " + TruncateOutput(result.Output, 200), "conv:" + _conversationId, context.WorkFolder); + } + } + catch + { + } + continue; + } + consecutiveErrors = 0; + messages.Add(LlmService.CreateToolResultMessage(call.ToolId, call.ToolName, TruncateOutput(result.Output, 4000))); + if (call.ToolName == "document_plan") + { + documentPlanCalled = true; + string po = result.Output; + Match pm = Regex.Match(po, "path:\\s*\"([^\"]+)\""); + if (pm.Success) + { + documentPlanPath = pm.Groups[1].Value; + } + Match tm = Regex.Match(po, "title:\\s*\"([^\"]+)\""); + if (tm.Success) + { + documentPlanTitle = tm.Groups[1].Value; + } + int bs = po.IndexOf("--- body 시작 ---", StringComparison.Ordinal); + int be = po.IndexOf("--- body 끝 ---", StringComparison.Ordinal); + if (bs >= 0 && be > bs) + { + int num3 = bs + "--- body 시작 ---".Length; + documentPlanScaffold = po.Substring(num3, be - num3).Trim(); + } + } + if (call.ToolName == "document_plan" && result.Output.Contains("즉시 실행:")) + { + string toolHint = (result.Output.Contains("html_create") ? "html_create" : (result.Output.Contains("document_assemble") ? "document_assemble" : (result.Output.Contains("file_write") ? "file_write" : "html_create"))); + messages.Add(new ChatMessage + { + Role = "user", + Content = $"document_plan이 완료되었습니다. 위 결과의 body/sections의 [내용...] 부분을 실제 상세 내용으로 모두 채워서 {toolHint} 도구를 지금 즉시 호출하세요. 각 섹션마다 반드시 충분한 내용을 작성하고, 설명 없이 도구를 바로 호출하세요." + }); + EmitEvent(AgentEventType.Thinking, "", "문서 개요 완성 — " + toolHint + " 호출 중...", null, 0, 0, null, 0L); + } + if (result.Success && IsTerminalDocumentTool(call.ToolName) && toolCalls.Count == 1) + { + if ((!(ActiveTab == "Code")) ? (llm.EnableCoworkVerification && IsDocumentCreationTool(call.ToolName)) : (llm.Code.EnableCodeVerification && IsCodeVerificationTarget(call.ToolName))) + { + await RunPostToolVerificationAsync(messages, call.ToolName, result, context, ct); + iteration++; + } + EmitEvent(AgentEventType.Complete, "", "에이전트 작업 완료", null, 0, 0, null, 0L); + return result.Output; + } + if (((!(ActiveTab == "Code")) ? (llm.EnableCoworkVerification && IsDocumentCreationTool(call.ToolName)) : (llm.Code.EnableCodeVerification && IsCodeVerificationTarget(call.ToolName))) && result.Success) + { + await RunPostToolVerificationAsync(messages, call.ToolName, result, context, ct); + iteration++; + } + } + } + if (iteration >= maxIterations) + { + EmitEvent(AgentEventType.Error, "", $"최대 반복 횟수 도달 ({maxIterations}회)", null, 0, 0, null, 0L); + return "⚠ 에이전트가 최대 반복 횟수에 도달했습니다."; + } + return "(취소됨)"; + } + finally + { + IsRunning = false; + if (IsPaused) + { + IsPaused = false; + try + { + _pauseSemaphore.Release(); + } + catch (SemaphoreFullException) + { + } + } + if (totalToolCalls > 0) + { + long durationMs = (long)(DateTime.Now - statsStart).TotalMilliseconds; + AgentStatsService.RecordSession(new AgentStatsService.AgentSessionRecord + { + Timestamp = statsStart, + Tab = (ActiveTab ?? ""), + Model = (_settings.Settings.Llm.Model ?? ""), + ToolCalls = totalToolCalls, + SuccessCount = statsSuccessCount, + FailCount = statsFailCount, + InputTokens = statsInputTokens, + OutputTokens = statsOutputTokens, + DurationMs = durationMs, + UsedTools = statsUsedTools + }); + if (llm.ShowTotalCallStats) + { + int totalTokens = statsInputTokens + statsOutputTokens; + double durationSec = (double)durationMs / 1000.0; + string toolList = string.Join(", ", statsUsedTools); + string summary = $"\ud83d\udcca 전체 통계: LLM {iteration}회 호출 | 도구 {totalToolCalls}회 (성공 {statsSuccessCount}, 실패 {statsFailCount}) | 토큰 {statsInputTokens:N0}→{statsOutputTokens:N0} (합계 {totalTokens:N0}) | 소요 {durationSec:F1}초 | 사용 도구: {toolList}"; + EmitEvent(AgentEventType.StepDone, "total_stats", summary, null, 0, 0, null, 0L); + } + } + } + } + + private string? AutoSaveAsHtml(string textContent, string userQuery, AgentContext context) + { + try + { + string text = ((userQuery.Length > 60) ? userQuery.Substring(0, 60) : userQuery); + string[] array = new string[14] + { + "작성해줘", "작성해 줘", "만들어줘", "만들어 줘", "써줘", "써 줘", "생성해줘", "생성해 줘", "작성해", "만들어", + "생성해", "해줘", "해 줘", "부탁해" + }; + string text2 = text; + string[] array2 = array; + foreach (string oldValue in array2) + { + text2 = text2.Replace(oldValue, "", StringComparison.OrdinalIgnoreCase); + } + char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); + foreach (char oldChar in invalidFileNameChars) + { + text2 = text2.Replace(oldChar, '_'); + } + text2 = text2.Trim().TrimEnd('.').Trim(); + string path = text2 + ".html"; + string text3 = FileReadTool.ResolvePath(path, context.WorkFolder); + if (context.ActiveTab == "Cowork") + { + text3 = AgentContext.EnsureTimestampedPath(text3); + } + string directoryName = Path.GetDirectoryName(text3); + if (!string.IsNullOrEmpty(directoryName)) + { + Directory.CreateDirectory(directoryName); + } + string css = TemplateService.GetCss("professional"); + string value = ConvertTextToHtml(textContent); + string contents = $"\n\n\n\n{EscapeHtml(text)}\n\n\n\n
\n

{EscapeHtml(text)}

\n
작성일: {DateTime.Now:yyyy-MM-dd} | AX Copilot 자동 생성
\n{value}\n
\n\n"; + File.WriteAllText(text3, contents, Encoding.UTF8); + LogService.Info("[AgentLoop] 문서 자동 저장 완료: " + text3); + return text3; + } + catch (Exception ex) + { + LogService.Warn("[AgentLoop] 문서 자동 저장 실패: " + ex.Message); + return null; + } + } + + private static string ConvertTextToHtml(string text) + { + StringBuilder stringBuilder = new StringBuilder(); + string[] array = text.Split('\n'); + bool flag = false; + string value = "ul"; + string[] array2 = array; + foreach (string text2 in array2) + { + string text3 = text2.TrimEnd(); + if (string.IsNullOrWhiteSpace(text3)) + { + if (flag) + { + StringBuilder stringBuilder2 = stringBuilder; + StringBuilder stringBuilder3 = stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(3, 1, stringBuilder2); + handler.AppendLiteral(""); + stringBuilder3.AppendLine(ref handler); + flag = false; + } + } + else if (text3.StartsWith("### ")) + { + StringBuilder stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler; + if (flag) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder4 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(3, 1, stringBuilder2); + handler.AppendLiteral(""); + stringBuilder4.AppendLine(ref handler); + flag = false; + } + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder5 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder2); + handler.AppendLiteral("

"); + string text4 = text3; + handler.AppendFormatted(EscapeHtml(text4.Substring(4, text4.Length - 4))); + handler.AppendLiteral("

"); + stringBuilder5.AppendLine(ref handler); + } + else if (text3.StartsWith("## ")) + { + StringBuilder stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler; + if (flag) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder6 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(3, 1, stringBuilder2); + handler.AppendLiteral(""); + stringBuilder6.AppendLine(ref handler); + flag = false; + } + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder7 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder2); + handler.AppendLiteral("

"); + string text4 = text3; + handler.AppendFormatted(EscapeHtml(text4.Substring(3, text4.Length - 3))); + handler.AppendLiteral("

"); + stringBuilder7.AppendLine(ref handler); + } + else if (text3.StartsWith("# ")) + { + StringBuilder stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler; + if (flag) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder8 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(3, 1, stringBuilder2); + handler.AppendLiteral(""); + stringBuilder8.AppendLine(ref handler); + flag = false; + } + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder9 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder2); + handler.AppendLiteral("

"); + string text4 = text3; + handler.AppendFormatted(EscapeHtml(text4.Substring(2, text4.Length - 2))); + handler.AppendLiteral("

"); + stringBuilder9.AppendLine(ref handler); + } + else if (Regex.IsMatch(text3, "^\\d+\\.\\s+\\S")) + { + string text5 = Regex.Replace(text3, "^\\d+\\.\\s+", ""); + if (text5.Length < 80 && !text5.Contains('.') && !text3.StartsWith(" ")) + { + StringBuilder stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler; + if (flag) + { + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder10 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(3, 1, stringBuilder2); + handler.AppendLiteral(""); + stringBuilder10.AppendLine(ref handler); + flag = false; + } + stringBuilder2 = stringBuilder; + StringBuilder stringBuilder11 = stringBuilder2; + handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder2); + handler.AppendLiteral("

"); + handler.AppendFormatted(EscapeHtml(text3)); + handler.AppendLiteral("

"); + stringBuilder11.AppendLine(ref handler); + } + else + { + if (!flag) + { + stringBuilder.AppendLine("
    "); + flag = true; + value = "ol"; + } + StringBuilder stringBuilder2 = stringBuilder; + StringBuilder stringBuilder12 = stringBuilder2; + StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder2); + handler.AppendLiteral("
  1. "); + handler.AppendFormatted(EscapeHtml(text5)); + handler.AppendLiteral("
  2. "); + stringBuilder12.AppendLine(ref handler); + } + } + else if (text3.TrimStart().StartsWith("- ") || text3.TrimStart().StartsWith("* ") || text3.TrimStart().StartsWith("• ")) + { + string text4 = text3.TrimStart(); + string text6 = text4.Substring(2, text4.Length - 2).Trim(); + if (!flag) + { + stringBuilder.AppendLine("