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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user