using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using AxCopilot.Models; using AxCopilot.Services; namespace AxCopilot.Views; public partial class ChatWindow { private void UpdateInputBoxHeight() { if (InputBox == null) return; var text = InputBox.Text ?? string.Empty; var explicitLineCount = 1 + text.Count(ch => ch == '\n'); var displayMode = (_settings.Settings.Llm.AgentUiExpressionLevel ?? "balanced").Trim().ToLowerInvariant(); if (displayMode is not ("rich" or "balanced" or "simple")) displayMode = "balanced"; var maxLines = displayMode switch { "rich" => 6, "simple" => 4, _ => 5, }; const double baseHeight = 42; const double lineStep = 22; var visibleLines = Math.Clamp(explicitLineCount, 1, maxLines); var targetHeight = baseHeight + ((visibleLines - 1) * lineStep); InputBox.MinLines = 1; InputBox.MaxLines = maxLines; InputBox.Height = targetHeight; InputBox.VerticalScrollBarVisibility = explicitLineCount > maxLines ? ScrollBarVisibility.Auto : ScrollBarVisibility.Disabled; } private string BuildComposerDraftText() { var rawText = InputBox?.Text?.Trim() ?? ""; return _slashPalette.ActiveCommand != null ? (_slashPalette.ActiveCommand + " " + rawText).Trim() : rawText; } private static string InferDraftKind(string text, string? explicitKind = null) { var trimmed = text?.Trim() ?? ""; var requestedKind = explicitKind?.Trim().ToLowerInvariant(); if (requestedKind is "followup" or "steering") return requestedKind; if (trimmed.StartsWith("/", StringComparison.OrdinalIgnoreCase)) return "command"; if (requestedKind is "direct" or "message") return requestedKind; if (trimmed.StartsWith("steer:", StringComparison.OrdinalIgnoreCase) || trimmed.StartsWith("@steer ", StringComparison.OrdinalIgnoreCase) || trimmed.StartsWith("조정:", StringComparison.OrdinalIgnoreCase)) return "steering"; return "message"; } private void QueueComposerDraft(string priority = "next", string? explicitKind = null, bool startImmediatelyWhenIdle = true) { if (InputBox == null) return; var text = BuildComposerDraftText(); if (string.IsNullOrWhiteSpace(text)) return; if (_isStreaming && string.Equals(priority, "now", StringComparison.OrdinalIgnoreCase)) priority = "next"; HideSlashChip(restoreText: false); ClearPromptCardPlaceholder(); var queuedItem = EnqueueDraftRequest(text, priority, explicitKind); InputBox.Clear(); InputBox.Focus(); UpdateInputBoxHeight(); RefreshDraftQueueUi(); if (queuedItem == null) return; if (!_isStreaming && startImmediatelyWhenIdle) { StartNextQueuedDraftIfAny(queuedItem.Id); return; } var toast = queuedItem.Kind switch { "command" => "명령이 대기열에 추가되었습니다.", "direct" => "직접 실행 요청이 대기열에 추가되었습니다.", "steering" => "조정 요청이 대기열에 추가되었습니다.", "followup" => "후속 작업이 대기열에 추가되었습니다.", _ => "메시지가 대기열에 추가되었습니다.", }; ShowToast(toast); } private void RefreshDraftQueueUi() { if (DraftPreviewCard == null || DraftPreviewText == null || DraftQueuePanel == null || BtnDraftEnqueue == null) return; lock (_convLock) { var session = ChatSession; if (session != null) _draftQueueProcessor.PromoteReadyBlockedItems(session, _activeTab, _storage); } var items = _appState.GetDraftQueueItems(_activeTab); DraftPreviewCard.Visibility = Visibility.Collapsed; BtnDraftEnqueue.IsEnabled = false; DraftPreviewText.Text = string.Empty; RebuildDraftQueuePanel(items); } private bool IsDraftQueueExpanded() => _expandedDraftQueueTabs.Contains(_activeTab); private void ToggleDraftQueueExpanded() { if (!_expandedDraftQueueTabs.Add(_activeTab)) _expandedDraftQueueTabs.Remove(_activeTab); RefreshDraftQueueUi(); } private void RebuildDraftQueuePanel(IReadOnlyList items) { if (DraftQueuePanel == null) return; DraftQueuePanel.Children.Clear(); var visibleItems = items .OrderBy(GetDraftStateRank) .ThenBy(GetDraftPriorityRank) .ThenBy(x => x.CreatedAt) .ToList(); if (visibleItems.Count == 0) { DraftQueuePanel.Visibility = Visibility.Collapsed; return; } var summary = _appState.GetDraftQueueSummary(_activeTab); var shouldShowQueue = IsDraftQueueExpanded() || summary.RunningCount > 0 || summary.QueuedCount > 0 || summary.FailedCount > 0; if (!shouldShowQueue) { DraftQueuePanel.Visibility = Visibility.Collapsed; return; } DraftQueuePanel.Visibility = Visibility.Visible; DraftQueuePanel.Children.Add(CreateDraftQueueSummaryStrip(summary, IsDraftQueueExpanded())); if (!IsDraftQueueExpanded()) { DraftQueuePanel.Children.Add(CreateCompactDraftQueuePanel(visibleItems, summary)); return; } const int maxPerSection = 3; var runningItems = visibleItems .Where(item => string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase)) .Take(maxPerSection) .ToList(); var queuedItems = visibleItems .Where(item => string.Equals(item.State, "queued", StringComparison.OrdinalIgnoreCase) && !IsDraftBlocked(item)) .Take(maxPerSection) .ToList(); var blockedItems = visibleItems .Where(IsDraftBlocked) .Take(maxPerSection) .ToList(); var completedItems = visibleItems .Where(item => string.Equals(item.State, "completed", StringComparison.OrdinalIgnoreCase)) .Take(maxPerSection) .ToList(); var failedItems = visibleItems .Where(item => string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase)) .Take(maxPerSection) .ToList(); AddDraftQueueSection("실행 중", runningItems, summary.RunningCount); AddDraftQueueSection("다음 작업", queuedItems, summary.QueuedCount); AddDraftQueueSection("보류", blockedItems, summary.BlockedCount); AddDraftQueueSection("완료", completedItems, summary.CompletedCount); AddDraftQueueSection("실패", failedItems, summary.FailedCount); if (summary.CompletedCount > 0 || summary.FailedCount > 0) { var footer = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 0), }; if (summary.CompletedCount > 0) footer.Children.Add(CreateDraftQueueActionButton($"완료 정리 {summary.CompletedCount}", ClearCompletedDrafts, BrushFromHex("#ECFDF5"))); if (summary.FailedCount > 0) footer.Children.Add(CreateDraftQueueActionButton($"실패 정리 {summary.FailedCount}", ClearFailedDrafts, BrushFromHex("#FEF2F2"))); DraftQueuePanel.Children.Add(footer); } } private UIElement CreateCompactDraftQueuePanel(IReadOnlyList items, AppStateService.DraftQueueSummaryState summary) { var primaryText = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#111827"); var secondaryText = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#6B7280"); var background = TryFindResource("ItemBackground") as Brush ?? BrushFromHex("#F7F7F8"); var borderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E4E4E7"); var focusItem = items.FirstOrDefault(item => string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase)) ?? items.FirstOrDefault(item => string.Equals(item.State, "queued", StringComparison.OrdinalIgnoreCase) && !IsDraftBlocked(item)) ?? items.FirstOrDefault(IsDraftBlocked) ?? items.FirstOrDefault(item => string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase)) ?? items.FirstOrDefault(item => string.Equals(item.State, "completed", StringComparison.OrdinalIgnoreCase)); var container = new Border { Background = background, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(14), Padding = new Thickness(12, 10, 12, 10), Margin = new Thickness(0, 0, 0, 4), }; var root = new Grid(); root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); root.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); container.Child = root; var left = new StackPanel(); left.Children.Add(new TextBlock { Text = focusItem == null ? "대기열 항목이 준비되면 여기에서 요약됩니다." : $"{GetDraftStateLabel(focusItem)} · {GetDraftKindLabel(focusItem)}", FontSize = 11, FontWeight = FontWeights.SemiBold, Foreground = primaryText, }); left.Children.Add(new TextBlock { Text = focusItem?.Text ?? BuildDraftQueueCompactSummaryText(summary), FontSize = 10.5, Foreground = secondaryText, TextTrimming = TextTrimming.CharacterEllipsis, Margin = new Thickness(0, 4, 0, 0), MaxWidth = 520, }); Grid.SetColumn(left, 0); root.Children.Add(left); var action = CreateDraftQueueActionButton("상세", ToggleDraftQueueExpanded); action.Margin = new Thickness(12, 0, 0, 0); Grid.SetColumn(action, 1); root.Children.Add(action); return container; } private static string BuildDraftQueueCompactSummaryText(AppStateService.DraftQueueSummaryState summary) { var parts = new List(); if (summary.RunningCount > 0) parts.Add($"실행 {summary.RunningCount}"); if (summary.QueuedCount > 0) parts.Add($"다음 {summary.QueuedCount}"); if (summary.FailedCount > 0) parts.Add($"실패 {summary.FailedCount}"); return parts.Count == 0 ? "대기열 0" : string.Join(" · ", parts); } private void AddDraftQueueSection(string label, IReadOnlyList items, int totalCount) { if (DraftQueuePanel == null || totalCount <= 0) return; DraftQueuePanel.Children.Add(CreateDraftQueueSectionLabel($"{label} · {totalCount}")); foreach (var item in items) DraftQueuePanel.Children.Add(CreateDraftQueueCard(item)); if (totalCount > items.Count) { DraftQueuePanel.Children.Add(new TextBlock { Text = $"추가 항목 {totalCount - items.Count}개", Margin = new Thickness(8, -2, 0, 8), FontSize = 10.5, Foreground = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#7A7F87"), }); } } private UIElement CreateDraftQueueSummaryStrip(AppStateService.DraftQueueSummaryState summary, bool isExpanded) { var root = new Grid { Margin = new Thickness(0, 0, 0, 8), }; root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); root.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var wrap = new WrapPanel(); if (summary.RunningCount > 0) wrap.Children.Add(CreateQueueSummaryPill("실행 중", summary.RunningCount.ToString(), "#EFF6FF", "#BFDBFE", "#1D4ED8")); if (summary.QueuedCount > 0) wrap.Children.Add(CreateQueueSummaryPill("다음", summary.QueuedCount.ToString(), "#F5F3FF", "#DDD6FE", "#6D28D9")); if (isExpanded && summary.BlockedCount > 0) wrap.Children.Add(CreateQueueSummaryPill("보류", summary.BlockedCount.ToString(), "#FFF7ED", "#FDBA74", "#C2410C")); if (isExpanded && summary.CompletedCount > 0) wrap.Children.Add(CreateQueueSummaryPill("완료", summary.CompletedCount.ToString(), "#ECFDF5", "#BBF7D0", "#166534")); if (summary.FailedCount > 0) wrap.Children.Add(CreateQueueSummaryPill("실패", summary.FailedCount.ToString(), "#FEF2F2", "#FECACA", "#991B1B")); if (wrap.Children.Count == 0) wrap.Children.Add(CreateQueueSummaryPill("대기열", "0", "#F8FAFC", "#E2E8F0", "#475569")); Grid.SetColumn(wrap, 0); root.Children.Add(wrap); var toggle = CreateDraftQueueActionButton(isExpanded ? "간단히" : "상세 보기", ToggleDraftQueueExpanded); toggle.Margin = new Thickness(10, 0, 0, 0); Grid.SetColumn(toggle, 1); root.Children.Add(toggle); return root; } private Border CreateQueueSummaryPill(string label, string value, string bgHex, string borderHex, string fgHex) { return new Border { Background = BrushFromHex(bgHex), BorderBrush = BrushFromHex(borderHex), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(999), Padding = new Thickness(8, 3, 8, 3), Margin = new Thickness(0, 0, 6, 0), Child = new StackPanel { Orientation = Orientation.Horizontal, Children = { new TextBlock { Text = label, FontSize = 10, Foreground = BrushFromHex(fgHex), }, new TextBlock { Text = $" {value}", FontSize = 10, FontWeight = FontWeights.SemiBold, Foreground = BrushFromHex(fgHex), } } } }; } private TextBlock CreateDraftQueueSectionLabel(string text) { return new TextBlock { Text = text, FontSize = 10.5, FontWeight = FontWeights.SemiBold, Margin = new Thickness(8, 0, 8, 6), Foreground = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#64748B"), }; } private Border CreateDraftQueueCard(DraftQueueItem item) { var background = TryFindResource("ItemBackground") as Brush ?? BrushFromHex("#F7F7F8"); var borderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E4E4E7"); var primaryText = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#111827"); var secondaryText = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#6B7280"); var neutralSurface = BrushFromHex("#F5F6F8"); var (kindIcon, kindForeground) = GetDraftKindVisual(item); var (stateBackground, stateBorder, stateForeground) = GetDraftStateBadgeColors(item); var (priorityBackground, priorityBorder, priorityForeground) = GetDraftPriorityBadgeColors(item.Priority); var container = new Border { Background = background, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(14), Padding = new Thickness(12, 10, 12, 10), Margin = new Thickness(0, 0, 0, 8), }; var root = new Grid(); root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); root.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); container.Child = root; var left = new StackPanel(); Grid.SetColumn(left, 0); root.Children.Add(left); var header = new StackPanel { Orientation = Orientation.Horizontal, }; header.Children.Add(new TextBlock { Text = kindIcon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 11, Foreground = kindForeground, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), }); header.Children.Add(CreateDraftQueueBadge(GetDraftKindLabel(item), BrushFromHex("#F8FAFC"), borderBrush, kindForeground)); header.Children.Add(CreateDraftQueueBadge(GetDraftStateLabel(item), stateBackground, stateBorder, stateForeground)); header.Children.Add(CreateDraftQueueBadge(GetDraftPriorityLabel(item.Priority), priorityBackground, priorityBorder, priorityForeground)); left.Children.Add(header); left.Children.Add(new TextBlock { Text = item.Text, FontSize = 12.5, Foreground = primaryText, Margin = new Thickness(0, 6, 0, 0), TextWrapping = TextWrapping.Wrap, TextTrimming = TextTrimming.CharacterEllipsis, MaxWidth = 520, }); var meta = $"{item.CreatedAt:HH:mm}"; if (item.AttemptCount > 0) meta += $" · 시도 {item.AttemptCount}"; if (item.NextRetryAt.HasValue && item.NextRetryAt.Value > DateTime.Now) meta += $" · 재시도 {item.NextRetryAt.Value:HH:mm:ss}"; if (!string.IsNullOrWhiteSpace(item.LastError)) meta += $" · {TruncateForStatus(item.LastError, 36)}"; left.Children.Add(new TextBlock { Text = meta, FontSize = 10.5, Foreground = secondaryText, Margin = new Thickness(0, 6, 0, 0), }); var actions = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Top, }; Grid.SetColumn(actions, 1); root.Children.Add(actions); if (!string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase)) actions.Children.Add(CreateDraftQueueActionButton("실행", () => QueueDraftForImmediateRun(item.Id))); if (string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase) || string.Equals(item.State, "completed", StringComparison.OrdinalIgnoreCase)) { actions.Children.Add(CreateDraftQueueActionButton("대기", () => ResetDraftInQueue(item.Id), neutralSurface)); } actions.Children.Add(CreateDraftQueueActionButton("삭제", () => RemoveDraftFromQueue(item.Id), neutralSurface)); return container; } private Border CreateDraftQueueBadge(string text, Brush background, Brush borderBrush, Brush foreground) { return new Border { Background = background, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(999), Padding = new Thickness(7, 2, 7, 2), Margin = new Thickness(0, 0, 6, 0), Child = new TextBlock { Text = text, FontSize = 10, FontWeight = FontWeights.SemiBold, Foreground = foreground, } }; } private Button CreateDraftQueueActionButton(string label, Action onClick, Brush? background = null) { var btn = new Button { Content = label, Margin = new Thickness(6, 0, 0, 0), Padding = new Thickness(10, 5, 10, 5), MinWidth = 48, FontSize = 11, Background = background ?? BrushFromHex("#EEF2FF"), BorderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#D4D4D8"), BorderThickness = new Thickness(1), Foreground = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#111827"), Cursor = Cursors.Hand, }; btn.Click += (_, _) => onClick(); return btn; } private static int GetDraftStateRank(DraftQueueItem item) => string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase) ? 0 : IsDraftBlocked(item) ? 1 : string.Equals(item.State, "queued", StringComparison.OrdinalIgnoreCase) ? 2 : string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase) ? 3 : 4; private static int GetDraftPriorityRank(DraftQueueItem item) => item.Priority?.ToLowerInvariant() switch { "now" => 0, "next" => 1, _ => 2, }; private static string GetDraftPriorityLabel(string? priority) => priority?.ToLowerInvariant() switch { "now" => "지금", "later" => "나중", _ => "다음", }; private static string GetDraftKindLabel(DraftQueueItem item) => item.Kind?.ToLowerInvariant() switch { "followup" => "후속 작업", "steering" => "조정", "command" => "명령", "direct" => "직접 실행", _ => "메시지", }; private (string Icon, Brush Foreground) GetDraftKindVisual(DraftQueueItem item) => item.Kind?.ToLowerInvariant() switch { "followup" => ("\uE8A5", BrushFromHex("#0F766E")), "steering" => ("\uE7C3", BrushFromHex("#B45309")), "command" => ("\uE756", BrushFromHex("#7C3AED")), "direct" => ("\uE8A7", BrushFromHex("#2563EB")), _ => ("\uE8BD", BrushFromHex("#475569")), }; private static string GetDraftStateLabel(DraftQueueItem item) => IsDraftBlocked(item) ? "재시도 대기" : item.State?.ToLowerInvariant() switch { "running" => "실행 중", "failed" => "실패", "completed" => "완료", _ => "대기", }; private Brush GetDraftStateBrush(DraftQueueItem item) => IsDraftBlocked(item) ? BrushFromHex("#B45309") : item.State?.ToLowerInvariant() switch { "running" => BrushFromHex("#2563EB"), "failed" => BrushFromHex("#DC2626"), "completed" => BrushFromHex("#059669"), _ => BrushFromHex("#7C3AED"), }; private (Brush Background, Brush Border, Brush Foreground) GetDraftStateBadgeColors(DraftQueueItem item) => IsDraftBlocked(item) ? (BrushFromHex("#FFF7ED"), BrushFromHex("#FDBA74"), BrushFromHex("#C2410C")) : item.State?.ToLowerInvariant() switch { "running" => (BrushFromHex("#EFF6FF"), BrushFromHex("#BFDBFE"), BrushFromHex("#1D4ED8")), "failed" => (BrushFromHex("#FEF2F2"), BrushFromHex("#FECACA"), BrushFromHex("#991B1B")), "completed" => (BrushFromHex("#ECFDF5"), BrushFromHex("#BBF7D0"), BrushFromHex("#166534")), _ => (BrushFromHex("#F5F3FF"), BrushFromHex("#DDD6FE"), BrushFromHex("#6D28D9")), }; private static (Brush Background, Brush Border, Brush Foreground) GetDraftPriorityBadgeColors(string? priority) => priority?.ToLowerInvariant() switch { "now" => (BrushFromHex("#EEF2FF"), BrushFromHex("#C7D2FE"), BrushFromHex("#3730A3")), "later" => (BrushFromHex("#F8FAFC"), BrushFromHex("#E2E8F0"), BrushFromHex("#475569")), _ => (BrushFromHex("#FEF3C7"), BrushFromHex("#FDE68A"), BrushFromHex("#92400E")), }; private static bool IsDraftBlocked(DraftQueueItem item) => string.Equals(item.State, "queued", StringComparison.OrdinalIgnoreCase) && item.NextRetryAt.HasValue && item.NextRetryAt.Value > DateTime.Now; }