From 68524c1c942db0329d132be29b0c5cbbf2640783 Mon Sep 17 00:00:00 2001 From: lacvet Date: Mon, 6 Apr 2026 08:24:25 +0900 Subject: [PATCH] =?UTF-8?q?AX=20Agent=20composer=C2=B7=EB=8C=80=EA=B8=B0?= =?UTF-8?q?=EC=97=B4=20=EB=A0=8C=EB=8D=94=20=EA=B5=AC=EC=A1=B0=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EB=AC=B8=EC=84=9C=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ChatWindow.ComposerQueuePresentation.cs를 추가해 입력창 높이 계산, draft kind 해석, 대기열 요약/카드/배지/액션 버튼 생성 책임을 메인 창 코드에서 분리함 - ChatWindow.xaml.cs는 대기열 실행 orchestration과 세션 변경 흐름 중심으로 정리해 claw-code 기준 footer/composer 품질 개선 기반을 강화함 - README.md와 docs/DEVELOPMENT.md에 2026-04-06 08:28 (KST) 기준 변경 이력을 반영함 - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 기준 경고 0 / 오류 0 확인 --- README.md | 4 + docs/DEVELOPMENT.md | 2 + .../ChatWindow.ComposerQueuePresentation.cs | 624 ++++++++++++++++++ src/AxCopilot/Views/ChatWindow.xaml.cs | 611 ----------------- 4 files changed, 630 insertions(+), 611 deletions(-) create mode 100644 src/AxCopilot/Views/ChatWindow.ComposerQueuePresentation.cs diff --git a/README.md b/README.md index fbe86d2..5fc5935 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저 개발 참고: Claw Code 동등성 작업 추적 문서 `docs/claw-code-parity-plan.md` +- 업데이트: 2026-04-06 08:28 (KST) +- AX Agent 하단 composer와 대기열 UI 렌더를 `ChatWindow.ComposerQueuePresentation.cs`로 분리했습니다. 입력창 높이 계산, draft kind 해석, 후속 요청 큐 카드/요약 pill/배지/액션 버튼 생성 책임을 메인 창 코드에서 떼어냈습니다. +- `ChatWindow.xaml.cs`는 대기열 실행 orchestration과 세션 변경 흐름만 더 선명하게 남겨, claw-code 기준 입력부/queued command UX 개선을 계속하기 쉬운 구조로 정리했습니다. + - 업데이트: 2026-04-06 08:12 (KST) - AX Agent 하단 작업 바 관련 presentation 메서드를 메인 창 코드에서 더 분리했습니다. `ChatWindow.FooterPresentation.cs`를 추가해 폴더 바, 선택된 프리셋 안내, Git 브랜치 버튼/팝업 렌더, 요약 pill 생성 책임을 별도 partial로 옮겼습니다. - `ChatWindow.xaml.cs`는 대화 흐름과 런타임 orchestration 중심으로 더 정리했고, claw-code 기준으로 footer/preset/Git popup 품질 작업을 계속 이어가기 쉬운 구조를 만들었습니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 2b218d3..be5a402 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4893,3 +4893,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - 이벤트 배너 renderer를 [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs) 로 분리했다. 기존 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 있던 `CreateCompactEventPill`, `AddAgentEventBanner`, `GetDecisionBadgeMeta`를 별도 partial로 옮겨, 이후 `permission/tool-result/plan` 타입별 renderer 확장을 더 쉽게 할 수 있는 구조를 마련했다. - Document update: 2026-04-06 08:12 (KST) - Split footer/preset/Git popup presentation logic out of `ChatWindow.xaml.cs` into `ChatWindow.FooterPresentation.cs`. The folder bar refresh, selected preset guide, Git branch surface update, Git popup assembly, and popup summary-pill/row helpers now live in a dedicated partial instead of the main window orchestration file. - Document update: 2026-04-06 08:12 (KST) - This pass keeps the AX Agent transcript/runtime flow closer to the `claw-code` separation model by reducing UI assembly inside the main chat window file and isolating footer/prompt-adjacent presentation code for future parity work. +- Document update: 2026-04-06 08:28 (KST) - Split composer/draft queue presentation logic out of `ChatWindow.xaml.cs` into `ChatWindow.ComposerQueuePresentation.cs`. Input-box height calculation, draft-kind inference, queue summary pills, compact queue cards, expanded queue sections, queue badges, and queue action buttons now live in a dedicated partial. +- Document update: 2026-04-06 08:28 (KST) - This keeps `ChatWindow.xaml.cs` more orchestration-focused while preserving the same runtime behavior, and it aligns AX Agent more closely with the `claw-code` model of separating prompt/footer presentation from session execution logic. diff --git a/src/AxCopilot/Views/ChatWindow.ComposerQueuePresentation.cs b/src/AxCopilot/Views/ChatWindow.ComposerQueuePresentation.cs new file mode 100644 index 0000000..1ba128c --- /dev/null +++ b/src/AxCopilot/Views/ChatWindow.ComposerQueuePresentation.cs @@ -0,0 +1,624 @@ +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; +} diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index ff063d8..6db6bee 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -4989,36 +4989,6 @@ public partial class ChatWindow : Window ScheduleInputUiRefresh(); } - 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 static void SyncLatestAssistantMessage(ChatConversation conv, string content) { if (conv.Messages.Count == 0) @@ -17778,78 +17748,6 @@ public partial class ChatWindow : Window return text.Length <= max ? text : text[..max] + "…"; } - 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 DraftQueueItem? EnqueueDraftRequest(string text, string priority, string? explicitKind = null) { DraftQueueItem? queuedItem = null; @@ -17907,428 +17805,6 @@ public partial class ChatWindow : Window RefreshDraftQueueUi(); } - 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 summary = _appState.GetDraftQueueSummary(_activeTab); - 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 void QueueDraftForImmediateRun(string draftId) { if (_isStreaming) @@ -18413,93 +17889,6 @@ public partial class ChatWindow : Window _ = SendMessageAsync(next.Text); } - 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; - private void OpenCommandSkillBrowser(string seedInput) { if (InputBox == null)