From 6b3e5e6797995a92b37b5851ff640bd923a5afb1 Mon Sep 17 00:00:00 2001 From: lacvet Date: Wed, 15 Apr 2026 08:34:24 +0900 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFpreview=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EA=B3=A0=EC=A0=95=EA=B3=BC=20no-LSP=20=EC=96=B8=EC=96=B4=20fal?= =?UTF-8?q?lback=20=EB=A7=88=EA=B0=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 목적: - 긴 세션, compact, query view 생성 시점에도 tool_result preview 축약 상태를 더 안정적으로 유지합니다. - 격리 환경에서 로컬 LSP 서버가 없더라도 코드 탭이 언어별 정적 분석 힌트를 계속 제공하도록 마감합니다. - 설정/프롬프트/로드맵 문서까지 현재 구현 상태와 일치시키고 남은 고도화 범위를 정리합니다. 핵심 수정: - AgentQueryContextBuilder와 ContextCondenser가 query/compact 진입 전에 누락된 tool_result preview를 먼저 복원하도록 정리했습니다. - AgentToolResultBudget는 sourceMessages가 없는 호출에서도 현재 window의 tool_use_id preview를 재사용하도록 보강했습니다. - CodeLanguageCatalog에 언어별 manifest/build/test/lint fallback 힌트를 추가하고, LspTool은 LSP 서버 미가동 시 정적 fallback 안내를 반환하도록 변경했습니다. - SettingsViewModel, SettingsWindow, ChatWindow.SystemPromptBuilder에 Fallback 분석 설명과 LSP 미사용 시 대체 분석 지침을 반영했습니다. - AgentQueryContextBuilderTests를 새로 추가하고 AgentToolResultBudgetTests, CodeLanguageCatalogTests를 확장했습니다. - README.md, docs/DEVELOPMENT.md, docs/AGENT_ROADMAP.md, docs/NEXT_ROADMAP.md를 2026-04-15 08:32 (KST) 기준으로 갱신했습니다. 검증: - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_loop_lang_finish\\ -p:IntermediateOutputPath=obj\\verify_loop_lang_finish\\ - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentToolResultBudgetTests|AgentQueryContextBuilderTests|CodeLanguageCatalogTests|ContextCondenserTests" -p:OutputPath=bin\\verify_loop_lang_finish_tests\\ -p:IntermediateOutputPath=obj\\verify_loop_lang_finish_tests\\ (통과 20) --- README.md | 8 ++ docs/AGENT_ROADMAP.md | 8 ++ docs/DEVELOPMENT.md | 9 ++ docs/NEXT_ROADMAP.md | 12 ++ .../Services/AgentQueryContextBuilderTests.cs | 42 +++++++ .../Services/AgentToolResultBudgetTests.cs | 33 ++++++ .../Services/CodeLanguageCatalogTests.cs | 11 ++ .../Agent/AgentQueryContextBuilder.cs | 3 + .../Services/Agent/AgentToolResultBudget.cs | 4 +- .../Services/Agent/ContextCondenser.cs | 2 + src/AxCopilot/Services/Agent/LspTool.cs | 9 +- src/AxCopilot/Services/CodeLanguageCatalog.cs | 104 ++++++++++++++++++ src/AxCopilot/ViewModels/SettingsViewModel.cs | 1 + .../Views/ChatWindow.SystemPromptBuilder.cs | 1 + src/AxCopilot/Views/SettingsWindow.xaml | 1 + 15 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 src/AxCopilot.Tests/Services/AgentQueryContextBuilderTests.cs diff --git a/README.md b/README.md index a14b38d..ea0ae57 100644 --- a/README.md +++ b/README.md @@ -1901,3 +1901,11 @@ MIT License - [SlashCommandCatalogTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/SlashCommandCatalogTests.cs)는 exact token 충돌 시 skill 우선 해석을 회귀 검증하도록 확장했습니다. - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_command_resolution\\ -p:IntermediateOutputPath=obj\\verify_command_resolution\\` 경고 0 / 오류 0 - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "SlashCommandCatalogTests|ChatSessionStateServiceTests|AgentToolResultBudgetTests|AgentCommandQueueTests" -p:OutputPath=bin\\verify_command_resolution_tests\\ -p:IntermediateOutputPath=obj\\verify_command_resolution_tests\\` 통과 50 + +업데이트: 2026-04-15 08:32 (KST) +- `tool_result` preview 상태 고정을 한 단계 더 강화했습니다. [AgentQueryContextBuilder.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs), [ContextCondenser.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ContextCondenser.cs), [AgentToolResultBudget.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs)는 query view 생성과 compact 전에 누락된 preview를 먼저 복원하고, `sourceMessages`가 없는 호출에서도 같은 `tool_use_id` preview를 재사용하도록 정리했습니다. +- 개발언어 지원은 `no-LSP fallback`까지 마감했습니다. [CodeLanguageCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/CodeLanguageCatalog.cs)는 언어별 `manifest/build/test/lint` 힌트를 제공하고, [LspTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/LspTool.cs)는 로컬 언어 서버가 없을 때 실패만 반환하지 않고 정적 분석 fallback 안내를 돌려줍니다. +- 코드 탭 설정과 프롬프트도 함께 맞췄습니다. [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml), [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [ChatWindow.SystemPromptBuilder.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs)는 `Fallback 분석` 설명과 LSP 미사용 시 대체 분석 지침을 명시합니다. +- 테스트로 [AgentQueryContextBuilderTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentQueryContextBuilderTests.cs)를 추가했고, [AgentToolResultBudgetTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentToolResultBudgetTests.cs), [CodeLanguageCatalogTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs)를 확장해 preview 재사용과 fallback 힌트를 회귀 검증했습니다. +- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_loop_lang_finish\\ -p:IntermediateOutputPath=obj\\verify_loop_lang_finish\\` 경고 0 / 오류 0 +- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentToolResultBudgetTests|AgentQueryContextBuilderTests|CodeLanguageCatalogTests|ContextCondenserTests" -p:OutputPath=bin\\verify_loop_lang_finish_tests\\ -p:IntermediateOutputPath=obj\\verify_loop_lang_finish_tests\\` 통과 20 diff --git a/docs/AGENT_ROADMAP.md b/docs/AGENT_ROADMAP.md index b1ac98c..14f22c5 100644 --- a/docs/AGENT_ROADMAP.md +++ b/docs/AGENT_ROADMAP.md @@ -228,3 +228,11 @@ P1 + 멀티모델 후 ──→ P7 (모델 성격 매칭) > - 서브에이전트: 특화 system prompt + 제한된 도구 + 작업별 temperature > > AX Copilot도 이 패턴을 따릅니다. `SubAgentTool`이 이미 존재하므로, **프로파일 체계화(P2)**만으로 사실상 멀티에이전트가 됩니다. +## 2026-04-15 안정화 마감 메모 + +업데이트: 2026-04-15 08:32 (KST) + +- `tool_result` preview 안정화는 분기/저장/재로드 단계를 넘어 query view 생성과 context compact 이전 정규화까지 연결했습니다. +- 슬래시 명령은 팔레트 정렬과 실제 실행 해석이 같은 우선순위를 사용하도록 맞춰 skill/builtin 충돌 시 일관성이 높아졌습니다. +- 코드 탭은 `LSP 심화 지원 + 정적 fallback` 2단 구조로 정리했습니다. 로컬 언어 서버가 없어도 언어별 매니페스트, build/test/lint 힌트 기반 분석을 계속 제공하는 방향으로 마감 중입니다. +- 다음 남은 축은 `AgentLoopService` 세분화, `tool_result replacement state` 장기 세션 고정, 문서 포맷 최종 마감, 릴리즈 게이트 정리입니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 5446d03..1f70daf 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -970,3 +970,12 @@ UI ?붿옄???€洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾 - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentToolResultBudgetTests|ChatSessionStateServiceTests" -p:OutputPath=bin\\verify_preview_state_tests\\ -p:IntermediateOutputPath=obj\\verify_preview_state_tests\\` 통과 38 - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_command_resolution\\ -p:IntermediateOutputPath=obj\\verify_command_resolution\\` 경고 0 / 오류 0 - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "SlashCommandCatalogTests|ChatSessionStateServiceTests|AgentToolResultBudgetTests|AgentCommandQueueTests" -p:OutputPath=bin\\verify_command_resolution_tests\\ -p:IntermediateOutputPath=obj\\verify_command_resolution_tests\\` 통과 50 + +업데이트: 2026-04-15 08:32 (KST) +- `tool_result` replacement state를 query/compact 진입 전에도 더 고정했습니다. [AgentQueryContextBuilder.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs)는 query view 생성 전에 누락된 preview를 먼저 복원하고, [ContextCondenser.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ContextCondenser.cs)는 compact 이전에 같은 정규화를 적용해 긴 세션과 재시작 후 상태 차이를 줄였습니다. +- [AgentToolResultBudget.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs)는 `sourceMessages`가 없는 호출에서도 현재 window 자체의 `tool_use_id` preview를 재사용하도록 보강했습니다. 이 변경으로 query view 내부의 cloned tool_result도 source list 유무와 관계없이 같은 preview를 더 안정적으로 유지합니다. +- 개발언어 지원은 `no-LSP fallback`까지 연결했습니다. [CodeLanguageCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/CodeLanguageCatalog.cs)는 언어별 `manifest/build/test/lint` 힌트를 제공하고, [LspTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/LspTool.cs)는 로컬 언어 서버가 없거나 연결되지 않아도 정적 fallback 안내를 반환합니다. +- 설정과 프롬프트도 같은 모델로 맞췄습니다. [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml), [ChatWindow.SystemPromptBuilder.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs)는 `Fallback 분석` 설명과 LSP 미사용 시 대체 분석 지침을 노출합니다. +- 테스트로 [AgentQueryContextBuilderTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentQueryContextBuilderTests.cs)를 추가했고, [AgentToolResultBudgetTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentToolResultBudgetTests.cs), [CodeLanguageCatalogTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs)를 확장해 preview 재사용과 fallback 힌트를 회귀 검증했습니다. +- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_loop_lang_finish\\ -p:IntermediateOutputPath=obj\\verify_loop_lang_finish\\` 경고 0 / 오류 0 +- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentToolResultBudgetTests|AgentQueryContextBuilderTests|CodeLanguageCatalogTests|ContextCondenserTests" -p:OutputPath=bin\\verify_loop_lang_finish_tests\\ -p:IntermediateOutputPath=obj\\verify_loop_lang_finish_tests\\` 통과 20 diff --git a/docs/NEXT_ROADMAP.md b/docs/NEXT_ROADMAP.md index 5138746..1ba3f1f 100644 --- a/docs/NEXT_ROADMAP.md +++ b/docs/NEXT_ROADMAP.md @@ -131,3 +131,15 @@ - `LlmRuntimeOverrideTests` 보강: - vLLM 암호화 API키 런타임 복호화 검증 - 등록모델/전역 TLS 우회 플래그 합성 규칙 검증 +업데이트: 2026-04-15 08:32 (KST) + +### 이번 배치 마감 상태 +- 에이전틱 루프는 우선순위 큐, tool_result preview 복원, 슬래시 합성 일원화까지 반영되어 `claw-code` 대비 남은 격차가 `루프 세분화/장기 상태 고정` 중심으로 축소됐습니다. +- 문서 포맷은 PPTX/DOCX/XLSX/HTML 모두 실무형 상급 수준까지 올라왔고, 이후 남은 작업은 `slide/workbook/document-level critic`과 `golden regression` 확대 같은 마감 단계입니다. +- 개발언어 지원은 `빠른 선택 + LSP 심화 지원 + 정적 fallback` 구조로 정리됐습니다. 다음 단계는 언어별 실행 힌트 활용 범위를 더 넓히고, no-LSP 환경에서도 분석 품질을 더 끌어올리는 것입니다. + +### 다음 우선순위 +1. `AgentLoopService` 실행 상태 분해와 장기 세션 replacement state 고정 +2. PPTX/XLSX/DOCX/HTML critic/repair loop 최종 마감 +3. 언어별 build/test/lint 힌트의 실제 도구 활용 경로 확대 +4. 릴리즈 게이트와 로드맵 문서 최종 정합화 diff --git a/src/AxCopilot.Tests/Services/AgentQueryContextBuilderTests.cs b/src/AxCopilot.Tests/Services/AgentQueryContextBuilderTests.cs new file mode 100644 index 0000000..82f4316 --- /dev/null +++ b/src/AxCopilot.Tests/Services/AgentQueryContextBuilderTests.cs @@ -0,0 +1,42 @@ +using AxCopilot.Models; +using AxCopilot.Services.Agent; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Services; + +public class AgentQueryContextBuilderTests +{ + [Fact] + public void Build_ShouldPopulateMissingToolResultPreviewBeforeCreatingQueryView() + { + var longContent = new string('Z', 1800); + var sourceMessages = new List + { + new() + { + MsgId = "tool-source-1", + Role = "user", + Content = $$"""{"type":"tool_result","tool_use_id":"call-view","tool_name":"file_read","content":"{{longContent}}"}""", + QueryPreviewContent = """{"type":"tool_result","tool_use_id":"call-view","tool_name":"file_read","content":"preview-view"}""" + }, + new() + { + MsgId = "tool-source-2", + Role = "user", + Content = $$"""{"type":"tool_result","tool_use_id":"call-view","tool_name":"file_read","content":"{{longContent}}"}""" + }, + new() + { + MsgId = "tail-1", + Role = "assistant", + Content = "recent tail" + } + }; + + var result = AgentQueryContextBuilder.Build(sourceMessages); + + sourceMessages[1].QueryPreviewContent.Should().Be(sourceMessages[0].QueryPreviewContent); + result.Messages[1].QueryPreviewContent.Should().Be(sourceMessages[0].QueryPreviewContent); + } +} diff --git a/src/AxCopilot.Tests/Services/AgentToolResultBudgetTests.cs b/src/AxCopilot.Tests/Services/AgentToolResultBudgetTests.cs index e4e4844..202f972 100644 --- a/src/AxCopilot.Tests/Services/AgentToolResultBudgetTests.cs +++ b/src/AxCopilot.Tests/Services/AgentToolResultBudgetTests.cs @@ -98,4 +98,37 @@ public class AgentToolResultBudgetTests result.ReusedPreviewCount.Should().Be(1); queryView[0].Content.Should().Be(sourceMessages[0].QueryPreviewContent); } + + [Fact] + public void Apply_ShouldReusePreviewWithinSameWindow_WhenSourceMessagesAreOmitted() + { + var longContent = new string('C', 1600); + var queryView = new List + { + new() + { + MsgId = "tool-1", + Role = "user", + Content = $$"""{"type":"tool_result","tool_use_id":"call-inline","tool_name":"file_read","content":"{{longContent}}"}""", + QueryPreviewContent = """{"type":"tool_result","tool_use_id":"call-inline","tool_name":"file_read","content":"preview-inline"}""" + }, + new() + { + MsgId = "tool-2", + Role = "user", + Content = $$"""{"type":"tool_result","tool_use_id":"call-inline","tool_name":"file_read","content":"{{longContent}}"}""" + }, + new() + { + MsgId = "tail-1", + Role = "assistant", + Content = "recent tail" + } + }; + + var result = AgentToolResultBudget.Apply(queryView, protectedRecentNonSystemMessages: 1); + + result.ReusedPreviewCount.Should().BeGreaterThan(0); + queryView[1].QueryPreviewContent.Should().Be(queryView[0].QueryPreviewContent); + } } diff --git a/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs b/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs index 667857f..6f5363a 100644 --- a/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs +++ b/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs @@ -46,4 +46,15 @@ public class CodeLanguageCatalogTests CodeLanguageCatalog.BuildQuickSelectSupportDescription().Should().Contain("JavaScript / Vue"); CodeLanguageCatalog.BuildQuickSelectSupportDescription().Should().NotContain("Go"); } + + [Fact] + public void BuildFallbackSummary_ShouldExposeManifestAndBuildHints() + { + var summary = CodeLanguageCatalog.BuildFallbackSummary("sample.go"); + + summary.Should().Contain("정적 fallback 분석: Go"); + summary.Should().Contain("go.mod"); + summary.Should().Contain("go build ./..."); + summary.Should().Contain("go test ./..."); + } } diff --git a/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs b/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs index 122fe09..28ff45d 100644 --- a/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs +++ b/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs @@ -27,6 +27,9 @@ public static class AgentQueryContextBuilder public static AgentQueryContextWindowResult Build(IReadOnlyList sourceMessages) { + if (sourceMessages is IList mutableSourceMessages) + AgentMessageInvariantHelper.PopulateMissingToolResultPreviews(mutableSourceMessages); + if (sourceMessages.Count == 0) { return new AgentQueryContextWindowResult diff --git a/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs b/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs index c9cae5f..4837440 100644 --- a/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs +++ b/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs @@ -27,10 +27,12 @@ public static class AgentToolResultBudget IReadOnlyList? sourceMessages = null) { var result = new AgentToolResultBudgetResult(); + AgentMessageInvariantHelper.PopulateMissingToolResultPreviews(messages); var sourceById = sourceMessages? .Where(message => !string.IsNullOrWhiteSpace(message.MsgId)) .ToDictionary(message => message.MsgId, StringComparer.OrdinalIgnoreCase); - var sourcePreviewByToolResultId = AgentMessageInvariantHelper.BuildToolResultPreviewMap(sourceMessages); + var previewSourceMessages = sourceMessages ?? messages; + var sourcePreviewByToolResultId = AgentMessageInvariantHelper.BuildToolResultPreviewMap(previewSourceMessages); var nonSystemIndexes = messages .Select((message, index) => new { message, index }) .Where(x => !string.Equals(x.message.Role, "system", StringComparison.OrdinalIgnoreCase)) diff --git a/src/AxCopilot/Services/Agent/ContextCondenser.cs b/src/AxCopilot/Services/Agent/ContextCondenser.cs index 2f445c2..f290ece 100644 --- a/src/AxCopilot/Services/Agent/ContextCondenser.cs +++ b/src/AxCopilot/Services/Agent/ContextCondenser.cs @@ -117,6 +117,8 @@ public static class ContextCondenser if (messages.Count < 6) return result; if (!force && !proactiveEnabled) return result; + AgentMessageInvariantHelper.PopulateMissingToolResultPreviews(messages); + // 현재 모델의 입력 토큰 한도 var settings = llm.GetCurrentModelInfo(); // 사용자가 설정한 컨텍스트 크기를 우선 사용. 미설정 시 모델별 기본값 적용. diff --git a/src/AxCopilot/Services/Agent/LspTool.cs b/src/AxCopilot/Services/Agent/LspTool.cs index 017e6cf..51d5527 100644 --- a/src/AxCopilot/Services/Agent/LspTool.cs +++ b/src/AxCopilot/Services/Agent/LspTool.cs @@ -23,6 +23,7 @@ public class LspTool : IAgentTool, IDisposable "- action=\"incoming_calls\": 현재 심볼을 호출하는 상위 호출자를 찾습니다\n" + "- action=\"outgoing_calls\": 현재 심볼이 호출하는 하위 호출 대상을 찾습니다\n" + $"지원 언어 서버: {Services.CodeLanguageCatalog.BuildLspSupportDescription()}\n" + + $"정적 fallback: {Services.CodeLanguageCatalog.BuildFallbackSupportDescription()}\n" + "line/character는 기본적으로 1-based 입력을 기대하며, 0-based 값도 호환 처리합니다."; public ToolParameterSchema Parameters => new() @@ -99,12 +100,16 @@ public class LspTool : IAgentTool, IDisposable // 언어 감지 var language = DetectLanguage(filePath); if (language == null) - return ToolResult.Fail($"지원하지 않는 파일 형식: {Path.GetExtension(filePath)}"); + return ToolResult.Ok( + $"지원하지 않는 LSP 파일 형식: {Path.GetExtension(filePath)}\n\n" + + Services.CodeLanguageCatalog.BuildFallbackSummary(filePath)); // LSP 클라이언트 시작 (캐시) var client = await GetOrCreateClientAsync(language, context.WorkFolder, ct); if (client == null || !client.IsConnected) - return ToolResult.Fail($"{language} 언어 서버를 시작할 수 없습니다. 해당 언어 서버가 설치되어 있는지 확인하세요."); + return ToolResult.Ok( + $"{language} 언어 서버를 시작할 수 없습니다. 로컬 LSP 서버가 없거나 연결되지 않았습니다.\n\n" + + Services.CodeLanguageCatalog.BuildFallbackSummary(filePath)); try { diff --git a/src/AxCopilot/Services/CodeLanguageCatalog.cs b/src/AxCopilot/Services/CodeLanguageCatalog.cs index d24ac8a..87e051b 100644 --- a/src/AxCopilot/Services/CodeLanguageCatalog.cs +++ b/src/AxCopilot/Services/CodeLanguageCatalog.cs @@ -29,6 +29,73 @@ public sealed record CodeLanguageCapability( /// public static class CodeLanguageCatalog { + private static readonly IReadOnlyDictionary s_manifestHints = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["csharp"] = ["*.sln", "*.csproj", "Directory.Build.props", "Directory.Build.targets"], + ["python"] = ["pyproject.toml", "requirements.txt", "setup.py", "environment.yml", "Pipfile"], + ["java"] = ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"], + ["cpp"] = ["CMakeLists.txt", "*.vcxproj", "compile_commands.json", "Makefile"], + ["typescript"] = ["package.json", "tsconfig.json", "pnpm-lock.yaml", "yarn.lock", "package-lock.json"], + ["javascript"] = ["package.json", "vite.config.*", "nuxt.config.*", "pnpm-lock.yaml", "yarn.lock", "package-lock.json"], + ["go"] = ["go.mod", "go.sum"], + ["rust"] = ["Cargo.toml", "Cargo.lock"], + ["php"] = ["composer.json", "composer.lock"], + ["ruby"] = ["Gemfile", "Gemfile.lock", "*.gemspec"], + ["kotlin"] = ["build.gradle.kts", "settings.gradle.kts", "gradle.properties"], + ["swift"] = ["Package.swift", "*.xcodeproj", "*.xcworkspace"], + }; + + private static readonly IReadOnlyDictionary s_buildHints = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["csharp"] = ["dotnet build", "dotnet msbuild"], + ["python"] = ["python -m py_compile", "python -m build"], + ["java"] = ["mvn compile", "gradle build"], + ["cpp"] = ["cmake --build .", "msbuild", "make"], + ["typescript"] = ["npm run build", "pnpm build", "tsc -p ."], + ["javascript"] = ["npm run build", "pnpm build", "vite build"], + ["go"] = ["go build ./..."], + ["rust"] = ["cargo build"], + ["php"] = ["composer install --dry-run", "php -l"], + ["ruby"] = ["bundle exec rake", "ruby -c"], + ["kotlin"] = ["gradle build", "./gradlew build"], + ["swift"] = ["swift build", "xcodebuild build"], + }; + + private static readonly IReadOnlyDictionary s_testHints = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["csharp"] = ["dotnet test"], + ["python"] = ["pytest", "python -m unittest"], + ["java"] = ["mvn test", "gradle test"], + ["cpp"] = ["ctest", "cmake --build . --target test"], + ["typescript"] = ["npm test", "pnpm test", "vitest", "jest"], + ["javascript"] = ["npm test", "pnpm test", "vitest", "jest"], + ["go"] = ["go test ./..."], + ["rust"] = ["cargo test"], + ["php"] = ["phpunit", "composer test"], + ["ruby"] = ["bundle exec rspec", "bundle exec rake test"], + ["kotlin"] = ["gradle test", "./gradlew test"], + ["swift"] = ["swift test", "xcodebuild test"], + }; + + private static readonly IReadOnlyDictionary s_lintHints = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["csharp"] = ["dotnet format", "dotnet build /warnaserror"], + ["python"] = ["ruff check", "black --check", "flake8"], + ["cpp"] = ["clang-format --dry-run", "clang-tidy"], + ["typescript"] = ["npm run lint", "pnpm lint", "eslint ."], + ["javascript"] = ["npm run lint", "pnpm lint", "eslint ."], + ["go"] = ["gofmt -w", "golangci-lint run"], + ["rust"] = ["cargo fmt --check", "cargo clippy"], + ["php"] = ["php -l", "phpcs"], + ["ruby"] = ["rubocop", "standardrb"], + ["kotlin"] = ["./gradlew ktlintCheck", "./gradlew detekt"], + ["swift"] = ["swiftformat --lint", "swiftlint"], + }; + private static readonly ReadOnlyCollection s_all = new(new List { @@ -313,4 +380,41 @@ public static class CodeLanguageCatalog sb.Append(BuildLspSupportDescription()); return sb.ToString(); } + public static string BuildFallbackSupportDescription() + => "LSP 서버가 없거나 연결되지 않아도 확장자, 매니페스트, build/test/lint 힌트 기반의 정적 분석을 계속 제공합니다."; + + public static string BuildFallbackSummary(string? filePathOrExtension) + { + CodeLanguageCapability? capability = null; + if (!string.IsNullOrWhiteSpace(filePathOrExtension)) + { + capability = filePathOrExtension.StartsWith('.') + ? FindByExtension(filePathOrExtension) + : FindByExtension(Path.GetExtension(filePathOrExtension)); + } + + if (capability == null) + return "정적 fallback: 확장자와 프로젝트 매니페스트를 먼저 확인하고 관련 build/test 명령 힌트를 따라 수동 검증하세요."; + + var sb = new StringBuilder(); + sb.AppendLine($"정적 fallback 분석: {capability.DisplayName}"); + + if (s_manifestHints.TryGetValue(capability.Key, out var manifests) && manifests.Length > 0) + sb.AppendLine("주요 매니페스트: " + string.Join(", ", manifests)); + + if (s_buildHints.TryGetValue(capability.Key, out var buildHints) && buildHints.Length > 0) + sb.AppendLine("권장 build 힌트: " + string.Join(" | ", buildHints)); + + if (s_testHints.TryGetValue(capability.Key, out var testHints) && testHints.Length > 0) + sb.AppendLine("권장 test 힌트: " + string.Join(" | ", testHints)); + + if (s_lintHints.TryGetValue(capability.Key, out var lintHints) && lintHints.Length > 0) + sb.AppendLine("권장 lint/format 힌트: " + string.Join(" | ", lintHints)); + + var primaryGuidance = capability.Guidance.FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(primaryGuidance)) + sb.Append("분석 포인트: ").Append(primaryGuidance); + + return sb.ToString().TrimEnd(); + } } diff --git a/src/AxCopilot/ViewModels/SettingsViewModel.cs b/src/AxCopilot/ViewModels/SettingsViewModel.cs index 29f5b41..25fb0ea 100644 --- a/src/AxCopilot/ViewModels/SettingsViewModel.cs +++ b/src/AxCopilot/ViewModels/SettingsViewModel.cs @@ -141,6 +141,7 @@ public class SettingsViewModel : INotifyPropertyChanged public string CodeQuickSelectLanguagesText => CodeLanguageCatalog.BuildQuickSelectSupportDescription(); public string CodeLspSupportedLanguagesText => CodeLanguageCatalog.BuildLspSupportDescription(); public string CodeStaticSupportedLanguagesText => CodeLanguageCatalog.BuildStaticSupportDescription(); + public string CodeFallbackSupportText => CodeLanguageCatalog.BuildFallbackSupportDescription(); public string CodeTabSupportSummaryText => CodeLanguageCatalog.BuildSupportSummaryDescription(); // ─── 작업 복사본 ─────────────────────────────────────────────────────── diff --git a/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs b/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs index d0cfb0d..e4c5e43 100644 --- a/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs +++ b/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs @@ -289,6 +289,7 @@ public partial class ChatWindow sb.AppendLine("\n## Language Guidelines"); 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."); // 코드 품질 + 안전 수칙 sb.AppendLine("\n## Code Quality & Safety"); diff --git a/src/AxCopilot/Views/SettingsWindow.xaml b/src/AxCopilot/Views/SettingsWindow.xaml index e015821..76d1b64 100644 --- a/src/AxCopilot/Views/SettingsWindow.xaml +++ b/src/AxCopilot/Views/SettingsWindow.xaml @@ -5469,6 +5469,7 @@ +