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 동등성 작업 추적 문서
|
개발 참고: Claw Code 동등성 작업 추적 문서
|
||||||
`docs/claw-code-parity-plan.md`
|
`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)
|
- 업데이트: 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 진입까지 핵심 회귀 흐름을 한 세트로 검증하도록 정리했습니다.
|
- `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 메시지 구조가 더 정교하다는 차이를 명시했습니다.
|
- 도구/스킬 비교 기준도 parity 문서에 추가했습니다. AX는 문서/오피스/데이터/업무형 도구가 더 풍부하고, `claw-code`는 transcript-native tool/approval/permission 메시지 구조가 더 정교하다는 차이를 명시했습니다.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# AX Copilot - 媛쒕컻 臾몄꽌
|
# 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) - 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) - 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.
|
- 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
|
- Keep AX's richer business/document tool set
|
||||||
- Bring transcript rendering and approval/status UX closer to `claw-code`
|
- 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
|
## Current Snapshot
|
||||||
- Updated: 2026-04-05 19:42 (KST)
|
- Updated: 2026-04-05 19:42 (KST)
|
||||||
- Estimated parity:
|
- Estimated parity:
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ public partial class ChatWindow : Window
|
|||||||
private bool _aiIconPulseStopped; // 펄스 1회만 중지
|
private bool _aiIconPulseStopped; // 펄스 1회만 중지
|
||||||
private WorkflowAnalyzerWindow? _analyzerWindow; // 워크플로우 분석기
|
private WorkflowAnalyzerWindow? _analyzerWindow; // 워크플로우 분석기
|
||||||
private PlanViewerWindow? _planViewerWindow; // 실행 계획 뷰어
|
private PlanViewerWindow? _planViewerWindow; // 실행 계획 뷰어
|
||||||
|
private Border? _userAskCard; // transcript 내 질문 카드
|
||||||
private bool _userScrolled; // 사용자가 위로 스크롤했는지
|
private bool _userScrolled; // 사용자가 위로 스크롤했는지
|
||||||
private readonly HashSet<string> _sessionPermissionRules = new(StringComparer.OrdinalIgnoreCase);
|
private readonly HashSet<string> _sessionPermissionRules = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly Dictionary<string, bool> _sessionMcpEnabledOverrides = 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;
|
return decision != PermissionRequestWindow.PermissionPromptResult.Reject;
|
||||||
},
|
},
|
||||||
UserAskCallback = async (question, options, defaultValue) =>
|
UserAskCallback = ShowInlineUserAskAsync,
|
||||||
{
|
|
||||||
string? response = null;
|
|
||||||
var appDispatcher = System.Windows.Application.Current?.Dispatcher ?? Dispatcher;
|
|
||||||
await appDispatcher.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
response = UserAskDialog.Show(question, options, defaultValue);
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
SubAgentTool.StatusChanged += OnSubAgentStatusChanged;
|
SubAgentTool.StatusChanged += OnSubAgentStatusChanged;
|
||||||
|
|
||||||
@@ -9902,6 +9894,250 @@ public partial class ChatWindow : Window
|
|||||||
outerStack.Children.Add(resultLabel);
|
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) 연동
|
// 실행 계획 뷰어 (PlanViewerWindow) 연동
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user