AX Agent 실행 보조 UI를 축약형으로 안정화하고 메시지 축 흔들림을 줄임
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
- 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)
This commit is contained in:
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 완료 카드와 큐 완료 후처리를 더 엔진 중심으로 맞추는 것입니다.
|
||||
|
||||
@@ -121,6 +121,7 @@ public partial class ChatWindow : Window
|
||||
private int _sessionPostCompactionCompletionTokens;
|
||||
private bool _pendingExecutionHistoryAutoScroll;
|
||||
private readonly Dictionary<string, ChatConversation> _pendingConversationPersists = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _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
|
||||
/// <summary>suggest_actions 도구 결과를 클릭 가능한 칩으로 렌더링합니다.</summary>
|
||||
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<DraftQueueItem> 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<DraftQueueItem> 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<string>();
|
||||
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<DraftQueueItem> 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)
|
||||
|
||||
Reference in New Issue
Block a user