diff --git a/README.md b/README.md index 7f40e52..4e258a9 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,14 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저 - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "SkillServiceRuntimePolicyTests|SlashCommandCatalogTests|McpSkillCatalogTests" -p:OutputPath=bin\\verify_phase4_tests\\ -p:IntermediateOutputPath=obj\\verify_phase4_tests\\` 통과 17 - 참고: 테스트 프로젝트의 기존 파일 `src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs(76)` nullable 경고 1건은 유지됩니다. +- 업데이트: 2026-04-14 19:50 (KST) +- AX Agent의 내부 실행 품질을 `claude-code` 기준으로 한 단계 더 끌어올렸습니다. 실행 중 추가 입력은 [AgentCommandQueue.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentCommandQueue.cs)에서 우선순위와 인터럽트 여부를 함께 보존하고, [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)는 이를 주기적으로 소진해 대화 중 새 지시가 들어와도 더 안정적으로 반영합니다. +- 컨텍스트 관리도 보강했습니다. [AgentToolResultBudget.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs), [AgentQueryContextBuilder.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs), [ChatModels.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/ChatModels.cs)가 tool result preview를 대화 메시지에 캐시해 긴 세션과 재질문에서도 같은 축약 결과를 재사용하도록 정리했습니다. +- 코드 탭의 언어 지원도 내장 중심으로 넓혔습니다. [CodeLanguageCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/CodeLanguageCatalog.cs)를 추가해 코드 분류, 시스템 프롬프트, LSP 언어 판정, 인덱싱 확장자를 한 카탈로그로 묶었고, 설정의 코드 탭에는 `지원 언어(LSP)`와 `코드 탭 기본 지원`을 명시적으로 노출했습니다. 격리 환경에서는 외부 설치를 전제하지 않고 내장 분석과 로컬에 이미 있는 언어 서버만 활용합니다. +- PPT 생성 품질도 크게 올렸습니다. [PptxSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PptxSkill.cs)에 `executive_summary`, `recommendation`, `roadmap`, `comparison`, `kpi_dashboard` 레이아웃을 추가했고, [DocumentPlannerTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DocumentPlannerTool.cs)는 발표용 문서 계획을 `Executive Summary -> Situation & Imperative -> Key Findings -> Options & Recommendation -> Implementation Roadmap -> Impact & Ask` 구조로 생성하도록 바꿨습니다. [pptx-creator.skill.md](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/skills/pptx-creator.skill.md)도 Python 우선 흐름에서 AX native `pptx_create` 중심으로 재작성했습니다. +- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_impl\\ -p:IntermediateOutputPath=obj\\verify_impl\\` 경고 0 / 오류 0 +- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "CodeLanguageCatalogTests|AgentCommandQueueTests|AgentToolResultBudgetTests|DocumentPlannerPresentationTests|PptxSkillConsultingDeckTests" -p:OutputPath=bin\\verify_impl_tests\\ -p:IntermediateOutputPath=obj\\verify_impl_tests\\` 통과 15 + - 업데이트: 2026-04-14 19:16 (KST) - 분석용 로그 저장 방식을 롤링 형태로 정리했습니다. `app`, `perf`, `audit`, `workflow` 로그는 이제 날짜별 파일을 유지하되 각 파일이 최대 1MB를 넘지 않도록 오래된 내용부터 밀어내며 새 로그를 이어 붙입니다. - 보관 정책도 같이 정리했습니다. 공통 로그/성능 로그/감사 로그는 14일까지만 유지하고, 워크플로우 상세 로그는 기존 설정값을 따르되 최대 14일을 넘지 않게 제한합니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index d726782..9905d0a 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,861 +1,835 @@ -# AX Copilot - 개발 문서 +업데이트: 2026-04-14 19:50 (KST) +- Agent loop/queue/context 품질을 보강했습니다. `src/AxCopilot/Services/Agent/AgentCommandQueue.cs`로 실행 중 추가 입력을 우선순위와 interrupt 여부까지 포함해 관리하고, `AgentLoopService`는 이를 안전하게 반영합니다. +- `AgentToolResultBudget`, `AgentQueryContextBuilder`, `ChatModels`는 tool result preview를 메시지에 캐시해 긴 세션과 재질문에서도 같은 축약 결과를 재사용하도록 정리했습니다. +- 코드 탭의 내장 언어 지원을 `src/AxCopilot/Services/CodeLanguageCatalog.cs`로 통합했고, 설정의 코드 탭에 지원 언어(LSP)와 코드 탭 기본 지원 언어를 명시적으로 표시합니다. +- `PptxSkill`에 `executive_summary`, `recommendation`, `roadmap`, `comparison`, `kpi_dashboard` 레이아웃을 추가했고, `DocumentPlannerTool`은 발표 자료 계획을 `Executive Summary -> Situation & Imperative -> Key Findings -> Options & Recommendation -> Implementation Roadmap -> Impact & Ask` 구조로 생성합니다. `pptx-creator.skill.md`도 AX native `pptx_create` 중심으로 재작성했습니다. +- 테스트: `CodeLanguageCatalogTests`, `AgentCommandQueueTests`, `AgentToolResultBudgetTests`, `DocumentPlannerPresentationTests`, `PptxSkillConsultingDeckTests` 추가 및 기존 `WorkspaceContextGeneratorTests.cs(76)` nullable 경고 수정 +- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_impl\\ -p:IntermediateOutputPath=obj\\verify_impl\\` 경고 0 / 오류 0 +- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "CodeLanguageCatalogTests|AgentCommandQueueTests|AgentToolResultBudgetTests|DocumentPlannerPresentationTests|PptxSkillConsultingDeckTests" -p:OutputPath=bin\\verify_impl_tests\\ -p:IntermediateOutputPath=obj\\verify_impl_tests\\` 통과 15 -> 최종 업데이트: 2026-04-14 19:13 (KST) · 버전 0.7.3 +--- +# AX Copilot - 媛쒕컻 臾몄꽌 -## 업데이트 로그 +> 理쒖쥌 ?낅뜲?댄듃: 2026-04-14 19:13 (KST) 쨌 踰꾩쟾 0.7.3 -- 업데이트: 2026-04-14 19:13 (KST) -- `claude-code` 기준 Phase 4를 이어서 반영했습니다. `src/AxCopilot/Services/Agent/McpSkillCatalog.cs`를 추가해 MCP 서버 메타데이터를 `mcp` source scope의 synthetic skill로 변환하고, `ToolRegistry.RegisterMcpToolsAsync()` 이후 snapshot을 갱신하도록 연결했습니다. -- `src/AxCopilot/Services/Agent/SkillService.cs`는 source policy를 `managed/user/additional/project/plugin/mcp/legacy` 단위로 판단하도록 확장했고, source 우선순위 기반 dedupe와 inline shell trust boundary를 함께 적용합니다. plugin-only mode가 켜져 있으면 managed/plugin/bundled만 유지하고 나머지 source는 숨깁니다. -- 슬래시 명령 합성은 `src/AxCopilot/Views/SlashCommandCatalog.cs`와 `src/AxCopilot/Views/ChatWindow.xaml.cs`에서 재구성했습니다. builtin command와 skill을 공통 priority로 합성해 충돌 시 한 항목만 노출하고, builtin `/review` 같은 예약 명령이 project skill보다 안정적으로 우선합니다. -- 설정/UI는 `src/AxCopilot/Views/SettingsWindow.xaml`, `src/AxCopilot/Views/AgentSettingsWindow.xaml`, `src/AxCopilot/Views/AgentSettingsWindow.xaml.cs`, `src/AxCopilot/Views/SkillGalleryWindow.xaml.cs`에 연결했습니다. MCP 스킬 source 토글, plugin-only mode, source별 inline shell 허용 범위, MCP 카테고리/배지, synthetic skill의 파일 액션 차단을 함께 반영했습니다. -- 테스트는 `src/AxCopilot.Tests/Services/SkillServiceRuntimePolicyTests.cs`, `src/AxCopilot.Tests/Services/McpSkillCatalogTests.cs`, `src/AxCopilot.Tests/Views/SlashCommandCatalogTests.cs`에 추가했습니다. -- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase4\\ -p:IntermediateOutputPath=obj\\verify_phase4\\` 경고 0 / 오류 0 -- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "SkillServiceRuntimePolicyTests|SlashCommandCatalogTests|McpSkillCatalogTests" -p:OutputPath=bin\\verify_phase4_tests\\ -p:IntermediateOutputPath=obj\\verify_phase4_tests\\` 통과 17 -- 참고: 테스트 빌드 중 기존 파일 `src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs(76)`의 nullable 경고 1건은 유지됩니다. +## ?낅뜲?댄듃 濡쒓렇 -- 업데이트: 2026-04-14 17:46 (KST) -- 도구 이름 정합성 문제를 줄이기 위해 `src/AxCopilot/Services/Agent/AgentToolCatalog.cs`를 추가했습니다. canonical id, legacy alias, 탭 노출, 설정 카테고리, 병렬 read-only 분류를 한곳에서 관리하도록 정리했습니다. -- `ToolRegistry`, `AgentLoopService`, `AgentLoopParallelExecution`, `IAgentTool`, `AgentHookRunner`, `SkillService`가 모두 같은 카탈로그를 사용하도록 연결했습니다. 이에 따라 `git/lsp/zip/project_rule/snippet_run` 같은 예전 이름도 런타임에서 자동 정규화됩니다. -- 내부설정 연동도 함께 반영했습니다. `AgentSettingsWindow`와 `SettingsWindow`의 도구 카드, 훅 편집기, 비활성 도구 저장, 도구 권한 저장이 canonical 이름 기준으로 동작하며 기존 저장값은 alias 호환으로 흡수합니다. -- 스킬 관련 설명은 현재 구조에 맞게 완화했습니다. 기본 스킬 폴더와 추가 폴더를 함께 로드하는 흐름, 직접 호출 스킬과 런타임 정책 연결 스킬을 같이 보여주는 방향으로 설정/헬프 문구를 보정했습니다. -- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_toolcat\\ -p:IntermediateOutputPath=obj\\verify_toolcat\\` 경고 0 / 오류 0 -- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentToolCatalogTests -p:OutputPath=bin\\verify_toolcat_tests\\ -p:IntermediateOutputPath=obj\\verify_toolcat_tests\\` 통과 8 -- 참고: 테스트 빌드 중 기존 파일 `src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs`의 nullable 경고 1건이 함께 표시되었으나, 이번 변경에서 새 경고를 추가하지는 않았습니다. +- ?낅뜲?댄듃: 2026-04-14 19:13 (KST) +- `claude-code` 湲곗? Phase 4瑜??댁뼱??諛섏쁺?덉뒿?덈떎. `src/AxCopilot/Services/Agent/McpSkillCatalog.cs`瑜?異붽???MCP ?쒕쾭 硫뷀??곗씠?곕? `mcp` source scope??synthetic skill濡?蹂€?섑븯怨? `ToolRegistry.RegisterMcpToolsAsync()` ?댄썑 snapshot??媛깆떊?섎룄濡??곌껐?덉뒿?덈떎. +- `src/AxCopilot/Services/Agent/SkillService.cs`??source policy瑜?`managed/user/additional/project/plugin/mcp/legacy` ?⑥쐞濡??먮떒?섎룄濡??뺤옣?덇퀬, source ?곗꽑?쒖쐞 湲곕컲 dedupe?€ inline shell trust boundary瑜??④퍡 ?곸슜?⑸땲?? plugin-only mode媛€ 耳쒖졇 ?덉쑝硫?managed/plugin/bundled留??좎??섍퀬 ?섎㉧吏€ source???④퉩?덈떎. +- ?щ옒??紐낅졊 ?⑹꽦?€ `src/AxCopilot/Views/SlashCommandCatalog.cs`?€ `src/AxCopilot/Views/ChatWindow.xaml.cs`?먯꽌 ?ш뎄?깊뻽?듬땲?? builtin command?€ skill??怨듯넻 priority濡??⑹꽦??異⑸룎 ??????ぉ留??몄텧?섍퀬, builtin `/review` 媛숈? ?덉빟 紐낅졊??project skill蹂대떎 ?덉젙?곸쑝濡??곗꽑?⑸땲?? +- ?ㅼ젙/UI??`src/AxCopilot/Views/SettingsWindow.xaml`, `src/AxCopilot/Views/AgentSettingsWindow.xaml`, `src/AxCopilot/Views/AgentSettingsWindow.xaml.cs`, `src/AxCopilot/Views/SkillGalleryWindow.xaml.cs`???곌껐?덉뒿?덈떎. MCP ?ㅽ궗 source ?좉?, plugin-only mode, source蹂?inline shell ?덉슜 踰붿쐞, MCP 移댄뀒怨좊━/諛곗?, synthetic skill???뚯씪 ?≪뀡 李⑤떒???④퍡 諛섏쁺?덉뒿?덈떎. +- ?뚯뒪?몃뒗 `src/AxCopilot.Tests/Services/SkillServiceRuntimePolicyTests.cs`, `src/AxCopilot.Tests/Services/McpSkillCatalogTests.cs`, `src/AxCopilot.Tests/Views/SlashCommandCatalogTests.cs`??異붽??덉뒿?덈떎. +- 寃€利? `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase4\\ -p:IntermediateOutputPath=obj\\verify_phase4\\` 寃쎄퀬 0 / ?ㅻ쪟 0 +- 寃€利? `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "SkillServiceRuntimePolicyTests|SlashCommandCatalogTests|McpSkillCatalogTests" -p:OutputPath=bin\\verify_phase4_tests\\ -p:IntermediateOutputPath=obj\\verify_phase4_tests\\` ?듦낵 17 +- 李멸퀬: ?뚯뒪??鍮뚮뱶 以?湲곗〈 ?뚯씪 `src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs(76)`??nullable 寃쎄퀬 1嫄댁? ?좎??⑸땲?? + +- ?낅뜲?댄듃: 2026-04-14 17:46 (KST) +- ?꾧뎄 ?대쫫 ?뺥빀??臾몄젣瑜?以꾩씠湲??꾪빐 `src/AxCopilot/Services/Agent/AgentToolCatalog.cs`瑜?異붽??덉뒿?덈떎. canonical id, legacy alias, ???몄텧, ?ㅼ젙 移댄뀒怨좊━, 蹂묐젹 read-only 遺꾨쪟瑜??쒓납?먯꽌 愿€由ы븯?꾨줉 ?뺣━?덉뒿?덈떎. +- `ToolRegistry`, `AgentLoopService`, `AgentLoopParallelExecution`, `IAgentTool`, `AgentHookRunner`, `SkillService`媛€ 紐⑤몢 媛숈? 移댄깉濡쒓렇瑜??ъ슜?섎룄濡??곌껐?덉뒿?덈떎. ?댁뿉 ?곕씪 `git/lsp/zip/project_rule/snippet_run` 媛숈? ?덉쟾 ?대쫫???고??꾩뿉???먮룞 ?뺢퇋?붾맗?덈떎. +- ?대??ㅼ젙 ?곕룞???④퍡 諛섏쁺?덉뒿?덈떎. `AgentSettingsWindow`?€ `SettingsWindow`???꾧뎄 移대뱶, ???몄쭛湲? 鍮꾪솢???꾧뎄 ?€?? ?꾧뎄 沅뚰븳 ?€?μ씠 canonical ?대쫫 湲곗??쇰줈 ?숈옉?섎ʼn 湲곗〈 ?€?κ컪?€ alias ?명솚?쇰줈 ?≪닔?⑸땲?? +- ?ㅽ궗 愿€???ㅻ챸?€ ?꾩옱 援ъ“??留욊쾶 ?꾪솕?덉뒿?덈떎. 湲곕낯 ?ㅽ궗 ?대뜑?€ 異붽? ?대뜑瑜??④퍡 濡쒕뱶?섎뒗 ?먮쫫, 吏곸젒 ?몄텧 ?ㅽ궗怨??고????뺤콉 ?곌껐 ?ㅽ궗??媛숈씠 蹂댁뿬二쇰뒗 諛⑺뼢?쇰줈 ?ㅼ젙/?ы봽 臾멸뎄瑜?蹂댁젙?덉뒿?덈떎. +- 寃€利? `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_toolcat\\ -p:IntermediateOutputPath=obj\\verify_toolcat\\` 寃쎄퀬 0 / ?ㅻ쪟 0 +- 寃€利? `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentToolCatalogTests -p:OutputPath=bin\\verify_toolcat_tests\\ -p:IntermediateOutputPath=obj\\verify_toolcat_tests\\` ?듦낵 8 +- 李멸퀬: ?뚯뒪??鍮뚮뱶 以?湲곗〈 ?뚯씪 `src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs`??nullable 寃쎄퀬 1嫄댁씠 ?④퍡 ?쒖떆?섏뿀?쇰굹, ?대쾲 蹂€寃쎌뿉????寃쎄퀬瑜?異붽??섏????딆븯?듬땲?? --- -## 1. 프로젝트 개요 +## 1. ?꾨줈?앺듃 媛쒖슂 -AX Copilot은 Windows용 생산성 런처 + AI 에이전트 데스크톱 앱입니다. -- **런처**: Alfred/Raycast 스타일의 퍼지 검색, 명령 실행, 위젯 -- **에이전트**: LLM 기반 대화형 코드/문서 작업 자동화 (도구 호출 루프) -- **독 바**: 시스템 리소스, 클립보드, 스크린샷 등 빠른 접근 +AX Copilot?€ Windows???앹궛???곗쿂 + AI ?먯씠?꾪듃 ?곗뒪?ы넲 ?깆엯?덈떎. +- **?곗쿂**: Alfred/Raycast ?ㅽ??쇱쓽 ?쇱? 寃€?? 紐낅졊 ?ㅽ뻾, ?꾩젽 +- **?먯씠?꾪듃**: LLM 湲곕컲 ?€?뷀삎 肄붾뱶/臾몄꽌 ?묒뾽 ?먮룞??(?꾧뎄 ?몄텧 猷⑦봽) +- **??諛?*: ?쒖뒪??由ъ냼?? ?대┰蹂대뱶, ?ㅽ겕由곗꺑 ??鍮좊Ⅸ ?묎렐 --- -## 2. 기술 스택 +## 2. 湲곗닠 ?ㅽ깮 -| 항목 | 값 | +| ??ぉ | 媛?| |------|-----| -| 프레임워크 | .NET 8 (net8.0-windows10.0.17763.0) | -| UI | WPF + Windows Forms (하이브리드) | -| 언어 | C# 12 | -| 패턴 | MVVM, 이벤트 기반, 싱글톤 서비스 | -| 테스트 | xUnit 2.9 + FluentAssertions 6.12 | -| 빌드 | dotnet CLI, PublishSingleFile | +| ?꾨젅?꾩썙??| .NET 8 (net8.0-windows10.0.17763.0) | +| UI | WPF + Windows Forms (?섏씠釉뚮━?? | +| ?몄뼱 | C# 12 | +| ?⑦꽩 | MVVM, ?대깽??湲곕컲, ?깃????쒕퉬??| +| ?뚯뒪??| xUnit 2.9 + FluentAssertions 6.12 | +| 鍮뚮뱶 | dotnet CLI, PublishSingleFile | -### 주요 NuGet 패키지 +### 二쇱슂 NuGet ?⑦궎吏€ -| 패키지 | 용도 | +| ?⑦궎吏€ | ?⑸룄 | |--------|------| -| DocumentFormat.OpenXml 3.2.0 | DOCX/XLSX/PPTX 생성 | -| Markdig 0.37.0 | Markdown → HTML 렌더링 | -| Microsoft.Data.Sqlite 8.0 | SQLite (대화 저장소) | -| Microsoft.Web.WebView2 | HTML 미리보기, 가이드 뷰어 | -| QRCoder 1.6.0 | QR 코드 생성 | -| System.Security.Cryptography.ProtectedData | DPAPI 암호화 | -| UglyToad.PdfPig | PDF 읽기 | +| DocumentFormat.OpenXml 3.2.0 | DOCX/XLSX/PPTX ?앹꽦 | +| Markdig 0.37.0 | Markdown ??HTML ?뚮뜑留?| +| Microsoft.Data.Sqlite 8.0 | SQLite (?€???€?μ냼) | +| Microsoft.Web.WebView2 | HTML 誘몃━蹂닿린, 媛€?대뱶 酉곗뼱 | +| QRCoder 1.6.0 | QR 肄붾뱶 ?앹꽦 | +| System.Security.Cryptography.ProtectedData | DPAPI ?뷀샇??| +| UglyToad.PdfPig | PDF ?쎄린 | --- -## 3. 솔루션 구조 +## 3. ?붾(??援ъ“ ``` src/ -├── AxCopilot/ # 메인 WPF 앱 (v0.7.3) -│ ├── Assets/ # 아이콘, 프리셋 JSON, 암호화된 가이드, 마스코트 -│ ├── Core/ # FuzzyEngine, CommandResolver, InputListener, PluginHost -│ ├── Handlers/ # 136개 빌트인 명령 핸들러 -│ ├── Models/ # AppSettings, ChatModels, McpSettings -│ ├── Security/ # AntiTamper (디버거/디컴파일러 탐지) -│ ├── Services/ # 60개 서비스 -│ │ └── Agent/ # 에이전트 루프 + 114개 도구 -│ ├── Themes/ # 9개 테마 (Dark, Light, OLED, Nord, Monokai 등) -│ ├── ViewModels/ # LauncherViewModel, SettingsViewModel, StatisticsViewModel -│ └── Views/ # 30개 XAML 윈도우 -├── AxCopilot.SDK/ # 플러그인 SDK (IActionHandler 인터페이스) -├── AxCopilot.Installer/ # Windows Forms 설치 프로그램 (.NET Framework 4.8) -├── AxCopilot.Tests/ # xUnit 단위/통합 테스트 -└── AxKeyEncryptor/ # API 키 DPAPI 암호화 유틸리티 +?쒋??€ AxCopilot/ # 硫붿씤 WPF ??(v0.7.3) +?? ?쒋??€ Assets/ # ?꾩씠肄? ?꾨━??JSON, ?뷀샇?붾맂 媛€?대뱶, 留덉뒪肄뷀듃 +?? ?쒋??€ Core/ # FuzzyEngine, CommandResolver, InputListener, PluginHost +?? ?쒋??€ Handlers/ # 136媛?鍮뚰듃??紐낅졊 ?몃뱾???? ?쒋??€ Models/ # AppSettings, ChatModels, McpSettings +?? ?쒋??€ Security/ # AntiTamper (?붾쾭嫄??붿뺨?뚯씪???먯?) +?? ?쒋??€ Services/ # 60媛??쒕퉬???? ?? ?붴??€ Agent/ # ?먯씠?꾪듃 猷⑦봽 + 114媛??꾧뎄 +?? ?쒋??€ Themes/ # 9媛??뚮쭏 (Dark, Light, OLED, Nord, Monokai ?? +?? ?쒋??€ ViewModels/ # LauncherViewModel, SettingsViewModel, StatisticsViewModel +?? ?붴??€ Views/ # 30媛?XAML ?덈룄???쒋??€ AxCopilot.SDK/ # ?뚮윭洹몄씤 SDK (IActionHandler ?명꽣?섏씠?? +?쒋??€ AxCopilot.Installer/ # Windows Forms ?ㅼ튂 ?꾨줈洹몃옩 (.NET Framework 4.8) +?쒋??€ AxCopilot.Tests/ # xUnit ?⑥쐞/?듯빀 ?뚯뒪???붴??€ AxKeyEncryptor/ # API ??DPAPI ?뷀샇???좏떥由ы떚 ``` --- -## 4. 앱 시작 흐름 (App.xaml.cs) +## 4. ???쒖옉 ?먮쫫 (App.xaml.cs) ``` OnStartup() - ├─ AntiTamper 디버거 감지 (Release 빌드) - ├─ 단일 인스턴스 뮤텍스 확인 - ├─ SettingsService 초기화 + 설정 로드 - ├─ ChatStorageService 보관 정책 실행 (만료 대화 정리) - ├─ L10n 언어 초기화 - ├─ 서비스 초기화 - │ ├─ AgentMemoryService - │ ├─ ChatSessionStateService - │ ├─ AppStateService - │ ├─ IndexService (백그라운드 파일 인덱싱) - │ ├─ FuzzyEngine + CommandResolver - │ ├─ ContextManager - │ ├─ SessionTrackingService - │ ├─ WorktimeReminderService - │ └─ ClipboardHistoryService - ├─ 빌트인 핸들러 등록 (136개) - ├─ SchedulerService + PluginHost 초기화 - ├─ InputListener 시작 (글로벌 핫키) - └─ 런처/설정/트레이 윈도우 생성 + ?쒋? AntiTamper ?붾쾭嫄?媛먯? (Release 鍮뚮뱶) + ?쒋? ?⑥씪 ?몄뒪?댁뒪 裕ㅽ뀓???뺤씤 + ?쒋? SettingsService 珥덇린??+ ?ㅼ젙 濡쒕뱶 + ?쒋? ChatStorageService 蹂닿? ?뺤콉 ?ㅽ뻾 (留뚮즺 ?€???뺣━) + ?쒋? L10n ?몄뼱 珥덇린?? ?쒋? ?쒕퉬??珥덇린?? ?? ?쒋? AgentMemoryService + ?? ?쒋? ChatSessionStateService + ?? ?쒋? AppStateService + ?? ?쒋? IndexService (諛깃렇?쇱슫???뚯씪 ?몃뜳?? + ?? ?쒋? FuzzyEngine + CommandResolver + ?? ?쒋? ContextManager + ?? ?쒋? SessionTrackingService + ?? ?쒋? WorktimeReminderService + ?? ?붴? ClipboardHistoryService + ?쒋? 鍮뚰듃???몃뱾???깅줉 (136媛? + ?쒋? SchedulerService + PluginHost 珥덇린?? ?쒋? InputListener ?쒖옉 (湲€濡쒕쾶 ?ロ궎) + ?붴? ?곗쿂/?ㅼ젙/?몃젅???덈룄???앹꽦 ``` --- -## 5. 핵심 아키텍처 +## 5. ?듭떖 ?꾪궎?띿쿂 -### 5.1 런처 (Launcher) +### 5.1 ?곗쿂 (Launcher) -**검색 파이프라인**: 사용자 입력 → `CommandResolver` (접두어 매칭) → `FuzzyEngine` (퍼지 검색) → 결과 정렬 → UI 렌더링 +**寃€???뚯씠?꾨씪??*: ?ъ슜???낅젰 ??`CommandResolver` (?묐몢??留ㅼ묶) ??`FuzzyEngine` (?쇱? 寃€?? ??寃곌낵 ?뺣젹 ??UI ?뚮뜑留? +- `FuzzyEngine`: ?뚯씪 ?몃뜳??湲곕컲 ?쇱? 留ㅼ묶, ?먯닔 ?쒖쐞 +- `CommandResolver`: ?몃뱾???쇱슦??(?묐몢??`@`, `!`, `#`, `~`, `>`, `$` ?? +- `IndexService`: 諛깃렇?쇱슫???뚯씪 ?몃뜳??(`.git`, `node_modules` ???쒖쇅) -- `FuzzyEngine`: 파일 인덱스 기반 퍼지 매칭, 점수 순위 -- `CommandResolver`: 핸들러 라우팅 (접두어 `@`, `!`, `#`, `~`, `>`, `$` 등) -- `IndexService`: 백그라운드 파일 인덱싱 (`.git`, `node_modules` 등 제외) - -**위젯**: 성능 모니터, 포모도로, 메모, 날씨, 캘린더, 배터리 - -### 5.2 에이전트 (Agent Loop) +**?꾩젽**: ?깅뒫 紐⑤땲?? ?щえ?꾨줈, 硫붾え, ?좎뵪, 罹섎┛?? 諛고꽣由? +### 5.2 ?먯씠?꾪듃 (Agent Loop) ``` -사용자 메시지 - → LlmService.StreamAsync() (LLM API 호출) - → 응답 스트리밍 수신 - → 도구 호출 감지 시: - → ToolRegistry에서 도구 조회 - → 권한 확인 (AskPermissionCallback) - → 도구 실행 - → 결과를 컨텍스트에 추가 - → LLM 재호출 (반복) - → 최종 텍스트 응답 반환 +?ъ슜??硫붿떆吏€ + ??LlmService.StreamAsync() (LLM API ?몄텧) + ???묐떟 ?ㅽ듃由щ컢 ?섏떊 + ???꾧뎄 ?몄텧 媛먯? ?? + ??ToolRegistry?먯꽌 ?꾧뎄 議고쉶 + ??沅뚰븳 ?뺤씤 (AskPermissionCallback) + ???꾧뎄 ?ㅽ뻾 + ??寃곌낵瑜?而⑦뀓?ㅽ듃??異붽? + ??LLM ?ы샇異?(諛섎났) + ??理쒖쥌 ?띿뒪???묐떟 諛섑솚 ``` -**핵심 클래스**: -- `AgentLoopService` — 루프 엔진 (반복, 일시정지/재개, 이벤트 발행) -- `AxAgentExecutionEngine` — 도구 실행 조율 -- `AgentLoopParallelExecution` — 병렬 도구 실행 -- `AgentLoopTransitions` / `.Execution` — 상태 전이 로직 -- `ToolRegistry` — 도구 등록/조회 -- `ContextCondenser` — 컨텍스트 압축 (토큰 관리) +**?듭떖 ?대옒??*: +- `AgentLoopService` ??猷⑦봽 ?붿쭊 (諛섎났, ?쇱떆?뺤?/?ш컻, ?대깽??諛쒗뻾) +- `AxAgentExecutionEngine` ???꾧뎄 ?ㅽ뻾 議곗쑉 +- `AgentLoopParallelExecution` ??蹂묐젹 ?꾧뎄 ?ㅽ뻾 +- `AgentLoopTransitions` / `.Execution` ???곹깭 ?꾩씠 濡쒖쭅 +- `ToolRegistry` ???꾧뎄 ?깅줉/議고쉶 +- `ContextCondenser` ??而⑦뀓?ㅽ듃 ?뺤텞 (?좏겙 愿€由? -**도구 카테고리** (114개): -| 카테고리 | 예시 | +**?꾧뎄 移댄뀒怨좊━** (114媛?: +| 移댄뀒怨좊━ | ?덉떆 | |---------|------| -| 파일 I/O | FileReadTool, FileEditTool, FileManageTool, FileWriteTool | -| 검색 | GlobTool, GrepTool, CodeSearchTool, FileSearchTool | -| 문서 | DocumentReaderTool, ExcelSkill, DocxSkill, PptxSkill, CsvSkill, HtmlSkill | -| 코드 | BuildRunTool, SnippetRunnerTool, CodeReviewTool, TestLoopTool, LspTool | -| 데이터 | JsonTool, XmlTool, SqlTool, DataPivotTool, RegexTool | -| 시스템 | ProcessTool, EnvTool, ZipTool, ClipboardTool | -| 계획/추적 | TodoWriteTool, TaskTrackerTool, CheckpointTool, PlaybookTool | -| 사용자 | UserAskTool, SuggestActionsTool, NotifyTool | +| ?뚯씪 I/O | FileReadTool, FileEditTool, FileManageTool, FileWriteTool | +| 寃€??| GlobTool, GrepTool, CodeSearchTool, FileSearchTool | +| 臾몄꽌 | DocumentReaderTool, ExcelSkill, DocxSkill, PptxSkill, CsvSkill, HtmlSkill | +| 肄붾뱶 | BuildRunTool, SnippetRunnerTool, CodeReviewTool, TestLoopTool, LspTool | +| ?곗씠??| JsonTool, XmlTool, SqlTool, DataPivotTool, RegexTool | +| ?쒖뒪??| ProcessTool, EnvTool, ZipTool, ClipboardTool | +| 怨꾪쉷/異붿쟻 | TodoWriteTool, TaskTrackerTool, CheckpointTool, PlaybookTool | +| ?ъ슜??| UserAskTool, SuggestActionsTool, NotifyTool | | MCP | McpTool, McpListResourcesTool, McpReadResourceTool | -**탭별 도구 필터링** (`ToolRegistry.ToolTabOverrides`): +**??퀎 ?꾧뎄 ?꾪꽣留?* (`ToolRegistry.ToolTabOverrides`): -`IAgentTool.TabCategory` 또는 `ToolTabOverrides` 딕셔너리로 도구를 탭별로 분류합니다. -`GetActiveToolsForTab(activeTab)` 메서드가 현재 탭에 맞는 도구만 LLM에 전송하여 토큰을 절약합니다. +`IAgentTool.TabCategory` ?먮뒗 `ToolTabOverrides` ?뺤뀛?덈━濡??꾧뎄瑜???퀎濡?遺꾨쪟?⑸땲?? +`GetActiveToolsForTab(activeTab)` 硫붿꽌?쒓? ?꾩옱 ??뿉 留욌뒗 ?꾧뎄留?LLM???꾩넚?섏뿬 ?좏겙???덉빟?⑸땲?? -| 탭 | 활성 도구 범위 | 설명 | +| ??| ?쒖꽦 ?꾧뎄 踰붿쐞 | ?ㅻ챸 | |------|--------------|------| -| **Chat** | 0개 | 순수 대화. 도구 없이 LLM만 응답 | -| **Cowork** | ~50개 | 파일/검색 + 문서생성(xlsx, docx, pptx...) + 데이터/유틸 | -| **Code** | ~50개 | 파일/검색 + 개발(git, build, lsp...) + 태스크/워크트리 + 유틸 | +| **Chat** | 0媛?| ?쒖닔 ?€?? ?꾧뎄 ?놁씠 LLM留??묐떟 | +| **Cowork** | ~50媛?| ?뚯씪/寃€??+ 臾몄꽌?앹꽦(xlsx, docx, pptx...) + ?곗씠???좏떥 | +| **Code** | ~50媛?| ?뚯씪/寃€??+ 媛쒕컻(git, build, lsp...) + ?쒖뒪???뚰겕?몃━ + ?좏떥 | -- Chat 탭 토큰 절약: ~14,400토큰 → 0 (도구 정의 완전 제거) -- Cowork/Code: 교차 제외로 각 ~3,600토큰 추가 절약 +- Chat ???좏겙 ?덉빟: ~14,400?좏겙 ??0 (?꾧뎄 ?뺤쓽 ?꾩쟾 ?쒓굅) +- Cowork/Code: 援먯감 ?쒖쇅濡?媛?~3,600?좏겙 異붽? ?덉빟 -### 5.3 LLM 서비스 - -**지원 공급자**: -| 서비스 | 설명 | +### 5.3 LLM ?쒕퉬?? +**吏€??怨듦툒??*: +| ?쒕퉬??| ?ㅻ챸 | |--------|------| -| `claude` / `sigmoid` | Anthropic Claude (Sigmoid API 경유) | +| `claude` / `sigmoid` | Anthropic Claude (Sigmoid API 寃쎌쑀) | | `gemini` | Google Gemini API | -| `vllm` | OpenAI 호환 vLLM (IBM CP4D 지원 포함) | -| `ollama` | 로컬 Ollama 모델 | +| `vllm` | OpenAI ?명솚 vLLM (IBM CP4D 吏€???ы븿) | +| `ollama` | 濡쒖뺄 Ollama 紐⑤뜽 | -**모델 라우팅**: `ModelRouterService`를 통한 오버라이드 스택 — 대화 중 모델/서비스를 동적으로 전환 가능 +**紐⑤뜽 ?쇱슦??*: `ModelRouterService`瑜??듯븳 ?ㅻ쾭?쇱씠???ㅽ깮 ???€??以?紐⑤뜽/?쒕퉬?ㅻ? ?숈쟻?쇰줈 ?꾪솚 媛€?? +**?좏겙 愿€由?*: `TokenEstimator`濡?而⑦뀓?ㅽ듃 湲몄씠 異붿젙, ?ㅻ쾭?뚮줈????`ContextCondenser`媛€ ?먮룞 ?뺤텞 +- `EstimateBaseOverhead(systemPromptLength, toolCount)`: ?쒖뒪???꾨\?꾪듃 + ?꾧뎄 ?뺤쓽 ?ㅻ쾭?ㅻ뱶 異붿젙 +- `_tool_use_blocks` 硫붿떆吏€ 0.6x, `tool_result` 硫붿떆吏€ 0.7x ?좎씤 ?곸슜 +- 而⑦뀓?ㅽ듃 ?ъ슜???쒖떆???쒖뒪???꾨\?꾪듃 + ?꾧뎄 ?ㅻ쾭?ㅻ뱶 ?ы븿 -**토큰 관리**: `TokenEstimator`로 컨텍스트 길이 추정, 오버플로우 시 `ContextCondenser`가 자동 압축 -- `EstimateBaseOverhead(systemPromptLength, toolCount)`: 시스템 프롬프트 + 도구 정의 오버헤드 추정 -- `_tool_use_blocks` 메시지 0.6x, `tool_result` 메시지 0.7x 할인 적용 -- 컨텍스트 사용량 표시에 시스템 프롬프트 + 도구 오버헤드 포함 - -### 5.4 대화 저장소 - -- `ChatStorageService`: SQLite 기반 대화 영속화 -- `ChatSessionStateService`: 메모리 내 세션 상태 관리 -- `ChatConversation`: 메시지 목록 + 실행 이벤트 타임라인 + `Archived` 아카이브 플래그 +### 5.4 ?€???€?μ냼 +- `ChatStorageService`: SQLite 湲곕컲 ?€???곸냽??- `ChatSessionStateService`: 硫붾え由????몄뀡 ?곹깭 愿€由?- `ChatConversation`: 硫붿떆吏€ 紐⑸줉 + ?ㅽ뻾 ?대깽???€?꾨씪??+ `Archived` ?꾩뭅?대툕 ?뚮옒洹? --- -## 6. UI 계층 +## 6. UI 怨꾩링 -### 주요 윈도우 - -| 윈도우 | 역할 | +### 二쇱슂 ?덈룄?? +| ?덈룄??| ??븷 | |--------|------| -| `LauncherWindow` | 메인 런처 (검색, 위젯, 결과 목록) | -| `ChatWindow` | AI 에이전트 대화 (채팅/Cowork/코드 탭) | -| `DockBarWindow` | 독 바 (시스템 리소스, 빠른 접근) | -| `SettingsWindow` | 설정 관리 | -| `AgentSettingsWindow` | 에이전트 전용 설정 | -| `AgentStatsDashboardWindow` | 에이전트 통계 대시보드 | -| `SkillEditorWindow` | 스킬 편집기 | -| `SkillGalleryWindow` | 스킬 갤러리 | -| `TrayMenuWindow` | 시스템 트레이 메뉴 | -| `PreviewWindow` | 문서 미리보기 (WebView2) | +| `LauncherWindow` | 硫붿씤 ?곗쿂 (寃€?? ?꾩젽, 寃곌낵 紐⑸줉) | +| `ChatWindow` | AI ?먯씠?꾪듃 ?€??(梨꾪똿/Cowork/肄붾뱶 ?? | +| `DockBarWindow` | ??諛?(?쒖뒪??由ъ냼?? 鍮좊Ⅸ ?묎렐) | +| `SettingsWindow` | ?ㅼ젙 愿€由?| +| `AgentSettingsWindow` | ?먯씠?꾪듃 ?꾩슜 ?ㅼ젙 | +| `AgentStatsDashboardWindow` | ?먯씠?꾪듃 ?듦퀎 ?€?쒕낫??| +| `SkillEditorWindow` | ?ㅽ궗 ?몄쭛湲?| +| `SkillGalleryWindow` | ?ㅽ궗 媛ㅻ윭由?| +| `TrayMenuWindow` | ?쒖뒪???몃젅??硫붾돱 | +| `PreviewWindow` | 臾몄꽌 誘몃━蹂닿린 (WebView2) | -### ChatWindow 분할 구조 +### ChatWindow 遺꾪븷 援ъ“ -`ChatWindow.xaml.cs`는 partial class로 기능별 분할: +`ChatWindow.xaml.cs`??partial class濡?湲곕뒫蹂?遺꾪븷: -| 파일 | 역할 | +| ?뚯씪 | ??븷 | |------|------| -| `ChatWindow.xaml.cs` | 메인 오케스트레이션, 스트리밍, 입력 처리 | -| `ChatWindow.AgentEventProcessor.cs` | 에이전트 이벤트 수신/라우팅 | -| `ChatWindow.AgentEventRendering.cs` | 에이전트 이벤트 배너/카드 렌더링 (SessionStart/UserPromptSubmit 숨김) | -| `ChatWindow.AgentStatusPresentation.cs` | 에이전트 실시간 상태 표시 | -| `ChatWindow.ComposerQueuePresentation.cs` | 작성기 큐 UI | -| `ChatWindow.ContextUsagePresentation.cs` | 컨텍스트 사용량 링/팝업 | -| `ChatWindow.ConversationFilterPresentation.cs` | 대화 필터링 | -| `ChatWindow.ConversationListPresentation.cs` | 사이드바 대화 목록 | -| `ChatWindow.ConversationManagementPresentation.cs` | 대화 생성/삭제/관리 | -| `ChatWindow.FileBrowserPresentation.cs` | 파일 브라우저 UI | -| `ChatWindow.FooterPresentation.cs` | 하단 바 (폴더, 권한) | -| `ChatWindow.GitBranchPresentation.cs` | Git 브랜치 표시/전환 | -| `ChatWindow.LiveProgressPresentation.cs` | 실시간 진행 상태 | -| `ChatWindow.MessageBubblePresentation.cs` | 메시지 버블 렌더링 | -| `ChatWindow.MessageInteractions.cs` | 메시지 복사/편집/재전송 | -| `ChatWindow.PermissionPresentation.cs` | 권한 팝업/배너 UI | -| `ChatWindow.PlanApprovalPresentation.cs` | 계획 승인 카드 | -| `ChatWindow.PopupPresentation.cs` | 공통 팝업 구성 | -| `ChatWindow.PreviewPresentation.cs` | 파일 미리보기 탭 | -| `ChatWindow.SelectionPopupPresentation.cs` | 워크트리 선택 팝업 | -| `ChatWindow.SidebarInteractionPresentation.cs` | 사이드바 상호작용 | -| `ChatWindow.StatusPresentation.cs` | 상태 배지/스트립 | -| `ChatWindow.SurfaceVisualPresentation.cs` | 시각 효과 (글로우, 펄스 등) | -| `ChatWindow.TaskSummary.cs` | 작업 요약 카드 | -| `ChatWindow.TimelinePresentation.cs` | 타임라인 정렬, 캐시, 이벤트 필터링 | -| `ChatWindow.TopicPresetPresentation.cs` | 주제 프리셋 UI | -| `ChatWindow.TranscriptHost.cs` | 트랜스크립트 호스트 컨테이너 | -| `ChatWindow.TranscriptPolicy.cs` | 트랜스크립트 표시 정책 | -| `ChatWindow.TranscriptRenderExecution.cs` | 트랜스크립트 렌더 실행 | -| `ChatWindow.TranscriptRenderPlanner.cs` | 트랜스크립트 렌더 계획 | -| `ChatWindow.TranscriptRendering.cs` | 트랜스크립트 렌더링 | -| `ChatWindow.TranscriptVirtualization.cs` | 트랜스크립트 가상화 (대규모 대화) | -| `ChatWindow.SystemPromptBuilder.cs` | 시스템 프롬프트 동적 조립 (탭/프리셋/컨텍스트 주입) | -| `ChatWindow.OverlaySettingsPresentation.cs` | 인라인 설정 팝업 (모델, 빠른액션) | -| `ChatWindow.UserAskPresentation.cs` | 사용자 질문 인라인 카드 | -| `ChatWindow.VisualInteractionHelpers.cs` | 시각 상호작용 헬퍼 | +| `ChatWindow.xaml.cs` | 硫붿씤 ?ㅼ??ㅽ듃?덉씠?? ?ㅽ듃由щ컢, ?낅젰 泥섎━ | +| `ChatWindow.AgentEventProcessor.cs` | ?먯씠?꾪듃 ?대깽???섏떊/?쇱슦??| +| `ChatWindow.AgentEventRendering.cs` | ?먯씠?꾪듃 ?대깽??諛곕꼫/移대뱶 ?뚮뜑留?(SessionStart/UserPromptSubmit ?④?) | +| `ChatWindow.AgentStatusPresentation.cs` | ?먯씠?꾪듃 ?ㅼ떆媛??곹깭 ?쒖떆 | +| `ChatWindow.ComposerQueuePresentation.cs` | ?묒꽦湲???UI | +| `ChatWindow.ContextUsagePresentation.cs` | 而⑦뀓?ㅽ듃 ?ъ슜??留??앹뾽 | +| `ChatWindow.ConversationFilterPresentation.cs` | ?€???꾪꽣留?| +| `ChatWindow.ConversationListPresentation.cs` | ?ъ씠?쒕컮 ?€??紐⑸줉 | +| `ChatWindow.ConversationManagementPresentation.cs` | ?€???앹꽦/??젣/愿€由?| +| `ChatWindow.FileBrowserPresentation.cs` | ?뚯씪 釉뚮씪?곗? UI | +| `ChatWindow.FooterPresentation.cs` | ?섎떒 諛?(?대뜑, 沅뚰븳) | +| `ChatWindow.GitBranchPresentation.cs` | Git 釉뚮옖移??쒖떆/?꾪솚 | +| `ChatWindow.LiveProgressPresentation.cs` | ?ㅼ떆媛?吏꾪뻾 ?곹깭 | +| `ChatWindow.MessageBubblePresentation.cs` | 硫붿떆吏€ 踰꾨툝 ?뚮뜑留?| +| `ChatWindow.MessageInteractions.cs` | 硫붿떆吏€ 蹂듭궗/?몄쭛/?ъ쟾??| +| `ChatWindow.PermissionPresentation.cs` | 沅뚰븳 ?앹뾽/諛곕꼫 UI | +| `ChatWindow.PlanApprovalPresentation.cs` | 怨꾪쉷 ?뱀씤 移대뱶 | +| `ChatWindow.PopupPresentation.cs` | 怨듯넻 ?앹뾽 援ъ꽦 | +| `ChatWindow.PreviewPresentation.cs` | ?뚯씪 誘몃━蹂닿린 ??| +| `ChatWindow.SelectionPopupPresentation.cs` | ?뚰겕?몃━ ?좏깮 ?앹뾽 | +| `ChatWindow.SidebarInteractionPresentation.cs` | ?ъ씠?쒕컮 ?곹샇?묒슜 | +| `ChatWindow.StatusPresentation.cs` | ?곹깭 諛곗?/?ㅽ듃由?| +| `ChatWindow.SurfaceVisualPresentation.cs` | ?쒓컖 ?④낵 (湲€濡쒖슦, ?꾩뒪 ?? | +| `ChatWindow.TaskSummary.cs` | ?묒뾽 ?붿빟 移대뱶 | +| `ChatWindow.TimelinePresentation.cs` | ?€?꾨씪???뺣젹, 罹먯떆, ?대깽???꾪꽣留?| +| `ChatWindow.TopicPresetPresentation.cs` | 二쇱젣 ?꾨━??UI | +| `ChatWindow.TranscriptHost.cs` | ?몃옖?ㅽ겕由쏀듃 ?몄뒪??而⑦뀒?대꼫 | +| `ChatWindow.TranscriptPolicy.cs` | ?몃옖?ㅽ겕由쏀듃 ?쒖떆 ?뺤콉 | +| `ChatWindow.TranscriptRenderExecution.cs` | ?몃옖?ㅽ겕由쏀듃 ?뚮뜑 ?ㅽ뻾 | +| `ChatWindow.TranscriptRenderPlanner.cs` | ?몃옖?ㅽ겕由쏀듃 ?뚮뜑 怨꾪쉷 | +| `ChatWindow.TranscriptRendering.cs` | ?몃옖?ㅽ겕由쏀듃 ?뚮뜑留?| +| `ChatWindow.TranscriptVirtualization.cs` | ?몃옖?ㅽ겕由쏀듃 媛€?곹솕 (?€洹쒕え ?€?? | +| `ChatWindow.SystemPromptBuilder.cs` | ?쒖뒪???꾨\?꾪듃 ?숈쟻 議곕┰ (???꾨━??而⑦뀓?ㅽ듃 二쇱엯) | +| `ChatWindow.OverlaySettingsPresentation.cs` | ?몃씪???ㅼ젙 ?앹뾽 (紐⑤뜽, 鍮좊Ⅸ?≪뀡) | +| `ChatWindow.UserAskPresentation.cs` | ?ъ슜??吏덈Ц ?몃씪??移대뱶 | +| `ChatWindow.VisualInteractionHelpers.cs` | ?쒓컖 ?곹샇?묒슜 ?ы띁 | -### 테마 시스템 +### ?뚮쭏 ?쒖뒪?? +9媛??뚮쭏 XAML 由ъ냼???뺤뀛?덈━: `Dark`, `Light`, `OLED`, `Nord`, `Monokai`, `Catppuccin`, `Sepia`, `Alfred`, `AlfredLight` -9개 테마 XAML 리소스 딕셔너리: `Dark`, `Light`, `OLED`, `Nord`, `Monokai`, `Catppuccin`, `Sepia`, `Alfred`, `AlfredLight` - -런타임 테마 전환: `SettingsService.Settings.Launcher.Theme` 변경 → 리소스 딕셔너리 교체 +?고????뚮쭏 ?꾪솚: `SettingsService.Settings.Launcher.Theme` 蹂€寃???由ъ냼???뺤뀛?덈━ 援먯껜 --- -## 7. 설정 구조 (AppSettings) +## 7. ?ㅼ젙 援ъ“ (AppSettings) -### 최상위 설정 +### 理쒖긽???ㅼ젙 -| 속성 | 기본값 | 설명 | +| ?띿꽦 | 湲곕낯媛?| ?ㅻ챸 | |------|--------|------| -| `AiEnabled` | true | AI 기능 활성화 | -| `OperationMode` | "internal" | 운영 모드 (internal/external) | -| `Hotkey` | "Alt+Space" | 런처 단축키 | -| `CleanupPeriodDays` | 30 | 대화 보관 기간 (일) | -| `InternalModeEnabled` | true | 사내 모드 여부 | +| `AiEnabled` | true | AI 湲곕뒫 ?쒖꽦??| +| `OperationMode` | "internal" | ?댁쁺 紐⑤뱶 (internal/external) | +| `Hotkey` | "Alt+Space" | ?곗쿂 ?⑥텞??| +| `CleanupPeriodDays` | 30 | ?€??蹂닿? 湲곌컙 (?? | +| `InternalModeEnabled` | true | ?щ궡 紐⑤뱶 ?щ? | -### LauncherSettings (중첩) +### LauncherSettings (以묒꺽) -| 그룹 | 주요 속성 | +| 洹몃9 | 二쇱슂 ?띿꽦 | |------|----------| -| 표시 | `Theme`, `Opacity`, `Position`, `Width`, `MaxResults` | -| 글로우 | `EnableRainbowGlow`, `EnableSelectionGlow`, `ShowLauncherBorder` | -| 위젯 | `ShowWidgetPerf`, `ShowWidgetPomo`, `ShowWidgetNote`, `ShowWidgetWeather`, `ShowWidgetCalendar`, `ShowWidgetBattery` | -| 독 바 | `DockBarItems`, `DockBarAutoShow`, `DockBarOpacity`, `DockBarRainbowGlow` | -| 기능 | `EnableFavorites`, `EnableRecent`, `EnableActionMode`, `EnableClipboardAutoCategory` | +| ?쒖떆 | `Theme`, `Opacity`, `Position`, `Width`, `MaxResults` | +| 湲€濡쒖슦 | `EnableRainbowGlow`, `EnableSelectionGlow`, `ShowLauncherBorder` | +| ?꾩젽 | `ShowWidgetPerf`, `ShowWidgetPomo`, `ShowWidgetNote`, `ShowWidgetWeather`, `ShowWidgetCalendar`, `ShowWidgetBattery` | +| ??諛?| `DockBarItems`, `DockBarAutoShow`, `DockBarOpacity`, `DockBarRainbowGlow` | +| 湲곕뒫 | `EnableFavorites`, `EnableRecent`, `EnableActionMode`, `EnableClipboardAutoCategory` | -### LlmSettings (중첩) +### LlmSettings (以묒꺽) -에이전트의 LLM 연결 설정: 서비스 선택, 모델, API 키 (DPAPI 암호화), 엔드포인트, 온도, 최대 토큰 등 - -| 속성 | 기본값 | 설명 | +?먯씠?꾪듃??LLM ?곌껐 ?ㅼ젙: ?쒕퉬???좏깮, 紐⑤뜽, API ??(DPAPI ?뷀샇??, ?붾뱶?ъ씤?? ?⑤룄, 理쒕? ?좏겙 ?? +| ?띿꽦 | 湲곕낯媛?| ?ㅻ챸 | |------|--------|------| -| `UseAutomaticProfileTemperature` | true | 등록 모델 프로파일의 자동 temperature 정책 | -| `EnableDetailedLog` | false | 워크플로우 상세 로그 (LLM 요청/응답, 도구 이력) | -| `DetailedLogRetentionDays` | 3 | 상세 로그 보관 기간 (일) | -| `EnableRawLlmLog` | false | LLM 요청/응답 원문 기록 (디버깅용) | +| `UseAutomaticProfileTemperature` | true | ?깅줉 紐⑤뜽 ?꾨줈?뚯씪???먮룞 temperature ?뺤콉 | +| `EnableDetailedLog` | false | ?뚰겕?뚮줈???곸꽭 濡쒓렇 (LLM ?붿껌/?묐떟, ?꾧뎄 ?대젰) | +| `DetailedLogRetentionDays` | 3 | ?곸꽭 濡쒓렇 蹂닿? 湲곌컙 (?? | +| `EnableRawLlmLog` | false | LLM ?붿껌/?묐떟 ?먮Ц 湲곕줉 (?붾쾭源낆슜) | -### RegisteredModel 실행 프로파일 +### RegisteredModel ?ㅽ뻾 ?꾨줈?뚯씪 -모델별 `ExecutionProfile`로 도구 호출 강도, 재시도, 메모리/압축 주입량을 조절: +紐⑤뜽蹂?`ExecutionProfile`濡??꾧뎄 ?몄텧 媛뺣룄, ?ъ떆?? 硫붾え由??뺤텞 二쇱엯?됱쓣 議곗젅: -| 프로파일 | 설명 | +| ?꾨줈?뚯씪 | ?ㅻ챸 | |---------|------| -| `balanced` | 기본 균형 모드 | -| `tool_call_strict` | 도구 호출 강제/엄격 모드 | -| `reasoning_first` | 추론 우선 모드 | -| `fast_readonly` | 빠른 읽기 전용 모드 | -| `document_heavy` | 문서 처리 집중 모드 | +| `balanced` | 湲곕낯 洹좏삎 紐⑤뱶 | +| `tool_call_strict` | ?꾧뎄 ?몄텧 媛뺤젣/?꾧꺽 紐⑤뱶 | +| `reasoning_first` | 異붾줎 ?곗꽑 紐⑤뱶 | +| `fast_readonly` | 鍮좊Ⅸ ?쎄린 ?꾩슜 紐⑤뱶 | +| `document_heavy` | 臾몄꽌 泥섎━ 吏묒쨷 紐⑤뱶 | --- -## 8. 플러그인 시스템 - +## 8. ?뚮윭洹몄씤 ?쒖뒪?? ### SDK (AxCopilot.SDK) ```csharp public interface IActionHandler { - string? Prefix { get; } // 접두어 (null이면 퍼지 검색만) + string? Prefix { get; } // ?묐몢??(null?대㈃ ?쇱? 寃€?됰쭔) PluginMetadata Metadata { get; } Task> GetItemsAsync(string query, CancellationToken ct); Task ExecuteAsync(LauncherItem item, CancellationToken ct); } ``` -### 개발 방법 +### 媛쒕컻 諛⑸쾿 -1. `AxCopilot.SDK` 참조하여 `IActionHandler` 구현 -2. 빌드된 `.dll`을 `settings.json`의 `Plugins` 배열에 경로 등록 -3. `PluginHost`가 앱 시작 시 동적 로드 +1. `AxCopilot.SDK` 李몄“?섏뿬 `IActionHandler` 援ы쁽 +2. 鍮뚮뱶??`.dll`??`settings.json`??`Plugins` 諛곗뿴??寃쎈줈 ?깅줉 +3. `PluginHost`媛€ ???쒖옉 ???숈쟻 濡쒕뱶 --- -## 9. 빌드 및 실행 +## 9. 鍮뚮뱶 諛??ㅽ뻾 -### 개발 빌드 +### 媛쒕컻 鍮뚮뱶 ```bash dotnet build src/AxCopilot/AxCopilot.csproj ``` -### 빠른 빌드 (암호화/설치프로그램 생략) +### 鍮좊Ⅸ 鍮뚮뱶 (?뷀샇???ㅼ튂?꾨줈洹몃옩 ?앸왂) ```bash ./build-quick.sh ``` -메인 앱만 self-contained 단일 파일로 빌드합니다. AxKeyEncryptor/Installer/난독화를 건너뛰어 빠른 개발 반복에 적합합니다. +硫붿씤 ?깅쭔 self-contained ?⑥씪 ?뚯씪濡?鍮뚮뱶?⑸땲?? AxKeyEncryptor/Installer/?쒕룆?붾? 嫄대꼫?곗뼱 鍮좊Ⅸ 媛쒕컻 諛섎났???곹빀?⑸땲?? -### 릴리스 빌드 (단일 파일) +### 由대━??鍮뚮뱶 (?⑥씪 ?뚯씪) ```bash dotnet publish src/AxCopilot/AxCopilot.csproj -c Release -r win-x64 --self-contained ``` -릴리스 빌드 옵션: -- `PublishSingleFile`: 단일 실행 파일 -- `EnableCompressionInSingleFile`: 압축 적용 -- `PublishReadyToRun`: AOT 프리컴파일 -- `DebugType=none`: 디버그 심볼 제거 -- `TrimMode=partial`: IL 트리밍 - -### 테스트 - +由대━??鍮뚮뱶 ?듭뀡: +- `PublishSingleFile`: ?⑥씪 ?ㅽ뻾 ?뚯씪 +- `EnableCompressionInSingleFile`: ?뺤텞 ?곸슜 +- `PublishReadyToRun`: AOT ?꾨━而댄뙆??- `DebugType=none`: ?붾쾭洹??щ낵 ?쒓굅 +- `TrimMode=partial`: IL ?몃━諛? +### ?뚯뒪?? ```bash dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj ``` --- -## 10. 버전 관리 - -- `AxCopilot.csproj`의 `` 태그 하나만 변경하면 앱 전체에 반영 -- 설정 스키마 버전은 `SettingsService.cs` → `CurrentSettingsVersion`에서 별도 관리 -- 마이그레이션: `SettingsService`가 이전 버전 설정 파일을 자동 업그레이드 - +## 10. 踰꾩쟾 愿€由? +- `AxCopilot.csproj`??`` ?쒓렇 ?섎굹留?蹂€寃쏀븯硫????꾩껜??諛섏쁺 +- ?ㅼ젙 ?ㅽ궎留?踰꾩쟾?€ `SettingsService.cs` ??`CurrentSettingsVersion`?먯꽌 蹂꾨룄 愿€由?- 留덉씠洹몃젅?댁뀡: `SettingsService`媛€ ?댁쟾 踰꾩쟾 ?ㅼ젙 ?뚯씪???먮룞 ?낃렇?덉씠?? --- -## 11. 보안 +## 11. 蹂댁븞 -| 항목 | 구현 | +| ??ぉ | 援ы쁽 | |------|------| -| API 키 저장 | DPAPI 암호화 (System.Security.Cryptography.ProtectedData) | -| 키 관리 도구 | AxKeyEncryptor (별도 유틸리티) | -| 안티 탬퍼 | 디버거/디컴파일러 감지 (Release 빌드, `Security/AntiTamper.cs`) | -| Unsafe 코드 | `AllowUnsafeBlocks=true` (ScreenCaptureHandler 포인터 연산용) | +| API ???€??| DPAPI ?뷀샇??(System.Security.Cryptography.ProtectedData) | +| ??愿€由??꾧뎄 | AxKeyEncryptor (蹂꾨룄 ?좏떥由ы떚) | +| ?덊떚 ?ы띁 | ?붾쾭嫄??붿뺨?뚯씪??媛먯? (Release 鍮뚮뱶, `Security/AntiTamper.cs`) | +| Unsafe 肄붾뱶 | `AllowUnsafeBlocks=true` (ScreenCaptureHandler ?ъ씤???곗궛?? | --- -## 12. 성능 최적화 내역 +## 12. ?깅뒫 理쒖쟻???댁뿭 -### 유휴 CPU 최적화 (2026-04-09) +### ?좏쑕 CPU 理쒖쟻??(2026-04-09) -| 대상 | 변경 전 | 변경 후 | +| ?€??| 蹂€寃???| 蹂€寃???| |------|---------|---------| -| PerformanceMonitorService 폴링 | 2초 | 5초 | -| 위젯 타이머 | 1초 | 3초 | -| 레인보우 글로우 타이머 | 150ms | 300ms | -| ServerStatusService 핑 | 15초 | 60초 | +| PerformanceMonitorService ?대쭅 | 2珥?| 5珥?| +| ?꾩젽 ?€?대㉧ | 1珥?| 3珥?| +| ?덉씤蹂댁슦 湲€濡쒖슦 ?€?대㉧ | 150ms | 300ms | +| ServerStatusService ??| 15珥?| 60珥?| -### 스트리밍 렌더링 최적화 (2026-04-09) +### ?ㅽ듃由щ컢 ?뚮뜑留?理쒖쟻??(2026-04-09) -- **TypingTimer**: 50ms → 80ms, `string.Concat` → `char[]` 버퍼 재사용 -- **CursorTimer**: 전체 문자열 재생성 → 마지막 문자만 교체 -- **StringBuilder.ToString()**: 30ms 최소 간격 쓰로틀링 -- **RenderMessages**: 스트리밍 중 불필요한 전체 재렌더링 방지 (조기 반환) -- **타임라인 이벤트**: 접힌 모드에서 연속 동일 ToolCall 병합 +- **TypingTimer**: 50ms ??80ms, `string.Concat` ??`char[]` 踰꾪띁 ?ъ궗??- **CursorTimer**: ?꾩껜 臾몄옄???ъ깮????留덉?留?臾몄옄留?援먯껜 +- **StringBuilder.ToString()**: 30ms 理쒖냼 媛꾧꺽 ?곕줈?€留?- **RenderMessages**: ?ㅽ듃由щ컢 以?遺덊븘?뷀븳 ?꾩껜 ?щ젋?붾쭅 諛⑹? (議곌린 諛섑솚) +- **?€?꾨씪???대깽??*: ?묓엺 紐⑤뱶?먯꽌 ?곗냽 ?숈씪 ToolCall 蹂묓빀 -### 런타임 안정성 수정 (2026-04-09) +### ?고????덉젙???섏젙 (2026-04-09) -| 파일 | 수정 내용 | +| ?뚯씪 | ?섏젙 ?댁슜 | |------|----------| -| `CsvSkill.cs` | JSON 배열 첫 요소 `ValueKind` 검증 추가 | -| `HtmlSkill.cs` | gradient `Split(',')` 결과 `Length >= 2` 가드 추가 | -| `ChatWindow.xaml.cs` | `ParseGenericAction` 빈 배열 가드, `ShowDropActionMenu` null 가드, `GetAgentLoop` `.FirstOrDefault()` 전환 | -| `ChatWindow.GitBranchPresentation.cs` | async void 핸들러 try/catch 보호 | -| `ChatWindow.xaml.cs` (BtnGitBranch_Click) | async void 핸들러 try/catch 보호 | +| `CsvSkill.cs` | JSON 諛곗뿴 泥??붿냼 `ValueKind` 寃€利?異붽? | +| `HtmlSkill.cs` | gradient `Split(',')` 寃곌낵 `Length >= 2` 媛€??異붽? | +| `ChatWindow.xaml.cs` | `ParseGenericAction` 鍮?諛곗뿴 媛€?? `ShowDropActionMenu` null 媛€?? `GetAgentLoop` `.FirstOrDefault()` ?꾪솚 | +| `ChatWindow.GitBranchPresentation.cs` | async void ?몃뱾??try/catch 蹂댄샇 | +| `ChatWindow.xaml.cs` (BtnGitBranch_Click) | async void ?몃뱾??try/catch 蹂댄샇 | -### UI 스레드 부하 최적화 2차 (2026-04-09) +### UI ?ㅻ젅??遺€??理쒖쟻??2李?(2026-04-09) -| 대상 | 변경 전 | 변경 후 | 효과 | +| ?€??| 蹂€寃???| 蹂€寃???| ?④낵 | |------|---------|---------|------| -| 스크롤 애니메이션 | 매번 새 16ms 타이머 생성 | 재사용 32ms 타이머 1개 | GC 압력 + 타이머 누적 해소 | -| 사이드바 애니메이션 | 매번 새 10ms 타이머 생성 | 재사용 32ms 타이머 1개 | 동일 | -| Git 브랜치 UI | `Dispatcher.Invoke` (블로킹) | `Dispatcher.InvokeAsync` (논블로킹) | UI 스레드 차단 해소 | -| 토큰 사용량 호 | 매 250ms PathGeometry 재생성 | 1% 미만 변화 시 렌더링 생략 | 불필요한 레이아웃 연산 제거 | -| 대화 검색 타이머 | 140ms | 300ms | 초당 7회 → 3회 | -| 에이전트 이벤트 타이머 | 140ms (스트리밍: 300/420) | 200ms (스트리밍: 350/500) | 이벤트 처리 빈도 완화 | -| 반응형 레이아웃 타이머 | 120ms | 250ms | 리사이즈 디바운스 강화 | -| 대화 목록 LINQ | Where×2 + Count×3 = 리스트 5회 순회 | Where 1회 병합 + 단일 루프 카운트 | 할당/순회 대폭 감소 | +| ?ㅽ겕濡??좊땲硫붿씠??| 留ㅻ쾲 ??16ms ?€?대㉧ ?앹꽦 | ?ъ궗??32ms ?€?대㉧ 1媛?| GC ?뺣젰 + ?€?대㉧ ?꾩쟻 ?댁냼 | +| ?ъ씠?쒕컮 ?좊땲硫붿씠??| 留ㅻ쾲 ??10ms ?€?대㉧ ?앹꽦 | ?ъ궗??32ms ?€?대㉧ 1媛?| ?숈씪 | +| Git 釉뚮옖移?UI | `Dispatcher.Invoke` (釉붾줈?? | `Dispatcher.InvokeAsync` (?쇰툝濡쒗궧) | UI ?ㅻ젅??李⑤떒 ?댁냼 | +| ?좏겙 ?ъ슜????| 留?250ms PathGeometry ?ъ깮??| 1% 誘몃쭔 蹂€?????뚮뜑留??앸왂 | 遺덊븘?뷀븳 ?덉씠?꾩썐 ?곗궛 ?쒓굅 | +| ?€??寃€???€?대㉧ | 140ms | 300ms | 珥덈떦 7????3??| +| ?먯씠?꾪듃 ?대깽???€?대㉧ | 140ms (?ㅽ듃由щ컢: 300/420) | 200ms (?ㅽ듃由щ컢: 350/500) | ?대깽??泥섎━ 鍮덈룄 ?꾪솕 | +| 諛섏쓳???덉씠?꾩썐 ?€?대㉧ | 120ms | 250ms | 由ъ궗?댁쫰 ?붾컮?댁뒪 媛뺥솕 | +| ?€??紐⑸줉 LINQ | Where횞2 + Count횞3 = 由ъ뒪??5???쒗쉶 | Where 1??蹂묓빀 + ?⑥씪 猷⑦봽 移댁슫??| ?좊떦/?쒗쉶 ?€??媛먯냼 | -### 구조적 메모리/안정성 수정 (2026-04-09) +### 援ъ“??硫붾え由??덉젙???섏젙 (2026-04-09) -| 문제 | 위치 | 수정 | +| 臾몄젣 | ?꾩튂 | ?섏젙 | |------|------|------| -| Events 컬렉션 무한 성장 | `AgentLoopService.cs` | 500개 초과 시 오래된 이벤트 자동 제거 | -| 파일 브라우저 타이머 좀비 | `ChatWindow.FileBrowserPresentation.cs` | 매번 새 타이머 생성 → 재사용 패턴 | -| 엘리먼트 캐시 미정리 | `ChatWindow.TranscriptVirtualization.cs` | 보유 한도 240→120, 1.5배 초과 시 정리 | -| WorkflowAnalyzer UI 블로킹 | `WorkflowAnalyzerWindow.xaml.cs` | `Dispatcher.Invoke` → `InvokeAsync` | +| Events 而щ젆??臾댄븳 ?깆옣 | `AgentLoopService.cs` | 500媛?珥덇낵 ???ㅻ옒???대깽???먮룞 ?쒓굅 | +| ?뚯씪 釉뚮씪?곗? ?€?대㉧ 醫€鍮?| `ChatWindow.FileBrowserPresentation.cs` | 留ㅻ쾲 ???€?대㉧ ?앹꽦 ???ъ궗???⑦꽩 | +| ?섎━癒쇳듃 罹먯떆 誘몄젙由?| `ChatWindow.TranscriptVirtualization.cs` | 蹂댁쑀 ?쒕룄 240??20, 1.5諛?珥덇낵 ???뺣━ | +| WorkflowAnalyzer UI 釉붾줈??| `WorkflowAnalyzerWindow.xaml.cs` | `Dispatcher.Invoke` ??`InvokeAsync` | -### 구조적 리팩토링 P1 (2026-04-09) +### 援ъ“??由ы뙥?좊쭅 P1 (2026-04-09) -| 대상 | 파일 | 변경 | +| ?€??| ?뚯씪 | 蹂€寃?| |------|------|------| -| 인크리멘탈 렌더 hiddenCount 안정화 | `ChatWindow.TranscriptRenderPlanner.cs` | 스트리밍 중 hiddenCount 감소 차단 → prefix 키 불일치로 인한 전체 재빌드 폴백 방지 | -| 비가시 렌더 차단 | `ChatWindow.TranscriptRendering.cs` | 최소화/숨김 상태에서 RenderMessages 즉시 반환 → 불필요한 UI 재구축 제거 | -| ConversationList 이벤트 위임 | `ChatWindow.ConversationListPresentation.cs` | 항목당 5개 람다 핸들러 → ConversationPanel에 단일 위임 핸들러 (Tag 기반 분기). 탭 전환 시 250개 핸들러 누적 해소 | -| TopicPreset 이벤트 위임 | `ChatWindow.TopicPresetPresentation.cs` | 카드당 3개 람다 핸들러 → TopicButtonPanel에 단일 위임 핸들러. 탭 전환 시 45개 핸들러 누적 해소 | -| 공통 VisualTree 헬퍼 | `ChatWindow.VisualInteractionHelpers.cs` | `FindAncestorWithTag`, `FindAncestor` 유틸 추가 | +| ?명겕由щ찘???뚮뜑 hiddenCount ?덉젙??| `ChatWindow.TranscriptRenderPlanner.cs` | ?ㅽ듃由щ컢 以?hiddenCount 媛먯냼 李⑤떒 ??prefix ??遺덉씪移섎줈 ?명븳 ?꾩껜 ?щ퉴???대갚 諛⑹? | +| 鍮꾧????뚮뜑 李⑤떒 | `ChatWindow.TranscriptRendering.cs` | 理쒖냼???④? ?곹깭?먯꽌 RenderMessages 利됱떆 諛섑솚 ??遺덊븘?뷀븳 UI ?ш뎄異??쒓굅 | +| ConversationList ?대깽???꾩엫 | `ChatWindow.ConversationListPresentation.cs` | ??ぉ??5媛??뚮떎 ?몃뱾????ConversationPanel???⑥씪 ?꾩엫 ?몃뱾??(Tag 湲곕컲 遺꾧린). ???꾪솚 ??250媛??몃뱾???꾩쟻 ?댁냼 | +| TopicPreset ?대깽???꾩엫 | `ChatWindow.TopicPresetPresentation.cs` | 移대뱶??3媛??뚮떎 ?몃뱾????TopicButtonPanel???⑥씪 ?꾩엫 ?몃뱾?? ???꾪솚 ??45媛??몃뱾???꾩쟻 ?댁냼 | +| 怨듯넻 VisualTree ?ы띁 | `ChatWindow.VisualInteractionHelpers.cs` | `FindAncestorWithTag`, `FindAncestor` ?좏떥 異붽? | -### 구조적 리팩토링 P2 (2026-04-09) +### 援ъ“??由ы뙥?좊쭅 P2 (2026-04-09) -| 대상 | 파일 | 변경 | +| ?€??| ?뚯씪 | 蹂€寃?| |------|------|------| -| _agentLiveContainer 인크리멘탈 허용 | `TranscriptRenderPlanner.cs`, `TranscriptRenderExecution.cs` | 라이브 컨테이너를 expectedChildCount에 포함, 인크리멘탈 시 임시 분리/재삽입 → `hasExternalChildren` 차단 해소 | -| 스트리밍 append-only 렌더 | `TranscriptRenderExecution.cs`, `TranscriptRendering.cs` | prefix 비교 우회하는 `TryApplyStreamingAppendRender` 추가 — stable 키 부분집합 관계만 확인, 새 항목만 추가 | -| Permission 이벤트 위임 | `ChatWindow.PermissionPresentation.cs` | 행당 4개 람다 → PermissionItems에 단일 위임 핸들러 + `PermissionItemTag` | -| Preview 탭 이벤트 위임 | `ChatWindow.PreviewPresentation.cs` | 탭당 7개 람다 → PreviewTabPanel에 단일 위임 핸들러 + `PreviewTabTag` | -| GitBranch 이벤트 위임 | `ChatWindow.GitBranchPresentation.cs`, `SelectionPopupPresentation.cs` | `CreateFlatPopupRow`/`CreatePopupMenuRow` 행의 람다 → GitBranchItems에 단일 위임 + `PopupRowTag` | +| _agentLiveContainer ?명겕由щ찘???덉슜 | `TranscriptRenderPlanner.cs`, `TranscriptRenderExecution.cs` | ?쇱씠釉?而⑦뀒?대꼫瑜?expectedChildCount???ы븿, ?명겕由щ찘?????꾩떆 遺꾨━/?ъ궫????`hasExternalChildren` 李⑤떒 ?댁냼 | +| ?ㅽ듃由щ컢 append-only ?뚮뜑 | `TranscriptRenderExecution.cs`, `TranscriptRendering.cs` | prefix 鍮꾧탳 ?고쉶?섎뒗 `TryApplyStreamingAppendRender` 異붽? ??stable ??遺€遺꾩쭛??愿€怨꾨쭔 ?뺤씤, ????ぉ留?異붽? | +| Permission ?대깽???꾩엫 | `ChatWindow.PermissionPresentation.cs` | ?됰떦 4媛??뚮떎 ??PermissionItems???⑥씪 ?꾩엫 ?몃뱾??+ `PermissionItemTag` | +| Preview ???대깽???꾩엫 | `ChatWindow.PreviewPresentation.cs` | ??떦 7媛??뚮떎 ??PreviewTabPanel???⑥씪 ?꾩엫 ?몃뱾??+ `PreviewTabTag` | +| GitBranch ?대깽???꾩엫 | `ChatWindow.GitBranchPresentation.cs`, `SelectionPopupPresentation.cs` | `CreateFlatPopupRow`/`CreatePopupMenuRow` ?됱쓽 ?뚮떎 ??GitBranchItems???⑥씪 ?꾩엫 + `PopupRowTag` | -### 구조적 리팩토링 P3 (2026-04-09) +### 援ъ“??由ы뙥?좊쭅 P3 (2026-04-09) -| 대상 | 파일 | 변경 | +| ?€??| ?뚯씪 | 蹂€寃?| |------|------|------| -| FileBrowser 명시적 해제 | `ChatWindow.FileBrowserPresentation.cs` | TreeViewItem 람다→명명 메서드(`FileTreeItem_Expanded/DoubleClick/RightClick`) 전환. `BuildFileTree()` 시 `DetachFileTreeHandlers()` 재귀 호출로 Clear 전 핸들러 해제. 트리 재구축당 300개 핸들러 누적 해소 | +| FileBrowser 紐낆떆???댁젣 | `ChatWindow.FileBrowserPresentation.cs` | TreeViewItem ?뚮떎?믩챸紐?硫붿꽌??`FileTreeItem_Expanded/DoubleClick/RightClick`) ?꾪솚. `BuildFileTree()` ??`DetachFileTreeHandlers()` ?ш? ?몄텧濡?Clear ???몃뱾???댁젣. ?몃━ ?ш뎄異뺣떦 300媛??몃뱾???꾩쟻 ?댁냼 | -> 전체 계획 완료. `docs/STRUCTURAL_REFACTORING_PLAN.md` 참조. +> ?꾩껜 怨꾪쉷 ?꾨즺. `docs/STRUCTURAL_REFACTORING_PLAN.md` 李몄“. -### 런처 · 에이전트 리소스/안정성 수정 (2026-04-09) +### ?곗쿂 쨌 ?먯씠?꾪듃 由ъ냼???덉젙???섏젙 (2026-04-09) -| 대상 | 파일 | 변경 | +| ?€??| ?뚯씪 | 蹂€寃?| |------|------|------| -| LauncherWindow 이벤트 누수 | `LauncherWindow.xaml.cs` | `vm.CloseRequested`, `vm.PropertyChanged`, `app.IndexService.IndexRebuilt` 핸들러를 필드 저장 → `OnClosed`에서 `-=` 해제. ViewModel보다 Window가 먼저 닫힐 때 GC 누수 방지 | -| ChatWindow 타이머 정리 | `ChatWindow.xaml.cs` | `Closed` 핸들러에 누락된 8개 타이머 명시적 `Stop()` 추가 + `StopAgentEventProcessor()` 호출 | -| Events 스레드 안전 | `AgentLoopService.cs` | Dispatcher 없을 때 `Events` 접근에 `lock(Events)` 추가 — 동시 EmitEvent 호출 시 IndexOutOfRange 크래시 방지 | -| NotifyTool 타이머 누적 | `NotifyTool.cs` | 알림당 `new DispatcherTimer` → `DoubleAnimation.Completed` 콜백으로 대체. 100개 알림 시 100개 타이머 동시 존재 해소 | -| LauncherWindow 토스트 타이머 | `LauncherWindow.xaml.cs` | `ShowToast()` 매 호출 `new DispatcherTimer` → 재사용 패턴 + 명명 메서드(`ToastTimer_Tick`) | -| LauncherWindow 타이머 정리 | `LauncherWindow.xaml.cs` | `OnClosed`에 `_toastTimer?.Stop()`, `_indexStatusTimer?.Stop()` 추가 | +| LauncherWindow ?대깽???꾩닔 | `LauncherWindow.xaml.cs` | `vm.CloseRequested`, `vm.PropertyChanged`, `app.IndexService.IndexRebuilt` ?몃뱾?щ? ?꾨뱶 ?€????`OnClosed`?먯꽌 `-=` ?댁젣. ViewModel蹂대떎 Window媛€ 癒쇱? ?ロ옄 ??GC ?꾩닔 諛⑹? | +| ChatWindow ?€?대㉧ ?뺣━ | `ChatWindow.xaml.cs` | `Closed` ?몃뱾?ъ뿉 ?꾨씫??8媛??€?대㉧ 紐낆떆??`Stop()` 異붽? + `StopAgentEventProcessor()` ?몄텧 | +| Events ?ㅻ젅???덉쟾 | `AgentLoopService.cs` | Dispatcher ?놁쓣 ??`Events` ?묎렐??`lock(Events)` 異붽? ???숈떆 EmitEvent ?몄텧 ??IndexOutOfRange ?щ옒??諛⑹? | +| NotifyTool ?€?대㉧ ?꾩쟻 | `NotifyTool.cs` | ?뚮┝??`new DispatcherTimer` ??`DoubleAnimation.Completed` 肄쒕갚?쇰줈 ?€泥? 100媛??뚮┝ ??100媛??€?대㉧ ?숈떆 議댁옱 ?댁냼 | +| LauncherWindow ?좎뒪???€?대㉧ | `LauncherWindow.xaml.cs` | `ShowToast()` 留??몄텧 `new DispatcherTimer` ???ъ궗???⑦꽩 + 紐낅챸 硫붿꽌??`ToastTimer_Tick`) | +| LauncherWindow ?€?대㉧ ?뺣━ | `LauncherWindow.xaml.cs` | `OnClosed`??`_toastTimer?.Stop()`, `_indexStatusTimer?.Stop()` 異붽? | -### Hot path · 리소스 추가 최적화 (2026-04-09) +### Hot path 쨌 由ъ냼??異붽? 理쒖쟻??(2026-04-09) -| 대상 | 파일 | 변경 | +| ?€??| ?뚯씪 | 蹂€寃?| |------|------|------| -| GetRuntimeActiveTools 캐시 | `AgentLoopService.cs` | 반복당 1~4회 호출 → `cachedActiveTools` 로컬 변수로 1회 캐시. foreach 내 `activeToolNames` 계산도 루프 밖으로 호이스트 | -| SubAgentTool 취소 전파 | `SubAgentTool.cs` | `CancellationTokenSource.CreateLinkedTokenSource(ct)` 연동. Task.Run + loop.RunAsync에 토큰 전달. 부모 중지 시 자식 즉시 취소 | -| 아이콘 애니메이션 재귀 제어 | `LauncherWindow.xaml.cs` | `sb.Completed`에서 즉시 재귀 → `_iconAnimationDelayTimer` 8초 딜레이. 할당 빈도 75% 감소. 클릭 시 딜레이 취소 후 즉시 전환 | -| JsonSerializerOptions 공유 | `AgentLoopService.cs` | `s_jsonOpts` 정적 필드 추가, 4개 `JsonSerializer.Serialize` 호출에 적용. L4096 `System.Text.Json.` 접두사 정규화 | +| GetRuntimeActiveTools 罹먯떆 | `AgentLoopService.cs` | 諛섎났??1~4???몄텧 ??`cachedActiveTools` 濡쒖뺄 蹂€?섎줈 1??罹먯떆. foreach ??`activeToolNames` 怨꾩궛??猷⑦봽 諛뽰쑝濡??몄씠?ㅽ듃 | +| SubAgentTool 痍⑥냼 ?꾪뙆 | `SubAgentTool.cs` | `CancellationTokenSource.CreateLinkedTokenSource(ct)` ?곕룞. Task.Run + loop.RunAsync???좏겙 ?꾨떖. 遺€紐?以묒? ???먯떇 利됱떆 痍⑥냼 | +| ?꾩씠肄??좊땲硫붿씠???ш? ?쒖뼱 | `LauncherWindow.xaml.cs` | `sb.Completed`?먯꽌 利됱떆 ?ш? ??`_iconAnimationDelayTimer` 8珥??쒕젅?? ?좊떦 鍮덈룄 75% 媛먯냼. ?대┃ ???쒕젅??痍⑥냼 ??利됱떆 ?꾪솚 | +| JsonSerializerOptions 怨듭쑀 | `AgentLoopService.cs` | `s_jsonOpts` ?뺤쟻 ?꾨뱶 異붽?, 4媛?`JsonSerializer.Serialize` ?몄텧???곸슜. L4096 `System.Text.Json.` ?묐몢???뺢퇋??| -### Claude Desktop 스타일 UI 개선 (2026-04-09) +### Claude Desktop ?ㅽ???UI 媛쒖꽑 (2026-04-09) -| 항목 | 파일 | 수정 내용 | +| ??ぉ | ?뚯씪 | ?섏젙 ?댁슜 | |------|------|----------| -| 미리보기 Split Button | `ChatWindow.xaml` | 기존 `BtnPreviewToggle` (Ellipse 점 + "프리뷰") → `[▶ 미리보기 | ∨]` Split Button으로 교체. 좌측 토글, 우측 셰브론 드롭다운 | -| 미리보기 드롭다운 | `ChatWindow.PreviewPresentation.cs` | `ShowPreviewTabDropdown()` — 열린 탭 목록 팝업, 파일 확장자별 아이콘, 활성 탭 하이라이트 | -| PreviewDot → PreviewIcon | `ChatWindow.PreviewPresentation.cs` | `PreviewDot.Fill` 4곳 → `PreviewIcon.Foreground` (AccentColor/SecondaryText) 전환 | -| 셰브론 동기화 | `ChatWindow.PreviewPresentation.cs` | `UpdatePreviewChevronState()` — `_previewTabs.Count` 기반 IsHitTestVisible/Opacity 제어 | -| 계획 버튼 이동 | `ChatWindow.xaml` | MoodIconPanel 동적 주입 → StatusBar XAML 선언 요소 `BtnPlanViewer`로 이동 | -| ShowPlanButton 리팩토링 | `ChatWindow.PlanApprovalPresentation.cs` | 동적 Add/Remove → `Visibility` 토글 단순화 + 레거시 정리 유지 | +| 誘몃━蹂닿린 Split Button | `ChatWindow.xaml` | 湲곗〈 `BtnPreviewToggle` (Ellipse ??+ "?꾨━酉?) ??`[??誘몃━蹂닿린 | ??` Split Button?쇰줈 援먯껜. 醫뚯륫 ?좉?, ?곗륫 ?곕툕濡??쒕∼?ㅼ슫 | +| 誘몃━蹂닿린 ?쒕∼?ㅼ슫 | `ChatWindow.PreviewPresentation.cs` | `ShowPreviewTabDropdown()` ???대┛ ??紐⑸줉 ?앹뾽, ?뚯씪 ?뺤옣?먮퀎 ?꾩씠肄? ?쒖꽦 ???섏씠?쇱씠??| +| PreviewDot ??PreviewIcon | `ChatWindow.PreviewPresentation.cs` | `PreviewDot.Fill` 4怨???`PreviewIcon.Foreground` (AccentColor/SecondaryText) ?꾪솚 | +| ?곕툕濡??숆린??| `ChatWindow.PreviewPresentation.cs` | `UpdatePreviewChevronState()` ??`_previewTabs.Count` 湲곕컲 IsHitTestVisible/Opacity ?쒖뼱 | +| 怨꾪쉷 踰꾪듉 ?대룞 | `ChatWindow.xaml` | MoodIconPanel ?숈쟻 二쇱엯 ??StatusBar XAML ?좎뼵 ?붿냼 `BtnPlanViewer`濡??대룞 | +| ShowPlanButton 由ы뙥?좊쭅 | `ChatWindow.PlanApprovalPresentation.cs` | ?숈쟻 Add/Remove ??`Visibility` ?좉? ?⑥닚??+ ?덇굅???뺣━ ?좎? | -### UX 개선 및 탭별 도구 필터링 (2026-04-09) +### UX 媛쒖꽑 諛???퀎 ?꾧뎄 ?꾪꽣留?(2026-04-09) -| 항목 | 파일 | 수정 내용 | +| ??ぉ | ?뚯씪 | ?섏젙 ?댁슜 | |------|------|----------| -| 탭별 도구 필터링 | `IAgentTool.cs`, `ToolRegistry.cs`, `AgentLoopService.cs` | `TabCategory` 속성 + `ToolTabOverrides` 딕셔너리로 Chat/Cowork/Code 탭별 도구 분류. Chat=0개, Cowork=문서/데이터, Code=개발/태스크 | -| FolderMapTool 기본값 변경 | `FolderMapTool.cs` | `include_files` 기본값 `false` → `true`. Description에 사용 제한 가이드 추가 | -| 에이전트 이벤트 숨김 | `AgentEventRendering.cs`, `TimelinePresentation.cs` | `SessionStart`/`UserPromptSubmit` 내부 이벤트 타임라인 비표시 | -| 사용자 메시지 렌더링 | `ChatWindow.xaml.cs` | 전송 시 `InvalidateTimelineCache()` + `preserveViewport:false`로 즉시 표시 보장 | -| 아카이브 기능 | `ChatModels.cs`, `ConversationManagementPresentation.cs`, `ConversationFilterPresentation.cs`, `ConversationListPresentation.cs` | `Archived` 속성, 컨텍스트 메뉴 아카이브 토글, 사이드바 필터 버튼 | -| 커스텀 슬림 스크롤바 | `ChatWindow.xaml` | 6px 슬림 ScrollBar + Thumb 라운드, ScrollViewer 마우스 오버 fade in/out 애니메이션 | -| 스트리밍 메트릭 레이블 | `ChatWindow.xaml`, `StatusPresentation.cs` | 입력 박스 위에 `StreamMetricsLabel` (경과시간 · ↓ 토큰 수) 실시간 표시 | -| 프리셋 카드 클릭 안정화 | `ChatWindow.xaml`, `ChatWindow.xaml.cs` | WrapPanel `Background="Transparent"` + Dispatcher 우선순위 `Loaded`로 상향 | -| 컨텍스트 토큰 정확도 | `TokenEstimator.cs`, `ContextUsagePresentation.cs`, `ILlmService.cs` | 시스템 프롬프트 + 도구 오버헤드 추정, `_tool_use_blocks`/`tool_result` 할인 | -| UI 프리징 방지 | `ChatWindow.xaml.cs` | `SaveLastConversations()`/`PersistConversationSnapshot()` → 렌더링 후 `Task.Run()` 비동기 실행 | -| 빠른 빌드 스크립트 | `build-quick.sh` | 암호화/난독화/설치프로그램 건너뛰는 개발용 빌드 스크립트 | +| ??퀎 ?꾧뎄 ?꾪꽣留?| `IAgentTool.cs`, `ToolRegistry.cs`, `AgentLoopService.cs` | `TabCategory` ?띿꽦 + `ToolTabOverrides` ?뺤뀛?덈━濡?Chat/Cowork/Code ??퀎 ?꾧뎄 遺꾨쪟. Chat=0媛? Cowork=臾몄꽌/?곗씠?? Code=媛쒕컻/?쒖뒪??| +| FolderMapTool 湲곕낯媛?蹂€寃?| `FolderMapTool.cs` | `include_files` 湲곕낯媛?`false` ??`true`. Description???ъ슜 ?쒗븳 媛€?대뱶 異붽? | +| ?먯씠?꾪듃 ?대깽???④? | `AgentEventRendering.cs`, `TimelinePresentation.cs` | `SessionStart`/`UserPromptSubmit` ?대? ?대깽???€?꾨씪??鍮꾪몴??| +| ?ъ슜??硫붿떆吏€ ?뚮뜑留?| `ChatWindow.xaml.cs` | ?꾩넚 ??`InvalidateTimelineCache()` + `preserveViewport:false`濡?利됱떆 ?쒖떆 蹂댁옣 | +| ?꾩뭅?대툕 湲곕뒫 | `ChatModels.cs`, `ConversationManagementPresentation.cs`, `ConversationFilterPresentation.cs`, `ConversationListPresentation.cs` | `Archived` ?띿꽦, 而⑦뀓?ㅽ듃 硫붾돱 ?꾩뭅?대툕 ?좉?, ?ъ씠?쒕컮 ?꾪꽣 踰꾪듉 | +| 而ㅼ뒪?€ ?щ┝ ?ㅽ겕濡ㅻ컮 | `ChatWindow.xaml` | 6px ?щ┝ ScrollBar + Thumb ?쇱슫?? ScrollViewer 留덉슦???ㅻ쾭 fade in/out ?좊땲硫붿씠??| +| ?ㅽ듃由щ컢 硫뷀듃由??덉씠釉?| `ChatWindow.xaml`, `StatusPresentation.cs` | ?낅젰 諛뺤뒪 ?꾩뿉 `StreamMetricsLabel` (寃쎄낵?쒓컙 쨌 ???좏겙 ?? ?ㅼ떆媛??쒖떆 | +| ?꾨━??移대뱶 ?대┃ ?덉젙??| `ChatWindow.xaml`, `ChatWindow.xaml.cs` | WrapPanel `Background="Transparent"` + Dispatcher ?곗꽑?쒖쐞 `Loaded`濡??곹뼢 | +| 而⑦뀓?ㅽ듃 ?좏겙 ?뺥솗??| `TokenEstimator.cs`, `ContextUsagePresentation.cs`, `ILlmService.cs` | ?쒖뒪???꾨\?꾪듃 + ?꾧뎄 ?ㅻ쾭?ㅻ뱶 異붿젙, `_tool_use_blocks`/`tool_result` ?좎씤 | +| UI ?꾨━吏?諛⑹? | `ChatWindow.xaml.cs` | `SaveLastConversations()`/`PersistConversationSnapshot()` ???뚮뜑留???`Task.Run()` 鍮꾨룞湲??ㅽ뻾 | +| 鍮좊Ⅸ 鍮뚮뱶 ?ㅽ겕由쏀듃 | `build-quick.sh` | ?뷀샇???쒕룆???ㅼ튂?꾨줈洹몃옩 嫄대꼫?곕뒗 媛쒕컻??鍮뚮뱶 ?ㅽ겕由쏀듃 | -### 에이전트 루프 문서 생성 흐름 수정 (2026-04-09) +### ?먯씠?꾪듃 猷⑦봽 臾몄꽌 ?앹꽦 ?먮쫫 ?섏젙 (2026-04-09) -| 파일 | 수정 내용 | +| ?뚯씪 | ?섏젙 ?댁슜 | |------|----------| -| `AgentLoopTransitions.Documents.cs` | `TryHandleTerminalDocumentCompletionTransitionAsync`에서 `document_plan` 없이 바로 문서 도구 호출 시 조기 종료 방지 — LLM이 추가 반복으로 내용을 보강할 수 있도록 허용 | -| `HtmlSkill.cs` | `MarkdownToHtml`에서 LLM이 삽입한 `
` 태그가 이스케이프되는 버그 수정 — 이스케이프 전 플레이스홀더로 보존 후 복원 | +| `AgentLoopTransitions.Documents.cs` | `TryHandleTerminalDocumentCompletionTransitionAsync`?먯꽌 `document_plan` ?놁씠 諛붾줈 臾몄꽌 ?꾧뎄 ?몄텧 ??議곌린 醫낅즺 諛⑹? ??LLM??異붽? 諛섎났?쇰줈 ?댁슜??蹂닿컯?????덈룄濡??덉슜 | +| `HtmlSkill.cs` | `MarkdownToHtml`?먯꽌 LLM???쎌엯??`
` ?쒓렇媛€ ?댁뒪耳€?댄봽?섎뒗 踰꾧렇 ?섏젙 ???댁뒪耳€?댄봽 ???뚮젅?댁뒪?€?붾줈 蹂댁〈 ??蹂듭썝 | -### Cowork 문서 미생성 · 스크롤 · 전송 후 10초 멈춤 수정 (2026-04-09) +### Cowork 臾몄꽌 誘몄깮??쨌 ?ㅽ겕濡?쨌 ?꾩넚 ??10珥?硫덉땄 ?섏젙 (2026-04-09) -#### 문서 생성 탐색 정책 수정 +#### 臾몄꽌 ?앹꽦 ?먯깋 ?뺤콉 ?섏젙 -| 파일 | 수정 내용 | +| ?뚯씪 | ?섏젙 ?댁슜 | |------|----------| -| `AgentLoopExplorationPolicy.cs` | `ExplorationScope.DirectCreation` 신규 스코프 추가. `HasDocumentCreationIntent()`로 "작성해줘/만들어줘/써줘" 등 생성 동사 + 문서/보고서 대상 키워드 감지 | -| `AgentLoopExplorationPolicy.cs` | `DirectCreation` 스코프에서 glob/grep/folder_map 탐색 차단 → `document_plan → docx_create/html_create` 바로 이동 | -| `AgentLoopExplorationPolicy.cs` | `FilterExplorationToolsForCurrentIteration`에서 문서 생성 도구를 최우선 순위로 배치 | -| `AgentLoopExplorationPolicy.cs` | `ShouldInjectExplorationCorrection`에서 DirectCreation 시 탐색 도구 1회 호출만으로 즉시 교정 주입 | -| `TaskTypePolicy.cs` | docs 가이던스를 생성 vs 읽기로 분기 — 생성 시 "반드시 실제 파일을 만들어라" 명시 | -| `AgentLoopService.cs` | DirectCreation 스코프 이벤트 메시지: "문서 생성 모드 · 바로 문서를 만드는 중" | +| `AgentLoopExplorationPolicy.cs` | `ExplorationScope.DirectCreation` ?좉퇋 ?ㅼ퐫??異붽?. `HasDocumentCreationIntent()`濡?"?묒꽦?댁쨾/留뚮뱾?댁쨾/?⑥쨾" ???앹꽦 ?숈궗 + 臾몄꽌/蹂닿퀬???€???ㅼ썙??媛먯? | +| `AgentLoopExplorationPolicy.cs` | `DirectCreation` ?ㅼ퐫?꾩뿉??glob/grep/folder_map ?먯깋 李⑤떒 ??`document_plan ??docx_create/html_create` 諛붾줈 ?대룞 | +| `AgentLoopExplorationPolicy.cs` | `FilterExplorationToolsForCurrentIteration`?먯꽌 臾몄꽌 ?앹꽦 ?꾧뎄瑜?理쒖슦???쒖쐞濡?諛곗튂 | +| `AgentLoopExplorationPolicy.cs` | `ShouldInjectExplorationCorrection`?먯꽌 DirectCreation ???먯깋 ?꾧뎄 1???몄텧留뚯쑝濡?利됱떆 援먯젙 二쇱엯 | +| `TaskTypePolicy.cs` | docs 媛€?대뜕?ㅻ? ?앹꽦 vs ?쎄린濡?遺꾧린 ???앹꽦 ??"諛섎뱶???ㅼ젣 ?뚯씪??留뚮뱾?대씪" 紐낆떆 | +| `AgentLoopService.cs` | DirectCreation ?ㅼ퐫???대깽??硫붿떆吏€: "臾몄꽌 ?앹꽦 紐⑤뱶 쨌 諛붾줈 臾몄꽌瑜?留뚮뱶??以? | -#### 스크롤 버그 수정 +#### ?ㅽ겕濡?踰꾧렇 ?섏젙 -| 파일 | 수정 내용 | +| ?뚯씪 | ?섏젙 ?댁슜 | |------|----------| -| `ChatWindow.xaml.cs` | 메시지 전송/슬래시 커맨드/컴팩트 후 `RenderMessages(preserveViewport: true)` + `ForceScrollToEnd()` 조합 → `RenderMessages(preserveViewport: false)`로 변경. viewport 복원과 ForceScrollToEnd 경합 제거 | +| `ChatWindow.xaml.cs` | 硫붿떆吏€ ?꾩넚/?щ옒??而ㅻ㎤??而댄뙥????`RenderMessages(preserveViewport: true)` + `ForceScrollToEnd()` 議고빀 ??`RenderMessages(preserveViewport: false)`濡?蹂€寃? viewport 蹂듭썝怨?ForceScrollToEnd 寃쏀빀 ?쒓굅 | -**원인**: `preserveViewport: true`는 렌더링 후 이전 스크롤 위치를 복원하는 코드를 `DispatcherPriority.Background`로 예약. `ForceScrollToEnd()`도 같은 우선순위로 하단 스크롤을 예약하여 두 코드가 경합, 스크롤이 하단으로 안 가는 문제 발생. +**?먯씤**: `preserveViewport: true`???뚮뜑留????댁쟾 ?ㅽ겕濡??꾩튂瑜?蹂듭썝?섎뒗 肄붾뱶瑜?`DispatcherPriority.Background`濡??덉빟. `ForceScrollToEnd()`??媛숈? ?곗꽑?쒖쐞濡??섎떒 ?ㅽ겕濡ㅼ쓣 ?덉빟?섏뿬 ??肄붾뱶媛€ 寃쏀빀, ?ㅽ겕濡ㅼ씠 ?섎떒?쇰줈 ??媛€??臾몄젣 諛쒖깮. -#### 전송 후 10초 멈춤 수정 (Critical Performance Fix) +#### ?꾩넚 ??10珥?硫덉땄 ?섏젙 (Critical Performance Fix) -| 파일 | 수정 내용 | +| ?뚯씪 | ?섏젙 ?댁슜 | |------|----------| -| `ChatWindow.AgentStatusPresentation.cs` | `BuildFeedbackContext()` — `_storage.LoadAllMeta()` (모든 .axchat 파일 복호화) + `_storage.Load()` x20회를 매 전송마다 동기 실행 → 1분 캐시 + 현재 대화 피드백만 즉시 반영 + 전체 갱신은 백그라운드 | -| `ChatWindow.xaml.cs` | `Dispatcher.Invoke()` (동기 블로킹) → `Dispatcher.InvokeAsync()` (비동기). background task에서 UI 스레드 블로킹 제거 | -| `ChatWindow.xaml.cs` | `PrepareExecutionForConversation()` (시스템 프롬프트 빌드: 프로젝트 규칙/메모리/피드백 디스크 I/O) → `await Task.Run()`으로 백그라운드 실행. UI 스레드 즉시 해방 | +| `ChatWindow.AgentStatusPresentation.cs` | `BuildFeedbackContext()` ??`_storage.LoadAllMeta()` (紐⑤뱺 .axchat ?뚯씪 蹂듯샇?? + `_storage.Load()` x20?뚮? 留??꾩넚留덈떎 ?숆린 ?ㅽ뻾 ??1遺?罹먯떆 + ?꾩옱 ?€???쇰뱶諛깅쭔 利됱떆 諛섏쁺 + ?꾩껜 媛깆떊?€ 諛깃렇?쇱슫??| +| `ChatWindow.xaml.cs` | `Dispatcher.Invoke()` (?숆린 釉붾줈?? ??`Dispatcher.InvokeAsync()` (鍮꾨룞湲?. background task?먯꽌 UI ?ㅻ젅??釉붾줈???쒓굅 | +| `ChatWindow.xaml.cs` | `PrepareExecutionForConversation()` (?쒖뒪???꾨\?꾪듃 鍮뚮뱶: ?꾨줈?앺듃 洹쒖튃/硫붾え由??쇰뱶諛??붿뒪??I/O) ??`await Task.Run()`?쇰줈 諛깃렇?쇱슫???ㅽ뻾. UI ?ㅻ젅??利됱떆 ?대갑 | -**원인 분석**: `BuildFeedbackContext()`가 `LoadAllMeta()` (모든 `.axchat` 파일 복호화·파싱) + `Load()` x20 (20개 대화 전체 로드·복호화) 를 UI 스레드에서 동기 실행. 대화 30개 이상이면 5~10초 블로킹 발생. +**?먯씤 遺꾩꽍**: `BuildFeedbackContext()`媛€ `LoadAllMeta()` (紐⑤뱺 `.axchat` ?뚯씪 蹂듯샇?붋룻뙆?? + `Load()` x20 (20媛??€???꾩껜 濡쒕뱶쨌蹂듯샇?? 瑜?UI ?ㅻ젅?쒖뿉???숆린 ?ㅽ뻾. ?€??30媛??댁긽?대㈃ 5~10珥?釉붾줈??諛쒖깮. -### 스트리밍 중 UI 버벅임 대폭 개선 (2026-04-09) +### ?ㅽ듃由щ컢 以?UI 踰꾨쾮???€??媛쒖꽑 (2026-04-09) -Claude Desktop(React)은 도구 호출이 수십 건이어도 매끄러운 반면, WPF 앱은 심하게 버벅이는 문제의 원인 분석 및 수정. +Claude Desktop(React)?€ ?꾧뎄 ?몄텧???섏떗 嫄댁씠?대룄 留ㅻ걚?ъ슫 諛섎㈃, WPF ?깆? ?ы븯寃?踰꾨쾮?대뒗 臾몄젣???먯씤 遺꾩꽍 諛??섏젙. -**근본 원인**: React virtual DOM은 변경된 부분만 diff/patch하지만, WPF는 매 렌더마다 전체 시각적 트리를 파괴 후 재생성. +**洹쇰낯 ?먯씤**: React virtual DOM?€ 蹂€寃쎈맂 遺€遺꾨쭔 diff/patch?섏?留? WPF??留??뚮뜑留덈떎 ?꾩껜 ?쒓컖???몃━瑜??뚭눼 ???ъ깮?? -| 원인 | 파일 | 수정 | +| ?먯씤 | ?뚯씪 | ?섏젙 | |------|------|------| -| `ItemsSource = null/재연결` — 전체 시각적 트리 파괴 + VirtualizingStackPanel 컨테이너 재생성 | `TranscriptRenderExecution.cs` | 스트리밍 중에는 ItemsSource 분리/재연결 건너뜀 — ObservableCollection 직접 변경으로 레이아웃 패스 최소화 | -| 라이브 진행 카드 매번 재생성 — 헤더/구분선/스텝 전체를 0부터 다시 생성 + 애니메이션 재적용 | `AgentEventRendering.cs` | `_liveProgressCard` 캐시 + `UpdateLiveProgressStepsInPlace()` — 카드 1회 생성 후 새 스텝만 추가, 기존 스텝은 터치 안 함 | -| 렌더 타이머 간격 1.5~2.2초 — WPF 전체 재빌드에 비해 너무 공격적 | `ChatWindow.xaml.cs` | lightweight: 2.2s→4s, normal: 1.5s→3s — 렌더 간 충분한 여유 확보 | -| 매 렌더마다 3개 애니메이션(Opacity + ScaleX + ScaleY) 재적용 | `AgentEventRendering.cs` | 라이브 카드 in-place 업데이트로 기존 애니메이션 보존, 새 스텝에만 애니메이션 적용 | +| `ItemsSource = null/?ъ뿰寃? ???꾩껜 ?쒓컖???몃━ ?뚭눼 + VirtualizingStackPanel 而⑦뀒?대꼫 ?ъ깮??| `TranscriptRenderExecution.cs` | ?ㅽ듃由щ컢 以묒뿉??ItemsSource 遺꾨━/?ъ뿰寃?嫄대꼫?€ ??ObservableCollection 吏곸젒 蹂€寃쎌쑝濡??덉씠?꾩썐 ?⑥뒪 理쒖냼??| +| ?쇱씠釉?吏꾪뻾 移대뱶 留ㅻ쾲 ?ъ깮?????ㅻ뜑/援щ텇???ㅽ뀦 ?꾩껜瑜?0遺€???ㅼ떆 ?앹꽦 + ?좊땲硫붿씠???ъ쟻??| `AgentEventRendering.cs` | `_liveProgressCard` 罹먯떆 + `UpdateLiveProgressStepsInPlace()` ??移대뱶 1???앹꽦 ?????ㅽ뀦留?異붽?, 湲곗〈 ?ㅽ뀦?€ ?곗튂 ????| +| ?뚮뜑 ?€?대㉧ 媛꾧꺽 1.5~2.2珥???WPF ?꾩껜 ?щ퉴?쒖뿉 鍮꾪빐 ?덈Т 怨듦꺽??| `ChatWindow.xaml.cs` | lightweight: 2.2s??s, normal: 1.5s??s ???뚮뜑 媛?異⑸텇???ъ쑀 ?뺣낫 | +| 留??뚮뜑留덈떎 3媛??좊땲硫붿씠??Opacity + ScaleX + ScaleY) ?ъ쟻??| `AgentEventRendering.cs` | ?쇱씠釉?移대뱶 in-place ?낅뜲?댄듃濡?湲곗〈 ?좊땲硫붿씠??蹂댁〈, ???ㅽ뀦?먮쭔 ?좊땲硫붿씠???곸슜 | -**비교**: +**鍮꾧탳**: -| 항목 | 수정 전 (WPF) | 수정 후 | Claude Desktop (React) | +| ??ぉ | ?섏젙 ??(WPF) | ?섏젙 ??| Claude Desktop (React) | |------|-------------|---------|----------------------| -| 업데이트 전략 | 전체 트리 파괴→재생성 | 4단계: StreamingAppend → Incremental → **DiffRender** → FullRender | Virtual DOM diff | -| 렌더 간격 | 1.5~2.2초 | 3~4초 | ~16ms (requestAnimationFrame) | -| 요소 재사용 | Clear→재생성 | 캐시→재사용 + 키 기반 diff | Recycled/Memoized | -| 애니메이션 | 매번 재적용 (3개/요소) | 1회 적용 후 보존 | CSS transform (GPU) | +| ?낅뜲?댄듃 ?꾨왂 | ?꾩껜 ?몃━ ?뚭눼?믪옱?앹꽦 | 4?④퀎: StreamingAppend ??Incremental ??**DiffRender** ??FullRender | Virtual DOM diff | +| ?뚮뜑 媛꾧꺽 | 1.5~2.2珥?| 3~4珥?| ~16ms (requestAnimationFrame) | +| ?붿냼 ?ъ궗??| Clear?믪옱?앹꽦 | 罹먯떆?믪옱?ъ슜 + ??湲곕컲 diff | Recycled/Memoized | +| ?좊땲硫붿씠??| 留ㅻ쾲 ?ъ쟻??(3媛??붿냼) | 1???곸슜 ??蹂댁〈 | CSS transform (GPU) | --- -### 12-3. Virtual DOM Diff 렌더 (`TryApplyDiffRender`) +### 12-3. Virtual DOM Diff ?뚮뜑 (`TryApplyDiffRender`) -React의 reconciliation과 동일한 원리를 WPF에 적용한 키 기반 diff 렌더입니다. +React??reconciliation怨??숈씪???먮━瑜?WPF???곸슜????湲곕컲 diff ?뚮뜑?낅땲?? -**렌더 체인 (우선순위 순):** +**?뚮뜑 泥댁씤 (?곗꽑?쒖쐞 ??:** ``` -StreamingAppend → Incremental(prefix-match) → DiffRender(key-based) → FullRender +StreamingAppend ??Incremental(prefix-match) ??DiffRender(key-based) ??FullRender ``` -| 단계 | 조건 | 동작 | +| ?④퀎 | 議곌굔 | ?숈옉 | |------|------|------| -| StreamingAppend | 스트리밍 중 + 기존 stable 키가 부분집합 | 새 키만 append | -| Incremental | prefix가 완전 일치 | 꼬리 부분만 추가 | -| **DiffRender** | hiddenCount 동일 + 키 집합 변화 있음 | old에만 있는 키 삭제(뒤→앞) + new에만 있는 키 추가 | -| FullRender | 위 3개 모두 실패 | 전체 Clear→재생성 | +| StreamingAppend | ?ㅽ듃由щ컢 以?+ 湲곗〈 stable ?ㅺ? 遺€遺꾩쭛??| ???ㅻ쭔 append | +| Incremental | prefix媛€ ?꾩쟾 ?쇱튂 | 瑗щ━ 遺€遺꾨쭔 異붽? | +| **DiffRender** | hiddenCount ?숈씪 + ??吏묓빀 蹂€???덉쓬 | old?먮쭔 ?덈뒗 ????젣(?ㅲ넂?? + new?먮쭔 ?덈뒗 ??異붽? | +| FullRender | ??3媛?紐⑤몢 ?ㅽ뙣 | ?꾩껜 Clear?믪옱?앹꽦 | -**핵심 알고리즘:** -1. `oldKeys` → index 딕셔너리 / `newKeys` → HashSet 구축 -2. 라이브 컨테이너 임시 분리 -3. `oldKeys` 뒤에서부터 순회하며 `newKeySet`에 없는 항목 제거 (인덱스 안정성) -4. `renderPlan.VisibleTimeline`에서 `oldKeyIndex`에 없는 항목만 `Render()` -5. 라이브 컨테이너 재삽입 - -**파일:** `ChatWindow.TranscriptRenderExecution.cs`, `ChatWindow.TranscriptRendering.cs` (체인 삽입) +**?듭떖 ?뚭퀬由ъ쬁:** +1. `oldKeys` ??index ?뺤뀛?덈━ / `newKeys` ??HashSet 援ъ텞 +2. ?쇱씠釉?而⑦뀒?대꼫 ?꾩떆 遺꾨━ +3. `oldKeys` ?ㅼ뿉?쒕????쒗쉶?섎ʼn `newKeySet`???녿뒗 ??ぉ ?쒓굅 (?몃뜳???덉젙?? +4. `renderPlan.VisibleTimeline`?먯꽌 `oldKeyIndex`???녿뒗 ??ぉ留?`Render()` +5. ?쇱씠釉?而⑦뀒?대꼫 ?ъ궫?? +**?뚯씪:** `ChatWindow.TranscriptRenderExecution.cs`, `ChatWindow.TranscriptRendering.cs` (泥댁씤 ?쎌엯) --- -### 12-4. LSP 코드 인텔리전스 도구 확장 +### 12-4. LSP 肄붾뱶 ?명뀛由ъ쟾???꾧뎄 ?뺤옣 -`lsp_code_intel` 도구를 6개 액션에서 9개로 확장하여 구조적 코드 탐색을 대폭 강화했습니다. +`lsp_code_intel` ?꾧뎄瑜?6媛??≪뀡?먯꽌 9媛쒕줈 ?뺤옣?섏뿬 援ъ“??肄붾뱶 ?먯깋???€??媛뺥솕?덉뒿?덈떎. -| 액션 | 용도 | 신규 | +| ?≪뀡 | ?⑸룄 | ?좉퇋 | |------|------|------| -| `goto_definition` | 심볼 정의 위치 | | -| `find_references` | 심볼 사용 위치 | | -| `hover` | 타입/문서 정보 | ✅ | -| `goto_implementation` | 인터페이스/추상 구현 위치 | ✅ | -| `symbols` | 파일 내 심볼 목록 | | -| `workspace_symbols` | 워크스페이스 전체 심볼 검색 | ✅ | -| `prepare_call_hierarchy` | 호출 계층 기준 심볼 | ✅ | -| `incoming_calls` | 상위 호출자 | ✅ | -| `outgoing_calls` | 하위 호출 대상 | ✅ | +| `goto_definition` | ?щ낵 ?뺤쓽 ?꾩튂 | | +| `find_references` | ?щ낵 ?ъ슜 ?꾩튂 | | +| `hover` | ?€??臾몄꽌 ?뺣낫 | ??| +| `goto_implementation` | ?명꽣?섏씠??異붿긽 援ы쁽 ?꾩튂 | ??| +| `symbols` | ?뚯씪 ???щ낵 紐⑸줉 | | +| `workspace_symbols` | ?뚰겕?ㅽ럹?댁뒪 ?꾩껜 ?щ낵 寃€??| ??| +| `prepare_call_hierarchy` | ?몄텧 怨꾩링 湲곗? ?щ낵 | ??| +| `incoming_calls` | ?곸쐞 ?몄텧??| ??| +| `outgoing_calls` | ?섏쐞 ?몄텧 ?€??| ??| -**주요 변경:** -- `line`/`character` 입력: 1-based 기대 → 내부에서 0-based 자동 변환 (`NormalizePosition`) -- `query` 파라미터 추가 (workspace_symbols용) -- 결과에 파일 수, 대표 위치, 첫 결과 요약 포함 -- LSP 프로토콜: `textDocument/implementation`, `textDocument/hover`, `workspace/symbol`, `textDocument/prepareCallHierarchy`, `callHierarchy/incomingCalls`, `callHierarchy/outgoingCalls` +**二쇱슂 蹂€寃?** +- `line`/`character` ?낅젰: 1-based 湲곕? ???대??먯꽌 0-based ?먮룞 蹂€??(`NormalizePosition`) +- `query` ?뚮씪誘명꽣 異붽? (workspace_symbols?? +- 寃곌낵???뚯씪 ?? ?€???꾩튂, 泥?寃곌낵 ?붿빟 ?ы븿 +- LSP ?꾨줈?좎퐳: `textDocument/implementation`, `textDocument/hover`, `workspace/symbol`, `textDocument/prepareCallHierarchy`, `callHierarchy/incomingCalls`, `callHierarchy/outgoingCalls` -**파일:** `LspTool.cs`, `LspClientService.cs` +**?뚯씪:** `LspTool.cs`, `LspClientService.cs` --- -### 12-5. IBM/Qwen 도구 이력 평탄화 +### 12-5. IBM/Qwen ?꾧뎄 ?대젰 ?됲깂?? +IBM watsonx + Qwen 諛고룷?뺤뿉??`tool_calls`/`role=tool` ?대젰 寃€?ш? ?꾧꺽??臾몄젣瑜??닿껐?⑸땲?? -IBM watsonx + Qwen 배포형에서 `tool_calls`/`role=tool` 이력 검사가 엄격한 문제를 해결합니다. - -**변경 전:** +**蹂€寃???** ``` -assistant { tool_calls: [...] } → tool { tool_call_id, content } +assistant { tool_calls: [...] } ?? tool { tool_call_id, content } ``` -**변경 후 (평탄 transcript):** +**蹂€寃???(?됲깂 transcript):** ``` -assistant: "텍스트\n\n{name,arguments}\n" +assistant: "?띿뒪??n\n{name,arguments}\n" user: "[Tool Result: tool_name] (id=xxx)\ncontent" ``` -**핵심 메서드:** -- `BuildIbmAssistantTranscript()` — tool_use 블록 → `` 태그 직렬화 -- `BuildIbmToolResultTranscript()` — tool_result → `[Tool Result]` 헤더 + 내용 -- `TryExtractTextContent()` — string/array/nested 형태 모두 텍스트 추출 -- `TryParseContentArrayToolBlock()` — content 배열 내 tool_use/tool_call 블록 파싱 +**?듭떖 硫붿꽌??** +- `BuildIbmAssistantTranscript()` ??tool_use 釉붾줉 ??`` ?쒓렇 吏곷젹??- `BuildIbmToolResultTranscript()` ??tool_result ??`[Tool Result]` ?ㅻ뜑 + ?댁슜 +- `TryExtractTextContent()` ??string/array/nested ?뺥깭 紐⑤몢 ?띿뒪??異붿텧 +- `TryParseContentArrayToolBlock()` ??content 諛곗뿴 ??tool_use/tool_call 釉붾줉 ?뚯떛 -**파일:** `LlmService.ToolUse.cs` +**?뚯씪:** `LlmService.ToolUse.cs` --- -### 12-6. 도구 노출 순서 정렬 및 프롬프트 완화 +### 12-6. ?꾧뎄 ?몄텧 ?쒖꽌 ?뺣젹 諛??꾨\?꾪듃 ?꾪솕 -**도구 순서 (`ToolRegistry.OrderToolsForExposure`):** +**?꾧뎄 ?쒖꽌 (`ToolRegistry.OrderToolsForExposure`):** -| 버킷 | 도구 | +| 踰꾪궥 | ?꾧뎄 | |------|------| -| 0 (최우선) | file_read, file_edit, glob, grep, lsp_code_intel, build_run, document_plan, 생성 도구 등 | +| 0 (理쒖슦?? | file_read, file_edit, glob, grep, lsp_code_intel, build_run, document_plan, ?앹꽦 ?꾧뎄 ??| | 1 | document_review, format_convert, tool_search, code_search | | 2 | mcp_*, spawn_agent, wait_agents | | 3 | task_* | -**프롬프트 완화 (SystemPromptBuilder):** -- "Tools First, Always" → "Tools First When Needed" -- `tool_search`: 후보에서 바로 선택 가능하면 직접 호출, 모호할 때만 사용 -- `spawn_agent`: 병렬 조사가 실제로 도움이 될 때만 사용 -- `document_review`: 큰 문서/명시적 요청 시에만 권장 -- Code 탐색: 정의/참조/구현/호출관계 → `lsp_code_intel` 우선 +**?꾨\?꾪듃 ?꾪솕 (SystemPromptBuilder):** +- "Tools First, Always" ??"Tools First When Needed" +- `tool_search`: ?꾨낫?먯꽌 諛붾줈 ?좏깮 媛€?ν븯硫?吏곸젒 ?몄텧, 紐⑦샇???뚮쭔 ?ъ슜 +- `spawn_agent`: 蹂묐젹 議곗궗媛€ ?ㅼ젣濡??꾩??????뚮쭔 ?ъ슜 +- `document_review`: ??臾몄꽌/紐낆떆???붿껌 ?쒖뿉留?沅뚯옣 +- Code ?먯깋: ?뺤쓽/李몄“/援ы쁽/?몄텧愿€怨???`lsp_code_intel` ?곗꽑 -**파일:** `ToolRegistry.cs`, `ChatWindow.SystemPromptBuilder.cs`, `AgentLoopService.cs`, `TaskTypePolicy.cs`, `AgentLoopExplorationPolicy.cs` +**?뚯씪:** `ToolRegistry.cs`, `ChatWindow.SystemPromptBuilder.cs`, `AgentLoopService.cs`, `TaskTypePolicy.cs`, `AgentLoopExplorationPolicy.cs` --- -## 13. 디렉토리별 가이드 +## 13. ?붾젆?좊━蹂?媛€?대뱶 -| 디렉토리 | 수정 시 주의사항 | +| ?붾젆?좊━ | ?섏젙 ??二쇱쓽?ы빆 | |---------|----------------| -| `Core/` | `FuzzyEngine` 점수 공식 변경 시 검색 품질에 직접 영향 | -| `Handlers/` | 새 핸들러 추가 시 `App.xaml.cs`에 등록 필요 | -| `Services/Agent/` | 새 도구 추가 시 `ToolRegistry`에 등록 + 스킬 파일(`.skill.md`) 작성 + `ToolTabOverrides`에 탭 카테고리 지정 | -| `Themes/` | 리소스 키 변경 시 모든 테마에 동일하게 적용 필요 | -| `Models/AppSettings.cs` | 속성 추가 시 `SettingsService` 마이그레이션 고려 | -| `Views/ChatWindow.*` | partial class 분할 — 관련 기능은 해당 파일에서 수정 | +| `Core/` | `FuzzyEngine` ?먯닔 怨듭떇 蹂€寃???寃€???덉쭏??吏곸젒 ?곹뼢 | +| `Handlers/` | ???몃뱾??異붽? ??`App.xaml.cs`???깅줉 ?꾩슂 | +| `Services/Agent/` | ???꾧뎄 異붽? ??`ToolRegistry`???깅줉 + ?ㅽ궗 ?뚯씪(`.skill.md`) ?묒꽦 + `ToolTabOverrides`????移댄뀒怨좊━ 吏€??| +| `Themes/` | 由ъ냼????蹂€寃???紐⑤뱺 ?뚮쭏???숈씪?섍쾶 ?곸슜 ?꾩슂 | +| `Models/AppSettings.cs` | ?띿꽦 異붽? ??`SettingsService` 留덉씠洹몃젅?댁뀡 怨좊젮 | +| `Views/ChatWindow.*` | partial class 遺꾪븷 ??愿€??湲곕뒫?€ ?대떦 ?뚯씪?먯꽌 ?섏젙 | --- -### 12-7. PPT 고품질 템플릿 시스템 +### 12-7. PPT 怨좏뭹吏??쒗뵆由??쒖뒪?? +`template` ?뚮씪誘명꽣濡?8媛?怨좏뭹吏??묒떇???됱긽/?덉씠?꾩썐???ъ슜?????덉뒿?덈떎. -`template` 파라미터로 8개 고품질 양식의 색상/레이아웃을 사용할 수 있습니다. - -**현재 구현 (방법 1 — 내장 메타데이터):** -- 각 템플릿의 테마 색상을 `FullThemes` 딕셔너리에 하드코딩 (0KB 추가) -- 원본 .pptx 없이도 동일 색상+레이아웃으로 PPT 생성 가능 -- 원본 .pptx가 `Assets/ppt/` 또는 `%APPDATA%/AXCopilot/templates/ppt/`에 있으면 마스터 복제(고품질) 자동 업그레이드 - -| 템플릿 이름 | 원본 파일 | 색상 특징 | +**?꾩옱 援ы쁽 (諛⑸쾿 1 ???댁옣 硫뷀??곗씠??:** +- 媛??쒗뵆由우쓽 ?뚮쭏 ?됱긽??`FullThemes` ?뺤뀛?덈━???섎뱶肄붾뵫 (0KB 異붽?) +- ?먮낯 .pptx ?놁씠???숈씪 ?됱긽+?덉씠?꾩썐?쇰줈 PPT ?앹꽦 媛€??- ?먮낯 .pptx媛€ `Assets/ppt/` ?먮뒗 `%APPDATA%/AXCopilot/templates/ppt/`???덉쑝硫?留덉뒪??蹂듭젣(怨좏뭹吏? ?먮룞 ?낃렇?덉씠?? +| ?쒗뵆由??대쫫 | ?먮낯 ?뚯씪 | ?됱긽 ?뱀쭠 | |------------|----------|----------| -| `basic100` | BASIC100 기준 템플릿 V1.pptx (67MB) | 모던 블루 (#2572EF) | -| `core100` | CORE100 기준템플릿 V1.pptx (141MB) | 딥 블루 (#266DF1) | -| `frame_blue` | 프레임디자인 블루 (19MB) | 프레임 블루 (#126BF6) + 카드 | -| `mr_ppt_01` | 미스터 피피티 01 (18MB) | 다크 네이비 + 블루 (#0049F0) | -| `mr_ppt_02` | 미스터 피피티 02 (24MB) | 블루 + 그레이 카드 (#2269F7) | -| `mr_ppt_03` | 미스터 피피티 03 (5.5MB) | 네이비 + 골드 (#F4BB05) | -| `mr_ppt_04` | 미스터 피피티 04 (8.8MB) | 딥 인디고 + 스카이블루 (#0583F2) | -| `mr_ppt_05` | 미스터 피피티 05 (16MB) | 모던 블랙 + 블루 (#007AF9) | +| `basic100` | BASIC100 湲곗? ?쒗뵆由?V1.pptx (67MB) | 紐⑤뜕 釉붾( (#2572EF) | +| `core100` | CORE100 湲곗??쒗뵆由?V1.pptx (141MB) | ??釉붾( (#266DF1) | +| `frame_blue` | ?꾨젅?꾨뵒?먯씤 釉붾( (19MB) | ?꾨젅??釉붾( (#126BF6) + 移대뱶 | +| `mr_ppt_01` | 誘몄뒪???쇳뵾??01 (18MB) | ?ㅽ겕 ?ㅼ씠鍮?+ 釉붾( (#0049F0) | +| `mr_ppt_02` | 誘몄뒪???쇳뵾??02 (24MB) | 釉붾( + 洹몃젅??移대뱶 (#2269F7) | +| `mr_ppt_03` | 誘몄뒪???쇳뵾??03 (5.5MB) | ?ㅼ씠鍮?+ 怨⑤뱶 (#F4BB05) | +| `mr_ppt_04` | 誘몄뒪???쇳뵾??04 (8.8MB) | ???몃뵒怨?+ ?ㅼ뭅?대툝猷?(#0583F2) | +| `mr_ppt_05` | 誘몄뒪???쇳뵾??05 (16MB) | 紐⑤뜕 釉붾옓 + 釉붾( (#007AF9) | -**향후 구현 옵션:** +**?ν썑 援ы쁽 ?듭뀡:** -#### 방법 2 — 자동 다운로드 (권장) +#### 諛⑸쾿 2 ???먮룞 ?ㅼ슫濡쒕뱶 (沅뚯옣) ``` -첫 사용 시 사내 NAS/서버에서 템플릿 자동 다운로드 → %APPDATA%/AXCopilot/templates/ppt/ 캐시 +泥??ъ슜 ???щ궡 NAS/?쒕쾭?먯꽌 ?쒗뵆由??먮룞 ?ㅼ슫濡쒕뱶 ??%APPDATA%/AXCopilot/templates/ppt/ 罹먯떆 -구현 포인트: -- AppSettings에 TemplateServerUrl 설정 추가 (예: https://nas.internal/ax-templates/) -- ResolveTemplatePath에서 파일 미발견 시 다운로드 트리거 -- 다운로드 진행률 UI (ChatWindow 또는 설정 화면) -- 오프라인 폴백: 내장 메타데이터(방법 1)로 자동 전환 -- 버전 관리: 서버에 manifest.json → 로컬 캐시 버전과 비교 +援ы쁽 ?ъ씤?? +- AppSettings??TemplateServerUrl ?ㅼ젙 異붽? (?? https://nas.internal/ax-templates/) +- ResolveTemplatePath?먯꽌 ?뚯씪 誘몃컻寃????ㅼ슫濡쒕뱶 ?몃━嫄?- ?ㅼ슫濡쒕뱶 吏꾪뻾瑜?UI (ChatWindow ?먮뒗 ?ㅼ젙 ?붾㈃) +- ?ㅽ봽?쇱씤 ?대갚: ?댁옣 硫뷀??곗씠??諛⑸쾿 1)濡??먮룞 ?꾪솚 +- 踰꾩쟾 愿€由? ?쒕쾭??manifest.json ??濡쒖뺄 罹먯떆 踰꾩쟾怨?鍮꾧탳 -예상 작업량: 중 (다운로드 서비스 + UI + 설정) -파일: PptxSkill.cs, AppSettings.cs, SettingsService.cs +?덉긽 ?묒뾽?? 以?(?ㅼ슫濡쒕뱶 ?쒕퉬??+ UI + ?ㅼ젙) +?뚯씪: PptxSkill.cs, AppSettings.cs, SettingsService.cs ``` -#### 방법 3 — 빌드에 포함 +#### 諛⑸쾿 3 ??鍮뚮뱶???ы븿 ``` -csproj에 Content로 등록하여 배포 패키지에 포함 +csproj??Content濡??깅줉?섏뿬 諛고룷 ?⑦궎吏€???ы븿 -구현: -1. AxCopilot.csproj에 아래 추가: +援ы쁽: +1. AxCopilot.csproj???꾨옒 異붽?: PreserveNewest -2. 설치파일 용량 영향: +~200MB (압축 후) - - 현재 설치파일 ~107MB → ~307MB 예상 +2. ?ㅼ튂?뚯씪 ?⑸웾 ?곹뼢: +~200MB (?뺤텞 ?? + - ?꾩옱 ?ㅼ튂?뚯씪 ~107MB ??~307MB ?덉긽 -3. 선택적 포함 (용량 절충): - - 경량 템플릿만 포함 (mr_ppt_03: 5.5MB, mr_ppt_04: 8.8MB 등) - - 대형 템플릿 (core100: 141MB)은 방법 2로 다운로드 +3. ?좏깮???ы븿 (?⑸웾 ?덉땐): + - 寃쎈웾 ?쒗뵆由용쭔 ?ы븿 (mr_ppt_03: 5.5MB, mr_ppt_04: 8.8MB ?? + - ?€???쒗뵆由?(core100: 141MB)?€ 諛⑸쾿 2濡??ㅼ슫濡쒕뱶 - + PreserveNewest -주의: build.bat의 payload.zip 압축 단계에서 자동 포함됨 +二쇱쓽: build.bat??payload.zip ?뺤텞 ?④퀎?먯꽌 ?먮룞 ?ы븿??``` + +--- + +## 14. 吏€?ν삎 ?먯씠?꾪듃 怨좊룄??(oh-my-openagent 李몄“) + +> ?곸꽭 怨꾪쉷: `docs/AGENT_ROADMAP.md` 8??李몄“ + +### 利됱떆 媛쒕컻 (P1~P5) + +| ?쒖쐞 | 湲곕뒫 | ?듭떖 ?뚯씪 | ?ㅻ챸 | +|------|------|----------|------| +| P1 | **IntentGate** (?섎룄 遺꾨쪟湲? | `IntentGateService.cs`(?좉퇋) | ?ъ슜???낅젰 ???묒뾽 ?좏삎 ?먮룞 遺꾨쪟 ??理쒖쟻 ?ㅽ뻾 ?꾨줈?뚯씪(temperature/tool 沅뚰븳/諛섎났 ?곹븳) ?먮룞 ?곸슜. 湲곗〈 `ClassifyTaskType` + `IntentDetector` ?듯빀 ?뺤옣 | +| P2 | **移댄뀒怨좊━ ?쒕툕?먯씠?꾪듃 ?꾨줈?뚯씪** | `SubAgentProfile.cs`(?좉퇋), `SubAgentTool.cs` | ?⑥씪 紐⑤뜽 + ?ㅻⅨ system prompt/tool 沅뚰븳/temperature 議고빀?쇰줈 媛€??硫€?곗뿉?댁쟾?? researcher/coder/writer/reviewer/planner 5媛??꾨줈?뚯씪 | +| P3 | **?꾩쟻 ?숈뒿** | `SessionLearningCollector.cs`(?좉퇋) | ?몄뀡 ??諛쒓껄?ы빆(鍮뚮뱶 ?먮윭, ?뚯씪 援ъ“, ?⑦꽩)???먮룞 ?섏쭛?섏뿬 ?꾩냽 諛섎났??而⑦뀓?ㅽ듃濡?二쇱엯. 諛섎났 ?ㅼ닔 諛⑹? | +| P4 | **?뚰겕?ㅽ럹?댁뒪 而⑦뀓?ㅽ듃 ?먮룞 ?앹꽦** | `WorkspaceContextGenerator.cs`(?좉퇋) | ?묒뾽 ?대뜑 援ъ“/湲곗닠?ㅽ깮??`.ax-context.md`濡??먮룞 ?앹꽦. ?쒕툕?먯씠?꾪듃 而⑦뀓?ㅽ듃 ?⑥쑉??| +| P5 | **蹂묐젹 ?쒕툕?먯씠?꾪듃 ?뺤옣** | `SpawnAgentsTool.cs`(?좉퇋) | ?щ윭 ?쒕툕?먯씠?꾪듃瑜???踰덉뿉 ?앹꽦/?ㅽ뻾. IntentGate ?곕룞?쇰줈 蹂듯빀 ?붿껌 ?먮룞 遺꾪빐 | + +### 異뷀썑 媛쒕컻 (P6~P7) + +| ?쒖쐞 | 湲곕뒫 | ?좏뻾 議곌굔 | ?ㅻ챸 | +|------|------|----------|------| +| P6 | **?대갚 泥댁씤** | P1 + P2 | ?ㅽ뻾 ?ㅽ뙣 ???ㅻⅨ ?꾨줈?뚯씪/?꾨왂?쇰줈 ?먮룞 ?ъ떆??(理쒕? 2?? | +| P7 | **紐⑤뜽 ?깃꺽 留ㅼ묶** | P1 + 硫€?곕え??| ?묒뾽 ?좏삎蹂?理쒖쟻 紐⑤뜽 ?먮룞 ?좏깮 (RegisteredModel.strengths 留ㅼ묶) | + +### 援ы쁽 ?섏〈 愿€怨? +``` +P1 (IntentGate) ?€?€?€?€?€?р??€??P2 (移댄뀒怨좊━ ?꾨줈?뚯씪) ?€?€??P5 (蹂묐젹 ?뺤옣) + ?쒋??€??P3 (?꾩쟻 ?숈뒿) [?낅┰] + ?붴??€??P4 (?뚰겕?ㅽ럹?댁뒪 而⑦뀓?ㅽ듃) [?낅┰] +P1 + P2 ?꾨즺 ???€?€??P6 (?대갚 泥댁씤) +P1 + 硫€?곕え?????€?€??P7 (紐⑤뜽 ?깃꺽 留ㅼ묶) ``` --- -## 14. 지능형 에이전트 고도화 (oh-my-openagent 참조) +## 15. 蹂듭썝 泥댄겕?ъ씤?? +UI ?붿옄???€洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾 蹂듭썝 吏€?먯엯?덈떎. -> 상세 계획: `docs/AGENT_ROADMAP.md` 8절 참조 - -### 즉시 개발 (P1~P5) - -| 순위 | 기능 | 핵심 파일 | 설명 | -|------|------|----------|------| -| P1 | **IntentGate** (의도 분류기) | `IntentGateService.cs`(신규) | 사용자 입력 → 작업 유형 자동 분류 → 최적 실행 프로파일(temperature/tool 권한/반복 상한) 자동 적용. 기존 `ClassifyTaskType` + `IntentDetector` 통합 확장 | -| P2 | **카테고리 서브에이전트 프로파일** | `SubAgentProfile.cs`(신규), `SubAgentTool.cs` | 단일 모델 + 다른 system prompt/tool 권한/temperature 조합으로 가상 멀티에이전트. researcher/coder/writer/reviewer/planner 5개 프로파일 | -| P3 | **누적 학습** | `SessionLearningCollector.cs`(신규) | 세션 내 발견사항(빌드 에러, 파일 구조, 패턴)을 자동 수집하여 후속 반복에 컨텍스트로 주입. 반복 실수 방지 | -| P4 | **워크스페이스 컨텍스트 자동 생성** | `WorkspaceContextGenerator.cs`(신규) | 작업 폴더 구조/기술스택을 `.ax-context.md`로 자동 생성. 서브에이전트 컨텍스트 효율화 | -| P5 | **병렬 서브에이전트 확장** | `SpawnAgentsTool.cs`(신규) | 여러 서브에이전트를 한 번에 생성/실행. IntentGate 연동으로 복합 요청 자동 분해 | - -### 추후 개발 (P6~P7) - -| 순위 | 기능 | 선행 조건 | 설명 | -|------|------|----------|------| -| P6 | **폴백 체인** | P1 + P2 | 실행 실패 시 다른 프로파일/전략으로 자동 재시도 (최대 2회) | -| P7 | **모델 성격 매칭** | P1 + 멀티모델 | 작업 유형별 최적 모델 자동 선택 (RegisteredModel.strengths 매칭) | - -### 구현 의존 관계 - -``` -P1 (IntentGate) ─────┬──→ P2 (카테고리 프로파일) ──→ P5 (병렬 확장) - ├──→ P3 (누적 학습) [독립] - └──→ P4 (워크스페이스 컨텍스트) [독립] -P1 + P2 완료 후 ──→ P6 (폴백 체인) -P1 + 멀티모델 후 ──→ P7 (모델 성격 매칭) -``` - ---- - -## 15. 복원 체크포인트 - -UI 디자인 대규모 리팩토링 등 위험 작업 전 기록한 안전 복원 지점입니다. - -| 날짜 | 커밋 해시 | 설명 | 복원 명령 | +| ?좎쭨 | 而ㅻ컠 ?댁떆 | ?ㅻ챸 | 蹂듭썝 紐낅졊 | |------|-----------|------|-----------| -| 2026-04-13 | `4d1d160` | UI 디자인 개선 직전 — 테마 교정, IBM 진단 로깅, 뷰어 명칭 변경 완료 (704 tests pass) | `git checkout 4d1d160 -- src/AxCopilot/` | +| 2026-04-13 | `4d1d160` | UI ?붿옄??媛쒖꽑 吏곸쟾 ???뚮쭏 援먯젙, IBM 吏꾨떒 濡쒓퉭, 酉곗뼱 紐낆묶 蹂€寃??꾨즺 (704 tests pass) | `git checkout 4d1d160 -- src/AxCopilot/` | -> **전체 롤백**: `git revert <커밋>` 또는 `git reset --hard 4d1d160` (주의: 이후 작업 모두 소실) -> **부분 복원**: `git checkout 4d1d160 -- <파일경로>` 로 특정 파일만 되돌리기 +> **?꾩껜 濡ㅻ갚**: `git revert <而ㅻ컠>` ?먮뒗 `git reset --hard 4d1d160` (二쇱쓽: ?댄썑 ?묒뾽 紐⑤몢 ?뚯떎) +> **遺€遺?蹂듭썝**: `git checkout 4d1d160 -- ` 濡??뱀젙 ?뚯씪留??섎룎由ш린 --- -## 16. 관련 문서 +## 16. 愿€??臾몄꽌 -| 문서 | 내용 | +| 臾몄꽌 | ?댁슜 | |------|------| -| `docs/AGENT_ROADMAP.md` | 에이전트 기능 로드맵 (지능형 고도화 P1~P7 상세 포함) | -| `docs/LAUNCHER_ROADMAP.md` | 런처 기능 로드맵 | -| `docs/OPENCODE_PARITY_PLAN.md` | OpenCode 기능 대응 계획 | -| `docs/TOOL_PARITY_REPORT.md` | 도구 호환성 리포트 | -| `docs/AX_AGENT_UI_CHECKLIST.md` | 에이전트 UI 체크리스트 | -| `docs/UI_UX_CHECKLIST.md` | UI/UX 체크리스트 | -> 업데이트: 2026-04-14 18:08 (KST) -> - 스킬 시스템 Phase 2 1~6번을 반영했습니다. `SkillService`는 프로젝트 `.claude/skills` 재귀 로드, namespaced `SKILL.md`, 번들 스킬 주입, `$ARGUMENTS`/named args/스킬 폴더 변수 치환, inline shell block 실행까지 지원하도록 확장했습니다. -> - `ChatWindow` 런타임 경로도 함께 정리했습니다. 슬래시 호출은 `BuildSlashInvocationAsync`를 통해 컴파일된 스킬 프롬프트를 사용하고, 일반 대화는 `when_to_use`/`paths`/`user-invocable` 메타데이터를 바탕으로 선택된 자동 스킬 가이드를 보조 시스템 프롬프트로 붙입니다. -> - 설정/UI 연결도 새 스킬 모델 기준으로 맞췄습니다. Agent 설정, 일반 설정, 오버레이, 스킬 관리자 도구는 번들/프로젝트/사용자 스킬 분류와 프로젝트 `.claude/skills` 경로를 반영해 설명과 리스트를 구성합니다. -> - 도구 노출 단계는 `AgentLoopService.GetRuntimeActiveTools()`에서 blanket deny 권한을 먼저 적용하도록 보강했습니다. 패턴 기반 규칙은 call-time 검사를 유지하고, 단순 deny 도구는 모델 노출 전 필터링으로 정리했습니다. -> - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase2\\ -p:IntermediateOutputPath=obj\\verify_phase2\\` 경고 0 / 오류 0 -> - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentToolCatalogTests|SkillServiceRuntimePolicyTests" -p:OutputPath=bin\\verify_phase2_tests\\ -p:IntermediateOutputPath=obj\\verify_phase2_tests\\` 통과 16 -> - 참고: 테스트 프로젝트의 기존 nullable 경고 `src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs(76)` 1건은 유지됩니다. -> 업데이트: 2026-04-14 18:22 (KST) -> - 스킬 소스 확장 Phase 3을 반영했습니다. `SkillService`는 상위 디렉터리까지 포함한 프로젝트 `.claude/skills` 탐색, 플러그인 스킬 폴더 탐색, 추가 공용 폴더 목록, `.claude/commands` markdown command를 legacy skill로 변환하는 경로를 함께 지원합니다. -> - 파일형 스킬은 body를 즉시 메모리에 올리지 않고 필요 시점에만 읽는 lazy prompt body 캐시를 추가했습니다. `SkillManagerTool`, `SkillEditorWindow`, `SkillGalleryWindow`는 이 경로를 통해 실제 본문을 표시합니다. -> - 인자 모델도 확장했습니다. `arguments`와 `argument-hint`를 함께 해석해 named placeholder 치환을 강화했고, 인자가 부족하면 usage 가이드를 프롬프트 앞에 붙여 실행 품질을 보완합니다. -> - 도구 deny 필터는 `AgentToolCatalog` 공통 메서드로 이동해 런타임과 설정 UI가 같은 blanket deny 규칙을 공유하도록 정리했습니다. -> - 설정 저장에는 `additionalSkillFolders`를 추가했고, 일반 설정/AX Agent 설정 UI에 줄 단위 입력 필드를 넣어 여러 공용 스킬 폴더를 연결할 수 있게 했습니다. -> - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase3\\ -p:IntermediateOutputPath=obj\\verify_phase3\\` 경고 0 / 오류 0 -> - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentToolCatalogTests|SkillServiceRuntimePolicyTests" -p:OutputPath=bin\\verify_phase3_tests\\ -p:IntermediateOutputPath=obj\\verify_phase3_tests\\` 통과 18 -> - 참고: 테스트 프로젝트의 기존 nullable 경고 `src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs(76)` 1건은 유지됩니다. -> 업데이트: 2026-04-14 18:33 (KST) -> - 스킬 정책 제어를 추가했습니다. `LlmSettings`에 `enableProjectSkillDiscovery`, `enablePluginSkillDiscovery`, `enableLegacyCommandSkills`, `enableSkillInlineShell`, `skillInlineShellTimeoutSeconds`, `skillInlineShellMaxOutputChars`를 추가하고 일반 설정/AX Agent 설정 UI에 연결했습니다. -> - 스킬 로드 시그니처는 이제 소스 디렉터리 목록뿐 아니라 실제 스킬 파일 수와 최근 수정 시각을 함께 반영합니다. 같은 폴더 구성이라도 파일 내용이 바뀌면 다음 로드 요청에서 재탐색됩니다. -> - inline shell 실행기는 설정 기반 비활성화, timeout, 출력 길이 제한을 적용하도록 보강했습니다. 비활성 상태나 시간 초과는 프롬프트 안에서 식별 가능한 안내 문자열로 반환합니다. -> - `SkillEditorWindow`와 `SkillGalleryWindow`는 lazy prompt body 경로를 사용하도록 맞췄고, 설정 변경 후 `ReloadFromCurrentSettings()`를 통해 현재 스킬 소스를 다시 읽도록 정리했습니다. -> - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase4b\\ -p:IntermediateOutputPath=obj\\verify_phase4b\\` 경고 0 / 오류 0 -> - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentToolCatalogTests|SkillServiceRuntimePolicyTests" -p:OutputPath=bin\\verify_phase4b_tests\\ -p:IntermediateOutputPath=obj\\verify_phase4b_tests\\` 통과 18 -> - 참고: 테스트 프로젝트의 기존 nullable 경고 `src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs(76)` 1건은 유지됩니다. +| `docs/AGENT_ROADMAP.md` | ?먯씠?꾪듃 湲곕뒫 濡쒕뱶留?(吏€?ν삎 怨좊룄??P1~P7 ?곸꽭 ?ы븿) | +| `docs/LAUNCHER_ROADMAP.md` | ?곗쿂 湲곕뒫 濡쒕뱶留?| +| `docs/OPENCODE_PARITY_PLAN.md` | OpenCode 湲곕뒫 ?€??怨꾪쉷 | +| `docs/TOOL_PARITY_REPORT.md` | ?꾧뎄 ?명솚??由ы룷??| +| `docs/AX_AGENT_UI_CHECKLIST.md` | ?먯씠?꾪듃 UI 泥댄겕由ъ뒪??| +| `docs/UI_UX_CHECKLIST.md` | UI/UX 泥댄겕由ъ뒪??| +> ?낅뜲?댄듃: 2026-04-14 18:08 (KST) +> - ?ㅽ궗 ?쒖뒪??Phase 2 1~6踰덉쓣 諛섏쁺?덉뒿?덈떎. `SkillService`???꾨줈?앺듃 `.claude/skills` ?ш? 濡쒕뱶, namespaced `SKILL.md`, 踰덈뱾 ?ㅽ궗 二쇱엯, `$ARGUMENTS`/named args/?ㅽ궗 ?대뜑 蹂€??移섑솚, inline shell block ?ㅽ뻾源뚯? 吏€?먰븯?꾨줉 ?뺤옣?덉뒿?덈떎. +> - `ChatWindow` ?고???寃쎈줈???④퍡 ?뺣━?덉뒿?덈떎. ?щ옒???몄텧?€ `BuildSlashInvocationAsync`瑜??듯빐 而댄뙆?쇰맂 ?ㅽ궗 ?꾨\?꾪듃瑜??ъ슜?섍퀬, ?쇰컲 ?€?붾뒗 `when_to_use`/`paths`/`user-invocable` 硫뷀??곗씠?곕? 諛뷀깢?쇰줈 ?좏깮???먮룞 ?ㅽ궗 媛€?대뱶瑜?蹂댁“ ?쒖뒪???꾨\?꾪듃濡?遺숈엯?덈떎. +> - ?ㅼ젙/UI ?곌껐?????ㅽ궗 紐⑤뜽 湲곗??쇰줈 留욎톬?듬땲?? Agent ?ㅼ젙, ?쇰컲 ?ㅼ젙, ?ㅻ쾭?덉씠, ?ㅽ궗 愿€由ъ옄 ?꾧뎄??踰덈뱾/?꾨줈?앺듃/?ъ슜???ㅽ궗 遺꾨쪟?€ ?꾨줈?앺듃 `.claude/skills` 寃쎈줈瑜?諛섏쁺???ㅻ챸怨?由ъ뒪?몃? 援ъ꽦?⑸땲?? +> - ?꾧뎄 ?몄텧 ?④퀎??`AgentLoopService.GetRuntimeActiveTools()`?먯꽌 blanket deny 沅뚰븳??癒쇱? ?곸슜?섎룄濡?蹂닿컯?덉뒿?덈떎. ?⑦꽩 湲곕컲 洹쒖튃?€ call-time 寃€?щ? ?좎??섍퀬, ?⑥닚 deny ?꾧뎄??紐⑤뜽 ?몄텧 ???꾪꽣留곸쑝濡??뺣━?덉뒿?덈떎. +> - 寃€利? `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase2\\ -p:IntermediateOutputPath=obj\\verify_phase2\\` 寃쎄퀬 0 / ?ㅻ쪟 0 +> - 寃€利? `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentToolCatalogTests|SkillServiceRuntimePolicyTests" -p:OutputPath=bin\\verify_phase2_tests\\ -p:IntermediateOutputPath=obj\\verify_phase2_tests\\` ?듦낵 16 +> - 李멸퀬: ?뚯뒪???꾨줈?앺듃??湲곗〈 nullable 寃쎄퀬 `src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs(76)` 1嫄댁? ?좎??⑸땲?? +> ?낅뜲?댄듃: 2026-04-14 18:22 (KST) +> - ?ㅽ궗 ?뚯뒪 ?뺤옣 Phase 3??諛섏쁺?덉뒿?덈떎. `SkillService`???곸쐞 ?붾젆?곕━源뚯? ?ы븿???꾨줈?앺듃 `.claude/skills` ?먯깋, ?뚮윭洹몄씤 ?ㅽ궗 ?대뜑 ?먯깋, 異붽? 怨듭슜 ?대뜑 紐⑸줉, `.claude/commands` markdown command瑜?legacy skill濡?蹂€?섑븯??寃쎈줈瑜??④퍡 吏€?먰빀?덈떎. +> - ?뚯씪???ㅽ궗?€ body瑜?利됱떆 硫붾え由ъ뿉 ?щ━吏€ ?딄퀬 ?꾩슂 ?쒖젏?먮쭔 ?쎈뒗 lazy prompt body 罹먯떆瑜?異붽??덉뒿?덈떎. `SkillManagerTool`, `SkillEditorWindow`, `SkillGalleryWindow`????寃쎈줈瑜??듯빐 ?ㅼ젣 蹂몃Ц???쒖떆?⑸땲?? +> - ?몄옄 紐⑤뜽???뺤옣?덉뒿?덈떎. `arguments`?€ `argument-hint`瑜??④퍡 ?댁꽍??named placeholder 移섑솚??媛뺥솕?덇퀬, ?몄옄媛€ 遺€議깊븯硫?usage 媛€?대뱶瑜??꾨\?꾪듃 ?욎뿉 遺숈뿬 ?ㅽ뻾 ?덉쭏??蹂댁셿?⑸땲?? +> - ?꾧뎄 deny ?꾪꽣??`AgentToolCatalog` 怨듯넻 硫붿꽌?쒕줈 ?대룞???고??꾧낵 ?ㅼ젙 UI媛€ 媛숈? blanket deny 洹쒖튃??怨듭쑀?섎룄濡??뺣━?덉뒿?덈떎. +> - ?ㅼ젙 ?€?μ뿉??`additionalSkillFolders`瑜?異붽??덇퀬, ?쇰컲 ?ㅼ젙/AX Agent ?ㅼ젙 UI??以??⑥쐞 ?낅젰 ?꾨뱶瑜??l뼱 ?щ윭 怨듭슜 ?ㅽ궗 ?대뜑瑜??곌껐?????덇쾶 ?덉뒿?덈떎. +> - 寃€利? `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase3\\ -p:IntermediateOutputPath=obj\\verify_phase3\\` 寃쎄퀬 0 / ?ㅻ쪟 0 +> - 寃€利? `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentToolCatalogTests|SkillServiceRuntimePolicyTests" -p:OutputPath=bin\\verify_phase3_tests\\ -p:IntermediateOutputPath=obj\\verify_phase3_tests\\` ?듦낵 18 +> - 李멸퀬: ?뚯뒪???꾨줈?앺듃??湲곗〈 nullable 寃쎄퀬 `src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs(76)` 1嫄댁? ?좎??⑸땲?? +> ?낅뜲?댄듃: 2026-04-14 18:33 (KST) +> - ?ㅽ궗 ?뺤콉 ?쒖뼱瑜?異붽??덉뒿?덈떎. `LlmSettings`??`enableProjectSkillDiscovery`, `enablePluginSkillDiscovery`, `enableLegacyCommandSkills`, `enableSkillInlineShell`, `skillInlineShellTimeoutSeconds`, `skillInlineShellMaxOutputChars`瑜?異붽??섍퀬 ?쇰컲 ?ㅼ젙/AX Agent ?ㅼ젙 UI???곌껐?덉뒿?덈떎. +> - ?ㅽ궗 濡쒕뱶 ?쒓렇?덉쿂???댁젣 ?뚯뒪 ?붾젆?곕━ 紐⑸줉肉??꾨땲???ㅼ젣 ?ㅽ궗 ?뚯씪 ?섏? 理쒓렐 ?섏젙 ?쒓컖???④퍡 諛섏쁺?⑸땲?? 媛숈? ?대뜑 援ъ꽦?대씪???뚯씪 ?댁슜??諛붾€뚮㈃ ?ㅼ쓬 濡쒕뱶 ?붿껌?먯꽌 ?ы깘?됰맗?덈떎. +> - inline shell ?ㅽ뻾湲곕뒗 ?ㅼ젙 湲곕컲 鍮꾪솢?깊솕, timeout, 異쒕젰 湲몄씠 ?쒗븳???곸슜?섎룄濡?蹂닿컯?덉뒿?덈떎. 鍮꾪솢???곹깭???쒓컙 珥덇낵???꾨\?꾪듃 ?덉뿉???앸퀎 媛€?ν븳 ?덈궡 臾몄옄?대줈 諛섑솚?⑸땲?? +> - `SkillEditorWindow`?€ `SkillGalleryWindow`??lazy prompt body 寃쎈줈瑜??ъ슜?섎룄濡?留욎톬怨? ?ㅼ젙 蹂€寃???`ReloadFromCurrentSettings()`瑜??듯빐 ?꾩옱 ?ㅽ궗 ?뚯뒪瑜??ㅼ떆 ?쎈룄濡??뺣━?덉뒿?덈떎. +> - 寃€利? `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase4b\\ -p:IntermediateOutputPath=obj\\verify_phase4b\\` 寃쎄퀬 0 / ?ㅻ쪟 0 +> - 寃€利? `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentToolCatalogTests|SkillServiceRuntimePolicyTests" -p:OutputPath=bin\\verify_phase4b_tests\\ -p:IntermediateOutputPath=obj\\verify_phase4b_tests\\` ?듦낵 18 +> - 李멸퀬: ?뚯뒪???꾨줈?앺듃??湲곗〈 nullable 寃쎄퀬 `src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs(76)` 1嫄댁? ?좎??⑸땲?? -- 업데이트: 2026-04-14 18:37 (KST) -- claude-code 로컬 스냅샷을 다시 확인했지만, 현재 스냅샷에는 PPT/문서 전용 번들 스킬이 뚜렷하지 않았습니다. 대신 AX가 기본 포함하고 있는 문서형 managed skill 세트를 중심으로 배포 자산 품질을 다듬었습니다. -- pptx-creator, docx-creator, report-writer, prd-generator, meeting-minutes, weekly-report, markdown-to-doc에 when_to_use와 argument-hint 메타를 추가해 proactive skill 선택과 슬래시 호출 가이드를 보강했습니다. -- 일반 설정과 AX Agent 설정의 스킬 목록은 managed 스코프를 별도 기본 제공 스킬 그룹으로 분리했고, 스킬 갤러리도 기본 제공 / 프로젝트 / 플러그인 / 사용자 / 고급 필터와 배지를 사용하도록 정리했습니다. -- 이 변경으로 문서·프레젠테이션 스킬은 빌드 출력 skills 폴더를 통해 기본 배포되면서도, UI에서 사용자 스킬과 구분된 상태로 확인할 수 있습니다. -- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_docskills\\ -p:IntermediateOutputPath=obj\\verify_docskills\\ 경고 0 / 오류 0 +- ?낅뜲?댄듃: 2026-04-14 18:37 (KST) +- claude-code 濡쒖뺄 ?ㅻ깄?룹쓣 ?ㅼ떆 ?뺤씤?덉?留? ?꾩옱 ?ㅻ깄?룹뿉??PPT/臾몄꽌 ?꾩슜 踰덈뱾 ?ㅽ궗???쒕졆?섏? ?딆븯?듬땲?? ?€??AX媛€ 湲곕낯 ?ы븿?섍퀬 ?덈뒗 臾몄꽌??managed skill ?명듃瑜?以묒떖?쇰줈 諛고룷 ?먯궛 ?덉쭏???ㅻ벉?덉뒿?덈떎. +- pptx-creator, docx-creator, report-writer, prd-generator, meeting-minutes, weekly-report, markdown-to-doc??when_to_use?€ argument-hint 硫뷀?瑜?異붽???proactive skill ?좏깮怨??щ옒???몄텧 媛€?대뱶瑜?蹂닿컯?덉뒿?덈떎. +- ?쇰컲 ?ㅼ젙怨?AX Agent ?ㅼ젙???ㅽ궗 紐⑸줉?€ managed ?ㅼ퐫?꾨? 蹂꾨룄 湲곕낯 ?쒓났 ?ㅽ궗 洹몃9?쇰줈 遺꾨━?덇퀬, ?ㅽ궗 媛ㅻ윭由щ룄 湲곕낯 ?쒓났 / ?꾨줈?앺듃 / ?뚮윭洹몄씤 / ?ъ슜??/ 怨좉툒 ?꾪꽣?€ 諛곗?瑜??ъ슜?섎룄濡??뺣━?덉뒿?덈떎. +- ??蹂€寃쎌쑝濡?臾몄꽌쨌?꾨젅?좏뀒?댁뀡 ?ㅽ궗?€ 鍮뚮뱶 異쒕젰 skills ?대뜑瑜??듯빐 湲곕낯 諛고룷?섎㈃?쒕룄, UI?먯꽌 ?ъ슜???ㅽ궗怨?援щ텇???곹깭濡??뺤씤?????덉뒿?덈떎. +- 寃€利? dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_docskills\\ -p:IntermediateOutputPath=obj\\verify_docskills\\ 寃쎄퀬 0 / ?ㅻ쪟 0 -- 업데이트: 2026-04-14 18:45 (KST) -- AX Agent 내부 설정의 스킬 탭 안내 블록에 커스텀 라벨을 추가했습니다. 이제 .claude/skills/.../SKILL.md 프로젝트 호환 경로가 스킬 탭 첫 화면에서 바로 보여, 워크스페이스에 같은 구조가 있으면 AX가 함께 읽는다는 점을 UI에서도 확인할 수 있습니다. -- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_skilllabel\\ -p:IntermediateOutputPath=obj\\verify_skilllabel\\ 경고 0 / 오류 0 +- ?낅뜲?댄듃: 2026-04-14 18:45 (KST) +- AX Agent ?대? ?ㅼ젙???ㅽ궗 ???덈궡 釉붾줉??而ㅼ뒪?€ ?쇰꺼??異붽??덉뒿?덈떎. ?댁젣 .claude/skills/.../SKILL.md ?꾨줈?앺듃 ?명솚 寃쎈줈媛€ ?ㅽ궗 ??泥??붾㈃?먯꽌 諛붾줈 蹂댁뿬, ?뚰겕?ㅽ럹?댁뒪??媛숈? 援ъ“媛€ ?덉쑝硫?AX媛€ ?④퍡 ?쎈뒗?ㅻ뒗 ?먯쓣 UI?먯꽌???뺤씤?????덉뒿?덈떎. +- 寃€利? dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_skilllabel\\ -p:IntermediateOutputPath=obj\\verify_skilllabel\\ 寃쎄퀬 0 / ?ㅻ쪟 0 -- 업데이트: 2026-04-14 19:02 (KST) -- 코워크/코드의 작업 폴더 선택 후 UI가 2~3초 멈추던 흐름을 점검해, 폴더 변경 직후 실행되던 스킬 소스 재탐색을 UI 스레드 밖으로 분리했습니다. 이제 작업 폴더 변경, 탭 전환, 대화 복원 시 필요한 스킬 재로드는 백그라운드에서 수행하고, 조건부 스킬 활성화만 UI에 다시 반영합니다. -- 첨부 파일 추가/제거처럼 작업 폴더가 바뀌지 않는 경로는 기존 스킬 집합만 기준으로 조건부 스킬을 갱신하도록 분리해 불필요한 재탐색도 줄였습니다. -- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_folderpick\\ -p:IntermediateOutputPath=obj\\verify_folderpick\\ 경고 0 / 오류 0 +- ?낅뜲?댄듃: 2026-04-14 19:02 (KST) +- 肄붿썙??肄붾뱶???묒뾽 ?대뜑 ?좏깮 ??UI媛€ 2~3珥?硫덉텛???먮쫫???먭??? ?대뜑 蹂€寃?吏곹썑 ?ㅽ뻾?섎뜕 ?ㅽ궗 ?뚯뒪 ?ы깘?됱쓣 UI ?ㅻ젅??諛뽰쑝濡?遺꾨━?덉뒿?덈떎. ?댁젣 ?묒뾽 ?대뜑 蹂€寃? ???꾪솚, ?€??蹂듭썝 ???꾩슂???ㅽ궗 ?щ줈?쒕뒗 諛깃렇?쇱슫?쒖뿉???섑뻾?섍퀬, 議곌굔遺€ ?ㅽ궗 ?쒖꽦?붾쭔 UI???ㅼ떆 諛섏쁺?⑸땲?? +- 泥⑤? ?뚯씪 異붽?/?쒓굅泥섎읆 ?묒뾽 ?대뜑媛€ 諛붾€뚯? ?딅뒗 寃쎈줈??湲곗〈 ?ㅽ궗 吏묓빀留?湲곗??쇰줈 議곌굔遺€ ?ㅽ궗??媛깆떊?섎룄濡?遺꾨━??遺덊븘?뷀븳 ?ы깘?됰룄 以꾩??듬땲?? +- 寃€利? dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_folderpick\\ -p:IntermediateOutputPath=obj\\verify_folderpick\\ 寃쎄퀬 0 / ?ㅻ쪟 0 -- 업데이트: 2026-04-14 19:16 (KST) -- 분석용 로그 저장 방식을 롤링 형태로 정리했습니다. app, perf, audit, workflow 로그는 날짜별 파일을 유지하되 각 파일이 최대 1MB를 넘지 않도록 오래된 내용부터 밀어내며 새 로그를 이어 붙입니다. -- 공통 유틸 RollingTextLogStore를 추가하고 LogService, AgentPerformanceLogService, AuditLogService, WorkflowLogService에 함께 적용했습니다. -- 공통 로그/성능 로그/감사 로그는 14일까지만 유지하고, 워크플로우 상세 로그는 기존 설정값을 따르되 최대 14일을 넘지 않도록 App 시작 시 상한을 적용했습니다. -- RollingTextLogStoreTests 3건을 추가해 파일 크기 상한 유지, 오래된 파일 삭제, 날짜 디렉터리 삭제 동작을 검증했습니다. -- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_logroll\\ -p:IntermediateOutputPath=obj\\verify_logroll\\ 경고 0 / 오류 0 -- 검증: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter RollingTextLogStoreTests -p:OutputPath=bin\\verify_logroll_tests\\ -p:IntermediateOutputPath=obj\\verify_logroll_tests\\ 통과 3 -- 참고: 테스트 프로젝트의 기존 nullable 경고 src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs(76) 1건은 유지됩니다. +- ?낅뜲?댄듃: 2026-04-14 19:16 (KST) +- 遺꾩꽍??濡쒓렇 ?€??諛⑹떇??濡ㅻ쭅 ?뺥깭濡??뺣━?덉뒿?덈떎. app, perf, audit, workflow 濡쒓렇???좎쭨蹂??뚯씪???좎??섎릺 媛??뚯씪??理쒕? 1MB瑜??섏? ?딅룄濡??ㅻ옒???댁슜遺€??諛€?대궡硫???濡쒓렇瑜??댁뼱 遺숈엯?덈떎. +- 怨듯넻 ?좏떥 RollingTextLogStore瑜?異붽??섍퀬 LogService, AgentPerformanceLogService, AuditLogService, WorkflowLogService???④퍡 ?곸슜?덉뒿?덈떎. +- 怨듯넻 濡쒓렇/?깅뒫 濡쒓렇/媛먯궗 濡쒓렇??14?쇨퉴吏€留??좎??섍퀬, ?뚰겕?뚮줈???곸꽭 濡쒓렇??湲곗〈 ?ㅼ젙媛믪쓣 ?곕Ⅴ??理쒕? 14?쇱쓣 ?섏? ?딅룄濡?App ?쒖옉 ???곹븳???곸슜?덉뒿?덈떎. +- RollingTextLogStoreTests 3嫄댁쓣 異붽????뚯씪 ?ш린 ?곹븳 ?좎?, ?ㅻ옒???뚯씪 ??젣, ?좎쭨 ?붾젆?곕━ ??젣 ?숈옉??寃€利앺뻽?듬땲?? +- 寃€利? dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_logroll\\ -p:IntermediateOutputPath=obj\\verify_logroll\\ 寃쎄퀬 0 / ?ㅻ쪟 0 +- 寃€利? dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter RollingTextLogStoreTests -p:OutputPath=bin\\verify_logroll_tests\\ -p:IntermediateOutputPath=obj\\verify_logroll_tests\\ ?듦낵 3 +- 李멸퀬: ?뚯뒪???꾨줈?앺듃??湲곗〈 nullable 寃쎄퀬 src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs(76) 1嫄댁? ?좎??⑸땲?? diff --git a/src/AxCopilot.Tests/Services/AgentCommandQueueTests.cs b/src/AxCopilot.Tests/Services/AgentCommandQueueTests.cs new file mode 100644 index 0000000..5e97125 --- /dev/null +++ b/src/AxCopilot.Tests/Services/AgentCommandQueueTests.cs @@ -0,0 +1,34 @@ +using AxCopilot.Services.Agent; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Services; + +public class AgentCommandQueueTests +{ + [Fact] + public void DrainAll_ShouldRespectPriorityThenSequence() + { + var queue = new AgentCommandQueue(); + queue.EnqueuePrompt("later", "later"); + queue.EnqueuePrompt("next", "next"); + queue.EnqueuePrompt("now", "now", requestInterrupt: true); + + var drained = queue.DrainAll(); + + drained.Select(x => x.Content).Should().Equal("now", "next", "later"); + drained[0].RequestInterrupt.Should().BeTrue(); + } + + [Fact] + public void Clear_ShouldRemoveQueuedItems() + { + var queue = new AgentCommandQueue(); + queue.EnqueuePrompt("first"); + queue.EnqueueNotification("note"); + + queue.Clear(); + + queue.DrainAll().Should().BeEmpty(); + } +} diff --git a/src/AxCopilot.Tests/Services/AgentToolResultBudgetTests.cs b/src/AxCopilot.Tests/Services/AgentToolResultBudgetTests.cs new file mode 100644 index 0000000..6528f3b --- /dev/null +++ b/src/AxCopilot.Tests/Services/AgentToolResultBudgetTests.cs @@ -0,0 +1,58 @@ +using AxCopilot.Models; +using AxCopilot.Services.Agent; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Services; + +public class AgentToolResultBudgetTests +{ + [Fact] + public void Apply_ShouldPersistPreviewToSourceAndReuseItOnNextQueryView() + { + var longContent = new string('A', 1400); + var sourceMessages = new List + { + new() + { + MsgId = "tool-1", + Role = "user", + Content = $$"""{"type":"tool_result","tool_use_id":"call-1","tool_name":"file_read","content":"{{longContent}}"}""" + }, + new() + { + MsgId = "tail-1", + Role = "assistant", + Content = "recent tail" + } + }; + + var firstWindow = sourceMessages.Select(message => new ChatMessage + { + MsgId = message.MsgId, + Role = message.Role, + Content = message.Content, + QueryPreviewContent = message.QueryPreviewContent, + Timestamp = message.Timestamp + }).ToList(); + + var first = AgentToolResultBudget.Apply(firstWindow, protectedRecentNonSystemMessages: 1, sourceMessages: sourceMessages); + + sourceMessages[0].QueryPreviewContent.Should().NotBeNullOrWhiteSpace(); + first.TruncatedCount.Should().Be(1); + + var secondWindow = sourceMessages.Select(message => new ChatMessage + { + MsgId = message.MsgId, + Role = message.Role, + Content = message.Content, + QueryPreviewContent = message.QueryPreviewContent, + Timestamp = message.Timestamp + }).ToList(); + + var second = AgentToolResultBudget.Apply(secondWindow, protectedRecentNonSystemMessages: 1, sourceMessages: sourceMessages); + + second.ReusedPreviewCount.Should().Be(1); + secondWindow[0].Content.Should().Be(sourceMessages[0].QueryPreviewContent); + } +} diff --git a/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs b/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs new file mode 100644 index 0000000..4677c66 --- /dev/null +++ b/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs @@ -0,0 +1,38 @@ +using AxCopilot.Services; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Services; + +public class CodeLanguageCatalogTests +{ + [Theory] + [InlineData(".cs", "csharp")] + [InlineData(".py", "python")] + [InlineData(".java", "java")] + [InlineData(".go", null)] + [InlineData(".unknown", null)] + public void DetectLspLanguageId_ShouldMatchCatalog(string extension, string? expected) + { + CodeLanguageCatalog.DetectLspLanguageId($"sample{extension}").Should().Be(expected); + } + + [Theory] + [InlineData(".go", true)] + [InlineData(".rs", true)] + [InlineData(".sql", true)] + [InlineData(".foo", false)] + public void IsCodeLikeFile_ShouldReflectBuiltInSupport(string extension, bool expected) + { + CodeLanguageCatalog.IsCodeLikeFile(extension).Should().Be(expected); + } + + [Fact] + public void SupportDescriptions_ShouldContainBroadStaticSupportAndFocusedLspSupport() + { + CodeLanguageCatalog.BuildStaticSupportDescription().Should().Contain("Go"); + CodeLanguageCatalog.BuildStaticSupportDescription().Should().Contain("Rust"); + CodeLanguageCatalog.BuildLspSupportDescription().Should().Contain("C# (.NET)"); + CodeLanguageCatalog.BuildLspSupportDescription().Should().NotContain("Go"); + } +} diff --git a/src/AxCopilot.Tests/Services/DocumentPlannerPresentationTests.cs b/src/AxCopilot.Tests/Services/DocumentPlannerPresentationTests.cs new file mode 100644 index 0000000..f4c1af4 --- /dev/null +++ b/src/AxCopilot.Tests/Services/DocumentPlannerPresentationTests.cs @@ -0,0 +1,40 @@ +using System.IO; +using System.Text.Json; +using AxCopilot.Services.Agent; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Services; + +public class DocumentPlannerPresentationTests +{ + [Fact] + public async Task ExecuteAsync_ForPresentation_ShouldReturnConsultingStoryline() + { + var tool = new DocumentPlannerTool(); + var context = new AgentContext + { + WorkFolder = Path.GetTempPath(), + Permission = "Auto", + OperationMode = "external", + }; + + var args = JsonDocument.Parse( + """ + { + "topic": "2026 사업 전략 제안", + "document_type": "presentation", + "target_pages": 6 + } + """).RootElement; + + var result = await tool.ExecuteAsync(args, context, CancellationToken.None); + + result.Success.Should().BeTrue(); + result.Output.Should().Contain("Executive Summary"); + result.Output.Should().Contain("Situation & Imperative"); + result.Output.Should().Contain("Options & Recommendation"); + result.Output.Should().Contain("Implementation Roadmap"); + result.Output.Should().Contain("Impact & Ask"); + } +} diff --git a/src/AxCopilot.Tests/Services/PptxSkillConsultingDeckTests.cs b/src/AxCopilot.Tests/Services/PptxSkillConsultingDeckTests.cs new file mode 100644 index 0000000..20017ba --- /dev/null +++ b/src/AxCopilot.Tests/Services/PptxSkillConsultingDeckTests.cs @@ -0,0 +1,118 @@ +using System.IO; +using System.Text.Json; +using AxCopilot.Services.Agent; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Services; + +public class PptxSkillConsultingDeckTests +{ + [Fact] + public async Task ExecuteAsync_ShouldCreateDeck_WithConsultingLayouts() + { + var workDir = Path.Combine(Path.GetTempPath(), "ax-pptx-consulting-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(workDir); + + try + { + var context = new AgentContext + { + WorkFolder = workDir, + Permission = "Auto", + OperationMode = "external", + }; + + var tool = new PptxSkill(); + var args = JsonDocument.Parse( + """ + { + "path": "consulting-deck.pptx", + "theme": "professional", + "slides": [ + { + "layout": "executive_summary", + "title": "Executive Summary", + "headline": "핵심 투자 우선순위를 재배치하면 두 개 분기 내 수익성을 개선할 수 있습니다.", + "summary_points": [ + "유지율 개선이 신규 확보보다 높은 수익 효과를 보입니다.", + "상위 3개 비용 항목에 구조적 비효율이 확인되었습니다.", + "즉시 실행 가능한 과제와 중기 투자 과제를 분리해야 합니다." + ], + "recommendation": "CRM 고도화와 운영 자동화를 병행하고 저효율 캠페인은 축소합니다.", + "kpis": [ + { "label": "매출총이익", "value": "+4.2%p", "trend": "2Q forecast", "note": "product mix 개선" }, + { "label": "CAC", "value": "-11%", "trend": "target", "note": "퍼널 최적화" }, + { "label": "Retention", "value": "+6pt", "trend": "12M", "note": "핵심 세그먼트" } + ] + }, + { + "layout": "comparison", + "title": "Options", + "headline": "균형형 대안이 실행 속도와 확장성의 균형이 가장 좋습니다.", + "options": [ + { "name": "Option A", "pros": "빠른 적용", "cons": "확장성 제한", "verdict": "Fastest" }, + { "name": "Option B", "pros": "균형 잡힌 투자", "cons": "의사결정 필요", "verdict": "Recommended" }, + { "name": "Option C", "pros": "장기 최적화", "cons": "리드타임 길음", "verdict": "Strategic" } + ] + }, + { + "layout": "roadmap", + "title": "Roadmap", + "phases": [ + { "title": "Phase 1", "detail": "진단 및 설계", "timeline": "0-30일", "owner": "PM" }, + { "title": "Phase 2", "detail": "구현 및 검증", "timeline": "30-60일", "owner": "Delivery" }, + { "title": "Phase 3", "detail": "확산 및 운영정착", "timeline": "60-90일", "owner": "Business" } + ] + }, + { + "layout": "kpi_dashboard", + "title": "KPI Dashboard", + "summary_points": [ + "비용 구조와 유지율 개선이 동시에 진행될 때 목표 이익률 달성이 가능합니다." + ], + "kpis": [ + { "label": "Revenue", "value": "12%", "trend": "YoY", "note": "Strong" }, + { "label": "Cost", "value": "-8%", "trend": "vs plan", "note": "Improving" }, + { "label": "NPS", "value": "61", "trend": "survey", "note": "Stable" }, + { "label": "Delivery", "value": "94%", "trend": "SLA", "note": "On track" } + ] + }, + { + "layout": "recommendation", + "title": "Recommendation", + "recommendation": "고객가치가 높은 세그먼트에 집중하고 운영 자동화로 비용을 즉시 절감합니다.", + "summary_points": [ + "저효율 캠페인 예산을 재배치할 수 있습니다.", + "고객 이탈 구간을 CRM 자동화로 줄일 수 있습니다.", + "성과 확인까지의 시간이 상대적으로 짧습니다." + ], + "next_steps": [ + "2주 내 우선순위 확정", + "6주 내 자동화 MVP 구축", + "분기 말 KPI 리뷰" + ] + } + ] + } + """).RootElement; + + var result = await tool.ExecuteAsync(args, context, CancellationToken.None); + + result.Success.Should().BeTrue(); + File.Exists(Path.Combine(workDir, "consulting-deck.pptx")).Should().BeTrue(); + result.Output.Should().Contain("PPTX"); + } + finally + { + try + { + if (Directory.Exists(workDir)) + Directory.Delete(workDir, true); + } + catch + { + } + } + } +} diff --git a/src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs b/src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs index 96cc8c1..d0846b9 100644 --- a/src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs +++ b/src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs @@ -73,7 +73,7 @@ public class WorkspaceContextGeneratorTests var result = WorkspaceContextGenerator.LoadContext(tempDir); result.Should().NotBeNull(); result!.Should().Contain("(truncated)"); - result.Length.Should().BeLessOrEqualTo(4100); // 4000 + truncation message + result!.Length.Should().BeLessOrEqualTo(4100); // 4000 + truncation message } finally { diff --git a/src/AxCopilot/Models/ChatModels.cs b/src/AxCopilot/Models/ChatModels.cs index c901be9..5d87dc9 100644 --- a/src/AxCopilot/Models/ChatModels.cs +++ b/src/AxCopilot/Models/ChatModels.cs @@ -286,6 +286,13 @@ public class ChatMessage /// 첨부된 이미지 목록. base64 인코딩된 이미지 데이터. [JsonPropertyName("images")] public List? Images { get; set; } + + /// + /// 긴 tool_result를 query view에서 안정적으로 재사용하기 위한 축약본. + /// 원본 content는 유지하고, 모델에 보낼 때만 이 preview를 우선 사용합니다. + /// + [JsonPropertyName("queryPreviewContent")] + public string? QueryPreviewContent { get; set; } } public class ChatExecutionEvent diff --git a/src/AxCopilot/Services/Agent/AgentCommandQueue.cs b/src/AxCopilot/Services/Agent/AgentCommandQueue.cs new file mode 100644 index 0000000..61c1e82 --- /dev/null +++ b/src/AxCopilot/Services/Agent/AgentCommandQueue.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace AxCopilot.Services.Agent; + +public enum AgentQueuePriority +{ + Now = 0, + Next = 1, + Later = 2, +} + +public enum AgentCommandKind +{ + Prompt, + Notification, +} + +public sealed record AgentQueuedCommand( + long Sequence, + AgentCommandKind Kind, + AgentQueuePriority Priority, + string Content, + DateTime CreatedAt, + bool RequestInterrupt); + +/// +/// 에이전트 실행 중 추가 입력과 내부 알림을 관리하는 경량 큐. +/// UI 구조는 유지하면서도 우선순위/종류를 명확히 구분해 +/// 실행 중 메시지 유입을 안정적으로 처리하도록 돕습니다. +/// +public sealed class AgentCommandQueue +{ + private readonly ConcurrentQueue _now = new(); + private readonly ConcurrentQueue _next = new(); + private readonly ConcurrentQueue _later = new(); + private long _sequence; + + public void EnqueuePrompt(string content, string priority = "next", bool requestInterrupt = false) + => Enqueue(AgentCommandKind.Prompt, content, priority, requestInterrupt); + + public void EnqueueNotification(string content, string priority = "later") + => Enqueue(AgentCommandKind.Notification, content, priority, requestInterrupt: false); + + public void Clear() + { + while (_now.TryDequeue(out _)) { } + while (_next.TryDequeue(out _)) { } + while (_later.TryDequeue(out _)) { } + } + + public List DrainAll() + { + var items = new List(); + DrainInto(_now, items); + DrainInto(_next, items); + DrainInto(_later, items); + return items + .OrderBy(x => x.Priority) + .ThenBy(x => x.Sequence) + .ToList(); + } + + private void Enqueue(AgentCommandKind kind, string content, string? priority, bool requestInterrupt) + { + if (string.IsNullOrWhiteSpace(content)) + return; + + var item = new AgentQueuedCommand( + Interlocked.Increment(ref _sequence), + kind, + NormalizePriority(priority), + content.Trim(), + DateTime.Now, + requestInterrupt); + + switch (item.Priority) + { + case AgentQueuePriority.Now: + _now.Enqueue(item); + break; + case AgentQueuePriority.Later: + _later.Enqueue(item); + break; + default: + _next.Enqueue(item); + break; + } + } + + private static void DrainInto(ConcurrentQueue queue, List target) + { + while (queue.TryDequeue(out var item)) + target.Add(item); + } + + private static AgentQueuePriority NormalizePriority(string? priority) + => priority?.Trim().ToLowerInvariant() switch + { + "now" => AgentQueuePriority.Now, + "later" => AgentQueuePriority.Later, + _ => AgentQueuePriority.Next, + }; +} diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.cs b/src/AxCopilot/Services/Agent/AgentLoopService.cs index c4be07f..89ea9fc 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopService.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopService.cs @@ -36,14 +36,64 @@ public partial class AgentLoopService }; private readonly ConcurrentDictionary _pendingPermissionPreviews = new(StringComparer.OrdinalIgnoreCase); - /// 실행 중 사용자 메시지 주입 큐 (Claude Code 스타일 mid-execution steering). - private readonly ConcurrentQueue _pendingUserMessages = new(); + /// 실행 중 추가 입력/알림 큐. 우선순위와 종류를 함께 보존합니다. + private readonly AgentCommandQueue _pendingCommands = new(); /// 실행 중인 에이전트 루프에 사용자 메시지를 주입합니다. 다음 LLM 호출 전에 반영됩니다. public void InjectUserMessage(string message) { if (!string.IsNullOrWhiteSpace(message)) - _pendingUserMessages.Enqueue(message); + _pendingCommands.EnqueuePrompt( + message, + IsRunning ? "now" : "next", + requestInterrupt: IsRunning); + } + + private void DrainPendingCommands(List messages) + { + var drained = _pendingCommands.DrainAll(); + if (drained.Count == 0) + return; + + var interruptingPrompts = drained.Count(x => x.Kind == AgentCommandKind.Prompt && x.RequestInterrupt); + if (interruptingPrompts > 0) + { + messages.Add(new ChatMessage + { + Role = "system", + MetaKind = "queued_input_interrupt", + Content = $"[queued input] {interruptingPrompts} new prompt(s) arrived during execution. Prioritize the newest user direction before continuing.", + Timestamp = DateTime.Now, + }); + } + + foreach (var item in drained) + { + switch (item.Kind) + { + case AgentCommandKind.Notification: + messages.Add(new ChatMessage + { + Role = "system", + MetaKind = "queue_notification", + Content = item.Content, + Timestamp = item.CreatedAt, + }); + EmitEvent(AgentEventType.Thinking, "", item.Content); + break; + + default: + messages.Add(new ChatMessage + { + Role = "user", + MetaKind = item.RequestInterrupt ? "queued_prompt_interrupt" : "queued_prompt", + Content = item.Content, + Timestamp = item.CreatedAt, + }); + EmitEvent(AgentEventType.UserMessage, "", item.Content); + break; + } + } } /// 에이전트 이벤트 스트림 (UI 바인딩용). @@ -179,7 +229,7 @@ public partial class AgentLoopService _currentRunId = Guid.NewGuid().ToString("N"); _docFallbackAttempted = false; _documentPlanApproved = false; - while (_pendingUserMessages.TryDequeue(out _)) { } // 이전 실행의 잔여 메시지 제거 + _pendingCommands.Clear(); // 이전 실행의 잔여 큐 제거 var llm = _settings.Settings.Llm; var baseMax = llm.MaxAgentIterations > 0 ? llm.MaxAgentIterations : 25; var maxIterations = baseMax; // 동적 조정 가능 @@ -440,11 +490,7 @@ public partial class AgentLoopService } // ── 실행 중 사용자 메시지 주입 (Claude Code 스타일 steering) ── - while (_pendingUserMessages.TryDequeue(out var injectedMsg)) - { - messages.Add(new ChatMessage { Role = "user", Content = injectedMsg }); - EmitEvent(AgentEventType.UserMessage, "", injectedMsg); - } + DrainPendingCommands(messages); var queryView = AgentQueryContextBuilder.Build(messages); var queryMessages = queryView.Messages; @@ -455,6 +501,7 @@ public partial class AgentLoopService $"start={queryView.WindowStartIndex}, " + $"pairs={queryView.PreservedToolPairCount}, " + $"tool_result_budget={queryView.TruncatedToolResultCount}, " + + $"tool_result_preview_reuse={queryView.ReusedToolResultPreviewCount}, " + $"tokens {queryView.TokensBeforeBudget}->{queryView.TokensAfterBudget}"; WorkflowLogService.LogTransition( _conversationId, diff --git a/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs b/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs index 964d768..122fe09 100644 --- a/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs +++ b/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs @@ -12,6 +12,7 @@ public sealed class AgentQueryContextWindowResult public bool ToolPairExpanded { get; init; } public int PreservedToolPairCount { get; init; } public int TruncatedToolResultCount { get; init; } + public int ReusedToolResultPreviewCount { get; init; } public int TokensBeforeBudget { get; init; } public int TokensAfterBudget { get; init; } } @@ -38,6 +39,7 @@ public static class AgentQueryContextBuilder ToolPairExpanded = false, PreservedToolPairCount = 0, TruncatedToolResultCount = 0, + ReusedToolResultPreviewCount = 0, TokensBeforeBudget = 0, TokensAfterBudget = 0, }; @@ -65,7 +67,7 @@ public static class AgentQueryContextBuilder InjectPostCompactContextMessage(windowMessages); var tokensBeforeBudget = TokenEstimator.EstimateMessages(windowMessages); - var budgetResult = AgentToolResultBudget.Apply(windowMessages, ProtectedRecentNonSystemMessages); + var budgetResult = AgentToolResultBudget.Apply(windowMessages, ProtectedRecentNonSystemMessages, sourceMessages: sourceMessages); var tokensAfterBudget = TokenEstimator.EstimateMessages(windowMessages); return new AgentQueryContextWindowResult @@ -78,6 +80,7 @@ public static class AgentQueryContextBuilder ToolPairExpanded = toolPairExpanded, PreservedToolPairCount = preservedToolPairs, TruncatedToolResultCount = budgetResult.TruncatedCount, + ReusedToolResultPreviewCount = budgetResult.ReusedPreviewCount, TokensBeforeBudget = tokensBeforeBudget, TokensAfterBudget = tokensAfterBudget, }; @@ -130,6 +133,7 @@ public static class AgentQueryContextBuilder PromptTokens = source.PromptTokens, CompletionTokens = source.CompletionTokens, AttachedFiles = source.AttachedFiles?.ToList(), + QueryPreviewContent = source.QueryPreviewContent, Images = source.Images?.Select(image => new ImageAttachment { Base64 = image.Base64, diff --git a/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs b/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs index 4f01a12..6b12129 100644 --- a/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs +++ b/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs @@ -8,6 +8,7 @@ public sealed class AgentToolResultBudgetResult public int TruncatedCount { get; set; } public int ProcessedCount { get; set; } public int TotalCharsBefore { get; set; } + public int ReusedPreviewCount { get; set; } } /// @@ -22,9 +23,13 @@ public static class AgentToolResultBudget List messages, int protectedRecentNonSystemMessages, int softCharLimit = DefaultSoftCharLimit, - int aggregateBudgetChars = DefaultAggregateBudgetChars) + int aggregateBudgetChars = DefaultAggregateBudgetChars, + IReadOnlyList? sourceMessages = null) { var result = new AgentToolResultBudgetResult(); + var sourceById = sourceMessages? + .Where(message => !string.IsNullOrWhiteSpace(message.MsgId)) + .ToDictionary(message => message.MsgId, StringComparer.OrdinalIgnoreCase); var nonSystemIndexes = messages .Select((message, index) => new { message, index }) .Where(x => !string.Equals(x.message.Role, "system", StringComparison.OrdinalIgnoreCase)) @@ -49,6 +54,15 @@ public static class AgentToolResultBudget result.ProcessedCount++; result.TotalCharsBefore += content.Length; + + if (!string.IsNullOrWhiteSpace(message.QueryPreviewContent)) + { + messages[i] = CloneMessage(message, message.QueryPreviewContent); + result.TruncatedCount++; + result.ReusedPreviewCount++; + continue; + } + spentChars += content.Length; if (content.Length <= softCharLimit && spentChars <= aggregateBudgetChars) continue; @@ -57,6 +71,14 @@ public static class AgentToolResultBudget if (string.Equals(truncated, content, StringComparison.Ordinal)) continue; + message.QueryPreviewContent = truncated; + if (sourceById != null + && sourceById.TryGetValue(message.MsgId, out var source) + && string.IsNullOrWhiteSpace(source.QueryPreviewContent)) + { + source.QueryPreviewContent = truncated; + } + messages[i] = CloneMessage(message, truncated); result.TruncatedCount++; } @@ -147,6 +169,7 @@ public static class AgentToolResultBudget PromptTokens = source.PromptTokens, CompletionTokens = source.CompletionTokens, AttachedFiles = source.AttachedFiles?.ToList(), + QueryPreviewContent = source.QueryPreviewContent, Images = source.Images?.Select(image => new ImageAttachment { Base64 = image.Base64, diff --git a/src/AxCopilot/Services/Agent/DocumentPlannerTool.cs b/src/AxCopilot/Services/Agent/DocumentPlannerTool.cs index 529e34e..9a1d3cf 100644 --- a/src/AxCopilot/Services/Agent/DocumentPlannerTool.cs +++ b/src/AxCopilot/Services/Agent/DocumentPlannerTool.cs @@ -616,6 +616,15 @@ public class DocumentPlannerTool : IAgentTool new() { Id = "sec-4", Heading = "4. 액션 아이템", Level = 1, KeyPoints = ["담당자", "기한", "세부 내용"] }, new() { Id = "sec-5", Heading = "5. 다음 회의", Level = 1, KeyPoints = ["예정일", "주요 안건"] }, }, + "presentation" => new List + { + new() { Id = "sec-1", Heading = "1. Executive Summary", Level = 1, KeyPoints = ["핵심 메시지", "의사결정 포인트", "한 줄 권고안"] }, + new() { Id = "sec-2", Heading = "2. Situation & Imperative", Level = 1, KeyPoints = ["배경", "왜 지금 중요한가", "문제 정의"] }, + new() { Id = "sec-3", Heading = "3. Key Findings", Level = 1, KeyPoints = ["핵심 데이터", "주요 인사이트", "시사점"] }, + new() { Id = "sec-4", Heading = "4. Options & Recommendation", Level = 1, KeyPoints = ["대안 비교", "추천안", "선택 근거"] }, + new() { Id = "sec-5", Heading = "5. Implementation Roadmap", Level = 1, KeyPoints = ["단계", "일정", "책임 주체"] }, + new() { Id = "sec-6", Heading = "6. Impact & Ask", Level = 1, KeyPoints = ["기대 효과", "핵심 KPI", "의사결정 요청 사항"] }, + }, _ => new List { new() { Id = "sec-1", Heading = "1. 개요", Level = 1, KeyPoints = ["배경", "목적", "범위"] }, diff --git a/src/AxCopilot/Services/Agent/LspTool.cs b/src/AxCopilot/Services/Agent/LspTool.cs index ed3005b..017e6cf 100644 --- a/src/AxCopilot/Services/Agent/LspTool.cs +++ b/src/AxCopilot/Services/Agent/LspTool.cs @@ -22,6 +22,7 @@ public class LspTool : IAgentTool, IDisposable "- action=\"prepare_call_hierarchy\": 현재 위치의 호출 계층 기준 심볼을 확인합니다\n" + "- action=\"incoming_calls\": 현재 심볼을 호출하는 상위 호출자를 찾습니다\n" + "- action=\"outgoing_calls\": 현재 심볼이 호출하는 하위 호출 대상을 찾습니다\n" + + $"지원 언어 서버: {Services.CodeLanguageCatalog.BuildLspSupportDescription()}\n" + "line/character는 기본적으로 1-based 입력을 기대하며, 0-based 값도 호환 처리합니다."; public ToolParameterSchema Parameters => new() @@ -298,19 +299,7 @@ public class LspTool : IAgentTool, IDisposable } private static string? DetectLanguage(string filePath) - { - var ext = Path.GetExtension(filePath).ToLowerInvariant(); - return ext switch - { - ".cs" => "csharp", - ".ts" or ".tsx" => "typescript", - ".js" or ".jsx" => "javascript", - ".py" => "python", - ".cpp" or ".cc" or ".cxx" or ".c" or ".h" or ".hpp" => "cpp", - ".java" => "java", - _ => null - }; - } + => Services.CodeLanguageCatalog.DetectLspLanguageId(filePath); private static int NormalizePosition(int value) { diff --git a/src/AxCopilot/Services/Agent/PptxSkill.cs b/src/AxCopilot/Services/Agent/PptxSkill.cs index 4c22922..12bd4bf 100644 --- a/src/AxCopilot/Services/Agent/PptxSkill.cs +++ b/src/AxCopilot/Services/Agent/PptxSkill.cs @@ -36,6 +36,10 @@ public class PptxSkill : IAgentTool "section (chapter divider), two_column (title+left+right), " + "table (title+real styled table), quote (styled quotation slide), blank, " + "image_full (full-slide background image), " + + "executive_summary (consulting-style headline and takeaways), " + + "recommendation (single recommendation with rationale), " + + "roadmap (phase timeline), comparison (option comparison cards), " + + "kpi_dashboard (metric cards and takeaways), " + "chart (native OpenXML chart — bar/line/pie with embedded data). " + "Built-in themes (each controls BOTH colors AND layout/composition): " + "professional (bar_left), modern (top_band+card), dark (center_bold), " + @@ -72,10 +76,17 @@ public class PptxSkill : IAgentTool Type = "array", Description = "Array of slide objects. Each slide: " + - "{\"layout\": \"title|content|section|two_column|table|quote|blank|image_full|chart\", " + + "{\"layout\": \"title|content|section|two_column|table|quote|blank|image_full|chart|executive_summary|recommendation|roadmap|comparison|kpi_dashboard\", " + "\"title\": \"...\", \"subtitle\": \"...\", " + - "\"body\": \"bullet1\\nbullet2\\n - sub-bullet (IMPORTANT: write 5-8 detailed bullet points per slide to fill the space fully)\", " + + "\"headline\": \"single-message headline for the slide\", " + + "\"body\": \"3-5 concise evidence-backed bullets or short statements\", " + "\"left\": \"...\", \"right\": \"...\", " + + "\"summary_points\": [\"key point 1\", \"key point 2\"], " + + "\"recommendation\": \"recommended action or decision\", " + + "\"next_steps\": [\"step 1\", \"step 2\"], " + + "\"phases\": [{\"title\":\"Phase 1\",\"detail\":\"Design\",\"timeline\":\"Q1\",\"owner\":\"PM\"}], " + + "\"options\": [{\"name\":\"Option A\",\"pros\":\"...\",\"cons\":\"...\",\"verdict\":\"Recommended\"}], " + + "\"kpis\": [{\"label\":\"Revenue\",\"value\":\"12%\",\"trend\":\"YoY\",\"note\":\"Top-line growth\"}], " + "\"headers\": [\"col1\",\"col2\"], \"rows\": [[\"a\",\"b\"]], " + "\"quote\": \"Quote text\", \"author\": \"Author\", " + "\"image\": \"path/to/image.png (local file, embedded in slide; position: top_right|bottom_right|center|full|left_half|right_half; default: center)\", " + @@ -818,6 +829,21 @@ public class PptxSkill : IAgentTool case "chart": BuildChartSlide(slidePart, shapeTree, slideEl, fullTheme, slideW, slideH, ref sid); break; + case "executive_summary": + BuildExecutiveSummarySlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid); + break; + case "recommendation": + BuildRecommendationSlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid); + break; + case "roadmap": + BuildRoadmapSlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid); + break; + case "comparison": + BuildComparisonSlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid); + break; + case "kpi_dashboard": + BuildKpiDashboardSlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid); + break; case "blank": break; default: // content @@ -1432,6 +1458,334 @@ public class PptxSkill : IAgentTool /// bar(세로 막대), line(꺾은선), pie(원형) 차트를 지원합니다. /// ChartPart에 내장 스프레드시트 데이터를 포함하므로 PowerPoint에서 편집 가능합니다. /// + private sealed record PresentationCardItem(string Title, string Primary, string Secondary, string Badge); + + private static void BuildExecutiveSummarySlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id) + { + var c = theme.Colors; + const long M = 360000; + AddRect(t, ref id, 0, 0, W, H, c.Bg); + AddRect(t, ref id, 0, 0, W, 170000, c.Accent); + + var title = CoalesceText(Str(s, "title"), "Executive Summary"); + var headline = CoalesceText(Str(s, "headline"), Str(s, "subtitle"), title); + var summaryPoints = GetStringList(s, "summary_points"); + if (summaryPoints.Count == 0) + summaryPoints = GetStringList(s, "body"); + if (summaryPoints.Count == 0) + summaryPoints = ["핵심 메시지를 3개 이하로 정리", "가장 중요한 수치와 근거를 포함", "의사결정자가 다음 행동을 바로 이해할 수 있도록 구성"]; + + var recommendation = CoalesceText(Str(s, "recommendation"), Str(s, "subtitle"), "권고안과 기대효과를 한 문장으로 정리"); + var kpis = GetStructuredItems(s, "kpis"); + + AddText(t, ref id, M, 220000, W - M * 2, 420000, title, 2200, c.Accent, bold: true, align: "l"); + AddText(t, ref id, M, 520000, W - M * 2, 760000, headline, 3200, c.Primary, bold: true, align: "l"); + + long leftX = M; + long topY = 1420000; + long gap = 220000; + long leftW = (W - M * 2 - gap) * 58 / 100; + long rightX = leftX + leftW + gap; + long rightW = W - M - rightX; + long topH = 2300000; + + AddRoundedRect(t, ref id, leftX, topY, leftW, topH, c.BgAlt); + AddText(t, ref id, leftX + 120000, topY + 70000, leftW - 240000, 300000, "Key Takeaways", 1800, c.Primary, bold: true, align: "l"); + AddBulletBody(t, ref id, leftX + 100000, topY + 360000, leftW - 200000, topH - 460000, + string.Join("\n", summaryPoints.Select(x => $"- {x}")), 1700, c.TextDark, c.Accent); + + AddRoundedRect(t, ref id, rightX, topY, rightW, topH, c.Primary); + AddText(t, ref id, rightX + 120000, topY + 70000, rightW - 240000, 260000, "Recommendation", 1800, c.TextLight, bold: true, align: "l"); + AddTextEx(t, ref id, rightX + 120000, topY + 420000, rightW - 240000, 1100000, recommendation, 2200, c.TextLight, bold: true, italic: false, align: "l"); + var rationaleLines = GetStringList(s, "next_steps"); + if (rationaleLines.Count == 0) + rationaleLines = GetStringList(s, "body").Take(2).ToList(); + if (rationaleLines.Count > 0) + AddBulletBody(t, ref id, rightX + 100000, topY + 1500000, rightW - 200000, topH - 1620000, + string.Join("\n", rationaleLines.Select(x => $"- {x}")), 1500, c.TextLight, c.Accent); + + if (kpis.Count > 0) + { + var metricCount = Math.Min(3, kpis.Count); + long cardY = topY + topH + 200000; + long cardGap = 180000; + long cardW = (W - M * 2 - cardGap * (metricCount - 1)) / metricCount; + long cardH = 1050000; + + for (int i = 0; i < metricCount; i++) + { + var metric = kpis[i]; + var cardX = M + i * (cardW + cardGap); + AddRoundedRect(t, ref id, cardX, cardY, cardW, cardH, c.BgAlt); + AddText(t, ref id, cardX + 90000, cardY + 70000, cardW - 180000, 220000, metric.Title, 1400, c.TextDark, bold: false, align: "l"); + AddText(t, ref id, cardX + 90000, cardY + 260000, cardW - 180000, 330000, CoalesceText(metric.Primary, "-"), 2600, c.Primary, bold: true, align: "l"); + AddText(t, ref id, cardX + 90000, cardY + 640000, cardW - 180000, 170000, CoalesceText(metric.Secondary, metric.Badge), 1300, c.Accent, bold: true, align: "l"); + } + } + } + + private static void BuildRecommendationSlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id) + { + var c = theme.Colors; + const long M = 380000; + AddRect(t, ref id, 0, 0, W, H, c.Bg); + + var title = CoalesceText(Str(s, "title"), "Recommendation"); + var recommendation = CoalesceText(Str(s, "recommendation"), Str(s, "headline"), Str(s, "subtitle"), "권고안을 한 문장으로 제시"); + var reasons = GetStringList(s, "summary_points"); + if (reasons.Count == 0) + reasons = GetStringList(s, "body"); + var nextSteps = GetStringList(s, "next_steps"); + + AddText(t, ref id, M, 220000, W - M * 2, 360000, title, 2200, c.Accent, bold: true, align: "l"); + AddText(t, ref id, M, 520000, W - M * 2, 420000, recommendation, 3000, c.Primary, bold: true, align: "l"); + + long mainY = 1220000; + long gap = 220000; + long leftW = (W - M * 2 - gap) * 52 / 100; + long rightX = M + leftW + gap; + long rightW = W - M - rightX; + long boxH = 2450000; + + AddRoundedRect(t, ref id, M, mainY, leftW, boxH, c.Primary); + AddText(t, ref id, M + 120000, mainY + 80000, leftW - 240000, 240000, "Recommended Move", 1800, c.TextLight, bold: true, align: "l"); + AddTextEx(t, ref id, M + 120000, mainY + 400000, leftW - 240000, 1200000, recommendation, 2400, c.TextLight, bold: true, italic: false, align: "l"); + if (nextSteps.Count > 0) + { + AddText(t, ref id, M + 120000, mainY + 1780000, leftW - 240000, 180000, "Immediate Actions", 1600, c.Accent, bold: true, align: "l"); + AddBulletBody(t, ref id, M + 100000, mainY + 1990000, leftW - 200000, boxH - 2090000, + string.Join("\n", nextSteps.Take(3).Select(x => $"- {x}")), 1450, c.TextLight, c.Accent); + } + + AddRoundedRect(t, ref id, rightX, mainY, rightW, boxH, c.BgAlt); + AddText(t, ref id, rightX + 120000, mainY + 80000, rightW - 240000, 240000, "Why This Wins", 1800, c.Primary, bold: true, align: "l"); + AddBulletBody(t, ref id, rightX + 100000, mainY + 360000, rightW - 200000, boxH - 480000, + string.Join("\n", reasons.Take(5).Select(x => $"- {x}")), 1600, c.TextDark, c.Accent); + } + + private static void BuildRoadmapSlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id) + { + var c = theme.Colors; + const long M = 360000; + AddRect(t, ref id, 0, 0, W, H, c.Bg); + + var title = CoalesceText(Str(s, "title"), "Implementation Roadmap"); + var headline = CoalesceText(Str(s, "headline"), Str(s, "subtitle"), "단계별 우선순위와 산출물을 한 눈에 정리"); + var phases = GetStructuredItems(s, "phases"); + if (phases.Count == 0) + { + phases = + [ + new("Phase 1", "진단 및 설계", "0-30일", "PM"), + new("Phase 2", "구현 및 검증", "30-60일", "Delivery"), + new("Phase 3", "확산 및 운영정착", "60-90일", "Business") + ]; + } + + AddText(t, ref id, M, 220000, W - M * 2, 320000, title, 2200, c.Accent, bold: true, align: "l"); + AddText(t, ref id, M, 500000, W - M * 2, 420000, headline, 2800, c.Primary, bold: true, align: "l"); + AddRect(t, ref id, M, 1260000, W - M * 2, 12000, c.Accent); + + var phaseCount = Math.Min(4, phases.Count); + long gap = 160000; + long laneY = 1540000; + long laneW = (W - M * 2 - gap * (phaseCount - 1)) / phaseCount; + long laneH = 2100000; + + for (int i = 0; i < phaseCount; i++) + { + var phase = phases[i]; + var laneX = M + i * (laneW + gap); + var fill = i % 2 == 0 ? c.BgAlt : c.Bg; + AddRoundedRect(t, ref id, laneX, laneY, laneW, laneH, fill); + AddRect(t, ref id, laneX, laneY, laneW, 160000, i % 2 == 0 ? c.Primary : c.Accent); + AddText(t, ref id, laneX + 90000, laneY + 220000, laneW - 180000, 240000, phase.Title, 1800, c.Primary, bold: true, align: "l"); + AddText(t, ref id, laneX + 90000, laneY + 520000, laneW - 180000, 520000, CoalesceText(phase.Primary, "핵심 과업 정의"), 1500, c.TextDark, bold: false, align: "l"); + AddText(t, ref id, laneX + 90000, laneY + 1200000, laneW - 180000, 180000, CoalesceText(phase.Secondary, "기간 미정"), 1350, c.Accent, bold: true, align: "l"); + AddText(t, ref id, laneX + 90000, laneY + 1450000, laneW - 180000, 180000, CoalesceText(phase.Badge, "담당 미정"), 1250, c.TextDark, bold: false, align: "l"); + } + } + + private static void BuildComparisonSlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id) + { + var c = theme.Colors; + const long M = 340000; + AddRect(t, ref id, 0, 0, W, H, c.Bg); + + var title = CoalesceText(Str(s, "title"), "Option Comparison"); + var headline = CoalesceText(Str(s, "headline"), Str(s, "subtitle"), "대안별 장단점과 권고안을 비교"); + var options = GetStructuredItems(s, "options"); + if (options.Count == 0) + { + options = + [ + new("Option A", "빠른 적용", "확장성 제한", "Fastest"), + new("Option B", "균형 잡힌 투자", "의사결정 필요", "Recommended"), + new("Option C", "장기 최적화", "리드타임 길음", "Strategic") + ]; + } + + AddText(t, ref id, M, 220000, W - M * 2, 320000, title, 2200, c.Accent, bold: true, align: "l"); + AddText(t, ref id, M, 500000, W - M * 2, 420000, headline, 2800, c.Primary, bold: true, align: "l"); + + var optionCount = Math.Min(3, options.Count); + long gap = 160000; + long cardY = 1360000; + long cardH = 2600000; + long cardW = (W - M * 2 - gap * (optionCount - 1)) / optionCount; + + for (int i = 0; i < optionCount; i++) + { + var option = options[i]; + var cardX = M + i * (cardW + gap); + AddRoundedRect(t, ref id, cardX, cardY, cardW, cardH, c.BgAlt); + AddRect(t, ref id, cardX, cardY, cardW, 140000, i == 1 ? c.Accent : c.Primary); + AddText(t, ref id, cardX + 90000, cardY + 220000, cardW - 180000, 240000, option.Title, 1800, c.Primary, bold: true, align: "l"); + AddText(t, ref id, cardX + 90000, cardY + 560000, cardW - 180000, 180000, "Pros", 1400, c.Accent, bold: true, align: "l"); + AddTextEx(t, ref id, cardX + 90000, cardY + 760000, cardW - 180000, 520000, CoalesceText(option.Primary, "-"), 1450, c.TextDark, bold: false, italic: false, align: "l"); + AddText(t, ref id, cardX + 90000, cardY + 1480000, cardW - 180000, 180000, "Risks", 1400, c.Accent, bold: true, align: "l"); + AddTextEx(t, ref id, cardX + 90000, cardY + 1680000, cardW - 180000, 460000, CoalesceText(option.Secondary, "-"), 1450, c.TextDark, bold: false, italic: false, align: "l"); + AddText(t, ref id, cardX + 90000, cardY + 2280000, cardW - 180000, 160000, CoalesceText(option.Badge, ""), 1300, c.Primary, bold: true, align: "l"); + } + } + + private static void BuildKpiDashboardSlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id) + { + var c = theme.Colors; + const long M = 360000; + AddRect(t, ref id, 0, 0, W, H, c.Bg); + + var title = CoalesceText(Str(s, "title"), "KPI Dashboard"); + var headline = CoalesceText(Str(s, "headline"), Str(s, "subtitle"), "핵심 지표의 현재 상태와 시사점"); + var metrics = GetStructuredItems(s, "kpis"); + if (metrics.Count == 0) + { + metrics = + [ + new("Revenue", "12%", "YoY", "Strong"), + new("Cost", "-8%", "vs plan", "Improving"), + new("NPS", "61", "survey", "Stable"), + new("Delivery", "94%", "SLA", "On track") + ]; + } + + AddText(t, ref id, M, 220000, W - M * 2, 320000, title, 2200, c.Accent, bold: true, align: "l"); + AddText(t, ref id, M, 500000, W - M * 2, 420000, headline, 2800, c.Primary, bold: true, align: "l"); + + long cardTop = 1320000; + long gap = 180000; + long cardW = (W - M * 2 - gap) / 2; + long cardH = 1160000; + + for (int i = 0; i < Math.Min(4, metrics.Count); i++) + { + var metric = metrics[i]; + long row = i / 2; + long col = i % 2; + long x = M + col * (cardW + gap); + long y = cardTop + row * (cardH + 180000); + AddRoundedRect(t, ref id, x, y, cardW, cardH, c.BgAlt); + AddText(t, ref id, x + 100000, y + 80000, cardW - 200000, 180000, metric.Title, 1450, c.TextDark, bold: false, align: "l"); + AddText(t, ref id, x + 100000, y + 280000, cardW - 200000, 340000, CoalesceText(metric.Primary, "-"), 2800, c.Primary, bold: true, align: "l"); + AddText(t, ref id, x + 100000, y + 700000, cardW - 200000, 150000, CoalesceText(metric.Secondary, ""), 1300, c.Accent, bold: true, align: "l"); + AddText(t, ref id, x + 100000, y + 900000, cardW - 200000, 140000, CoalesceText(metric.Badge, ""), 1200, c.TextDark, bold: false, align: "l"); + } + + var takeawayLines = GetStringList(s, "summary_points"); + if (takeawayLines.Count == 0) + takeawayLines = GetStringList(s, "body"); + if (takeawayLines.Count > 0) + { + AddRect(t, ref id, M, H - 650000, W - M * 2, 9000, c.Accent); + AddText(t, ref id, M, H - 560000, W - M * 2, 140000, "Implication", 1500, c.Primary, bold: true, align: "l"); + AddTextEx(t, ref id, M, H - 380000, W - M * 2, 220000, takeawayLines[0], 1450, c.TextDark, bold: false, italic: false, align: "l"); + } + } + + private static List GetStringList(JsonElement source, string propertyName) + { + if (!source.SafeTryGetProperty(propertyName, out var value)) + return []; + + if (value.ValueKind == JsonValueKind.Array) + { + return value.EnumerateArray() + .Select(item => item.ValueKind switch + { + JsonValueKind.String => item.SafeGetString() ?? string.Empty, + JsonValueKind.Object => CoalesceText( + FirstNonEmpty(item, "title", "name", "label", "value", "detail", "note"), + item.ToString()), + _ => item.ToString() + }) + .Where(item => !string.IsNullOrWhiteSpace(item)) + .ToList(); + } + + if (value.ValueKind == JsonValueKind.String) + { + return (value.SafeGetString() ?? string.Empty) + .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(item => !string.IsNullOrWhiteSpace(item)) + .ToList(); + } + + return []; + } + + private static List GetStructuredItems(JsonElement source, string propertyName) + { + var items = new List(); + if (!source.SafeTryGetProperty(propertyName, out var value) || value.ValueKind != JsonValueKind.Array) + return items; + + foreach (var item in value.EnumerateArray()) + { + if (item.ValueKind == JsonValueKind.String) + { + var text = item.SafeGetString() ?? string.Empty; + if (!string.IsNullOrWhiteSpace(text)) + items.Add(new PresentationCardItem(text, string.Empty, string.Empty, string.Empty)); + continue; + } + + if (item.ValueKind != JsonValueKind.Object) + continue; + + items.Add(new PresentationCardItem( + CoalesceText(FirstNonEmpty(item, "title", "name", "label"), "Item"), + CoalesceText(FirstNonEmpty(item, "value", "detail", "pros", "timeline", "summary"), string.Empty), + CoalesceText(FirstNonEmpty(item, "note", "cons", "trend", "owner", "secondary"), string.Empty), + CoalesceText(FirstNonEmpty(item, "badge", "verdict", "status", "tag"), string.Empty))); + } + + return items; + } + + private static string? FirstNonEmpty(JsonElement source, params string[] keys) + { + foreach (var key in keys) + { + if (!source.SafeTryGetProperty(key, out var value)) + continue; + + var text = value.ValueKind switch + { + JsonValueKind.String => value.SafeGetString(), + JsonValueKind.Number => value.ToString(), + _ => null + }; + + if (!string.IsNullOrWhiteSpace(text)) + return text; + } + + return null; + } + + private static string CoalesceText(params string?[] values) + => values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim() ?? string.Empty; + private static void BuildChartSlide(SlidePart slidePart, ShapeTree tree, JsonElement s, FullTheme theme, long W, long H, ref uint id) { diff --git a/src/AxCopilot/Services/CodeIndexService.cs b/src/AxCopilot/Services/CodeIndexService.cs index a172d3e..2f7096d 100644 --- a/src/AxCopilot/Services/CodeIndexService.cs +++ b/src/AxCopilot/Services/CodeIndexService.cs @@ -35,14 +35,7 @@ public class CodeIndexService : IDisposable "import", "export", "function", "const", "let", "def", "self", }; - private static readonly HashSet CodeExtensions = new(StringComparer.OrdinalIgnoreCase) - { - ".cs", ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".cpp", ".c", ".h", ".hpp", - ".go", ".rs", ".rb", ".php", ".swift", ".kt", ".scala", - ".html", ".css", ".scss", ".json", ".xml", ".yaml", ".yml", - ".md", ".txt", ".sql", ".sh", ".bat", ".ps1", - ".csproj", ".sln", ".gradle", ".pom", - }; + private static readonly IReadOnlyCollection CodeExtensions = CodeLanguageCatalog.CodeExtensions; // ── DB 초기화 ─────────────────────────────────────────────────────── diff --git a/src/AxCopilot/Services/CodeLanguageCatalog.cs b/src/AxCopilot/Services/CodeLanguageCatalog.cs new file mode 100644 index 0000000..968851b --- /dev/null +++ b/src/AxCopilot/Services/CodeLanguageCatalog.cs @@ -0,0 +1,304 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Text; + +namespace AxCopilot.Services; + +public sealed record CodeLanguageCapability( + string Key, + string DisplayName, + IReadOnlyList Extensions, + IReadOnlyList Guidance, + string? LspLanguageId = null, + bool ShowInQuickSelect = false, + string? QuickSelectKey = null, + string? QuickSelectLabel = null, + string? QuickSelectIcon = null); + +/// +/// 코드 탭과 에이전트가 공통으로 참조하는 언어 지원 카탈로그. +/// - 파일 분류 +/// - 인덱싱 대상 확장자 +/// - 시스템 프롬프트 언어 가이드 +/// - LSP 연동 가능 언어 +/// - 설정 UI 설명 문자열 +/// 를 한 곳에서 관리합니다. +/// +public static class CodeLanguageCatalog +{ + private static readonly ReadOnlyCollection s_all = + 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"), + 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"), + 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"), + 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"), + 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"), + 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"), + 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." + ]), + 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." + ]), + 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." + ]), + 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." + ]), + 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." + ]), + 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." + ]), + 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." + ]), + 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." + ]), + 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." + ]), + new( + "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." + ]), + 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." + ]), + }); + + private static readonly ReadOnlyDictionary s_byKey = + new(s_all.ToDictionary(x => x.Key, StringComparer.OrdinalIgnoreCase)); + + private static readonly ReadOnlyDictionary s_byExtension = + new(s_all + .SelectMany(cap => cap.Extensions.Select(ext => new KeyValuePair(ext, cap))) + .ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase)); + + private static readonly HashSet s_codeExtensions = new( + s_all.SelectMany(cap => cap.Extensions), + StringComparer.OrdinalIgnoreCase); + + public static IReadOnlyList All => s_all; + + public static IReadOnlyCollection CodeExtensions => s_codeExtensions; + + public static IReadOnlyList QuickSelectLanguages => + s_all.Where(x => x.ShowInQuickSelect).ToList(); + + public static IReadOnlyList LspBackedLanguages => + s_all.Where(x => !string.IsNullOrWhiteSpace(x.LspLanguageId)).ToList(); + + public static bool IsCodeLikeFile(string? extension) + => !string.IsNullOrWhiteSpace(extension) && s_codeExtensions.Contains(extension); + + public static CodeLanguageCapability? FindByKey(string? key) + { + if (string.IsNullOrWhiteSpace(key)) + return null; + return s_byKey.TryGetValue(key.Trim(), out var found) ? found : null; + } + + public static CodeLanguageCapability? FindByExtension(string? extension) + { + if (string.IsNullOrWhiteSpace(extension)) + return null; + return s_byExtension.TryGetValue(extension.Trim(), out var found) ? found : null; + } + + public static string? DetectLspLanguageId(string? filePath) + => FindByExtension(Path.GetExtension(filePath ?? string.Empty))?.LspLanguageId; + + public static string GetQuickSelectLabel(string? key) + => FindByKey(key)?.DisplayName ?? key ?? "Auto"; + + public static string BuildSelectedLanguagePrompt(string? key) + { + if (string.IsNullOrWhiteSpace(key) || string.Equals(key, "auto", StringComparison.OrdinalIgnoreCase)) + return string.Empty; + + var capability = FindByKey(key); + if (capability == null) + return string.Empty; + + return $"IMPORTANT: User selected language: {capability.DisplayName}. Prioritize this language for code analysis and generation."; + } + + public static IEnumerable GetGuidanceLines(string? selectedKey) + { + var selected = FindByKey(selectedKey); + if (selected != null) + { + foreach (var line in selected.Guidance) + yield return $"- {selected.DisplayName}: {line}"; + yield break; + } + + foreach (var capability in s_all.Where(x => + x.Key is "csharp" or "python" or "java" or "cpp" or "typescript" or "javascript" or "go" or "rust" or "kotlin" or "swift")) + { + var summary = capability.Guidance.FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(summary)) + yield return $"- {capability.DisplayName}: {summary}"; + } + } + + public static string BuildLspSupportDescription() + => string.Join(", ", LspBackedLanguages.Select(x => x.DisplayName)); + + public static string BuildStaticSupportDescription() + => string.Join(", ", s_all.Select(x => x.DisplayName)); + + public static string BuildCodeTabSupportDescription() + { + var sb = new StringBuilder(); + sb.Append("정적 분류/검색/프롬프트 지원: "); + sb.Append(BuildStaticSupportDescription()); + sb.Append(" | LSP 심화 분석: "); + sb.Append(BuildLspSupportDescription()); + return sb.ToString(); + } +} diff --git a/src/AxCopilot/ViewModels/SettingsViewModel.cs b/src/AxCopilot/ViewModels/SettingsViewModel.cs index 797d30a..9e0658f 100644 --- a/src/AxCopilot/ViewModels/SettingsViewModel.cs +++ b/src/AxCopilot/ViewModels/SettingsViewModel.cs @@ -138,6 +138,9 @@ public class SettingsViewModel : INotifyPropertyChanged /// CodeSettings 바인딩용 프로퍼티. XAML에서 {Binding Code.EnableLsp} 등으로 접근. public Models.CodeSettings Code => _service.Settings.Llm.Code; + public string CodeLspSupportedLanguagesText => CodeLanguageCatalog.BuildLspSupportDescription(); + public string CodeStaticSupportedLanguagesText => CodeLanguageCatalog.BuildStaticSupportDescription(); + public string CodeTabSupportSummaryText => CodeLanguageCatalog.BuildCodeTabSupportDescription(); // ─── 작업 복사본 ─────────────────────────────────────────────────────── private string _hotkey; diff --git a/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs b/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs index f385071..d0cfb0d 100644 --- a/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs +++ b/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs @@ -281,19 +281,14 @@ public partial class ChatWindow sb.AppendLine($"\nPreferred IDE: {code.PreferredIdePath}"); // 사용자 선택 개발 언어 - if (_selectedLanguage != "auto") - { - var langName = _selectedLanguage switch { "python" => "Python", "java" => "Java", "csharp" => "C# (.NET)", "cpp" => "C/C++", "javascript" => "JavaScript/TypeScript", _ => _selectedLanguage }; - sb.AppendLine($"\nIMPORTANT: User selected language: {langName}. Prioritize this language for code analysis and generation."); - } + var selectedLanguagePrompt = CodeLanguageCatalog.BuildSelectedLanguagePrompt(_selectedLanguage); + if (!string.IsNullOrWhiteSpace(selectedLanguagePrompt)) + sb.AppendLine($"\n{selectedLanguagePrompt}"); // 언어별 가이드라인 sb.AppendLine("\n## Language Guidelines"); - sb.AppendLine("- C# (.NET): Use dotnet CLI. NuGet for packages. Follow Microsoft naming conventions."); - sb.AppendLine("- Python: Use conda/pip. Follow PEP8. Use type hints. Virtual env preferred."); - sb.AppendLine("- Java: Use Maven/Gradle. Follow Google Java Style Guide."); - sb.AppendLine("- C++: Use CMake for build. Follow C++ Core Guidelines."); - sb.AppendLine("- JavaScript/TypeScript: Use npm/yarn. Follow ESLint rules. Vue3 uses Composition API."); + foreach (var guidance in CodeLanguageCatalog.GetGuidanceLines(_selectedLanguage == "auto" ? null : _selectedLanguage)) + sb.AppendLine(guidance); // 코드 품질 + 안전 수칙 sb.AppendLine("\n## Code Quality & Safety"); diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index 7ed6d4b..0e19fea 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -3655,8 +3655,7 @@ public partial class ChatWindow : Window ], }; - private static readonly HashSet CodeExtensions = new(StringComparer.OrdinalIgnoreCase) - { ".cs", ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".cpp", ".c", ".h", ".go", ".rs", ".rb", ".php", ".swift", ".kt", ".scala", ".sh", ".ps1", ".bat", ".cmd", ".sql", ".xaml", ".vue" }; + private static readonly HashSet CodeExtensions = new(Services.CodeLanguageCatalog.CodeExtensions, StringComparer.OrdinalIgnoreCase); private static readonly HashSet DataExtensions = new(StringComparer.OrdinalIgnoreCase) { ".csv", ".json", ".xml", ".yaml", ".yml", ".tsv" }; // ImageExtensions는 이미지 첨부 영역(line ~1323)에서 정의됨 — 재사용 diff --git a/src/AxCopilot/Views/SettingsWindow.xaml b/src/AxCopilot/Views/SettingsWindow.xaml index 8f48bce..df49bcf 100644 --- a/src/AxCopilot/Views/SettingsWindow.xaml +++ b/src/AxCopilot/Views/SettingsWindow.xaml @@ -5458,7 +5458,7 @@ • 심볼 정의 위치 찾기 (Goto Definition) • 심볼이 사용된 모든 위치 검색 (Find References) • 파일 내 클래스/메서드/필드 목록 조회 - 지원 언어: C#, TypeScript, Python, C++, Java + 지원 언어는 아래 설정 설명에 표시됩니다. 해당 언어 서버가 PC에 설치되어 있어야 동작합니다. @@ -5466,6 +5466,9 @@ + + + diff --git a/src/AxCopilot/skills/pptx-creator.skill.md b/src/AxCopilot/skills/pptx-creator.skill.md index e8a7396..abc1766 100644 --- a/src/AxCopilot/skills/pptx-creator.skill.md +++ b/src/AxCopilot/skills/pptx-creator.skill.md @@ -1,125 +1,109 @@ ---- +--- name: pptx-creator label: PPT 프레젠테이션 생성 -description: Python을 사용하여 전문적인 PowerPoint 프레젠테이션을 생성합니다. 작업 폴더의 양식 파일을 자동 활용합니다. +description: AX 기본 PPT 엔진으로 고급 제안서, 보고서, 발표 자료를 생성합니다. 작업 폴더의 양식 파일과 프로젝트 자료를 함께 활용합니다. icon: \uE7BE -when_to_use: 발표 자료, 제안서 deck, 보고용 슬라이드, 교육용 프레젠테이션을 새로 만들어야 하거나 기존 양식 PPT를 활용해야 할 때 +when_to_use: 제안서 deck, 경영보고, 임원 보고자료, 프로젝트 현황 발표, 교육용 슬라이드, 회의 발표 자료를 새로 만들어야 하거나 기존 양식 PPT를 활용해야 할 때 argument-hint: <주제 또는 문서 목적> allowed-tools: - folder_map + - document_plan - document_read - file_read - - file_write - - process - pptx_create - - template_render tabs: cowork --- -사용자의 요구에 맞는 PowerPoint 프레젠테이션을 Python으로 생성하세요. -## 실행 경로 선택 (Python 가능/불가) -- 먼저 `process`로 `python --version`을 확인하세요. -- Python 가능: 기존 python-pptx 경로를 사용하세요. -- Python 불가: `pptx_create`로 슬라이드 초안을 생성하고 `template_render` + `file_write`로 발표자료 구조를 보강하세요. +사용자의 요구에 맞는 PowerPoint 프레젠테이션을 AX 기본 `pptx_create` 도구로 생성하세요. +## 기본 원칙 +- 기본 경로는 항상 `pptx_create`입니다. Python 스크립트 생성은 예외 상황에서만 고려하세요. +- 슬라이드마다 메시지는 하나만 두고, 제목은 설명형 문장이 아니라 결론형 headline으로 작성하세요. +- bullet은 슬라이드당 3~5개 이내의 짧고 강한 문장으로 유지하세요. 공간을 채우기 위한 장문 bullet은 금지합니다. +- 숫자, 비교, 일정, 권고안은 각각 맞는 레이아웃으로 분리하세요. -## 사전 준비 -필요한 패키지를 확인하고 설치하세요: -``` -process: pip install python-pptx +## 권장 작업 순서 +1. `folder_map`으로 작업 폴더를 확인하고 기존 `.pptx`, 보고서, 분석 문서, 회의록이 있는지 찾으세요. +2. 참고할 양식 PPT가 있으면 파일명을 기억하고, 필요 시 `document_read`로 슬라이드 구조를 파악하세요. +3. 먼저 `document_plan`을 `document_type: presentation`으로 호출해서 스토리라인을 정리하세요. +4. 그 결과를 바탕으로 `pptx_create`용 슬라이드 배열을 설계하세요. +5. 생성 후 결과 파일 경로와 핵심 구성(슬라이드 수, 사용 템플릿/테마, 주요 레이아웃)을 간단히 안내하세요. + +## 컨설팅형 스토리라인 규칙 +- 기본 구조는 가능하면 아래 순서를 따르세요. +- `Executive Summary` +- `Situation & Imperative` +- `Key Findings` +- `Options & Recommendation` +- `Implementation Roadmap` +- `Impact & Ask` + +## 레이아웃 선택 기준 +- `executive_summary`: 경영진 요약, 한 줄 권고안, 핵심 takeaways, KPI 요약 +- `recommendation`: 단일 권고안과 근거, 즉시 실행 항목 +- `comparison`: 대안 비교, 옵션별 pros/risks, 추천안 강조 +- `roadmap`: 단계별 일정, 소유자, 주요 산출물 +- `kpi_dashboard`: 핵심 수치, 추세, 시사점 +- `chart`: 정량 데이터 시각화 +- `table`: 비교표, 상세 데이터표 +- `section`: 장 구분 +- `content`: 일반 설명 슬라이드 + +## 양식 활용 +- 작업 폴더에 기존 양식 PPT가 있으면 `template` 또는 `theme_file`로 우선 활용하세요. +- 양식 후보 예시: + - 파일명에 `양식`, `template`, `표준`, `기본`, `보고`, `proposal`, `deck` 포함 + - 사용자가 특정 `.pptx` 파일명을 직접 언급 +- 양식이 명확하지 않으면 기본 `template: basic100` 또는 문서 성격에 맞는 `corporate`, `professional`, `modern` 중 하나를 사용하세요. + +## 슬라이드 작성 규칙 +- 제목은 `무엇을 다룰지`가 아니라 `무슨 결론인지`를 말해야 합니다. +- 각 슬라이드는 headline, supporting evidence, takeaway의 3요소를 갖추세요. +- 데이터가 없으면 억지 차트를 만들지 말고, 비교 카드나 권고안 슬라이드로 전환하세요. +- 표는 설명보다 비교에 유리할 때만 사용하세요. +- speaker notes가 유용하면 `notes`를 넣어 발표 포인트를 남기세요. + +## 예시 흐름 +- `document_plan`으로 발표 구조 생성 +- `pptx_create`에서 아래와 같은 레이아웃 조합 사용 + - `title` + - `executive_summary` + - `comparison` + - `recommendation` + - `roadmap` + - `kpi_dashboard` + - `section` + - `content` + +## 예시 파라미터 스케치 +```json +{ + "path": "strategy-deck.pptx", + "template": "basic100", + "slides": [ + { + "layout": "title", + "title": "2026 사업 전략 제안", + "subtitle": "성장 가속과 운영 효율 동시 달성" + }, + { + "layout": "executive_summary", + "title": "Executive Summary", + "headline": "핵심 투자 우선순위를 재배치하면 2개 분기 내 수익성을 개선할 수 있습니다.", + "summary_points": [ + "핵심 비용 3개 영역에서 구조적 비효율이 확인되었습니다.", + "고객 유지율 개선이 신규 확보보다 더 큰 수익 기회를 만듭니다.", + "선행 투자 없이 가능한 빠른 실행과제도 존재합니다." + ], + "recommendation": "우선 CRM 고도화와 운영 자동화를 병행하고, 저효율 캠페인은 즉시 축소합니다.", + "kpis": [ + { "label": "매출총이익", "value": "+4.2%p", "trend": "2Q forecast", "note": "product mix 개선" }, + { "label": "CAC", "value": "-11%", "trend": "target", "note": "퍼널 최적화" }, + { "label": "Retention", "value": "+6pt", "trend": "12M", "note": "핵심 세그먼트" } + ] + } + ] +} ``` -## 양식 활용 (마스터 슬라이드 템플릿) -작업 폴더에 PPT 양식이 있으면 **반드시** 활용하세요: - -1. **양식 탐색**: `folder_map`으로 작업 폴더를 스캔하여 `.pptx` 파일 확인 -2. **양식 후보 판별**: - - 파일명에 "양식", "template", "서식", "표준", "기본" 포함 - - 또는 사용자가 명시적으로 "XX 양식으로 작성해줘" 요청 - - 또는 사용자가 특정 .pptx 파일명을 언급 -3. **양식 구조 파악**: `document_read`로 양식의 슬라이드 레이아웃 목록 확인 -4. **양식 기반 생성**: - ```python - prs = Presentation('양식_발표.pptx') - # 마스터 슬라이드의 배경, 로고, 색 테마, 폰트가 자동 상속 - # 기존 슬라이드 제거 후 새 내용 추가 - while len(prs.slides) > 0: - rId = prs.slides._sldIdLst[0].rId - prs.part.drop_rel(rId) - del prs.slides._sldIdLst[0] - ``` -5. **레이아웃 확인** (양식마다 다를 수 있음): - ```python - for i, layout in enumerate(prs.slide_layouts): - print(f'{i}: {layout.name}') - ``` -6. **양식이 없으면**: 아래 기본 템플릿으로 새 프레젠테이션 생성 - -## 작업 절차 -1. **요구사항 파악**: 발표 주제, 슬라이드 수, 스타일 확인 -2. **양식 확인**: folder_map으로 작업 폴더에 양식 .pptx 파일이 있는지 확인 -3. **스크립트 작성**: file_write로 Python 스크립트 생성 -4. **실행**: `process`로 스크립트 실행 -5. **결과 안내**: 생성된 .pptx 파일 경로를 사용자에게 전달 - -## 스크립트 템플릿 -```python -from pptx import Presentation -from pptx.util import Inches, Pt, Emu -from pptx.dml.color import RGBColor -from pptx.enum.text import PP_ALIGN -import os - -# 양식 파일 자동 감지 -template_keywords = ['양식', 'template', '서식', '표준', '기본'] -template_file = None -for f in os.listdir('.'): - if f.endswith('.pptx') and any(kw in f.lower() for kw in template_keywords): - template_file = f - break - -# 양식이 있으면 활용, 없으면 새 프레젠테이션 -if template_file: - prs = Presentation(template_file) - # 기존 슬라이드 제거 (마스터/레이아웃은 유지) - while len(prs.slides) > 0: - rId = prs.slides._sldIdLst[0].rId - prs.part.drop_rel(rId) - del prs.slides._sldIdLst[0] - print(f'양식 활용: {template_file}') - print(f'사용 가능한 레이아웃: {[l.name for l in prs.slide_layouts]}') -else: - prs = Presentation() - prs.slide_width = Inches(13.333) - prs.slide_height = Inches(7.5) - -# 제목 슬라이드 -slide = prs.slides.add_slide(prs.slide_layouts[0]) -slide.shapes.title.text = '프레젠테이션 제목' -if len(slide.placeholders) > 1: - slide.placeholders[1].text = '부제목' - -# 내용 슬라이드 -slide = prs.slides.add_slide(prs.slide_layouts[1]) -slide.shapes.title.text = '섹션 제목' -body = slide.placeholders[1] -body.text = '첫 번째 포인트' -p = body.text_frame.add_paragraph() -p.text = '두 번째 포인트' - -prs.save('presentation.pptx') -print('프레젠테이션 생성 완료: presentation.pptx') -``` - -## 지원 기능 -- 제목/내용/빈 슬라이드 레이아웃 -- 텍스트 서식 (글꼴, 크기, 색상, 정렬) -- 표 삽입 -- 이미지 삽입 -- 도형 (사각형, 원, 화살표) -- 차트 (막대, 선, 원형) -- 슬라이드 번호 -- 마스터 슬라이드 커스터마이징 -- **양식 파일 기반 마스터/레이아웃 상속** (배경, 로고, 색 테마, 폰트 자동 유지) - -한국어로 안내하세요. 작업 폴더에 결과 파일을 저장하세요. +한국어로 안내하고, 결과 파일은 작업 폴더에 저장하세요.