using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Threading; using AxCopilot.Models; using AxCopilot.Services; using AxCopilot.Services.Agent; namespace AxCopilot.Views; /// AX Agent 李? Claude Desktop ?ㅽ??????ъ씠?쒕컮 + 移댄뀒怨좊━ 遺꾨쪟 + ?€?꾨씪?? public partial class ChatWindow : Window { 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 ChatSessionStateService _sessionState; private readonly AppStateService _appState; private readonly object _convLock = new(); private ChatConversation? _currentConversation { get => _sessionState.CurrentConversation; set => _sessionState.CurrentConversation = value; } private CancellationTokenSource? _streamCts; private bool _isStreaming { get => _sessionState.IsStreaming; set => _sessionState.IsStreaming = value; } private bool _sidebarVisible = true; private string _selectedCategory = ""; // "" = ?꾩껜 private DraftQueueItem? _activeQueuedDraftItem; private Popup? _taskSummaryPopup; private string _taskSummaryFilter = "all"; private UIElement? _taskSummaryTarget; private bool _forceClose = false; // ??醫낅즺 ??吏꾩쭨 ?リ린 ?뚮옒洹? // ?ㅽ듃由щ컢 UI ??而ㅼ꽌 源쒕묀??+ 濡쒕뵫 ?꾩씠肄? private readonly DispatcherTimer _cursorTimer; private bool _cursorVisible = true; private TextBlock? _activeStreamText; private string _cachedStreamContent = ""; // sb.ToString() 罹먯떆 ??以묐났 ?몄텧 諛⑹? private TextBlock? _activeAiIcon; // 濡쒕뵫 ?꾩뒪 以묒씤 AI ?꾩씠肄? private bool _aiIconPulseStopped; // ?꾩뒪 1?뚮쭔 以묒? 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 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; } = ChatCategory.General; public string Symbol { get; init; } = "\uE8BD"; public string ColorHex { get; init; } = "#6B7280"; public string Tab { get; init; } = "Chat"; public DateTime UpdatedAt { get; init; } /// 泥??ъ슜??硫붿떆吏€ ?붿빟 (寃€?됱슜, 理쒕? 100??. public string Preview { get; init; } = ""; /// 遺꾧린 ?먮낯 ?€??ID. null?대㈃ ?먮낯 ?€?? public string? ParentId { get; init; } } public ChatWindow(SettingsService settings) { InitializeComponent(); _settings = settings; var app = System.Windows.Application.Current as App; _sessionState = app?.ChatSessionState ?? new ChatSessionStateService(); _appState = app?.AppState ?? new AppStateService(); _sessionState.Load(_settings); _appState.AttachChatSession(_sessionState); _appState.LoadFromSettings(_settings); _storage = new ChatStorageService(); _llm = new LlmService(settings); _router = new ModelRouterService(settings); _toolRegistry = ToolRegistry.CreateDefault(); _appState.RefreshAgentCatalog(_toolRegistry); _agentLoop = new AgentLoopService(_llm, _toolRegistry, settings) { Dispatcher = action => System.Windows.Application.Current.Dispatcher.Invoke(action), AskPermissionCallback = async (toolName, filePath) => { var result = MessageBoxResult.None; await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => { result = CustomMessageBox.Show( $"?꾧뎄 '{toolName}'??媛€) ?ㅼ쓬 ?뚯씪???묎렐?섎젮 ?⑸땲??\n\n{filePath}\n\n?덉슜?섏떆寃좎뒿?덇퉴?", "AX Agent ??沅뚰븳 ?뺤씤", MessageBoxButton.YesNo, MessageBoxImage.Question); }); return result == MessageBoxResult.Yes; }, UserAskCallback = async (question, options, defaultValue) => { string? response = null; await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => { response = UserAskDialog.Show(question, options, defaultValue); }); return response; }, }; // ?ㅼ젙?먯꽌 珥덇린媛?濡쒕뱶 (Loaded ?꾩뿉??null 諛⑹?) _selectedMood = settings.Settings.Llm.DefaultMood ?? "modern"; _folderDataUsage = settings.Settings.Llm.FolderDataUsage ?? "active"; _cursorTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(530) }; _cursorTimer.Tick += CursorTimer_Tick; _elapsedTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; _elapsedTimer.Tick += ElapsedTimer_Tick; _typingTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(12) }; _typingTimer.Tick += TypingTimer_Tick; SubAgentTool.StatusChanged += OnSubAgentStatusChanged; _appState.StateChanged += OnAppStateChanged; KeyDown += ChatWindow_KeyDown; Loaded += (_, _) => { // ?€?€ 利됱떆 ?꾩슂??UI 珥덇린?붾쭔 ?숆린 ?ㅽ뻾 ?€?€ SetupUserInfo(); ApplySessionTabSelection(); ApplyRuntimeStateToUi(); _selectedMood = _settings.Settings.Llm.DefaultMood ?? "modern"; _folderDataUsage = _settings.Settings.Llm.FolderDataUsage ?? "active"; UpdateAnalyzerButtonVisibility(); UpdateModelLabel(); InputBox.Focus(); MessageScroll.ScrollChanged += MessageScroll_ScrollChanged; // ?€?€ 臾닿굅???묒뾽?€ ?좏쑕 ?쒖젏??鍮꾨룞湲??ㅽ뻾 ?€?€ Dispatcher.BeginInvoke(() => { TemplateService.LoadCustomMoods(_settings.Settings.Llm.CustomMoods); BuildTopicButtons(); RestoreLastConversations(); RefreshConversationList(); // ?곗씠???뺣━ (?붿뒪??I/O) _ = Task.Run(() => { var retention = _settings.Settings.Llm.RetentionDays; if (retention > 0) _storage.PurgeExpired(retention); _storage.PurgeForDiskSpace(); }); }, System.Windows.Threading.DispatcherPriority.ApplicationIdle); // ?낅젰 諛??ъ빱??湲€濡쒖슦 ?④낵 var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue; var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; InputBox.GotFocus += (_, _) => InputBorder.BorderBrush = accentBrush; InputBox.LostFocus += (_, _) => InputBorder.BorderBrush = borderBrush; // ?쒕옒洹????쒕∼ ?뚯씪 泥⑤? + AI ?≪뀡 ?앹뾽 InputBorder.AllowDrop = true; InputBorder.DragOver += (_, de) => { de.Effects = de.Data.GetDataPresent(DataFormats.FileDrop) ? DragDropEffects.Copy : DragDropEffects.None; de.Handled = true; }; InputBorder.Drop += (_, de) => { if (de.Data.GetData(DataFormats.FileDrop) is string[] files && files.Length > 0) { var enableAi = _settings.Settings.Llm.EnableDragDropAiActions; if (enableAi && files.Length <= 5) ShowDropActionMenu(files); else foreach (var f in files) AddAttachedFile(f); } }; // ?ㅽ궗 ?쒖뒪??珥덇린?? if (_settings.Settings.Llm.EnableSkillSystem) { SkillService.EnsureSkillFolder(); SkillService.LoadSkills(_settings.Settings.Llm.SkillsFolderPath); _appState.RefreshSkillCatalog(SkillService.Skills, _settings.Settings.Llm.SkillsFolderPath, true); } else { _appState.RefreshSkillCatalog([], _settings.Settings.Llm.SkillsFolderPath, false); } _ = InitializeDynamicAgentSourcesAsync(); // ?щ옒???앹뾽 ?ㅻ퉬寃뚯씠??踰꾪듉 SlashNavUp.MouseLeftButtonUp += (_, _) => { _slashPageOffset = Math.Max(0, _slashPageOffset - SlashPageSize); RenderSlashPage(); }; SlashNavDown.MouseLeftButtonUp += (_, _) => { _slashPageOffset = Math.Min(_slashAllMatches.Count - 1, _slashPageOffset + SlashPageSize); RenderSlashPage(); }; // ?щ옒??紐낅졊??移??リ린 (횞 踰꾪듉) SlashChipClose.MouseLeftButtonUp += (_, _) => { HideSlashChip(restoreText: true); InputBox.Focus(); }; // InputBox?먯꽌 ?щ옒???앹뾽 ?대┛ ?곹깭濡?留덉슦???????앹뾽 ?ㅽ겕濡? InputBox.PreviewMouseWheel += (_, me) => { if (!SlashPopup.IsOpen) return; me.Handled = true; SlashPopup_ScrollByDelta(me.Delta); }; // ??UI 珥덇린 ?곹깭 UpdateFolderBar(); // ?몃쾭 ?좊땲硫붿씠?????낅┰ 怨듦컙???덈뒗 踰꾪듉?먮쭔 Scale ?곸슜 // (GhostBtn ?ㅽ???踰꾪듉?€ XAML?먯꽌 諛곌꼍??opacity ?몃쾭 泥섎━) ApplyHoverBounceAnimation(BtnModelSelector); ApplyHoverBounceAnimation(BtnTemplateSelector, -1.5); ApplyHoverScaleAnimation(BtnSend, 1.12); ApplyHoverScaleAnimation(BtnStop, 1.12); }; Closed += (_, _) => { SubAgentTool.StatusChanged -= OnSubAgentStatusChanged; _appState.StateChanged -= OnAppStateChanged; if (_taskSummaryPopup != null) _taskSummaryPopup.IsOpen = false; _streamCts?.Cancel(); _cursorTimer.Stop(); _elapsedTimer.Stop(); _typingTimer.Stop(); _llm.Dispose(); }; } /// /// X 踰꾪듉?쇰줈 ?レ쓣 ??李쎌쓣 ?④린湲곕쭔 ?⑸땲??(?ъ궗?⑹쑝濡??ㅼ쓬 踰?鍮좊Ⅴ寃??대┝). /// ??醫낅즺 ?쒖뿉??ForceClose()瑜??ъ슜?⑸땲?? /// protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { if (!_forceClose) { e.Cancel = true; Hide(); return; } base.OnClosing(e); } /// ??醫낅즺 ??李쎌쓣 ?ㅼ젣濡??レ뒿?덈떎. public void ForceClose() { // ?꾩옱 ?€???€??+ ??퀎 留덉?留??€??ID瑜??ㅼ젙???곸냽 ?€?? lock (_convLock) { if (_currentConversation != null && _currentConversation.Messages.Count > 0) { _sessionState.RememberConversation(_activeTab, _currentConversation.Id); try { _storage.Save(_currentConversation); } catch { } } } SaveLastConversations(); _forceClose = true; Close(); } // ?€?€?€ ?ъ슜???뺣낫 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void SetupUserInfo() { var userName = Environment.UserName; // AD\, AD/, AD: ?묐몢???쒓굅 var cleanName = userName; foreach (var sep in new[] { '\\', '/', ':' }) { var idx = cleanName.LastIndexOf(sep); if (idx >= 0) cleanName = cleanName[(idx + 1)..]; } var initial = cleanName.Length > 0 ? cleanName[..1].ToUpper() : "U"; var pcName = Environment.MachineName; UserInitialSidebar.Text = initial; UserInitialIconBar.Text = initial; UserNameText.Text = cleanName; UserPcText.Text = pcName; BtnUserIconBar.ToolTip = $"{cleanName} ({pcName})"; } // ?€?€?€ ?ㅽ겕濡??숈옉 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void MessageScroll_ScrollChanged(object sender, ScrollChangedEventArgs e) { // ?ㅽ겕濡?媛€???곸뿭???놁쑝硫?肄섑뀗痢좉? 吏㏃쓬) ??긽 諛붾떏 if (MessageScroll.ScrollableHeight <= 1) { _userScrolled = false; return; } // 肄섑뀗痢??ш린 蹂€寃?ExtentHeightChange > 0)???섑븳 ?ㅽ겕濡ㅼ? 臾댁떆 ???ъ슜??議곗옉留?媛먯? if (Math.Abs(e.ExtentHeightChange) > 0.5) return; var atBottom = MessageScroll.VerticalOffset >= MessageScroll.ScrollableHeight - 40; _userScrolled = !atBottom; } private void AutoScrollIfNeeded() { if (!_userScrolled) SmoothScrollToEnd(); } /// ???묐떟 ?쒖옉 ??媛뺤젣濡??섎떒 ?ㅽ겕濡ㅽ빀?덈떎 (?ъ슜???ㅽ겕濡??곹깭 由ъ뀑). private void ForceScrollToEnd() { _userScrolled = false; Dispatcher.InvokeAsync(() => SmoothScrollToEnd(), DispatcherPriority.Background); } /// 遺€?쒕윭???먮룞 ?ㅽ겕濡????섎떒?쇰줈 遺€?쒕읇寃??대룞?⑸땲?? private void SmoothScrollToEnd() { var targetOffset = MessageScroll.ScrollableHeight; var currentOffset = MessageScroll.VerticalOffset; var diff = targetOffset - currentOffset; // 李⑥씠媛€ ?묒쑝硫?利됱떆 ?대룞 (源쒕묀??諛⑹?) if (diff <= 60) { MessageScroll.ScrollToEnd(); return; } // 遺€?쒕읇寃??ㅽ겕濡?(DoubleAnimation) var animation = new DoubleAnimation { From = currentOffset, To = targetOffset, Duration = TimeSpan.FromMilliseconds(200), EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }, }; animation.Completed += (_, _) => MessageScroll.ScrollToVerticalOffset(targetOffset); // ScrollViewer??吏곸젒 ?좊땲硫붿씠?섏쓣 ?곸슜?????놁쑝誘€濡??€?대㉧ 湲곕컲?쇰줈 蹂닿컙 var startTime = DateTime.UtcNow; var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(16) }; // ~60fps EventHandler tickHandler = null!; tickHandler = (_, _) => { var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds; var progress = Math.Min(elapsed / 200.0, 1.0); var eased = 1.0 - Math.Pow(1.0 - progress, 3); var offset = currentOffset + diff * eased; MessageScroll.ScrollToVerticalOffset(offset); if (progress >= 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, KeyEventArgs e) { if (e.Key == Key.Enter) { CommitTitleEdit(); e.Handled = true; } if (e.Key == Key.Escape) { CancelTitleEdit(); e.Handled = true; } } private void CommitTitleEdit() { var newTitle = ChatTitleEdit.Text.Trim(); ChatTitleEdit.Visibility = Visibility.Collapsed; ChatTitle.Visibility = Visibility.Visible; if (string.IsNullOrEmpty(newTitle)) return; lock (_convLock) { if (_currentConversation == null) return; _currentConversation.Title = newTitle; } ChatTitle.Text = newTitle; try { ChatConversation conv; lock (_convLock) conv = _currentConversation!; _storage.Save(conv); } catch { } RefreshConversationList(); } private void CancelTitleEdit() { ChatTitleEdit.Visibility = Visibility.Collapsed; ChatTitle.Visibility = Visibility.Visible; } // ?€?€?€ 移댄뀒怨좊━ ?쒕∼?ㅼ슫 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void BtnCategoryDrop_Click(object sender, RoutedEventArgs e) { var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E)); var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(30, 255, 255, 255)); var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var popup = new Popup { StaysOpen = false, AllowsTransparency = true, PopupAnimation = PopupAnimation.Fade, PlacementTarget = BtnCategoryDrop, Placement = PlacementMode.Bottom, HorizontalOffset = 0, VerticalOffset = 4, }; var container = new Border { Background = bgBrush, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(12), Padding = new Thickness(6), MinWidth = 180, Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3, Color = Colors.Black }, }; var stack = new StackPanel(); Border CreateCatItem(string icon, string text, Brush iconColor, bool isSelected, Action onClick) { var item = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 7, 10, 7), Margin = new Thickness(0, 1, 0, 1), Cursor = Cursors.Hand, }; var g = new Grid(); g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(24) }); g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(20) }); var iconTb = new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = iconColor, VerticalAlignment = VerticalAlignment.Center, }; Grid.SetColumn(iconTb, 0); g.Children.Add(iconTb); var textTb = new TextBlock { Text = text, FontSize = 12.5, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, }; Grid.SetColumn(textTb, 1); g.Children.Add(textTb); if (isSelected) { var check = CreateSimpleCheck(accentBrush, 14); Grid.SetColumn(check, 2); g.Children.Add(check); } item.Child = g; item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; }; item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; item.MouseLeftButtonUp += (_, _) => { popup.IsOpen = false; onClick(); }; return item; } Border CreateSep() => new() { Height = 1, Background = borderBrush, Opacity = 0.3, Margin = new Thickness(8, 4, 8, 4), }; // ?꾩껜 蹂닿린 var allLabel = _activeTab switch { "Cowork" => "紐⑤뱺 ?묒뾽", "Code" => "紐⑤뱺 ?묒뾽", _ => "紐⑤뱺 二쇱젣", }; stack.Children.Add(CreateCatItem("\uE8BD", allLabel, secondaryText, string.IsNullOrEmpty(_selectedCategory), () => { _selectedCategory = ""; UpdateCategoryLabel(); RefreshConversationList(); })); stack.Children.Add(CreateSep()); if (_activeTab == "Cowork" || _activeTab == "Code") { // 肄붿썙??肄붾뱶: ?꾨━??移댄뀒怨좊━ 湲곕컲 ?꾪꽣 var presets = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets); var seen = new HashSet(); foreach (var p in presets) { if (p.IsCustom) continue; // 而ㅼ뒪?€?€ 蹂꾨룄 洹몃9 if (!seen.Add(p.Category)) continue; var capturedCat = p.Category; stack.Children.Add(CreateCatItem(p.Symbol, p.Label, BrushFromHex(p.Color), _selectedCategory == capturedCat, () => { _selectedCategory = capturedCat; UpdateCategoryLabel(); RefreshConversationList(); })); } // 而ㅼ뒪?€ ?꾨━???듯빀 ?꾪꽣 if (presets.Any(p => p.IsCustom)) { stack.Children.Add(CreateSep()); stack.Children.Add(CreateCatItem("\uE710", "而ㅼ뒪?€ ?꾨━??, secondaryText, _selectedCategory == "__custom__", () => { _selectedCategory = "__custom__"; UpdateCategoryLabel(); RefreshConversationList(); })); } } else { // Chat: 湲곗〈 ChatCategory 湲곕컲 foreach (var (key, label, symbol, color) in ChatCategory.All) { var capturedKey = key; stack.Children.Add(CreateCatItem(symbol, label, BrushFromHex(color), _selectedCategory == capturedKey, () => { _selectedCategory = capturedKey; UpdateCategoryLabel(); RefreshConversationList(); })); } // 而ㅼ뒪?€ ?꾨━???듯빀 ?꾪꽣 (Chat) var chatCustom = _settings.Settings.Llm.CustomPresets.Where(c => c.Tab == "Chat").ToList(); if (chatCustom.Count > 0) { stack.Children.Add(CreateSep()); stack.Children.Add(CreateCatItem("\uE710", "而ㅼ뒪?€ ?꾨━??, secondaryText, _selectedCategory == "__custom__", () => { _selectedCategory = "__custom__"; UpdateCategoryLabel(); RefreshConversationList(); })); } } container.Child = stack; popup.Child = container; popup.IsOpen = true; } private void UpdateCategoryLabel() { if (string.IsNullOrEmpty(_selectedCategory)) { CategoryLabel.Text = _activeTab switch { "Cowork" or "Code" => "紐⑤뱺 ?묒뾽", _ => "紐⑤뱺 二쇱젣" }; CategoryIcon.Text = "\uE8BD"; } else if (_selectedCategory == "__custom__") { CategoryLabel.Text = "而ㅼ뒪?€ ?꾨━??; CategoryIcon.Text = "\uE710"; } else { // ChatCategory?먯꽌 李얘린 foreach (var (key, label, symbol, _) in ChatCategory.All) { if (key == _selectedCategory) { CategoryLabel.Text = label; CategoryIcon.Text = symbol; return; } } // ?꾨━??移댄뀒怨좊━?먯꽌 李얘린 (Cowork/Code) var presets = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets); var match = presets.FirstOrDefault(p => p.Category == _selectedCategory); if (match != null) { CategoryLabel.Text = match.Label; CategoryIcon.Text = match.Symbol; } else { CategoryLabel.Text = _selectedCategory; CategoryIcon.Text = "\uE8BD"; } } } // ?€?€?€ 李?而⑦듃濡??€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ // WindowChrome??CaptionHeight媛€ ?쒕옒洹몃? 泥섎━?섎?濡?蹂꾨룄 ?몃뱾??遺덊븘?? protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); var source = System.Windows.Interop.HwndSource.FromHwnd( new System.Windows.Interop.WindowInteropHelper(this).Handle); source?.AddHook(WndProc); } private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { // WM_GETMINMAXINFO ??理쒕??????묒뾽 ?쒖떆以??곸뿭 ?뺣낫 if (msg == 0x0024) { var screen = System.Windows.Forms.Screen.FromHandle(hwnd); var workArea = screen.WorkingArea; var monitor = screen.Bounds; var source = System.Windows.Interop.HwndSource.FromHwnd(hwnd); var dpiScale = source?.CompositionTarget?.TransformToDevice.M11 ?? 1.0; // MINMAXINFO: ptReserved(0,4) ptMaxSize(8,12) ptMaxPosition(16,20) ptMinTrackSize(24,28) ptMaxTrackSize(32,36) System.Runtime.InteropServices.Marshal.WriteInt32(lParam, 8, workArea.Width); // ptMaxSize.cx System.Runtime.InteropServices.Marshal.WriteInt32(lParam, 12, workArea.Height); // ptMaxSize.cy System.Runtime.InteropServices.Marshal.WriteInt32(lParam, 16, workArea.Left - monitor.Left); // ptMaxPosition.x System.Runtime.InteropServices.Marshal.WriteInt32(lParam, 20, workArea.Top - monitor.Top); // ptMaxPosition.y handled = true; } return IntPtr.Zero; } private void BtnMinimize_Click(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized; private void BtnMaximize_Click(object sender, RoutedEventArgs e) { WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; MaximizeIcon.Text = WindowState == WindowState.Maximized ? "\uE923" : "\uE739"; // 蹂듭썝/理쒕????꾩씠肄? } private void BtnClose_Click(object sender, RoutedEventArgs e) => Close(); // ?€?€?€ ???꾪솚 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private string _activeTab { get => _sessionState.ActiveTab; set => _sessionState.ActiveTab = value; } private void SaveCurrentTabConversationId() { lock (_convLock) { if (_currentConversation != null && _currentConversation.Messages.Count > 0) { _sessionState.RememberConversation(_activeTab, _currentConversation.Id); // ???꾪솚 ???꾩옱 ?€?붾? 利됱떆 ?€??(?ㅽ듃由щ컢 以묒씠?대룄 吏꾪뻾 以묒씤 ?댁슜 蹂댁〈) try { _storage.Save(_currentConversation); } catch { } } } // ??퀎 留덉?留??€??ID瑜??ㅼ젙???곸냽 ?€??(???ъ떆????蹂듭썝?? SaveLastConversations(); } /// ???꾪솚 ???ㅽ듃由щ컢 以묒씠硫?利됱떆 以묐떒?⑸땲?? private void StopStreamingIfActive() { if (!_isStreaming) return; // ?ㅽ듃由щ컢 以묐떒 _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; _sessionState.ClearRuntimeActivities("?묒뾽 以묐떒"); _appState.ClearTasksByPrefix("agent:", "?묒뾽 以묐떒", "cancelled"); _appState.ClearTasksByPrefix("tool:", "?묒뾽 以묐떒", "cancelled"); SetStatusIdle(); } private void TabChat_Checked(object sender, RoutedEventArgs e) { if (_activeTab == "Chat") return; StopStreamingIfActive(); SaveCurrentTabConversationId(); _activeTab = "Chat"; _selectedCategory = ""; UpdateCategoryLabel(); UpdateTabUI(); } private void TabCowork_Checked(object sender, RoutedEventArgs e) { if (_activeTab == "Cowork") return; StopStreamingIfActive(); SaveCurrentTabConversationId(); _activeTab = "Cowork"; _selectedCategory = ""; UpdateCategoryLabel(); UpdateTabUI(); } private void TabCode_Checked(object sender, RoutedEventArgs e) { if (_activeTab == "Code") return; StopStreamingIfActive(); SaveCurrentTabConversationId(); _activeTab = "Code"; _selectedCategory = ""; UpdateCategoryLabel(); UpdateTabUI(); } private void ApplySessionTabSelection() { switch (_activeTab) { case "Cowork": if (TabCowork != null) TabCowork.IsChecked = true; break; case "Code": if (TabCode != null && TabCode.IsEnabled) TabCode.IsChecked = true; else if (TabChat != null) TabChat.IsChecked = true; break; default: if (TabChat != null) TabChat.IsChecked = true; break; } } private void ApplyRuntimeStateToUi() { if (StatusLabel != null) StatusLabel.Text = string.IsNullOrWhiteSpace(_sessionState.StatusText) ? "대기 중" : _sessionState.StatusText; if (_sessionState.IsStatusSpinning) StartStatusAnimation(); else StopStatusAnimation(); UpdateRuntimeActivityIndicators(); } UpdateRuntimeActivityIndicators(); } private void UpdateRuntimeActivityIndicators() { if (RuntimeActivityBadge != null && RuntimeActivityLabel != null) { var count = _appState.ActiveTasks.Count; RuntimeActivityBadge.Visibility = count > 0 ? Visibility.Visible : Visibility.Collapsed; RuntimeActivityLabel.Text = $"?ㅽ뻾 以?{count}"; var tooltipLines = _appState.ActiveTasks .OrderBy(task => task.StartedAt) .Select(task => string.IsNullOrWhiteSpace(task.Summary) ? task.Title : $"{task.Title}: {task.Summary}") .Where(line => !string.IsNullOrWhiteSpace(line)) .ToList(); RuntimeActivityBadge.ToolTip = tooltipLines.Count > 0 ? string.Join(Environment.NewLine, tooltipLines) : null; } if (LastCompletedLabel != null) { var recentTask = _appState.RecentTasks.FirstOrDefault(); var summary = recentTask?.Summary; LastCompletedLabel.Text = string.IsNullOrWhiteSpace(summary) ? "" : $"留덉?留?{GetTaskStatusLabel(recentTask?.Status)}: {TruncateForStatus(summary, 48)}"; LastCompletedLabel.Visibility = string.IsNullOrWhiteSpace(summary) ? Visibility.Collapsed : Visibility.Visible; LastCompletedLabel.ToolTip = string.IsNullOrWhiteSpace(summary) ? null : $"{recentTask?.Title}{Environment.NewLine}{summary}"; } } private static string GetTaskStatusLabel(string? status) => status switch { "failed" => "?ㅽ뙣", "cancelled" => "以묐떒", _ => "?꾨즺", }; private static string GetAgentRunStatusLabel(string? status) => status switch { "failed" => "?ㅽ뙣", "completed" => "?꾨즺", "paused" => "?쇱떆以묒?", _ => "吏꾪뻾 以?, }; private static string GetAgentTaskKey(AgentEvent evt) => string.IsNullOrWhiteSpace(evt.RunId) ? "agent:main" : $"agent:{evt.RunId}"; private static string GetToolTaskKey(AgentEvent evt) { var suffix = string.IsNullOrWhiteSpace(evt.ToolName) ? "unknown" : evt.ToolName; return string.IsNullOrWhiteSpace(evt.RunId) ? $"tool:{suffix}" : $"tool:{evt.RunId}:{suffix}"; } private void RuntimeTaskSummary_Click(object sender, MouseButtonEventArgs e) { e.Handled = true; _taskSummaryTarget = sender as UIElement ?? RuntimeActivityBadge; ShowTaskSummaryPopup(_taskSummaryTarget); } private void ShowTaskSummaryPopup(UIElement? target) { if (target == null) return; _taskSummaryTarget = target; if (_taskSummaryPopup != null) _taskSummaryPopup.IsOpen = false; var bg = TryFindResource("LauncherBackground") as Brush ?? Brushes.White; var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x10, 0, 0, 0)); var panel = new StackPanel { Margin = new Thickness(2) }; panel.Children.Add(new TextBlock { Text = "?묒뾽 ?곹깭", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = primaryText, Margin = new Thickness(10, 8, 10, 2), }); panel.Children.Add(new TextBlock { Text = $"?쒖꽦 {_appState.ActiveTasks.Count}媛?쨌 理쒓렐 {_appState.RecentTasks.Count}媛?, FontSize = 11, Foreground = secondaryText, Margin = new Thickness(10, 0, 10, 8), }); if (!string.IsNullOrWhiteSpace(_appState.AgentRun.RunId)) panel.Children.Add(BuildAgentRunSummaryCard(primaryText, secondaryText)); var filterRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(8, 0, 8, 8), }; filterRow.Children.Add(CreateTaskFilterChip("?꾩껜", "all")); filterRow.Children.Add(CreateTaskFilterChip("?먯씠?꾪듃", "agent")); filterRow.Children.Add(CreateTaskFilterChip("?꾧뎄", "tool")); filterRow.Children.Add(CreateTaskFilterChip("?쒕툕", "subagent")); filterRow.Children.Add(CreateTaskFilterChip("?ㅽ뙣", "failed")); panel.Children.Add(filterRow); AddTaskSummarySection(panel, "?꾩옱 ?ㅽ뻾 以?, FilterTaskRuns(_appState.ActiveTasks).OrderBy(t => t.StartedAt).Take(8), hoverBg, primaryText, secondaryText); AddTaskSummarySection(panel, "理쒓렐 ?묒뾽", FilterTaskRuns(_appState.RecentTasks).Take(8), hoverBg, primaryText, secondaryText); var container = new Border { Background = bg, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(12), Padding = new Thickness(6), Child = new ScrollViewer { Content = panel, MaxHeight = 360, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, }, Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 20, ShadowDepth = 4, Opacity = 0.18, }, }; _taskSummaryPopup = new Popup { PlacementTarget = target, Placement = PlacementMode.Top, AllowsTransparency = true, StaysOpen = false, PopupAnimation = PopupAnimation.Fade, Child = container, }; Dispatcher.BeginInvoke(() => _taskSummaryPopup.IsOpen = true, DispatcherPriority.Input); } private Border BuildAgentRunSummaryCard(Brush primaryText, Brush secondaryText) { var statusChip = CreateMetaChip( GetAgentRunStatusLabel(_appState.AgentRun.Status), _appState.AgentRun.Status switch { "failed" => "#FEE2E2", "completed" => "#E0F2FE", "paused" => "#FEF3C7", _ => "#DCFCE7", }, _appState.AgentRun.Status switch { "failed" => "#B91C1C", "completed" => "#0369A1", "paused" => "#B45309", _ => "#166534", }); var header = new DockPanel(); DockPanel.SetDock(statusChip, Dock.Right); header.Children.Add(statusChip); header.Children.Add(new TextBlock { Text = $"?먯씠?꾪듃 ?ㅽ뻾 run {ShortRunId(_appState.AgentRun.RunId)}", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, }); var body = new StackPanel(); body.Children.Add(header); body.Children.Add(new TextBlock { Text = $"iteration {_appState.AgentRun.LastIteration} 쨌 {GetRelativeTime(_appState.AgentRun.UpdatedAt)}", FontSize = 10.5, Foreground = secondaryText, Margin = new Thickness(0, 4, 0, 2), }); if (!string.IsNullOrWhiteSpace(_appState.AgentRun.Summary)) { body.Children.Add(new TextBlock { Text = _appState.AgentRun.Summary, TextWrapping = TextWrapping.Wrap, FontSize = 11.5, Foreground = secondaryText, Margin = new Thickness(0, 2, 0, 0), }); } return new Border { Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F8FAFC")), BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E2E8F0")), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(10), Padding = new Thickness(10, 9, 10, 9), Margin = new Thickness(10, 0, 10, 8), Child = body, }; } private IEnumerable FilterTaskRuns(IEnumerable tasks) { return _taskSummaryFilter switch { "agent" => tasks.Where(t => string.Equals(t.Kind, "agent", StringComparison.OrdinalIgnoreCase)), "tool" => tasks.Where(t => string.Equals(t.Kind, "tool", StringComparison.OrdinalIgnoreCase) || string.Equals(t.Kind, "skill", StringComparison.OrdinalIgnoreCase)), "subagent" => tasks.Where(t => string.Equals(t.Kind, "subagent", StringComparison.OrdinalIgnoreCase)), "failed" => tasks.Where(t => string.Equals(t.Status, "failed", StringComparison.OrdinalIgnoreCase)), _ => tasks, }; } private Border CreateTaskFilterChip(string label, string filterKey) { var isSelected = string.Equals(_taskSummaryFilter, filterKey, StringComparison.OrdinalIgnoreCase); var chip = CreateMetaChip( label, isSelected ? "#DBEAFE" : "#F3F4F6", isSelected ? "#1D4ED8" : "#4B5563"); chip.Cursor = Cursors.Hand; chip.MouseLeftButtonUp += (_, _) => { _taskSummaryFilter = filterKey; if (_taskSummaryTarget != null) ShowTaskSummaryPopup(_taskSummaryTarget); }; return chip; } private void AddTaskSummarySection( Panel panel, string title, IEnumerable tasks, Brush hoverBg, Brush primaryText, Brush secondaryText) { var taskList = tasks.ToList(); panel.Children.Add(new TextBlock { Text = title, FontSize = 11, FontWeight = FontWeights.SemiBold, Foreground = secondaryText, Margin = new Thickness(10, 4, 10, 6), }); if (taskList.Count == 0) { panel.Children.Add(new TextBlock { Text = "?쒖떆???묒뾽???놁뒿?덈떎.", FontSize = 11.5, Foreground = secondaryText, Margin = new Thickness(10, 0, 10, 8), }); return; } foreach (var task in taskList) { var item = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 8, 10, 8), Margin = new Thickness(4, 0, 4, 4), ToolTip = string.IsNullOrWhiteSpace(task.FilePath) ? task.Summary : $"{task.Summary}{Environment.NewLine}{task.FilePath}", }; item.MouseEnter += (_, _) => item.Background = hoverBg; item.MouseLeave += (_, _) => item.Background = Brushes.Transparent; var stack = new StackPanel(); var header = new DockPanel(); header.Children.Add(new TextBlock { Text = task.Title, FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, }); var statusChip = CreateMetaChip( GetTaskStatusLabel(task.Status), task.Status switch { "failed" => "#FEE2E2", "cancelled" => "#FEF3C7", "running" => "#DCFCE7", _ => "#E0F2FE", }, task.Status switch { "failed" => "#B91C1C", "cancelled" => "#B45309", "running" => "#166534", _ => "#0369A1", }); DockPanel.SetDock(statusChip, Dock.Right); header.Children.Add(statusChip); stack.Children.Add(header); stack.Children.Add(new TextBlock { Text = TruncateForStatus(task.Summary, 96), FontSize = 11.5, Foreground = secondaryText, Margin = new Thickness(0, 4, 0, 0), TextWrapping = TextWrapping.Wrap, }); stack.Children.Add(new TextBlock { Text = $"{task.Kind} 쨌 {task.UpdatedAt:HH:mm:ss}", FontSize = 10.5, Foreground = secondaryText, Margin = new Thickness(0, 4, 0, 0), }); var runId = ExtractRunIdFromTaskKey(task.Id); if (!string.IsNullOrWhiteSpace(runId)) { stack.Children.Add(new TextBlock { Text = $"run {runId}", FontSize = 10, Foreground = secondaryText, Margin = new Thickness(0, 2, 0, 0), }); } item.Child = stack; panel.Children.Add(item); } } private static string ExtractRunIdFromTaskKey(string? taskId) { if (string.IsNullOrWhiteSpace(taskId)) return ""; var parts = taskId.Split(':', StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 2) return ""; if (string.Equals(parts[0], "agent", StringComparison.OrdinalIgnoreCase)) return parts[1].Length > 8 ? parts[1][..8] : parts[1]; if (string.Equals(parts[0], "tool", StringComparison.OrdinalIgnoreCase) && parts.Length >= 3) return parts[1].Length > 8 ? parts[1][..8] : parts[1]; return ""; } /// ??퀎濡?留덉?留됱쑝濡??쒖꽦?붾맂 ?€??ID瑜?湲곗뼲. private Dictionary _tabConversationId => _sessionState.TabConversationIds; private void UpdateTabUI() { // ?대뜑 諛붾뒗 Cowork/Code ??뿉?쒕쭔 ?쒖떆 if (FolderBar != null) FolderBar.Visibility = _activeTab != "Chat" ? Visibility.Visible : Visibility.Collapsed; // ??퀎 ?낅젰 ?덈궡 臾멸뎄 if (InputWatermark != null) { InputWatermark.Text = _activeTab switch { "Cowork" => "?먯씠?꾪듃?먭쾶 ?묒뾽???붿껌?섏꽭??(?뚯씪 ?쎄린/?곌린, 臾몄꽌 ?앹꽦...)", "Code" => "肄붾뱶 愿€???묒뾽???붿껌?섏꽭??..", _ => _promptCardPlaceholder, }; } // 沅뚰븳 湲곕낯媛??곸슜 (Cowork/Code ??? ?ㅼ젙??湲곕낯媛??ъ슜) ApplyTabDefaultPermission(); // ?щ㎎/?붿옄???쒕∼?ㅼ슫?€ Cowork ??뿉?쒕쭔 ?쒖떆 if (_activeTab == "Cowork") { BuildBottomBar(); if (_settings.Settings.Llm.ShowFileBrowser && FileBrowserPanel != null) { FileBrowserPanel.Visibility = Visibility.Visible; BuildFileTree(); } } else if (_activeTab == "Code") { // 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(); // Cowork/Code ???꾪솚 ?????쒖떆 ShowRandomTip(); } private void SwitchToTabConversation() { // ?댁쟾 ??쓽 ?€???€?? lock (_convLock) { _sessionState.SaveCurrentConversation(_storage, _activeTab); } lock (_convLock) _currentConversation = _sessionState.LoadOrCreateConversation(_activeTab, _storage, _settings); RenderMessages(); _attachedFiles.Clear(); RefreshAttachedFilesUI(); UpdateChatTitle(); RefreshConversationList(); UpdateFolderBar(); } // ?€?€?€ ?묒뾽 ?대뜑 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private readonly List _attachedFiles = new(); private readonly List _pendingImages = new(); private void FolderPathLabel_Click(object sender, MouseButtonEventArgs e) => ShowFolderMenu(); private void ShowFolderMenu() { FolderMenuItems.Children.Clear(); var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; // 理쒓렐 ?대뜑 紐⑸줉 var maxDisplay = Math.Clamp(_settings.Settings.Llm.MaxRecentFolders, 3, 30); var recentFolders = _settings.Settings.Llm.RecentWorkFolders .Where(p => IsPathAllowed(p) && System.IO.Directory.Exists(p)) .Take(maxDisplay) .ToList(); if (recentFolders.Count > 0) { FolderMenuItems.Children.Add(new TextBlock { Text = "理쒓렐 ?대뜑", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = secondaryText, Margin = new Thickness(10, 6, 10, 4), }); var currentFolder = GetCurrentWorkFolder(); foreach (var folder in recentFolders) { var isActive = folder.Equals(currentFolder, StringComparison.OrdinalIgnoreCase); var displayName = System.IO.Path.GetFileName(folder); if (string.IsNullOrEmpty(displayName)) displayName = folder; var sp = new StackPanel { Orientation = Orientation.Horizontal }; if (isActive) { var checkEl = CreateSimpleCheck(accentBrush, 14); checkEl.Margin = new Thickness(0, 0, 8, 0); sp.Children.Add(checkEl); } var nameBlock = new TextBlock { Text = displayName, FontSize = 14, FontWeight = isActive ? FontWeights.SemiBold : FontWeights.Normal, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, MaxWidth = 340, TextTrimming = TextTrimming.CharacterEllipsis, }; sp.Children.Add(nameBlock); var itemBorder = new Border { Child = sp, Background = Brushes.Transparent, CornerRadius = new CornerRadius(8), Cursor = Cursors.Hand, Padding = new Thickness(10, 7, 10, 7), ToolTip = folder, }; itemBorder.MouseEnter += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); }; itemBorder.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; var capturedPath = folder; itemBorder.MouseLeftButtonUp += (_, _) => { FolderMenuPopup.IsOpen = false; SetWorkFolder(capturedPath); }; // ?고겢由???而⑦뀓?ㅽ듃 硫붾돱 (??젣, ?대뜑 ?닿린) itemBorder.MouseRightButtonUp += (_, re) => { re.Handled = true; ShowRecentFolderContextMenu(capturedPath); }; FolderMenuItems.Children.Add(itemBorder); } // 援щ텇?? FolderMenuItems.Children.Add(new Border { Height = 1, Background = TryFindResource("BorderColor") as Brush ?? Brushes.Gray, Margin = new Thickness(8, 4, 8, 4), Opacity = 0.5, }); } // ?대뜑 李얠븘蹂닿린 踰꾪듉 var browseSp = new StackPanel { Orientation = Orientation.Horizontal }; browseSp.Children.Add(new TextBlock { Text = "\uED25", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 14, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0), }); browseSp.Children.Add(new TextBlock { Text = "?대뜑 李얠븘蹂닿린...", FontSize = 14, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, }); var browseBorder = new Border { Child = browseSp, Background = Brushes.Transparent, CornerRadius = new CornerRadius(8), Cursor = Cursors.Hand, Padding = new Thickness(10, 7, 10, 7), }; browseBorder.MouseEnter += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); }; browseBorder.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; browseBorder.MouseLeftButtonUp += (_, _) => { FolderMenuPopup.IsOpen = false; BrowseWorkFolder(); }; FolderMenuItems.Children.Add(browseBorder); FolderMenuPopup.IsOpen = true; } private void BrowseWorkFolder() { var dlg = new System.Windows.Forms.FolderBrowserDialog { Description = "?묒뾽 ?대뜑瑜??좏깮?섏꽭??, ShowNewFolderButton = false, UseDescriptionForTitle = true, }; var currentFolder = GetCurrentWorkFolder(); if (!string.IsNullOrEmpty(currentFolder) && System.IO.Directory.Exists(currentFolder)) dlg.SelectedPath = currentFolder; if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return; if (!IsPathAllowed(dlg.SelectedPath)) { CustomMessageBox.Show("??寃쎈줈???묒뾽 ?대뜑濡??좏깮?????놁뒿?덈떎.", "寃쎈줈 ?쒗븳", MessageBoxButton.OK, MessageBoxImage.Warning); return; } SetWorkFolder(dlg.SelectedPath); } /// 寃쎈줈 ?좏슚??寃€????李⑤떒 ?€??寃쎈줈 ?꾪꽣留? private static bool IsPathAllowed(string path) { if (string.IsNullOrWhiteSpace(path)) return false; // C:\ 猷⑦듃 李⑤떒 var normalized = path.TrimEnd('\\', '/'); if (normalized.Equals("C:", StringComparison.OrdinalIgnoreCase)) return false; // "Document" ?ы븿 寃쎈줈 李⑤떒 (?€?뚮Ц??臾댁떆) if (path.IndexOf("Document", StringComparison.OrdinalIgnoreCase) >= 0) return false; return true; } private void SetWorkFolder(string path) { // 猷⑦듃 ?쒕씪?대툕 ?꾩껜瑜??묒뾽怨듦컙?쇰줈 ?ㅼ젙?섎뒗 寃껋쓣 李⑤떒 // ?? "C:\", "D:\", "E:\" ?? var fullPath = System.IO.Path.GetFullPath(path); var root = System.IO.Path.GetPathRoot(fullPath); if (!string.IsNullOrEmpty(root) && fullPath.TrimEnd('\\', '/').Equals(root.TrimEnd('\\', '/'), StringComparison.OrdinalIgnoreCase)) { ShowToast($"?쒕씪?대툕 猷⑦듃({root})???묒뾽怨듦컙?쇰줈 ?ㅼ젙?????놁뒿?덈떎. ?섏쐞 ?대뜑瑜??좏깮?섏꽭??", "\uE783", 3000); return; } FolderPathLabel.Text = path; FolderPathLabel.ToolTip = path; lock (_convLock) { if (_currentConversation != null) _currentConversation.WorkFolder = path; } // 理쒓렐 ?대뜑 紐⑸줉??異붽? (李⑤떒 寃쎈줈 ?쒖쇅) var recent = _settings.Settings.Llm.RecentWorkFolders; recent.RemoveAll(p => !IsPathAllowed(p)); recent.Remove(path); recent.Insert(0, path); var maxRecent = Math.Clamp(_settings.Settings.Llm.MaxRecentFolders, 3, 30); if (recent.Count > maxRecent) recent.RemoveRange(maxRecent, recent.Count - maxRecent); _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; } /// ?뚮쭏??留욌뒗 ContextMenu瑜??앹꽦?⑸땲?? private ContextMenu CreateThemedContextMenu() { var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); var border = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; return new ContextMenu { Background = bg, BorderBrush = border, BorderThickness = new Thickness(1), Padding = new Thickness(4), }; } /// 理쒓렐 ?대뜑 ??ぉ ?고겢由?而⑦뀓?ㅽ듃 硫붾돱瑜??쒖떆?⑸땲?? private void ShowRecentFolderContextMenu(string folderPath) { var menu = CreateThemedContextMenu(); var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; void AddItem(string icon, string label, Action action) { var sp = new StackPanel { Orientation = Orientation.Horizontal }; sp.Children.Add(new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0), }); sp.Children.Add(new TextBlock { Text = label, FontSize = 12, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, }); var mi = new MenuItem { Header = sp, Padding = new Thickness(8, 6, 16, 6) }; mi.Click += (_, _) => action(); menu.Items.Add(mi); } AddItem("\uED25", "?대뜑 ?닿린", () => { try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = folderPath, UseShellExecute = true, }); } catch { } }); AddItem("\uE8C8", "寃쎈줈 蹂듭궗", () => { try { Clipboard.SetText(folderPath); } catch { } }); menu.Items.Add(new Separator()); AddItem("\uE74D", "紐⑸줉?먯꽌 ??젣", () => { _settings.Settings.Llm.RecentWorkFolders.RemoveAll( p => p.Equals(folderPath, StringComparison.OrdinalIgnoreCase)); _settings.Save(); // 硫붾돱 ?덈줈怨좎묠 if (FolderMenuPopup.IsOpen) ShowFolderMenu(); }); menu.IsOpen = true; } 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; var folder = GetCurrentWorkFolder(); if (!string.IsNullOrEmpty(folder)) { FolderPathLabel.Text = folder; FolderPathLabel.ToolTip = folder; } else { FolderPathLabel.Text = "?대뜑瑜??좏깮?섏꽭??; FolderPathLabel.ToolTip = null; } // ?€?붾퀎 ?ㅼ젙 蹂듭썝 (?놁쑝硫??꾩뿭 湲곕낯媛? LoadConversationSettings(); UpdatePermissionUI(); UpdateDataUsageUI(); } /// ?꾩옱 ?€?붿쓽 媛쒕퀎 ?ㅼ젙??濡쒕뱶?⑸땲?? null?대㈃ ?꾩뿭 湲곕낯媛??ъ슜. private void LoadConversationSettings() { ChatConversation? conv; lock (_convLock) conv = _currentConversation; var llm = _settings.Settings.Llm; if (conv != null && conv.Permission != null) _settings.Settings.Llm.FilePermission = conv.Permission; _folderDataUsage = conv?.DataUsage ?? llm.FolderDataUsage ?? "active"; _selectedMood = conv?.Mood ?? llm.DefaultMood ?? "modern"; ApplyConversationUiState(conv); } /// ?꾩옱 ?섎떒 諛??ㅼ젙???€?붿뿉 ?€?ν빀?덈떎. private void SaveConversationSettings() { ChatConversation? conv; lock (_convLock) conv = _currentConversation; if (conv == null) return; conv.Permission = _settings.Settings.Llm.FilePermission; conv.DataUsage = _folderDataUsage; conv.Mood = _selectedMood; try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"?€???€???ㅽ뙣: {ex.Message}"); } } private void SaveConversationUiState() { ChatConversation? conv; lock (_convLock) conv = _currentConversation; if (conv == null) return; conv.ShowExecutionHistory = !Equals(BtnToggleExecutionLog?.Tag, "hidden"); var queueItems = GetDraftQueueState().Select(CloneDraftQueueItem).ToList(); conv.DraftQueueItems = queueItems; conv.DraftQueue = queueItems.Select(item => item.Text).ToList(); try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"?€??UI ?곹깭 ?€???ㅽ뙣: {ex.Message}"); } } private void ApplyConversationUiState(ChatConversation? conv) { var queue = GetConversationDraftQueueItems(conv); DraftQueuePanel.Tag = queue; var showExecutionHistory = conv?.ShowExecutionHistory ?? true; if (BtnToggleExecutionLog != null) BtnToggleExecutionLog.Tag = showExecutionHistory ? "visible" : "hidden"; if (ExecutionLogLabel != null) ExecutionLogLabel.Text = showExecutionHistory ? "?ㅽ뻾 濡쒓렇 ?④?" : "?ㅽ뻾 濡쒓렇"; if (ExecutionLogIcon != null) ExecutionLogIcon.Text = showExecutionHistory ? "\uE8F8" : "\uE946"; RefreshDraftComposerUi(); } private bool IsExecutionHistoryVisible() => !Equals(BtnToggleExecutionLog?.Tag, "hidden"); private AgentEvent ToAgentEvent(ChatExecutionEvent evt) { if (!Enum.TryParse(evt.Type, true, out var parsedType)) parsedType = AgentEventType.Thinking; return new AgentEvent { Timestamp = evt.Timestamp, RunId = evt.RunId ?? "", Type = parsedType, ToolName = evt.ToolName ?? "", Summary = evt.Summary ?? "", FilePath = evt.FilePath, Success = evt.Success, StepCurrent = evt.StepCurrent, StepTotal = evt.StepTotal, Steps = evt.Steps, ElapsedMs = evt.ElapsedMs, InputTokens = evt.InputTokens, OutputTokens = evt.OutputTokens, ToolInput = evt.ToolInput, Iteration = evt.Iteration, }; } private ChatExecutionEvent ToChatExecutionEvent(AgentEvent evt) => new() { Timestamp = evt.Timestamp, RunId = evt.RunId, 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 sealed class ConversationTimelineItem { public DateTime Timestamp { get; init; } public ChatMessage? Message { get; init; } public ChatExecutionEvent? ExecutionEvent { get; init; } } // ?€?€?€ 沅뚰븳 硫붾돱 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void BtnPermission_Click(object sender, RoutedEventArgs e) { if (PermissionPopup == null) return; PermissionItems.Children.Clear(); var levels = new (string Level, string Sym, string Desc, string Color)[] { ("Ask", "\uE8D7", "留ㅻ쾲 ?뺤씤 ???뚯씪 ?묎렐 ???ъ슜?먯뿉寃?臾살뒿?덈떎", "#4B5EFC"), ("Auto", "\uE73E", "?먮룞 ?덉슜 ???뚯씪???먮룞?쇰줈 ?쎄퀬 ?곷땲??, "#DD6B20"), ("Deny", "\uE711", "?묎렐 李⑤떒 ???뚯씪 ?묎렐???덉슜?섏? ?딆뒿?덈떎", "#C50F1F"), }; var current = _settings.Settings.Llm.FilePermission; foreach (var (level, sym, desc, color) in levels) { var isActive = level.Equals(current, StringComparison.OrdinalIgnoreCase); // ?쇱슫??肄붾꼫 ?쒗뵆由?(湲곕낯 Button ?щ\ ?쒓굅) var template = new ControlTemplate(typeof(Button)); var bdFactory = new FrameworkElementFactory(typeof(Border)); bdFactory.SetValue(Border.BackgroundProperty, Brushes.Transparent); bdFactory.SetValue(Border.CornerRadiusProperty, new CornerRadius(8)); bdFactory.SetValue(Border.PaddingProperty, new Thickness(12, 8, 12, 8)); bdFactory.Name = "Bd"; var cpFactory = new FrameworkElementFactory(typeof(ContentPresenter)); cpFactory.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Left); cpFactory.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center); bdFactory.AppendChild(cpFactory); template.VisualTree = bdFactory; // ?몃쾭 ?④낵 var hoverTrigger = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true }; hoverTrigger.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)), "Bd")); template.Triggers.Add(hoverTrigger); var btn = new Button { Template = template, BorderThickness = new Thickness(0), Cursor = Cursors.Hand, HorizontalContentAlignment = HorizontalAlignment.Left, Margin = new Thickness(0, 1, 0, 1), }; ApplyHoverScaleAnimation(btn, 1.02); var sp = new StackPanel { Orientation = Orientation.Horizontal }; // 而ㅼ뒪?€ 泥댄겕 ?꾩씠肄? sp.Children.Add(CreateCheckIcon(isActive)); sp.Children.Add(new TextBlock { Text = sym, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 14, Foreground = BrushFromHex(color), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0), }); var textStack = new StackPanel(); textStack.Children.Add(new TextBlock { Text = level, FontSize = 13, FontWeight = FontWeights.Bold, Foreground = BrushFromHex(color), }); textStack.Children.Add(new TextBlock { Text = desc, FontSize = 11, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, TextWrapping = TextWrapping.Wrap, MaxWidth = 220, }); sp.Children.Add(textStack); btn.Content = sp; var capturedLevel = level; btn.Click += (_, _) => { _settings.Settings.Llm.FilePermission = capturedLevel; UpdatePermissionUI(); SaveConversationSettings(); PermissionPopup.IsOpen = false; }; PermissionItems.Children.Add(btn); } PermissionPopup.IsOpen = true; } private bool _autoWarningDismissed; // Auto 寃쎄퀬 諛곕꼫 ?ъ슜?먭? ?レ븯?붿? 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; var perm = _settings.Settings.Llm.FilePermission; PermissionLabel.Text = perm; PermissionIcon.Text = perm switch { "Auto" => "\uE73E", "Deny" => "\uE711", _ => "\uE8D7", }; // Auto 紐⑤뱶????寃쎄퀬 ?됱긽 + 諛곕꼫 ?쒖떆 if (perm == "Auto") { var warnColor = new SolidColorBrush(Color.FromRgb(0xDD, 0x6B, 0x20)); PermissionLabel.Foreground = warnColor; PermissionIcon.Foreground = warnColor; // Auto ?꾪솚 ?????€?붿뿉?쒕쭔 1???꾩닔 ?쒖떆 (湲곗〈 ?€?붿뿉???대? Auto?€?쇰㈃ ?④?) ChatConversation? convForWarn; lock (_convLock) convForWarn = _currentConversation; var isExisting = convForWarn != null && convForWarn.Messages.Count > 0 && convForWarn.Permission == "Auto"; if (AutoPermissionWarning != null && !_autoWarningDismissed && !isExisting) AutoPermissionWarning.Visibility = Visibility.Visible; } else { _autoWarningDismissed = false; // Auto媛€ ?꾨땶 紐⑤뱶濡??꾪솚?섎㈃ 由ъ뀑 var defaultFg = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var iconFg = perm == "Deny" ? new SolidColorBrush(Color.FromRgb(0xC5, 0x0F, 0x1F)) : new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); // Ask = ?뚮??? PermissionLabel.Foreground = defaultFg; PermissionIcon.Foreground = iconFg; if (AutoPermissionWarning != null) AutoPermissionWarning.Visibility = Visibility.Collapsed; } } // ?€?€?€?€ ?곗씠???쒖슜 ?섏? 硫붾돱 ?€?€?€?€ private void BtnDataUsage_Click(object sender, System.Windows.Input.MouseButtonEventArgs e) { if (DataUsagePopup == null) return; DataUsageItems.Children.Clear(); var options = new (string Key, string Sym, string Label, string Desc, string Color)[] { ("active", "\uE9F5", "?곴레 ?쒖슜", "?대뜑 ??臾몄꽌瑜??먮룞 ?먯깋?섏뿬 蹂닿퀬???묒꽦???곴레 ?쒖슜?⑸땲??, "#107C10"), ("passive", "\uE8FD", "?뚭레 ?쒖슜", "?ъ슜?먭? ?붿껌???뚮쭔 ?대뜑 ?곗씠?곕? 李몄“?⑸땲??, "#D97706"), ("none", "\uE8D8", "?쒖슜?섏? ?딆쓬", "?대뜑 ??臾몄꽌瑜??쎄굅??李몄“?섏? ?딆뒿?덈떎", "#9CA3AF"), }; foreach (var (key, sym, label, desc, color) in options) { var isActive = key.Equals(_folderDataUsage, StringComparison.OrdinalIgnoreCase); var template = new ControlTemplate(typeof(Button)); var bdFactory = new FrameworkElementFactory(typeof(Border)); bdFactory.SetValue(Border.BackgroundProperty, Brushes.Transparent); bdFactory.SetValue(Border.CornerRadiusProperty, new CornerRadius(8)); bdFactory.SetValue(Border.PaddingProperty, new Thickness(12, 8, 12, 8)); bdFactory.Name = "Bd"; var cpFactory = new FrameworkElementFactory(typeof(ContentPresenter)); cpFactory.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Left); cpFactory.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center); bdFactory.AppendChild(cpFactory); template.VisualTree = bdFactory; var hoverTrigger = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true }; hoverTrigger.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)), "Bd")); template.Triggers.Add(hoverTrigger); var btn = new Button { Template = template, BorderThickness = new Thickness(0), Cursor = Cursors.Hand, HorizontalContentAlignment = HorizontalAlignment.Left, Margin = new Thickness(0, 1, 0, 1), }; ApplyHoverScaleAnimation(btn, 1.02); var sp = new StackPanel { Orientation = Orientation.Horizontal }; // 而ㅼ뒪?€ 泥댄겕 ?꾩씠肄? sp.Children.Add(CreateCheckIcon(isActive)); sp.Children.Add(new TextBlock { Text = sym, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 14, Foreground = BrushFromHex(color), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0), }); var textStack = new StackPanel(); textStack.Children.Add(new TextBlock { Text = label, FontSize = 13, FontWeight = FontWeights.Bold, Foreground = BrushFromHex(color), }); textStack.Children.Add(new TextBlock { Text = desc, FontSize = 11, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, TextWrapping = TextWrapping.Wrap, MaxWidth = 240, }); sp.Children.Add(textStack); btn.Content = sp; var capturedKey = key; btn.Click += (_, _) => { _folderDataUsage = capturedKey; UpdateDataUsageUI(); SaveConversationSettings(); DataUsagePopup.IsOpen = false; }; DataUsageItems.Children.Add(btn); } DataUsagePopup.IsOpen = true; } private void UpdateDataUsageUI() { if (DataUsageLabel == null || DataUsageIcon == null) return; var (label, icon, color) = _folderDataUsage switch { "passive" => ("?뚭레", "\uE8FD", "#D97706"), "none" => ("誘몄궗??, "\uE8D8", "#9CA3AF"), _ => ("?곴레", "\uE9F5", "#107C10"), }; DataUsageLabel.Text = label; DataUsageIcon.Text = icon; DataUsageIcon.Foreground = BrushFromHex(color); } /// Cowork/Code ??吏꾩엯 ???ㅼ젙??湲곕낯 沅뚰븳???곸슜. private void ApplyTabDefaultPermission() { if (_activeTab == "Chat") { // Chat ?? 寃쎄퀬 諛곕꼫 ?④린怨?湲곕낯 Ask 紐⑤뱶濡?蹂듭썝 _settings.Settings.Llm.FilePermission = "Ask"; UpdatePermissionUI(); return; } var defaultPerm = _settings.Settings.Llm.DefaultAgentPermission; if (!string.IsNullOrEmpty(defaultPerm)) { _settings.Settings.Llm.FilePermission = defaultPerm; UpdatePermissionUI(); } } // ?€?€?€ ?뚯씪 泥⑤? ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void BtnAttach_Click(object sender, RoutedEventArgs e) { var dlg = new Microsoft.Win32.OpenFileDialog { Multiselect = true, Title = "泥⑤????뚯씪???좏깮?섏꽭??, Filter = "紐⑤뱺 ?뚯씪 (*.*)|*.*|?띿뒪??(*.txt;*.md;*.csv)|*.txt;*.md;*.csv|肄붾뱶 (*.cs;*.py;*.js;*.ts)|*.cs;*.py;*.js;*.ts", }; // ?묒뾽 ?대뜑媛€ ?덉쑝硫?珥덇린 寃쎈줈 ?ㅼ젙 var workFolder = GetCurrentWorkFolder(); if (!string.IsNullOrEmpty(workFolder) && System.IO.Directory.Exists(workFolder)) dlg.InitialDirectory = workFolder; if (dlg.ShowDialog() != true) return; foreach (var file in dlg.FileNames) AddAttachedFile(file); } private static readonly HashSet ImageExtensions = new(StringComparer.OrdinalIgnoreCase) { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp" }; private void AddAttachedFile(string filePath) { if (_attachedFiles.Contains(filePath)) return; // ?뚯씪 ?ш린 ?쒗븳 (10MB) try { var fi = new System.IO.FileInfo(filePath); if (fi.Length > 10 * 1024 * 1024) { CustomMessageBox.Show($"?뚯씪???덈Т ?쎈땲??(10MB 珥덇낵):\n{fi.Name}", "泥⑤? ?쒗븳", MessageBoxButton.OK, MessageBoxImage.Warning); return; } // ?대?吏€ ?뚯씪 ??Vision API??base64 蹂€?? var ext = fi.Extension.ToLowerInvariant(); if (ImageExtensions.Contains(ext) && _settings.Settings.Llm.EnableImageInput) { var maxKb = _settings.Settings.Llm.MaxImageSizeKb; if (maxKb <= 0) maxKb = 5120; if (fi.Length > maxKb * 1024) { CustomMessageBox.Show($"?대?吏€媛€ ?덈Т ?쎈땲??({fi.Length / 1024}KB, 理쒕? {maxKb}KB).", "?대?吏€ ?ш린 珥덇낵", MessageBoxButton.OK, MessageBoxImage.Warning); return; } var bytes = System.IO.File.ReadAllBytes(filePath); var mimeType = ext switch { ".jpg" or ".jpeg" => "image/jpeg", ".gif" => "image/gif", ".bmp" => "image/bmp", ".webp" => "image/webp", _ => "image/png", }; var attachment = new ImageAttachment { Base64 = Convert.ToBase64String(bytes), MimeType = mimeType, FileName = fi.Name, }; // 以묐났 ?뺤씤 if (_pendingImages.Any(i => i.FileName == attachment.FileName)) return; _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; var secondaryBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var hintBg = TryFindResource("HintBackground") as Brush ?? Brushes.LightGray; foreach (var file in _attachedFiles.ToList()) { var fileName = System.IO.Path.GetFileName(file); var capturedFile = file; var chip = new Border { Background = hintBg, CornerRadius = new CornerRadius(6), Padding = new Thickness(8, 4, 4, 4), Margin = new Thickness(0, 0, 4, 4), Cursor = Cursors.Hand, }; var sp = new StackPanel { Orientation = Orientation.Horizontal }; sp.Children.Add(new TextBlock { Text = "\uE8A5", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 10, Foreground = secondaryBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0), }); sp.Children.Add(new TextBlock { Text = fileName, FontSize = 11, Foreground = secondaryBrush, VerticalAlignment = VerticalAlignment.Center, MaxWidth = 150, TextTrimming = TextTrimming.CharacterEllipsis, ToolTip = file, }); var removeBtn = new Button { Content = new TextBlock { Text = "\uE711", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 8, Foreground = secondaryBrush }, Background = Brushes.Transparent, BorderThickness = new Thickness(0), Cursor = Cursors.Hand, Padding = new Thickness(4, 2, 4, 2), Margin = new Thickness(2, 0, 0, 0), }; removeBtn.Click += (_, _) => RemoveAttachedFile(capturedFile); sp.Children.Add(removeBtn); chip.Child = sp; AttachedFilesPanel.Items.Add(chip); } } /// 泥⑤? ?뚯씪 ?댁슜???쒖뒪??硫붿떆吏€濡?蹂€?섑빀?덈떎. private string BuildFileContextPrompt() { if (_attachedFiles.Count == 0) return ""; var sb = new System.Text.StringBuilder(); sb.AppendLine("\n[泥⑤? ?뚯씪 而⑦뀓?ㅽ듃]"); foreach (var file in _attachedFiles) { try { var ext = System.IO.Path.GetExtension(file).ToLowerInvariant(); var isBinary = ext is ".exe" or ".dll" or ".zip" or ".7z" or ".rar" or ".tar" or ".gz" or ".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".ico" or ".webp" or ".svg" or ".pdf" or ".docx" or ".xlsx" or ".pptx" or ".doc" or ".xls" or ".ppt" or ".mp3" or ".mp4" or ".avi" or ".mov" or ".mkv" or ".wav" or ".flac" or ".psd" or ".ai" or ".sketch" or ".fig" or ".msi" or ".iso" or ".img" or ".bin" or ".dat" or ".db" or ".sqlite"; if (isBinary) { sb.AppendLine($"\n--- {System.IO.Path.GetFileName(file)} (諛붿씠?덈━ ?뚯씪, ?댁슜 ?앸왂) ---"); continue; } var content = System.IO.File.ReadAllText(file); // 理쒕? 8000?먮줈 ?쒗븳 if (content.Length > 8000) content = content[..8000] + "\n... (?댄븯 ?앸왂)"; sb.AppendLine($"\n--- {System.IO.Path.GetFileName(file)} ---"); sb.AppendLine(content); } catch (Exception ex) { sb.AppendLine($"\n--- {System.IO.Path.GetFileName(file)} (?쎄린 ?ㅽ뙣: {ex.Message}) ---"); } } return sb.ToString(); } private void ResizeGrip_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { var newW = Width + e.HorizontalChange; var newH = Height + e.VerticalChange; if (newW >= MinWidth) Width = newW; if (newH >= MinHeight) Height = newH; } // ?€?€?€ ?ъ씠?쒕컮 ?좉? ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void BtnToggleSidebar_Click(object sender, RoutedEventArgs e) { _sidebarVisible = !_sidebarVisible; if (_sidebarVisible) { // ?ъ씠?쒕컮 ?닿린, ?꾩씠肄?諛??④린湲? IconBarColumn.Width = new GridLength(0); IconBarPanel.Visibility = Visibility.Collapsed; SidebarPanel.Visibility = Visibility.Visible; ToggleSidebarIcon.Text = "\uE76B"; AnimateSidebar(0, 270, () => SidebarColumn.MinWidth = 200); } else { // ?ъ씠?쒕컮 ?リ린, ?꾩씠肄?諛??쒖떆 SidebarColumn.MinWidth = 0; ToggleSidebarIcon.Text = "\uE76C"; AnimateSidebar(270, 0, () => { SidebarPanel.Visibility = Visibility.Collapsed; IconBarColumn.Width = new GridLength(52); IconBarPanel.Visibility = Visibility.Visible; }); } } private void AnimateSidebar(double from, double to, Action? onComplete = null) { var duration = 200.0; var start = DateTime.UtcNow; var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(10) }; EventHandler tickHandler = null!; tickHandler = (_, _) => { var elapsed = (DateTime.UtcNow - start).TotalMilliseconds; var t = Math.Min(elapsed / duration, 1.0); t = 1 - (1 - t) * (1 - t); SidebarColumn.Width = new GridLength(from + (to - from) * t); if (elapsed >= duration) { timer.Stop(); timer.Tick -= tickHandler; SidebarColumn.Width = new GridLength(to); onComplete?.Invoke(); } }; timer.Tick += tickHandler; timer.Start(); } // ?€?€?€ ?€??紐⑸줉 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ public void RefreshConversationList() { var metas = _storage.LoadAllMeta(); // ?꾨━??移댄뀒怨좊━ ???꾩씠肄??됱긽 留ㅽ븨 (ChatCategory???녿뒗 肄붿썙??肄붾뱶 移댄뀒怨좊━ 吏€?? var allPresets = Services.PresetService.GetByTabWithCustom("Cowork", _settings.Settings.Llm.CustomPresets) .Concat(Services.PresetService.GetByTabWithCustom("Code", _settings.Settings.Llm.CustomPresets)) .Concat(Services.PresetService.GetByTabWithCustom("Chat", _settings.Settings.Llm.CustomPresets)); var presetMap = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var p in allPresets) presetMap.TryAdd(p.Category, (p.Symbol, p.Color)); var items = metas.Select(c => { var symbol = ChatCategory.GetSymbol(c.Category); var color = ChatCategory.GetColor(c.Category); // ChatCategory 湲곕낯媛믪씠硫??꾨━?뗭뿉??寃€?? if (symbol == "\uE8BD" && color == "#6B7280" && c.Category != ChatCategory.General) { if (presetMap.TryGetValue(c.Category, out var pm)) { symbol = pm.Symbol; color = pm.Color; } } return new ConversationMeta { Id = c.Id, Title = c.Title, Pinned = c.Pinned, Category = c.Category, Symbol = symbol, ColorHex = color, Tab = string.IsNullOrEmpty(c.Tab) ? "Chat" : c.Tab, UpdatedAtText = FormatDate(c.UpdatedAt), UpdatedAt = c.UpdatedAt, Preview = c.Preview ?? "", ParentId = c.ParentId, }; }).ToList(); // ???꾪꽣 ???꾩옱 ?쒖꽦 ??쓽 ?€?붾쭔 ?쒖떆 items = items.Where(i => i.Tab == _activeTab).ToList(); // 移댄뀒怨좊━ ?꾪꽣 ?곸슜 if (_selectedCategory == "__custom__") { // 而ㅼ뒪?€ ?꾨━?뗭쑝濡?留뚮뱺 ?€?붾쭔 ?쒖떆 var customCats = _settings.Settings.Llm.CustomPresets .Select(c => $"custom_{c.Id}").ToHashSet(); items = items.Where(i => customCats.Contains(i.Category)).ToList(); } else if (!string.IsNullOrEmpty(_selectedCategory)) items = items.Where(i => i.Category == _selectedCategory).ToList(); // 寃€???꾪꽣 (?쒕ぉ + ?댁슜 誘몃━蹂닿린) var search = SearchBox?.Text?.Trim() ?? ""; if (!string.IsNullOrEmpty(search)) items = items.Where(i => i.Title.Contains(search, StringComparison.OrdinalIgnoreCase) || i.Preview.Contains(search, StringComparison.OrdinalIgnoreCase) ).ToList(); RenderConversationList(items); } private const int ConversationPageSize = 50; private List? _pendingConversations; private void RenderConversationList(List items) { ConversationPanel.Children.Clear(); _pendingConversations = null; if (items.Count == 0) { var empty = new TextBlock { Text = "?€?붽? ?놁뒿?덈떎", FontSize = 12, Foreground = (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray), HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 20, 0, 0) }; ConversationPanel.Children.Add(empty); return; } // ?ㅻ뒛 / ?댁쟾 洹몃9 遺꾨━ var today = DateTime.Today; var todayItems = items.Where(i => i.UpdatedAt.Date == today).ToList(); var olderItems = items.Where(i => i.UpdatedAt.Date < today).ToList(); var allOrdered = new List<(string Group, ConversationMeta Item)>(); foreach (var item in todayItems) allOrdered.Add(("?ㅻ뒛", item)); foreach (var item in olderItems) allOrdered.Add(("?댁쟾", item)); // 泥??섏씠吏€留??뚮뜑留? var firstPage = allOrdered.Take(ConversationPageSize).ToList(); string? lastGroup = null; foreach (var (group, item) in firstPage) { if (group != lastGroup) { AddGroupHeader(group); lastGroup = group; } AddConversationItem(item); } // ?섎㉧吏€媛€ ?덉쑝硫?"??蹂닿린" 踰꾪듉 if (allOrdered.Count > ConversationPageSize) { _pendingConversations = items; AddLoadMoreButton(allOrdered.Count - ConversationPageSize); } } private void AddLoadMoreButton(int remaining) { var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var btn = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(8), Cursor = Cursors.Hand, Padding = new Thickness(8, 10, 8, 10), Margin = new Thickness(6, 4, 6, 4), HorizontalAlignment = HorizontalAlignment.Stretch, }; var sp = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center }; sp.Children.Add(new TextBlock { Text = $"??蹂닿린 ({remaining}媛??⑥쓬)", FontSize = 12, Foreground = accentBrush, HorizontalAlignment = HorizontalAlignment.Center, }); btn.Child = sp; btn.MouseEnter += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x12, 0xFF, 0xFF, 0xFF)); }; btn.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; btn.MouseLeftButtonUp += (_, _) => { // ?꾩껜 紐⑸줉 ?뚮뜑留? if (_pendingConversations != null) { var all = _pendingConversations; _pendingConversations = null; ConversationPanel.Children.Clear(); var today = DateTime.Today; var todayItems = all.Where(i => i.UpdatedAt.Date == today).ToList(); var olderItems = all.Where(i => i.UpdatedAt.Date < today).ToList(); if (todayItems.Count > 0) { AddGroupHeader("?ㅻ뒛"); foreach (var item in todayItems) AddConversationItem(item); } if (olderItems.Count > 0) { AddGroupHeader("?댁쟾"); foreach (var item in olderItems) AddConversationItem(item); } } }; ConversationPanel.Children.Add(btn); } private void AddGroupHeader(string text) { var header = new TextBlock { Text = text, FontSize = 11, FontWeight = FontWeights.SemiBold, Foreground = (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray), Margin = new Thickness(8, 12, 0, 4) }; ConversationPanel.Children.Add(header); } private void AddConversationItem(ConversationMeta item) { var isSelected = false; lock (_convLock) isSelected = _currentConversation?.Id == item.Id; var isBranch = !string.IsNullOrEmpty(item.ParentId); var border = new Border { Background = isSelected ? new SolidColorBrush(Color.FromArgb(0x30, 0x4B, 0x5E, 0xFC)) : Brushes.Transparent, CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 8, 10, 8), Margin = isBranch ? new Thickness(16, 1, 0, 1) : new Thickness(0, 1, 0, 1), Cursor = Cursors.Hand }; var grid = new Grid(); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(28) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // 移댄뀒怨좊━ ?꾩씠肄?(怨좎젙 ???€ ?꾩씠肄? 洹???移댄뀒怨좊━ ?됱긽) Brush iconBrush; if (item.Pinned) iconBrush = Brushes.Orange; else { try { iconBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(item.ColorHex)); } catch { iconBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue; } } var iconText = item.Pinned ? "\uE718" : !string.IsNullOrEmpty(item.ParentId) ? "\uE8A5" : item.Symbol; if (!string.IsNullOrEmpty(item.ParentId)) iconBrush = new SolidColorBrush(Color.FromRgb(0x8B, 0x5C, 0xF6)); // 遺꾧린: 蹂대씪?? var icon = new TextBlock { Text = iconText, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 13, Foreground = iconBrush, VerticalAlignment = VerticalAlignment.Center }; Grid.SetColumn(icon, 0); grid.Children.Add(icon); // ?쒕ぉ + ?좎쭨 (?좏깮 ???쎄컙 諛앷쾶) var titleColor = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var dateColor = TryFindResource("HintText") as Brush ?? Brushes.DarkGray; var stack = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; var title = new TextBlock { Text = item.Title, FontSize = 12.5, Foreground = titleColor, TextTrimming = TextTrimming.CharacterEllipsis }; var date = new TextBlock { Text = item.UpdatedAtText, FontSize = 10, Foreground = dateColor, Margin = new Thickness(0, 2, 0, 0) }; stack.Children.Add(title); stack.Children.Add(date); Grid.SetColumn(stack, 1); grid.Children.Add(stack); // 移댄뀒怨좊━ 蹂€寃?踰꾪듉 (?몃쾭 ???쒖떆) var catBtn = new Button { Content = new TextBlock { Text = "\uE70F", // Edit FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 10, Foreground = (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray) }, Background = Brushes.Transparent, BorderThickness = new Thickness(0), Cursor = Cursors.Hand, VerticalAlignment = VerticalAlignment.Center, Visibility = Visibility.Collapsed, Padding = new Thickness(4), ToolTip = _activeTab == "Cowork" ? "?묒뾽 ?좏삎" : "?€??二쇱젣 蹂€寃? }; var capturedId = item.Id; catBtn.Click += (_, _) => ShowConversationMenu(capturedId); Grid.SetColumn(catBtn, 2); grid.Children.Add(catBtn); // ?좏깮 ??醫뚯륫 ?≪꽱??諛? if (isSelected) { border.BorderBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; border.BorderThickness = new Thickness(2, 0, 0, 0); } border.Child = grid; // ?몃쾭 ?대깽????諛곌꼍 + 誘몄꽭 ?뺣? border.RenderTransformOrigin = new Point(0.5, 0.5); border.RenderTransform = new ScaleTransform(1, 1); var selectedBg = new SolidColorBrush(Color.FromArgb(0x30, 0x4B, 0x5E, 0xFC)); border.MouseEnter += (_, _) => { if (!isSelected) border.Background = new SolidColorBrush(Color.FromArgb(0x15, 0xFF, 0xFF, 0xFF)); catBtn.Visibility = Visibility.Visible; var st = border.RenderTransform as ScaleTransform; st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120))); st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120))); }; border.MouseLeave += (_, _) => { if (!isSelected) border.Background = Brushes.Transparent; catBtn.Visibility = Visibility.Collapsed; var st = border.RenderTransform as ScaleTransform; st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150))); st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150))); }; // ?대┃ ???대? ?좏깮???€?붾㈃ ?쒕ぉ ?몄쭛, ?꾨땲硫??€???꾪솚 border.MouseLeftButtonDown += (_, _) => { try { if (isSelected) { // ?대? ?좏깮???€?????쒕ぉ ?몄쭛 紐⑤뱶 EnterTitleEditMode(title, item.Id, titleColor); return; } // ?ㅽ듃由щ컢 以묒씠硫?痍⑥냼 if (_isStreaming) { _streamCts?.Cancel(); _cursorTimer.Stop(); _typingTimer.Stop(); _elapsedTimer.Stop(); _activeStreamText = null; _elapsedLabel = null; _isStreaming = false; } var conv = _storage.Load(item.Id); if (conv != null) { // Tab 蹂댁젙 if (string.IsNullOrEmpty(conv.Tab)) conv.Tab = _activeTab; lock (_convLock) _currentConversation = conv; _sessionState.RememberConversation(_activeTab, conv.Id); UpdateChatTitle(); RenderMessages(); RefreshConversationList(); } } catch (Exception ex) { LogService.Error($"?€???꾪솚 ?ㅻ쪟: {ex.Message}"); } }; // ?고겢由????€??愿€由?硫붾돱 諛붾줈 ?쒖떆 border.MouseRightButtonUp += (_, me) => { me.Handled = true; // ?좏깮?섏? ?딆? ?€?붾? ?고겢由?븯硫?癒쇱? ?좏깮 if (!isSelected) { var conv = _storage.Load(item.Id); if (conv != null) { if (string.IsNullOrEmpty(conv.Tab)) conv.Tab = _activeTab; lock (_convLock) _currentConversation = conv; _sessionState.RememberConversation(_activeTab, conv.Id); UpdateChatTitle(); RenderMessages(); } } // Dispatcher濡?吏€???몄텧 ??留덉슦???대깽???꾨즺 ??Popup ?닿린 Dispatcher.BeginInvoke(new Action(() => ShowConversationMenu(item.Id)), DispatcherPriority.Input); }; ConversationPanel.Children.Add(border); } // ?€?€?€ ?€???쒕ぉ ?몃씪???몄쭛 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void EnterTitleEditMode(TextBlock titleTb, string conversationId, Brush titleColor) { try { // titleTb媛€ ?대? 遺€紐⑥뿉??遺꾨━??寃쎌슦(?몄쭛 以? 臾댁떆 var parent = titleTb.Parent as StackPanel; if (parent == null) return; var idx = parent.Children.IndexOf(titleTb); if (idx < 0) return; var editBox = new TextBox { Text = titleTb.Text, FontSize = 12.5, Foreground = titleColor, Background = Brushes.Transparent, BorderBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue, BorderThickness = new Thickness(0, 0, 0, 1), CaretBrush = titleColor, Padding = new Thickness(0), Margin = new Thickness(0), }; // ?덉쟾?섍쾶 ?먯떇 援먯껜: 癒쇱? ?쒓굅 ???쎌엯 parent.Children.RemoveAt(idx); parent.Children.Insert(idx, editBox); var committed = false; void CommitEdit() { if (committed) return; committed = true; var newTitle = editBox.Text.Trim(); if (string.IsNullOrEmpty(newTitle)) newTitle = titleTb.Text; titleTb.Text = newTitle; // editBox媛€ ?꾩쭅 parent???덈뒗吏€ ?뺤씤 ??援먯껜 try { var currentIdx = parent.Children.IndexOf(editBox); if (currentIdx >= 0) { parent.Children.RemoveAt(currentIdx); parent.Children.Insert(currentIdx, titleTb); } } catch { /* 遺€紐④? ?대? ?댁젣??寃쎌슦 臾댁떆 */ } var conv = _storage.Load(conversationId); if (conv != null) { conv.Title = newTitle; _storage.Save(conv); lock (_convLock) { if (_currentConversation?.Id == conversationId) _currentConversation.Title = newTitle; } UpdateChatTitle(); } } void CancelEdit() { if (committed) return; committed = true; try { var currentIdx = parent.Children.IndexOf(editBox); if (currentIdx >= 0) { parent.Children.RemoveAt(currentIdx); parent.Children.Insert(currentIdx, titleTb); } } catch { /* 遺€紐④? ?대? ?댁젣??寃쎌슦 臾댁떆 */ } } editBox.KeyDown += (_, ke) => { if (ke.Key == Key.Enter) { ke.Handled = true; CommitEdit(); } if (ke.Key == Key.Escape) { ke.Handled = true; CancelEdit(); } }; editBox.LostFocus += (_, _) => CommitEdit(); editBox.Focus(); editBox.SelectAll(); } catch (Exception ex) { LogService.Error($"?쒕ぉ ?몄쭛 ?ㅻ쪟: {ex.Message}"); } } // ?€?€?€ 移댄뀒怨좊━ 蹂€寃??앹뾽 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void ShowConversationMenu(string conversationId) { var conv = _storage.Load(conversationId); var isPinned = conv?.Pinned ?? false; var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E)); var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(30, 255, 255, 255)); var popup = new Popup { StaysOpen = false, AllowsTransparency = true, PopupAnimation = PopupAnimation.Fade, Placement = PlacementMode.MousePoint, }; var container = new Border { Background = bgBrush, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(12), Padding = new Thickness(6), MinWidth = 200, Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3, Color = Colors.Black }, }; var stack = new StackPanel(); // 硫붾돱 ??ぉ ?ы띁 Border CreateMenuItem(string icon, string text, Brush iconColor, Action onClick) { var item = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 7, 10, 7), Margin = new Thickness(0, 1, 0, 1), Cursor = Cursors.Hand, }; var g = new Grid(); g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(24) }); g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); var iconTb = new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = iconColor, VerticalAlignment = VerticalAlignment.Center, }; Grid.SetColumn(iconTb, 0); g.Children.Add(iconTb); var textTb = new TextBlock { Text = text, FontSize = 12.5, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, }; Grid.SetColumn(textTb, 1); g.Children.Add(textTb); item.Child = g; item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; }; item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; item.MouseLeftButtonUp += (_, _) => { popup.IsOpen = false; onClick(); }; return item; } Border CreateSeparator() => new() { Height = 1, Background = borderBrush, Opacity = 0.3, Margin = new Thickness(8, 4, 8, 4), }; // 怨좎젙/?댁젣 stack.Children.Add(CreateMenuItem( isPinned ? "\uE77A" : "\uE718", isPinned ? "怨좎젙 ?댁젣" : "?곷떒 怨좎젙", TryFindResource("AccentColor") as Brush ?? Brushes.Blue, () => { var c = _storage.Load(conversationId); if (c != null) { c.Pinned = !c.Pinned; _storage.Save(c); lock (_convLock) { if (_currentConversation?.Id == conversationId) _currentConversation.Pinned = c.Pinned; } RefreshConversationList(); } })); // ?대쫫 蹂€寃? stack.Children.Add(CreateMenuItem("\uE8AC", "?대쫫 蹂€寃?, secondaryText, () => { // ?€??紐⑸줉?먯꽌 ?대떦 ??ぉ 李얠븘???몄쭛 紐⑤뱶 吏꾩엯 foreach (UIElement child in ConversationPanel.Children) { if (child is Border b && b.Child is Grid g) { foreach (UIElement gc in g.Children) { if (gc is StackPanel sp && sp.Children.Count > 0 && sp.Children[0] is TextBlock tb) { // title怨?留ㅼ묶 if (conv != null && tb.Text == conv.Title) { var titleColor = TryFindResource("PrimaryText") as Brush ?? Brushes.White; EnterTitleEditMode(tb, conversationId, titleColor); return; } } } } } })); // Cowork/Code ?? ?묒뾽 ?좏삎 ?쎄린 ?꾩슜 ?쒖떆 if ((_activeTab == "Cowork" || _activeTab == "Code") && conv != null) { var catKey = conv.Category ?? ChatCategory.General; // ChatCategory ?먮뒗 ?꾨━?뗭뿉???꾩씠肄??쇰꺼 寃€?? string catSymbol = "\uE8BD", catLabel = catKey, catColor = "#6B7280"; var chatCat = ChatCategory.All.FirstOrDefault(c => c.Key == catKey); if (chatCat != default && chatCat.Key != ChatCategory.General) { catSymbol = chatCat.Symbol; catLabel = chatCat.Label; catColor = chatCat.Color; } else { var preset = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets) .FirstOrDefault(p => p.Category == catKey); if (preset != null) { catSymbol = preset.Symbol; catLabel = preset.Label; catColor = preset.Color; } } stack.Children.Add(CreateSeparator()); var infoSp = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(10, 4, 10, 4) }; try { var catBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(catColor)); infoSp.Children.Add(new TextBlock { Text = catSymbol, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = catBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), }); infoSp.Children.Add(new TextBlock { Text = catLabel, FontSize = 12, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, }); } catch { infoSp.Children.Add(new TextBlock { Text = catLabel, FontSize = 12, Foreground = primaryText }); } stack.Children.Add(infoSp); } // Chat ??쭔 遺꾨쪟 蹂€寃??쒖떆 (Cowork/Code ??? 遺꾨쪟 遺덊븘?? var showCategorySection = _activeTab == "Chat"; if (showCategorySection) { stack.Children.Add(CreateSeparator()); // 遺꾨쪟 ?ㅻ뜑 stack.Children.Add(new TextBlock { Text = "遺꾨쪟 蹂€寃?, FontSize = 10.5, Foreground = secondaryText, Margin = new Thickness(10, 4, 0, 4), FontWeight = FontWeights.SemiBold, }); var currentCategory = conv?.Category ?? ChatCategory.General; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; foreach (var (key, label, symbol, color) in ChatCategory.All) { var capturedKey = key; var isCurrentCat = capturedKey == currentCategory; // 移댄뀒怨좊━ ??ぉ (泥댄겕 ?쒖떆 ?ы븿) var catItem = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 7, 10, 7), Margin = new Thickness(0, 1, 0, 1), Cursor = Cursors.Hand, }; var catGrid = new Grid(); catGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(24) }); catGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); catGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(20) }); var catIcon = new TextBlock { Text = symbol, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = BrushFromHex(color), VerticalAlignment = VerticalAlignment.Center, }; Grid.SetColumn(catIcon, 0); catGrid.Children.Add(catIcon); var catText = new TextBlock { Text = label, FontSize = 12.5, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, FontWeight = isCurrentCat ? FontWeights.Bold : FontWeights.Normal, }; Grid.SetColumn(catText, 1); catGrid.Children.Add(catText); if (isCurrentCat) { var check = CreateSimpleCheck(accentBrush, 14); Grid.SetColumn(check, 2); catGrid.Children.Add(check); } catItem.Child = catGrid; catItem.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; }; catItem.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; catItem.MouseLeftButtonUp += (_, _) => { popup.IsOpen = false; var c = _storage.Load(conversationId); if (c != null) { c.Category = capturedKey; var preset = Services.PresetService.GetByCategory(capturedKey); if (preset != null) c.SystemCommand = preset.SystemPrompt; _storage.Save(c); lock (_convLock) { if (_currentConversation?.Id == conversationId) { _currentConversation.Category = capturedKey; if (preset != null) _currentConversation.SystemCommand = preset.SystemPrompt; } } // ?꾩옱 ?€?붿쓽 移댄뀒怨좊━媛€ 蹂€寃쎈릺硫??낅젰 ?덈궡 臾멸뎄??媛깆떊 bool isCurrent; lock (_convLock) { isCurrent = _currentConversation?.Id == conversationId; } if (isCurrent && preset != null && !string.IsNullOrEmpty(preset.Placeholder)) { _promptCardPlaceholder = preset.Placeholder; UpdateWatermarkVisibility(); if (string.IsNullOrEmpty(InputBox.Text)) { InputWatermark.Text = preset.Placeholder; InputWatermark.Visibility = Visibility.Visible; } } else if (isCurrent) { ClearPromptCardPlaceholder(); } RefreshConversationList(); } }; stack.Children.Add(catItem); } } // end showCategorySection stack.Children.Add(CreateSeparator()); // ??젣 stack.Children.Add(CreateMenuItem("\uE74D", "???€????젣", Brushes.IndianRed, () => { var result = CustomMessageBox.Show("???€?붾? ??젣?섏떆寃좎뒿?덇퉴?", "?€????젣", MessageBoxButton.YesNo, MessageBoxImage.Question); if (result != MessageBoxResult.Yes) return; _storage.Delete(conversationId); lock (_convLock) { if (_currentConversation?.Id == conversationId) { _currentConversation = null; MessagePanel.Children.Clear(); EmptyState.Visibility = Visibility.Visible; UpdateChatTitle(); } } RefreshConversationList(); })); container.Child = stack; popup.Child = container; popup.IsOpen = true; } // ?€?€?€ 寃€???€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void SearchBox_TextChanged(object sender, TextChangedEventArgs e) { RefreshConversationList(); } private static string FormatDate(DateTime dt) { var diff = DateTime.Now - dt; if (diff.TotalMinutes < 1) return "諛⑷툑 ??; if (diff.TotalHours < 1) return $"{(int)diff.TotalMinutes}遺???; if (diff.TotalDays < 1) return $"{(int)diff.TotalHours}?쒓컙 ??; if (diff.TotalDays < 7) return $"{(int)diff.TotalDays}????; return dt.ToString("MM/dd"); } private void UpdateChatTitle() { lock (_convLock) { ChatTitle.Text = _currentConversation?.Title ?? ""; } } // ?€?€?€ 硫붿떆吏€ ?뚮뜑留??€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void RenderMessages() { MessagePanel.Children.Clear(); ChatConversation? conv; lock (_convLock) conv = _currentConversation; if (conv == null) { EmptyState.Visibility = Visibility.Visible; return; } var items = new List(); items.AddRange(conv.Messages .Where(msg => msg.Role != "system") .Select(msg => new ConversationTimelineItem { Timestamp = msg.Timestamp, Message = msg, })); if (IsExecutionHistoryVisible()) { items.AddRange(conv.ExecutionEvents.Select(evt => new ConversationTimelineItem { Timestamp = evt.Timestamp, ExecutionEvent = evt, })); } if (items.Count == 0) { EmptyState.Visibility = Visibility.Visible; return; } EmptyState.Visibility = Visibility.Collapsed; foreach (var item in items.OrderBy(i => i.Timestamp)) { if (item.Message != null) { AddMessageBubble(item.Message.Role, item.Message.Content, animate: false, message: item.Message); continue; } if (item.ExecutionEvent != null) AddAgentEventBanner(ToAgentEvent(item.ExecutionEvent), animate: false); } _ = Dispatcher.InvokeAsync(() => MessageScroll.ScrollToEnd(), DispatcherPriority.Background); } private void AddMessageBubble(string role, string content, bool animate = true, ChatMessage? message = null) { var isUser = role == "user"; if (isUser) { // ?ъ슜?? ?곗륫 ?뺣젹, ?낆꽱??諛곌꼍 + ?몄쭛 踰꾪듉 var wrapper = new StackPanel { HorizontalAlignment = HorizontalAlignment.Right, MaxWidth = 540, Margin = new Thickness(120, 6, 40, 6), }; var bubble = new Border { Background = TryFindResource("AccentColor") as Brush ?? Brushes.Blue, CornerRadius = new CornerRadius(16, 4, 16, 16), Padding = new Thickness(16, 10, 16, 10), Child = new TextBlock { Text = content, FontSize = 13.5, Foreground = Brushes.White, TextWrapping = TextWrapping.Wrap, LineHeight = 21, } }; wrapper.Children.Add(bubble); // ?≪뀡 踰꾪듉 諛?(蹂듭궗 + ?몄쭛, hover ???쒖떆) var userActionBar = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Opacity = 0, Margin = new Thickness(0, 2, 0, 0), }; var capturedUserContent = content; var userBtnColor = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; userActionBar.Children.Add(CreateActionButton("\uE8C8", "蹂듭궗", userBtnColor, () => { try { Clipboard.SetText(capturedUserContent); } catch { } })); userActionBar.Children.Add(CreateActionButton("\uE70F", "?몄쭛", userBtnColor, () => EnterEditMode(wrapper, capturedUserContent))); // ?€?꾩뒪?ы봽 + ?≪뀡 諛? var userBottomBar = new Grid { Margin = new Thickness(0, 2, 0, 0) }; var timestamp = message?.Timestamp ?? DateTime.Now; userBottomBar.Children.Add(new TextBlock { Text = timestamp.ToString("HH:mm"), FontSize = 10, Opacity = 0.5, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0), }); userBottomBar.Children.Add(userActionBar); wrapper.Children.Add(userBottomBar); wrapper.MouseEnter += (_, _) => userActionBar.Opacity = 1; wrapper.MouseLeave += (_, _) => userActionBar.Opacity = 0; // ?고겢由???硫붿떆吏€ 而⑦뀓?ㅽ듃 硫붾돱 var userContent = content; wrapper.MouseRightButtonUp += (_, re) => { re.Handled = true; ShowMessageContextMenu(userContent, "user"); }; if (animate) ApplyMessageEntryAnimation(wrapper); MessagePanel.Children.Add(wrapper); } else { // ?댁떆?ㅽ꽩?? 醫뚯륫 ?뺣젹, ?ㅽ겕 諛곌꼍 var container = new StackPanel { HorizontalAlignment = HorizontalAlignment.Left, MaxWidth = GetMessageMaxWidth(), Margin = new Thickness(40, 8, 80, 8) }; if (animate) ApplyMessageEntryAnimation(container); // AI ?먯씠?꾪듃 ?대쫫 + ?꾩씠肄? var (agentName, agentSymbol, agentColor) = GetAgentIdentity(); var agentBrush = BrushFromHex(agentColor); var headerSp = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 4) }; // ?ㅼ씠?꾨が???щ낵 ?꾩씠肄?(?뚯쟾 ?좊땲硫붿씠?? var iconBlock = new TextBlock { Text = "??, FontSize = 13, Foreground = agentBrush, VerticalAlignment = VerticalAlignment.Center, RenderTransformOrigin = new Point(0.5, 0.5), RenderTransform = new RotateTransform(0), }; if (animate) { var spin = new System.Windows.Media.Animation.DoubleAnimation { From = 0, To = 360, Duration = TimeSpan.FromSeconds(1.2), EasingFunction = new System.Windows.Media.Animation.CubicEase { EasingMode = System.Windows.Media.Animation.EasingMode.EaseOut }, }; ((RotateTransform)iconBlock.RenderTransform).BeginAnimation(RotateTransform.AngleProperty, spin); } headerSp.Children.Add(iconBlock); headerSp.Children.Add(new TextBlock { Text = agentName, FontSize = 11, FontWeight = FontWeights.SemiBold, Foreground = agentBrush, Margin = new Thickness(6, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center, }); container.Children.Add(headerSp); // 留덊겕?ㅼ슫 ?뚮뜑留?(?뚯씪 寃쎈줈 媛뺤“ ?ㅼ젙 ?곕룞) var app = System.Windows.Application.Current as App; MarkdownRenderer.EnableFilePathHighlight = app?.SettingsService?.Settings.Llm.EnableFilePathHighlight ?? true; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue; var codeBgBrush = TryFindResource("HintBackground") as Brush ?? Brushes.DarkGray; var mdPanel = MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, codeBgBrush); mdPanel.Margin = new Thickness(0, 0, 0, 4); container.Children.Add(mdPanel); // ?≪뀡 踰꾪듉 諛?(蹂듭궗 / 醫뗭븘??/ ?レ뼱?? var actionBar = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Left, Margin = new Thickness(0, 6, 0, 0) }; var btnColor = (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray); var btnHoverColor = new SolidColorBrush(Color.FromRgb(0x8B, 0x90, 0xB0)); var capturedContent = content; actionBar.Children.Add(CreateActionButton("\uE8C8", "蹂듭궗", btnColor, () => { try { Clipboard.SetText(capturedContent); } catch { } })); actionBar.Children.Add(CreateActionButton("\uE72C", "?ㅼ떆 ?앹꽦", btnColor, () => _ = RegenerateLastAsync())); actionBar.Children.Add(CreateActionButton("\uE70F", "?섏젙 ???ъ떆??, btnColor, () => ShowRetryWithFeedbackInput())); AddLinkedFeedbackButtons(actionBar, btnColor, message); // ?€?꾩뒪?ы봽 var aiTimestamp = message?.Timestamp ?? DateTime.Now; actionBar.Children.Add(new TextBlock { Text = aiTimestamp.ToString("HH:mm"), FontSize = 10, Opacity = 0.5, Foreground = btnColor, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(8, 0, 0, 0), }); container.Children.Add(actionBar); // ?고겢由???硫붿떆吏€ 而⑦뀓?ㅽ듃 硫붾돱 var aiContent = content; container.MouseRightButtonUp += (_, re) => { re.Handled = true; ShowMessageContextMenu(aiContent, "assistant"); }; MessagePanel.Children.Add(container); } } // ?€?€?€ 而ㅼ뒪?€ 泥댄겕 ?꾩씠肄?(紐⑤뱺 ?앹뾽 硫붾돱 怨듯넻) ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ /// 而ㅼ뒪?€ 泥댄겕/誘몄꽑???꾩씠肄섏쓣 ?앹꽦?⑸땲?? Path ?꾪삎 湲곕컲, ?좏깮 ???ㅼ???諛붿슫???좊땲硫붿씠?? /// ?꾩옱 ?뚮쭏??泥댄겕 ?ㅽ??쇱쓣 諛섑솚?⑸땲?? private string GetCheckStyle() { var theme = (_settings.Settings.Launcher.Theme ?? "system").ToLowerInvariant(); return theme switch { "dark" or "system" => "circle", // ??+ 泥댄겕留덊겕, 諛붿슫?? "oled" => "glow", // ?ㅼ삩 湲€濡쒖슦 ?? ?섏씠?쒖씤 "light" => "roundrect", // ?κ렐 ?ш컖?? ?щ씪?대뱶?? "nord" => "diamond", // ?ㅼ씠?꾨が??留덈쫫紐?, ?ㅻТ???ㅼ??? "catppuccin" => "pill", // ??紐⑥뼇, ?ㅽ봽留?諛붿슫?? "monokai" => "square", // ?뺤궗媛곹삎, ???? "sepia" => "stamp", // ?꾩옣 ?ㅽ????? ?뚯쟾 ?깆옣 "alfred" => "minimal", // 誘몃땲硫€ ?? ?곗븘???섏씠?? "alfredlight" => "minimal", // 誘몃땲硫€ ?? ?곗븘???섏씠?? _ => "circle", }; } private FrameworkElement CreateCheckIcon(bool isChecked, Brush? accentBrush = null) { var accent = accentBrush ?? TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; // ?ы뵆 V 泥댄겕 ???좏깮 ??而щ윭 V, 誘몄꽑????鍮?怨듦컙 if (isChecked) { return CreateSimpleCheck(accent, 14); } // 誘몄꽑?? ?숈씪 ?ш린 鍮?怨듦컙 (?뺣젹 ?좎?) return new System.Windows.Shapes.Rectangle { Width = 14, Height = 14, Fill = Brushes.Transparent, Margin = new Thickness(0, 0, 10, 0), }; } /// ScaleTransform 諛붿슫???ㅼ????좊땲硫붿씠???ы띁. private static void AnimateScale(FrameworkElement el, double from, double to, int ms, IEasingFunction ease) { if (el.RenderTransform is TransformGroup tg) { var st = tg.Children.OfType().FirstOrDefault(); if (st != null) { var anim = new DoubleAnimation(from, to, TimeSpan.FromMilliseconds(ms)) { EasingFunction = ease }; st.BeginAnimation(ScaleTransform.ScaleXProperty, anim); st.BeginAnimation(ScaleTransform.ScaleYProperty, anim); return; } } if (el.RenderTransform is ScaleTransform scale) { var anim = new DoubleAnimation(from, to, TimeSpan.FromMilliseconds(ms)) { EasingFunction = ease }; scale.BeginAnimation(ScaleTransform.ScaleXProperty, anim); scale.BeginAnimation(ScaleTransform.ScaleYProperty, anim); } } /// 留덉슦???ㅻ쾭 ???댁쭩 ?뺣? + 蹂듦??섎뒗 ?몃쾭 ?좊땲硫붿씠?섏쓣 ?곸슜?⑸땲?? /// /// 留덉슦???ㅻ쾭 ???댁쭩 ?뺣??섎뒗 ?몃쾭 ?좊땲硫붿씠?? /// 二쇱쓽: ?몄젒 ?붿냼(??踰꾪듉, 媛€濡??섏뿴 硫붾돱 ???먮뒗 ?ъ슜 湲덉? ???뺣? ???댁썐 ?붿냼瑜?媛€由쎈땲?? /// ?낅┰??怨듦컙???덈뒗 踰꾪듉?먮쭔 ?곸슜?섏꽭?? /// private static void ApplyHoverScaleAnimation(FrameworkElement element, double hoverScale = 1.08) { // Loaded ?대깽?몄뿉???ㅽ뻾?댁빞 XAML Style??遊됱씤??Transform???덉쟾?섍쾶 援먯껜 媛€?? void EnsureTransform() { element.RenderTransformOrigin = new Point(0.5, 0.5); // 遊됱씤(frozen)??Transform?대㈃ ?덈줈 ?앹꽦?섏뿬 援먯껜 if (element.RenderTransform is not ScaleTransform || element.RenderTransform.IsFrozen) element.RenderTransform = new ScaleTransform(1, 1); } element.Loaded += (_, _) => EnsureTransform(); element.MouseEnter += (_, _) => { EnsureTransform(); var st = (ScaleTransform)element.RenderTransform; var grow = new DoubleAnimation(hoverScale, TimeSpan.FromMilliseconds(150)) { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } }; st.BeginAnimation(ScaleTransform.ScaleXProperty, grow); st.BeginAnimation(ScaleTransform.ScaleYProperty, grow); }; element.MouseLeave += (_, _) => { EnsureTransform(); var st = (ScaleTransform)element.RenderTransform; var shrink = new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(200)) { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } }; st.BeginAnimation(ScaleTransform.ScaleXProperty, shrink); st.BeginAnimation(ScaleTransform.ScaleYProperty, shrink); }; } /// 留덉슦???ㅻ쾭 ???띿뒪?멸? ?댁쭩 ?€?댁삤瑜대뒗 諛붿슫???좊땲硫붿씠?섏쓣 ?곸슜?⑸땲?? /// /// 留덉슦???ㅻ쾭 ???띿뒪?멸? ?댁쭩 ?€?댁삤瑜대뒗 諛붿슫???좊땲硫붿씠?? /// Scale怨??щ━ ?ш린媛€ 蹂€?섏? ?딆븘 ?몄젒 ?붿냼瑜?媛€由ъ? ?딆뒿?덈떎. /// private static void ApplyHoverBounceAnimation(FrameworkElement element, double bounceY = -2.5) { void EnsureTransform() { if (element.RenderTransform is not TranslateTransform || element.RenderTransform.IsFrozen) element.RenderTransform = new TranslateTransform(0, 0); } element.Loaded += (_, _) => EnsureTransform(); element.MouseEnter += (_, _) => { EnsureTransform(); var tt = (TranslateTransform)element.RenderTransform; tt.BeginAnimation(TranslateTransform.YProperty, new DoubleAnimation(bounceY, TimeSpan.FromMilliseconds(200)) { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } }); }; element.MouseLeave += (_, _) => { EnsureTransform(); var tt = (TranslateTransform)element.RenderTransform; tt.BeginAnimation(TranslateTransform.YProperty, new DoubleAnimation(0, TimeSpan.FromMilliseconds(250)) { EasingFunction = new ElasticEase { EasingMode = EasingMode.EaseOut, Oscillations = 1, Springiness = 10 } }); }; } /// ?ы뵆??V 泥댄겕 ?꾩씠肄섏쓣 ?앹꽦?⑸땲??(?붿옄???듭씪??. private static FrameworkElement CreateSimpleCheck(Brush color, double size = 14) { 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, StrokeStartLineCap = PenLineCap.Round, StrokeEndLineCap = PenLineCap.Round, StrokeLineJoin = PenLineJoin.Round, Width = size, Height = size, Margin = new Thickness(0, 0, 10, 0), VerticalAlignment = VerticalAlignment.Center, }; } /// ?앹뾽 硫붾돱 ??ぉ???몃쾭 諛곌꼍??+ 誘몄꽭 ?뺣? ?④낵瑜??곸슜?⑸땲?? private static void ApplyMenuItemHover(Border item) { var originalBg = item.Background?.Clone() ?? Brushes.Transparent; if (originalBg.CanFreeze) originalBg.Freeze(); item.RenderTransformOrigin = new Point(0.5, 0.5); item.RenderTransform = new ScaleTransform(1, 1); item.MouseEnter += (s, _) => { if (s is Border b) { // ?먮옒 諛곌꼍???щ챸?대㈃ 諛섑닾紐??곗깋, ?꾨땲硫?諛앷린 蹂€寃? if (originalBg is SolidColorBrush scb && scb.Color.A > 0x20) b.Opacity = 0.85; else b.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); } var st = item.RenderTransform as ScaleTransform; st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120))); st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120))); }; item.MouseLeave += (s, _) => { if (s is Border b) { b.Opacity = 1.0; b.Background = originalBg; } var st = item.RenderTransform as ScaleTransform; st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150))); st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150))); }; } private Button CreateActionButton(string symbol, string tooltip, Brush foreground, Action onClick) { var hoverBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var icon = new TextBlock { Text = symbol, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = foreground, VerticalAlignment = VerticalAlignment.Center }; var btn = new Button { Content = icon, Background = Brushes.Transparent, BorderThickness = new Thickness(0), Cursor = Cursors.Hand, Padding = new Thickness(6, 4, 6, 4), Margin = new Thickness(0, 0, 4, 0), ToolTip = tooltip }; btn.MouseEnter += (_, _) => icon.Foreground = hoverBrush; btn.MouseLeave += (_, _) => icon.Foreground = foreground; btn.Click += (_, _) => onClick(); ApplyHoverScaleAnimation(btn, 1.15); return btn; } /// 醫뗭븘???レ뼱???좉? ?쇰뱶諛?踰꾪듉 (?곹깭 ?곴뎄 ?€?? private Button CreateFeedbackButton(string outline, string filled, string tooltip, Brush normalColor, Brush activeColor, ChatMessage? message = null, string feedbackType = "", Action? resetSibling = null, Action? registerReset = null) { var hoverBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var isActive = message?.Feedback == feedbackType; var icon = new TextBlock { Text = isActive ? filled : outline, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = isActive ? activeColor : normalColor, VerticalAlignment = VerticalAlignment.Center, RenderTransformOrigin = new Point(0.5, 0.5), RenderTransform = new ScaleTransform(1, 1) }; var btn = new Button { Content = icon, Background = Brushes.Transparent, BorderThickness = new Thickness(0), Cursor = Cursors.Hand, Padding = new Thickness(6, 4, 6, 4), Margin = new Thickness(0, 0, 4, 0), ToolTip = tooltip }; // ?곷? 踰꾪듉??由ъ뀑?????덈룄濡??깅줉 registerReset?.Invoke(() => { isActive = false; icon.Text = outline; icon.Foreground = normalColor; }); btn.MouseEnter += (_, _) => { if (!isActive) icon.Foreground = hoverBrush; }; btn.MouseLeave += (_, _) => { if (!isActive) icon.Foreground = normalColor; }; btn.Click += (_, _) => { 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? conv; lock (_convLock) conv = _currentConversation; if (conv != null) _storage.Save(conv); } catch { } } // 諛붿슫???좊땲硫붿씠?? var scale = (ScaleTransform)icon.RenderTransform; var bounce = new DoubleAnimation(1.3, 1.0, TimeSpan.FromMilliseconds(250)) { EasingFunction = new ElasticEase { EasingMode = EasingMode.EaseOut, Oscillations = 1, Springiness = 5 } }; scale.BeginAnimation(ScaleTransform.ScaleXProperty, bounce); scale.BeginAnimation(ScaleTransform.ScaleYProperty, bounce); }; return btn; } /// 醫뗭븘???レ뼱??踰꾪듉???곹샇 諛고?濡??곌껐?섏뿬 異붽? private void AddLinkedFeedbackButtons(StackPanel actionBar, Brush btnColor, ChatMessage? message) { // resetSibling???섏쨷???ㅼ젙?섎?濡?Action ?섑띁濡?媛꾩젒 李몄“ Action? resetLikeAction = null; Action? resetDislikeAction = null; var likeBtn = CreateFeedbackButton("\uE8E1", "\uEB51", "醫뗭븘??, btnColor, new SolidColorBrush(Color.FromRgb(0x38, 0xA1, 0x69)), message, "like", resetSibling: () => resetDislikeAction?.Invoke(), registerReset: reset => resetLikeAction = reset); var dislikeBtn = CreateFeedbackButton("\uE8E0", "\uEB50", "?レ뼱??, btnColor, new SolidColorBrush(Color.FromRgb(0xE5, 0x3E, 0x3E)), message, "dislike", resetSibling: () => resetLikeAction?.Invoke(), registerReset: reset => resetDislikeAction = reset); actionBar.Children.Add(likeBtn); actionBar.Children.Add(dislikeBtn); } // ?€?€?€ 硫붿떆吏€ ?깆옣 ?좊땲硫붿씠???€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private static void ApplyMessageEntryAnimation(FrameworkElement element) { element.Opacity = 0; element.RenderTransform = new TranslateTransform(0, 16); element.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(350)) { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } }); ((TranslateTransform)element.RenderTransform).BeginAnimation( TranslateTransform.YProperty, new DoubleAnimation(16, 0, TimeSpan.FromMilliseconds(400)) { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } }); } // ?€?€?€ 硫붿떆吏€ ?몄쭛 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private bool _isEditing; // ?몄쭛 紐⑤뱶 以묐났 諛⑹? private void EnterEditMode(StackPanel wrapper, string originalText) { if (_isStreaming || _isEditing) return; _isEditing = true; // wrapper ?꾩튂(?몃뜳?? 湲곗뼲 var idx = MessagePanel.Children.IndexOf(wrapper); if (idx < 0) { _isEditing = false; return; } // ?몄쭛 UI ?앹꽦 var editPanel = new StackPanel { HorizontalAlignment = HorizontalAlignment.Right, MaxWidth = 540, Margin = wrapper.Margin, }; var editBox = new TextBox { Text = originalText, FontSize = 13.5, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White, Background = TryFindResource("ItemBackground") as Brush ?? Brushes.DarkGray, CaretBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue, BorderBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue, BorderThickness = new Thickness(1.5), Padding = new Thickness(14, 10, 14, 10), TextWrapping = TextWrapping.Wrap, AcceptsReturn = false, MaxHeight = 200, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, }; // ?κ렐 紐⑥꽌由? var editBorder = new Border { CornerRadius = new CornerRadius(14), Child = editBox, ClipToBounds = true, }; editPanel.Children.Add(editBorder); // 踰꾪듉 諛? var btnBar = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 6, 0, 0), }; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue; var secondaryBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; // 痍⑥냼 踰꾪듉 var cancelBtn = new Button { Content = new TextBlock { Text = "痍⑥냼", FontSize = 12, Foreground = secondaryBrush }, Background = Brushes.Transparent, BorderThickness = new Thickness(0), Cursor = Cursors.Hand, Padding = new Thickness(12, 5, 12, 5), Margin = new Thickness(0, 0, 6, 0), }; cancelBtn.Click += (_, _) => { _isEditing = false; if (idx >= 0 && idx < MessagePanel.Children.Count) MessagePanel.Children[idx] = wrapper; // ?먮옒 踰꾨툝 蹂듭썝 }; btnBar.Children.Add(cancelBtn); // ?꾩넚 踰꾪듉 var sendBtn = new Button { Cursor = Cursors.Hand, Padding = new Thickness(0), }; sendBtn.Template = (ControlTemplate)System.Windows.Markup.XamlReader.Parse( "" + "" + "" + ""); sendBtn.Click += (_, _) => { var newText = editBox.Text.Trim(); if (!string.IsNullOrEmpty(newText)) _ = SubmitEditAsync(idx, newText); }; btnBar.Children.Add(sendBtn); editPanel.Children.Add(btnBar); // 湲곗〈 wrapper ??editPanel 援먯껜 MessagePanel.Children[idx] = editPanel; // Enter ?ㅻ줈???꾩넚 editBox.KeyDown += (_, ke) => { if (ke.Key == Key.Enter && Keyboard.Modifiers == ModifierKeys.None) { ke.Handled = true; var newText = editBox.Text.Trim(); if (!string.IsNullOrEmpty(newText)) _ = SubmitEditAsync(idx, newText); } if (ke.Key == Key.Escape) { 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; } // bubbleIndex???대떦?섎뒗 user 硫붿떆吏€ 李얘린 // UI??children 以?user 硫붿떆吏€媛€ ?꾨땶 寃?system)?€ ?ㅽ궢?? // ?곗씠??紐⑤뜽?먯꽌 ?대떦 ?꾩튂??user 硫붿떆吏€ 李얘린 int userMsgIdx = -1; int uiIdx = 0; lock (_convLock) { for (int i = 0; i < conv.Messages.Count; i++) { if (conv.Messages[i].Role == "system") continue; 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); } // UI?먯꽌 ?몄쭛??踰꾨툝 ?댄썑 紐⑤몢 ?쒓굅 while (MessagePanel.Children.Count > bubbleIndex + 1) MessagePanel.Children.RemoveAt(MessagePanel.Children.Count - 1); // ?몄쭛??硫붿떆吏€瑜???踰꾨툝濡?援먯껜 MessagePanel.Children.RemoveAt(bubbleIndex); AddMessageBubble("user", newText, animate: false); // 留덉?留??꾩튂???쎌엯?섎룄濡?議곗젙 (AddMessageBubble?€ ?앹뿉 異붽??? // bubbleIndex媛€ ?앹씠 ?꾨땲硫??대룞 ????寃쎌슦 ?댄썑媛€ ???쒓굅?섏뿀?쇰?濡??앹뿉 異붽??? // AI ?ъ쓳?? await SendRegenerateAsync(conv); try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"?€???€???ㅽ뙣: {ex.Message}"); } RefreshConversationList(); } // ?€?€?€ ?ㅽ듃由щ컢 而ㅼ꽌 源쒕묀??+ AI ?꾩씠肄??꾩뒪 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void StopAiIconPulse() { if (_aiIconPulseStopped || _activeAiIcon == null) return; _activeAiIcon.BeginAnimation(UIElement.OpacityProperty, null); _activeAiIcon.Opacity = 1.0; _activeAiIcon = null; _aiIconPulseStopped = true; } private void CursorTimer_Tick(object? sender, EventArgs e) { _cursorVisible = !_cursorVisible; // 而ㅼ꽌 ?곹깭留??좉? ???ㅼ젣 ?띿뒪??媛깆떊?€ _typingTimer媛€ ?대떦 if (_activeStreamText != null && _displayedLength > 0) { var displayed = _cachedStreamContent.Length > 0 ? _cachedStreamContent[..Math.Min(_displayedLength, _cachedStreamContent.Length)] : ""; _activeStreamText.Text = displayed + (_cursorVisible ? "\u258c" : " "); } } private void ElapsedTimer_Tick(object? sender, EventArgs e) { var elapsed = DateTime.UtcNow - _streamStartTime; var sec = (int)elapsed.TotalSeconds; if (_elapsedLabel != null) _elapsedLabel.Text = $"{sec}s"; // ?섎떒 ?곹깭諛??쒓컙 媛깆떊 if (StatusElapsed != null) StatusElapsed.Text = $"{sec}珥?; } private void TypingTimer_Tick(object? sender, EventArgs e) { if (_activeStreamText == null || string.IsNullOrEmpty(_cachedStreamContent)) return; var targetLen = _cachedStreamContent.Length; if (_displayedLength >= targetLen) return; // 踰꾪띁???볦씤 誘명몴??湲€???섏뿉 ?곕씪 ?띾룄 ?곸쓳 var pending = targetLen - _displayedLength; int step; if (pending > 200) step = Math.Min(pending / 5, 40); // ?€??踰꾪띁: 鍮좊Ⅴ寃??곕씪?↔린 else if (pending > 50) step = Math.Min(pending / 4, 15); // 以묎컙 踰꾪띁: ?곷떦??媛€?? else step = Math.Min(3, pending); // ?뚮웾: ?먯뿰?ㅻ윭??1~3?? _displayedLength += step; var displayed = _cachedStreamContent[.._displayedLength]; _activeStreamText.Text = displayed + (_cursorVisible ? "\u258c" : " "); // ?ㅽ듃由щ컢 以묒뿉??利됱떆 ?ㅽ겕濡?(遺€?쒕윭???좊땲硫붿씠?섏? 吏€???좊컻) if (!_userScrolled) MessageScroll.ScrollToVerticalOffset(MessageScroll.ScrollableHeight); } // ?€?€?€ ?꾩넚 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ public void SendInitialMessage(string message) { StartNewConversation(); InputBox.Text = message; _ = SendMessageAsync(); } private void StartNewConversation() { // ?꾩옱 ?€?붽? ?덉쑝硫??€???????€???쒖옉 lock (_convLock) { _sessionState.SaveCurrentConversation(_storage, _activeTab); _currentConversation = _sessionState.LoadOrCreateConversation(_activeTab, _storage, _settings); _currentConversation.Messages.Clear(); _currentConversation.ExecutionEvents.Clear(); _currentConversation.DraftQueue.Clear(); _currentConversation.DraftQueueItems.Clear(); _currentConversation.ShowExecutionHistory = true; _currentConversation.Id = Guid.NewGuid().ToString("N"); _currentConversation.Title = "???€??; _currentConversation.CreatedAt = DateTime.Now; _currentConversation.UpdatedAt = DateTime.Now; _currentConversation.Preview = ""; _currentConversation.ParentId = null; _currentConversation.BranchLabel = null; _currentConversation.BranchAtIndex = null; } // ??湲곗뼲 珥덇린??(???€?붿씠誘€濡? _sessionState.RememberConversation(_activeTab, null); MessagePanel.Children.Clear(); EmptyState.Visibility = Visibility.Visible; _attachedFiles.Clear(); RefreshAttachedFilesUI(); UpdateChatTitle(); RefreshConversationList(); UpdateFolderBar(); if (_activeTab == "Cowork") BuildBottomBar(); } /// ?ㅼ젙???€?λ맂 ??퀎 留덉?留??€??ID瑜?蹂듭썝?섍퀬, ?꾩옱 ??쓽 ?€?붾? 濡쒕뱶?⑸땲?? private void RestoreLastConversations() { _sessionState.Load(_settings); _currentConversation = null; // ?꾩옱 ?쒖꽦 ??쓽 ?€?붾? 利됱떆 濡쒕뱶 var currentTabId = _sessionState.GetConversationId(_activeTab); if (!string.IsNullOrEmpty(currentTabId)) { var conv = _storage.Load(currentTabId); if (conv != null) { if (string.IsNullOrEmpty(conv.Tab)) conv.Tab = _activeTab; lock (_convLock) _currentConversation = conv; LoadConversationSettings(); RenderMessages(); UpdateChatTitle(); UpdateFolderBar(); } } else { lock (_convLock) _currentConversation = _sessionState.LoadOrCreateConversation(_activeTab, _storage, _settings); } } /// ?꾩옱 _tabConversationId瑜??ㅼ젙???€?ν빀?덈떎. private void SaveLastConversations() { try { _sessionState.Save(_settings); } catch { } } private async void BtnSend_Click(object sender, RoutedEventArgs e) => await SendMessageAsync(); private async void InputBox_PreviewKeyDown(object sender, KeyEventArgs e) { // Ctrl+V: ?대┰蹂대뱶 ?대?吏€ 遺숈뿬?j린 if (e.Key == Key.V && Keyboard.Modifiers.HasFlag(ModifierKeys.Control)) { if (TryPasteClipboardImage()) { e.Handled = true; return; } // ?대?吏€媛€ ?꾨땲硫?湲곕낯 ?띿뒪??遺숈뿬?j린濡??꾩엫 } if (e.Key == Key.Enter) { if (Keyboard.Modifiers.HasFlag(ModifierKeys.Shift)) { // Shift+Enter ??以꾨컮轅?(AcceptsReturn=true?대?濡?湲곕낯 ?숈옉?쇰줈 ?꾩엫) return; } // ?щ옒???앹뾽???대젮 ?덉쑝硫??좏깮????ぉ ?ㅽ뻾 if (SlashPopup.IsOpen && _slashSelectedIndex >= 0) { e.Handled = true; ExecuteSlashSelectedItem(); return; } // /help 吏곸젒 ?낅젰 ???꾩?留?李??쒖떆 if (InputBox.Text.Trim().Equals("/help", StringComparison.OrdinalIgnoreCase)) { e.Handled = true; InputBox.Text = ""; SlashPopup.IsOpen = false; ShowSlashHelpWindow(); return; } // Enter留???硫붿떆吏€ ?꾩넚 e.Handled = true; await SendMessageAsync(); } } /// ?대┰蹂대뱶???대?吏€媛€ ?덉쑝硫?遺숈뿬?j린. ?깃났 ??true. private bool TryPasteClipboardImage() { if (!_settings.Settings.Llm.EnableImageInput) return false; if (!Clipboard.ContainsImage()) return false; try { var img = Clipboard.GetImage(); if (img == null) return false; // base64 ?몄퐫?? var encoder = new System.Windows.Media.Imaging.PngBitmapEncoder(); encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(img)); using var ms = new System.IO.MemoryStream(); encoder.Save(ms); var bytes = ms.ToArray(); // ?ш린 ?쒗븳 ?뺤씤 var maxKb = _settings.Settings.Llm.MaxImageSizeKb; if (maxKb <= 0) maxKb = 5120; if (bytes.Length > maxKb * 1024) { CustomMessageBox.Show($"?대?吏€媛€ ?덈Т ?쎈땲??({bytes.Length / 1024}KB, 理쒕? {maxKb}KB).", "?대?吏€ ?ш린 珥덇낵", MessageBoxButton.OK, MessageBoxImage.Warning); return true; // 泥섎━??(?먮윭?댁?留??대?吏€?€?? } var base64 = Convert.ToBase64String(bytes); var attachment = new ImageAttachment { Base64 = base64, MimeType = "image/png", FileName = $"clipboard_{DateTime.Now:HHmmss}.png", }; _pendingImages.Add(attachment); AddImagePreview(attachment, img); return true; } catch (Exception ex) { Services.LogService.Debug($"?대┰蹂대뱶 ?대?吏€ 遺숈뿬?j린 ?ㅽ뙣: {ex.Message}"); return false; } } /// ?대?吏€ 誘몃━蹂닿린 UI 異붽?. private void AddImagePreview(ImageAttachment attachment, System.Windows.Media.Imaging.BitmapSource? thumbnail = null) { var secondaryBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var hintBg = TryFindResource("HintBackground") as Brush ?? Brushes.LightGray; var chip = new Border { Background = hintBg, CornerRadius = new CornerRadius(6), Padding = new Thickness(4), Margin = new Thickness(0, 0, 4, 4), }; var sp = new StackPanel { Orientation = Orientation.Horizontal }; // ?몃꽕???대?吏€ if (thumbnail != null) { sp.Children.Add(new System.Windows.Controls.Image { Source = thumbnail, MaxHeight = 48, MaxWidth = 64, Stretch = Stretch.Uniform, Margin = new Thickness(0, 0, 4, 0), }); } else { // base64?먯꽌 ?몃꽕???앹꽦 sp.Children.Add(new TextBlock { Text = "\uE8B9", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 16, Foreground = secondaryBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(2, 0, 4, 0), }); } sp.Children.Add(new TextBlock { Text = attachment.FileName, FontSize = 10, Foreground = secondaryBrush, VerticalAlignment = VerticalAlignment.Center, MaxWidth = 100, TextTrimming = TextTrimming.CharacterEllipsis, }); var capturedAttachment = attachment; var capturedChip = chip; var removeBtn = new Button { Content = new TextBlock { Text = "\uE711", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 8, Foreground = secondaryBrush }, Background = Brushes.Transparent, BorderThickness = new Thickness(0), Cursor = Cursors.Hand, Padding = new Thickness(4, 2, 4, 2), Margin = new Thickness(2, 0, 0, 0), }; removeBtn.Click += (_, _) => { _pendingImages.Remove(capturedAttachment); AttachedFilesPanel.Items.Remove(capturedChip); if (_pendingImages.Count == 0 && _attachedFiles.Count == 0) AttachedFilesPanel.Visibility = Visibility.Collapsed; }; sp.Children.Add(removeBtn); chip.Child = sp; AttachedFilesPanel.Items.Add(chip); AttachedFilesPanel.Visibility = Visibility.Visible; } // ?€?€?€ ?щ옒??紐낅졊???€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ // ?€?€ ?щ옒??紐낅졊???앹뾽 ?섏씠吏??€?€ private int SlashPageSize => Math.Clamp(_settings.Settings.Llm.SlashPopupPageSize, 3, 20); private List<(string Cmd, string Label, bool IsSkill)> _slashAllMatches = []; private int _slashPageOffset = 0; private int _slashSelectedIndex = -1; // ?앹뾽 ???ㅻ낫???좏깮 ?몃뜳??(?섏씠吏€ ???곷?) // ?€?€ ?щ옒??紐낅졊??移??€?€ private string? _activeSlashCmd = null; // ?€?€ ?щ옒??紐낅졊??(??퀎 遺꾨쪟) ?€?€ /// 怨듯넻 ?щ옒??紐낅졊????紐⑤뱺 ??뿉???ъ슜 媛€?? private static readonly Dictionary SlashCommands = new(StringComparer.OrdinalIgnoreCase) { // 怨듯넻 ["/summary"] = ("Summary", "?ъ슜?먭? ?쒓났???댁슜???듭떖 ?ъ씤???꾩<濡?媛꾧껐?섍쾶 ?붿빟??二쇱꽭?? 遺덈┸ ?ъ씤???뺤떇???ъ슜?섏꽭??", "all"), ["/translate"] = ("Translate", "?ъ슜?먭? ?쒓났???띿뒪?몃? ?곸뼱濡?踰덉뿭??二쇱꽭?? ?먮Ц???ㅺ낵 ?섏븰?ㅻ? ?좎??섏꽭??", "all"), ["/explain"] = ("Explain", "?ъ슜?먭? ?쒓났???댁슜???쎄퀬 ?먯꽭?섍쾶 ?ㅻ챸??二쇱꽭?? ?꾩슂?섎㈃ ?덉떆瑜??ы븿?섏꽭??", "all"), ["/fix"] = ("Fix", "?ъ슜?먭? ?쒓났???띿뒪?몄쓽 留욎땄踰? 臾몃쾿, ?먯뿰?ㅻ윭???쒗쁽??援먯젙??二쇱꽭?? ?섏젙 ?ы빆??紐낇솗???쒖떆?섏꽭??", "all"), // Cowork/Code ?꾩슜 ["/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 void InputBox_TextChanged(object sender, TextChangedEventArgs e) { UpdateWatermarkVisibility(); var text = InputBox.Text; // 移⑹씠 ?쒖꽦?붾맂 ?곹깭?먯꽌 ?ъ슜?먭? /瑜??€?댄븨?섎㈃ 移??댁젣 if (_activeSlashCmd != null && text.StartsWith("/")) HideSlashChip(restoreText: false); if (text.StartsWith("/") && !text.Contains(' ')) { // ??퀎 ?꾪꽣留? Chat ??"all"留? Cowork/Code ??"all" + "dev" bool isDev = _activeTab is "Cowork" or "Code"; // ?댁옣 ?щ옒??紐낅졊??留ㅼ묶 (???꾪꽣) var matches = SlashCommands .Where(kv => kv.Key.StartsWith(text, StringComparison.OrdinalIgnoreCase)) .Where(kv => kv.Value.Tab == "all" || (isDev && kv.Value.Tab == "dev")) .Select(kv => (Cmd: kv.Key, Label: kv.Value.Label, IsSkill: false)) .ToList(); // ?ㅽ궗 ?щ옒??紐낅졊??留ㅼ묶 (??퀎 ?꾪꽣) if (_settings.Settings.Llm.EnableSkillSystem) { var skillMatches = SkillService.MatchSlashCommand(text) .Where(s => s.IsVisibleInTab(_activeTab)) .Select(s => (Cmd: "/" + s.Name, Label: s.IsAvailable ? s.Label : $"{s.Label} {s.UnavailableHint}", IsSkill: true, Available: s.IsAvailable)); foreach (var sm in skillMatches) matches.Add((sm.Cmd, sm.Label, sm.IsSkill)); } if (matches.Count > 0) { // 利먭꺼李얘린瑜??곷떒??怨좎젙 ?뺣젹 var favorites = _settings.Settings.Llm.FavoriteSlashCommands; if (favorites.Count > 0) { matches = matches .OrderByDescending(m => favorites.Contains(m.Cmd, StringComparer.OrdinalIgnoreCase)) .ToList(); } _slashAllMatches = matches; _slashPageOffset = 0; _slashSelectedIndex = -1; RenderSlashPage(); SlashPopup.IsOpen = true; return; } } SlashPopup.IsOpen = false; } /// ?꾩옱 ?섏씠吏€???щ옒??紐낅졊????ぉ???뚮뜑留곹빀?덈떎. private void RenderSlashPage() { SlashItems.Items.Clear(); var total = _slashAllMatches.Count; var start = _slashPageOffset; var end = Math.Min(start + SlashPageSize, total); // ???붿궡?? if (start > 0) { SlashNavUp.Visibility = Visibility.Visible; SlashNavUpText.Text = $"?? ?꾨줈 {start}媛?; } else SlashNavUp.Visibility = Visibility.Collapsed; // ?꾩씠???뚮뜑留? for (int i = start; i < end; i++) { var (cmd, label, isSkill) = _slashAllMatches[i]; var capturedCmd = cmd; var skillDef = isSkill ? SkillService.Find(cmd.TrimStart('/')) : null; var skillAvailable = skillDef?.IsAvailable ?? true; var isFav = _settings.Settings.Llm.FavoriteSlashCommands .Contains(cmd, StringComparer.OrdinalIgnoreCase); var pageLocalIndex = i - start; var isSelected = pageLocalIndex == _slashSelectedIndex; var hoverBrushItem = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.LightGray; var item = new Border { Background = isSelected ? hoverBrushItem : Brushes.Transparent, CornerRadius = new CornerRadius(6), Padding = new Thickness(10, 6, 10, 6), Margin = new Thickness(0, 1, 0, 1), Cursor = skillAvailable ? Cursors.Hand : Cursors.Arrow, Opacity = skillAvailable ? 1.0 : 0.5, }; var itemGrid = new Grid(); itemGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); itemGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var stack = new StackPanel(); var headerRow = new StackPanel { Orientation = Orientation.Horizontal }; if (isSkill) { headerRow.Children.Add(new TextBlock { Text = "\uE768 ", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 11, Foreground = skillAvailable ? TryFindResource("AccentColor") as Brush ?? Brushes.Blue : TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, VerticalAlignment = VerticalAlignment.Center, }); } if (isFav) { headerRow.Children.Add(new TextBlock { Text = "\uE735 ", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 10, Foreground = new SolidColorBrush(Color.FromRgb(0xF5, 0x9E, 0x0B)), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 2, 0), }); } headerRow.Children.Add(new TextBlock { Text = cmd, FontSize = 13, FontWeight = FontWeights.SemiBold, Foreground = skillAvailable ? TryFindResource("AccentColor") as Brush ?? Brushes.Blue : TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, VerticalAlignment = VerticalAlignment.Center, }); headerRow.Children.Add(new TextBlock { Text = $" ??{label}", FontSize = 12, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, VerticalAlignment = VerticalAlignment.Center, }); stack.Children.Add(headerRow); stack.Children.Add(new TextBlock { Text = isSkill ? (skillDef?.Description ?? label) : "?댁옣 紐낅졊?대? 鍮좊Ⅴ寃??곸슜?????덉뒿?덈떎.", FontSize = 11.5, Margin = new Thickness(0, 4, 0, 0), Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, TextTrimming = TextTrimming.CharacterEllipsis, }); if (isSkill && skillDef != null) { var metaRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 6, 0, 0), }; var isSystemSkill = skillDef.FilePath.StartsWith( System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "skills"), StringComparison.OrdinalIgnoreCase); metaRow.Children.Add(CreateMetaChip( isSystemSkill ? "?쒖뒪?? : "?ъ슜??, isSystemSkill ? "#EEF2FF" : "#ECFDF5", isSystemSkill ? "#4338CA" : "#047857")); metaRow.Children.Add(CreateMetaChip( skillDef.IsStandardFormat ? "SKILL.md" : "legacy", "#F3F4F6", "#4B5563")); if (!string.IsNullOrWhiteSpace(skillDef.Requires)) { metaRow.Children.Add(CreateMetaChip( skillDef.IsAvailable ? $"?섏〈??{skillDef.Requires}" : $"{skillDef.Requires} ?꾩슂", skillDef.IsAvailable ? "#E0F2FE" : "#FEF3C7", skillDef.IsAvailable ? "#0369A1" : "#B45309")); } stack.Children.Add(metaRow); } Grid.SetColumn(stack, 0); itemGrid.Children.Add(stack); // 利먭꺼李얘린 ?좉? 蹂??꾩씠肄? var favCapturedCmd = cmd; var favBtn = new Border { Width = 24, Height = 24, CornerRadius = new CornerRadius(4), Background = Brushes.Transparent, Cursor = Cursors.Hand, VerticalAlignment = VerticalAlignment.Center, Child = new TextBlock { Text = isFav ? "\uE735" : "\uE734", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 11, Foreground = isFav ? new SolidColorBrush(Color.FromRgb(0xF5, 0x9E, 0x0B)) : TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, Opacity = isFav ? 1.0 : 0.4, }, }; favBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.8; favBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0; favBtn.MouseLeftButtonDown += (_, me) => { me.Handled = true; // ?꾩씠???대┃ ?대깽??諛⑹? ToggleSlashFavorite(favCapturedCmd); }; Grid.SetColumn(favBtn, 1); itemGrid.Children.Add(favBtn); item.Child = itemGrid; if (skillAvailable) { var hoverBrush = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.LightGray; item.MouseEnter += (_, _) => item.Background = hoverBrush; item.MouseLeave += (_, _) => item.Background = Brushes.Transparent; item.MouseLeftButtonDown += (_, _) => { SlashPopup.IsOpen = false; if (capturedCmd.Equals("/help", StringComparison.OrdinalIgnoreCase)) { InputBox.Text = ""; ShowSlashHelpWindow(); return; } // 移??쒖떆: 紐낅졊?대? 移⑹쑝濡? InputBox??鍮??띿뒪?몃줈 ShowSlashChip(capturedCmd); InputBox.Focus(); }; } SlashItems.Items.Add(item); } // ?꾨옒 ?붿궡?? if (end < total) { SlashNavDown.Visibility = Visibility.Visible; SlashNavDownText.Text = $"?? ?꾨옒濡?{total - end}媛?; } else SlashNavDown.Visibility = Visibility.Collapsed; } /// ?щ옒???앹뾽 留덉슦?????ㅽ겕濡?泥섎━. private void SlashPopup_PreviewMouseWheel(object sender, MouseWheelEventArgs e) { e.Handled = true; SlashPopup_ScrollByDelta(e.Delta); } /// ?щ옒???앹뾽??Delta 諛⑺뼢?쇰줈 ?ㅽ겕濡ㅽ빀?덈떎. private void SlashPopup_ScrollByDelta(int delta) { if (_slashAllMatches.Count == 0) return; var pageItemCount = Math.Min(SlashPageSize, _slashAllMatches.Count - _slashPageOffset); if (delta > 0) // ?꾨줈 ?ㅽ겕濡?(Up ?? { if (_slashSelectedIndex > 0) _slashSelectedIndex--; else if (_slashSelectedIndex == 0 && _slashPageOffset > 0) { _slashPageOffset = Math.Max(0, _slashPageOffset - 1); _slashSelectedIndex = 0; } } else // ?꾨옒濡??ㅽ겕濡?(Down ?? { if (_slashSelectedIndex < 0) { // 珥덇린 ?곹깭: 泥?踰덉㎏ ??ぉ ?좏깮 _slashSelectedIndex = 0; } else if (_slashSelectedIndex < pageItemCount - 1) _slashSelectedIndex++; else if (_slashPageOffset + SlashPageSize < _slashAllMatches.Count) { _slashPageOffset++; _slashSelectedIndex = Math.Min(SlashPageSize - 1, _slashAllMatches.Count - _slashPageOffset - 1); } } RenderSlashPage(); } /// ?ㅻ낫?쒕줈 ?좏깮???щ옒???꾩씠?쒖쓣 ?ㅽ뻾?⑸땲?? private void ExecuteSlashSelectedItem() { var absoluteIdx = _slashPageOffset + _slashSelectedIndex; if (absoluteIdx < 0 || absoluteIdx >= _slashAllMatches.Count) return; var (cmd, _, isSkill) = _slashAllMatches[absoluteIdx]; var skillDef = isSkill ? SkillService.Find(cmd.TrimStart('/')) : null; var skillAvailable = skillDef?.IsAvailable ?? true; if (!skillAvailable) return; SlashPopup.IsOpen = false; _slashSelectedIndex = -1; if (cmd.Equals("/help", StringComparison.OrdinalIgnoreCase)) { InputBox.Text = ""; ShowSlashHelpWindow(); return; } ShowSlashChip(cmd); InputBox.Focus(); } /// ?щ옒??紐낅졊??利먭꺼李얘린瑜??좉??섍퀬 ?ㅼ젙???€?ν빀?덈떎. private void ToggleSlashFavorite(string cmd) { var favs = _settings.Settings.Llm.FavoriteSlashCommands; var existing = favs.FirstOrDefault(f => f.Equals(cmd, StringComparison.OrdinalIgnoreCase)); if (existing != null) favs.Remove(existing); else favs.Add(cmd); _settings.Save(); // ?앹뾽 ?덈줈怨좎묠: TextChanged ?대깽?몃? ?몃━嫄고븯???앹뾽 ?щ젋?붾쭅 var currentText = InputBox.Text; InputBox.TextChanged -= InputBox_TextChanged; InputBox.Text = ""; InputBox.TextChanged += InputBox_TextChanged; InputBox.Text = currentText; } /// ?щ옒??紐낅졊??移⑹쓣 ?쒖떆?섍퀬 InputBox瑜?鍮꾩썎?덈떎. private void ShowSlashChip(string cmd) { _activeSlashCmd = cmd; SlashChipText.Text = cmd; SlashCommandChip.Visibility = Visibility.Visible; // 移??덈퉬 痢≪젙 ??InputBox ?쇱そ ?щ갚 議곗젙 SlashCommandChip.UpdateLayout(); var chipRight = SlashCommandChip.Margin.Left + SlashCommandChip.ActualWidth + 6; InputBox.Padding = new Thickness(chipRight, 10, 14, 10); InputBox.Text = ""; } /// ?щ옒??紐낅졊??移⑹쓣 ?④퉩?덈떎. /// true?대㈃ InputBox??紐낅졊???띿뒪?몃? 蹂듭썝?⑸땲?? private void HideSlashChip(bool restoreText = false) { if (_activeSlashCmd == null) return; var prev = _activeSlashCmd; _activeSlashCmd = null; SlashCommandChip.Visibility = Visibility.Collapsed; InputBox.Padding = new Thickness(14, 10, 14, 10); if (restoreText) { InputBox.Text = prev + " "; InputBox.CaretIndex = InputBox.Text.Length; } } /// ?щ옒??紐낅졊?대? 媛먯??섏뿬 ?쒖뒪???꾨\?꾪듃?€ ?ъ슜???띿뒪?몃? 遺꾨━?⑸땲?? private static (string? slashSystem, string userText) ParseSlashCommand(string input) { // ?댁옣 紐낅졊???곗꽑 foreach (var (cmd, (_, prompt, _)) in SlashCommands) { if (input.StartsWith(cmd, StringComparison.OrdinalIgnoreCase)) { // __HELP__???뱀닔 泥섎━ (ParseSlashCommand?먯꽌??臾댁떆) if (prompt == "__HELP__") return (null, input); var rest = input[cmd.Length..].Trim(); return (prompt, string.IsNullOrEmpty(rest) ? cmd : rest); } } // ?ㅽ궗 紐낅졊??留ㅼ묶 foreach (var skill in SkillService.Skills) { var slashCmd = "/" + skill.Name; if (input.StartsWith(slashCmd, StringComparison.OrdinalIgnoreCase)) { var rest = input[slashCmd.Length..].Trim(); return (skill.SystemPrompt, string.IsNullOrEmpty(rest) ? skill.Label : rest); } } return (null, input); } // ?€?€?€ ?쒕옒洹????쒕∼ AI ?≪뀡 ?앹뾽 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private static readonly Dictionary> DropActions = new(StringComparer.OrdinalIgnoreCase) { ["code"] = [ ("肄붾뱶 由щ럭", "\uE943", "泥⑤???肄붾뱶瑜?由щ럭??二쇱꽭?? 踰꾧렇, ?깅뒫 ?댁뒋, 蹂댁븞 痍⑥빟?? 媛쒖꽑?먯쓣 李얠븘 援ъ껜?곸쑝濡??쒖븞?섏꽭??"), ("肄붾뱶 ?ㅻ챸", "\uE946", "泥⑤???肄붾뱶瑜??곸꽭???ㅻ챸??二쇱꽭?? 二쇱슂 ?⑥닔, ?곗씠???먮쫫, ?ㅺ퀎 ?⑦꽩???ы븿?섏꽭??"), ("由ы뙥?좊쭅 ?쒖븞", "\uE70F", "泥⑤???肄붾뱶??由ы뙥?좊쭅 諛⑹븞???쒖븞??二쇱꽭?? 媛€?낆꽦, ?좎?蹂댁닔?? ?깅뒫??怨좊젮?섏꽭??"), ("?뚯뒪???앹꽦", "\uE9D5", "泥⑤???肄붾뱶???€???⑥쐞 ?뚯뒪??肄붾뱶瑜??앹꽦??二쇱꽭??"), ], ["document"] = [ ("?붿빟", "\uE8AB", "泥⑤???臾몄꽌瑜??듭떖 ?ъ씤???꾩<濡?媛꾧껐?섍쾶 ?붿빟??二쇱꽭??"), ("遺꾩꽍", "\uE9D9", "泥⑤???臾몄꽌???댁슜??遺꾩꽍?섍퀬 二쇱슂 ?몄궗?댄듃瑜??꾩텧??二쇱꽭??"), ("踰덉뿭", "\uE8C1", "泥⑤???臾몄꽌瑜??곸뼱濡?踰덉뿭??二쇱꽭?? ?먮Ц???ㅺ낵 ?섏븰?ㅻ? ?좎??섏꽭??"), ], ["data"] = [ ("?곗씠??遺꾩꽍", "\uE9D9", "泥⑤????곗씠?곕? 遺꾩꽍??二쇱꽭?? ?듦퀎, 異붿꽭, ?댁긽移섎? 李얠븘 蹂닿퀬?섏꽭??"), ("?쒓컖???쒖븞", "\uE9D9", "泥⑤????곗씠?곕? ?쒓컖?뷀븷 理쒖쟻??李⑦듃 ?좏삎???쒖븞?섍퀬 chart_create濡??앹꽦??二쇱꽭??"), ("?щ㎎ 蹂€??, "\uE8AB", "泥⑤????곗씠?곕? ?ㅻⅨ ?뺤떇?쇰줈 蹂€?섑빐 二쇱꽭?? (CSV?봋SON?봂xcel ??"), ], ["image"] = [ ("?대?吏€ ?ㅻ챸", "\uE946", "泥⑤????대?吏€瑜??먯꽭???ㅻ챸??二쇱꽭?? ?댁슜, ?덉씠?꾩썐, ?띿뒪?몃? 遺꾩꽍?섏꽭??"), ("UI 由щ럭", "\uE70F", "泥⑤???UI ?ㅽ겕由곗꺑??由щ럭??二쇱꽭?? UX 媛쒖꽑?? ?묎렐?? ?붿옄???쇨??깆쓣 ?됯??섏꽭??"), ], }; private static readonly HashSet CodeExtensions = new(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" }; private static readonly HashSet DataExtensions = new(StringComparer.OrdinalIgnoreCase) { ".csv", ".json", ".xml", ".yaml", ".yml", ".tsv" }; // ImageExtensions???대?吏€ 泥⑤? ?곸뿭(line ~1323)?먯꽌 ?뺤쓽?????ъ궗?? private Popup? _dropActionPopup; private void ShowDropActionMenu(string[] files) { // ?뚯씪 ?좏삎 ?먮퀎 var ext = System.IO.Path.GetExtension(files[0]).ToLowerInvariant(); string category; if (CodeExtensions.Contains(ext)) category = "code"; else if (DataExtensions.Contains(ext)) category = "data"; else if (ImageExtensions.Contains(ext)) category = "image"; else category = "document"; var actions = DropActions.GetValueOrDefault(category) ?? DropActions["document"]; // ?앹뾽 ?앹꽦 _dropActionPopup?.SetValue(Popup.IsOpenProperty, false); var panel = new StackPanel(); // ?ㅻ뜑 var header = new TextBlock { Text = $"?뱨 {System.IO.Path.GetFileName(files[0])}{(files.Length > 1 ? $" ??{files.Length - 1}媛? : "")}", FontSize = 11, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, Margin = new Thickness(12, 8, 12, 6), }; panel.Children.Add(header); // ?≪뀡 ??ぉ var hoverBrush = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(24, 255, 255, 255)); var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue; var textBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; foreach (var (label, icon, prompt) in actions) { var capturedPrompt = prompt; var row = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(6), Padding = new Thickness(12, 7, 12, 7), Margin = new Thickness(4, 1, 4, 1), Cursor = Cursors.Hand, }; var stack = new StackPanel { Orientation = Orientation.Horizontal }; stack.Children.Add(new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 13, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0), }); stack.Children.Add(new TextBlock { Text = label, FontSize = 13, FontWeight = FontWeights.SemiBold, Foreground = textBrush, VerticalAlignment = VerticalAlignment.Center, }); row.Child = stack; row.MouseEnter += (_, _) => row.Background = hoverBrush; row.MouseLeave += (_, _) => row.Background = Brushes.Transparent; row.MouseLeftButtonUp += (_, _) => { if (_dropActionPopup != null) _dropActionPopup.IsOpen = false; foreach (var f in files) AddAttachedFile(f); InputBox.Text = capturedPrompt; InputBox.CaretIndex = InputBox.Text.Length; InputBox.Focus(); if (_settings.Settings.Llm.DragDropAutoSend) _ = SendMessageAsync(); }; panel.Children.Add(row); } // "泥⑤?留? ??ぉ var attachOnly = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(6), Padding = new Thickness(12, 7, 12, 7), Margin = new Thickness(4, 1, 4, 1), Cursor = Cursors.Hand, }; var attachStack = new StackPanel { Orientation = Orientation.Horizontal }; attachStack.Children.Add(new TextBlock { Text = "\uE723", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 13, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0), }); attachStack.Children.Add(new TextBlock { Text = "泥⑤?留?, FontSize = 13, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, VerticalAlignment = VerticalAlignment.Center, }); attachOnly.Child = attachStack; attachOnly.MouseEnter += (_, _) => attachOnly.Background = hoverBrush; attachOnly.MouseLeave += (_, _) => attachOnly.Background = Brushes.Transparent; attachOnly.MouseLeftButtonUp += (_, _) => { if (_dropActionPopup != null) _dropActionPopup.IsOpen = false; foreach (var f in files) AddAttachedFile(f); InputBox.Focus(); }; panel.Children.Add(attachOnly); var container = new Border { Background = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(26, 27, 46)), CornerRadius = new CornerRadius(12), BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray, BorderThickness = new Thickness(1), Padding = new Thickness(4, 4, 4, 6), Child = panel, MinWidth = 200, Effect = new System.Windows.Media.Effects.DropShadowEffect { Color = Colors.Black, BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3, }, }; _dropActionPopup = new Popup { PlacementTarget = InputBorder, Placement = PlacementMode.Top, StaysOpen = false, AllowsTransparency = true, PopupAnimation = PopupAnimation.Fade, Child = container, }; _dropActionPopup.IsOpen = true; } // ?€?€?€ /help ?꾩?留?李??€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void ShowSlashHelpWindow() { var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(26, 27, 46)); var fg = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var fg2 = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue; var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(30, 255, 255, 255)); var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(40, 255, 255, 255)); var win = new Window { Title = "AX Agent ???щ옒??紐낅졊???꾩?留?, Width = 560, Height = 640, MinWidth = 440, MinHeight = 500, WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent, ResizeMode = ResizeMode.CanResize, WindowStartupLocation = WindowStartupLocation.CenterOwner, Owner = this, Icon = Icon, }; var mainBorder = new Border { Background = bg, CornerRadius = new CornerRadius(16), BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray, BorderThickness = new Thickness(1), Margin = new Thickness(10), Effect = new System.Windows.Media.Effects.DropShadowEffect { Color = Colors.Black, BlurRadius = 20, ShadowDepth = 4, Opacity = 0.3 }, }; var rootGrid = new Grid(); rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(52) }); rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); // ?ㅻ뜑 var headerBorder = new Border { CornerRadius = new CornerRadius(16, 16, 0, 0), Background = new LinearGradientBrush( Color.FromRgb(26, 27, 46), Color.FromRgb(59, 78, 204), new Point(0, 0), new Point(1, 1)), Padding = new Thickness(20, 0, 20, 0), }; var headerGrid = new Grid(); var headerStack = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center }; headerStack.Children.Add(new TextBlock { Text = "\uE946", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 16, Foreground = Brushes.LightCyan, Margin = new Thickness(0, 0, 10, 0), VerticalAlignment = VerticalAlignment.Center }); headerStack.Children.Add(new TextBlock { Text = "?щ옒??紐낅졊??(/ Commands)", FontSize = 15, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White, VerticalAlignment = VerticalAlignment.Center }); headerGrid.Children.Add(headerStack); var closeBtn = new Border { Width = 30, Height = 30, CornerRadius = new CornerRadius(8), Background = Brushes.Transparent, Cursor = Cursors.Hand, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center }; closeBtn.Child = new TextBlock { Text = "\uE711", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 11, Foreground = new SolidColorBrush(Color.FromArgb(136, 170, 255, 204)), HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center }; closeBtn.MouseEnter += (_, _) => closeBtn.Background = new SolidColorBrush(Color.FromArgb(34, 255, 255, 255)); closeBtn.MouseLeave += (_, _) => closeBtn.Background = Brushes.Transparent; closeBtn.MouseLeftButtonDown += (_, me) => { me.Handled = true; win.Close(); }; headerGrid.Children.Add(closeBtn); headerBorder.Child = headerGrid; Grid.SetRow(headerBorder, 0); rootGrid.Children.Add(headerBorder); // 肄섑뀗痢? var scroll = new ScrollViewer { VerticalScrollBarVisibility = ScrollBarVisibility.Auto, Padding = new Thickness(20, 14, 20, 20) }; var contentPanel = new StackPanel(); // ?ㅻ챸 contentPanel.Children.Add(new TextBlock { Text = "?낅젰李쎌뿉 /瑜??낅젰?섎㈃ ?ъ슜?????덈뒗 紐낅졊?닿? ?쒖떆?⑸땲??\n紐낅졊?대? ?좏깮?????댁슜???낅젰?섎㈃ ?대떦 湲곕뒫???곸슜?⑸땲??", FontSize = 12, Foreground = fg2, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 0, 0, 16), LineHeight = 20 }); // 怨듯넻 紐낅졊???뱀뀡 AddHelpSection(contentPanel, "?뱦 怨듯넻 紐낅졊??, "紐⑤뱺 ??Chat, Cowork, Code)?먯꽌 ?ъ슜 媛€??, fg, fg2, accent, itemBg, hoverBg, ("/summary", "?띿뒪??臾몄꽌瑜??듭떖 ?ъ씤??以묒떖?쇰줈 ?붿빟?⑸땲??"), ("/translate", "?띿뒪?몃? ?곸뼱濡?踰덉뿭?⑸땲?? ?먮Ц???ㅼ쓣 ?좎??⑸땲??"), ("/explain", "?댁슜???쎄퀬 ?먯꽭?섍쾶 ?ㅻ챸?⑸땲?? ?덉떆瑜??ы븿?⑸땲??"), ("/fix", "留욎땄踰? 臾몃쾿, ?먯뿰?ㅻ윭???쒗쁽??援먯젙?⑸땲??")); // 媛쒕컻 紐낅졊???뱀뀡 AddHelpSection(contentPanel, "?썱截?媛쒕컻 紐낅졊??, "Cowork, Code ??뿉?쒕쭔 ?ъ슜 媛€??, fg, fg2, accent, itemBg, hoverBg, ("/review", "Git diff瑜?遺꾩꽍?섏뿬 踰꾧렇, ?깅뒫, 蹂댁븞 ?댁뒋瑜?李얠뒿?덈떎."), ("/pr", "蹂€寃쎌궗??쓣 PR ?ㅻ챸 ?뺤떇(Summary, Changes, Test Plan)?쇰줈 ?붿빟?⑸땲??"), ("/test", "肄붾뱶???€???⑥쐞 ?뚯뒪?몃? ?먮룞 ?앹꽦?⑸땲??"), ("/structure", "?꾨줈?앺듃???대뜑/?뚯씪 援ъ“瑜?遺꾩꽍?섍퀬 ?ㅻ챸?⑸땲??"), ("/build", "?꾨줈?앺듃瑜?鍮뚮뱶?⑸땲?? ?ㅻ쪟 諛쒖깮 ??遺꾩꽍?⑸땲??"), ("/search", "?먯뿰?대줈 肄붾뱶踰좎씠?ㅻ? ?쒕㎤??寃€?됲빀?덈떎.")); // ?ㅽ궗 紐낅졊???뱀뀡 var skills = SkillService.Skills; if (skills.Count > 0) { var skillItems = skills.Select(s => ($"/{s.Name}", s.Description)).ToArray(); AddHelpSection(contentPanel, "???ㅽ궗 紐낅졊??, $"{skills.Count}媛?濡쒕뱶????%APPDATA%\\AxCopilot\\skills\\?먯꽌 異붽? 媛€??, fg, fg2, accent, itemBg, hoverBg, skillItems); } // ?ъ슜 ?? contentPanel.Children.Add(new Border { Height = 1, Background = new SolidColorBrush(Color.FromArgb(30, 255, 255, 255)), Margin = new Thickness(0, 12, 0, 12) }); var tipPanel = new StackPanel(); tipPanel.Children.Add(new TextBlock { Text = "?뮕 ?ъ슜 ??, FontSize = 13, FontWeight = FontWeights.SemiBold, Foreground = fg, Margin = new Thickness(0, 0, 0, 8) }); var tips = new[] { "/ ?낅젰 ???꾩옱 ??뿉 留욌뒗 紐낅졊?대쭔 ?먮룞?꾩꽦?⑸땲??", "?뚯씪???쒕옒洹명븯硫??좏삎蹂?AI ?≪뀡 ?앹뾽???섑??⑸땲??", "?ㅽ궗 ?뚯씪(*.skill.md)??異붽??섎㈃ ?섎쭔???뚰겕?뚮줈?곕? 留뚮뱾 ???덉뒿?덈떎.", "Cowork/Code ??뿉???먯씠?꾪듃媛€ ?꾧뎄瑜??쒖슜?섏뿬 ??媛뺣젰???묒뾽???섑뻾?⑸땲??", }; foreach (var tip in tips) { tipPanel.Children.Add(new TextBlock { Text = $"??{tip}", FontSize = 12, Foreground = fg2, Margin = new Thickness(8, 2, 0, 2), TextWrapping = TextWrapping.Wrap, LineHeight = 18 }); } contentPanel.Children.Add(tipPanel); scroll.Content = contentPanel; Grid.SetRow(scroll, 1); rootGrid.Children.Add(scroll); mainBorder.Child = rootGrid; win.Content = mainBorder; // ?ㅻ뜑 ?곸뿭?먯꽌留??쒕옒洹??대룞 (?リ린 踰꾪듉 ?대┃ 諛⑺빐 諛⑹?) headerBorder.MouseLeftButtonDown += (_, me) => { try { win.DragMove(); } catch { } }; win.ShowDialog(); } private static void AddHelpSection(StackPanel parent, string title, string subtitle, Brush fg, Brush fg2, Brush accent, Brush itemBg, Brush hoverBg, params (string Cmd, string Desc)[] items) { parent.Children.Add(new TextBlock { Text = title, FontSize = 14, FontWeight = FontWeights.SemiBold, Foreground = fg, Margin = new Thickness(0, 8, 0, 2) }); parent.Children.Add(new TextBlock { Text = subtitle, FontSize = 11, Foreground = fg2, Margin = new Thickness(0, 0, 0, 8) }); foreach (var (cmd, desc) in items) { var row = new Border { Background = itemBg, CornerRadius = new CornerRadius(8), Padding = new Thickness(12, 8, 12, 8), Margin = new Thickness(0, 3, 0, 3) }; var grid = new Grid(); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(120) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); var cmdText = new TextBlock { Text = cmd, FontSize = 13, FontWeight = FontWeights.SemiBold, Foreground = accent, VerticalAlignment = VerticalAlignment.Center, FontFamily = new FontFamily("Consolas") }; Grid.SetColumn(cmdText, 0); grid.Children.Add(cmdText); var descText = new TextBlock { Text = desc, FontSize = 12, Foreground = fg2, VerticalAlignment = VerticalAlignment.Center, TextWrapping = TextWrapping.Wrap }; Grid.SetColumn(descText, 1); grid.Children.Add(descText); row.Child = grid; row.MouseEnter += (_, _) => row.Background = hoverBg; row.MouseLeave += (_, _) => row.Background = itemBg; parent.Children.Add(row); } } private async Task SendMessageAsync() { var rawText = InputBox.Text.Trim(); // ?щ옒??移⑹씠 ?쒖꽦?붾맂 寃쎌슦 紐낅졊???욎뿉 遺숈엫 var text = _activeSlashCmd != null ? (_activeSlashCmd + " " + rawText).Trim() : rawText; HideSlashChip(restoreText: false); if (string.IsNullOrEmpty(text) || _isStreaming) return; // placeholder ?뺣━ ClearPromptCardPlaceholder(); // ?щ옒??紐낅졊??泥섎━ var (slashSystem, displayText) = ParseSlashCommand(text); // ???꾪솚 ?쒖뿉???щ컮瑜???뿉 ?€?ν븯湲??꾪빐 ?쒖옉 ?쒖젏????쓣 罹≪쿂 var originTab = _activeTab; ChatConversation conv; lock (_convLock) { if (_currentConversation == null) _currentConversation = _sessionState.LoadOrCreateConversation(_activeTab, _storage, _settings); conv = _currentConversation; } if (string.IsNullOrWhiteSpace(conv.Tab)) conv.Tab = _activeTab; var userMsg = new ChatMessage { Role = "user", Content = text }; lock (_convLock) conv.Messages.Add(userMsg); if (conv.Messages.Count(m => m.Role == "user") == 1) conv.Title = text.Length > 30 ? text[..30] + "?? : text; UpdateChatTitle(); AddMessageBubble("user", text); InputBox.Text = ""; EmptyState.Visibility = Visibility.Collapsed; // ?€???듦퀎 湲곕줉 Services.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(); var assistantMsg = new ChatMessage { Role = "assistant", Content = "" }; lock (_convLock) conv.Messages.Add(assistantMsg); // ?댁떆?ㅽ꽩???ㅽ듃由щ컢 而⑦뀒?대꼫 var streamContainer = CreateStreamingContainer(out var streamText); MessagePanel.Children.Add(streamContainer); ForceScrollToEnd(); // ?묐떟 ?쒖옉 ??媛뺤젣 ?섎떒 ?대룞 var sb = new System.Text.StringBuilder(); _activeStreamText = streamText; _cachedStreamContent = ""; _displayedLength = 0; _cursorVisible = true; _aiIconPulseStopped = false; _cursorTimer.Start(); _typingTimer.Start(); _streamStartTime = DateTime.UtcNow; _elapsedTimer.Start(); SetStatus("?묐떟 ?앹꽦 以?..", spinning: true); // ?€?€ ?먮룞 紐⑤뜽 ?쇱슦??(try ?몃? ?좎뼵 ??finally?먯꽌 ?뺣━) ?€?€ ModelRouteResult? routeResult = null; var finishedSuccessfully = false; 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) { var fileContext = BuildFileContextPrompt(); if (!string.IsNullOrEmpty(fileContext)) { var lastUserIdx = sendMessages.FindLastIndex(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(); // 留덉?留??ъ슜??硫붿떆吏€???대?吏€ ?곗씠???곌껐 var lastUserIdx = sendMessages.FindLastIndex(m => m.Role == "user"); if (lastUserIdx >= 0) sendMessages[lastUserIdx] = new ChatMessage { Role = "user", Content = sendMessages[lastUserIdx].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; // ?ъ슜???섏궗寃곗젙 肄쒕갚 ??PlanViewerWindow濡?怨꾪쉷 ?쒖떆 _agentLoop.UserDecisionCallback = CreatePlanDecisionCallback(); try { // 肄붿썙???쒖뒪???꾨\?꾪듃 ?쎌엯 var coworkSystem = BuildCoworkSystemPrompt(); if (!string.IsNullOrEmpty(coworkSystem)) sendMessages.Insert(0, new ChatMessage { Role = "system", Content = coworkSystem }); _agentLoop.ActiveTab = _activeTab; var response = await _agentLoop.RunAsync(sendMessages, _streamCts!.Token); sb.Append(response); assistantMsg.Content = response; StopAiIconPulse(); _cachedStreamContent = response; // ?꾨즺 ?뚮┝ if (_settings.Settings.Llm.NotifyOnComplete) Services.NotificationService.Notify("AX Cowork Agent", "肄붿썙???묒뾽???꾨즺?섏뿀?듬땲??"); } finally { _agentLoop.EventOccurred -= OnAgentEvent; _agentLoop.UserDecisionCallback = null; } } else if (_activeTab == "Code") { // ?뚰겕?뚮줈??遺꾩꽍湲??먮룞 ?닿린 OpenWorkflowAnalyzerIfEnabled(); // ?꾩쟻 ?좏겙 珥덇린?? _agentCumulativeInputTokens = 0; _agentCumulativeOutputTokens = 0; // Code ?? ?먯씠?꾪듃 猷⑦봽 ?ъ슜 (Cowork怨??숈씪 ?⑦꽩) _agentLoop.EventOccurred += OnAgentEvent; _agentLoop.UserDecisionCallback = CreatePlanDecisionCallback(); try { var codeSystem = BuildCodeSystemPrompt(); if (!string.IsNullOrEmpty(codeSystem)) sendMessages.Insert(0, new ChatMessage { Role = "system", Content = codeSystem }); _agentLoop.ActiveTab = "Code"; var response = await _agentLoop.RunAsync(sendMessages, _streamCts!.Token); sb.Append(response); assistantMsg.Content = response; StopAiIconPulse(); _cachedStreamContent = response; // ?꾨즺 ?뚮┝ if (_settings.Settings.Llm.NotifyOnComplete) Services.NotificationService.Notify("AX Code Agent", "肄붾뱶 ?묒뾽???꾨즺?섏뿀?듬땲??"); } finally { _agentLoop.EventOccurred -= OnAgentEvent; _agentLoop.UserDecisionCallback = null; } } else if (_settings.Settings.Llm.Streaming) { await foreach (var chunk in _llm.StreamAsync(sendMessages, _streamCts.Token)) { sb.Append(chunk); StopAiIconPulse(); _cachedStreamContent = sb.ToString(); // UI ?ㅻ젅?쒖뿉 ?쒖뼱 ?묐낫 ??DispatcherTimer媛€ ?붾㈃ 媛깆떊?????덈룄濡? await Dispatcher.InvokeAsync(() => { }, DispatcherPriority.Background); } _cachedStreamContent = sb.ToString(); assistantMsg.Content = _cachedStreamContent; // ?€?댄븨 ?좊땲硫붿씠?섏씠 ?⑥? 踰꾪띁瑜??뚯쭊???뚭퉴吏€ ?€湲?(理쒕? 600ms) var drainStart = DateTime.UtcNow; while (_displayedLength < _cachedStreamContent.Length && (DateTime.UtcNow - drainStart).TotalMilliseconds < 600) { await Dispatcher.InvokeAsync(() => { }, DispatcherPriority.Background); } } else { var response = await _llm.SendAsync(sendMessages, _streamCts.Token); sb.Append(response); assistantMsg.Content = response; } finishedSuccessfully = true; } catch (OperationCanceledException) { if (sb.Length == 0) sb.Append("(痍⑥냼??"); assistantMsg.Content = sb.ToString(); _activeQueuedDraftItem = null; } catch (Exception ex) { var errMsg = $"???ㅻ쪟: {ex.Message}"; sb.Clear(); sb.Append(errMsg); assistantMsg.Content = errMsg; RestoreFailedQueuedDraft(text, ex); 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(); if (finishedSuccessfully) _activeQueuedDraftItem = null; } // ?ㅽ듃由щ컢 plaintext ??留덊겕?ㅼ슫 ?뚮뜑留곸쑝濡?援먯껜 FinalizeStreamingContainer(streamContainer, streamText, assistantMsg.Content, assistantMsg); AutoScrollIfNeeded(); try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"?€???€???ㅽ뙣: {ex.Message}"); } _sessionState.RememberConversation(originTab, conv.Id); RefreshConversationList(); if (finishedSuccessfully) await StartNextQueuedDraftIfAnyAsync(); } // ?€?€?€ 肄붿썙???먯씠?꾪듃 吏€???€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private string BuildCoworkSystemPrompt() { var workFolder = GetCurrentWorkFolder(); var llm = _settings.Settings.Llm; var sb = new System.Text.StringBuilder(); sb.AppendLine("You are AX Copilot Agent. You can read, write, and edit files using the provided tools."); sb.AppendLine($"Today's date: {DateTime.Now:yyyy??M??d?? ({DateTime.Now:yyyy-MM-dd}, {DateTime.Now:dddd})."); sb.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 (?щ㎎ 蹂€??."); sb.AppendLine("Always explain your plan step by step BEFORE executing tools. After creating files, summarize what was created."); sb.AppendLine("IMPORTANT: When creating documents with dates, always use today's actual date above. Never use placeholder or fictional dates."); sb.AppendLine("IMPORTANT: When asked to create a document with multiple sections (reports, proposals, analyses, etc.), you MUST:"); sb.AppendLine(" 1. First, plan the document: decide the exact sections (headings), their order, and key points for each section based on the topic."); sb.AppendLine(" 2. Call document_plan with sections_hint = your planned section titles (comma-separated). Example: sections_hint=\"?뚯궗 媛쒖슂, ?ъ뾽 ?꾪솴, ?щТ 遺꾩꽍, SWOT, ?꾨왂 ?쒖뼵, 寃곕줎\""); sb.AppendLine(" This ensures the document structure matches YOUR plan, not a generic template."); sb.AppendLine(" 3. Then immediately call html_create (or docx_create/file_write) using the scaffold from document_plan."); sb.AppendLine(" 4. Write actual detailed content for EVERY section ??no skipping, no placeholders, no minimal content."); sb.AppendLine(" 5. Do NOT call html_create directly without document_plan for multi-section documents."); // 臾몄꽌 ?덉쭏 寃€利?猷⑦봽 sb.AppendLine("\n## Document Quality Review"); sb.AppendLine("After creating any document (html_create, docx_create, excel_create, etc.), you MUST perform a self-review:"); sb.AppendLine("1. Use file_read to read the generated file and verify the content is complete"); sb.AppendLine("2. Check for logical errors: incorrect dates, inconsistent data, missing sections, broken formatting"); sb.AppendLine("3. Verify all requested topics/sections from the user's original request are covered"); sb.AppendLine("4. If issues found, fix them using file_write or file_edit, then re-verify"); sb.AppendLine("5. Report the review result to the user: what was checked and whether corrections were made"); // 臾몄꽌 ?щ㎎ 蹂€??吏€?? sb.AppendLine("\n## Format Conversion"); sb.AppendLine("When the user requests format conversion (e.g., HTML?뭌ord, Excel?묬SV, Markdown?묱TML):"); sb.AppendLine("1. Use file_read or document_read to read the source file content"); sb.AppendLine("2. Create a new file in the target format using the appropriate skill (docx_create, html_create, etc.)"); sb.AppendLine("3. Preserve the content structure, formatting, and data as closely as possible"); // ?ъ슜??吏€??異쒕젰 ?щ㎎ var fmt = llm.DefaultOutputFormat; if (!string.IsNullOrEmpty(fmt) && fmt != "auto") { var fmtMap = 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 (fmtMap.TryGetValue(fmt, out var fmtDesc)) sb.AppendLine($"IMPORTANT: User prefers output format: {fmtDesc}. Use this format unless the user specifies otherwise."); } // ?붿옄??臾대뱶 ??HTML 臾몄꽌 ?앹꽦 ??mood ?뚮씪誘명꽣濡??꾨떖?섎룄濡??덈궡 if (!string.IsNullOrEmpty(_selectedMood) && _selectedMood != "modern") sb.AppendLine($"When creating HTML documents with html_create, use mood=\"{_selectedMood}\" for the design template."); else sb.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(workFolder)) sb.AppendLine($"Current work folder: {workFolder}"); sb.AppendLine($"File permission mode: {llm.FilePermission}"); // ?대뜑 ?곗씠???쒖슜 吏€移? switch (_folderDataUsage) { case "active": sb.AppendLine("IMPORTANT: Folder Data Usage = ACTIVE. You have 'document_read' and 'folder_map' tools available."); sb.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."); break; case "passive": sb.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."); break; default: // "none" sb.AppendLine("Folder Data Usage = NONE. Do NOT read or reference documents in the work folder unless the user explicitly provides a file path."); break; } // ?꾨━???쒖뒪???꾨\?꾪듃媛€ ?덉쑝硫?異붽? lock (_convLock) { if (_currentConversation != null && !string.IsNullOrEmpty(_currentConversation.SystemCommand)) sb.AppendLine("\n" + _currentConversation.SystemCommand); } // ?꾨줈?앺듃 臾몃㎘ ?뚯씪 (AX.md) 二쇱엯 sb.Append(LoadProjectContext(workFolder)); // ?꾨줈?앺듃 洹쒖튃 (.ax/rules/) ?먮룞 二쇱엯 sb.Append(BuildProjectRulesSection(workFolder)); // ?먯씠?꾪듃 硫붾え由?二쇱엯 sb.Append(BuildMemorySection(workFolder)); // ?쇰뱶諛??숈뒿 而⑦뀓?ㅽ듃 二쇱엯 sb.Append(BuildFeedbackContext()); return sb.ToString(); } private string BuildCodeSystemPrompt() { var workFolder = GetCurrentWorkFolder(); var llm = _settings.Settings.Llm; var code = llm.Code; var sb = new System.Text.StringBuilder(); sb.AppendLine("You are AX Copilot Code Agent ??a senior software engineer for enterprise development."); sb.AppendLine($"Today's date: {DateTime.Now:yyyy??M??d?? ({DateTime.Now:yyyy-MM-dd})."); sb.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."); sb.AppendLine("IMPORTANT: When creating documents with dates, always use today's actual date above."); sb.AppendLine("\n## Core Workflow (MANDATORY ??follow this order)"); sb.AppendLine("1. ORIENT: Run folder_map (depth=2) to understand project structure. Check .gitignore, README, config files."); sb.AppendLine("2. BASELINE: If tests exist, run build_run action='test' FIRST to establish baseline. Record pass/fail count."); sb.AppendLine("3. ANALYZE: Use grep (with context_lines=2) + file_read to deeply understand the code you'll modify."); sb.AppendLine(" - Always check callers/references: grep for function/class names to find all usage points."); sb.AppendLine(" - Read test files related to the code you're changing to understand expected behavior."); sb.AppendLine("4. PLAN: Present your analysis + impact assessment. List ALL files that will be modified."); sb.AppendLine(" - Explain WHY each change is needed and what could break."); sb.AppendLine(" - Wait for user approval before proceeding."); sb.AppendLine("5. IMPLEMENT: Apply changes using file_edit (preferred ??shows diff). Use file_write only for new files."); sb.AppendLine(" - Make the MINIMUM changes needed. Don't refactor unrelated code."); sb.AppendLine(" - Prefer file_edit with replace_all=false for precision edits."); sb.AppendLine("6. VERIFY: Run build_run action='build' then action='test'. Compare results with baseline."); sb.AppendLine(" - If tests fail that passed before, fix immediately."); sb.AppendLine(" - If build fails, analyze error output and correct."); sb.AppendLine("7. GIT: Use git_tool to check status, create diff, and optionally commit."); sb.AppendLine("8. REPORT: Summarize changes, test results, and any remaining concerns."); sb.AppendLine("\n## Development Environment"); sb.AppendLine("Use dev_env_detect to check installed IDEs, runtimes, and build tools before running commands."); sb.AppendLine("IMPORTANT: Do NOT attempt to install compilers, IDEs, or build tools. Only use what is already installed."); // ?⑦궎吏€ ?€?μ냼 ?뺣낫 sb.AppendLine("\n## Package Repositories"); if (!string.IsNullOrEmpty(code.NexusBaseUrl)) sb.AppendLine($"Enterprise Nexus: {code.NexusBaseUrl}"); sb.AppendLine($"NuGet (.NET): {code.NugetSource}"); sb.AppendLine($"PyPI/Conda (Python): {code.PypiSource}"); sb.AppendLine($"Maven (Java): {code.MavenSource}"); sb.AppendLine($"npm (JavaScript): {code.NpmSource}"); sb.AppendLine("When adding dependencies, use these repository URLs."); // IDE ?뺣낫 if (!string.IsNullOrEmpty(code.PreferredIdePath)) sb.AppendLine($"\nPreferred IDE: {code.PreferredIdePath}"); // ?ъ슜???좏깮 媛쒕컻 ?몄뼱 if (_selectedLanguage != "auto") { var langName = _selectedLanguage switch { "python" => "Python", "java" => "Java", "csharp" => "C# (.NET)", "cpp" => "C/C++", "javascript" => "JavaScript/TypeScript", _ => _selectedLanguage }; sb.AppendLine($"\nIMPORTANT: User selected language: {langName}. Prioritize this language for code analysis and generation."); } // ?몄뼱蹂?媛€?대뱶?쇱씤 sb.AppendLine("\n## Language Guidelines"); sb.AppendLine("- C# (.NET): Use dotnet CLI. NuGet for packages. Follow Microsoft naming conventions."); sb.AppendLine("- Python: Use conda/pip. Follow PEP8. Use type hints. Virtual env preferred."); sb.AppendLine("- Java: Use Maven/Gradle. Follow Google Java Style Guide."); sb.AppendLine("- C++: Use CMake for build. Follow C++ Core Guidelines."); sb.AppendLine("- JavaScript/TypeScript: Use npm/yarn. Follow ESLint rules. Vue3 uses Composition API."); // 肄붾뱶 ?덉쭏 + ?덉쟾 ?섏튃 sb.AppendLine("\n## Code Quality & Safety"); sb.AppendLine("- NEVER delete or overwrite files without user confirmation."); sb.AppendLine("- ALWAYS read a file before editing it. Don't guess contents."); sb.AppendLine("- Prefer file_edit over file_write for existing files (shows diff)."); sb.AppendLine("- Use grep to find ALL references before renaming/removing anything."); sb.AppendLine("- If unsure about a change's impact, ask the user first."); sb.AppendLine("- For large refactors, do them incrementally with build verification between steps."); sb.AppendLine("- Use git_tool action='diff' to review your changes before committing."); sb.AppendLine("\n## Lint & Format"); sb.AppendLine("After code changes, check for available linters:"); sb.AppendLine("- Python: ruff, black, flake8, pylint"); sb.AppendLine("- JavaScript: eslint, prettier"); sb.AppendLine("- C#: dotnet format"); sb.AppendLine("- C++: clang-format"); sb.AppendLine("Run the appropriate linter via process tool if detected by dev_env_detect."); if (!string.IsNullOrEmpty(workFolder)) sb.AppendLine($"\nCurrent work folder: {workFolder}"); sb.AppendLine($"File permission mode: {llm.FilePermission}"); // ?대뜑 ?곗씠???쒖슜 sb.AppendLine("\nFolder Data Usage = ACTIVE. Use folder_map and file_read to understand the codebase."); sb.AppendLine("Analyze project structure before making changes. Read relevant files to understand context."); // ?꾨━???쒖뒪???꾨\?꾪듃 lock (_convLock) { if (_currentConversation?.SystemCommand is { Length: > 0 } sysCmd) sb.AppendLine("\n" + sysCmd); } // ?꾨줈?앺듃 臾몃㎘ ?뚯씪 (AX.md) 二쇱엯 sb.Append(LoadProjectContext(workFolder)); // ?꾨줈?앺듃 洹쒖튃 (.ax/rules/) ?먮룞 二쇱엯 sb.Append(BuildProjectRulesSection(workFolder)); // ?먯씠?꾪듃 硫붾え由?二쇱엯 sb.Append(BuildMemorySection(workFolder)); // ?쇰뱶諛??숈뒿 而⑦뀓?ㅽ듃 二쇱엯 sb.Append(BuildFeedbackContext()); return sb.ToString(); } /// ?꾨줈?앺듃 洹쒖튃 (.ax/rules/)???쒖뒪???꾨\?꾪듃 ?뱀뀡?쇰줈 ?щ㎎?⑸땲?? private string BuildProjectRulesSection(string? workFolder) { if (string.IsNullOrEmpty(workFolder)) return ""; if (!_settings.Settings.Llm.EnableProjectRules) return ""; try { var rules = Services.Agent.ProjectRulesService.LoadRules(workFolder); if (rules.Count == 0) return ""; // 而⑦뀓?ㅽ듃蹂??꾪꽣留? Cowork=document, Code=always (湲곕낯) var when = _activeTab == "Code" ? "always" : "always"; var filtered = Services.Agent.ProjectRulesService.FilterRules(rules, when); return Services.Agent.ProjectRulesService.FormatForSystemPrompt(filtered); } catch { return ""; } } /// ?먯씠?꾪듃 硫붾え由щ? ?쒖뒪???꾨\?꾪듃 ?뱀뀡?쇰줈 ?щ㎎?⑸땲?? private string BuildMemorySection(string? workFolder) { if (!_settings.Settings.Llm.EnableAgentMemory) return ""; var app = System.Windows.Application.Current as App; var memService = app?.MemoryService; if (memService == null || memService.Count == 0) return ""; // 硫붾え由щ? 濡쒕뱶 (?묒뾽 ?대뜑 蹂€寃????щ줈?? memService.Load(workFolder ?? ""); var all = memService.All; if (all.Count == 0) return ""; var sb = new System.Text.StringBuilder(); sb.AppendLine("\n## ?꾨줈?앺듃 硫붾え由?(?댁쟾 ?€?붿뿉???숈뒿???댁슜)"); sb.AppendLine("?꾨옒???댁쟾 ?€?붿뿉???숈뒿??洹쒖튃怨??좏샇?꾩엯?덈떎. ?묒뾽 ??李멸퀬?섏꽭??"); sb.AppendLine("?덈줈??洹쒖튃?대굹 ?좏샇?꾨? 諛쒓껄?섎㈃ memory ?꾧뎄??save ?≪뀡?쇰줈 ?€?ν븯?몄슂."); sb.AppendLine("?ъ슜?먭? ?댁쟾 ?숈뒿 ?댁슜怨??ㅻⅨ 吏€?쒕? ?섎㈃ memory ?꾧뎄??delete ???덈줈 save ?섏꽭??\n"); foreach (var group in all.GroupBy(e => e.Type)) { var label = group.Key switch { "rule" => "?꾨줈?앺듃 洹쒖튃", "preference" => "?ъ슜???좏샇", "fact" => "?꾨줈?앺듃 ?ъ떎", "correction" => "?댁쟾 援먯젙", _ => group.Key, }; sb.AppendLine($"[{label}]"); foreach (var e in group.OrderByDescending(e => e.UseCount).Take(15)) sb.AppendLine($"- {e.Content}"); sb.AppendLine(); } return sb.ToString(); } /// ?뚰겕?뚮줈???쒓컖???ㅼ젙??耳쒖졇?덉쑝硫?遺꾩꽍湲?李쎌쓣 ?닿퀬 ?대깽?몃? 援щ룆?⑸땲?? private void OpenWorkflowAnalyzerIfEnabled() { var llm = _settings.Settings.Llm; if (!llm.DevMode || !llm.WorkflowVisualizer) return; if (_analyzerWindow == null) { // ?덈줈 ?앹꽦 _analyzerWindow = new WorkflowAnalyzerWindow(); _analyzerWindow.Closed += (_, _) => _analyzerWindow = null; // ?뚮쭏 由ъ냼???꾨떖 foreach (var dict in System.Windows.Application.Current.Resources.MergedDictionaries) _analyzerWindow.Resources.MergedDictionaries.Add(dict); _analyzerWindow.Show(); } else if (!_analyzerWindow.IsVisible) { // Hide()濡??④꺼吏?李???湲곗〈 ?댁슜 ?좎???梨??ㅼ떆 ?쒖떆 _analyzerWindow.Show(); _analyzerWindow.Activate(); } else { // ?대? 蹂댁씠???곹깭 ?????먯씠?꾪듃 ?ㅽ뻾???꾪빐 珥덇린?????쒖꽦?? _analyzerWindow.Reset(); _analyzerWindow.Activate(); } // ?€?꾨씪????쑝濡??꾪솚 (???ㅽ뻾 ?쒖옉) _analyzerWindow.SwitchToTimelineTab(); // ?대깽??援щ룆 (以묐났 諛⑹?) _agentLoop.EventOccurred -= _analyzerWindow.OnAgentEvent; _agentLoop.EventOccurred += _analyzerWindow.OnAgentEvent; } /// ?뚰겕?뚮줈??遺꾩꽍湲?踰꾪듉???쒖떆 ?곹깭瑜?媛깆떊?⑸땲?? private void UpdateAnalyzerButtonVisibility() { var llm = _settings.Settings.Llm; BtnShowAnalyzer.Visibility = (llm.DevMode && llm.WorkflowVisualizer) ? Visibility.Visible : Visibility.Collapsed; } /// ?뚰겕?뚮줈??遺꾩꽍湲?李쎌쓣 ?섎룞?쇰줈 ?닿굅???ъ빱?ㅽ빀?덈떎 (?섎떒 諛?踰꾪듉). private void BtnShowAnalyzer_Click(object sender, MouseButtonEventArgs e) { if (_analyzerWindow == null) { _analyzerWindow = new WorkflowAnalyzerWindow(); _analyzerWindow.Closed += (_, _) => _analyzerWindow = null; foreach (var dict in System.Windows.Application.Current.Resources.MergedDictionaries) _analyzerWindow.Resources.MergedDictionaries.Add(dict); // ?먯씠?꾪듃 ?대깽??援щ룆 _agentLoop.EventOccurred -= _analyzerWindow.OnAgentEvent; _agentLoop.EventOccurred += _analyzerWindow.OnAgentEvent; _analyzerWindow.Show(); } else if (!_analyzerWindow.IsVisible) { _analyzerWindow.Show(); _analyzerWindow.Activate(); } else { _analyzerWindow.Activate(); } } /// ?먯씠?꾪듃 猷⑦봽 ?숈븞 ?꾩쟻 ?좏겙 (?섎떒 諛??쒖떆?? private int _agentCumulativeInputTokens; private int _agentCumulativeOutputTokens; private static readonly HashSet WriteToolNames = new(StringComparer.OrdinalIgnoreCase) { "file_write", "file_edit", "html_create", "xlsx_create", "docx_create", "csv_create", "md_create", "script_create", "diff_preview", "open_external", }; private void OnAgentEvent(AgentEvent evt) { ChatConversation? conv; lock (_convLock) { conv = _currentConversation; conv?.ExecutionEvents.Add(ToChatExecutionEvent(evt)); } if (conv != null) { try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"?ㅽ뻾 ?대깽???€???ㅽ뙣: {ex.Message}"); } } UpdateRuntimeActivityState(evt); // ?먯씠?꾪듃 ?대깽?몃? 梨꾪똿 UI???쒖떆 (?꾧뎄 ?몄텧/寃곌낵 諛곕꼫) if (IsExecutionHistoryVisible()) { AddAgentEventBanner(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(); // suggest_actions ?꾧뎄 寃곌낵 ???꾩냽 ?묒뾽 移??쒖떆 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)) { var 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 UpdateRuntimeActivityState(AgentEvent evt) private void UpdateRuntimeActivityState(AgentEvent evt) { switch (evt.Type) { case AgentEventType.ToolCall: _appState.UpsertTask( GetToolTaskKey(evt), "tool", evt.ToolName, string.IsNullOrWhiteSpace(evt.Summary) ? $"{evt.ToolName} 실행 중" : evt.Summary, "running", evt.FilePath); _sessionState.UpsertRuntimeActivity( $"tool:{evt.ToolName}", "tool", evt.ToolName, string.IsNullOrWhiteSpace(evt.Summary) ? $"{evt.ToolName} 실행 중" : evt.Summary); break; case AgentEventType.ToolResult: _appState.CompleteTask( GetToolTaskKey(evt), string.IsNullOrWhiteSpace(evt.Summary) ? evt.ToolName : evt.Summary, evt.Success ? "completed" : "failed"); _sessionState.RemoveRuntimeActivity( $"tool:{evt.ToolName}", string.IsNullOrWhiteSpace(evt.Summary) ? evt.ToolName : evt.Summary); break; case AgentEventType.Error: if (!string.IsNullOrWhiteSpace(evt.ToolName)) { _appState.CompleteTask( GetToolTaskKey(evt), string.IsNullOrWhiteSpace(evt.Summary) ? evt.ToolName : evt.Summary, "failed"); _sessionState.RemoveRuntimeActivity( $"tool:{evt.ToolName}", string.IsNullOrWhiteSpace(evt.Summary) ? evt.ToolName : evt.Summary); } else { _appState.CompleteTask(GetAgentTaskKey(evt), evt.Summary, "failed"); _appState.CompleteAgentRun( evt.RunId, "failed", string.IsNullOrWhiteSpace(evt.Summary) ? "에이전트 실행 실패" : evt.Summary, evt.Iteration); } break; case AgentEventType.Thinking: _appState.UpsertTask(GetAgentTaskKey(evt), "agent", "main", evt.Summary, "running"); _appState.UpsertAgentRun( evt.RunId, "running", string.IsNullOrWhiteSpace(evt.Summary) ? "응답을 준비하는 중" : evt.Summary, evt.Iteration); _sessionState.UpsertRuntimeActivity("agent:thinking", "agent", "thinking", evt.Summary); break; case AgentEventType.Planning: _appState.UpsertTask(GetAgentTaskKey(evt), "agent", "main", evt.Summary, "running"); _appState.UpsertAgentRun( evt.RunId, "running", string.IsNullOrWhiteSpace(evt.Summary) ? "작업 계획을 세우는 중" : evt.Summary, evt.Iteration); _sessionState.UpsertRuntimeActivity("agent:planning", "agent", "planning", evt.Summary); break; case AgentEventType.SkillCall: _appState.UpsertTask( GetToolTaskKey(evt), "skill", evt.ToolName, string.IsNullOrWhiteSpace(evt.Summary) ? $"{evt.ToolName} 실행 중" : evt.Summary, "running", evt.FilePath); break; case AgentEventType.Complete: _appState.CompleteTask(GetAgentTaskKey(evt), string.IsNullOrWhiteSpace(evt.Summary) ? "작업 완료" : evt.Summary); _appState.CompleteAgentRun( evt.RunId, "completed", string.IsNullOrWhiteSpace(evt.Summary) ? "작업 완료" : evt.Summary, evt.Iteration); _appState.ClearTasksByPrefix( string.IsNullOrWhiteSpace(evt.RunId) ? "tool:" : $"tool:{evt.RunId}:", string.IsNullOrWhiteSpace(evt.Summary) ? "작업 완료" : evt.Summary); _sessionState.ClearRuntimeActivities(string.IsNullOrWhiteSpace(evt.Summary) ? "작업 완료" : evt.Summary); break; case AgentEventType.Paused: _appState.UpsertAgentRun( evt.RunId, "paused", string.IsNullOrWhiteSpace(evt.Summary) ? "에이전트 일시중지" : evt.Summary, evt.Iteration); break; case AgentEventType.Resumed: _appState.UpsertAgentRun( evt.RunId, "running", string.IsNullOrWhiteSpace(evt.Summary) ? "에이전트 재개" : evt.Summary, evt.Iteration); break; } UpdateRuntimeActivityIndicators(); } private void OnSubAgentStatusChanged(SubAgentStatusEvent evt) { Dispatcher.Invoke(() => { var key = $"subagent:{evt.Id}"; switch (evt.Status) { case SubAgentRunStatus.Started: _appState.UpsertTask(key, "subagent", evt.Id, evt.Summary, "running"); _sessionState.UpsertRuntimeActivity(key, "subagent", evt.Id, evt.Summary); break; case SubAgentRunStatus.Completed: _appState.CompleteTask(key, evt.Summary, "completed"); _sessionState.RemoveRuntimeActivity(key, evt.Summary); break; case SubAgentRunStatus.Failed: _appState.CompleteTask(key, evt.Summary, "failed"); _sessionState.RemoveRuntimeActivity(key, evt.Summary); break; } UpdateRuntimeActivityIndicators(); }); } // ?€?€?€ Task Decomposition UI ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private Border? _planningCard; private StackPanel? _planStepsPanel; private ProgressBar? _planProgressBar; private TextBlock? _planProgressText; /// ?묒뾽 怨꾪쉷 移대뱶瑜??앹꽦?⑸땲??(?④퀎 紐⑸줉 + 吏꾪뻾瑜?諛?. private void AddPlanningCard(AgentEvent evt) { var steps = evt.Steps!; var card = new Border { Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F0F4FF")), CornerRadius = new CornerRadius(10), Padding = new Thickness(14, 10, 14, 10), Margin = new Thickness(40, 4, 80, 4), HorizontalAlignment = HorizontalAlignment.Left, MaxWidth = 560, }; var sp = new StackPanel(); // ?ㅻ뜑 var header = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 6) }; header.Children.Add(new TextBlock { Text = "\uE9D5", // plan icon FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 13, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5EFC")), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), }); header.Children.Add(new TextBlock { Text = $"?묒뾽 怨꾪쉷 ??{steps.Count}?④퀎", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#3730A3")), VerticalAlignment = VerticalAlignment.Center, }); sp.Children.Add(header); // 吏꾪뻾瑜?諛? var progressGrid = new Grid { Margin = new Thickness(0, 0, 0, 8) }; progressGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); progressGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); _planProgressBar = new ProgressBar { Minimum = 0, Maximum = steps.Count, Value = 0, Height = 4, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5EFC")), Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#D0D5FF")), VerticalAlignment = VerticalAlignment.Center, }; // Remove the default border on ProgressBar _planProgressBar.BorderThickness = new Thickness(0); Grid.SetColumn(_planProgressBar, 0); progressGrid.Children.Add(_planProgressBar); _planProgressText = new TextBlock { Text = "0%", FontSize = 10.5, FontWeight = FontWeights.SemiBold, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5EFC")), Margin = new Thickness(8, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center, }; Grid.SetColumn(_planProgressText, 1); progressGrid.Children.Add(_planProgressText); sp.Children.Add(progressGrid); // ?④퀎 紐⑸줉 _planStepsPanel = new StackPanel(); for (int i = 0; i < steps.Count; i++) { var stepRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 1, 0, 1), Tag = i, // ?몃뜳???€?? }; stepRow.Children.Add(new TextBlock { Text = "??, // 鍮???(誘몄셿猷? FontSize = 11, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CA3AF")), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), Tag = "status", }); stepRow.Children.Add(new TextBlock { Text = $"{i + 1}. {steps[i]}", FontSize = 11.5, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5563")), TextWrapping = TextWrapping.Wrap, MaxWidth = 480, VerticalAlignment = VerticalAlignment.Center, }); _planStepsPanel.Children.Add(stepRow); } sp.Children.Add(_planStepsPanel); card.Child = sp; _planningCard = card; // ?섏씠?쒖씤 card.Opacity = 0; card.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(300))); MessagePanel.Children.Add(card); } /// 怨꾪쉷 移대뱶 ?꾨옒???뱀씤/?섏젙/痍⑥냼 ?섏궗寃곗젙 踰꾪듉??異붽??⑸땲?? private void AddDecisionButtons(TaskCompletionSource tcs, List options) { var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var accentColor = ((SolidColorBrush)accentBrush).Color; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var container = new Border { Margin = new Thickness(40, 2, 80, 6), HorizontalAlignment = HorizontalAlignment.Left, MaxWidth = 560, }; var outerStack = new StackPanel(); // 踰꾪듉 ?? var btnRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 0) }; // ?뱀씤 踰꾪듉 (媛뺤“) var approveBtn = new Border { Background = accentBrush, CornerRadius = new CornerRadius(16), Padding = new Thickness(16, 7, 16, 7), Margin = new Thickness(0, 0, 8, 0), Cursor = Cursors.Hand, }; var approveSp = new StackPanel { Orientation = Orientation.Horizontal }; approveSp.Children.Add(new TextBlock { Text = "\uE73E", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 11, Foreground = Brushes.White, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 5, 0), }); approveSp.Children.Add(new TextBlock { Text = "?뱀씤", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White }); approveBtn.Child = approveSp; ApplyMenuItemHover(approveBtn); approveBtn.MouseLeftButtonUp += (_, _) => { CollapseDecisionButtons(outerStack, "???뱀씤??, accentBrush); tcs.TrySetResult(null); // null = ?뱀씤 }; btnRow.Children.Add(approveBtn); // ?섏젙 ?붿껌 踰꾪듉 var editBtn = new Border { Background = new SolidColorBrush(Color.FromArgb(0x18, accentColor.R, accentColor.G, accentColor.B)), CornerRadius = new CornerRadius(16), Padding = new Thickness(14, 7, 14, 7), Margin = new Thickness(0, 0, 8, 0), Cursor = Cursors.Hand, BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, accentColor.R, accentColor.G, accentColor.B)), BorderThickness = new Thickness(1), }; var editSp = new StackPanel { Orientation = Orientation.Horizontal }; editSp.Children.Add(new TextBlock { Text = "\uE70F", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 11, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 5, 0), }); editSp.Children.Add(new TextBlock { Text = "?섏젙 ?붿껌", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = accentBrush }); editBtn.Child = editSp; ApplyMenuItemHover(editBtn); // ?섏젙 ?붿껌???띿뒪???낅젰 ?⑤꼸 (珥덇린 ?④?) var editInputPanel = new Border { Visibility = Visibility.Collapsed, Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F8F9FC")), CornerRadius = new CornerRadius(10), Padding = new Thickness(10, 8, 10, 8), Margin = new Thickness(0, 8, 0, 0), }; var editInputStack = new StackPanel(); editInputStack.Children.Add(new TextBlock { Text = "?섏젙 ?ы빆???낅젰?섏꽭??", FontSize = 11.5, Foreground = secondaryText, Margin = new Thickness(0, 0, 0, 6), }); var editTextBox = new TextBox { MinHeight = 36, MaxHeight = 100, AcceptsReturn = true, TextWrapping = TextWrapping.Wrap, FontSize = 12.5, Background = Brushes.White, BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, accentColor.R, accentColor.G, accentColor.B)), BorderThickness = new Thickness(1), Padding = new Thickness(8, 6, 8, 6), }; editInputStack.Children.Add(editTextBox); var submitEditBtn = new Border { Background = accentBrush, CornerRadius = new CornerRadius(8), Padding = new Thickness(12, 5, 12, 5), Margin = new Thickness(0, 6, 0, 0), Cursor = Cursors.Hand, HorizontalAlignment = HorizontalAlignment.Right, }; submitEditBtn.Child = new TextBlock { Text = "?꾩넚", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White }; ApplyHoverScaleAnimation(submitEditBtn, 1.05); submitEditBtn.MouseLeftButtonUp += (_, _) => { var feedback = editTextBox.Text.Trim(); if (string.IsNullOrEmpty(feedback)) return; CollapseDecisionButtons(outerStack, "???섏젙 ?붿껌??, accentBrush); tcs.TrySetResult(feedback); }; editInputStack.Children.Add(submitEditBtn); editInputPanel.Child = editInputStack; editBtn.MouseLeftButtonUp += (_, _) => { editInputPanel.Visibility = editInputPanel.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible; if (editInputPanel.Visibility == Visibility.Visible) editTextBox.Focus(); }; btnRow.Children.Add(editBtn); // 痍⑥냼 踰꾪듉 var cancelBtn = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(16), Padding = new Thickness(14, 7, 14, 7), Cursor = Cursors.Hand, BorderBrush = new SolidColorBrush(Color.FromArgb(0x30, 0xDC, 0x26, 0x26)), BorderThickness = new Thickness(1), }; var cancelSp = new StackPanel { Orientation = Orientation.Horizontal }; cancelSp.Children.Add(new TextBlock { Text = "\uE711", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 11, Foreground = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 5, 0), }); cancelSp.Children.Add(new TextBlock { Text = "痍⑥냼", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)), }); cancelBtn.Child = cancelSp; ApplyMenuItemHover(cancelBtn); cancelBtn.MouseLeftButtonUp += (_, _) => { CollapseDecisionButtons(outerStack, "??痍⑥냼??, new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26))); tcs.TrySetResult("痍⑥냼"); }; btnRow.Children.Add(cancelBtn); outerStack.Children.Add(btnRow); outerStack.Children.Add(editInputPanel); container.Child = outerStack; // ?щ씪?대뱶 + ?섏씠???깆옣 ?좊땲硫붿씠?? ApplyMessageEntryAnimation(container); MessagePanel.Children.Add(container); ForceScrollToEnd(); // ?섏궗寃곗젙 踰꾪듉 ?쒖떆 ??媛뺤젣 ?섎떒 ?대룞 // PlanViewerWindow ???몃??먯꽌 TCS媛€ ?꾨즺?섎㈃ ?몃씪??踰꾪듉???먮룞 ?묎린 var capturedOuterStack = outerStack; var capturedAccent = accentBrush; _ = tcs.Task.ContinueWith(t => { Dispatcher.BeginInvoke(() => { // ?대? ?묓??덉쑝硫??ㅽ궢 (?몃씪??踰꾪듉?쇰줈 吏곸젒 ?대┃??寃쎌슦) if (capturedOuterStack.Children.Count <= 1) return; var label = t.Result == null ? "???뱀씤?? : t.Result == "痍⑥냼" ? "??痍⑥냼?? : "???섏젙 ?붿껌??; var fg = t.Result == "痍⑥냼" ? new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)) : capturedAccent; CollapseDecisionButtons(capturedOuterStack, label, fg); }); }, TaskScheduler.Default); } /// ?섏궗寃곗젙 踰꾪듉???④린怨?寃곌낵 ?쇰꺼濡?援먯껜?⑸땲?? private void CollapseDecisionButtons(StackPanel outerStack, string resultText, Brush fg) { outerStack.Children.Clear(); var resultLabel = new TextBlock { Text = resultText, FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = fg, Opacity = 0.8, Margin = new Thickness(0, 2, 0, 2), }; outerStack.Children.Add(resultLabel); } // ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧 // ?ㅽ뻾 怨꾪쉷 酉곗뼱 (PlanViewerWindow) ?곕룞 // ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧 /// PlanViewerWindow瑜??ъ슜?섎뒗 UserDecisionCallback???앹꽦?⑸땲?? private Func, Task> CreatePlanDecisionCallback() { return async (planSummary, options) => { var tcs = new TaskCompletionSource(); var steps = Services.Agent.TaskDecomposer.ExtractSteps(planSummary); await Dispatcher.InvokeAsync(() => { // PlanViewerWindow ?앹꽦 ?먮뒗 ?ъ궗?? if (_planViewerWindow == null || !IsWindowAlive(_planViewerWindow)) { _planViewerWindow = new PlanViewerWindow(); _planViewerWindow.Closing += (_, e) => { e.Cancel = true; _planViewerWindow.Hide(); }; } // 怨꾪쉷 ?쒖떆 + ?뱀씤 ?€湲? _planViewerWindow.ShowPlanAsync(planSummary, steps, tcs); // 梨꾪똿 李쎌뿉 媛꾨왂 諛곕꼫 異붽? + ?몃씪???뱀씤 踰꾪듉???쒖떆 AddDecisionButtons(tcs, options); // ?섎떒 諛?怨꾪쉷 踰꾪듉 ?쒖떆 ShowPlanButton(true); }); // 5遺??€?꾩븘?? var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromMinutes(5))); if (completed != tcs.Task) { await Dispatcher.InvokeAsync(() => _planViewerWindow?.Hide()); return "痍⑥냼"; } var result = await tcs.Task; // ?뱀씤??寃쎌슦 ???ㅽ뻾 紐⑤뱶濡??꾪솚 if (result == null) // null = ?뱀씤 { await Dispatcher.InvokeAsync(() => { _planViewerWindow?.SwitchToExecutionMode(); _planViewerWindow?.Hide(); // ?④린怨??섎떒 踰꾪듉?쇰줈 ?ㅼ떆 ?닿린 }); } else { await Dispatcher.InvokeAsync(() => _planViewerWindow?.Hide()); } return result; }; } /// ?섎떒 諛붿뿉 怨꾪쉷 蹂닿린 踰꾪듉???쒖떆/?④??⑸땲?? private void ShowPlanButton(bool show) { if (!show) { // 怨꾪쉷 踰꾪듉 ?쒓굅 for (int i = MoodIconPanel.Children.Count - 1; i >= 0; i--) { if (MoodIconPanel.Children[i] is Border b && b.Tag?.ToString() == "PlanBtn") { // ?욎쓽 援щ텇?좊룄 ?쒓굅 if (i > 0 && MoodIconPanel.Children[i - 1] is Border sep && sep.Tag?.ToString() == "PlanSep") MoodIconPanel.Children.RemoveAt(i - 1); if (i < MoodIconPanel.Children.Count) MoodIconPanel.Children.RemoveAt(Math.Min(i, MoodIconPanel.Children.Count - 1)); break; } } return; } // ?대? ?덉쑝硫?臾댁떆 foreach (var child in MoodIconPanel.Children) { if (child is Border b && b.Tag?.ToString() == "PlanBtn") return; } // 援щ텇?? var separator = new Border { Width = 1, Height = 18, Background = TryFindResource("SeparatorColor") as Brush ?? Brushes.Gray, Margin = new Thickness(4, 0, 4, 0), VerticalAlignment = VerticalAlignment.Center, Tag = "PlanSep", }; MoodIconPanel.Children.Add(separator); // 怨꾪쉷 踰꾪듉 var planBtn = CreateFolderBarButton("\uE9D2", "怨꾪쉷", "?ㅽ뻾 怨꾪쉷 蹂닿린", "#10B981"); planBtn.Tag = "PlanBtn"; planBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow)) { _planViewerWindow.Show(); _planViewerWindow.Activate(); } }; MoodIconPanel.Children.Add(planBtn); } /// 怨꾪쉷 酉곗뼱?먯꽌 ?꾩옱 ?ㅽ뻾 ?④퀎瑜?媛깆떊?⑸땲?? private void UpdatePlanViewerStep(AgentEvent evt) { if (_planViewerWindow == null || !IsWindowAlive(_planViewerWindow)) return; if (evt.StepCurrent > 0) _planViewerWindow.UpdateCurrentStep(evt.StepCurrent - 1); // 0-based } /// 怨꾪쉷 ?ㅽ뻾 ?꾨즺瑜?酉곗뼱???뚮┰?덈떎. private void CompletePlanViewer() { if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow)) _planViewerWindow.MarkComplete(); ShowPlanButton(false); } private static bool IsWindowAlive(Window? w) { if (w == null) return false; try { var _ = w.IsVisible; return true; } catch { return false; } } // ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧 // ?꾩냽 ?묒뾽 ?쒖븞 移?(suggest_actions) // ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧 /// suggest_actions ?꾧뎄 寃곌낵瑜??대┃ 媛€?ν븳 移⑹쑝濡??뚮뜑留곹빀?덈떎. private void RenderSuggestActionChips(string jsonSummary) { // JSON?먯꽌 ?≪뀡 紐⑸줉 ?뚯떛 ?쒕룄 List<(string label, string command)> actions = new(); try { // summary ?뺤떇: "label: command" 以꾨컮轅?援щ텇 ?먮뒗 JSON if (jsonSummary.Contains("\"label\"")) { using var doc = System.Text.Json.JsonDocument.Parse(jsonSummary); if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Array) { foreach (var item in doc.RootElement.EnumerateArray()) { var label = item.TryGetProperty("label", out var l) ? l.GetString() ?? "" : ""; var cmd = item.TryGetProperty("command", out var c) ? c.GetString() ?? label : label; if (!string.IsNullOrEmpty(label)) actions.Add((label, cmd)); } } } else { // 以꾨컮轅??뺤떇: "1. label ??command" foreach (var line in jsonSummary.Split('\n')) { var trimmed = line.Trim().TrimStart('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', ' '); if (string.IsNullOrEmpty(trimmed)) continue; var parts = trimmed.Split('??, ':', '??); if (parts.Length >= 2) actions.Add((parts[0].Trim(), parts[1].Trim())); else if (!string.IsNullOrEmpty(trimmed)) actions.Add((trimmed, trimmed)); } } } catch { return; } if (actions.Count == 0) return; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); var container = new Border { Margin = new Thickness(40, 4, 40, 8), HorizontalAlignment = HorizontalAlignment.Stretch, }; var headerStack = new StackPanel { Margin = new Thickness(0, 0, 0, 6) }; headerStack.Children.Add(new TextBlock { Text = "?뮕 ?ㅼ쓬 ?묒뾽 ?쒖븞:", FontSize = 12, Foreground = secondaryText, }); var chipPanel = new WrapPanel { Margin = new Thickness(0, 2, 0, 0) }; foreach (var (label, command) in actions.Take(5)) { var capturedCmd = command; var chip = new Border { CornerRadius = new CornerRadius(16), Padding = new Thickness(14, 7, 14, 7), Margin = new Thickness(0, 0, 8, 6), Cursor = Cursors.Hand, Background = new SolidColorBrush(Color.FromArgb(0x15, ((SolidColorBrush)accentBrush).Color.R, ((SolidColorBrush)accentBrush).Color.G, ((SolidColorBrush)accentBrush).Color.B)), BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, ((SolidColorBrush)accentBrush).Color.R, ((SolidColorBrush)accentBrush).Color.G, ((SolidColorBrush)accentBrush).Color.B)), BorderThickness = new Thickness(1), }; chip.Child = new TextBlock { Text = label, FontSize = 12.5, Foreground = accentBrush, FontWeight = FontWeights.SemiBold, }; chip.MouseEnter += (s, _) => ((Border)s).Opacity = 0.8; chip.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0; chip.MouseLeftButtonUp += (_, _) => { // 移??⑤꼸 ?쒓굅 ???대떦 紐낅졊 ?ㅽ뻾 MessagePanel.Children.Remove(container); if (capturedCmd.StartsWith("/")) { InputBox.Text = capturedCmd + " "; InputBox.CaretIndex = InputBox.Text.Length; InputBox.Focus(); } else { InputBox.Text = capturedCmd; _ = SendMessageAsync(); } }; chipPanel.Children.Add(chip); } var outerStack = new StackPanel(); outerStack.Children.Add(headerStack); outerStack.Children.Add(chipPanel); container.Child = outerStack; ApplyMessageEntryAnimation(container); MessagePanel.Children.Add(container); ForceScrollToEnd(); } // ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧 // ?쇰뱶諛??숈뒿 諛섏쁺 (J) // ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧 /// 理쒓렐 ?€?붿쓽 ?쇰뱶諛?醫뗭븘???レ뼱????遺꾩꽍?섏뿬 ?좏샇???붿빟??諛섑솚?⑸땲?? private string BuildFeedbackContext() { try { var recentConversations = _storage.LoadAllMeta() .OrderByDescending(m => m.UpdatedAt) .Take(20) .ToList(); var likedPatterns = new List(); var dislikedPatterns = new List(); foreach (var meta in recentConversations) { var conv = _storage.Load(meta.Id); if (conv == null) continue; foreach (var msg in conv.Messages.Where(m => m.Role == "assistant" && m.Feedback != null)) { // 泥?50?먮줈 ?⑦꽩 ?뚯븙 var preview = msg.Content?.Length > 80 ? msg.Content[..80] : msg.Content ?? ""; if (msg.Feedback == "like") likedPatterns.Add(preview); else if (msg.Feedback == "dislike") dislikedPatterns.Add(preview); } } if (likedPatterns.Count == 0 && dislikedPatterns.Count == 0) return ""; var sb = new System.Text.StringBuilder(); sb.AppendLine("\n[?ъ슜???좏샇??李멸퀬]"); if (likedPatterns.Count > 0) { sb.AppendLine($"?ъ슜?먭? 醫뗭븘???묐떟 ?ㅽ???({likedPatterns.Count}嫄?:"); foreach (var p in likedPatterns.Take(5)) sb.AppendLine($" - \"{p}...\""); } if (dislikedPatterns.Count > 0) { sb.AppendLine($"?ъ슜?먭? ?レ뼱???묐떟 ?ㅽ???({dislikedPatterns.Count}嫄?:"); foreach (var p in dislikedPatterns.Take(5)) sb.AppendLine($" - \"{p}...\""); } sb.AppendLine("???좏샇?꾨? 李멸퀬?섏뿬 ?묐떟 ?ㅽ??쇱쓣 議곗젙?섏꽭??"); return sb.ToString(); } catch { return ""; } } /// 吏꾪뻾瑜?諛붿? ?④퀎 ?곹깭瑜??낅뜲?댄듃?⑸땲?? private void UpdateProgressBar(AgentEvent evt) { if (_planProgressBar == null || _planStepsPanel == null || _planProgressText == null) return; var stepIdx = evt.StepCurrent - 1; // 0-based var total = evt.StepTotal; // 吏꾪뻾瑜?諛??낅뜲?댄듃 _planProgressBar.Value = evt.StepCurrent; var pct = (int)((double)evt.StepCurrent / total * 100); _planProgressText.Text = $"{pct}%"; // ?댁쟾 ?④퀎 ?꾨즺 ?쒖떆 + ?꾩옱 ?④퀎 媛뺤“ for (int i = 0; i < _planStepsPanel.Children.Count; i++) { if (_planStepsPanel.Children[i] is StackPanel row && row.Children.Count >= 2) { var statusTb = row.Children[0] as TextBlock; var textTb = row.Children[1] as TextBlock; if (statusTb == null || textTb == null) continue; if (i < stepIdx) { // ?꾨즺 statusTb.Text = "??; statusTb.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#16A34A")); textTb.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6B7280")); } else if (i == stepIdx) { // ?꾩옱 吏꾪뻾 以? statusTb.Text = "??; statusTb.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5EFC")); textTb.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1E293B")); textTb.FontWeight = FontWeights.SemiBold; } else { // ?€湲? statusTb.Text = "??; statusTb.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CA3AF")); textTb.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5563")); textTb.FontWeight = FontWeights.Normal; } } } } /// Diff ?띿뒪?몃? ?됱긽 ?섏씠?쇱씠?낅맂 StackPanel濡??뚮뜑留곹빀?덈떎. private static UIElement BuildDiffView(string text) { var panel = new StackPanel { Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FAFAFA")), MaxWidth = 520, }; var diffStarted = false; foreach (var rawLine in text.Split('\n')) { var line = rawLine.TrimEnd('\r'); // diff ?ㅻ뜑 ?꾩쓽 ?쇰컲 ?띿뒪?? if (!diffStarted && !line.StartsWith("--- ")) { panel.Children.Add(new TextBlock { Text = line, FontSize = 11, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5563")), FontFamily = new FontFamily("Consolas"), Margin = new Thickness(0, 0, 0, 1), }); continue; } diffStarted = true; string bgHex, fgHex; if (line.StartsWith("---") || line.StartsWith("+++")) { bgHex = "#F3F4F6"; fgHex = "#374151"; } else if (line.StartsWith("@@")) { bgHex = "#EFF6FF"; fgHex = "#3B82F6"; } else if (line.StartsWith("+")) { bgHex = "#ECFDF5"; fgHex = "#059669"; } else if (line.StartsWith("-")) { bgHex = "#FEF2F2"; fgHex = "#DC2626"; } else { bgHex = "Transparent"; fgHex = "#6B7280"; } var tb = new TextBlock { Text = line, FontSize = 10.5, FontFamily = new FontFamily("Consolas"), Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(fgHex)), Padding = new Thickness(4, 1, 4, 1), }; if (bgHex != "Transparent") tb.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString(bgHex)); panel.Children.Add(tb); } return panel; } private void AddAgentEventBanner(AgentEvent evt, bool animate = true) { var logLevel = _settings.Settings.Llm.AgentLogLevel; // Planning ?대깽?몃뒗 ?④퀎 紐⑸줉 移대뱶濡?蹂꾨룄 ?뚮뜑留? if (evt.Type == AgentEventType.Planning && evt.Steps is { Count: > 0 }) { AddPlanningCard(evt); return; } // StepStart ?대깽?몃뒗 吏꾪뻾瑜?諛??낅뜲?댄듃 if (evt.Type == AgentEventType.StepStart && evt.StepTotal > 0) { UpdateProgressBar(evt); return; } // simple 紐⑤뱶: ToolCall?€ 嫄대꼫?€ (ToolResult留???以꾨줈 ?쒖떆) if (logLevel == "simple" && evt.Type == AgentEventType.ToolCall) return; // ?꾩껜 ?듦퀎 ?대깽?몃뒗 蹂꾨룄 ?됱긽 (蹂대씪??怨꾩뿴) var isTotalStats = evt.Type == AgentEventType.StepDone && evt.ToolName == "total_stats"; var (icon, label, bgHex, fgHex) = isTotalStats ? ("\uE9D2", "Total Stats", "#F3EEFF", "#7C3AED") : evt.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"), }; var banner = new Border { Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString(bgHex)), CornerRadius = new CornerRadius(8), Padding = new Thickness(12, 8, 12, 8), Margin = new Thickness(40, 2, 40, 2), HorizontalAlignment = HorizontalAlignment.Stretch, }; var sp = new StackPanel(); // ?ㅻ뜑: Grid濡?醫뚯륫(?꾩씠肄??쇰꺼) / ?곗륫(?€?대컢+?좏겙) 遺꾨━ 怨좎젙 var headerGrid = new Grid(); headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // 醫뚯륫: ?꾩씠肄?+ ?쇰꺼 var headerLeft = new StackPanel { Orientation = Orientation.Horizontal }; headerLeft.Children.Add(new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 11, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(fgHex)), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), }); headerLeft.Children.Add(new TextBlock { Text = label, FontSize = 11.5, FontWeight = FontWeights.SemiBold, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(fgHex)), VerticalAlignment = VerticalAlignment.Center, }); Grid.SetColumn(headerLeft, 0); // ?곗륫: ?뚯슂 ?쒓컙 + ?좏겙 諛곗? (??긽 ?곗륫 ?앹뿉 怨좎젙) var headerRight = new StackPanel { Orientation = Orientation.Horizontal }; if (logLevel != "simple" && evt.ElapsedMs > 0) { headerRight.Children.Add(new TextBlock { Text = evt.ElapsedMs < 1000 ? $"{evt.ElapsedMs}ms" : $"{evt.ElapsedMs / 1000.0:F1}s", FontSize = 10, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CA3AF")), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(8, 0, 0, 0), }); } if (logLevel != "simple" && (evt.InputTokens > 0 || evt.OutputTokens > 0)) { var tokenText = evt.InputTokens > 0 && evt.OutputTokens > 0 ? $"{evt.InputTokens}??evt.OutputTokens}t" : evt.InputTokens > 0 ? $"??evt.InputTokens}t" : $"??evt.OutputTokens}t"; headerRight.Children.Add(new Border { Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F0F0F5")), CornerRadius = new CornerRadius(4), Padding = new Thickness(5, 1, 5, 1), Margin = new Thickness(6, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center, Child = new TextBlock { Text = tokenText, FontSize = 9.5, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#8B8FA3")), FontFamily = new FontFamily("Consolas"), }, }); } Grid.SetColumn(headerRight, 1); headerGrid.Children.Add(headerLeft); headerGrid.Children.Add(headerRight); // header 蹂€?섎? headerLeft濡??ㅼ젙 (?댄썑 expandIcon 異붽? ???ъ슜) var header = headerLeft; sp.Children.Add(headerGrid); // simple 紐⑤뱶: ?붿빟 ??以꾨쭔 ?쒖떆 (?묎린 ?놁쓬) if (logLevel == "simple") { if (!string.IsNullOrEmpty(evt.Summary)) { var shortSummary = evt.Summary.Length > 100 ? evt.Summary[..100] + "?? : evt.Summary; sp.Children.Add(new TextBlock { Text = shortSummary, FontSize = 11, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6B7280")), TextWrapping = TextWrapping.NoWrap, TextTrimming = TextTrimming.CharacterEllipsis, Margin = new Thickness(0, 2, 0, 0), }); } } // detailed/debug 紐⑤뱶: 湲곗〈 ?묒씠???쒖떆 else if (!string.IsNullOrEmpty(evt.Summary)) { var summaryText = evt.Summary; var isExpandable = (evt.Type == AgentEventType.ToolCall || evt.Type == AgentEventType.ToolResult) && summaryText.Length > 60; if (isExpandable) { // 泥?以꾨쭔 ?쒖떆?섍퀬 ?대┃?섎㈃ ?꾩껜 ?댁슜 ?쇱묠 var shortText = summaryText.Length > 80 ? summaryText[..80] + "..." : summaryText; var summaryTb = new TextBlock { Text = shortText, FontSize = 11.5, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5563")), TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 3, 0, 0), Cursor = Cursors.Hand, }; // Diff媛€ ?ы븿??寃쎌슦 ?됱긽 ?섏씠?쇱씠???곸슜 var hasDiff = summaryText.Contains("--- ") && summaryText.Contains("+++ "); UIElement fullContent; if (hasDiff) { fullContent = BuildDiffView(summaryText); } else { fullContent = new TextBlock { Text = summaryText, FontSize = 11, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6B7280")), TextWrapping = TextWrapping.Wrap, FontFamily = new FontFamily("Consolas"), }; } fullContent.Visibility = Visibility.Collapsed; ((FrameworkElement)fullContent).Margin = new Thickness(0, 4, 0, 0); // ?쇱묠/?묎린 ?좉? var expandIcon = new TextBlock { Text = "\uE70D", // ChevronDown FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 9, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CA3AF")), Margin = new Thickness(6, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center, }; header.Children.Add(expandIcon); var isExpanded = false; banner.MouseLeftButtonDown += (_, _) => { isExpanded = !isExpanded; fullContent.Visibility = isExpanded ? Visibility.Visible : Visibility.Collapsed; summaryTb.Visibility = isExpanded ? Visibility.Collapsed : Visibility.Visible; expandIcon.Text = isExpanded ? "\uE70E" : "\uE70D"; // ChevronUp : ChevronDown }; sp.Children.Add(summaryTb); sp.Children.Add(fullContent); } else { sp.Children.Add(new TextBlock { Text = summaryText, FontSize = 11.5, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5563")), TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 3, 0, 0), }); } } // debug 紐⑤뱶: ToolInput ?뚮씪誘명꽣 ?쒖떆 if (logLevel == "debug" && !string.IsNullOrEmpty(evt.ToolInput)) { sp.Children.Add(new Border { Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F8F8FC")), CornerRadius = new CornerRadius(4), Padding = new Thickness(8, 4, 8, 4), Margin = new Thickness(0, 4, 0, 0), Child = new TextBlock { Text = evt.ToolInput.Length > 500 ? evt.ToolInput[..500] + "?? : evt.ToolInput, FontSize = 10, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#7C7F93")), FontFamily = new FontFamily("Consolas"), TextWrapping = TextWrapping.Wrap, }, }); } // ?뚯씪 寃쎈줈 諛곕꼫 (Claude ?ㅽ??? if (!string.IsNullOrEmpty(evt.FilePath)) { var pathBorder = new Border { Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F8FAFC")), CornerRadius = new CornerRadius(4), Padding = new Thickness(8, 4, 8, 4), Margin = new Thickness(0, 4, 0, 0), }; var pathPanel = new StackPanel { Orientation = Orientation.Horizontal }; pathPanel.Children.Add(new TextBlock { Text = "\uE8B7", // folder icon FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 10, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CA3AF")), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0), }); pathPanel.Children.Add(new TextBlock { Text = evt.FilePath, FontSize = 10.5, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6B7280")), FontFamily = new FontFamily("Consolas"), VerticalAlignment = VerticalAlignment.Center, TextTrimming = TextTrimming.CharacterEllipsis, }); // 鍮좊Ⅸ ?묒뾽 踰꾪듉?? var quickActions = BuildFileQuickActions(evt.FilePath); pathPanel.Children.Add(quickActions); pathBorder.Child = pathPanel; sp.Children.Add(pathBorder); } banner.Child = sp; // Total Stats 諛곕꼫 ?대┃ ???뚰겕?뚮줈??遺꾩꽍湲?蹂묐ぉ 遺꾩꽍 ???닿린 if (isTotalStats) { banner.Cursor = Cursors.Hand; banner.ToolTip = "?대┃?섏뿬 蹂묐ぉ 遺꾩꽍 蹂닿린"; banner.MouseLeftButtonUp += (_, _) => { OpenWorkflowAnalyzerIfEnabled(); _analyzerWindow?.SwitchToBottleneckTab(); _analyzerWindow?.Activate(); }; } // ?섏씠?쒖씤 ?좊땲硫붿씠?? if (animate) { banner.Opacity = 0; banner.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200))); } MessagePanel.Children.Add(banner); } /// ?뚯씪 鍮좊Ⅸ ?묒뾽 踰꾪듉 ?⑤꼸???앹꽦?⑸땲?? private StackPanel BuildFileQuickActions(string filePath) { var panel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(6, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center, }; var accentColor = (Color)ColorConverter.ConvertFromString("#3B82F6"); var accentBrush = new SolidColorBrush(accentColor); Border MakeBtn(string mdlIcon, string label, Action action) { var sp = new StackPanel { Orientation = Orientation.Horizontal }; sp.Children.Add(new TextBlock { Text = mdlIcon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 9, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 3, 0), }); sp.Children.Add(new TextBlock { Text = label, FontSize = 10, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, }); var btn = new Border { Child = sp, Background = Brushes.Transparent, CornerRadius = new CornerRadius(4), Padding = new Thickness(5, 2, 5, 2), Cursor = Cursors.Hand, }; btn.MouseEnter += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x15, 0x3B, 0x82, 0xF6)); }; btn.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; btn.MouseLeftButtonUp += (_, _) => action(); return btn; } // ?꾨━酉?(吏€???뺤옣?먮쭔) var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant(); if (_previewableExtensions.Contains(ext)) { var path1 = filePath; panel.Children.Add(MakeBtn("\uE8A1", "?꾨━酉?, () => ShowPreviewPanel(path1))); } // ?몃? ?닿린 var path2 = filePath; panel.Children.Add(MakeBtn("\uE8A7", "?닿린", () => { try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path2, UseShellExecute = true }); } catch { } })); // ?대뜑 ?닿린 var path3 = filePath; panel.Children.Add(MakeBtn("\uED25", "?대뜑", () => { try { System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path3}\""); } catch { } })); // 寃쎈줈 蹂듭궗 var path4 = filePath; panel.Children.Add(MakeBtn("\uE8C8", "蹂듭궗", () => { try { Clipboard.SetText(path4); // 1.5珥??쇰뱶諛? "蹂듭궗?? ?쒖떆 if (panel.Children[^1] is Border lastBtn && lastBtn.Child is StackPanel lastSp) { var origLabel = lastSp.Children.OfType().LastOrDefault(); if (origLabel != null) { var prev = origLabel.Text; origLabel.Text = "蹂듭궗????; var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1500) }; timer.Tick += (_, _) => { origLabel.Text = prev; timer.Stop(); }; timer.Start(); } } } catch { } })); return panel; } // ?€?€?€ ?묐떟 ?ъ깮???€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private async Task RegenerateLastAsync() { if (_isStreaming) return; ChatConversation conv; lock (_convLock) { if (_currentConversation == null) return; conv = _currentConversation; } // 留덉?留?assistant 硫붿떆吏€ ?쒓굅 lock (_convLock) { if (conv.Messages.Count > 0 && conv.Messages[^1].Role == "assistant") conv.Messages.RemoveAt(conv.Messages.Count - 1); } // UI?먯꽌 留덉?留?AI ?묐떟 ?쒓굅 if (MessagePanel.Children.Count > 0) MessagePanel.Children.RemoveAt(MessagePanel.Children.Count - 1); // ?ъ쟾?? await SendRegenerateAsync(conv); } /// "?섏젙 ???ъ떆?? ???쇰뱶諛??낅젰 ?⑤꼸???쒖떆?섍퀬, ?ъ슜??吏€?쒕? 異붽??섏뿬 ?ъ깮?깊빀?덈떎. private void ShowRetryWithFeedbackInput() { if (_isStreaming) return; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40)); var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var container = new Border { Margin = new Thickness(40, 4, 40, 8), Padding = new Thickness(14, 10, 14, 10), CornerRadius = new CornerRadius(12), Background = itemBg, HorizontalAlignment = HorizontalAlignment.Stretch, }; var stack = new StackPanel(); stack.Children.Add(new TextBlock { Text = "?대뼸寃??섏젙?섎㈃ 醫뗭쓣吏€ ?뚮젮二쇱꽭??", FontSize = 12, Foreground = secondaryText, Margin = new Thickness(0, 0, 0, 6), }); var textBox = new TextBox { MinHeight = 38, MaxHeight = 80, AcceptsReturn = true, TextWrapping = TextWrapping.Wrap, FontSize = 13, Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.Black, Foreground = primaryText, CaretBrush = primaryText, BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(10, 6, 10, 6), }; stack.Children.Add(textBox); var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 8, 0, 0), }; var sendBtn = new Border { Background = accentBrush, CornerRadius = new CornerRadius(8), Padding = new Thickness(14, 6, 14, 6), Cursor = Cursors.Hand, Margin = new Thickness(6, 0, 0, 0), }; sendBtn.Child = new TextBlock { Text = "?ъ떆??, FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White }; sendBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85; sendBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0; sendBtn.MouseLeftButtonUp += (_, _) => { var feedback = textBox.Text.Trim(); if (string.IsNullOrEmpty(feedback)) return; MessagePanel.Children.Remove(container); _ = RetryWithFeedbackAsync(feedback); }; var cancelBtn = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(8), Padding = new Thickness(12, 6, 12, 6), Cursor = Cursors.Hand, }; cancelBtn.Child = new TextBlock { Text = "痍⑥냼", FontSize = 12, Foreground = secondaryText }; cancelBtn.MouseLeftButtonUp += (_, _) => MessagePanel.Children.Remove(container); btnRow.Children.Add(cancelBtn); btnRow.Children.Add(sendBtn); stack.Children.Add(btnRow); container.Child = stack; 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; } // 留덉?留?assistant 硫붿떆吏€ ?쒓굅 lock (_convLock) { if (conv.Messages.Count > 0 && conv.Messages[^1].Role == "assistant") conv.Messages.RemoveAt(conv.Messages.Count - 1); } // UI?먯꽌 留덉?留?AI ?묐떟 ?쒓굅 if (MessagePanel.Children.Count > 0) MessagePanel.Children.RemoveAt(MessagePanel.Children.Count - 1); // ?쇰뱶諛깆쓣 ?ъ슜??硫붿떆吏€濡?異붽? var feedbackMsg = new ChatMessage { Role = "user", Content = $"[?댁쟾 ?묐떟???€???섏젙 ?붿껌] {feedback}\n\n???쇰뱶諛깆쓣 諛섏쁺?섏뿬 ?ㅼ떆 ?묒꽦?댁<?몄슂." }; lock (_convLock) conv.Messages.Add(feedbackMsg); // ?쇰뱶諛?硫붿떆吏€ UI ?쒖떆 AddMessageBubble("user", $"[?섏젙 ?붿껌] {feedback}", true); // ?ъ쟾?? 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(); var assistantMsg = new ChatMessage { Role = "assistant", Content = "" }; lock (_convLock) conv.Messages.Add(assistantMsg); var streamContainer = CreateStreamingContainer(out var streamText); MessagePanel.Children.Add(streamContainer); ForceScrollToEnd(); // ?묐떟 ?쒖옉 ??媛뺤젣 ?섎떒 ?대룞 var sb = new System.Text.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 (var chunk in _llm.StreamAsync(sendMessages, _streamCts.Token)) { sb.Append(chunk); StopAiIconPulse(); _cachedStreamContent = sb.ToString(); await Dispatcher.InvokeAsync(() => { }, DispatcherPriority.Background); } _cachedStreamContent = sb.ToString(); assistantMsg.Content = _cachedStreamContent; // ?€?댄븨 ?좊땲硫붿씠?섏씠 ?⑥? 踰꾪띁瑜??뚯쭊???뚭퉴吏€ ?€湲?(理쒕? 600ms) var drainStart2 = DateTime.UtcNow; while (_displayedLength < _cachedStreamContent.Length && (DateTime.UtcNow - drainStart2).TotalMilliseconds < 600) { await Dispatcher.InvokeAsync(() => { }, DispatcherPriority.Background); } } catch (OperationCanceledException) { if (sb.Length == 0) sb.Append("(痍⑥냼??"); assistantMsg.Content = sb.ToString(); } catch (Exception ex) { var errMsg = $"???ㅻ쪟: {ex.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 ex) { Services.LogService.Debug($"?€???€???ㅽ뙣: {ex.Message}"); } _sessionState.RememberConversation(conv.Tab ?? _activeTab, conv.Id); RefreshConversationList(); } /// 硫붿떆吏€ 踰꾨툝??MaxWidth瑜?李??덈퉬??鍮꾨??섏뿬 怨꾩궛?⑸땲??(理쒖냼 500, 理쒕? 1200). private double GetMessageMaxWidth() { var scrollWidth = MessageScroll.ActualWidth; if (scrollWidth < 100) scrollWidth = 700; // 珥덇린????湲곕낯媛? // 醫뚯슦 留덉쭊(40+80=120)??鍮쇨퀬 ?꾩껜??90% var maxW = (scrollWidth - 120) * 0.90; return Math.Clamp(maxW, 500, 1200); } private StackPanel CreateStreamingContainer(out TextBlock streamText) { var msgMaxWidth = GetMessageMaxWidth(); var container = new StackPanel { HorizontalAlignment = HorizontalAlignment.Left, Width = msgMaxWidth, MaxWidth = msgMaxWidth, Margin = new Thickness(40, 8, 80, 8), Opacity = 0, RenderTransform = new TranslateTransform(0, 10) }; // 而⑦뀒?대꼫 ?섏씠?쒖씤 + ?щ씪?대뱶 ?? container.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(280))); ((TranslateTransform)container.RenderTransform).BeginAnimation( TranslateTransform.YProperty, new DoubleAnimation(10, 0, TimeSpan.FromMilliseconds(300)) { EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } }); var headerGrid = new Grid { Margin = new Thickness(0, 0, 0, 4) }; headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); var aiIcon = new TextBlock { Text = "\uE8BD", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = TryFindResource("AccentColor") as Brush ?? Brushes.Blue, VerticalAlignment = VerticalAlignment.Center }; // AI ?꾩씠肄??꾩뒪 ?좊땲硫붿씠??(?묐떟 ?€湲?以? aiIcon.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(1.0, 0.35, TimeSpan.FromMilliseconds(700)) { AutoReverse = true, RepeatBehavior = RepeatBehavior.Forever, EasingFunction = new SineEase() }); _activeAiIcon = aiIcon; Grid.SetColumn(aiIcon, 0); headerGrid.Children.Add(aiIcon); var (streamAgentName, _, _) = GetAgentIdentity(); var aiNameTb = new TextBlock { Text = streamAgentName, FontSize = 11, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("AccentColor") as Brush ?? Brushes.Blue, Margin = new Thickness(6, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center }; Grid.SetColumn(aiNameTb, 1); headerGrid.Children.Add(aiNameTb); // ?ㅼ떆媛?寃쎄낵 ?쒓컙 (?ㅻ뜑 ?곗륫) _elapsedLabel = new TextBlock { Text = "0s", FontSize = 10.5, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center, Opacity = 0.5, }; Grid.SetColumn(_elapsedLabel, 2); headerGrid.Children.Add(_elapsedLabel); container.Children.Add(headerGrid); streamText = new TextBlock { Text = "\u258c", // 釉붾줉 而ㅼ꽌留??쒖떆 (泥?泥?겕 ?? FontSize = 13.5, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, TextWrapping = TextWrapping.Wrap, LineHeight = 22, }; container.Children.Add(streamText); return container; } // ?€?€?€ ?ㅽ듃由щ컢 ?꾨즺 ??留덊겕?ㅼ슫 ?뚮뜑留곸쑝濡?援먯껜 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void FinalizeStreamingContainer(StackPanel container, TextBlock streamText, string finalContent, ChatMessage? message = null) { // ?ㅽ듃由щ컢 plaintext 釉붾줉 ?쒓굅 container.Children.Remove(streamText); // 留덊겕?ㅼ슫 ?뚮뜑留? var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue; var codeBgBrush = TryFindResource("HintBackground") as Brush ?? Brushes.DarkGray; var mdPanel = MarkdownRenderer.Render(finalContent, primaryText, secondaryText, accentBrush, codeBgBrush); mdPanel.Margin = new Thickness(0, 0, 0, 4); mdPanel.Opacity = 0; container.Children.Add(mdPanel); mdPanel.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(180))); // ?≪뀡 踰꾪듉 諛?+ ?좏겙 ?쒖떆 var btnColor = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var capturedContent = finalContent; var actionBar = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Left, Margin = new Thickness(0, 6, 0, 0) }; actionBar.Children.Add(CreateActionButton("\uE8C8", "蹂듭궗", btnColor, () => { try { Clipboard.SetText(capturedContent); } catch { } })); actionBar.Children.Add(CreateActionButton("\uE72C", "?ㅼ떆 ?앹꽦", btnColor, () => _ = RegenerateLastAsync())); actionBar.Children.Add(CreateActionButton("\uE70F", "?섏젙 ???ъ떆??, btnColor, () => ShowRetryWithFeedbackInput())); AddLinkedFeedbackButtons(actionBar, btnColor, message); container.Children.Add(actionBar); // 寃쎄낵 ?쒓컙 + ?좏겙 ?ъ슜??(?곗륫 ?섎떒, 蹂꾨룄 以? var elapsed = DateTime.UtcNow - _streamStartTime; var elapsedText = elapsed.TotalSeconds < 60 ? $"{elapsed.TotalSeconds:0.#}s" : $"{(int)elapsed.TotalMinutes}m {elapsed.Seconds}s"; var usage = _llm.LastTokenUsage; // ?먯씠?꾪듃 猷⑦봽(Cowork/Code)?먯꽌???꾩쟻 ?좏겙 ?ъ슜, ?쇰컲 ?€?붿뿉?쒕뒗 留덉?留??몄텧 ?좏겙 ?ъ슜 var isAgentTab = _activeTab is "Cowork" or "Code"; var displayInput = isAgentTab && _agentCumulativeInputTokens > 0 ? _agentCumulativeInputTokens : usage?.PromptTokens ?? 0; var displayOutput = isAgentTab && _agentCumulativeOutputTokens > 0 ? _agentCumulativeOutputTokens : usage?.CompletionTokens ?? 0; if (displayInput > 0 || displayOutput > 0) { UpdateStatusTokens(displayInput, displayOutput); Services.UsageStatisticsService.RecordTokens(displayInput, displayOutput); } string tokenText; if (displayInput > 0 || displayOutput > 0) tokenText = $"{FormatTokenCount(displayInput)} + {FormatTokenCount(displayOutput)} = {FormatTokenCount(displayInput + displayOutput)} tokens"; else if (usage != null) tokenText = $"{FormatTokenCount(usage.PromptTokens)} + {FormatTokenCount(usage.CompletionTokens)} = {FormatTokenCount(usage.TotalTokens)} tokens"; else tokenText = $"~{FormatTokenCount(EstimateTokenCount(finalContent))} tokens"; var metaText = new TextBlock { Text = $"{elapsedText} 쨌 {tokenText}", FontSize = 10.5, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 6, 0, 0), Opacity = 0.6, }; container.Children.Add(metaText); // Suggestion chips ??AI媛€ 踰덊샇 ?좏깮吏€瑜??쒖떆??寃쎌슦 ?대┃ 媛€??踰꾪듉 ?쒖떆 var chips = ParseSuggestionChips(finalContent); if (chips.Count > 0) { var chipPanel = new WrapPanel { Margin = new Thickness(0, 8, 0, 4), HorizontalAlignment = HorizontalAlignment.Left, }; foreach (var (num, label) in chips) { var chipBorder = new Border { Background = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent, BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(16), Padding = new Thickness(14, 7, 14, 7), Margin = new Thickness(0, 0, 8, 6), Cursor = Cursors.Hand, RenderTransformOrigin = new Point(0.5, 0.5), RenderTransform = new ScaleTransform(1, 1), }; chipBorder.Child = new TextBlock { Text = $"{num}. {label}", FontSize = 12.5, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White, }; var chipHover = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent; var chipNormal = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent; chipBorder.MouseEnter += (s, _) => { if (s is Border b && b.RenderTransform is ScaleTransform st) { st.ScaleX = 1.02; st.ScaleY = 1.02; b.Background = chipHover; } }; chipBorder.MouseLeave += (s, _) => { if (s is Border b && b.RenderTransform is ScaleTransform st) { st.ScaleX = 1.0; st.ScaleY = 1.0; b.Background = chipNormal; } }; var capturedLabel = $"{num}. {label}"; var capturedPanel = chipPanel; chipBorder.MouseLeftButtonDown += (_, _) => { // 移??⑤꼸 ?쒓굅 (1?뚯슜) if (capturedPanel.Parent is Panel parent) parent.Children.Remove(capturedPanel); // ?좏깮???듭뀡???ъ슜??硫붿떆吏€濡??꾩넚 InputBox.Text = capturedLabel; _ = SendMessageAsync(); }; chipPanel.Children.Add(chipBorder); } container.Children.Add(chipPanel); } } /// AI ?묐떟?먯꽌 踰덊샇 ?좏깮吏€瑜??뚯떛?⑸땲?? (1. xxx / 2. xxx ?⑦꽩) private static List<(string Num, string Label)> ParseSuggestionChips(string content) { var chips = new List<(string, string)>(); if (string.IsNullOrEmpty(content)) return chips; var lines = content.Split('\n'); // 留덉?留?踰덊샇 紐⑸줉 釉붾줉??李얠쓬 (?곗냽??踰덊샇 ?쇱씤) var candidates = new List<(string, string)>(); var lastBlockStart = -1; for (int i = 0; i < lines.Length; i++) { var line = lines[i].Trim(); // "1. xxx", "2) xxx", "??xxx" ??踰덊샇 ?⑦꽩 var m = System.Text.RegularExpressions.Regex.Match(line, @"^(\d+)[.\)]\s+(.+)$"); if (m.Success) { if (lastBlockStart < 0 || i == lastBlockStart + candidates.Count) { if (lastBlockStart < 0) { lastBlockStart = i; candidates.Clear(); } candidates.Add((m.Groups[1].Value, m.Groups[2].Value.TrimEnd())); } else { // ?덈줈??釉붾줉 ?쒖옉 lastBlockStart = i; candidates.Clear(); candidates.Add((m.Groups[1].Value, m.Groups[2].Value.TrimEnd())); } } else if (!string.IsNullOrWhiteSpace(line)) { // 踰덊샇 紐⑸줉???꾨땶 以꾩씠 ?섏삤硫?釉붾줉 由ъ뀑 lastBlockStart = -1; candidates.Clear(); } // 鍮?以꾩? 釉붾줉 ?좎? (踰덊샇 紐⑸줉 ?ъ씠 鍮?以??덉슜) } // 2媛??댁긽 ?좏깮吏€, 10媛??댄븯???뚮쭔 chips濡??쒖떆 if (candidates.Count >= 2 && candidates.Count <= 10) chips.AddRange(candidates); return chips; } /// ?좏겙 ?섎? k/m ?⑥쐞濡??щ㎎ private static string FormatTokenCount(int count) => count switch { >= 1_000_000 => $"{count / 1_000_000.0:0.#}m", >= 1_000 => $"{count / 1_000.0:0.#}k", _ => count.ToString(), }; /// ?좏겙 ??異붿젙 (?쒓뎅??3???좏겙, ?곸뼱~4???좏겙, ?쇳빀 ?됯퇏 ~3???좏겙) private static int EstimateTokenCount(string text) { if (string.IsNullOrEmpty(text)) return 0; // ?쒓뎅??臾몄옄 鍮꾩쑉???곕씪 媛€以? int cjk = 0; foreach (var c in text) if (c >= 0xAC00 && c <= 0xD7A3 || c >= 0x3000 && c <= 0x9FFF) cjk++; double ratio = text.Length > 0 ? (double)cjk / text.Length : 0; double charsPerToken = 4.0 - ratio * 2.0; // ?곸뼱 4, ?쒓뎅??2 return Math.Max(1, (int)Math.Round(text.Length / charsPerToken)); } // ?€?€?€ ?앹꽦 以묒? ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void StopGeneration() { _streamCts?.Cancel(); } // ?€?€?€ ?€???대낫?닿린 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ // ?€?€?€ ?€??遺꾧린 (Fork) ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void ForkConversation(ChatConversation source, int atIndex) { var branchCount = _storage.LoadAllMeta() .Count(m => m.ParentId == source.Id) + 1; var fork = new ChatConversation { Title = $"{source.Title} (遺꾧린 {branchCount})", Tab = source.Tab, Category = source.Category, WorkFolder = source.WorkFolder, SystemCommand = source.SystemCommand, ParentId = source.Id, BranchLabel = $"遺꾧린 {branchCount}", BranchAtIndex = atIndex, }; // 遺꾧린 ?쒖젏源뚯???硫붿떆吏€ 蹂듭젣 for (int i = 0; i <= atIndex && i < source.Messages.Count; i++) { var m = source.Messages[i]; fork.Messages.Add(new ChatMessage { Role = m.Role, Content = m.Content, Timestamp = m.Timestamp, }); } try { _storage.Save(fork); ShowToast($"遺꾧린 ?앹꽦: {fork.Title}"); // 遺꾧린 ?€?붾줈 ?꾪솚 lock (_convLock) _currentConversation = fork; ChatTitle.Text = fork.Title; RenderMessages(); RefreshConversationList(); } catch (Exception ex) { ShowToast($"遺꾧린 ?ㅽ뙣: {ex.Message}", "\uE783"); } } // ?€?€?€ 而ㅻ㎤???붾젅???€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void OpenCommandPalette() { var palette = new CommandPaletteWindow(ExecuteCommand) { Owner = this }; palette.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": var llm = _settings.Settings.Llm; llm.DevMode = !llm.DevMode; _settings.Save(); UpdateAnalyzerButtonVisibility(); ShowToast(llm.DevMode ? "媛쒕컻??紐⑤뱶 耳쒖쭚" : "媛쒕컻??紐⑤뱶 爰쇱쭚"); break; case "open_audit_log": try { System.Diagnostics.Process.Start("explorer.exe", Services.AuditLogService.GetAuditFolder()); } catch { } break; case "paste_clipboard": try { var text = Clipboard.GetText(); if (!string.IsNullOrEmpty(text)) InputBox.Text += text; } catch { } break; case "export_conversation": ExportConversation(); break; } } private void ExportConversation() { ChatConversation? conv; lock (_convLock) conv = _currentConversation; if (conv == null || conv.Messages.Count == 0) return; var dlg = new Microsoft.Win32.SaveFileDialog { FileName = $"{conv.Title}", DefaultExt = ".md", Filter = "Markdown (*.md)|*.md|JSON (*.json)|*.json|HTML (*.html)|*.html|PDF ?몄뇙??HTML (*.pdf.html)|*.pdf.html|Text (*.txt)|*.txt" }; if (dlg.ShowDialog() != true) return; var ext = System.IO.Path.GetExtension(dlg.FileName).ToLowerInvariant(); string content; if (ext == ".json") { content = System.Text.Json.JsonSerializer.Serialize(conv, new System.Text.Json.JsonSerializerOptions { WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, }); } else if (dlg.FileName.EndsWith(".pdf.html")) { // PDF ?몄뇙??HTML ??釉뚮씪?곗??먯꽌 ?먮룞?쇰줈 ?몄뇙 ?€?붿긽???쒖떆 content = PdfExportService.BuildHtml(conv); System.IO.File.WriteAllText(dlg.FileName, content, System.Text.Encoding.UTF8); PdfExportService.OpenInBrowser(dlg.FileName); ShowToast("PDF ?몄뇙??HTML???앹꽦?섏뼱 釉뚮씪?곗??먯꽌 ?대졇?듬땲??); return; } else if (ext == ".html") { content = ExportToHtml(conv); } else { var sb = new System.Text.StringBuilder(); sb.AppendLine($"# {conv.Title}"); sb.AppendLine($"_?앹꽦: {conv.CreatedAt:yyyy-MM-dd HH:mm} 쨌 二쇱젣: {conv.Category}_"); sb.AppendLine(); foreach (var msg in conv.Messages) { if (msg.Role == "system") continue; var label = msg.Role == "user" ? "**?ъ슜??*" : "**AI**"; sb.AppendLine($"{label} ({msg.Timestamp:HH:mm})"); sb.AppendLine(); sb.AppendLine(msg.Content); if (msg.AttachedFiles is { Count: > 0 }) { sb.AppendLine(); sb.AppendLine("_泥⑤? ?뚯씪: " + string.Join(", ", msg.AttachedFiles.Select(System.IO.Path.GetFileName)) + "_"); } sb.AppendLine(); sb.AppendLine("---"); sb.AppendLine(); } content = sb.ToString(); } System.IO.File.WriteAllText(dlg.FileName, content, System.Text.Encoding.UTF8); } private static string ExportToHtml(ChatConversation conv) { var sb = new System.Text.StringBuilder(); sb.AppendLine(""); sb.AppendLine($"{System.Net.WebUtility.HtmlEncode(conv.Title)}"); sb.AppendLine(""); sb.AppendLine($"

{System.Net.WebUtility.HtmlEncode(conv.Title)}

"); sb.AppendLine($"

?앹꽦: {conv.CreatedAt:yyyy-MM-dd HH:mm} 쨌 二쇱젣: {conv.Category}

"); foreach (var msg in conv.Messages) { if (msg.Role == "system") continue; var cls = msg.Role == "user" ? "user" : "ai"; var label = msg.Role == "user" ? "?ъ슜?? : "AI"; sb.AppendLine($"
"); sb.AppendLine($"
{label} 쨌 {msg.Timestamp:HH:mm}
"); sb.AppendLine($"
{System.Net.WebUtility.HtmlEncode(msg.Content)}
"); sb.AppendLine("
"); } sb.AppendLine(""); return sb.ToString(); } // ?€?€?€ 踰꾪듉 ?대깽???€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void ChatWindow_KeyDown(object sender, KeyEventArgs e) { var mod = Keyboard.Modifiers; // Ctrl ?⑥텞?? if (mod == ModifierKeys.Control) { switch (e.Key) { case Key.N: BtnNewChat_Click(this, new RoutedEventArgs()); e.Handled = true; break; case Key.W: Close(); e.Handled = true; break; case Key.E: ExportConversation(); e.Handled = true; break; case Key.L: InputBox.Text = ""; InputBox.Focus(); e.Handled = true; break; case Key.B: BtnToggleSidebar_Click(this, new RoutedEventArgs()); e.Handled = true; break; case Key.M: BtnModelSelector_Click(this, new RoutedEventArgs()); e.Handled = true; break; case Key.OemComma: BtnSettings_Click(this, new RoutedEventArgs()); e.Handled = true; break; case Key.F: ToggleMessageSearch(); e.Handled = true; break; case Key.D1: TabChat.IsChecked = true; e.Handled = true; break; case Key.D2: TabCowork.IsChecked = true; e.Handled = true; break; case Key.D3: if (TabCode.IsEnabled) TabCode.IsChecked = true; e.Handled = true; break; } } // Ctrl+Shift ?⑥텞?? if (mod == (ModifierKeys.Control | ModifierKeys.Shift)) { switch (e.Key) { case Key.C: // 留덉?留?AI ?묐떟 蹂듭궗 ChatConversation? conv; lock (_convLock) conv = _currentConversation; if (conv != null) { var lastAi = conv.Messages.LastOrDefault(m => m.Role == "assistant"); if (lastAi != null) try { Clipboard.SetText(lastAi.Content); } catch { } } e.Handled = true; break; case Key.R: // 留덉?留??묐떟 ?ъ깮?? _ = RegenerateLastAsync(); e.Handled = true; break; case Key.D: // 紐⑤뱺 ?€????젣 BtnDeleteAll_Click(this, new RoutedEventArgs()); e.Handled = true; break; case Key.P: // 而ㅻ㎤???붾젅?? OpenCommandPalette(); e.Handled = true; break; } } // Escape: 寃€??諛??リ린 ?먮뒗 ?ㅽ듃由щ컢 以묒? if (e.Key == Key.Escape) { if (MessageSearchBar.Visibility == Visibility.Visible) { CloseMessageSearch(); e.Handled = true; } else if (_isStreaming) { StopGeneration(); e.Handled = true; } } // ?щ옒??紐낅졊 ?앹뾽 ??泥섎━ if (SlashPopup.IsOpen) { if (e.Key == Key.Escape) { SlashPopup.IsOpen = false; _slashSelectedIndex = -1; e.Handled = true; } else if (e.Key == Key.Up) { SlashPopup_ScrollByDelta(120); // ?꾨줈 1移? e.Handled = true; } else if (e.Key == Key.Down) { SlashPopup_ScrollByDelta(-120); // ?꾨옒濡?1移? e.Handled = true; } else if (e.Key == Key.Enter && _slashSelectedIndex >= 0) { e.Handled = true; ExecuteSlashSelectedItem(); } } } private void BtnStop_Click(object sender, RoutedEventArgs e) => StopGeneration(); private void BtnPause_Click(object sender, System.Windows.Input.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(); // ?€?€?€ 硫붿떆吏€ ??寃€??(Ctrl+F) ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private List _searchMatchIndices = new(); private int _searchCurrentIndex = -1; private void ToggleMessageSearch() { if (MessageSearchBar.Visibility == Visibility.Visible) CloseMessageSearch(); else { 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) { var query = SearchTextBox.Text.Trim(); if (string.IsNullOrEmpty(query)) { SearchResultCount.Text = ""; _searchMatchIndices.Clear(); _searchCurrentIndex = -1; ClearSearchHighlights(); return; } // ?꾩옱 ?€?붿쓽 硫붿떆吏€?먯꽌 寃€?? ChatConversation? conv; lock (_convLock) conv = _currentConversation; if (conv == null) return; _searchMatchIndices.Clear(); for (int i = 0; i < conv.Messages.Count; i++) { if (conv.Messages[i].Content.Contains(query, 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) return; _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) return; _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; var msgIndex = _searchMatchIndices[_searchCurrentIndex]; // MessagePanel?먯꽌 ?대떦 硫붿떆吏€ ?몃뜳?ㅼ쓽 ?먯떇 ?붿냼瑜?李얠븘 ?ㅽ겕濡? // 硫붿떆吏€ ?⑤꼸???먯떇 ?섍? ?€??硫붿떆吏€ ?섏? ?뺥솗???쇱튂?섏? ?딆쓣 ???덉쑝誘€濡? // (諛곕꼫, 怨꾪쉷移대뱶 ???욎엫) BringIntoView濡??€?듭쟻 ?꾩튂 ?대룞 if (msgIndex < MessagePanel.Children.Count) { var element = MessagePanel.Children[msgIndex] as FrameworkElement; element?.BringIntoView(); } else if (MessagePanel.Children.Count > 0) { // 踰붿쐞 諛뽰씠硫?留덉?留??먯떇?쇰줈 ?대룞 (MessagePanel.Children[^1] as FrameworkElement)?.BringIntoView(); } } private void ClearSearchHighlights() { // ?꾩옱??BringIntoView 湲곕컲?대?濡?蹂꾨룄 ?섏씠?쇱씠???쒓굅 遺덊븘?? } // ?€?€?€ ?먮윭 蹂듦뎄 ?ъ떆??踰꾪듉 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void AddRetryButton() { Dispatcher.Invoke(() => { var retryBorder = new Border { Background = new SolidColorBrush(Color.FromArgb(0x18, 0xEF, 0x44, 0x44)), CornerRadius = new CornerRadius(8), Padding = new Thickness(12, 8, 12, 8), Margin = new Thickness(40, 4, 80, 4), HorizontalAlignment = HorizontalAlignment.Left, Cursor = System.Windows.Input.Cursors.Hand, }; var retrySp = new StackPanel { Orientation = Orientation.Horizontal }; retrySp.Children.Add(new TextBlock { Text = "\uE72C", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), }); retrySp.Children.Add(new TextBlock { Text = "?ъ떆??, FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)), VerticalAlignment = VerticalAlignment.Center, }); retryBorder.Child = retrySp; retryBorder.MouseEnter += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x30, 0xEF, 0x44, 0x44)); }; retryBorder.MouseLeave += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xEF, 0x44, 0x44)); }; retryBorder.MouseLeftButtonUp += (_, _) => { lock (_convLock) { if (_currentConversation != null) { var lastIdx = _currentConversation.Messages.Count - 1; if (lastIdx >= 0 && _currentConversation.Messages[lastIdx].Role == "assistant") _currentConversation.Messages.RemoveAt(lastIdx); } } _ = RegenerateLastAsync(); }; MessagePanel.Children.Add(retryBorder); ForceScrollToEnd(); }); } // ?€?€?€ 硫붿떆吏€ ?고겢由?而⑦뀓?ㅽ듃 硫붾돱 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private void ShowMessageContextMenu(string content, string role) { var menu = CreateThemedContextMenu(); var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; void AddItem(string icon, string label, Action action) { var sp = new StackPanel { Orientation = Orientation.Horizontal }; sp.Children.Add(new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0), }); sp.Children.Add(new TextBlock { Text = label, FontSize = 12, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, }); var mi = new MenuItem { Header = sp, Padding = new Thickness(8, 6, 16, 6) }; mi.Click += (_, _) => action(); menu.Items.Add(mi); } // 蹂듭궗 AddItem("\uE8C8", "?띿뒪??蹂듭궗", () => { try { Clipboard.SetText(content); ShowToast("蹂듭궗?섏뿀?듬땲??); } catch { } }); // 留덊겕?ㅼ슫 蹂듭궗 AddItem("\uE943", "留덊겕?ㅼ슫 蹂듭궗", () => { try { Clipboard.SetText(content); ShowToast("留덊겕?ㅼ슫?쇰줈 蹂듭궗??); } catch { } }); // ?몄슜?섏뿬 ?듭옣 AddItem("\uE97A", "?몄슜?섏뿬 ?듭옣", () => { var quote = content.Length > 200 ? content[..200] + "..." : content; var lines = quote.Split('\n'); var quoted = string.Join("\n", lines.Select(l => $"> {l}")); InputBox.Text = quoted + "\n\n"; InputBox.Focus(); InputBox.CaretIndex = InputBox.Text.Length; }); menu.Items.Add(new Separator()); // ?ъ깮??(AI ?묐떟留? if (role == "assistant") { AddItem("\uE72C", "?묐떟 ?ъ깮??, () => _ = RegenerateLastAsync()); } // ?€??遺꾧린 (Fork) AddItem("\uE8A5", "?ш린??遺꾧린", () => { ChatConversation? conv; lock (_convLock) conv = _currentConversation; if (conv == null) return; var idx = conv.Messages.FindLastIndex(m => m.Role == role && m.Content == content); if (idx < 0) return; ForkConversation(conv, idx); }); menu.Items.Add(new Separator()); // ?댄썑 硫붿떆吏€ 紐⑤몢 ??젣 var msgContent = content; var msgRole = role; AddItem("\uE74D", "?댄썑 硫붿떆吏€ 紐⑤몢 ??젣", () => { ChatConversation? conv; lock (_convLock) conv = _currentConversation; if (conv == null) return; var idx = conv.Messages.FindLastIndex(m => m.Role == msgRole && m.Content == msgContent); if (idx < 0) return; var removeCount = conv.Messages.Count - idx; if (MessageBox.Show($"??硫붿떆吏€ ?ы븿 {removeCount}媛?硫붿떆吏€瑜???젣?섏떆寃좎뒿?덇퉴?", "硫붿떆吏€ ??젣", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes) return; conv.Messages.RemoveRange(idx, removeCount); try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"?€???€???ㅽ뙣: {ex.Message}"); } RenderMessages(); ShowToast($"{removeCount}媛?硫붿떆吏€ ??젣??); }); menu.IsOpen = true; } // ?€?€?€ ???뚮┝ ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private static readonly string[] Tips = [ "?뮕 ?묒뾽 ?대뜑??AX.md ?뚯씪??留뚮뱾硫?留ㅻ쾲 ?쒖뒪???꾨\?꾪듃???먮룞 二쇱엯?⑸땲?? ?꾨줈?앺듃 ?ㅺ퀎 ?먯튃?대굹 肄붾뵫 洹쒖튃??湲곕줉?섏꽭??", "?뮕 Ctrl+1/2/3?쇰줈 Chat/Cowork/Code ??쓣 鍮좊Ⅴ寃??꾪솚?????덉뒿?덈떎.", "?뮕 Ctrl+F濡??꾩옱 ?€????硫붿떆吏€瑜?寃€?됲븷 ???덉뒿?덈떎.", "?뮕 硫붿떆吏€瑜??고겢由?븯硫?蹂듭궗, ?몄슜 ?듭옣, ?ъ깮?? ??젣瑜??????덉뒿?덈떎.", "?뮕 肄붾뱶 釉붾줉???붾툝?대┃?섎㈃ ?꾩껜?붾㈃?쇰줈 蹂????덇퀬, ?뮶 踰꾪듉?쇰줈 ?뚯씪 ?€?μ씠 媛€?ν빀?덈떎.", "?뮕 Cowork ?먯씠?꾪듃媛€ 留뚮뱺 ?뚯씪?€ ?먮룞?쇰줈 ?좎쭨_?쒓컙 ?묐??ш? 遺숈뼱 ??뼱?곌린瑜?諛⑹??⑸땲??", "?뮕 Code ??뿉??媛쒕컻 ?몄뼱瑜??좏깮?섎㈃ ?대떦 ?몄뼱 ?곗꽑?쇰줈 肄붾뱶瑜??앹꽦?⑸땲??", "?뮕 ?뚯씪 ?먯깋湲??섎떒 諛?'?뚯씪' 踰꾪듉)?먯꽌 ?붾툝?대┃?쇰줈 ?꾨━酉? ?고겢由?쑝濡?愿€由ы븷 ???덉뒿?덈떎.", "?뮕 ?먯씠?꾪듃媛€ 怨꾪쉷???쒖떆?섎㈃ '?섏젙 ?붿껌'?쇰줈 諛⑺뼢??諛붽씀嫄곕굹 '痍⑥냼'濡?以묐떒?????덉뒿?덈떎.", "?뮕 Code ??? 鍮뚮뱶/?뚯뒪?몃? ?먮룞?쇰줈 ?ㅽ뻾?⑸땲?? ?꾨줈?앺듃 ?대뜑瑜?癒쇱? ?좏깮?섏꽭??", "?뮕 臾대뱶 媛ㅻ윭由ъ뿉??10媛€吏€ ?붿옄???쒗뵆由?以??먰븯???ㅽ??쇱쓣 誘몃━蹂닿린濡??좏깮?????덉뒿?덈떎.", "?뮕 Git ?곕룞: Code ??뿉???먯씠?꾪듃媛€ git status, diff, commit???섑뻾?⑸땲?? (push??吏곸젒)", "?뮕 ?ㅼ젙 ??AX Agent ??怨듯넻?먯꽌 媛쒕컻??紐⑤뱶瑜?耳쒕㈃ ?먯씠?꾪듃 ?숈옉???ㅽ뀦蹂꾨줈 寃€利앺븷 ???덉뒿?덈떎.", "?뮕 ?몃젅???꾩씠肄??고겢由???'?ъ슜 ?듦퀎'?먯꽌 ?€??鍮덈룄?€ ?좏겙 ?ъ슜?됱쓣 ?뺤씤?????덉뒿?덈떎.", "?뮕 ?€???쒕ぉ???대┃?섎㈃ ?대쫫??蹂€寃쏀븷 ???덉뒿?덈떎.", "?뮕 LLM ?ㅻ쪟 諛쒖깮 ??'?ъ떆?? 踰꾪듉???먮룞?쇰줈 ?섑??⑸땲??", "?뮕 寃€?됰??먯꽌 ?€???쒕ぉ肉??꾨땲??泥?硫붿떆吏€ ?댁슜源뚯? 寃€?됰맗?덈떎.", "?뮕 ?꾨━???좏깮 ?꾩뿉???€?붽? 由ъ뀑?섏? ?딆뒿?덈떎. 吏꾪뻾 以묒씤 ?€?붿뿉???꾨━?뗭쓣 蹂€寃쏀븷 ???덉뒿?덈떎.", "?뮕 Shift+Enter濡??쇱? 寃€??寃곌낵???뚯씪???덈뒗 ?대뜑瑜??????덉뒿?덈떎.", "?뮕 理쒓렐 ?대뜑瑜??고겢由?븯硫?'?대뜑 ?닿린', '寃쎈줈 蹂듭궗', '紐⑸줉?먯꽌 ??젣'媛€ 媛€?ν빀?덈떎.", "?뮕 Cowork/Code ?먯씠?꾪듃 ?묒뾽 ?꾨즺 ???쒖뒪???몃젅?댁뿉 ?뚮┝???쒖떆?⑸땲??", "?뮕 留덊겕?ㅼ슫 ?뚯씠釉? ?몄슜(>), 痍⑥냼??~~), 留곹겕([text](url))媛€ 紐⑤몢 ?뚮뜑留곷맗?덈떎.", "?뮕 ???곗씠???대뜑瑜??뚰겕?ㅽ럹?댁뒪濡?吏€?뺥븷 ?뚮뒗 諛섎뱶??諛깆뾽??癒쇱? 留뚮뱶?몄슂!", "?뮕 ?쒕씪?대툕 猷⑦듃(C:\\, D:\\)???묒뾽怨듦컙?쇰줈 ?ㅼ젙?????놁뒿?덈떎. ?섏쐞 ?대뜑瑜??좏깮?섏꽭??", ]; private int _tipIndex; private DispatcherTimer? _tipDismissTimer; private void ShowRandomTip() { if (!_settings.Settings.Llm.ShowTips) return; if (_activeTab != "Cowork" && _activeTab != "Code") return; var tip = Tips[_tipIndex % Tips.Length]; _tipIndex++; // ?좎뒪???ㅽ??쇰줈 ?쒖떆 (湲곗〈 ?좎뒪?몄? ?ㅻⅨ ?꾩튂/?됱긽) ShowTip(tip); } private void ShowTip(string message) { _tipDismissTimer?.Stop(); ToastText.Text = message; ToastIcon.Text = "\uE82F"; // ?꾧뎄 ?꾩씠肄? ToastBorder.Visibility = Visibility.Visible; ToastBorder.BeginAnimation(UIElement.OpacityProperty, new System.Windows.Media.Animation.DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(300))); var duration = _settings.Settings.Llm.TipDurationSeconds; if (duration <= 0) return; // 0?대㈃ ?섎룞 ?リ린 (?먮룞 ?щ씪吏??놁쓬) _tipDismissTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(duration) }; _tipDismissTimer.Tick += (_, _) => { _tipDismissTimer.Stop(); var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(300)); fadeOut.Completed += (_, _) => ToastBorder.Visibility = Visibility.Collapsed; ToastBorder.BeginAnimation(UIElement.OpacityProperty, fadeOut); }; _tipDismissTimer.Start(); } // ?€?€?€ ?꾨줈?앺듃 臾몃㎘ ?뚯씪 (AX.md) ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ /// /// ?묒뾽 ?대뜑??AX.md媛€ ?덉쑝硫??댁슜???쎌뼱 ?쒖뒪???꾨\?꾪듃??二쇱엯?⑸땲?? /// Claude Code?€ ?숈씪???뚯씪紐??뺤떇???ъ슜?⑸땲?? /// private static string LoadProjectContext(string workFolder) { if (string.IsNullOrEmpty(workFolder)) return ""; // AX.md ?먯깋 (?묒뾽 ?대뜑 ???곸쐞 ?대뜑 ?? var searchDir = workFolder; for (int i = 0; i < 3; i++) // 理쒕? 3?④퀎 ?곸쐞源뚯? { if (string.IsNullOrEmpty(searchDir)) break; var filePath = System.IO.Path.Combine(searchDir, "AX.md"); if (System.IO.File.Exists(filePath)) { try { var content = System.IO.File.ReadAllText(filePath); if (content.Length > 8000) content = content[..8000] + "\n... (8000??珥덇낵 ?앸왂)"; return $"\n## Project Context (from AX.md)\n{content}\n"; } catch { } } searchDir = System.IO.Directory.GetParent(searchDir)?.FullName; } return ""; } // ?€?€?€ 臾댁?媛?湲€濡쒖슦 ?좊땲硫붿씠???€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private DispatcherTimer? _rainbowTimer; private DateTime _rainbowStartTime; /// ?낅젰李??뚮몢由ъ뿉 臾댁?媛?洹몃씪?곗씠???뚯쟾 ?좊땲硫붿씠?섏쓣 ?ъ깮?⑸땲??(3珥?. private void PlayRainbowGlow() { if (!_settings.Settings.Llm.EnableChatRainbowGlow) return; _rainbowTimer?.Stop(); _rainbowStartTime = DateTime.UtcNow; // ?섏씠?쒖씤 (鍮좊Ⅴ寃? InputGlowBorder.BeginAnimation(UIElement.OpacityProperty, new System.Windows.Media.Animation.DoubleAnimation(0, 0.9, TimeSpan.FromMilliseconds(150))); // 洹몃씪?곗씠???뚯쟾 ?€?대㉧ (~60fps) ???ㅽ듃由щ컢 醫낅즺源뚯? 吏€?? _rainbowTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(16) }; _rainbowTimer.Tick += (_, _) => { var elapsed = (DateTime.UtcNow - _rainbowStartTime).TotalMilliseconds; // 洹몃씪?곗씠???ㅽ봽???뚯쟾 var shift = (elapsed / 1500.0) % 1.0; // 1.5珥덉뿉 1諛뷀€?(?먮━寃? var brush = InputGlowBorder.BorderBrush as LinearGradientBrush; if (brush == null) return; // ?쒖옉/?앹젏 ?뚯쟾 (?먰삎 ?대룞) var angle = shift * Math.PI * 2; brush.StartPoint = new Point(0.5 + 0.5 * Math.Cos(angle), 0.5 + 0.5 * Math.Sin(angle)); brush.EndPoint = new Point(0.5 - 0.5 * Math.Cos(angle), 0.5 - 0.5 * Math.Sin(angle)); }; _rainbowTimer.Start(); } /// ?덉씤蹂댁슦 湲€濡쒖슦 ?④낵瑜??섏씠?쒖븘?껎븯硫?以묒??⑸땲?? private void StopRainbowGlow() { _rainbowTimer?.Stop(); _rainbowTimer = null; if (InputGlowBorder.Opacity > 0) { var fadeOut = new System.Windows.Media.Animation.DoubleAnimation( InputGlowBorder.Opacity, 0, TimeSpan.FromMilliseconds(600)); fadeOut.Completed += (_, _) => InputGlowBorder.Opacity = 0; InputGlowBorder.BeginAnimation(UIElement.OpacityProperty, fadeOut); } } // ?€?€?€ ?좎뒪???뚮┝ ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private DispatcherTimer? _toastHideTimer; private void ShowToast(string message, string icon = "\uE73E", int durationMs = 2000) { _toastHideTimer?.Stop(); ToastText.Text = message; ToastIcon.Text = icon; ToastBorder.Visibility = Visibility.Visible; // ?섏씠?쒖씤 ToastBorder.BeginAnimation(UIElement.OpacityProperty, new System.Windows.Media.Animation.DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200))); // ?먮룞 ?④린湲? _toastHideTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(durationMs) }; _toastHideTimer.Tick += (_, _) => { _toastHideTimer.Stop(); var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(300)); fadeOut.Completed += (_, _) => ToastBorder.Visibility = Visibility.Collapsed; ToastBorder.BeginAnimation(UIElement.OpacityProperty, fadeOut); }; _toastHideTimer.Start(); } // ?€?€?€ ?€??二쇱젣 踰꾪듉 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ /// ?꾨━?뗭뿉???€??二쇱젣 踰꾪듉???숈쟻?쇰줈 ?앹꽦?⑸땲?? private void BuildTopicButtons() { TopicButtonPanel.Children.Clear(); TopicButtonPanel.Visibility = Visibility.Visible; // ??퀎 EmptyState ?띿뒪?? 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 = "二쇱젣??留욌뒗 ?꾨Ц ?꾨━?뗭씠 ?먮룞 ?곸슜?⑸땲??; } var presets = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets); foreach (var preset in presets) { var capturedPreset = preset; var btnColor = BrushFromHex(preset.Color); var border = new Border { Background = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent, CornerRadius = new CornerRadius(14), Padding = new Thickness(14, 12, 14, 12), Margin = new Thickness(4, 4, 4, 8), Cursor = Cursors.Hand, Width = 120, Height = 105, ClipToBounds = true, RenderTransformOrigin = new Point(0.5, 0.5), RenderTransform = new ScaleTransform(1, 1), }; var stack = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center }; // ?꾩씠肄?而⑦뀒?대꼫 (?먰삎 諛곌꼍 + ?꾩뒪 ?좊땲硫붿씠?? var iconCircle = new Border { Width = 40, Height = 40, CornerRadius = new CornerRadius(20), Background = new SolidColorBrush(((SolidColorBrush)btnColor).Color) { Opacity = 0.15 }, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 0, 0, 10), }; var iconTb = new TextBlock { Text = preset.Symbol, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 18, Foreground = btnColor, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, }; iconCircle.Child = iconTb; stack.Children.Add(iconCircle); // ?쒕ぉ stack.Children.Add(new TextBlock { Text = preset.Label, FontSize = 13, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White, HorizontalAlignment = HorizontalAlignment.Center, }); // ?ㅻ챸 stack.Children.Add(new TextBlock { Text = preset.Description, FontSize = 9, TextWrapping = TextWrapping.Wrap, TextTrimming = TextTrimming.CharacterEllipsis, MaxHeight = 28, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 2, 0, 0), TextAlignment = TextAlignment.Center, }); // 而ㅼ뒪?€ ?꾨━?? 醫뚯륫 ?곷떒 諭껋? if (capturedPreset.IsCustom) { var grid = new Grid(); grid.Children.Add(stack); var badge = new Border { Width = 16, Height = 16, CornerRadius = new CornerRadius(4), Background = new SolidColorBrush(Color.FromArgb(0x60, 0xFF, 0xFF, 0xFF)), HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(2, 2, 0, 0), ToolTip = "而ㅼ뒪?€ ?꾨━??, }; badge.Child = new TextBlock { Text = "\uE710", // + ?꾩씠肄? FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 8, Foreground = btnColor, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, }; grid.Children.Add(badge); border.Child = grid; } else { border.Child = stack; } // ?몃쾭 ?좊땲硫붿씠?????ㅼ???1.05x + 諛앷린 蹂€寃? var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent; var normalBg = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent; border.MouseEnter += (s, _) => { if (s is Border b && b.RenderTransform is ScaleTransform st) { st.ScaleX = 1.03; st.ScaleY = 1.03; b.Background = hoverBg; } }; border.MouseLeave += (s, _) => { if (s is Border b && b.RenderTransform is ScaleTransform st) { st.ScaleX = 1.0; st.ScaleY = 1.0; b.Background = normalBg; } }; // ?대┃ ???대떦 二쇱젣濡????€???쒖옉 border.MouseLeftButtonDown += (_, _) => SelectTopic(capturedPreset); // 而ㅼ뒪?€ ?꾨━?? ?고겢由?硫붾돱 (?몄쭛/??젣) if (capturedPreset.IsCustom) { border.MouseRightButtonUp += (s, e) => { e.Handled = true; ShowCustomPresetContextMenu(s as Border, capturedPreset); }; } TopicButtonPanel.Children.Add(border); } // "湲고?" ?먯쑀 ?낅젰 踰꾪듉 異붽? { var etcColor = BrushFromHex("#6B7280"); // ?뚯깋 var etcBorder = new Border { Background = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent, CornerRadius = new CornerRadius(14), Padding = new Thickness(14, 12, 14, 12), Margin = new Thickness(4, 4, 4, 8), Cursor = Cursors.Hand, Width = 120, Height = 105, ClipToBounds = true, RenderTransformOrigin = new Point(0.5, 0.5), RenderTransform = new ScaleTransform(1, 1), }; var etcStack = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center }; var etcIconCircle = new Border { Width = 40, Height = 40, CornerRadius = new CornerRadius(20), Background = new SolidColorBrush(((SolidColorBrush)etcColor).Color) { Opacity = 0.15 }, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 0, 0, 10), }; etcIconCircle.Child = new TextBlock { Text = "\uE70F", // Edit ?꾩씠肄? FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 18, Foreground = etcColor, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, }; etcStack.Children.Add(etcIconCircle); etcStack.Children.Add(new TextBlock { Text = "湲고?", FontSize = 13, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White, HorizontalAlignment = HorizontalAlignment.Center, }); etcStack.Children.Add(new TextBlock { Text = "?꾨━???놁씠 ?먯쑀濡?쾶 ?€?뷀빀?덈떎", FontSize = 9, TextWrapping = TextWrapping.Wrap, TextTrimming = TextTrimming.CharacterEllipsis, MaxHeight = 28, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 2, 0, 0), TextAlignment = TextAlignment.Center, }); etcBorder.Child = etcStack; var hoverBg2 = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent; var normalBg2 = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent; etcBorder.MouseEnter += (s, _) => { if (s is Border b && b.RenderTransform is ScaleTransform st) { st.ScaleX = 1.03; st.ScaleY = 1.03; b.Background = hoverBg2; } }; etcBorder.MouseLeave += (s, _) => { if (s is Border b && b.RenderTransform is ScaleTransform st) { st.ScaleX = 1.0; st.ScaleY = 1.0; b.Background = normalBg2; } }; etcBorder.MouseLeftButtonDown += (_, _) => { EmptyState.Visibility = Visibility.Collapsed; InputBox.Focus(); }; TopicButtonPanel.Children.Add(etcBorder); } // ?€?€ "+" 而ㅼ뒪?€ ?꾨━??異붽? 踰꾪듉 ?€?€ { var addColor = BrushFromHex("#6366F1"); var addBorder = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(14), Padding = new Thickness(14, 12, 14, 12), Margin = new Thickness(4, 4, 4, 8), Cursor = Cursors.Hand, Width = 120, Height = 105, ClipToBounds = true, BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray, BorderThickness = new Thickness(1.5), RenderTransformOrigin = new Point(0.5, 0.5), RenderTransform = new ScaleTransform(1, 1), }; // ?먯꽑 ?④낵瑜??꾪븳 Dashes if (addBorder.BorderBrush is SolidColorBrush scb) { var dashPen = new Pen(scb, 1.5) { DashStyle = DashStyles.Dash }; } var addStack = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center }; // + ?꾩씠肄? var plusIcon = new TextBlock { Text = "\uE710", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 24, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 8, 0, 8), }; addStack.Children.Add(plusIcon); addStack.Children.Add(new TextBlock { Text = "?꾨━??異붽?", FontSize = 12, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, HorizontalAlignment = HorizontalAlignment.Center, }); addBorder.Child = addStack; var hoverBg3 = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent; addBorder.MouseEnter += (s, _) => { if (s is Border b && b.RenderTransform is ScaleTransform st) { st.ScaleX = 1.03; st.ScaleY = 1.03; b.Background = hoverBg3; } }; addBorder.MouseLeave += (s, _) => { if (s is Border b && b.RenderTransform is ScaleTransform st) { st.ScaleX = 1.0; st.ScaleY = 1.0; b.Background = Brushes.Transparent; } }; addBorder.MouseLeftButtonDown += (_, _) => ShowCustomPresetDialog(); TopicButtonPanel.Children.Add(addBorder); } } // ?€?€?€ 而ㅼ뒪?€ ?꾨━??愿€由??€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ /// 而ㅼ뒪?€ ?꾨━??異붽? ?ㅼ씠?쇰줈洹몃? ?쒖떆?⑸땲?? private void ShowCustomPresetDialog(Models.CustomPresetEntry? existing = null) { bool isEdit = existing != null; var dlg = new CustomPresetDialog( existingName: existing?.Label ?? "", existingDesc: existing?.Description ?? "", existingPrompt: existing?.SystemPrompt ?? "", existingColor: existing?.Color ?? "#6366F1", existingSymbol: existing?.Symbol ?? "\uE713", existingTab: existing?.Tab ?? _activeTab) { Owner = this, }; if (dlg.ShowDialog() == true) { if (isEdit) { existing!.Label = dlg.PresetName; existing.Description = dlg.PresetDescription; existing.SystemPrompt = dlg.PresetSystemPrompt; existing.Color = dlg.PresetColor; existing.Symbol = dlg.PresetSymbol; existing.Tab = dlg.PresetTab; } else { _settings.Settings.Llm.CustomPresets.Add(new Models.CustomPresetEntry { Label = dlg.PresetName, Description = dlg.PresetDescription, SystemPrompt = dlg.PresetSystemPrompt, Color = dlg.PresetColor, Symbol = dlg.PresetSymbol, Tab = dlg.PresetTab, }); } _settings.Save(); BuildTopicButtons(); } } /// 而ㅼ뒪?€ ?꾨━???고겢由?而⑦뀓?ㅽ듃 硫붾돱瑜??쒖떆?⑸땲?? private void ShowCustomPresetContextMenu(Border? anchor, Services.TopicPreset preset) { if (anchor == null || preset.CustomId == null) return; var popup = new System.Windows.Controls.Primitives.Popup { PlacementTarget = anchor, Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom, StaysOpen = false, AllowsTransparency = true, }; var menuBg = TryFindResource("LauncherBackground") as Brush ?? Brushes.Black; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var menuBorder = new Border { Background = menuBg, CornerRadius = new CornerRadius(10), BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(4), MinWidth = 120, Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 12, ShadowDepth = 2, Opacity = 0.3, Color = Colors.Black, }, }; var stack = new StackPanel(); // ?몄쭛 踰꾪듉 var editItem = CreateContextMenuItem("\uE70F", "?몄쭛", primaryText, secondaryText); editItem.MouseLeftButtonDown += (_, _) => { popup.IsOpen = false; var entry = _settings.Settings.Llm.CustomPresets.FirstOrDefault(c => c.Id == preset.CustomId); if (entry != null) ShowCustomPresetDialog(entry); }; stack.Children.Add(editItem); // ??젣 踰꾪듉 var deleteItem = CreateContextMenuItem("\uE74D", "??젣", new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)), secondaryText); deleteItem.MouseLeftButtonDown += (_, _) => { popup.IsOpen = false; var result = CustomMessageBox.Show( $"'{preset.Label}' ?꾨━?뗭쓣 ??젣?섏떆寃좎뒿?덇퉴?", "?꾨━????젣", MessageBoxButton.YesNo, MessageBoxImage.Question); if (result == MessageBoxResult.Yes) { _settings.Settings.Llm.CustomPresets.RemoveAll(c => c.Id == preset.CustomId); _settings.Save(); BuildTopicButtons(); } }; stack.Children.Add(deleteItem); menuBorder.Child = stack; popup.Child = menuBorder; popup.IsOpen = true; } /// 而⑦뀓?ㅽ듃 硫붾돱 ??ぉ???앹꽦?⑸땲?? private Border CreateContextMenuItem(string icon, string label, Brush fg, Brush secondaryFg) { var item = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(6), Padding = new Thickness(10, 6, 14, 6), Cursor = Cursors.Hand, }; var sp = new StackPanel { Orientation = Orientation.Horizontal }; sp.Children.Add(new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 13, Foreground = fg, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0), }); sp.Children.Add(new TextBlock { Text = label, FontSize = 13, Foreground = fg, VerticalAlignment = VerticalAlignment.Center, }); item.Child = sp; var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent; item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; }; item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; return item; } /// ?€??二쇱젣 ?좏깮 ???꾨━???쒖뒪???꾨\?꾪듃 + 移댄뀒怨좊━ ?곸슜. private void SelectTopic(Services.TopicPreset preset) { bool hasMessages; lock (_convLock) hasMessages = _currentConversation?.Messages.Count > 0; // ?낅젰?€???띿뒪?멸? ?덉쑝硫?湲곗〈 ?€?붾? ?좎? (?낅젰 ?댁슜 蹂댁〈) bool hasInput = !string.IsNullOrEmpty(InputBox.Text); bool keepConversation = hasMessages || hasInput; if (!keepConversation) { // 硫붿떆吏€???낅젰 ?띿뒪?몃룄 ?놁쑝硫????€???쒖옉 StartNewConversation(); } // ?꾨━???곸슜 (湲곗〈 ?€?붿뿉???꾨━??蹂€寃?媛€?? lock (_convLock) { if (_currentConversation != null) { _currentConversation.SystemCommand = preset.SystemPrompt; _currentConversation.Category = preset.Category; } } if (!keepConversation) EmptyState.Visibility = Visibility.Collapsed; InputBox.Focus(); if (!string.IsNullOrEmpty(preset.Placeholder)) { _promptCardPlaceholder = preset.Placeholder; if (!keepConversation) ShowPlaceholder(); } if (keepConversation) ShowToast($"?꾨━??蹂€寃? {preset.Label}"); // Cowork ?? ?섎떒 諛?媛깆떊 if (_activeTab == "Cowork") BuildBottomBar(); } /// ?좏깮???붿옄??臾대뱶 ??(HtmlSkill?먯꽌 ?ъ슜). private string _selectedMood = null!; // Loaded ?대깽?몄뿉??珥덇린?? private string _selectedLanguage = "auto"; // Code ??媛쒕컻 ?몄뼱 private string _folderDataUsage = null!; // Loaded ?대깽?몄뿉??珥덇린?? /// ?섎떒 諛붾? 援ъ꽦?⑸땲??(?щ㎎ + ?붿옄???쒕∼?ㅼ슫 踰꾪듉). private void BuildBottomBar() { MoodIconPanel.Children.Clear(); var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; // ?€?€ ?щ㎎ 踰꾪듉 ?€?€ var currentFormat = _settings.Settings.Llm.DefaultOutputFormat ?? "auto"; var formatLabel = GetFormatLabel(currentFormat); var formatBtn = CreateFolderBarButton("\uE9F9", formatLabel, "蹂닿퀬???뺥깭 ?좏깮", "#8B5CF6"); formatBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ShowFormatMenu(); }; // Name ?깅줉 (Popup PlacementTarget?? try { RegisterName("BtnFormatMenu", formatBtn); } catch { try { UnregisterName("BtnFormatMenu"); RegisterName("BtnFormatMenu", formatBtn); } catch { } } MoodIconPanel.Children.Add(formatBtn); // 援щ텇?? MoodIconPanel.Children.Add(new Border { Width = 1, Height = 18, Background = TryFindResource("SeparatorColor") as Brush ?? Brushes.Gray, Margin = new Thickness(4, 0, 4, 0), VerticalAlignment = VerticalAlignment.Center, }); // ?€?€ ?붿옄??踰꾪듉 (?뚭레 ?ㅽ??? ?€?€ var currentMood = TemplateService.AllMoods.FirstOrDefault(m => m.Key == _selectedMood); var moodLabel = currentMood?.Label ?? "紐⑤뜕"; var moodIcon = currentMood?.Icon ?? "?뵹"; var moodBtn = CreateFolderBarButton(null, $"{moodIcon} {moodLabel}", "?붿옄??臾대뱶 ?좏깮"); moodBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ShowMoodMenu(); }; try { RegisterName("BtnMoodMenu", moodBtn); } catch { try { UnregisterName("BtnMoodMenu"); RegisterName("BtnMoodMenu", moodBtn); } catch { } } MoodIconPanel.Children.Add(moodBtn); // 援щ텇?? MoodIconPanel.Children.Add(new Border { Width = 1, Height = 18, Background = TryFindResource("SeparatorColor") as Brush ?? Brushes.Gray, Margin = new Thickness(4, 0, 4, 0), VerticalAlignment = VerticalAlignment.Center, }); // ?€?€ ?뚯씪 ?먯깋湲??좉? 踰꾪듉 ?€?€ var fileBrowserBtn = CreateFolderBarButton("\uED25", "?뚯씪", "?뚯씪 ?먯깋湲??닿린/?リ린", "#D97706"); fileBrowserBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ToggleFileBrowser(); }; MoodIconPanel.Children.Add(fileBrowserBtn); // ?€?€ ?ㅽ뻾 ?대젰 ?곸꽭??踰꾪듉 ?€?€ AppendLogLevelButton(); // 援щ텇???쒖떆 if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Visible; } /// Code ???섎떒 諛? 媛쒕컻 ?몄뼱 ?좏깮 + ?뚯씪 ?먯깋湲??좉?. private void BuildCodeBottomBar() { MoodIconPanel.Children.Clear(); var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; // 媛쒕컻 ?몄뼱 ?좏깮 踰꾪듉 var langLabel = _selectedLanguage switch { "python" => "?릫 Python", "java" => "??Java", "csharp" => "?뵹 C#", "cpp" => "??C++", "javascript" => "?뙋 JavaScript", _ => "?뵩 ?먮룞 媛먯?", }; var langBtn = CreateFolderBarButton(null, langLabel, "媛쒕컻 ?몄뼱 ?좏깮"); langBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ShowLanguageMenu(); }; try { RegisterName("BtnLangMenu", langBtn); } catch { try { UnregisterName("BtnLangMenu"); RegisterName("BtnLangMenu", langBtn); } catch { } } MoodIconPanel.Children.Add(langBtn); // 援щ텇?? MoodIconPanel.Children.Add(new Border { Width = 1, Height = 18, Background = TryFindResource("SeparatorColor") as Brush ?? Brushes.Gray, Margin = new Thickness(4, 0, 4, 0), VerticalAlignment = VerticalAlignment.Center, }); // ?뚯씪 ?먯깋湲??좉? var fileBrowserBtn = CreateFolderBarButton("\uED25", "?뚯씪", "?뚯씪 ?먯깋湲??닿린/?リ린", "#D97706"); fileBrowserBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ToggleFileBrowser(); }; MoodIconPanel.Children.Add(fileBrowserBtn); // ?€?€ ?ㅽ뻾 ?대젰 ?곸꽭??踰꾪듉 ?€?€ AppendLogLevelButton(); if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Visible; } /// ?섎떒 諛붿뿉 ?ㅽ뻾 ?대젰 ?곸꽭???좏깮 踰꾪듉??異붽??⑸땲?? private void AppendLogLevelButton() { // 援щ텇?? MoodIconPanel.Children.Add(new Border { Width = 1, Height = 18, Background = TryFindResource("SeparatorColor") as Brush ?? Brushes.Gray, Margin = new Thickness(4, 0, 4, 0), VerticalAlignment = VerticalAlignment.Center, }); var currentLevel = _settings.Settings.Llm.AgentLogLevel ?? "simple"; var levelLabel = currentLevel switch { "debug" => "?붾쾭洹?, "detailed" => "?곸꽭", _ => "媛꾨왂", }; var logBtn = CreateFolderBarButton("\uE946", levelLabel, "?ㅽ뻾 ?대젰 ?곸꽭??, "#059669"); logBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ShowLogLevelMenu(); }; try { RegisterName("BtnLogLevelMenu", logBtn); } catch { try { UnregisterName("BtnLogLevelMenu"); RegisterName("BtnLogLevelMenu", logBtn); } catch { } } MoodIconPanel.Children.Add(logBtn); } /// ?ㅽ뻾 ?대젰 ?곸꽭???앹뾽 硫붾돱瑜??쒖떆?⑸땲?? private void ShowLogLevelMenu() { FormatMenuItems.Children.Clear(); var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var levels = new (string Key, string Label, string Desc)[] { ("simple", "Simple (媛꾨왂)", "?꾧뎄 寃곌낵留???以꾨줈 ?쒖떆"), ("detailed", "Detailed (?곸꽭)", "?꾧뎄 ?몄텧/寃곌낵 + ?묒씠???곸꽭"), ("debug", "Debug (?붾쾭洹?", "紐⑤뱺 ?뺣낫 + ?뚮씪誘명꽣 ?쒖떆"), }; var current = _settings.Settings.Llm.AgentLogLevel ?? "simple"; foreach (var (key, label, desc) in levels) { var isActive = current == key; var sp = new StackPanel { Orientation = Orientation.Horizontal }; sp.Children.Add(CreateCheckIcon(isActive, accentBrush)); sp.Children.Add(new TextBlock { Text = label, FontSize = 13, Foreground = isActive ? accentBrush : primaryText, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0), }); sp.Children.Add(new TextBlock { Text = desc, FontSize = 10, Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, }); var item = new Border { Child = sp, Padding = new Thickness(12, 8, 12, 8), CornerRadius = new CornerRadius(6), Background = Brushes.Transparent, Cursor = Cursors.Hand, }; var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); item.MouseEnter += (s, _) => ((Border)s!).Background = hoverBg; item.MouseLeave += (s, _) => ((Border)s!).Background = Brushes.Transparent; item.MouseLeftButtonUp += (_, _) => { _settings.Settings.Llm.AgentLogLevel = key; _settings.Save(); FormatMenuPopup.IsOpen = false; if (_activeTab == "Cowork") BuildBottomBar(); else if (_activeTab == "Code") BuildCodeBottomBar(); }; FormatMenuItems.Children.Add(item); } try { var target = FindName("BtnLogLevelMenu") as UIElement; if (target != null) FormatMenuPopup.PlacementTarget = target; } catch { } FormatMenuPopup.IsOpen = true; } private void ShowLanguageMenu() { FormatMenuItems.Children.Clear(); var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var languages = new (string Key, string Label, string Icon)[] { ("auto", "?먮룞 媛먯?", "?뵩"), ("python", "Python", "?릫"), ("java", "Java", "??), ("csharp", "C# (.NET)", "?뵹"), ("cpp", "C/C++", "??), ("javascript", "JavaScript / Vue", "?뙋"), }; foreach (var (key, label, icon) in languages) { var isActive = _selectedLanguage == key; var sp = new StackPanel { Orientation = Orientation.Horizontal }; sp.Children.Add(CreateCheckIcon(isActive, accentBrush)); sp.Children.Add(new TextBlock { Text = icon, FontSize = 13, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0) }); sp.Children.Add(new TextBlock { Text = label, FontSize = 13, Foreground = isActive ? accentBrush : primaryText, FontWeight = isActive ? FontWeights.SemiBold : FontWeights.Normal }); var itemBorder = new Border { Child = sp, Background = Brushes.Transparent, CornerRadius = new CornerRadius(8), Cursor = Cursors.Hand, Padding = new Thickness(8, 7, 12, 7), }; ApplyMenuItemHover(itemBorder); var capturedKey = key; itemBorder.MouseLeftButtonUp += (_, _) => { FormatMenuPopup.IsOpen = false; _selectedLanguage = capturedKey; BuildCodeBottomBar(); }; FormatMenuItems.Children.Add(itemBorder); } if (FindName("BtnLangMenu") is UIElement langTarget) FormatMenuPopup.PlacementTarget = langTarget; FormatMenuPopup.IsOpen = true; } /// ?대뜑諛????쒕∼?ㅼ슫 踰꾪듉 (?뚭레/?곴레 ?ㅽ??쇨낵 ?숈씪) private Border CreateFolderBarButton(string? mdlIcon, string label, string tooltip, string? iconColorHex = null) { var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var iconColor = iconColorHex != null ? BrushFromHex(iconColorHex) : secondaryText; var sp = new StackPanel { Orientation = Orientation.Horizontal }; if (mdlIcon != null) { sp.Children.Add(new TextBlock { Text = mdlIcon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = iconColor, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0), }); } sp.Children.Add(new TextBlock { Text = label, FontSize = 12, Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, }); return new Border { Child = sp, Background = Brushes.Transparent, Padding = new Thickness(6, 4, 6, 4), Cursor = Cursors.Hand, ToolTip = tooltip, }; } private static string GetFormatLabel(string key) => key switch { "xlsx" => "Excel", "html" => "HTML 蹂닿퀬??, "docx" => "Word", "md" => "Markdown", "csv" => "CSV", _ => "AI ?먮룞", }; /// ?꾩옱 ?꾨━??移댄뀒怨좊━??留욌뒗 ?먯씠?꾪듃 ?대쫫, ?щ낵, ?됱긽??諛섑솚?⑸땲?? private (string Name, string Symbol, string Color) GetAgentIdentity() { string? category = null; lock (_convLock) { category = _currentConversation?.Category; } return category switch { // Cowork ?꾨━??移댄뀒怨좊━ "蹂닿퀬?? => ("蹂닿퀬???먯씠?꾪듃", "??, "#3B82F6"), "?곗씠?? => ("?곗씠??遺꾩꽍 ?먯씠?꾪듃", "??, "#10B981"), "臾몄꽌" => ("臾몄꽌 ?묒꽦 ?먯씠?꾪듃", "??, "#6366F1"), "?쇰Ц" => ("?쇰Ц 遺꾩꽍 ?먯씠?꾪듃", "??, "#6366F1"), "?뚯씪" => ("?뚯씪 愿€由??먯씠?꾪듃", "??, "#8B5CF6"), "?먮룞?? => ("?먮룞???먯씠?꾪듃", "??, "#EF4444"), // Code ?꾨━??移댄뀒怨좊━ "肄붾뱶媛쒕컻" => ("肄붾뱶 媛쒕컻 ?먯씠?꾪듃", "??, "#3B82F6"), "由ы뙥?곕쭅" => ("由ы뙥?곕쭅 ?먯씠?꾪듃", "??, "#6366F1"), "肄붾뱶由щ럭" => ("肄붾뱶 由щ럭 ?먯씠?꾪듃", "??, "#10B981"), "蹂댁븞?먭?" => ("蹂댁븞 ?먭? ?먯씠?꾪듃", "??, "#EF4444"), "?뚯뒪?? => ("?뚯뒪???먯씠?꾪듃", "??, "#F59E0B"), // Chat 移댄뀒怨좊━ "?곌뎄媛쒕컻" => ("?곌뎄媛쒕컻 ?먯씠?꾪듃", "??, "#0EA5E9"), "?쒖뒪?? => ("?쒖뒪???먯씠?꾪듃", "??, "#64748B"), "?섏쑉遺꾩꽍" => ("?섏쑉遺꾩꽍 ?먯씠?꾪듃", "??, "#F59E0B"), "?쒗뭹遺꾩꽍" => ("?쒗뭹遺꾩꽍 ?먯씠?꾪듃", "??, "#EC4899"), "寃쎌쁺" => ("寃쎌쁺 遺꾩꽍 ?먯씠?꾪듃", "??, "#8B5CF6"), "?몄궗" => ("?몄궗 愿€由??먯씠?꾪듃", "??, "#14B8A6"), "?쒖“湲곗닠" => ("?쒖“湲곗닠 ?먯씠?꾪듃", "??, "#F97316"), "?щТ" => ("?щТ 遺꾩꽍 ?먯씠?꾪듃", "??, "#6366F1"), _ when _activeTab == "Code" => ("肄붾뱶 ?먯씠?꾪듃", "??, "#3B82F6"), _ when _activeTab == "Cowork" => ("肄붿썙???먯씠?꾪듃", "??, "#4B5EFC"), _ => ("AX ?먯씠?꾪듃", "??, "#4B5EFC"), }; } /// ?щ㎎ ?좏깮 ?앹뾽 硫붾돱瑜??쒖떆?⑸땲?? private void ShowFormatMenu() { FormatMenuItems.Children.Clear(); var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var currentFormat = _settings.Settings.Llm.DefaultOutputFormat ?? "auto"; var formats = new (string Key, string Label, string Icon, string Color)[] { ("auto", "AI ?먮룞 ?좏깮", "\uE8BD", "#8B5CF6"), ("xlsx", "Excel", "\uE9F9", "#217346"), ("html", "HTML 蹂닿퀬??, "\uE12B", "#E44D26"), ("docx", "Word", "\uE8A5", "#2B579A"), ("md", "Markdown", "\uE943", "#6B7280"), ("csv", "CSV", "\uE9D9", "#10B981"), }; foreach (var (key, label, icon, color) in formats) { var isActive = key == currentFormat; var sp = new StackPanel { Orientation = Orientation.Horizontal }; // 而ㅼ뒪?€ 泥댄겕 ?꾩씠肄? sp.Children.Add(CreateCheckIcon(isActive, accentBrush)); sp.Children.Add(new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 13, Foreground = BrushFromHex(color), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0), }); sp.Children.Add(new TextBlock { Text = label, FontSize = 13, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, }); var itemBorder = new Border { Child = sp, Background = Brushes.Transparent, CornerRadius = new CornerRadius(8), Cursor = Cursors.Hand, Padding = new Thickness(8, 7, 12, 7), }; ApplyMenuItemHover(itemBorder); var capturedKey = key; itemBorder.MouseLeftButtonUp += (_, _) => { FormatMenuPopup.IsOpen = false; _settings.Settings.Llm.DefaultOutputFormat = capturedKey; _settings.Save(); BuildBottomBar(); }; FormatMenuItems.Children.Add(itemBorder); } // PlacementTarget???숈쟻 ?깅줉??踰꾪듉?쇰줈 ?ㅼ젙 if (FindName("BtnFormatMenu") is UIElement formatTarget) FormatMenuPopup.PlacementTarget = formatTarget; FormatMenuPopup.IsOpen = true; } /// ?붿옄??臾대뱶 ?좏깮 ?앹뾽 硫붾돱瑜??쒖떆?⑸땲?? private void ShowMoodMenu() { MoodMenuItems.Children.Clear(); var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; // 2??媛ㅻ윭由?洹몃━?? var grid = new System.Windows.Controls.Primitives.UniformGrid { Columns = 2 }; foreach (var mood in TemplateService.AllMoods) { var isActive = _selectedMood == mood.Key; var isCustom = _settings.Settings.Llm.CustomMoods.Any(cm => cm.Key == mood.Key); var colors = TemplateService.GetMoodColors(mood.Key); // 誘몃땲 ?꾨━酉?移대뱶 var previewCard = new Border { Width = 160, Height = 80, CornerRadius = new CornerRadius(6), Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString(colors.Background)), BorderBrush = isActive ? accentBrush : new SolidColorBrush((Color)ColorConverter.ConvertFromString(colors.Border)), BorderThickness = new Thickness(isActive ? 2 : 1), Padding = new Thickness(8, 6, 8, 6), Margin = new Thickness(2), }; var previewContent = new StackPanel(); // ?ㅻ뵫 ?쇱씤 previewContent.Children.Add(new Border { Width = 60, Height = 6, CornerRadius = new CornerRadius(2), Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString(colors.PrimaryText)), HorizontalAlignment = HorizontalAlignment.Left, Margin = new Thickness(0, 0, 0, 4), }); // ?낆꽱???쇱씤 previewContent.Children.Add(new Border { Width = 40, Height = 3, CornerRadius = new CornerRadius(1), Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString(colors.Accent)), HorizontalAlignment = HorizontalAlignment.Left, Margin = new Thickness(0, 0, 0, 6), }); // ?띿뒪???쇱씤?? for (int i = 0; i < 3; i++) { previewContent.Children.Add(new Border { Width = 120 - i * 20, Height = 3, CornerRadius = new CornerRadius(1), Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString(colors.SecondaryText)) { Opacity = 0.5 }, HorizontalAlignment = HorizontalAlignment.Left, Margin = new Thickness(0, 0, 0, 3), }); } // 誘몃땲 移대뱶 ?곸뿭 var cardRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 0) }; for (int i = 0; i < 2; i++) { cardRow.Children.Add(new Border { Width = 28, Height = 14, CornerRadius = new CornerRadius(2), Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString(colors.CardBg)), BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(colors.Border)), BorderThickness = new Thickness(0.5), Margin = new Thickness(0, 0, 4, 0), }); } previewContent.Children.Add(cardRow); previewCard.Child = previewContent; // 臾대뱶 ?쇰꺼 var labelPanel = new StackPanel { Margin = new Thickness(4, 2, 4, 4) }; var labelRow = new StackPanel { Orientation = Orientation.Horizontal }; labelRow.Children.Add(new TextBlock { Text = mood.Icon, FontSize = 12, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0), }); labelRow.Children.Add(new TextBlock { Text = mood.Label, FontSize = 11.5, Foreground = primaryText, FontWeight = isActive ? FontWeights.SemiBold : FontWeights.Normal, VerticalAlignment = VerticalAlignment.Center, }); if (isActive) { labelRow.Children.Add(new TextBlock { Text = " ??, FontSize = 11, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, }); } labelPanel.Children.Add(labelRow); // ?꾩껜 移대뱶 ?섑띁 var cardWrapper = new Border { CornerRadius = new CornerRadius(8), Background = Brushes.Transparent, Cursor = Cursors.Hand, Padding = new Thickness(4), Margin = new Thickness(2), }; var wrapperContent = new StackPanel(); wrapperContent.Children.Add(previewCard); wrapperContent.Children.Add(labelPanel); cardWrapper.Child = wrapperContent; // ?몃쾭 cardWrapper.MouseEnter += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x12, 0xFF, 0xFF, 0xFF)); }; cardWrapper.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; var capturedMood = mood; cardWrapper.MouseLeftButtonUp += (_, _) => { MoodMenuPopup.IsOpen = false; _selectedMood = capturedMood.Key; _settings.Settings.Llm.DefaultMood = capturedMood.Key; _settings.Save(); BuildBottomBar(); }; // 而ㅼ뒪?€ 臾대뱶: ?고겢由? if (isCustom) { cardWrapper.MouseRightButtonUp += (s, e) => { e.Handled = true; MoodMenuPopup.IsOpen = false; ShowCustomMoodContextMenu(s as Border, capturedMood.Key); }; } grid.Children.Add(cardWrapper); } MoodMenuItems.Children.Add(grid); // ?€?€ 援щ텇??+ 異붽? 踰꾪듉 ?€?€ MoodMenuItems.Children.Add(new System.Windows.Shapes.Rectangle { Height = 1, Fill = borderBrush, Margin = new Thickness(8, 4, 8, 4), Opacity = 0.4, }); var addSp = new StackPanel { Orientation = Orientation.Horizontal }; addSp.Children.Add(new TextBlock { Text = "\uE710", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 13, Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(4, 0, 8, 0), }); addSp.Children.Add(new TextBlock { Text = "而ㅼ뒪?€ 臾대뱶 異붽?", FontSize = 13, Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, }); var addBorder = new Border { Child = addSp, Background = Brushes.Transparent, CornerRadius = new CornerRadius(8), Cursor = Cursors.Hand, Padding = new Thickness(8, 6, 12, 6), }; ApplyMenuItemHover(addBorder); addBorder.MouseLeftButtonUp += (_, _) => { MoodMenuPopup.IsOpen = false; ShowCustomMoodDialog(); }; MoodMenuItems.Children.Add(addBorder); if (FindName("BtnMoodMenu") is UIElement moodTarget) MoodMenuPopup.PlacementTarget = moodTarget; MoodMenuPopup.IsOpen = true; } /// 而ㅼ뒪?€ 臾대뱶 異붽?/?몄쭛 ?ㅼ씠?쇰줈洹몃? ?쒖떆?⑸땲?? private void ShowCustomMoodDialog(Models.CustomMoodEntry? existing = null) { bool isEdit = existing != null; var dlg = new CustomMoodDialog( existingKey: existing?.Key ?? "", existingLabel: existing?.Label ?? "", existingIcon: existing?.Icon ?? "?렞", existingDesc: existing?.Description ?? "", existingCss: existing?.Css ?? "") { Owner = this, }; if (dlg.ShowDialog() == true) { if (isEdit) { existing!.Label = dlg.MoodLabel; existing.Icon = dlg.MoodIcon; existing.Description = dlg.MoodDescription; existing.Css = dlg.MoodCss; } else { _settings.Settings.Llm.CustomMoods.Add(new Models.CustomMoodEntry { Key = dlg.MoodKey, Label = dlg.MoodLabel, Icon = dlg.MoodIcon, Description = dlg.MoodDescription, Css = dlg.MoodCss, }); } _settings.Save(); TemplateService.LoadCustomMoods(_settings.Settings.Llm.CustomMoods); BuildBottomBar(); } } /// 而ㅼ뒪?€ 臾대뱶 ?고겢由?而⑦뀓?ㅽ듃 硫붾돱. private void ShowCustomMoodContextMenu(Border? anchor, string moodKey) { if (anchor == null) return; var popup = new System.Windows.Controls.Primitives.Popup { PlacementTarget = anchor, Placement = System.Windows.Controls.Primitives.PlacementMode.Right, StaysOpen = false, AllowsTransparency = true, }; var menuBg = TryFindResource("LauncherBackground") as Brush ?? Brushes.Black; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var menuBorder = new Border { Background = menuBg, CornerRadius = new CornerRadius(10), BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(4), MinWidth = 120, Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 12, ShadowDepth = 2, Opacity = 0.3, Color = Colors.Black, }, }; var stack = new StackPanel(); var editItem = CreateContextMenuItem("\uE70F", "?몄쭛", primaryText, secondaryText); editItem.MouseLeftButtonDown += (_, _) => { popup.IsOpen = false; var entry = _settings.Settings.Llm.CustomMoods.FirstOrDefault(c => c.Key == moodKey); if (entry != null) ShowCustomMoodDialog(entry); }; stack.Children.Add(editItem); var deleteItem = CreateContextMenuItem("\uE74D", "??젣", new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)), secondaryText); deleteItem.MouseLeftButtonDown += (_, _) => { popup.IsOpen = false; var result = CustomMessageBox.Show( $"???붿옄??臾대뱶瑜???젣?섏떆寃좎뒿?덇퉴?", "臾대뱶 ??젣", MessageBoxButton.YesNo, MessageBoxImage.Question); if (result == MessageBoxResult.Yes) { _settings.Settings.Llm.CustomMoods.RemoveAll(c => c.Key == moodKey); if (_selectedMood == moodKey) _selectedMood = "modern"; _settings.Save(); TemplateService.LoadCustomMoods(_settings.Settings.Llm.CustomMoods); BuildBottomBar(); } }; stack.Children.Add(deleteItem); menuBorder.Child = stack; popup.Child = menuBorder; popup.IsOpen = true; } private string? _promptCardPlaceholder; private void ShowPlaceholder() { if (string.IsNullOrEmpty(_promptCardPlaceholder)) return; InputWatermark.Text = _promptCardPlaceholder; InputWatermark.Visibility = Visibility.Visible; InputBox.Text = ""; InputBox.Focus(); } private void UpdateWatermarkVisibility() { // ?щ옒??移⑹씠 ?쒖꽦?붾릺???덉쑝硫??뚰꽣留덊겕 ?④린湲?(寃뱀묠 諛⑹?) if (_activeSlashCmd != null) { InputWatermark.Visibility = Visibility.Collapsed; return; } 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) { var templates = _settings.Settings.Llm.PromptTemplates; TemplateItems.Items.Clear(); if (templates == null || templates.Count == 0) { TemplateEmptyHint.Visibility = Visibility.Visible; TemplatePopup.IsOpen = true; return; } TemplateEmptyHint.Visibility = Visibility.Collapsed; foreach (var tpl in templates) { var item = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 8, 10, 8), Margin = new Thickness(2), Cursor = System.Windows.Input.Cursors.Hand, Tag = tpl.Content, }; var stack = new StackPanel(); stack.Children.Add(new TextBlock { Text = tpl.Name, FontSize = 13, FontWeight = FontWeights.SemiBold, Foreground = (Brush)FindResource("PrimaryText"), }); var preview = tpl.Content.Length > 60 ? tpl.Content[..60] + "?? : tpl.Content; stack.Children.Add(new TextBlock { Text = preview, FontSize = 11, Foreground = (Brush)FindResource("SecondaryText"), TextTrimming = TextTrimming.CharacterEllipsis, Margin = new Thickness(0, 2, 0, 0), }); item.Child = stack; item.MouseEnter += (s, _) => { if (s is Border b) b.Background = (Brush)FindResource("ItemBackground"); }; item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; item.MouseLeftButtonUp += (s, _) => { if (s is Border b && b.Tag is string content) { InputBox.Text = content; InputBox.CaretIndex = InputBox.Text.Length; InputBox.Focus(); TemplatePopup.IsOpen = false; } }; TemplateItems.Items.Add(item); } TemplatePopup.IsOpen = true; } // ?€?€?€ 紐⑤뜽 ?꾪솚 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ // Gemini/Claude ?ъ쟾 ?뺤쓽 紐⑤뜽 紐⑸줉 private static readonly (string Id, string Label)[] GeminiModels = { ("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"), }; private static readonly (string Id, string Label)[] ClaudeModels = { ("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"), }; /// ?꾩옱 ?좏깮??紐⑤뜽???쒖떆紐낆쓣 諛섑솚?⑸땲?? private string GetCurrentModelDisplayName() { var llm = _settings.Settings.Llm; var service = llm.Service.ToLowerInvariant(); if (service is "ollama" or "vllm") { // ?깅줉 紐⑤뜽?먯꽌 蹂꾩묶 李얘린 var registered = llm.RegisteredModels .FirstOrDefault(rm => rm.EncryptedModelName == llm.Model); if (registered != null) return registered.Alias; return string.IsNullOrEmpty(llm.Model) ? "(誘몄꽕??" : "?™€™€™€?; } if (service == "gemini") { var m = GeminiModels.FirstOrDefault(g => g.Id == llm.Model); return m.Label ?? llm.Model; } if (service == "claude") { var m = ClaudeModels.FirstOrDefault(c => c.Id == llm.Model); return m.Label ?? llm.Model; } return string.IsNullOrEmpty(llm.Model) ? "(誘몄꽕??" : llm.Model; } private void UpdateModelLabel() { var service = _settings.Settings.Llm.Service.ToLowerInvariant(); var serviceLabel = service switch { "gemini" => "Gemini", "claude" => "Claude", "vllm" => "vLLM", _ => "Ollama", }; ModelLabel.Text = $"{serviceLabel} 쨌 {GetCurrentModelDisplayName()}"; } private void BtnModelSelector_Click(object sender, RoutedEventArgs e) { var llm = _settings.Settings.Llm; // ?앹뾽 ?닿린 ???먮낯 ?쒕퉬??紐⑤뜽 諛깆뾽 ??紐⑤뜽 誘몄꽑?????レ쑝硫?濡ㅻ갚 var originalService = llm.Service; var originalModel = llm.Model; var modelConfirmed = false; var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E)); var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(30, 255, 255, 255)); var checkColor = new SolidColorBrush(Color.FromRgb(0x38, 0xA1, 0x69)); var activeSvcBg = new SolidColorBrush(Color.FromArgb(0x18, 0x4B, 0x5E, 0xFC)); var popup = new Popup { StaysOpen = false, AllowsTransparency = true, PopupAnimation = PopupAnimation.Fade, PlacementTarget = BtnModelSelector, Placement = PlacementMode.Top, }; var container = new Border { Background = bgBrush, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(12), Padding = new Thickness(6), MinWidth = 240, MaxHeight = 460, Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3, Color = Colors.Black }, }; var scroll = new ScrollViewer { VerticalScrollBarVisibility = ScrollBarVisibility.Auto, MaxHeight = 440 }; var stack = new StackPanel(); // 紐⑤뜽 紐⑸줉???댁쓣 ?⑤꼸 (?쒕퉬??蹂€寃????숈쟻 ?ш뎄?? var modelSection = new StackPanel(); Border CreateMenuItem(string text, bool isChecked, Action onClick, bool closeOnClick = true) { var item = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 7, 10, 7), Margin = new Thickness(0, 1, 0, 1), Cursor = Cursors.Hand, }; var g = new Grid(); g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(20) }); var textTb = new TextBlock { Text = text, FontSize = 13, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, }; Grid.SetColumn(textTb, 0); g.Children.Add(textTb); if (isChecked) { var checkIcon = new Canvas { Width = 14, Height = 14, VerticalAlignment = VerticalAlignment.Center }; checkIcon.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, StrokeStartLineCap = PenLineCap.Round, StrokeEndLineCap = PenLineCap.Round, StrokeLineJoin = PenLineJoin.Round, }); Grid.SetColumn(checkIcon, 1); g.Children.Add(checkIcon); } item.Child = g; // ?몃쾭 ??諛곌꼍??+ ?댁쭩 ?뺣? item.RenderTransformOrigin = new Point(0.5, 0.5); item.RenderTransform = new ScaleTransform(1, 1); item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; var st = item.RenderTransform as ScaleTransform; st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120))); st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120))); }; item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; var st = item.RenderTransform as ScaleTransform; st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150))); st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150))); }; item.MouseLeftButtonUp += (_, _) => { if (closeOnClick) popup.IsOpen = false; onClick(); }; return item; } Border CreateSeparator() => new() { Height = 1, Background = borderBrush, Opacity = 0.3, Margin = new Thickness(8, 4, 8, 4), }; // 紐⑤뜽 紐⑸줉 鍮뚮뱶 ?⑥닔 (?쒕퉬??蹂€寃????몄텧) void RebuildModelList(string service) { modelSection.Children.Clear(); modelSection.Children.Add(new TextBlock { Text = "紐⑤뜽", FontSize = 11, Foreground = secondaryText, Margin = new Thickness(10, 4, 0, 4), FontWeight = FontWeights.SemiBold, }); if (service is "ollama" or "vllm") { var registered = llm.RegisteredModels.Where(rm => rm.Service == service).ToList(); if (registered.Count == 0) { modelSection.Children.Add(new TextBlock { Text = "?깅줉??紐⑤뜽 ?놁쓬 ???ㅼ젙?먯꽌 異붽?", FontSize = 11.5, Foreground = secondaryText, FontStyle = FontStyles.Italic, Margin = new Thickness(10, 4, 10, 4), }); } else { foreach (var rm in registered) { var capturedEnc = rm.EncryptedModelName; modelSection.Children.Add(CreateMenuItem(rm.Alias, llm.Model == rm.EncryptedModelName, () => { _settings.Settings.Llm.Model = capturedEnc; _settings.Save(); modelConfirmed = true; UpdateModelLabel(); })); } } } else if (service == "gemini") { foreach (var (id, label) in GeminiModels) { var capturedId = id; modelSection.Children.Add(CreateMenuItem(label, llm.Model == id, () => { _settings.Settings.Llm.Model = capturedId; _settings.Save(); modelConfirmed = true; UpdateModelLabel(); })); } } else if (service == "claude") { foreach (var (id, label) in ClaudeModels) { var capturedId = id; modelSection.Children.Add(CreateMenuItem(label, llm.Model == id, () => { _settings.Settings.Llm.Model = capturedId; _settings.Save(); modelConfirmed = true; UpdateModelLabel(); })); } } } // ?쒕퉬???ㅻ뜑 stack.Children.Add(new TextBlock { Text = "?쒕퉬??, FontSize = 11, Foreground = secondaryText, Margin = new Thickness(10, 4, 0, 4), FontWeight = FontWeights.SemiBold, }); // ?쒕퉬??踰꾪듉?ㅼ쓣 ?댁쓣 ?⑤꼸 (泥댄겕留덊겕 媛깆떊?? var serviceItems = new StackPanel(); var services = new[] { ("ollama", "Ollama"), ("vllm", "vLLM"), ("gemini", "Gemini"), ("claude", "Claude") }; void BuildServiceItems() { serviceItems.Children.Clear(); var currentSvc = llm.Service.ToLowerInvariant(); foreach (var (svc, label) in services) { var capturedSvc = svc; serviceItems.Children.Add(CreateMenuItem(label, currentSvc == svc, () => { // ?쒕퉬?ㅻ쭔 ?꾩떆 諛섏쁺 ??Save??紐⑤뜽 ?좏깮 ?쒖뿉留??몄텧 _settings.Settings.Llm.Service = capturedSvc; UpdateModelLabel(); // ?쒕퉬??蹂€寃???紐⑤뜽 紐⑸줉 + ?쒕퉬??泥댄겕留덊겕 媛깆떊 (?앹뾽?€ ?대┛ 梨꾨줈) BuildServiceItems(); RebuildModelList(capturedSvc); }, closeOnClick: false)); } } BuildServiceItems(); stack.Children.Add(serviceItems); stack.Children.Add(CreateSeparator()); // 紐⑤뜽 紐⑸줉 珥덇린 鍮뚮뱶 RebuildModelList(llm.Service.ToLowerInvariant()); stack.Children.Add(modelSection); // ?앹뾽???ロ옄 ??紐⑤뜽 誘몄꽑?앹씠硫??쒕퉬??紐⑤뜽 ?먮옒?€濡?濡ㅻ갚 popup.Closed += (_, _) => { if (!modelConfirmed) { _settings.Settings.Llm.Service = originalService; _settings.Settings.Llm.Model = originalModel; UpdateModelLabel(); } }; scroll.Content = stack; container.Child = scroll; popup.Child = container; popup.IsOpen = true; } private void BtnNewChat_Click(object sender, RoutedEventArgs e) { StartNewConversation(); InputBox.Focus(); } public void ResumeConversation(string conversationId) { var conv = _storage.Load(conversationId); if (conv != null) { if (string.IsNullOrEmpty(conv.Tab)) conv.Tab = _activeTab; lock (_convLock) _currentConversation = conv; _activeTab = conv.Tab ?? _activeTab; _sessionState.RememberConversation(_activeTab, conv.Id); ApplySessionTabSelection(); LoadConversationSettings(); UpdateChatTitle(); RefreshConversationList(); RenderMessages(); UpdateFolderBar(); } InputBox.Focus(); } public void StartNewAndFocus() { StartNewConversation(); InputBox.Focus(); } private void BtnDeleteAll_Click(object sender, RoutedEventArgs e) { var result = CustomMessageBox.Show( "?€?λ맂 紐⑤뱺 ?€???댁뿭????젣?섏떆寃좎뒿?덇퉴?\n???묒뾽?€ ?섎룎由????놁뒿?덈떎.", "?€???꾩껜 ??젣", MessageBoxButton.YesNo, MessageBoxImage.Warning); if (result != MessageBoxResult.Yes) return; _storage.DeleteAll(); lock (_convLock) _sessionState.ClearCurrentConversation(_activeTab); MessagePanel.Children.Clear(); EmptyState.Visibility = Visibility.Visible; UpdateChatTitle(); RefreshConversationList(); } // ?€?€?€ 誘몃━蹂닿린 ?⑤꼸 (??湲곕컲) ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private static readonly HashSet _previewableExtensions = new(StringComparer.OrdinalIgnoreCase) { ".html", ".htm", ".md", ".txt", ".csv", ".json", ".xml", ".log", }; /// ?대젮 ?덈뒗 ?꾨━酉???紐⑸줉 (?뚯씪 寃쎈줈 湲곗?). private readonly List _previewTabs = new(); private string? _activePreviewTab; private void TryShowPreview(string filePath) { if (string.IsNullOrEmpty(filePath) || !System.IO.File.Exists(filePath)) return; // 蹂꾨룄 而ㅼ뒪?€ 李쎌쑝濡?誘몃━蹂닿린 (WebView2 HWND airspace 臾몄젣 洹쇰낯 ?닿껐) 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) { PreviewColumn.Width = new GridLength(420); SplitterColumn.Width = new GridLength(5); } PreviewPanel.Visibility = Visibility.Visible; PreviewSplitter.Visibility = Visibility.Visible; BtnPreviewToggle.Visibility = Visibility.Visible; RebuildPreviewTabs(); LoadPreviewContent(filePath); } /// ??諛?UI瑜??ㅼ떆 援ъ꽦?⑸땲?? private void RebuildPreviewTabs() { PreviewTabPanel.Children.Clear(); var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; foreach (var tabPath in _previewTabs) { var fileName = System.IO.Path.GetFileName(tabPath); var isActive = string.Equals(tabPath, _activePreviewTab, StringComparison.OrdinalIgnoreCase); var tabBorder = new Border { Background = isActive ? new SolidColorBrush(Color.FromArgb(0x15, 0xFF, 0xFF, 0xFF)) : Brushes.Transparent, BorderBrush = isActive ? accentBrush : Brushes.Transparent, BorderThickness = new Thickness(0, 0, 0, isActive ? 2 : 0), Padding = new Thickness(8, 6, 4, 6), Cursor = Cursors.Hand, MaxWidth = _previewTabs.Count <= 3 ? 200 : (_previewTabs.Count <= 5 ? 140 : 100), }; var tabContent = new StackPanel { Orientation = Orientation.Horizontal }; // ?뚯씪紐? tabContent.Children.Add(new TextBlock { Text = fileName, FontSize = 11, Foreground = isActive ? primaryText : secondaryText, FontWeight = isActive ? FontWeights.SemiBold : FontWeights.Normal, VerticalAlignment = VerticalAlignment.Center, TextTrimming = TextTrimming.CharacterEllipsis, MaxWidth = tabBorder.MaxWidth - 30, ToolTip = tabPath, }); // ?リ린 踰꾪듉 (x) ???쒖꽦 ??? ??긽 ?쒖떆, 鍮꾪솢????? ?몃쾭 ?쒖뿉留??쒖떆 var closeFg = isActive ? primaryText : secondaryText; var closeBtnText = new TextBlock { Text = "\uE711", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 10, Foreground = closeFg, VerticalAlignment = VerticalAlignment.Center, }; var closeBtn = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(3), Padding = new Thickness(3, 2, 3, 2), Margin = new Thickness(5, 0, 0, 0), Cursor = Cursors.Hand, VerticalAlignment = VerticalAlignment.Center, // 鍮꾪솢????? 珥덇린???④?, ?쒖꽦 ??? ??긽 ?쒖떆 Visibility = isActive ? Visibility.Visible : Visibility.Hidden, Child = closeBtnText, }; var closePath = tabPath; closeBtn.MouseEnter += (s, _) => { if (s is Border b) { b.Background = new SolidColorBrush(Color.FromArgb(0x40, 0xFF, 0x50, 0x50)); if (b.Child is TextBlock tb) tb.Foreground = new SolidColorBrush(Color.FromRgb(0xFF, 0x60, 0x60)); } }; closeBtn.MouseLeave += (s, _) => { if (s is Border b) { b.Background = Brushes.Transparent; if (b.Child is TextBlock tb) tb.Foreground = closeFg; } }; closeBtn.Tag = "close"; // ?リ린 踰꾪듉 ?앸퀎?? closeBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; // 遺€紐????대┃ ?대깽??李⑤떒 ClosePreviewTab(closePath); }; tabContent.Children.Add(closeBtn); tabBorder.Child = tabContent; // ???대┃ ???쒖꽦??(MouseLeftButtonUp ?ъ슜: ?リ린 踰꾪듉??PreviewMouseLeftButtonDown蹂대떎 ??쾶 ?ㅽ뻾?섏뼱 異⑸룎 諛⑹?) var clickPath = tabPath; tabBorder.MouseLeftButtonUp += (_, e) => { if (e.Handled) return; e.Handled = true; _activePreviewTab = clickPath; RebuildPreviewTabs(); LoadPreviewContent(clickPath); }; // ?고겢由???而⑦뀓?ㅽ듃 硫붾돱 var ctxPath = tabPath; tabBorder.MouseRightButtonUp += (_, e) => { e.Handled = true; ShowPreviewTabContextMenu(ctxPath); }; // ?붾툝?대┃ ??蹂꾨룄 李쎌뿉??蹂닿린 var dblPath = tabPath; tabBorder.MouseLeftButtonDown += (_, e) => { if (e.Handled) return; if (e.ClickCount == 2) { e.Handled = true; OpenPreviewPopupWindow(dblPath); } }; // ?몃쾭 ?④낵 ??鍮꾪솢????뿉??諛곌꼍 媛뺤“ + ?リ린 踰꾪듉 ?쒖떆 var capturedIsActive = isActive; var capturedCloseBtn = closeBtn; tabBorder.MouseEnter += (s, _) => { if (s is Border b && !capturedIsActive) b.Background = new SolidColorBrush(Color.FromArgb(0x10, 0xFF, 0xFF, 0xFF)); // 鍮꾪솢????룄 ?몃쾭 ???リ린 踰꾪듉 ?쒖떆 if (!capturedIsActive) capturedCloseBtn.Visibility = Visibility.Visible; }; tabBorder.MouseLeave += (s, _) => { if (s is Border b && !capturedIsActive) b.Background = Brushes.Transparent; // 鍮꾪솢?????몃쾭 ?댁젣 ???リ린 踰꾪듉 ?④? if (!capturedIsActive) capturedCloseBtn.Visibility = Visibility.Hidden; }; PreviewTabPanel.Children.Add(tabBorder); // ???ъ씠 援щ텇?? if (tabPath != _previewTabs[^1]) { PreviewTabPanel.Children.Add(new Border { Width = 1, Height = 14, Background = borderBrush, Margin = new Thickness(0, 4, 0, 4), VerticalAlignment = VerticalAlignment.Center, }); } } } private void ClosePreviewTab(string filePath) { _previewTabs.Remove(filePath); if (_previewTabs.Count == 0) { HidePreviewPanel(); return; } // ?ロ엺 ??씠 ?쒖꽦 ??씠硫?留덉?留???쑝濡??꾪솚 if (string.Equals(filePath, _activePreviewTab, StringComparison.OrdinalIgnoreCase)) { _activePreviewTab = _previewTabs[^1]; LoadPreviewContent(_activePreviewTab); } RebuildPreviewTabs(); } private async void LoadPreviewContent(string filePath) { var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant(); // 紐⑤뱺 肄섑뀗痢??④린湲? PreviewWebView.Visibility = Visibility.Collapsed; PreviewTextScroll.Visibility = Visibility.Collapsed; PreviewDataGrid.Visibility = Visibility.Collapsed; PreviewEmpty.Visibility = Visibility.Collapsed; if (!System.IO.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(); var mdText = System.IO.File.ReadAllText(filePath); if (mdText.Length > 50000) mdText = mdText[..50000]; var mdHtml = Services.Agent.TemplateService.RenderMarkdownToHtml(mdText, _selectedMood); PreviewWebView.NavigateToString(mdHtml); PreviewWebView.Visibility = Visibility.Visible; break; case ".txt": case ".json": case ".xml": case ".log": var text = System.IO.File.ReadAllText(filePath); if (text.Length > 50000) text = text[..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 bool _webViewInitialized; private static readonly string WebView2DataFolder = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "WebView2"); private async Task EnsureWebViewInitializedAsync() { if (_webViewInitialized) return; try { var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync( userDataFolder: WebView2DataFolder); await PreviewWebView.EnsureCoreWebView2Async(env); _webViewInitialized = true; } catch (Exception ex) { Services.LogService.Warn($"WebView2 珥덇린???ㅽ뙣: {ex.Message}"); } } private void LoadCsvPreview(string filePath) { try { var lines = System.IO.File.ReadAllLines(filePath); if (lines.Length == 0) return; var dt = new System.Data.DataTable(); var headers = ParseCsvLine(lines[0]); foreach (var h in headers) dt.Columns.Add(h); var maxRows = Math.Min(lines.Length, 501); for (int i = 1; i < maxRows; i++) { var vals = ParseCsvLine(lines[i]); var row = dt.NewRow(); for (int j = 0; j < Math.Min(vals.Length, headers.Length); j++) row[j] = vals[j]; dt.Rows.Add(row); } PreviewDataGrid.ItemsSource = dt.DefaultView; } catch (Exception ex) { PreviewTextBlock.Text = $"CSV 濡쒕뱶 ?ㅻ쪟: {ex.Message}"; PreviewTextScroll.Visibility = Visibility.Visible; PreviewDataGrid.Visibility = Visibility.Collapsed; } } private static string[] ParseCsvLine(string line) { var fields = new System.Collections.Generic.List(); var current = new System.Text.StringBuilder(); bool inQuotes = false; for (int i = 0; i < line.Length; i++) { char c = line[i]; if (inQuotes) { if (c == '"' && i + 1 < line.Length && line[i + 1] == '"') { current.Append('"'); i++; } else if (c == '"') inQuotes = false; else current.Append(c); } else { if (c == '"') inQuotes = true; else if (c == ',') { fields.Add(current.ToString()); current.Clear(); } else current.Append(c); } } fields.Add(current.ToString()); return fields.ToArray(); } private void HidePreviewPanel() { _previewTabs.Clear(); _activePreviewTab = null; PreviewColumn.Width = new GridLength(0); SplitterColumn.Width = new GridLength(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 { } } /// ?꾨━酉???諛??대┃ ??WebView2?먯꽌 ?ъ빱?ㅻ? ?뚯닔 (HWND airspace 臾몄젣 諛⑹?). private void PreviewTabBar_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) { // WebView2媛€ ?ъ빱?ㅻ? ?↔퀬 ?덉쑝硫?WPF 踰꾪듉 ?대┃??臾댁떆?????덉쑝誘€濡??ъ빱?ㅻ? 媛뺤젣 ?대룞 if (PreviewWebView.IsFocused || PreviewWebView.IsKeyboardFocusWithin) { var border = sender as 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); SplitterColumn.Width = new GridLength(0); } else if (_previewTabs.Count > 0) { // ?ㅼ떆 ?닿린 PreviewPanel.Visibility = Visibility.Visible; PreviewSplitter.Visibility = Visibility.Visible; PreviewColumn.Width = new GridLength(420); SplitterColumn.Width = new GridLength(5); RebuildPreviewTabs(); if (_activePreviewTab != null) LoadPreviewContent(_activePreviewTab); } } private void BtnOpenExternal_Click(object sender, RoutedEventArgs e) { if (string.IsNullOrEmpty(_activePreviewTab) || !System.IO.File.Exists(_activePreviewTab)) return; try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = _activePreviewTab, UseShellExecute = true, }); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"?몃? ?꾨줈洹몃옩 ?ㅽ뻾 ?ㅻ쪟: {ex.Message}"); } } /// ?꾨━酉????고겢由?而⑦뀓?ㅽ듃 硫붾돱瑜??쒖떆?⑸땲?? private Popup? _previewTabPopup; private void ShowPreviewTabContextMenu(string filePath) { // 湲곗〈 ?앹뾽 ?リ린 if (_previewTabPopup != null) _previewTabPopup.IsOpen = false; var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); var stack = new StackPanel(); void AddItem(string icon, string iconColor, string label, Action action) { var itemBorder = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(6), Padding = new Thickness(10, 7, 16, 7), Cursor = Cursors.Hand, }; var sp = new StackPanel { Orientation = Orientation.Horizontal }; sp.Children.Add(new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = string.IsNullOrEmpty(iconColor) ? secondaryText : new SolidColorBrush((Color)ColorConverter.ConvertFromString(iconColor)), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0), }); sp.Children.Add(new TextBlock { Text = label, FontSize = 13, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, }); itemBorder.Child = sp; itemBorder.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; }; itemBorder.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; itemBorder.MouseLeftButtonUp += (_, _) => { _previewTabPopup!.IsOpen = false; action(); }; stack.Children.Add(itemBorder); } void AddSeparator() { stack.Children.Add(new Border { Height = 1, Background = borderBrush, Margin = new Thickness(8, 3, 8, 3), }); } AddItem("\uE8A7", "#64B5F6", "?몃? ?꾨줈洹몃옩?쇰줈 ?닿린", () => { try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = filePath, UseShellExecute = true, }); } catch { } }); AddItem("\uE838", "#FFB74D", "?뚯씪 ?꾩튂 ?닿린", () => { try { System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{filePath}\""); } catch { } }); AddItem("\uE8A7", "#81C784", "蹂꾨룄 李쎌뿉??蹂닿린", () => OpenPreviewPopupWindow(filePath)); AddSeparator(); AddItem("\uE8C8", "", "寃쎈줈 蹂듭궗", () => { try { Clipboard.SetText(filePath); } catch { } }); AddSeparator(); AddItem("\uE711", "#EF5350", "?????リ린", () => ClosePreviewTab(filePath)); if (_previewTabs.Count > 1) { AddItem("\uE8BB", "#EF5350", "?ㅻⅨ ??紐⑤몢 ?リ린", () => { var keep = filePath; _previewTabs.RemoveAll(p => !string.Equals(p, keep, StringComparison.OrdinalIgnoreCase)); _activePreviewTab = keep; RebuildPreviewTabs(); LoadPreviewContent(keep); }); } var popupBorder = new Border { Background = bg, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(12), Padding = new Thickness(4, 6, 4, 6), MinWidth = 180, Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 16, Opacity = 0.4, ShadowDepth = 4, Color = Colors.Black, }, Child = stack, }; _previewTabPopup = new Popup { Child = popupBorder, Placement = PlacementMode.MousePoint, StaysOpen = false, AllowsTransparency = true, PopupAnimation = PopupAnimation.Fade, }; _previewTabPopup.IsOpen = true; } /// ?꾨━酉곕? 蹂꾨룄 ?앹뾽 李쎌뿉???쎈땲?? private void OpenPreviewPopupWindow(string filePath) { if (!System.IO.File.Exists(filePath)) return; var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant(); var fileName = System.IO.Path.GetFileName(filePath); var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); var fg = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var win = new Window { Title = $"誘몃━蹂닿린 ??{fileName}", Width = 900, Height = 700, WindowStartupLocation = WindowStartupLocation.CenterScreen, Background = bg, }; FrameworkElement content; switch (ext) { case ".html": case ".htm": var wv = new Microsoft.Web.WebView2.Wpf.WebView2(); wv.Loaded += async (_, _) => { try { var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync( userDataFolder: WebView2DataFolder); await wv.EnsureCoreWebView2Async(env); wv.Source = new Uri(filePath); } catch { } }; content = wv; break; case ".md": var mdWv = new Microsoft.Web.WebView2.Wpf.WebView2(); var mdMood = _selectedMood; mdWv.Loaded += async (_, _) => { try { var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync( userDataFolder: WebView2DataFolder); await mdWv.EnsureCoreWebView2Async(env); var mdSrc = System.IO.File.ReadAllText(filePath); if (mdSrc.Length > 100000) mdSrc = mdSrc[..100000]; var html = Services.Agent.TemplateService.RenderMarkdownToHtml(mdSrc, mdMood); mdWv.NavigateToString(html); } catch { } }; content = mdWv; break; case ".csv": var dg = new System.Windows.Controls.DataGrid { AutoGenerateColumns = true, IsReadOnly = true, Background = Brushes.Transparent, Foreground = Brushes.White, BorderThickness = new Thickness(0), FontSize = 12, }; try { var lines = System.IO.File.ReadAllLines(filePath); if (lines.Length > 0) { var dt = new System.Data.DataTable(); var headers = ParseCsvLine(lines[0]); foreach (var h in headers) dt.Columns.Add(h); for (int i = 1; i < Math.Min(lines.Length, 1001); i++) { var vals = ParseCsvLine(lines[i]); var row = dt.NewRow(); for (int j = 0; j < Math.Min(vals.Length, dt.Columns.Count); j++) row[j] = vals[j]; dt.Rows.Add(row); } dg.ItemsSource = dt.DefaultView; } } catch { } content = dg; break; default: var text = System.IO.File.ReadAllText(filePath); if (text.Length > 100000) text = text[..100000] + "\n\n... (?댄썑 ?앸왂)"; var sv = new ScrollViewer { VerticalScrollBarVisibility = ScrollBarVisibility.Auto, Padding = new Thickness(20), Content = new TextBlock { Text = text, TextWrapping = TextWrapping.Wrap, FontFamily = new FontFamily("Consolas"), FontSize = 13, Foreground = fg, }, }; content = sv; break; } win.Content = content; win.Show(); } // ?€?€?€ ?먯씠?꾪듃 ?ㅽ떚??吏꾪뻾瑜?諛??€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private DateTime _progressStartTime; private DispatcherTimer? _progressElapsedTimer; private void UpdateAgentProgressBar(AgentEvent evt) { switch (evt.Type) { case AgentEventType.Planning when evt.Steps is { Count: > 0 }: ShowStickyProgress(evt.Steps.Count); break; case AgentEventType.StepStart when evt.StepTotal > 0: UpdateStickyProgress(evt.StepCurrent, evt.StepTotal, evt.Summary); break; case AgentEventType.Complete: HideStickyProgress(); break; } } private void ShowStickyProgress(int totalSteps) { _progressStartTime = DateTime.Now; AgentProgressBar.Visibility = Visibility.Visible; ProgressIcon.Text = "\uE768"; // play ProgressStepLabel.Text = $"?묒뾽 以€鍮?以?.. (0/{totalSteps})"; ProgressPercent.Text = "0%"; ProgressElapsed.Text = "0:00"; ProgressFill.Width = 0; // 寃쎄낵 ?쒓컙 ?€?대㉧ _progressElapsedTimer?.Stop(); _progressElapsedTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; _progressElapsedTimer.Tick += (_, _) => { var elapsed = DateTime.Now - _progressStartTime; ProgressElapsed.Text = elapsed.TotalHours >= 1 ? elapsed.ToString(@"h\:mm\:ss") : elapsed.ToString(@"m\:ss"); }; _progressElapsedTimer.Start(); } private void UpdateStickyProgress(int currentStep, int totalSteps, string stepDescription) { if (AgentProgressBar.Visibility != Visibility.Visible) return; var pct = totalSteps > 0 ? (double)currentStep / totalSteps : 0; ProgressStepLabel.Text = $"{stepDescription} ({currentStep}/{totalSteps})"; ProgressPercent.Text = $"{(int)(pct * 100)}%"; // ?꾨줈洹몃젅??諛??덈퉬 ?좊땲硫붿씠?? var parentBorder = ProgressFill.Parent as Border; if (parentBorder != null) { var targetWidth = parentBorder.ActualWidth * pct; var anim = new System.Windows.Media.Animation.DoubleAnimation( ProgressFill.Width, targetWidth, TimeSpan.FromMilliseconds(300)) { EasingFunction = new System.Windows.Media.Animation.QuadraticEase(), }; ProgressFill.BeginAnimation(WidthProperty, anim); } } private void HideStickyProgress() { _progressElapsedTimer?.Stop(); _progressElapsedTimer = null; if (AgentProgressBar.Visibility != Visibility.Visible) return; // ?꾨즺 ?쒖떆 ???섏씠?쒖븘?? ProgressIcon.Text = "\uE930"; // check ProgressStepLabel.Text = "?묒뾽 ?꾨즺"; ProgressPercent.Text = "100%"; // ?꾨줈洹몃젅??諛?100% var parentBorder = ProgressFill.Parent as Border; if (parentBorder != null) { var anim = new System.Windows.Media.Animation.DoubleAnimation( ProgressFill.Width, parentBorder.ActualWidth, TimeSpan.FromMilliseconds(200)); ProgressFill.BeginAnimation(WidthProperty, anim); } // 3珥????④린湲? var hideTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(3) }; hideTimer.Tick += (_, _) => { hideTimer.Stop(); var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(300)); fadeOut.Completed += (_, _) => { AgentProgressBar.Visibility = Visibility.Collapsed; AgentProgressBar.Opacity = 1; ProgressFill.BeginAnimation(WidthProperty, null); ProgressFill.Width = 0; }; AgentProgressBar.BeginAnimation(UIElement.OpacityProperty, fadeOut); }; hideTimer.Start(); } // ?€?€?€ ?뚯씪 ?먯깋湲??€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private static readonly HashSet _ignoredDirs = new(StringComparer.OrdinalIgnoreCase) { "bin", "obj", "node_modules", ".git", ".vs", ".idea", ".vscode", "__pycache__", ".mypy_cache", ".pytest_cache", "dist", "build", ".cache", ".next", ".nuxt", "coverage", ".terraform", }; private DispatcherTimer? _fileBrowserRefreshTimer; 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) { var folder = GetCurrentWorkFolder(); if (string.IsNullOrEmpty(folder) || !System.IO.Directory.Exists(folder)) return; try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = folder, UseShellExecute = true }); } catch { } } private void BtnFileBrowserClose_Click(object sender, RoutedEventArgs e) { FileBrowserPanel.Visibility = Visibility.Collapsed; } private void BuildFileTree() { FileTreeView.Items.Clear(); var folder = GetCurrentWorkFolder(); if (string.IsNullOrEmpty(folder) || !System.IO.Directory.Exists(folder)) { FileTreeView.Items.Add(new TreeViewItem { Header = "?묒뾽 ?대뜑瑜??좏깮?섏꽭??, IsEnabled = false }); return; } FileBrowserTitle.Text = $"?뚯씪 ?먯깋湲???{System.IO.Path.GetFileName(folder)}"; var count = 0; PopulateDirectory(new System.IO.DirectoryInfo(folder), FileTreeView.Items, 0, ref count); } private void PopulateDirectory(System.IO.DirectoryInfo dir, ItemCollection items, int depth, ref int count) { if (depth > 4 || count > 200) return; // ?붾젆?곕━ try { foreach (var subDir in dir.GetDirectories().OrderBy(d => d.Name)) { if (count > 200) break; if (_ignoredDirs.Contains(subDir.Name) || subDir.Name.StartsWith('.')) continue; count++; var dirItem = new TreeViewItem { Header = CreateFileTreeHeader("\uED25", subDir.Name, null), Tag = subDir.FullName, IsExpanded = depth < 1, }; // 吏€??濡쒕뵫: ?붾? ?먯떇 ???쇱튌 ???ㅼ젣 濡쒕뱶 if (depth < 3) { dirItem.Items.Add(new TreeViewItem { Header = "濡쒕뵫 以?.." }); // ?붾? var capturedDir = subDir; var capturedDepth = depth; dirItem.Expanded += (s, _) => { if (s is TreeViewItem ti && ti.Items.Count == 1 && ti.Items[0] is TreeViewItem d && d.Header?.ToString() == "濡쒕뵫 以?..") { ti.Items.Clear(); int c = 0; PopulateDirectory(capturedDir, ti.Items, capturedDepth + 1, ref c); } }; } else { PopulateDirectory(subDir, dirItem.Items, depth + 1, ref count); } items.Add(dirItem); } } catch { } // ?뚯씪 try { foreach (var file in dir.GetFiles().OrderBy(f => f.Name)) { if (count > 200) break; count++; var ext = file.Extension.ToLowerInvariant(); var icon = GetFileIcon(ext); var size = FormatFileSize(file.Length); var fileItem = new TreeViewItem { Header = CreateFileTreeHeader(icon, file.Name, size), Tag = file.FullName, }; // ?붾툝?대┃ ???꾨━酉? var capturedPath = file.FullName; fileItem.MouseDoubleClick += (s, e) => { e.Handled = true; TryShowPreview(capturedPath); }; // ?고겢由???而⑦뀓?ㅽ듃 硫붾돱 (MouseRightButtonUp?먯꽌 ?댁뼱??Popup??諛붾줈 ?ロ엳吏€ ?딆쓬) fileItem.MouseRightButtonUp += (s, e) => { e.Handled = true; if (s is TreeViewItem ti) ti.IsSelected = true; ShowFileTreeContextMenu(capturedPath); }; items.Add(fileItem); } } catch { } } private static StackPanel CreateFileTreeHeader(string icon, string name, string? sizeText) { var sp = new StackPanel { Orientation = Orientation.Horizontal }; sp.Children.Add(new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 11, Foreground = new SolidColorBrush(Color.FromRgb(0x9C, 0xA3, 0xAF)), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 5, 0), }); sp.Children.Add(new TextBlock { Text = name, FontSize = 11.5, VerticalAlignment = VerticalAlignment.Center, }); if (sizeText != null) { sp.Children.Add(new TextBlock { Text = $" {sizeText}", FontSize = 10, Foreground = new SolidColorBrush(Color.FromRgb(0x6B, 0x72, 0x80)), VerticalAlignment = VerticalAlignment.Center, }); } return sp; } private static string GetFileIcon(string ext) => ext switch { ".html" or ".htm" => "\uEB41", ".xlsx" or ".xls" => "\uE9F9", ".docx" or ".doc" => "\uE8A5", ".pdf" => "\uEA90", ".csv" => "\uE80A", ".md" => "\uE70B", ".json" or ".xml" => "\uE943", ".png" or ".jpg" or ".jpeg" or ".gif" or ".svg" or ".webp" => "\uEB9F", ".cs" or ".py" or ".js" or ".ts" or ".java" or ".cpp" => "\uE943", ".bat" or ".cmd" or ".ps1" or ".sh" => "\uE756", ".txt" or ".log" => "\uE8A5", _ => "\uE7C3", }; private static string FormatFileSize(long bytes) => bytes switch { < 1024 => $"{bytes} B", < 1024 * 1024 => $"{bytes / 1024.0:F1} KB", _ => $"{bytes / (1024.0 * 1024.0):F1} MB", }; private void ShowFileTreeContextMenu(string filePath) { var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var hoverBg = TryFindResource("HintBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); var dangerBrush = new SolidColorBrush(Color.FromRgb(0xE7, 0x4C, 0x3C)); var popup = new Popup { StaysOpen = false, AllowsTransparency = true, PopupAnimation = PopupAnimation.Fade, Placement = PlacementMode.MousePoint, }; var panel = new StackPanel { Margin = new Thickness(2) }; var container = new Border { Background = bg, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(10), Padding = new Thickness(6), MinWidth = 200, Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3, Color = Colors.Black, Direction = 270, }, Child = panel, }; popup.Child = container; void AddItem(string icon, string label, Action action, Brush? labelColor = null, Brush? iconColor = null) { var sp = new StackPanel { Orientation = Orientation.Horizontal }; sp.Children.Add(new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 13, Foreground = iconColor ?? secondaryText, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0), }); sp.Children.Add(new TextBlock { Text = label, FontSize = 12.5, Foreground = labelColor ?? primaryText, VerticalAlignment = VerticalAlignment.Center, }); var item = new Border { Child = sp, Background = Brushes.Transparent, CornerRadius = new CornerRadius(7), Cursor = Cursors.Hand, Padding = new Thickness(10, 8, 14, 8), Margin = new Thickness(0, 1, 0, 1), }; item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; }; item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; item.MouseLeftButtonUp += (_, _) => { popup.IsOpen = false; action(); }; panel.Children.Add(item); } void AddSep() { panel.Children.Add(new Border { Height = 1, Margin = new Thickness(10, 4, 10, 4), Background = borderBrush, Opacity = 0.3, }); } var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant(); if (_previewableExtensions.Contains(ext)) AddItem("\uE8A1", "誘몃━蹂닿린", () => ShowPreviewPanel(filePath)); AddItem("\uE8A7", "?몃? ?꾨줈洹몃옩?쇰줈 ?닿린", () => { try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = filePath, UseShellExecute = true }); } catch { } }); AddItem("\uED25", "?대뜑?먯꽌 蹂닿린", () => { try { System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{filePath}\""); } catch { } }); AddItem("\uE8C8", "寃쎈줈 蹂듭궗", () => { try { Clipboard.SetText(filePath); ShowToast("寃쎈줈 蹂듭궗??); } catch { } }); AddSep(); // ?대쫫 蹂€寃? AddItem("\uE8AC", "?대쫫 蹂€寃?, () => { var dir = System.IO.Path.GetDirectoryName(filePath) ?? ""; var oldName = System.IO.Path.GetFileName(filePath); var dlg = new Views.InputDialog("?대쫫 蹂€寃?, "???뚯씪 ?대쫫:", oldName) { Owner = this }; if (dlg.ShowDialog() == true && !string.IsNullOrWhiteSpace(dlg.ResponseText)) { var newPath = System.IO.Path.Combine(dir, dlg.ResponseText.Trim()); try { System.IO.File.Move(filePath, newPath); BuildFileTree(); ShowToast($"?대쫫 蹂€寃? {dlg.ResponseText.Trim()}"); } catch (Exception ex) { ShowToast($"?대쫫 蹂€寃??ㅽ뙣: {ex.Message}", "\uE783"); } } }); // ??젣 AddItem("\uE74D", "??젣", () => { var result = MessageBox.Show( $"?뚯씪????젣?섏떆寃좎뒿?덇퉴?\n{System.IO.Path.GetFileName(filePath)}", "?뚯씪 ??젣 ?뺤씤", MessageBoxButton.YesNo, MessageBoxImage.Warning); if (result == MessageBoxResult.Yes) { try { System.IO.File.Delete(filePath); BuildFileTree(); ShowToast("?뚯씪 ??젣??); } catch (Exception ex) { ShowToast($"??젣 ?ㅽ뙣: {ex.Message}", "\uE783"); } } }, dangerBrush, dangerBrush); // Dispatcher濡??댁뼱??MouseRightButtonUp ??諛붾줈 ?ロ엳吏€ ?딆쓬 Dispatcher.BeginInvoke(() => { popup.IsOpen = true; }, System.Windows.Threading.DispatcherPriority.Input); } /// ?먯씠?꾪듃媛€ ?뚯씪 ?앹꽦 ???뚯씪 ?먯깋湲곕? ?먮룞 ?덈줈怨좎묠?⑸땲?? private void RefreshFileTreeIfVisible() { if (FileBrowserPanel.Visibility != Visibility.Visible) return; // ?붾컮?댁뒪: 500ms ??以묐났 ?몄텧 諛⑹? _fileBrowserRefreshTimer?.Stop(); _fileBrowserRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) }; _fileBrowserRefreshTimer.Tick += (_, _) => { _fileBrowserRefreshTimer.Stop(); BuildFileTree(); }; _fileBrowserRefreshTimer.Start(); } // ?€?€?€ ?섎떒 ?곹깭諛??€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private System.Windows.Media.Animation.Storyboard? _statusSpinStoryboard; private void UpdateStatusBar(AgentEvent evt) { var toolLabel = evt.ToolName switch { "file_read" or "document_read" => "?뚯씪 ?쎄린", "file_write" => "?뚯씪 ?곌린", "file_edit" => "?뚯씪 ?섏젙", "html_create" => "HTML ?앹꽦", "xlsx_create" => "Excel ?앹꽦", "docx_create" => "Word ?앹꽦", "csv_create" => "CSV ?앹꽦", "md_create" => "Markdown ?앹꽦", "folder_map" => "?대뜑 ?먯깋", "glob" => "?뚯씪 寃€??, "grep" => "?댁슜 寃€??, "process" => "紐낅졊 ?ㅽ뻾", _ => evt.ToolName, }; switch (evt.Type) { case AgentEventType.Thinking: SetStatus("?앷컖 以?..", spinning: true); break; case AgentEventType.Planning: SetStatus($"怨꾪쉷 ?섎┰ 以???{evt.StepTotal}?④퀎", spinning: true); break; case AgentEventType.ToolCall: SetStatus($"{toolLabel} ?ㅽ뻾 以?..", spinning: true); break; case AgentEventType.ToolResult: SetStatus(evt.Success ? $"{toolLabel} ?꾨즺" : $"{toolLabel} ?ㅽ뙣", 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; } } private void SetStatus(string text, bool spinning) { if (StatusLabel != null) StatusLabel.Text = text; _sessionState.SetRuntimeState(_isStreaming, text, spinning); if (spinning) StartStatusAnimation(); else StopStatusAnimation(); UpdateRuntimeActivityIndicators(); } private void StartStatusAnimation() { if (_statusSpinStoryboard != null) return; var anim = new System.Windows.Media.Animation.DoubleAnimation { From = 0, To = 360, Duration = TimeSpan.FromSeconds(2), RepeatBehavior = System.Windows.Media.Animation.RepeatBehavior.Forever, }; _statusSpinStoryboard = new System.Windows.Media.Animation.Storyboard(); System.Windows.Media.Animation.Storyboard.SetTarget(anim, StatusDiamond); System.Windows.Media.Animation.Storyboard.SetTargetProperty(anim, new PropertyPath("(UIElement.RenderTransform).(RotateTransform.Angle)")); _statusSpinStoryboard.Children.Add(anim); _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 = ""; _sessionState.SetRuntimeState(false, "?€湲?以?, false); UpdateRuntimeActivityIndicators(); } private void UpdateStatusTokens(int inputTokens, int outputTokens) { if (StatusTokens == null) return; var llm = _settings.Settings.Llm; var (inCost, outCost) = Services.TokenEstimator.EstimateCost( inputTokens, outputTokens, llm.Service, llm.Model); var totalCost = inCost + outCost; var costText = totalCost > 0 ? $" 쨌 {Services.TokenEstimator.FormatCost(totalCost)}" : ""; StatusTokens.Text = $"??Services.TokenEstimator.Format(inputTokens)} ??Services.TokenEstimator.Format(outputTokens)}{costText}"; } private static string TruncateForStatus(string? text, int max = 40) { if (string.IsNullOrEmpty(text)) return ""; return text.Length <= max ? text : text[..max] + "??; } // ?€?€?€ ?ы띁 ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ private static DraftQueueItem CloneDraftQueueItem(DraftQueueItem item) => new() { Id = string.IsNullOrWhiteSpace(item.Id) ? Guid.NewGuid().ToString("N") : item.Id, Text = item.Text ?? "", Priority = NormalizeDraftPriority(item.Priority), State = NormalizeDraftState(item.State), AttemptCount = item.AttemptCount, LastError = item.LastError, CreatedAt = item.CreatedAt == default ? DateTime.Now : item.CreatedAt, }; private static string NormalizeDraftPriority(string? priority) => priority is "now" or "later" ? priority : "next"; private static string NormalizeDraftState(string? state) => state is "running" or "failed" ? state : "queued"; private static int GetDraftPriorityRank(string? priority) => NormalizeDraftPriority(priority) switch { "now" => 0, "next" => 1, "later" => 2, _ => 1, }; private List GetConversationDraftQueueItems(ChatConversation? conv) { if (conv?.DraftQueueItems?.Count > 0) return conv.DraftQueueItems.Select(CloneDraftQueueItem).ToList(); if (conv?.DraftQueue?.Count > 0) { return conv.DraftQueue .Where(text => !string.IsNullOrWhiteSpace(text)) .Select(text => new DraftQueueItem { Text = text.Trim() }) .ToList(); } return new List(); } private List GetDraftQueueState() { if (DraftQueuePanel?.Tag is List queue) return queue; ChatConversation? conv; lock (_convLock) conv = _currentConversation; queue = GetConversationDraftQueueItems(conv); if (DraftQueuePanel != null) DraftQueuePanel.Tag = queue; return queue; } private void RefreshDraftComposerUi() { var queue = GetDraftQueueState(); var prepared = InputBox?.Text?.Trim() ?? ""; if (DraftPreviewCard != null) DraftPreviewCard.Visibility = string.IsNullOrWhiteSpace(prepared) ? Visibility.Collapsed : Visibility.Visible; if (DraftPreviewText != null) DraftPreviewText.Text = string.IsNullOrWhiteSpace(prepared) ? "" : (prepared.Length > 140 ? prepared[..140] + "..." : prepared); if (DraftQueuePanel == null) return; DraftQueuePanel.Children.Clear(); DraftQueuePanel.Visibility = queue.Count > 0 ? Visibility.Visible : Visibility.Collapsed; for (var i = 0; i < queue.Count; i++) { var index = i; var queuedItem = queue[i]; var queuedText = queuedItem.Text ?? ""; var card = new Border { Background = TryFindResource("ItemBackground") as Brush ?? Brushes.White, BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(14), Padding = new Thickness(12, 8, 12, 8), Margin = new Thickness(0, 0, 0, 6), }; var row = new Grid(); row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var previewStack = new StackPanel { Orientation = Orientation.Vertical }; var chipRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 4), }; chipRow.Children.Add(new Border { Background = BrushFromHex(queuedItem.Priority switch { "now" => "#D1FAE5", "later" => "#E5E7EB", _ => "#DBEAFE", }), CornerRadius = new CornerRadius(10), Padding = new Thickness(7, 2, 7, 2), Margin = new Thickness(0, 0, 6, 0), Child = new TextBlock { Text = queuedItem.Priority switch { "now" => "吏€湲?, "later" => "?섏쨷", _ => "?ㅼ쓬", }, FontSize = 11, FontWeight = FontWeights.SemiBold, Foreground = BrushFromHex(queuedItem.Priority switch { "now" => "#047857", "later" => "#4B5563", _ => "#1D4ED8", }), }, }); chipRow.Children.Add(new Border { Background = BrushFromHex(queuedItem.State == "failed" ? "#FEE2E2" : "#F3F4F6"), CornerRadius = new CornerRadius(10), Padding = new Thickness(7, 2, 7, 2), Child = new TextBlock { Text = queuedItem.State == "failed" ? "?ㅽ뙣" : queuedItem.AttemptCount > 0 ? $"?쒕룄 {queuedItem.AttemptCount}" : "?€湲?, FontSize = 11, FontWeight = FontWeights.SemiBold, Foreground = BrushFromHex(queuedItem.State == "failed" ? "#B91C1C" : "#4B5563"), }, }); previewStack.Children.Add(chipRow); previewStack.Children.Add(new TextBlock { Text = queuedText.Length > 120 ? queuedText[..120] + "..." : queuedText, FontSize = 12.5, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, TextTrimming = TextTrimming.CharacterEllipsis, VerticalAlignment = VerticalAlignment.Center, }); if (!string.IsNullOrWhiteSpace(queuedItem.LastError)) { previewStack.Children.Add(new TextBlock { Text = queuedItem.LastError!.Length > 90 ? queuedItem.LastError[..90] + "..." : queuedItem.LastError, FontSize = 11.5, Margin = new Thickness(0, 4, 0, 0), Foreground = BrushFromHex("#B91C1C"), TextTrimming = TextTrimming.CharacterEllipsis, }); } Grid.SetColumn(previewStack, 0); row.Children.Add(previewStack); var bumpBtn = new Button { Style = TryFindResource("GhostBtn") as Style, Content = "?곗꽑", Padding = new Thickness(8, 3, 8, 3), Margin = new Thickness(8, 0, 0, 0), }; bumpBtn.Click += (_, _) => { if (index < 0 || index >= queue.Count) return; queue[index].Priority = "now"; queue[index].State = "queued"; queue[index].LastError = null; RefreshDraftComposerUi(); SaveConversationUiState(); }; Grid.SetColumn(bumpBtn, 1); row.Children.Add(bumpBtn); var runBtn = new Button { Style = TryFindResource("GhostBtn") as Style, Content = "?ㅽ뻾", Padding = new Thickness(8, 3, 8, 3), Margin = new Thickness(8, 0, 0, 0), }; runBtn.Click += async (_, _) => { if (_isStreaming || index < 0 || index >= queue.Count) return; var inputBox = InputBox; if (inputBox == null) return; var next = queue[index]; queue.RemoveAt(index); next.Priority = "now"; next.State = "queued"; next.LastError = null; next.AttemptCount++; next.State = "running"; _activeQueuedDraftItem = CloneDraftQueueItem(next); inputBox.Text = next.Text; inputBox.CaretIndex = inputBox.Text.Length; RefreshDraftComposerUi(); SaveConversationUiState(); await SendMessageAsync(); }; Grid.SetColumn(runBtn, 2); row.Children.Add(runBtn); var removeBtn = new Button { Style = TryFindResource("GhostBtn") as Style, Content = "?쒓굅", Padding = new Thickness(8, 3, 8, 3), Margin = new Thickness(6, 0, 0, 0), }; removeBtn.Click += (_, _) => { if (index < 0 || index >= queue.Count) return; queue.RemoveAt(index); RefreshDraftComposerUi(); SaveConversationUiState(); }; Grid.SetColumn(removeBtn, 3); row.Children.Add(removeBtn); card.Child = row; card.ToolTip = string.IsNullOrWhiteSpace(queuedItem.LastError) ? queuedText : $"{queuedText}{Environment.NewLine}{queuedItem.LastError}"; DraftQueuePanel.Children.Add(card); } } private async Task StartNextQueuedDraftIfAnyAsync() { if (_isStreaming || InputBox == null) return; var queue = GetDraftQueueState(); if (queue.Count == 0) return; if (!string.IsNullOrWhiteSpace(InputBox.Text)) return; var next = queue .OrderBy(item => GetDraftPriorityRank(item.Priority)) .ThenBy(item => item.CreatedAt) .FirstOrDefault(); if (next == null) return; queue.RemoveAll(item => string.Equals(item.Id, next.Id, StringComparison.OrdinalIgnoreCase)); next.AttemptCount++; next.State = "running"; next.LastError = null; _activeQueuedDraftItem = CloneDraftQueueItem(next); InputBox.Text = next.Text; InputBox.CaretIndex = InputBox.Text.Length; RefreshDraftComposerUi(); SaveConversationUiState(); await SendMessageAsync(); } private DraftQueueItem CreateDraftQueueItem(string text, string priority = "next") => new() { Id = Guid.NewGuid().ToString("N"), Text = text, Priority = NormalizeDraftPriority(priority), State = "queued", AttemptCount = 0, CreatedAt = DateTime.Now, }; private void RestoreFailedQueuedDraft(string text, Exception ex) { if (_activeQueuedDraftItem == null) return; var queue = GetDraftQueueState(); var failedItem = CloneDraftQueueItem(_activeQueuedDraftItem); failedItem.Text = text; failedItem.Priority = "now"; failedItem.State = "failed"; failedItem.AttemptCount = Math.Max(failedItem.AttemptCount, 1); failedItem.LastError = ex.Message; queue.Insert(0, failedItem); _activeQueuedDraftItem = null; RefreshDraftComposerUi(); SaveConversationUiState(); } private void BtnDraftEnqueue_Click(object sender, RoutedEventArgs e) { var inputBox = InputBox; if (inputBox == null) return; var prepared = inputBox.Text?.Trim() ?? ""; if (string.IsNullOrWhiteSpace(prepared)) return; var priority = Keyboard.Modifiers.HasFlag(ModifierKeys.Control) ? "now" : "next"; GetDraftQueueState().Add(CreateDraftQueueItem(prepared, priority)); inputBox.Clear(); RefreshDraftComposerUi(); SaveConversationUiState(); inputBox.Focus(); } private void BtnDraftEdit_Click(object sender, RoutedEventArgs e) { if (InputBox == null) return; InputBox.Focus(); InputBox.CaretIndex = InputBox.Text?.Length ?? 0; } private void BtnDraftClear_Click(object sender, RoutedEventArgs e) { if (InputBox == null) return; InputBox.Clear(); RefreshDraftComposerUi(); SaveConversationUiState(); InputBox.Focus(); } private void BtnToggleExecutionLog_Click(object sender, RoutedEventArgs e) { var hidden = Equals(BtnToggleExecutionLog.Tag, "hidden"); BtnToggleExecutionLog.Tag = hidden ? "visible" : "hidden"; if (ExecutionLogLabel != null) ExecutionLogLabel.Text = hidden ? "?ㅽ뻾 濡쒓렇" : "?ㅽ뻾 濡쒓렇 ?④?"; if (ExecutionLogIcon != null) ExecutionLogIcon.Text = hidden ? "\uE946" : "\uE8F8"; SaveConversationUiState(); RenderMessages(); } private static System.Windows.Media.SolidColorBrush BrushFromHex(string hex) { var c = (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(hex); return new System.Windows.Media.SolidColorBrush(c); } private static Border CreateMetaChip(string text, string backgroundHex, string foregroundHex) { return new Border { Background = BrushFromHex(backgroundHex), CornerRadius = new CornerRadius(10), Padding = new Thickness(7, 2, 7, 2), Margin = new Thickness(0, 0, 6, 0), Child = new TextBlock { Text = text, FontSize = 10.5, FontWeight = FontWeights.SemiBold, Foreground = BrushFromHex(foregroundHex), }, }; } }