From f18f48789a4d68e889cd7dde18741a27416713b0 Mon Sep 17 00:00:00 2001 From: lacvet Date: Sun, 5 Apr 2026 12:45:01 +0900 Subject: [PATCH] =?UTF-8?q?AX=20Agent=20=EC=8B=A4=ED=96=89=20=EB=B3=B4?= =?UTF-8?q?=EC=A1=B0=20UI=EB=A5=BC=20=EC=B6=95=EC=95=BD=ED=98=95=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=95=88=EC=A0=95=ED=99=94=ED=95=98=EA=B3=A0=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=B6=95=20=ED=9D=94=EB=93=A4?= =?UTF-8?q?=EB=A6=BC=EC=9D=84=20=EC=A4=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ChatWindow의 suggest_actions 결과를 본문 MessagePanel 직접 삽입 대신 토스트 요약으로 전환 - DraftQueuePanel을 기본 축약형으로 바꾸고 상세 보기 토글을 추가해 컴포저 위 레이아웃 변동을 줄임 - README와 DEVELOPMENT 문서에 2026-04-05 13:12 (KST) 기준 이력과 검증 결과를 반영 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0, 오류 0) --- README.md | 3 + docs/DEVELOPMENT.md | 4 + src/AxCopilot/Views/ChatWindow.xaml.cs | 188 ++++++++++++++----------- 3 files changed, 112 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 412041b..619d50c 100644 --- a/README.md +++ b/README.md @@ -745,6 +745,8 @@ ow + toggle 시각 언어로 통일했습니다. - 이번엔 `OnAgentEvent(...)`의 본문 재렌더를 배치형으로 바꿨습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 `DispatcherTimer` 기반 `ScheduleExecutionHistoryRender()`를 추가해서, Cowork/Code 실행 중 이벤트가 연속으로 들어와도 `RenderMessages()`가 매 이벤트마다 바로 돌지 않고 짧게 묶여 한 번씩만 반영됩니다. - 같은 흐름으로 작업 요약 스트립도 배치형 갱신으로 바꿨습니다. `UpdateTaskSummaryIndicators()`를 즉시 호출하는 대신 `ScheduleTaskSummaryRefresh()`가 120ms 단위로 상태 반영을 묶어, 실행 중 상단 상태 스트립과 런타임 배지가 과하게 흔들리지 않도록 정리했습니다. - 추가로 실행 이벤트/실행 기록 저장도 지연 저장으로 바꿨습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AppendConversationExecutionEvent()`와 `AppendConversationAgentRun()`은 이제 이벤트마다 바로 `_storage.Save(...)`를 호출하지 않고, `ScheduleConversationPersist()`를 통해 220ms 단위로 묶어서 flush 합니다. Cowork/Code의 연속 이벤트 구간에서 저장 I/O가 덜 붙도록 만든 조정입니다. +- 이번엔 실행 완료 뒤 메시지 축을 흔들던 보조 UI를 더 줄였습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `RenderSuggestActionChips()`는 더 이상 본문 `MessagePanel`에 제안 칩을 직접 삽입하지 않고, 요약 토스트만 띄우도록 바꿨습니다. 이 변경으로 Cowork/Code 작업 중간에 제안 칩이 본문 폭과 스크롤 위치를 흔들던 경로를 끊었습니다. +- 같은 파일의 대기열 UI도 기본 축약형으로 바꿨습니다. `DraftQueuePanel`은 이제 기본적으로 요약 pill + 핵심 항목 1개만 보이고, 필요할 때만 `상세 보기`로 전체 섹션 카드(`실행 중/다음 작업/보류/완료/실패`)를 펼칩니다. 대기열 카드가 매번 크게 다시 그려지면서 컴포저 위 레이아웃을 밀던 현상을 줄이기 위한 정리입니다. - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0 - 업데이트: 2026-04-05 12:24 (KST) - 업데이트: 2026-04-05 12:31 (KST) @@ -754,6 +756,7 @@ ow + toggle 시각 언어로 통일했습니다. - 업데이트: 2026-04-05 12:53 (KST) - 업데이트: 2026-04-05 12:58 (KST) - 업데이트: 2026-04-05 13:03 (KST) +- 업데이트: 2026-04-05 13:12 (KST) --- diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index b25258f..0bb5c62 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4511,3 +4511,7 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 `_conversationPersistTimer`, `ScheduleConversationPersist()`, `FlushPendingConversationPersists()`를 추가해, 실행 이벤트와 실행 기록이 들어올 때마다 곧바로 `_storage.Save(...)`를 치지 않도록 바꿨습니다. - `AppendConversationExecutionEvent()`와 `AppendConversationAgentRun()`는 이제 `ChatSessionStateService`의 append 메서드를 `storage=null`로 호출하고, 변경된 `ChatConversation`만 220ms 단위로 지연 저장합니다. 이 변경은 Cowork/Code 실행 중 빈번한 이벤트가 들어올 때 디스크 I/O 때문에 체감이 끊기는 문제를 줄이기 위한 배치 저장 단계입니다. - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0 +- 업데이트: 2026-04-05 13:12 (KST) +- 실행 완료 뒤 메시지 컬럼을 크게 흔들던 보조 UI를 더 줄였습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `RenderSuggestActionChips()`는 더 이상 본문 `MessagePanel`에 직접 제안 칩 컨테이너를 추가하지 않고, 제안 라벨 요약만 토스트로 표시합니다. Cowork/Code에서 `suggest_actions` 결과가 들어올 때 본문 레이아웃과 스크롤이 흔들리던 경로를 먼저 끊는 안정화 조치입니다. +- 같은 파일의 `DraftQueuePanel`도 기본 축약 표시로 바꿨습니다. 대기열은 처음부터 모든 섹션 카드를 다 렌더하지 않고, `CreateDraftQueueSummaryStrip(...)`의 요약 pill과 `CreateCompactDraftQueuePanel(...)`의 핵심 항목 한 장만 먼저 보여 줍니다. 사용자가 `상세 보기`를 누를 때만 `실행 중 / 다음 작업 / 보류 / 완료 / 실패` 섹션이 펼쳐집니다. +- 이번 조정으로 AX Agent 채팅 엔진 공통화 작업 중에도 보조 카드가 컴포저 위 레이아웃을 크게 밀어 올리거나, 실행 중간에 메시지 축을 흔드는 체감을 줄이는 방향으로 정리했습니다. 다음 단계는 이 축약형 UI 위에서 Cowork/Code 완료 카드와 큐 완료 후처리를 더 엔진 중심으로 맞추는 것입니다. diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index a6324b2..9bab645 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -121,6 +121,7 @@ public partial class ChatWindow : Window private int _sessionPostCompactionCompletionTokens; private bool _pendingExecutionHistoryAutoScroll; private readonly Dictionary _pendingConversationPersists = new(StringComparer.OrdinalIgnoreCase); + private readonly HashSet _expandedDraftQueueTabs = new(StringComparer.OrdinalIgnoreCase); private void ApplyQuickActionVisual(Button button, bool active, string activeBg, string activeFg) { if (button?.Content is not string text) @@ -10031,11 +10032,9 @@ public partial class ChatWindow : Window /// 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); @@ -10067,83 +10066,9 @@ public partial class ChatWindow : Window 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(); + var preview = string.Join(", ", actions.Take(3).Select(static action => action.label)); + var suffix = actions.Count > 3 ? $" 외 {actions.Count - 3}개" : ""; + ShowToast($"다음 작업 제안 준비됨: {preview}{suffix}"); } // ════════════════════════════════════════════════════════════ @@ -19652,6 +19577,17 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi 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) @@ -19673,7 +19609,13 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi DraftQueuePanel.Visibility = Visibility.Visible; var summary = _appState.GetDraftQueueSummary(_activeTab); - DraftQueuePanel.Children.Add(CreateDraftQueueSummaryStrip(summary)); + 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)) @@ -19720,6 +19662,74 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi } } + 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.BlockedCount > 0) parts.Add($"보류 {summary.BlockedCount}"); + if (summary.CompletedCount > 0) parts.Add($"완료 {summary.CompletedCount}"); + 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) @@ -19741,12 +19751,16 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi } } - private UIElement CreateDraftQueueSummaryStrip(AppStateService.DraftQueueSummaryState summary) + private UIElement CreateDraftQueueSummaryStrip(AppStateService.DraftQueueSummaryState summary, bool isExpanded) { - var wrap = new WrapPanel + 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")); @@ -19762,7 +19776,15 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi if (wrap.Children.Count == 0) wrap.Children.Add(CreateQueueSummaryPill("대기열", "0", "#F8FAFC", "#E2E8F0", "#475569")); - return wrap; + 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)