AX Agent 질문/의견 요청 흐름을 transcript 우선으로 전환
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
- 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
This commit is contained in:
@@ -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 메시지 구조가 더 정교하다는 차이를 명시했습니다.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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<string> _sessionPermissionRules = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, bool> _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<string?> ShowInlineUserAskAsync(string question, List<string> options, string defaultValue)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string?>();
|
||||
|
||||
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<string> options, string defaultValue, TaskCompletionSource<string?> 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) 연동
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user