From 4cbe60052ec30d567918a607742902e0ad311cda Mon Sep 17 00:00:00 2001 From: lacvet Date: Sun, 5 Apr 2026 18:59:32 +0900 Subject: [PATCH] =?UTF-8?q?AX=20Agent=20=EC=A7=88=EB=AC=B8/=EC=9D=98?= =?UTF-8?q?=EA=B2=AC=20=EC=9A=94=EC=B2=AD=20=ED=9D=90=EB=A6=84=EC=9D=84=20?= =?UTF-8?q?transcript=20=EC=9A=B0=EC=84=A0=EC=9C=BC=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - user_ask 콜백을 별도 팝업 대신 본문 inline 카드 경로로 변경 - 선택지 pill, 직접 입력, 전달/취소 버튼을 timeline 안에서 처리 - 계획 승인과 질문 요청이 같은 transcript-first UX 원칙을 따르도록 정리 - claw-code parity 문서와 개발 이력 문서에 질문/승인 UX 기준을 반영 검증 결과 - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ - 경고 0 / 오류 0 --- README.md | 5 + docs/DEVELOPMENT.md | 3 + docs/claw-code-parity-plan.md | 8 + src/AxCopilot/Views/ChatWindow.xaml.cs | 256 ++++++++++++++++++++++++- 4 files changed, 262 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 036bdf5..1de5e32 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,11 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저 개발 참고: Claw Code 동등성 작업 추적 문서 `docs/claw-code-parity-plan.md` +- 업데이트: 2026-04-05 18:58 (KST) +- AX Agent의 `의견 요청`/`질문` 흐름을 transcript 내 카드 우선 구조로 전환했습니다. +- `user_ask` 도구는 별도 `UserAskDialog`를 먼저 띄우지 않고, 본문 안에서 선택지/직접 입력/전달로 응답을 완료합니다. +- 계획 승인과 사용자 질문이 같은 transcript-first UX 원칙을 따르도록 정리했습니다. + - 업데이트: 2026-04-05 22:04 (KST) - `claw-code`와 AX Agent를 같은 기준으로 비교할 수 있도록 canonical prompt set 10종을 parity 문서에 고정했습니다. Chat 기본/장문, Cowork 문서/데이터, Code 수정/빌드, queue follow-up, post-compaction, permission 승인, slash skill 진입까지 핵심 회귀 흐름을 한 세트로 검증하도록 정리했습니다. - 도구/스킬 비교 기준도 parity 문서에 추가했습니다. AX는 문서/오피스/데이터/업무형 도구가 더 풍부하고, `claw-code`는 transcript-native tool/approval/permission 메시지 구조가 더 정교하다는 차이를 명시했습니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index d74eb47..8016b46 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,5 +1,8 @@ # AX Copilot - 媛쒕컻 臾몄꽌 +- Document update: 2026-04-05 18:58 (KST) - Switched `UserAskCallback` in `ChatWindow.xaml.cs` to a transcript-first inline card flow. `user_ask` no longer defaults to `UserAskDialog`; it renders an in-stream question card with choice pills, direct text input, and submit/cancel actions, then returns only the chosen answer to the engine. +- Document update: 2026-04-05 18:58 (KST) - Aligned user-question UX with the same transcript-first principle already used for plan approval. `PlanViewerWindow` remains a secondary detail surface, while the primary approval/question decision now happens inside the AX Agent message timeline. + - Document update: 2026-04-05 16:55 (KST) - Recorded the current `claw-code` parity estimate for AX Agent: core execution engine `82%`, main chat UI `68%`, Cowork/Code status UX `63%`, internal settings linkage `88%`, overall AX Agent parity `74%`. - Document update: 2026-04-05 16:55 (KST) - Added an engine-settings review rule for ongoing cleanup: settings that materially alter the main execution route should be minimized, kept developer-only when necessary, or removed from user-facing surfaces when they no longer represent real runtime choices. Plan-mode remnants were reduced further as part of this pass. - Document update: 2026-04-05 16:55 (KST) - Simplified AX Agent message rows and sidebar conversation items toward the `claw-code` reading model. Message bubbles now use tighter padding/radius/meta text, and conversation rows now prefer lightweight running/failure summary text over heavier success/failure badge cards. diff --git a/docs/claw-code-parity-plan.md b/docs/claw-code-parity-plan.md index fcc3d81..35ed820 100644 --- a/docs/claw-code-parity-plan.md +++ b/docs/claw-code-parity-plan.md @@ -199,6 +199,14 @@ - Keep AX's richer business/document tool set - Bring transcript rendering and approval/status UX closer to `claw-code` +## Transcript-First Approval / Ask UX +- Updated: 2026-04-05 18:58 (KST) +- `plan approval` and `user ask` should both resolve inside the transcript first. +- Secondary windows are allowed only as detail surfaces, not as the primary decision flow. +- AX implementation status: + - `plan approval`: transcript-first, detail view via `PlanViewerWindow` + - `user ask`: transcript-first inline question card with choices / direct input / submit + ## Current Snapshot - Updated: 2026-04-05 19:42 (KST) - Estimated parity: diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index b74b7d5..8a2e414 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -67,6 +67,7 @@ public partial class ChatWindow : Window private bool _aiIconPulseStopped; // 펄스 1회만 중지 private WorkflowAnalyzerWindow? _analyzerWindow; // 워크플로우 분석기 private PlanViewerWindow? _planViewerWindow; // 실행 계획 뷰어 + private Border? _userAskCard; // transcript 내 질문 카드 private bool _userScrolled; // 사용자가 위로 스크롤했는지 private readonly HashSet _sessionPermissionRules = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _sessionMcpEnabledOverrides = new(StringComparer.OrdinalIgnoreCase); @@ -200,16 +201,7 @@ public partial class ChatWindow : Window return decision != PermissionRequestWindow.PermissionPromptResult.Reject; }, - UserAskCallback = async (question, options, defaultValue) => - { - string? response = null; - var appDispatcher = System.Windows.Application.Current?.Dispatcher ?? Dispatcher; - await appDispatcher.InvokeAsync(() => - { - response = UserAskDialog.Show(question, options, defaultValue); - }); - return response; - }, + UserAskCallback = ShowInlineUserAskAsync, }; SubAgentTool.StatusChanged += OnSubAgentStatusChanged; @@ -9902,6 +9894,250 @@ public partial class ChatWindow : Window outerStack.Children.Add(resultLabel); } + private async Task ShowInlineUserAskAsync(string question, List options, string defaultValue) + { + var tcs = new TaskCompletionSource(); + + await Dispatcher.InvokeAsync(() => + { + AddUserAskCard(question, options, defaultValue, tcs); + }); + + var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromMinutes(5))); + if (completed != tcs.Task) + { + await Dispatcher.InvokeAsync(() => + { + RemoveUserAskCard(); + }); + return null; + } + + return await tcs.Task; + } + + private void RemoveUserAskCard() + { + if (_userAskCard == null) + return; + + MessagePanel.Children.Remove(_userAskCard); + _userAskCard = null; + } + + private void AddUserAskCard(string question, List options, string defaultValue, TaskCompletionSource tcs) + { + RemoveUserAskCard(); + + var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; + var accentColor = ((SolidColorBrush)accentBrush).Color; + var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; + var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; + var borderBrush = TryFindResource("BorderColor") as Brush + ?? new SolidColorBrush(Color.FromArgb(0x24, accentColor.R, accentColor.G, accentColor.B)); + var itemBg = TryFindResource("ItemBackground") as Brush + ?? new SolidColorBrush(Color.FromArgb(0x10, accentColor.R, accentColor.G, accentColor.B)); + var hoverBg = TryFindResource("ItemHoverBackground") as Brush + ?? new SolidColorBrush(Color.FromArgb(0x16, 0xFF, 0xFF, 0xFF)); + var okBrush = BrushFromHex("#10B981"); + var dangerBrush = BrushFromHex("#EF4444"); + + var container = new Border + { + Margin = new Thickness(40, 4, 90, 8), + HorizontalAlignment = HorizontalAlignment.Left, + MaxWidth = Math.Max(420, GetMessageMaxWidth() - 36), + Background = itemBg, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(14), + Padding = new Thickness(14, 12, 14, 12), + }; + + var outer = new StackPanel(); + outer.Children.Add(new TextBlock + { + Text = "의견 요청", + FontSize = 12.5, + FontWeight = FontWeights.SemiBold, + Foreground = primaryText, + }); + outer.Children.Add(new TextBlock + { + Text = question, + Margin = new Thickness(0, 4, 0, 10), + FontSize = 12.5, + Foreground = primaryText, + TextWrapping = TextWrapping.Wrap, + LineHeight = 20, + }); + + Border? selectedOption = null; + string selectedResponse = defaultValue; + + if (options.Count > 0) + { + var optionPanel = new WrapPanel + { + Margin = new Thickness(0, 0, 0, 10), + ItemWidth = double.NaN, + }; + + foreach (var option in options.Where(static option => !string.IsNullOrWhiteSpace(option))) + { + var optionLabel = option.Trim(); + var optBorder = new Border + { + Background = Brushes.Transparent, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(999), + Padding = new Thickness(10, 6, 10, 6), + Margin = new Thickness(0, 0, 8, 8), + Cursor = Cursors.Hand, + Child = new TextBlock + { + Text = optionLabel, + FontSize = 12, + Foreground = primaryText, + }, + }; + + optBorder.MouseEnter += (s, _) => + { + if (!ReferenceEquals(selectedOption, s)) + ((Border)s).Background = hoverBg; + }; + optBorder.MouseLeave += (s, _) => + { + if (!ReferenceEquals(selectedOption, s)) + ((Border)s).Background = Brushes.Transparent; + }; + optBorder.MouseLeftButtonUp += (_, _) => + { + if (selectedOption != null) + { + selectedOption.Background = Brushes.Transparent; + selectedOption.BorderBrush = borderBrush; + } + + selectedOption = optBorder; + selectedOption.Background = new SolidColorBrush(Color.FromArgb(0x18, accentColor.R, accentColor.G, accentColor.B)); + selectedOption.BorderBrush = accentBrush; + selectedResponse = optionLabel; + }; + + optionPanel.Children.Add(optBorder); + } + + outer.Children.Add(optionPanel); + } + + outer.Children.Add(new TextBlock + { + Text = "직접 입력", + FontSize = 11.5, + Foreground = secondaryText, + Margin = new Thickness(0, 0, 0, 6), + }); + + var inputBox = new TextBox + { + Text = defaultValue, + AcceptsReturn = true, + TextWrapping = TextWrapping.Wrap, + MinHeight = 42, + MaxHeight = 100, + FontSize = 12.5, + Padding = new Thickness(10, 8, 10, 8), + Background = Brushes.Transparent, + Foreground = primaryText, + CaretBrush = primaryText, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1), + }; + inputBox.TextChanged += (_, _) => + { + if (!string.IsNullOrWhiteSpace(inputBox.Text)) + { + selectedResponse = inputBox.Text.Trim(); + if (selectedOption != null) + { + selectedOption.Background = Brushes.Transparent; + selectedOption.BorderBrush = borderBrush; + selectedOption = null; + } + } + }; + outer.Children.Add(inputBox); + + var buttonRow = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Right, + Margin = new Thickness(0, 12, 0, 0), + }; + + Border BuildActionButton(string label, Brush bg, Brush fg) + { + return new Border + { + Background = bg, + BorderBrush = bg, + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(999), + Padding = new Thickness(12, 7, 12, 7), + Margin = new Thickness(8, 0, 0, 0), + Cursor = Cursors.Hand, + Child = new TextBlock + { + Text = label, + FontSize = 12, + FontWeight = FontWeights.SemiBold, + Foreground = fg, + }, + }; + } + + var cancelBtn = BuildActionButton("취소", Brushes.Transparent, dangerBrush); + cancelBtn.BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, 0xEF, 0x44, 0x44)); + cancelBtn.MouseLeftButtonUp += (_, _) => + { + RemoveUserAskCard(); + tcs.TrySetResult(null); + }; + buttonRow.Children.Add(cancelBtn); + + var submitBtn = BuildActionButton("전달", okBrush, Brushes.White); + submitBtn.MouseLeftButtonUp += (_, _) => + { + var finalResponse = !string.IsNullOrWhiteSpace(inputBox.Text) + ? inputBox.Text.Trim() + : selectedResponse?.Trim(); + + if (string.IsNullOrWhiteSpace(finalResponse)) + finalResponse = defaultValue?.Trim(); + + if (string.IsNullOrWhiteSpace(finalResponse)) + return; + + RemoveUserAskCard(); + tcs.TrySetResult(finalResponse); + }; + buttonRow.Children.Add(submitBtn); + + outer.Children.Add(buttonRow); + container.Child = outer; + _userAskCard = container; + + container.Opacity = 0; + container.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(180))); + MessagePanel.Children.Add(container); + ForceScrollToEnd(); + inputBox.Focus(); + inputBox.CaretIndex = inputBox.Text.Length; + } + // ════════════════════════════════════════════════════════════ // 실행 계획 뷰어 (PlanViewerWindow) 연동 // ════════════════════════════════════════════════════════════