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

@@ -14,8 +14,21 @@ public partial class ChatWindow
{
return async (planSummary, options) =>
{
// 도구 실행 승인(확인/건너뛰기/취소)은 간결한 별도 다이얼로그로 처리
var isToolApproval = options.Contains("확인") && !options.Contains("승인");
if (isToolApproval)
{
string? toolResult = null;
await Dispatcher.InvokeAsync(() =>
{
toolResult = ToolApprovalWindow.Show(this, planSummary, options);
});
return toolResult;
}
// 계획 승인은 PlanViewerV2로 처리
var tcs = new TaskCompletionSource<string?>();
var steps = TaskDecomposer.ExtractSteps(planSummary);
var steps = ExtractPlanSteps(planSummary);
await Dispatcher.InvokeAsync(() =>
{
@@ -26,10 +39,10 @@ public partial class ChatWindow
ShowPlanButton(true);
AddDecisionButtons(tcs, options);
// 플랜 창 자동 표시 (사용자가 직접 BtnPlanViewer 클릭 안 해도 됨)
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
if (_planViewerWindow != null && IsPlanWindowAlive())
{
_planViewerWindow.Show();
_planViewerWindow.Activate();
PlanWindow?.Show();
PlanWindow?.Activate();
}
});
@@ -38,7 +51,7 @@ public partial class ChatWindow
{
await Dispatcher.InvokeAsync(() =>
{
_planViewerWindow?.Hide();
PlanWindow?.Hide();
ResetPendingPlanPresentation();
});
return "취소";
@@ -59,21 +72,12 @@ public partial class ChatWindow
agentDecision = $"수정 요청: {result.Trim()}";
}
if (result == null)
// 승인/취소/수정 모두 계획 창 닫고 상태 초기화
await Dispatcher.InvokeAsync(() =>
{
await Dispatcher.InvokeAsync(() =>
{
_planViewerWindow?.SwitchToExecutionMode();
});
}
else
{
await Dispatcher.InvokeAsync(() =>
{
_planViewerWindow?.Hide();
ResetPendingPlanPresentation();
});
}
PlanWindow?.Hide();
ResetPendingPlanPresentation();
});
return agentDecision;
};
@@ -81,17 +85,27 @@ public partial class ChatWindow
private void EnsurePlanViewerWindow()
{
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
if (_planViewerWindow != null && IsPlanWindowAlive())
return;
_planViewerWindow = new PlanViewerWindow(this);
_planViewerWindow.Closing += (_, e) =>
if (_settings.Settings.Llm.EnableNewPlanViewer)
{
e.Cancel = true;
_planViewerWindow.Hide();
};
var v2 = new PlanViewerWindowV2(this);
v2.Closing += (_, e) => { e.Cancel = true; v2.Hide(); };
_planViewerWindow = v2;
}
else
{
var v1 = new PlanViewerWindow(this);
v1.Closing += (_, e) => { e.Cancel = true; v1.Hide(); };
_planViewerWindow = v1;
}
}
private bool IsPlanWindowAlive() => IsWindowAlive(_planViewerWindow as Window);
private Window? PlanWindow => _planViewerWindow as Window;
private void ShowPlanButton(bool show)
{
// 레거시: 이전 세션에서 동적 주입된 MoodIconPanel 칩 정리
@@ -127,7 +141,7 @@ public partial class ChatWindow
return;
EnsurePlanViewerWindow();
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
if (_planViewerWindow != null && IsPlanWindowAlive())
{
if (string.IsNullOrWhiteSpace(_planViewerWindow.PlanText)
|| _planViewerWindow.PlanText != (_pendingPlanSummary ?? string.Empty)
@@ -135,8 +149,8 @@ public partial class ChatWindow
{
_planViewerWindow.LoadPlanPreview(_pendingPlanSummary ?? "", _pendingPlanSteps);
}
_planViewerWindow.Show();
_planViewerWindow.Activate();
PlanWindow?.Show();
PlanWindow?.Activate();
}
}
@@ -153,7 +167,7 @@ public partial class ChatWindow
private void UpdatePlanViewerStep(AgentEvent evt)
{
if (_planViewerWindow == null || !IsWindowAlive(_planViewerWindow))
if (_planViewerWindow == null || !IsPlanWindowAlive())
return;
if (evt.StepCurrent > 0)
@@ -162,7 +176,7 @@ public partial class ChatWindow
private void CompletePlanViewer()
{
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
if (_planViewerWindow != null && IsPlanWindowAlive())
_planViewerWindow.MarkComplete();
ResetPendingPlanPresentation();
}
@@ -174,6 +188,49 @@ public partial class ChatWindow
ShowPlanButton(false);
}
/// <summary>document_plan 결과 텍스트에서 계획 단계(섹션 목록)를 추출합니다.</summary>
private static List<string> ExtractPlanSteps(string planText)
{
if (string.IsNullOrWhiteSpace(planText))
return new List<string> { "문서 계획 검토" };
// 1) 번호 매긴 단계 (기존 TaskDecomposer)
var numbered = TaskDecomposer.ExtractSteps(planText);
if (numbered.Count >= 2) return numbered;
var sections = new List<string>();
// 2) JSON "heading" 필드 추출 (ExecuteSinglePassWithData 경로)
var headingMatches = System.Text.RegularExpressions.Regex.Matches(
planText, @"""heading""\s*:\s*""([^""]+)""");
foreach (System.Text.RegularExpressions.Match m in headingMatches)
sections.Add(m.Groups[1].Value);
if (sections.Count >= 2) return sections;
// 3) HTML <h2> 태그 (ExecuteWithHtmlScaffold 경로)
sections.Clear();
var h2Matches = System.Text.RegularExpressions.Regex.Matches(
planText, @"<h2>([^<]+)</h2>");
foreach (System.Text.RegularExpressions.Match m in h2Matches)
sections.Add(m.Groups[1].Value);
if (sections.Count >= 2) return sections;
// 4) Markdown ## 헤딩
sections.Clear();
var mdMatches = System.Text.RegularExpressions.Regex.Matches(
planText, @"(?:^|\n)##\s+(.+?)(?:\n|$)");
foreach (System.Text.RegularExpressions.Match m in mdMatches)
{
var heading = m.Groups[1].Value.Trim();
if (!heading.StartsWith("[") && !heading.Contains("즉시 실행"))
sections.Add(heading);
}
if (sections.Count >= 2) return sections;
// 5) 폴백
return new List<string> { "문서 계획 검토" };
}
private static bool IsWindowAlive(Window? w)
{
if (w == null)