AX Agent 코워크·코드 흐름과 컨텍스트 관리를 claude-code 기준으로 대폭 정리

- 코워크·코드 프롬프트, 도구 선택, 문서 생성/검증 흐름을 claude-code 동등 품질 기준으로 재정렬함

- OpenAI/vLLM 경로의 오래된 tool history를 평탄화하고 최근 이력만 구조화해 컨텍스트 직렬화를 경량화함

- AX Agent UI를 테마 기준으로 재구성하고 플랜 승인/오버레이/이벤트 렌더링/명령 입력 상호작용을 개선함

- 파일 후보 제안, 반복 경로 정체 복구, LSP 보강, 문서·PPT 처리 개선, 설정/서비스 인터페이스 정리를 함께 반영함

- README.md 및 docs/DEVELOPMENT.md를 작업 시점별로 갱신함

- 검증: 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:
2026-04-12 22:02:14 +09:00
parent b8f4df1892
commit fb0bea41f7
137 changed files with 18532 additions and 1144 deletions

View File

@@ -17,7 +17,7 @@ namespace AxCopilot.Views;
/// - 사방 가장자리 드래그 리사이즈
/// - 항목 드래그로 순서 변경
/// </summary>
internal sealed class PlanViewerWindow : Window
internal sealed class PlanViewerWindow : Window, IPlanViewerWindow
{
// ── Win32 Resize ──
[DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
@@ -30,7 +30,7 @@ internal sealed class PlanViewerWindow : Window
private readonly StackPanel _stepsPanel;
private readonly ScrollViewer _scrollViewer;
private readonly StackPanel _btnPanel;
private readonly WrapPanel _btnPanel;
private readonly Border _statusBar;
private readonly TextBlock _statusText;
private readonly TextBlock _progressText;
@@ -60,19 +60,21 @@ internal sealed class PlanViewerWindow : Window
_uiExpressionLevel = ResolveUiExpressionLevel();
Width = 640;
Height = 520;
MinWidth = 480;
MinHeight = 360;
WindowStartupLocation = owner == null
? WindowStartupLocation.CenterScreen
: WindowStartupLocation.CenterOwner;
Width = 720;
Height = 640;
MinWidth = 520;
MinHeight = 420;
WindowStartupLocation = WindowStartupLocation.Manual; // 채팅 영역 중앙에 수동 배치
WindowStyle = WindowStyle.None;
AllowsTransparency = true;
Background = Brushes.Transparent;
ResizeMode = ResizeMode.CanResize; // WndProc로 직접 처리
ShowInTaskbar = false;
// 채팅 영역 기준 중앙 배치
if (owner != null)
PositionToChatArea(owner);
var bgBrush = TryFindResource("LauncherBackground") as Brush
?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
@@ -199,7 +201,7 @@ internal sealed class PlanViewerWindow : Window
mainGrid.Children.Add(_scrollViewer);
// ── 하단 버튼 패널 ──
_btnPanel = new StackPanel
_btnPanel = new WrapPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Center,
@@ -252,6 +254,52 @@ internal sealed class PlanViewerWindow : Window
// 창 이동 / 리사이즈
// ════════════════════════════════════════════════════════════
/// <summary>채팅 영역(사이드바 제외) 기준으로 창을 중앙 배치합니다.</summary>
private void PositionToChatArea(Window owner)
{
Loaded += (_, _) =>
{
try
{
// owner 좌표 기준으로 채팅 영역 산출
// 사이드바는 보통 좌측 ~250px, 나머지가 채팅 영역
double sidebarWidth = 250;
// XAML에서 SidebarColumn + IconBarColumn 너비를 동적으로 가져오기
if (owner is AxCopilot.Views.ChatWindow chatWin)
{
try
{
sidebarWidth = chatWin.SidebarColumn.ActualWidth
+ chatWin.IconBarColumn.ActualWidth;
}
catch { /* 폴백: 기본 250px */ }
}
double ownerLeft = owner.Left;
double ownerTop = owner.Top;
double ownerWidth = owner.ActualWidth;
double ownerHeight = owner.ActualHeight;
// 채팅 영역: 사이드바 이후 ~ 오른쪽 끝
double chatAreaLeft = ownerLeft + sidebarWidth;
double chatAreaWidth = ownerWidth - sidebarWidth;
double chatAreaCenterX = chatAreaLeft + (chatAreaWidth - ActualWidth) / 2;
double chatAreaCenterY = ownerTop + (ownerHeight - ActualHeight) / 2;
// 화면 밖으로 나가지 않도록 클램핑
Left = Math.Max(0, chatAreaCenterX);
Top = Math.Max(0, chatAreaCenterY);
}
catch
{
// 실패 시 owner 중앙 폴백
Left = owner.Left + (owner.ActualWidth - ActualWidth) / 2;
Top = owner.Top + (owner.ActualHeight - ActualHeight) / 2;
}
};
}
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ClickCount == 2) return;
@@ -403,54 +451,66 @@ internal sealed class PlanViewerWindow : Window
var canEdit = !_isExecuting && _currentStep < 0; // 승인 대기 중에만 편집/순서변경 가능
var summaryText = _uiExpressionLevel switch
// ── 계획 메타 정보 추출 ──
var planMeta = ExtractPlanMeta(_planText);
var summaryText = _isExecuting
? $"현재 단계를 기준으로 진행률을 표시합니다."
: $"{_steps.Count}개 섹션의 문서 구조를 검토한 후 승인 또는 수정 요청을 선택하세요.";
var accentColor = ((SolidColorBrush)accentBrush).Color;
var statusPanel = new StackPanel();
statusPanel.Children.Add(new TextBlock
{
"simple" => _isExecuting
? "진행률을 간단히 표시합니다."
: $"{_steps.Count}단계 계획입니다. 핵심 단계만 확인하고 승인하세요.",
"rich" => _isExecuting
? "현재 단계를 기준으로 진행률을 표시합니다. 필요 시 단계를 펼쳐 세부 내용을 확인할 수 있습니다."
: $"총 {_steps.Count}단계입니다. 단계별 내용을 열어 우선순위/의존성을 검토한 뒤 승인 또는 수정 요청을 선택하세요.",
_ => _isExecuting
? "현재 단계를 기준으로 진행률을 표시합니다."
: $"총 {_steps.Count}단계입니다. 단계를 펼쳐 검토한 후 승인 또는 수정 요청을 선택하세요.",
};
Text = _isExecuting ? "계획 실행 중" : "계획 승인 대기",
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = accentBrush,
});
// 메타 정보 표시 (토픽, 포맷, 분량)
if (!string.IsNullOrWhiteSpace(planMeta.Topic))
{
statusPanel.Children.Add(new TextBlock
{
Text = planMeta.Topic,
FontSize = 13,
FontWeight = FontWeights.Bold,
Foreground = primaryText,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 6, 0, 0),
});
}
if (!string.IsNullOrWhiteSpace(planMeta.Details))
{
statusPanel.Children.Add(new TextBlock
{
Text = planMeta.Details,
FontSize = 11.5,
Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 3, 0, 0),
});
}
statusPanel.Children.Add(new TextBlock
{
Text = summaryText,
FontSize = 11.5,
Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 3, 0, 0),
});
_stepsPanel.Children.Add(new Border
{
Background = new SolidColorBrush(Color.FromArgb(0x12,
((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)),
Background = new SolidColorBrush(Color.FromArgb(0x12, accentColor.R, accentColor.G, accentColor.B)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, accentColor.R, accentColor.G, accentColor.B)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(10),
Padding = new Thickness(10, 8, 10, 8),
Padding = new Thickness(12, 10, 12, 10),
Margin = new Thickness(0, 0, 0, 8),
Child = new StackPanel
{
Children =
{
new TextBlock
{
Text = _isExecuting ? "계획 실행 중" : "계획 승인 대기",
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = accentBrush,
},
new TextBlock
{
Text = summaryText,
FontSize = 11.5,
Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 3, 0, 0),
}
}
}
Child = statusPanel,
});
for (int i = 0; i < _steps.Count; i++)
@@ -866,39 +926,22 @@ internal sealed class PlanViewerWindow : Window
var editLabel = _uiExpressionLevel == "simple" ? "수정" : "수정 피드백";
var rejectLabel = _uiExpressionLevel == "simple" ? "취소" : "거부";
_btnPanel.Children.Add(new Border
{
Background = new SolidColorBrush(Color.FromArgb(0x16, accentColor.R, accentColor.G, accentColor.B)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, accentColor.R, accentColor.G, accentColor.B)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(10),
Padding = new Thickness(12, 8, 12, 8),
Margin = new Thickness(0, 0, 12, 0),
Child = new TextBlock
{
Text = "검토가 끝나면 바로 실행하거나 방향만 짧게 남길 수 있습니다.",
FontSize = 11.5,
Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap,
}
});
var cancelBrush = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26));
var cancelBtn = CreateActionButton("\uE711", rejectLabel, cancelBrush, cancelBrush, false);
cancelBtn.MouseLeftButtonUp += (_, _) => { _tcs?.TrySetResult("취소"); Hide(); };
_btnPanel.Children.Add(cancelBtn);
var approveBtn = CreateActionButton("\uE73E", approveLabel, accentBrush, Brushes.White, true);
approveBtn.MouseLeftButtonUp += (_, _) =>
{
_tcs?.TrySetResult(null);
SwitchToExecutionMode();
Hide();
};
_btnPanel.Children.Add(approveBtn);
var editBtn = CreateActionButton("\uE70F", editLabel, accentBrush, accentBrush, false);
editBtn.MouseLeftButtonUp += (_, _) => ShowEditInput();
_btnPanel.Children.Add(editBtn);
var cancelBrush = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26));
var cancelBtn = CreateActionButton("\uE711", rejectLabel, cancelBrush, cancelBrush, false);
cancelBtn.MouseLeftButtonUp += (_, _) => { _tcs?.TrySetResult("취소"); Hide(); };
_btnPanel.Children.Add(cancelBtn);
}
private void BuildExecutionButtons()
@@ -1114,6 +1157,71 @@ internal sealed class PlanViewerWindow : Window
return btn;
}
// ════════════════════════════════════════════════════════════
// 계획 메타 정보 추출
// ════════════════════════════════════════════════════════════
private readonly record struct PlanMeta(string Topic, string Details);
private static PlanMeta ExtractPlanMeta(string planText)
{
if (string.IsNullOrWhiteSpace(planText))
return new PlanMeta("", "");
string topic = "";
var details = new List<string>();
// 1) JSON "title" 필드
var titleMatch = System.Text.RegularExpressions.Regex.Match(
planText, @"""title""\s*:\s*""([^""]+)""");
if (titleMatch.Success)
topic = titleMatch.Groups[1].Value.Trim();
// 2) Markdown # 최상위 헤딩 (## 아닌 단일 #)
if (string.IsNullOrWhiteSpace(topic))
{
var h1Match = System.Text.RegularExpressions.Regex.Match(
planText, @"(?:^|\n)#\s+(.+?)(?:\n|$)");
if (h1Match.Success && !h1Match.Groups[1].Value.TrimStart().StartsWith("#"))
topic = h1Match.Groups[1].Value.Trim();
}
// 3) HTML <h1> 태그
if (string.IsNullOrWhiteSpace(topic))
{
var h1Html = System.Text.RegularExpressions.Regex.Match(
planText, @"<h1>([^<]+)</h1>");
if (h1Html.Success)
topic = h1Html.Groups[1].Value.Trim();
}
// JSON "format" 필드
var formatMatch = System.Text.RegularExpressions.Regex.Match(
planText, @"""format""\s*:\s*""([^""]+)""");
if (formatMatch.Success)
details.Add(formatMatch.Groups[1].Value.Trim());
// JSON "pages" / "page_count" 필드
var pagesMatch = System.Text.RegularExpressions.Regex.Match(
planText, @"""(?:pages?|page_count)""\s*:\s*""?(\d+)""?");
if (pagesMatch.Success)
details.Add($"{pagesMatch.Groups[1].Value}페이지");
// JSON "word_count" / "words" 필드
var wordsMatch = System.Text.RegularExpressions.Regex.Match(
planText, @"""(?:word_count|words)""\s*:\s*""?(\d+)""?");
if (wordsMatch.Success)
details.Add($"약 {wordsMatch.Groups[1].Value}자");
// JSON "tone" 필드
var toneMatch = System.Text.RegularExpressions.Regex.Match(
planText, @"""tone""\s*:\s*""([^""]+)""");
if (toneMatch.Success)
details.Add(toneMatch.Groups[1].Value.Trim());
return new PlanMeta(topic, string.Join(" · ", details));
}
private static string ResolveUiExpressionLevel()
{
if (Application.Current is App app)