Files
AX-Copilot-Codex/src/AxCopilot/Views/ChatWindow.UserAskPresentation.cs
lacvet 33c1db4dae
Some checks failed
Release Gate / gate (push) Has been cancelled
에이전트 선택적 탐색 구조 개선과 경고 정리 반영
- claude-code 선택적 탐색 흐름을 참고해 Cowork/Code 시스템 프롬프트에서 folder_map 상시 선행 지시를 완화하고 glob/grep 기반 좁은 탐색을 우선하도록 조정함

- FolderMapTool 기본 depth를 2로, include_files 기본값을 false로 낮추고 MultiReadTool 최대 파일 수를 8개로 줄여 초기 과탐색 폭을 보수적으로 조정함

- AgentLoopExplorationPolicy partial을 추가해 탐색 범위 분류, broad-scan corrective hint, exploration_breadth 성능 로그를 연결함

- AgentLoopService에 탐색 범위 가이드 주입과 실행 중 탐색 폭 추적을 추가하고, 좁은 질문에서 반복적인 folder_map/대량 multi_read를 교정하도록 정리함

- DocxToHtmlConverter nullable 경고를 수정해 Release 빌드 경고 0 / 오류 0 기준을 다시 충족함

- README와 docs/DEVELOPMENT.md에 2026-04-09 10:36 (KST) 기준 개발 이력을 반영함
2026-04-09 14:27:59 +09:00

302 lines
11 KiB
C#

using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace AxCopilot.Views;
public partial class ChatWindow
{
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;
RemoveTranscriptElement(_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 selectedBg = new SolidColorBrush(Color.FromArgb(0x20, accentColor.R, accentColor.G, accentColor.B));
var container = new Border
{
Margin = new Thickness(48, 6, 48, 10),
HorizontalAlignment = HorizontalAlignment.Stretch,
MaxWidth = Math.Max(540, GetMessageMaxWidth()),
Background = itemBg,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(16),
Padding = new Thickness(20, 18, 20, 18),
};
var outer = new StackPanel();
// ── 질문 텍스트 ──
outer.Children.Add(new TextBlock
{
Text = question,
FontSize = 14,
Foreground = primaryText,
TextWrapping = TextWrapping.Wrap,
LineHeight = 22,
Margin = new Thickness(0, 0, 0, 14),
});
// ── 옵션 카드 목록 ──
Border? selectedCard = null;
TextBox? customInputBox = null;
string selectedResponse = defaultValue;
var cardPanel = new StackPanel { Margin = new Thickness(0, 0, 0, 4) };
if (options.Count > 0)
{
for (int i = 0; i < options.Count; i++)
{
var optionLabel = options[i].Trim();
if (string.IsNullOrWhiteSpace(optionLabel)) continue;
var optionNumber = i + 1;
var card = new Border
{
Background = Brushes.Transparent,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(10),
Padding = new Thickness(14, 10, 14, 10),
Margin = new Thickness(0, 0, 0, 6),
Cursor = Cursors.Hand,
};
var cardGrid = new Grid();
cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var textStack = new StackPanel();
textStack.Children.Add(new TextBlock
{
Text = optionLabel,
FontSize = 13,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
TextWrapping = TextWrapping.Wrap,
});
Grid.SetColumn(textStack, 0);
cardGrid.Children.Add(textStack);
// 번호 배지
var badge = new Border
{
Width = 24, Height = 24,
CornerRadius = new CornerRadius(12),
Background = new SolidColorBrush(Color.FromArgb(0x20, 0xFF, 0xFF, 0xFF)),
VerticalAlignment = VerticalAlignment.Center,
Child = new TextBlock
{
Text = optionNumber.ToString(),
FontSize = 11,
FontWeight = FontWeights.Bold,
Foreground = secondaryText,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
},
};
Grid.SetColumn(badge, 1);
cardGrid.Children.Add(badge);
card.Child = cardGrid;
var capturedLabel = optionLabel;
var capturedCard = card;
card.MouseEnter += (_, _) =>
{
if (!ReferenceEquals(selectedCard, capturedCard))
capturedCard.Background = hoverBg;
};
card.MouseLeave += (_, _) =>
{
if (!ReferenceEquals(selectedCard, capturedCard))
capturedCard.Background = Brushes.Transparent;
};
card.MouseLeftButtonUp += (_, _) =>
{
// 이전 선택 해제
if (selectedCard != null)
{
selectedCard.Background = Brushes.Transparent;
selectedCard.BorderBrush = borderBrush;
}
// 커스텀 입력 카드 선택 해제
if (customInputBox != null)
customInputBox.BorderBrush = borderBrush;
selectedCard = capturedCard;
selectedCard.Background = selectedBg;
selectedCard.BorderBrush = accentBrush;
selectedResponse = capturedLabel;
};
cardPanel.Children.Add(card);
}
}
// ── 마지막 카드: 직접 입력 (Claude Desktop 스타일) ──
var customCard = new Border
{
Background = Brushes.Transparent,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(10),
Padding = new Thickness(2),
Margin = new Thickness(0, 0, 0, 6),
};
customInputBox = new TextBox
{
Text = defaultValue,
AcceptsReturn = true,
TextWrapping = TextWrapping.Wrap,
MinHeight = 38,
MaxHeight = 120,
FontSize = 13,
Padding = new Thickness(12, 8, 12, 8),
Background = Brushes.Transparent,
Foreground = primaryText,
CaretBrush = primaryText,
BorderThickness = new Thickness(0),
};
// 플레이스홀더
if (string.IsNullOrWhiteSpace(defaultValue))
{
customInputBox.Tag = "placeholder";
customInputBox.Text = "직접 입력...";
customInputBox.Foreground = secondaryText;
}
customInputBox.GotFocus += (_, _) =>
{
if (customInputBox.Tag?.ToString() == "placeholder")
{
customInputBox.Text = "";
customInputBox.Foreground = primaryText;
customInputBox.Tag = null;
}
// 옵션 카드 선택 해제 + 커스텀 카드 활성화
if (selectedCard != null)
{
selectedCard.Background = Brushes.Transparent;
selectedCard.BorderBrush = borderBrush;
selectedCard = null;
}
customCard.BorderBrush = accentBrush;
customCard.Background = selectedBg;
};
customInputBox.TextChanged += (_, _) =>
{
if (customInputBox.Tag?.ToString() != "placeholder" && !string.IsNullOrWhiteSpace(customInputBox.Text))
selectedResponse = customInputBox.Text.Trim();
};
customCard.Child = customInputBox;
cardPanel.Children.Add(customCard);
outer.Children.Add(cardPanel);
// ── 하단 버튼 ──
var buttonRow = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Left,
Margin = new Thickness(0, 8, 0, 0),
};
var skipBtn = new Border
{
Background = Brushes.Transparent,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(16, 7, 16, 7),
Margin = new Thickness(0, 0, 8, 0),
Cursor = Cursors.Hand,
Child = new TextBlock { Text = "건너뛰기", FontSize = 12.5, Foreground = secondaryText },
};
skipBtn.MouseEnter += (_, _) => skipBtn.Background = hoverBg;
skipBtn.MouseLeave += (_, _) => skipBtn.Background = Brushes.Transparent;
skipBtn.MouseLeftButtonUp += (_, _) =>
{
RemoveUserAskCard();
tcs.TrySetResult(null);
};
buttonRow.Children.Add(skipBtn);
var submitBtn = new Border
{
Background = accentBrush,
BorderThickness = new Thickness(0),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(16, 7, 16, 7),
Cursor = Cursors.Hand,
Child = new TextBlock { Text = "제출", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White },
};
submitBtn.MouseLeftButtonUp += (_, _) =>
{
var finalResponse = (customInputBox.Tag?.ToString() != "placeholder" && !string.IsNullOrWhiteSpace(customInputBox.Text))
? customInputBox.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)));
AddTranscriptElement(container);
ForceScrollToEnd();
}
}