diff --git a/README.md b/README.md index 94a40f5..78a14df 100644 --- a/README.md +++ b/README.md @@ -1976,3 +1976,17 @@ MIT License - golden 회귀를 확대했습니다. [HtmlSkillGoldenReportTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/HtmlSkillGoldenReportTests.cs)에 strategy brief 시나리오를 추가했고, [PptxSkillGoldenDeckTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/PptxSkillGoldenDeckTests.cs)는 PMO steering deck까지 고정했습니다. [ArtifactQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs), [ArtifactRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs), [DeckQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs), [DeckRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckRepairGuideServiceTests.cs)도 함께 확장했습니다. - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_finish_batch\\ -p:IntermediateOutputPath=obj\\verify_doc_finish_batch\\` 경고 0 / 오류 0 - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|ArtifactRepairGuideServiceTests|DeckQualityReviewServiceTests|DeckRepairGuideServiceTests|HtmlSkillGoldenReportTests|PptxSkillGoldenDeckTests|DocxSkillGoldenDocumentTests|ExcelSkillGoldenWorkbookTests" -p:OutputPath=bin\\verify_doc_finish_batch_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_finish_batch_tests\\` 통과 26 + +업데이트: 2026-04-15 10:50 (KST) +- 개발언어 fallback을 실제 코드 시스템 프롬프트와 워크스페이스 컨텍스트에 연결했습니다. + - [CodeLanguageCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/CodeLanguageCatalog.cs)를 UTF-8 기준으로 재정리하고, 언어별 `manifest/build/test/lint` 힌트를 조합하는 `BuildWorkspaceWorkflowSummaries()`를 추가했습니다. + - [WorkspaceContextGenerator.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/WorkspaceContextGenerator.cs)는 새 `DetectLanguageWorkflowHints()`를 통해 `.ax-context.md`의 `## Language Workflow` 섹션과 런타임 힌트 생성을 같은 카탈로그 기준으로 공유합니다. + - [ChatWindow.SystemPromptBuilder.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs)는 코드 탭 시스템 프롬프트에 `## Repository Language Workflow` 섹션을 주입해 no-LSP 저장소에서도 실제 실행 가능한 힌트를 먼저 보게 했습니다. +- 에이전틱 루프의 LLM 요청 준비를 helper로 더 분리했습니다. + - 새 [AgentLoopLlmRequestPreparationService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopLlmRequestPreparationService.cs)가 초기 tool-call 강제 여부와 pre-call reminder 주입을 담당하고, [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)는 orchestration 중심 구조를 더 유지하도록 정리했습니다. +- 테스트를 추가/보강했습니다. + - 새 [AgentLoopLlmRequestPreparationServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentLoopLlmRequestPreparationServiceTests.cs) + - [CodeLanguageCatalogTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs), [WorkspaceContextGeneratorTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs) 확장 +- 검증: + - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_final_batch\\ -p:IntermediateOutputPath=obj\\verify_final_batch\\` 경고 0 / 오류 0 + - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "CodeLanguageCatalogTests|WorkspaceContextGeneratorTests|AgentLoopLlmRequestPreparationServiceTests|AgentLoopIterationPreparationServiceTests|AgentMessageInvariantHelperTests|AgentToolResultBudgetTests|ChatStorageServiceTests|HtmlSkillGoldenReportTests|PptxSkillGoldenDeckTests|DocxSkillGoldenDocumentTests|ExcelSkillGoldenWorkbookTests" -p:OutputPath=bin\\verify_final_batch_tests\\ -p:IntermediateOutputPath=obj\\verify_final_batch_tests\\` 통과 54 diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 7b5c7f4..ffd520b 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1116,3 +1116,30 @@ UI ?붿옄???€洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾 - 검증: - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_finish_batch\\ -p:IntermediateOutputPath=obj\\verify_doc_finish_batch\\` - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ArtifactQualityReviewServiceTests|ArtifactRepairGuideServiceTests|DeckQualityReviewServiceTests|DeckRepairGuideServiceTests|HtmlSkillGoldenReportTests|PptxSkillGoldenDeckTests|DocxSkillGoldenDocumentTests|ExcelSkillGoldenWorkbookTests" -p:OutputPath=bin\\verify_doc_finish_batch_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_finish_batch_tests\\` + +업데이트: 2026-04-15 10:50 (KST) +- 개발언어 fallback 심화: + - `CodeLanguageCatalog.cs` + - 파일을 UTF-8 기준으로 재정리하고 중복 `BuildFallbackSupportDescription()`을 제거 + - `BuildWorkspaceWorkflowSummaries()` 추가 + - 키, quick select key, 확장자, 파일 경로를 모두 받아 capability로 정규화하는 `ResolveCapabilityFromKeyOrExtension()` 추가 + - `WorkspaceContextGenerator.cs` + - `DetectLanguageWorkflowHints()` 추가 + - `.ax-context.md`의 `Language Workflow` 생성이 카탈로그 공용 API를 사용하도록 정리 + - `ChatWindow.SystemPromptBuilder.cs` + - 코드 시스템 프롬프트에 `## Repository Language Workflow` 섹션 주입 + - no-LSP 저장소에서도 실제 manifest/build/test/lint 힌트를 prompt 안에서 직접 활용 +- 에이전틱 루프 분리: + - 새 `AgentLoopLlmRequestPreparationService.cs` + - 초기 tool-call 강제 여부 계산 + - pre-call tool reminder 삽입 여부 계산 + - 실제 LLM 전송용 `sendMessages` 배열 조립 + - `AgentLoopService.cs` + - LLM 요청 전 메시지 조립 책임을 helper 호출로 대체해 orchestration 집중도 향상 +- 테스트 보강: + - 새 `AgentLoopLlmRequestPreparationServiceTests.cs` + - `CodeLanguageCatalogTests.cs`: fallback summary, workflow summary, workspace workflow dedupe/우선순위 검증 + - `WorkspaceContextGeneratorTests.cs`: preferred language 우선 `Language Workflow` 힌트 검증 +- 검증: + - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_final_batch\\ -p:IntermediateOutputPath=obj\\verify_final_batch\\` 경고 0 / 오류 0 + - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "CodeLanguageCatalogTests|WorkspaceContextGeneratorTests|AgentLoopLlmRequestPreparationServiceTests|AgentLoopIterationPreparationServiceTests|AgentMessageInvariantHelperTests|AgentToolResultBudgetTests|ChatStorageServiceTests|HtmlSkillGoldenReportTests|PptxSkillGoldenDeckTests|DocxSkillGoldenDocumentTests|ExcelSkillGoldenWorkbookTests" -p:OutputPath=bin\\verify_final_batch_tests\\ -p:IntermediateOutputPath=obj\\verify_final_batch_tests\\` 통과 54 diff --git a/docs/NEXT_ROADMAP.md b/docs/NEXT_ROADMAP.md index 46acaa8..672d99a 100644 --- a/docs/NEXT_ROADMAP.md +++ b/docs/NEXT_ROADMAP.md @@ -158,6 +158,13 @@ 2. `tool_result replacement state`는 synthetic preview를 넘어 fingerprint 재바인딩까지 들어갔습니다. 남은 방향은 compact/branch 이후의 replacement policy를 세션 단위 상태로 더 오래 유지하는 것입니다. 3. 문서 포맷은 `ArtifactQualityOutputFormatter`가 HTML/XLSX뿐 아니라 DOCX/PPTX까지 확장되었습니다. 다음 마감은 포맷별 critic/repair 자체를 더 깊게 하고, golden fixture 샘플을 확대하는 단계입니다. +업데이트: 2026-04-15 10:50 (KST) + +### 추가 진행 메모 +1. 개발언어 fallback은 이제 `Language Workflow`를 `.ax-context.md`와 코드 시스템 프롬프트에 모두 주입하는 단계까지 올라왔습니다. 남은 마감은 no-LSP 환경에서 이 힌트를 실제 도구 사용/검증 흐름과 더 촘촘히 묶는 것입니다. +2. 에이전틱 루프는 `run lifecycle`, `queued command projection`, `iteration preparation`, `LLM request preparation` helper까지 분리됐습니다. 다음 큰 축은 `RunAsync`의 tool dispatch/finalize 분리를 더 진행해 본체 책임을 더 줄이는 것입니다. +3. 명령/스킬 합성은 우선순위 충돌 해소가 대부분 정리됐고, 이후 작업은 릴리즈 게이트와 체크리스트를 최종 상태에 맞춰 닫는 단계입니다. + 업데이트: 2026-04-15 10:10 (KST) ### 통합 마감 계획 diff --git a/src/AxCopilot.Tests/Services/AgentLoopLlmRequestPreparationServiceTests.cs b/src/AxCopilot.Tests/Services/AgentLoopLlmRequestPreparationServiceTests.cs new file mode 100644 index 0000000..7f12ebe --- /dev/null +++ b/src/AxCopilot.Tests/Services/AgentLoopLlmRequestPreparationServiceTests.cs @@ -0,0 +1,58 @@ +using AxCopilot.Models; +using AxCopilot.Services.Agent; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Services; + +public class AgentLoopLlmRequestPreparationServiceTests +{ + [Fact] + public void Prepare_ShouldInjectToolReminderOnFirstForcedCall() + { + var queryMessages = new List + { + new() + { + Role = "user", + Content = "inspect the repository" + } + }; + + var result = AgentLoopLlmRequestPreparationService.Prepare( + queryMessages, + totalToolCalls: 0, + forceInitialToolCallEnabled: true, + injectPreCallToolReminder: true, + noToolCallLoopRetry: 0); + + result.ForceInitialToolCall.Should().BeTrue(); + result.InjectedToolReminder.Should().BeTrue(); + result.SendMessages.Should().HaveCount(2); + result.SendMessages.Last().Content.Should().Contain("[TOOL_REQUIRED]"); + } + + [Fact] + public void Prepare_ShouldSkipReminderWhenRetryLoopIsAlreadyActive() + { + var queryMessages = new List + { + new() + { + Role = "user", + Content = "inspect the repository" + } + }; + + var result = AgentLoopLlmRequestPreparationService.Prepare( + queryMessages, + totalToolCalls: 0, + forceInitialToolCallEnabled: true, + injectPreCallToolReminder: true, + noToolCallLoopRetry: 1); + + result.ForceInitialToolCall.Should().BeTrue(); + result.InjectedToolReminder.Should().BeFalse(); + result.SendMessages.Should().HaveCount(1); + } +} diff --git a/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs b/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs index 7b0f9da..19c7bc1 100644 --- a/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs +++ b/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs @@ -57,6 +57,7 @@ public class CodeLanguageCatalogTests summary.Should().Contain("go build ./..."); summary.Should().Contain("go test ./..."); } + [Fact] public void BuildWorkflowSummary_ShouldExposeActionableWorkflowHints() { @@ -77,4 +78,18 @@ public class CodeLanguageCatalogTests CodeLanguageCatalog.GetTestHints("kotlin").Should().Contain("./gradlew test"); CodeLanguageCatalog.GetLintHints("javascript").Should().Contain("eslint ."); } + + [Fact] + public void BuildWorkspaceWorkflowSummaries_ShouldPreferSelectedLanguageAndDedupeEntries() + { + var summaries = CodeLanguageCatalog.BuildWorkspaceWorkflowSummaries( + [".go", ".py", ".go", ".rs"], + preferredKey: "python", + maxLanguages: 3); + + summaries.Should().HaveCount(3); + summaries[0].Should().StartWith("Python:"); + summaries.Should().Contain(summary => summary.StartsWith("Go:")); + summaries.Should().Contain(summary => summary.StartsWith("Rust:")); + } } diff --git a/src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs b/src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs index 234fa0d..0a1a465 100644 --- a/src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs +++ b/src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs @@ -338,4 +338,27 @@ public class WorkspaceContextGeneratorTests Directory.Delete(tempDir, recursive: true); } } + + [Fact] + public void DetectLanguageWorkflowHints_ShouldPreferSelectedLanguage() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempDir); + try + { + File.WriteAllText(Path.Combine(tempDir, "main.go"), "package main"); + File.WriteAllText(Path.Combine(tempDir, "worker.go"), "package main"); + File.WriteAllText(Path.Combine(tempDir, "helper.py"), "print('hi')"); + + var hints = WorkspaceContextGenerator.DetectLanguageWorkflowHints(tempDir, preferredLanguage: "python"); + + hints.Should().NotBeEmpty(); + hints[0].Should().StartWith("Python:"); + hints.Should().Contain(hint => hint.StartsWith("Go:")); + } + finally + { + Directory.Delete(tempDir, recursive: true); + } + } } diff --git a/src/AxCopilot/Services/Agent/AgentLoopLlmRequestPreparationService.cs b/src/AxCopilot/Services/Agent/AgentLoopLlmRequestPreparationService.cs new file mode 100644 index 0000000..68d2b3e --- /dev/null +++ b/src/AxCopilot/Services/Agent/AgentLoopLlmRequestPreparationService.cs @@ -0,0 +1,52 @@ +using AxCopilot.Models; + +namespace AxCopilot.Services.Agent; + +internal sealed record AgentLoopLlmRequestPreparationResult( + List SendMessages, + bool ForceInitialToolCall, + bool InjectedToolReminder); + +/// +/// query view가 만들어진 뒤 실제 LLM 요청 배열을 조립합니다. +/// 초기 tool call 강제 여부와 사전 reminder 주입을 한곳에서 결정해 +/// AgentLoopService 본체가 orchestration에 더 집중하도록 분리합니다. +/// +internal static class AgentLoopLlmRequestPreparationService +{ + public static AgentLoopLlmRequestPreparationResult Prepare( + IReadOnlyList queryMessages, + int totalToolCalls, + bool forceInitialToolCallEnabled, + bool injectPreCallToolReminder, + int noToolCallLoopRetry) + { + var forceInitialToolCall = totalToolCalls == 0 && forceInitialToolCallEnabled; + if (!forceInitialToolCall + || !injectPreCallToolReminder + || noToolCallLoopRetry > 0) + { + return new AgentLoopLlmRequestPreparationResult( + queryMessages.ToList(), + forceInitialToolCall, + false); + } + + var sendMessages = queryMessages.ToList(); + sendMessages.Add(BuildToolReminderMessage()); + return new AgentLoopLlmRequestPreparationResult( + sendMessages, + forceInitialToolCall, + true); + } + + internal static ChatMessage BuildToolReminderMessage() + { + return new ChatMessage + { + Role = "user", + Content = "[TOOL_REQUIRED] 지금 즉시 형식으로 도구를 호출하세요. 텍스트만 반환하면 거부됩니다.\n" + + "Output format:\n\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n" + }; + } +} diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.cs b/src/AxCopilot/Services/Agent/AgentLoopService.cs index d39ca5a..f398a44 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopService.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopService.cs @@ -87,22 +87,6 @@ public partial class AgentLoopService requestInterrupt && IsRunning); } - private void DrainPendingCommands(List messages) - { - var queuedSnapshot = _pendingCommands.Snapshot(); - if (queuedSnapshot.Count == 0) - return; - - var drained = _pendingCommands.DequeuePriorityBatch(); - if (drained.Count == 0) - return; - - var projection = AgentQueuedCommandProjector.Project(drained, queuedSnapshot.Count - drained.Count); - messages.AddRange(projection.Messages); - foreach (var evt in projection.Events) - EmitEvent(evt.Type, evt.ToolName, evt.Summary); - } - /// 에이전트 이벤트 스트림 (UI 바인딩용). public ObservableCollection Events { get; } = new(); @@ -557,25 +541,14 @@ public partial class AgentLoopService EmitEvent(AgentEventType.Error, "", "현재 스킬 런타임 정책으로 사용 가능한 도구가 없습니다."); return "⚠ 현재 스킬 정책에서 허용된 도구가 없어 작업을 진행할 수 없습니다. allowed-tools 설정을 확인하세요."; } - // totalToolCalls == 0: 아직 한 번도 도구를 안 불렀으면 tool_choice:"required" 강제 - // → chatty 모델(Qwen 등)이 텍스트 설명만 하고 도구를 안 부르는 현상 방지 - var forceFirst = totalToolCalls == 0 && executionPolicy.ForceInitialToolCall; - - // IBM/Qwen 등 chatty 모델 대응: 첫 번째 호출 직전 마지막 user 메시지로 도구 호출 강제 reminder 주입. - // recovery 메시지가 이미 추가된 경우(NoToolCallLoopRetry > 0)에는 중복 주입하지 않음. - // 임시 메시지이므로 실제 messages 목록은 수정하지 않고, 별도 sendMessages로 전달. - List sendMessages = queryMessages; - if (forceFirst - && executionPolicy.InjectPreCallToolReminder - && runState.NoToolCallLoopRetry == 0) - { - sendMessages = [.. queryMessages, new ChatMessage - { - Role = "user", - Content = "[TOOL_REQUIRED] 지금 즉시 형식으로 도구를 호출하세요. 텍스트만 반환하면 거부됩니다.\n" + - "Output format:\n\n{\"name\": \"TOOL_NAME\", \"arguments\": {\"param\": \"value\"}}\n" - }]; - } + var llmRequest = AgentLoopLlmRequestPreparationService.Prepare( + queryMessages, + totalToolCalls, + executionPolicy.ForceInitialToolCall, + executionPolicy.InjectPreCallToolReminder, + runState.NoToolCallLoopRetry); + var forceFirst = llmRequest.ForceInitialToolCall; + var sendMessages = llmRequest.SendMessages; // 워크플로우 상세 로그: LLM 요청 llmCallSw.Restart(); diff --git a/src/AxCopilot/Services/Agent/WorkspaceContextGenerator.cs b/src/AxCopilot/Services/Agent/WorkspaceContextGenerator.cs index 26ec742..0eb7376 100644 --- a/src/AxCopilot/Services/Agent/WorkspaceContextGenerator.cs +++ b/src/AxCopilot/Services/Agent/WorkspaceContextGenerator.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.IO; +using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -174,6 +175,21 @@ internal static class WorkspaceContextGenerator catch { return null; } } + internal static IReadOnlyList DetectLanguageWorkflowHints( + string? workFolder, + string? preferredLanguage = null, + int maxLanguages = 3) + { + if (string.IsNullOrWhiteSpace(workFolder) || !Directory.Exists(workFolder)) + return []; + + var extDist = GetExtensionDistribution(workFolder, CancellationToken.None); + return CodeLanguageCatalog.BuildWorkspaceWorkflowSummaries( + extDist.Select(x => x.Key), + preferredLanguage, + maxLanguages); + } + // ════════════════════════════════════════════════════════════ // 분석 로직 // ════════════════════════════════════════════════════════════ @@ -450,26 +466,7 @@ internal static class WorkspaceContextGenerator .ToList(); private static List BuildLanguageWorkflow(List> extDist) - { - var workflow = new List(); - var seen = new HashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var (extension, _) in extDist) - { - var capability = Services.CodeLanguageCatalog.FindByExtension(extension); - if (capability == null || !seen.Add(capability.Key)) - continue; - - var summary = Services.CodeLanguageCatalog.BuildWorkflowSummary(capability.Key); - if (!string.IsNullOrWhiteSpace(summary)) - workflow.Add(summary); - - if (workflow.Count >= 3) - break; - } - - return workflow; - } + => CodeLanguageCatalog.BuildWorkspaceWorkflowSummaries(extDist.Select(x => x.Key)).ToList(); private static async Task<(string? Branch, string? Remote)> GetGitInfoAsync( string folder, CancellationToken ct) diff --git a/src/AxCopilot/Services/CodeLanguageCatalog.cs b/src/AxCopilot/Services/CodeLanguageCatalog.cs index b74d84f..1c0959f 100644 --- a/src/AxCopilot/Services/CodeLanguageCatalog.cs +++ b/src/AxCopilot/Services/CodeLanguageCatalog.cs @@ -20,12 +20,7 @@ public sealed record CodeLanguageCapability( /// /// 코드 탭과 에이전트가 공통으로 참조하는 언어 지원 카탈로그. -/// - 파일 분류 -/// - 인덱싱 대상 확장자 -/// - 시스템 프롬프트 언어 가이드 -/// - LSP 연동 가능 언어 -/// - 설정 UI 설명 문자열 -/// 를 한 곳에서 관리합니다. +/// 파일 분류, 시스템 프롬프트 가이드, build/test/lint 힌트, LSP 가능 여부를 한 곳에서 관리합니다. /// public static class CodeLanguageCatalog { @@ -100,187 +95,187 @@ public static class CodeLanguageCatalog new(new List { new( - "csharp", - "C# (.NET)", - [".cs", ".csx", ".csproj", ".sln"], - [ - "Use dotnet CLI, solution/project files, and NuGet package conventions.", - "Follow Microsoft naming conventions and prefer targeted edits over broad rewrites.", - "Verify impact on callers, DI registration, nullable flow, and build configuration." - ], - LspLanguageId: "csharp", - ShowInQuickSelect: true, - QuickSelectKey: "csharp", - QuickSelectLabel: "C# (.NET)", - QuickSelectIcon: "\uD83D\uDD39"), + "csharp", + "C# (.NET)", + [".cs", ".csx", ".csproj", ".sln"], + [ + "Use dotnet CLI, solution/project files, and NuGet package conventions.", + "Follow Microsoft naming conventions and prefer targeted edits over broad rewrites.", + "Verify impact on callers, DI registration, nullable flow, and build configuration." + ], + LspLanguageId: "csharp", + ShowInQuickSelect: true, + QuickSelectKey: "csharp", + QuickSelectLabel: "C# (.NET)", + QuickSelectIcon: "\uD83D\uDD39"), new( - "python", - "Python", - [".py", ".pyi", ".ipynb"], - [ - "Use pip/venv or conda only if already available in the environment.", - "Follow PEP 8, type hints, and module/package boundaries.", - "Prefer small focused functions and verify import/runtime errors after edits." - ], - LspLanguageId: "python", - ShowInQuickSelect: true, - QuickSelectKey: "python", - QuickSelectLabel: "Python", - QuickSelectIcon: "\uD83D\uDC0D"), + "python", + "Python", + [".py", ".pyi", ".ipynb"], + [ + "Use pip/venv or conda only if already available in the environment.", + "Follow PEP 8, type hints, and module/package boundaries.", + "Prefer small focused functions and verify import/runtime errors after edits." + ], + LspLanguageId: "python", + ShowInQuickSelect: true, + QuickSelectKey: "python", + QuickSelectLabel: "Python", + QuickSelectIcon: "\uD83D\uDC0D"), new( - "java", - "Java", - [".java", ".gradle", ".pom"], - [ - "Use Maven or Gradle conventions already present in the repository.", - "Follow package structure, visibility rules, and style consistent with the existing codebase.", - "Check interfaces, implementations, and test fixtures together when modifying shared behavior." - ], - LspLanguageId: "java", - ShowInQuickSelect: true, - QuickSelectKey: "java", - QuickSelectLabel: "Java", - QuickSelectIcon: "\u2615"), + "java", + "Java", + [".java", ".gradle", ".pom"], + [ + "Use Maven or Gradle conventions already present in the repository.", + "Follow package structure, visibility rules, and style consistent with the existing codebase.", + "Check interfaces, implementations, and test fixtures together when modifying shared behavior." + ], + LspLanguageId: "java", + ShowInQuickSelect: true, + QuickSelectKey: "java", + QuickSelectLabel: "Java", + QuickSelectIcon: "\u2615"), new( - "cpp", - "C / C++", - [".c", ".cc", ".cxx", ".cpp", ".h", ".hh", ".hpp", ".inl"], - [ - "Respect the repository's existing build system, usually CMake, MSBuild, or compiler-specific scripts.", - "Be careful with headers, include order, ownership, ABI-sensitive changes, and platform guards.", - "Validate both declaration and implementation impact when editing shared types." - ], - LspLanguageId: "cpp", - ShowInQuickSelect: true, - QuickSelectKey: "cpp", - QuickSelectLabel: "C/C++", - QuickSelectIcon: "\u2699"), + "cpp", + "C / C++", + [".c", ".cc", ".cxx", ".cpp", ".h", ".hh", ".hpp", ".inl"], + [ + "Respect the repository's existing build system, usually CMake, MSBuild, or compiler-specific scripts.", + "Be careful with headers, include order, ownership, ABI-sensitive changes, and platform guards.", + "Validate both declaration and implementation impact when editing shared types." + ], + LspLanguageId: "cpp", + ShowInQuickSelect: true, + QuickSelectKey: "cpp", + QuickSelectLabel: "C/C++", + QuickSelectIcon: "\u2699"), new( - "typescript", - "TypeScript", - [".ts", ".tsx", ".mts", ".cts"], - [ - "Use the existing package manager and tsconfig structure.", - "Prefer explicit types on public boundaries and check build/lint config before changing module format.", - "Preserve framework conventions already used by the project." - ], - LspLanguageId: "typescript"), + "typescript", + "TypeScript", + [".ts", ".tsx", ".mts", ".cts"], + [ + "Use the existing package manager and tsconfig structure.", + "Prefer explicit types on public boundaries and check build/lint config before changing module format.", + "Preserve framework conventions already used by the project." + ], + LspLanguageId: "typescript"), new( - "javascript", - "JavaScript / Vue", - [".js", ".jsx", ".mjs", ".cjs", ".vue"], - [ - "Use the existing Node package manager and lint/format rules.", - "For Vue, preserve the current component style and API pattern used by the project.", - "Check module boundaries, imports, and runtime side effects after edits." - ], - LspLanguageId: "javascript", - ShowInQuickSelect: true, - QuickSelectKey: "javascript", - QuickSelectLabel: "JavaScript / Vue", - QuickSelectIcon: "\uD83C\uDF10"), + "javascript", + "JavaScript / Vue", + [".js", ".jsx", ".mjs", ".cjs", ".vue"], + [ + "Use the existing Node package manager and lint/format rules.", + "For Vue, preserve the current component style and API pattern used by the project.", + "Check module boundaries, imports, and runtime side effects after edits." + ], + LspLanguageId: "javascript", + ShowInQuickSelect: true, + QuickSelectKey: "javascript", + QuickSelectLabel: "JavaScript / Vue", + QuickSelectIcon: "\uD83C\uDF10"), new( - "go", - "Go", - [".go", ".mod", ".sum"], - [ - "Preserve package boundaries, error-first flow, and gofmt-style formatting.", - "Check interfaces, exported identifiers, and concurrency-sensitive changes together." - ], - LspLanguageId: "go"), + "go", + "Go", + [".go", ".mod", ".sum"], + [ + "Preserve package boundaries, error-first flow, and gofmt-style formatting.", + "Check interfaces, exported identifiers, and concurrency-sensitive changes together." + ], + LspLanguageId: "go"), new( - "rust", - "Rust", - [".rs", ".toml"], - [ - "Respect Cargo workspace structure, ownership/borrowing rules, and crate boundaries.", - "Prefer explicit enums/results and verify compiler diagnostics after edits." - ], - LspLanguageId: "rust"), + "rust", + "Rust", + [".rs", ".toml"], + [ + "Respect Cargo workspace structure, ownership/borrowing rules, and crate boundaries.", + "Prefer explicit enums/results and verify compiler diagnostics after edits." + ], + LspLanguageId: "rust"), new( - "php", - "PHP", - [".php", ".phtml"], - [ - "Follow the framework and autoloading structure already present in the project.", - "Be careful with runtime includes, container wiring, and mixed template/application files." - ], - LspLanguageId: "php"), + "php", + "PHP", + [".php", ".phtml"], + [ + "Follow the framework and autoloading structure already present in the project.", + "Be careful with runtime includes, container wiring, and mixed template/application files." + ], + LspLanguageId: "php"), new( - "ruby", - "Ruby", - [".rb", ".rake", ".gemspec"], - [ - "Preserve gem structure, Rails or plain Ruby conventions already used in the repository.", - "Check dynamic dispatch, concerns/modules, and tests together after edits." - ], - LspLanguageId: "ruby"), + "ruby", + "Ruby", + [".rb", ".rake", ".gemspec"], + [ + "Preserve gem structure, Rails or plain Ruby conventions already used in the repository.", + "Check dynamic dispatch, concerns/modules, and tests together after edits." + ], + LspLanguageId: "ruby"), new( - "kotlin", - "Kotlin", - [".kt", ".kts"], - [ - "Preserve Gradle structure, package layout, and nullability intent.", - "Be careful with JVM interop boundaries and Android-specific module structure when present." - ], - LspLanguageId: "kotlin"), + "kotlin", + "Kotlin", + [".kt", ".kts"], + [ + "Preserve Gradle structure, package layout, and nullability intent.", + "Be careful with JVM interop boundaries and Android-specific module structure when present." + ], + LspLanguageId: "kotlin"), new( - "swift", - "Swift", - [".swift"], - [ - "Preserve target structure, Apple framework imports, and protocol-oriented design already in use.", - "Check app lifecycle and platform-specific behavior after edits." - ], - LspLanguageId: "swift"), + "swift", + "Swift", + [".swift"], + [ + "Preserve target structure, Apple framework imports, and protocol-oriented design already in use.", + "Check app lifecycle and platform-specific behavior after edits." + ], + LspLanguageId: "swift"), new( - "scala", - "Scala", - [".scala", ".sc"], - [ - "Respect sbt/module structure, functional style, and existing typeclass or Akka patterns if present.", - "Keep public APIs simple and avoid unnecessary type-level churn." - ]), + "scala", + "Scala", + [".scala", ".sc"], + [ + "Respect sbt/module structure, functional style, and existing typeclass or Akka patterns if present.", + "Keep public APIs simple and avoid unnecessary type-level churn." + ]), new( - "shell", - "Shell", - [".sh", ".bash", ".zsh"], - [ - "Prefer safe quoting, explicit exit handling, and repository-local scripts over one-off inline shell.", - "Check portability assumptions and environment-specific commands." - ]), + "shell", + "Shell", + [".sh", ".bash", ".zsh"], + [ + "Prefer safe quoting, explicit exit handling, and repository-local scripts over one-off inline shell.", + "Check portability assumptions and environment-specific commands." + ]), new( - "powershell", - "PowerShell", - [".ps1", ".psm1", ".psd1", ".bat", ".cmd"], - [ - "Prefer native PowerShell cmdlets and safe path handling.", - "Be careful with Windows-specific side effects, quoting, and admin-sensitive operations." - ]), + "powershell", + "PowerShell", + [".ps1", ".psm1", ".psd1", ".bat", ".cmd"], + [ + "Prefer native PowerShell cmdlets and safe path handling.", + "Be careful with Windows-specific side effects, quoting, and admin-sensitive operations." + ]), new( - "sql", - "SQL", - [".sql"], - [ - "Preserve migration ordering, transactional safety, and index/constraint compatibility.", - "Call out destructive or data-migrating changes explicitly." - ]), + "sql", + "SQL", + [".sql"], + [ + "Preserve migration ordering, transactional safety, and index/constraint compatibility.", + "Call out destructive or data-migrating changes explicitly." + ]), new( - "web", - "HTML / CSS / SCSS", - [".html", ".htm", ".css", ".scss", ".sass", ".less", ".xaml"], - [ - "Preserve the existing design system, layout structure, and accessibility semantics.", - "Prefer incremental visual changes and keep selectors/components scoped." - ]), + "web", + "HTML / CSS / SCSS", + [".html", ".htm", ".css", ".scss", ".sass", ".less", ".xaml"], + [ + "Preserve the existing design system, layout structure, and accessibility semantics.", + "Prefer incremental visual changes and keep selectors/components scoped." + ]), new( - "markup", - "JSON / YAML / XML / Markdown", - [".json", ".jsonc", ".xml", ".yaml", ".yml", ".md", ".txt"], - [ - "Preserve schema shape, indentation style, and comment/document conventions already used in the repository.", - "Validate references, keys, and generated consumer impact after edits." - ]), + "markup", + "JSON / YAML / XML / Markdown", + [".json", ".jsonc", ".xml", ".yaml", ".yml", ".md", ".txt"], + [ + "Preserve schema shape, indentation style, and comment/document conventions already used in the repository.", + "Validate references, keys, and generated consumer impact after edits." + ]), }); private static readonly ReadOnlyDictionary s_byKey = @@ -312,6 +307,7 @@ public static class CodeLanguageCatalog { if (string.IsNullOrWhiteSpace(key)) return null; + return s_byKey.TryGetValue(key.Trim(), out var found) ? found : null; } @@ -319,6 +315,7 @@ public static class CodeLanguageCatalog { if (string.IsNullOrWhiteSpace(extension)) return null; + return s_byExtension.TryGetValue(extension.Trim(), out var found) ? found : null; } @@ -380,6 +377,7 @@ public static class CodeLanguageCatalog sb.Append(BuildLspSupportDescription()); return sb.ToString(); } + public static IReadOnlyList GetManifestHints(string? key) { var normalizedKey = FindByKey(key)?.Key; @@ -446,21 +444,45 @@ public static class CodeLanguageCatalog return $"{capability.DisplayName}: {string.Join(" | ", parts)}"; } + public static IReadOnlyList BuildWorkspaceWorkflowSummaries( + IEnumerable extensionsOrKeys, + string? preferredKey = null, + int maxLanguages = 3, + int maxHintsPerKind = 2) + { + var results = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + void TryAppend(CodeLanguageCapability? capability) + { + if (capability == null || !seen.Add(capability.Key)) + return; + + var summary = BuildWorkflowSummary(capability.Key, maxHintsPerKind); + if (!string.IsNullOrWhiteSpace(summary)) + results.Add(summary); + } + + TryAppend(ResolveCapabilityFromKeyOrExtension(preferredKey)); + + foreach (var value in extensionsOrKeys ?? []) + { + TryAppend(ResolveCapabilityFromKeyOrExtension(value)); + if (results.Count >= Math.Max(1, maxLanguages)) + break; + } + + return results; + } + public static string BuildFallbackSupportDescription() - => "LSP 서버가 없거나 연결되지 않아도 확장자, 매니페스트, build/test/lint 힌트 기반의 정적 분석을 계속 제공합니다."; + => "LSP 서버가 없거나 연결되지 않아도 확장자, 매니페스트, build/test/lint 힌트 기반의 정적 fallback 분석을 계속 제공합니다."; public static string BuildFallbackSummary(string? filePathOrExtension) { - CodeLanguageCapability? capability = null; - if (!string.IsNullOrWhiteSpace(filePathOrExtension)) - { - capability = filePathOrExtension.StartsWith('.') - ? FindByExtension(filePathOrExtension) - : FindByExtension(Path.GetExtension(filePathOrExtension)); - } - + var capability = ResolveCapabilityFromKeyOrExtension(filePathOrExtension); if (capability == null) - return "정적 fallback: 확장자와 프로젝트 매니페스트를 먼저 확인하고 관련 build/test 명령 힌트를 따라 수동 검증하세요."; + return "정적 fallback: 확장자와 프로젝트 매니페스트를 먼저 확인하고 관련 build/test/lint 힌트를 따라 수동 검증하세요."; var sb = new StringBuilder(); sb.AppendLine($"정적 fallback 분석: {capability.DisplayName}"); @@ -484,6 +506,30 @@ public static class CodeLanguageCatalog return sb.ToString().TrimEnd(); } + private static CodeLanguageCapability? ResolveCapabilityFromKeyOrExtension(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + var normalized = value.Trim(); + var capability = FindByKey(normalized); + if (capability != null) + return capability; + + if (normalized.StartsWith('.')) + return FindByExtension(normalized); + + capability = QuickSelectLanguages + .FirstOrDefault(x => string.Equals(x.QuickSelectKey, normalized, StringComparison.OrdinalIgnoreCase)); + if (capability != null) + return capability; + + var extension = Path.GetExtension(normalized); + return string.IsNullOrWhiteSpace(extension) + ? null + : FindByExtension(extension); + } + private static void AppendHintBlock(List parts, string label, IReadOnlyList hints, int maxHintsPerKind) { if (hints.Count == 0) diff --git a/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs b/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs index e4c5e43..7bdc952 100644 --- a/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs +++ b/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs @@ -290,6 +290,15 @@ public partial class ChatWindow foreach (var guidance in CodeLanguageCatalog.GetGuidanceLines(_selectedLanguage == "auto" ? null : _selectedLanguage)) sb.AppendLine(guidance); sb.AppendLine("- Fallback: If LSP is unavailable, continue with extension-based detection, manifest files, and likely build/test/lint commands instead of stopping."); + var repositoryLanguageWorkflow = WorkspaceContextGenerator.DetectLanguageWorkflowHints( + workFolder, + _selectedLanguage == "auto" ? null : _selectedLanguage); + if (repositoryLanguageWorkflow.Count > 0) + { + sb.AppendLine("\n## Repository Language Workflow"); + foreach (var workflow in repositoryLanguageWorkflow) + sb.AppendLine("- " + workflow); + } // 코드 품질 + 안전 수칙 sb.AppendLine("\n## Code Quality & Safety");