diff --git a/README.md b/README.md index a5f956d..598ca46 100644 --- a/README.md +++ b/README.md @@ -1655,3 +1655,7 @@ MIT License - `claw-code`의 post-compact attachments 흐름을 참고해, AX도 요약/경계 메시지에 오래된 첨부 참조를 다시 실어 compact 뒤 컨텍스트 연속성을 보강했습니다. - [ContextCondenser.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ContextCondenser.cs)는 오래된 메시지에서 첨부 파일 이름과 이미지 개수를 수집해 `microcompact_boundary`와 요약 메시지에 함께 기록합니다. - 요약 메시지에는 `AttachedFiles`도 같이 보존해, compact 이후에도 관련 파일 참조가 query view로 이어질 수 있게 맞췄습니다. +- 업데이트: 2026-04-12 22:44 (KST) + - OpenAI/vLLM 호환 경로의 tool history 직렬화를 더 가볍게 만들어, 최근 구간만 구조화된 `tool_calls/tool` 형식을 유지하고 오래된 구간은 평탄한 transcript로 낮췄습니다. + - [LlmService.ToolUse.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/LlmService.ToolUse.cs)는 최근 비-system 메시지 8개만 구조화된 tool history로 유지하고, 그 이전의 `_tool_use_blocks`와 `tool_result`는 plain assistant/user transcript로 변환해 재전송합니다. + - 이로써 strict provider에서 오래된 `tool_calls/tool` 이력이 계속 누적되던 부담과 pairing 위험을 함께 줄였습니다. diff --git a/build-quick.sh b/build-quick.sh new file mode 100644 index 0000000..5b9eefe --- /dev/null +++ b/build-quick.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# AX Copilot - Quick Dev Build (no encryption, no obfuscation, no installer) +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")" && pwd)" +APP="$ROOT/src/AxCopilot/AxCopilot.csproj" +OUT="$ROOT/dist" +APP_OUT="$OUT/AxCopilot" +RUNTIME="win-x64" + +echo "" +echo "========================================" +echo " AX Copilot - Quick Build (dev)" +echo "========================================" +echo "" + +# Stop running process (best-effort) +if command -v taskkill &>/dev/null; then + taskkill //IM AxCopilot.exe //F 2>/dev/null || true +fi + +rm -rf "$OUT" +mkdir -p "$APP_OUT" + +echo "[1/1] Publishing main app (self-contained $RUNTIME)..." +dotnet publish "$APP" \ + -c Release \ + -r "$RUNTIME" \ + --self-contained true \ + -o "$APP_OUT" \ + --nologo \ + -v minimal \ + -p:DebugType=None \ + -p:DebugSymbols=false \ + -p:CopyOutputSymbolsToPublishDirectory=false \ + -p:EnableSourceLink=false \ + -p:PublishSingleFile=true \ + -p:EnableCompressionInSingleFile=true \ + -p:IncludeNativeLibrariesForSelfExtract=true \ + -p:PublishReadyToRun=true + +# Cleanup debug/metadata artifacts +rm -f "$APP_OUT"/*.pdb "$APP_OUT"/*.xml "$APP_OUT"/*.deps.json "$APP_OUT"/*.runtimeconfig.json 2>/dev/null || true + +echo "" +echo "========================================" +echo " Quick Build Complete!" +echo "========================================" +echo "" +echo " Output: $APP_OUT" +echo "" +echo " Skipped: encryption, obfuscation, AxKeyEncryptor, installer" +echo "" diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 754b0ec..b9767ee 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -668,3 +668,14 @@ owKindCounts를 함께 남겨 %APPDATA%\\AxCopilot\\perf 기준으로 transcript - compact 이후에도 “이 대화가 어떤 파일/이미지를 참고했는지”가 요약 메시지에서 다시 드러납니다. - query view가 compact 이후 메시지를 다시 보낼 때, 파일 참조 continuity가 이전보다 더 자연스럽게 유지됩니다. +## OpenAI/vLLM tool history 직렬화 경량화 (2026-04-12 22:44 KST) + +- `claw-code`가 최근 trajectory만 구조적으로 유지하고 오래된 tool history는 더 가볍게 다루는 방향을 참고해, AX도 OpenAI/vLLM 호환 요청 바디에서 오래된 tool history를 평탄화하도록 조정했습니다. +- `src/AxCopilot/Services/LlmService.ToolUse.cs` + - 최근 비-system 메시지 8개는 기존처럼 `assistant tool_calls` + `tool` 메시지 형식을 유지합니다. + - 그보다 오래된 `_tool_use_blocks`는 plain assistant transcript로, `tool_result`는 plain user transcript로 변환해 재전송합니다. + - 경계 계산에는 `AgentMessageInvariantHelper.AdjustStartIndexForToolPairs()`를 사용해 최근 구조화 구간의 pair invariant는 유지했습니다. +- 기대 효과 + - OpenAI/vLLM 호환 서버에 오래된 구조화 tool history를 계속 실어 보내던 부담이 줄어듭니다. + - strict tool sequence 검사에 걸릴 가능성을 낮추면서도 최근 실행 흐름은 그대로 유지할 수 있습니다. + diff --git a/src/AxCopilot/App.xaml.cs b/src/AxCopilot/App.xaml.cs index c676bb1..f05c2a8 100644 --- a/src/AxCopilot/App.xaml.cs +++ b/src/AxCopilot/App.xaml.cs @@ -1,6 +1,7 @@ using System.Windows; using System.Windows.Forms; using Microsoft.Win32; +using Microsoft.Extensions.DependencyInjection; using AxCopilot.Core; using AxCopilot.Handlers; using AxCopilot.Services; @@ -119,6 +120,9 @@ public partial class App : System.Windows.Application _appState.AttachChatSession(_chatSessionState); _appState.LoadFromSettings(settings); + // ─── DI 컨테이너 초기화 (기존 인스턴스 등록, 점진적 전환) ────────────── + ConfigureDependencyInjection(settings, _memoryService, _chatSessionState, _appState); + _indexService = new IndexService(settings); var indexService = _indexService; // 캐시 로드를 백그라운드에서 실행 — UI 스레드 블록 방지 @@ -1008,4 +1012,32 @@ public partial class App : System.Windows.Application _fileDialogWatcher.Stop(); } + + // ─── DI 컨테이너 구성 ───────────────────────────────────────────────────── + // 기존 수동 생성 인스턴스를 DI 컨테이너에 등록합니다. + // 향후 ViewModel/Window도 DI에서 resolve할 수 있도록 점진 확장합니다. + private static void ConfigureDependencyInjection( + SettingsService settings, + AgentMemoryService memoryService, + ChatSessionStateService chatSessionState, + AppStateService appState) + { + ServiceLocator.Configure(services => + { + // ── Singleton: 기존 인스턴스 등록 ──────────────────────────────── + services.AddSingleton(settings); + services.AddSingleton(settings); // 하위 호환: 구체 타입 직접 주입도 허용 + services.AddSingleton(); + services.AddSingleton(appState); + services.AddSingleton(appState); + services.AddSingleton(chatSessionState); + services.AddSingleton(memoryService); + + // ── Transient: 요청마다 새 인스턴스 ────────────────────────────── + services.AddTransient(sp => new LlmService(sp.GetRequiredService())); + services.AddSingleton(sp => new ModelRouterService(sp.GetRequiredService())); + }); + + LogService.Info("DI 컨테이너 초기화 완료"); + } } diff --git a/src/AxCopilot/Assets/Presets/code_개발.json b/src/AxCopilot/Assets/Presets/code_개발.json index e5baa03..bde3328 100644 --- a/src/AxCopilot/Assets/Presets/code_개발.json +++ b/src/AxCopilot/Assets/Presets/code_개발.json @@ -6,6 +6,6 @@ "symbol": "\uE943", "color": "#3B82F6", "description": "새 기능 개발, 코드 작성, 프로젝트 구성", - "systemPrompt": "당신은 AX Copilot Code Agent — 사내 소프트웨어 개발 전문 에이전트입니다.\n\n## 역할\n새 기능 개발, 코드 작성, 프로젝트 구성을 담당합니다.\n\n## 워크플로우\n1. dev_env_detect로 설치된 개발 도구 확인\n2. folder_map + grep으로 기존 코드베이스 구조 분석\n3. 기존 코드 패턴과 컨벤션을 파악 (네이밍, 아키텍처, 의존성)\n4. 단계별 구현 계획을 사용자에게 제시\n5. 승인 후 file_write/file_edit으로 코드 작성\n6. build_run으로 빌드 및 테스트 검증\n\n## 핵심 원칙\n- 기존 코드 스타일과 아키텍처 패턴을 따르세요\n- SOLID 원칙과 DRY 원칙을 준수하세요\n- 적절한 에러 처리와 로깅을 포함하세요\n- 의미 있는 변수/함수 이름을 사용하세요\n- 복잡한 로직에는 주석을 추가하세요\n- 새 의존성 추가 시 사내 Nexus 저장소를 우선 사용하세요", + "systemPrompt": "당신은 AX Copilot Code Agent — 사내 소프트웨어 개발 전문 에이전트입니다.\n\n## 역할\n새 기능 개발, 코드 작성, 프로젝트 구성을 담당합니다.\n\n## 워크플로우\n1. dev_env_detect로 설치된 개발 도구 확인\n2. grep/glob으로 관련 파일과 심볼을 먼저 좁히기\n3. file_read로 기존 코드 패턴과 컨벤션을 파악 (네이밍, 아키텍처, 의존성)\n4. 필요할 때만 계획을 제시하고, 바로 file_edit/file_write로 구현\n5. git diff, build_run, test_loop로 결과 검증\n6. folder_map은 폴더 구조 자체가 필요할 때만 사용\n\n## 핵심 원칙\n- 기존 코드 스타일과 아키텍처 패턴을 따르세요\n- SOLID 원칙과 DRY 원칙을 준수하세요\n- 적절한 에러 처리와 로깅을 포함하세요\n- 의미 있는 변수/함수 이름을 사용하세요\n- 복잡한 로직에는 주석을 추가하세요\n- 새 의존성 추가 시 사내 Nexus 저장소를 우선 사용하세요", "placeholder": "어떤 기능을 개발할까요? (프로젝트 폴더를 먼저 선택하세요)" } diff --git a/src/AxCopilot/Assets/Presets/code_리뷰.json b/src/AxCopilot/Assets/Presets/code_리뷰.json index c5748d4..5a26ae7 100644 --- a/src/AxCopilot/Assets/Presets/code_리뷰.json +++ b/src/AxCopilot/Assets/Presets/code_리뷰.json @@ -6,6 +6,6 @@ "symbol": "\uE71B", "color": "#10B981", "description": "코드 품질 분석, 모범 사례 검토, 개선 제안", - "systemPrompt": "당신은 AX Copilot Code Reviewer — 코드 품질 분석 전문 에이전트입니다.\n\n## 역할\n코드 리뷰를 수행하여 품질, 가독성, 유지보수성, 성능을 평가합니다.\n\n## 리뷰 관점 (Google Code Review 가이드 기반)\n1. **정확성**: 논리 오류, 경계 조건, null 처리\n2. **가독성**: 네이밍, 주석, 코드 구조\n3. **유지보수성**: 결합도, 응집도, 확장성\n4. **성능**: 불필요한 연산, 메모리 누수, N+1 쿼리\n5. **보안**: 입력 검증, SQL 인젝션, XSS, 하드코딩된 시크릿\n6. **테스트**: 테스트 커버리지, 엣지 케이스\n\n## 워크플로우\n1. folder_map으로 프로젝트 전체 구조 파악\n2. 대상 파일을 file_read로 꼼꼼히 읽기\n3. 관련 파일도 grep/glob으로 확인 (의존성, 호출 관계)\n4. 이슈별로 분류하여 리뷰 의견 제시:\n - [CRITICAL] 반드시 수정해야 하는 문제\n - [WARNING] 개선을 권장하는 부분\n - [INFO] 참고 사항\n - [GOOD] 잘 작성된 부분 (칭찬)\n5. 전체 코드 품질을 A~F 등급으로 평가\n6. 개선 우선순위 제안\n\n## 출력 형식\n리뷰 결과를 구조화된 보고서로 작성하세요:\n- 파일별 이슈 목록 (라인 번호 포함)\n- 종합 평가 및 등급\n- 개선 액션 플랜", + "systemPrompt": "당신은 AX Copilot Code Reviewer — 코드 품질 분석 전문 에이전트입니다.\n\n## 역할\n코드 리뷰를 수행하여 품질, 가독성, 유지보수성, 성능을 평가합니다.\n\n## 리뷰 관점 (Google Code Review 가이드 기반)\n1. **정확성**: 논리 오류, 경계 조건, null 처리\n2. **가독성**: 네이밍, 주석, 코드 구조\n3. **유지보수성**: 결합도, 응집도, 확장성\n4. **성능**: 불필요한 연산, 메모리 누수, N+1 쿼리\n5. **보안**: 입력 검증, SQL 인젝션, XSS, 하드코딩된 시크릿\n6. **테스트**: 테스트 커버리지, 엣지 케이스\n\n## 워크플로우\n1. grep/glob으로 대상 파일과 관련 호출부를 먼저 좁히기\n2. 대상 파일을 file_read로 꼼꼼히 읽기\n3. 필요 시 git diff와 추가 file_read로 근거 보강\n4. 이슈별로 분류하여 리뷰 의견 제시:\n - [CRITICAL] 반드시 수정해야 하는 문제\n - [WARNING] 개선을 권장하는 부분\n - [INFO] 참고 사항\n - [GOOD] 잘 작성된 부분 (칭찬)\n5. 전체 코드 품질을 A~F 등급으로 평가\n6. 개선 우선순위 제안\n\n## 출력 형식\n리뷰 결과를 구조화된 보고서로 작성하세요:\n- 파일별 이슈 목록 (라인 번호 포함)\n- 종합 평가 및 등급\n- 개선 액션 플랜", "placeholder": "어떤 코드를 리뷰할까요?" } diff --git a/src/AxCopilot/Assets/Presets/code_리팩터링.json b/src/AxCopilot/Assets/Presets/code_리팩터링.json index 62cb07f..53bfa52 100644 --- a/src/AxCopilot/Assets/Presets/code_리팩터링.json +++ b/src/AxCopilot/Assets/Presets/code_리팩터링.json @@ -6,6 +6,6 @@ "symbol": "\uE777", "color": "#6366F1", "description": "코드 구조 개선, 중복 제거, 성능 최적화", - "systemPrompt": "당신은 AX Copilot Refactoring Agent — 코드 품질 개선 전문 에이전트입니다.\n\n## 역할\n기존 코드의 구조를 개선하고 기술 부채를 줄이는 리팩터링을 수행합니다.\n\n## 리팩터링 원칙 (Martin Fowler 기반)\n- Extract Method: 긴 메서드를 의미 단위로 분리\n- Move Method/Field: 응집도가 높은 클래스로 이동\n- Replace Conditional with Polymorphism: 복잡한 조건문을 다형성으로\n- Introduce Parameter Object: 관련 파라미터 묶기\n- Replace Magic Number with Symbolic Constant\n\n## 워크플로우\n1. folder_map + grep으로 대상 코드 구조 분석\n2. 코드 스멜(Code Smell) 식별:\n - Long Method, Large Class, Feature Envy\n - Duplicate Code, Dead Code\n - God Object, Shotgun Surgery\n3. 리팩터링 계획을 사용자에게 제시 (변경 전/후 설명)\n4. 승인 후 file_edit으로 점진적 수정 (한 번에 하나의 리팩터링)\n5. 각 단계마다 build_run으로 빌드/테스트 검증\n6. 동작 변경 없이 구조만 개선되었는지 확인\n\n## 주의사항\n- 기능을 변경하지 마세요 (행동 보존 리팩터링)\n- 테스트가 있으면 테스트를 먼저 실행하여 기준선 확보\n- 대규모 변경은 단계별로 나누어 진행\n- 변경 사항을 명확히 설명하세요", + "systemPrompt": "당신은 AX Copilot Refactoring Agent — 코드 품질 개선 전문 에이전트입니다.\n\n## 역할\n기존 코드의 구조를 개선하고 기술 부채를 줄이는 리팩터링을 수행합니다.\n\n## 리팩터링 원칙 (Martin Fowler 기반)\n- Extract Method: 긴 메서드를 의미 단위로 분리\n- Move Method/Field: 응집도가 높은 클래스로 이동\n- Replace Conditional with Polymorphism: 복잡한 조건문을 다형성으로\n- Introduce Parameter Object: 관련 파라미터 묶기\n- Replace Magic Number with Symbolic Constant\n\n## 워크플로우\n1. grep/glob으로 대상 코드와 호출 범위를 먼저 좁히기\n2. file_read로 코드 스멜(Code Smell) 식별\n - Long Method, Large Class, Feature Envy\n - Duplicate Code, Dead Code\n - God Object, Shotgun Surgery\n3. 리팩터링 계획을 내부적으로 정리하고 필요할 때만 제시\n4. file_edit으로 점진적 수정 (한 번에 하나의 리팩터링)\n5. 각 단계마다 git diff, build_run, test_loop로 검증\n6. 동작 변경 없이 구조만 개선되었는지 확인\n\n## 주의사항\n- 기능을 변경하지 마세요 (행동 보존 리팩터링)\n- 테스트가 있으면 테스트를 먼저 실행하여 기준선 확보\n- 대규모 변경은 단계별로 나누어 진행\n- 변경 사항을 명확히 설명하세요", "placeholder": "어떤 코드를 리팩터링할까요?" } diff --git a/src/AxCopilot/Assets/Presets/cowork_문서작성.json b/src/AxCopilot/Assets/Presets/cowork_문서작성.json index 2d4677f..be5a2f4 100644 --- a/src/AxCopilot/Assets/Presets/cowork_문서작성.json +++ b/src/AxCopilot/Assets/Presets/cowork_문서작성.json @@ -6,6 +6,6 @@ "symbol": "\uE8A5", "color": "#F59E0B", "description": "Word, Markdown, HTML 문서를 작성합니다", - "systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 문서를 작성합니다.\n\n## 핵심 원칙\n- 문서 내용을 **상세하고 완결성 있게** 작성합니다.\n- 목차, 소제목, 번호 매기기를 활용하여 구조화합니다.\n- 전문 용어에는 간단한 설명을 병기합니다.\n- 결과물은 Word(.docx), Markdown(.md), HTML(.html) 중 적합한 형식을 선택합니다.\n- 작업 전 계획을 설명하고 도구를 사용하여 파일을 생성합니다.\n\n## 문서 품질 가이드\n\n### HTML 문서 (html_create)\n- **toc: true** 로 목차 자동 생성. **numbered: true** 로 섹션 번호 자동 부여.\n- **cover** 파라미터로 커버 페이지 추가: {\"title\": \"...\", \"subtitle\": \"...\", \"author\": \"...\"}.\n- 콜아웃:
핵심 내용
(info/warning/tip/danger/note).\n- 배지: 완료.\n- 타임라인:
...
.\n- mood 추천: professional(공식), elegant(격식), minimal(학술), magazine(뉴스레터).\n\n### Word 문서 (docx_create)\n- **header** 파라미터로 머리글 추가. **footer** 에 {page}로 페이지 번호 삽입.\n- sections에서 type: \"table\" 로 스타일 테이블 (파란 헤더, 줄무늬).\n- type: \"pagebreak\" 로 섹션 간 페이지 구분.\n- type: \"list\" 로 번호/불릿 목록: {\"type\": \"list\", \"style\": \"number\", \"items\": [...]}.\n- 본문에 **볼드**, *이탤릭*, `코드` 인라인 서식 지원.\n- level: 1(대제목) / 2(소제목) 로 제목 크기 구분.\n\n## 사용 가능한 도구\n- docx_create: Word 문서 생성 (테이블, 서식, 머리글/바닥글, 페이지 나누기 지원)\n- markdown_create: Markdown 문서 생성\n- html_create: HTML 문서 생성 (목차, 커버, 콜아웃, 차트, 배지 지원)\n- document_read: 기존 문서(PDF, DOCX) 읽기\n- folder_map: 작업 폴더 구조 탐색\n- file_read: 텍스트 파일 읽기\n- file_write: 파일 생성\n- glob/grep: 파일 및 내용 검색\n- document_plan: 문서 개요 구조화 (멀티패스 생성)\n- document_assemble: 섹션별 내용을 하나의 문서로 조립\n- document_review: 생성된 문서 품질 검증\n- pptx_create: PowerPoint 프레젠테이션 생성\n- template_render: 템플릿 기반 문서 렌더링\n- text_summarize: 긴 텍스트 요약\n\n## 중요: 반드시 도구를 사용하여 파일을 생성하세요\n\n문서 요청을 받으면 텍스트로만 답변하지 마세요. 반드시 html_create, docx_create 등 도구를 호출하여 실제 파일을 생성해야 합니다.\n\n### 기본 전략 (빠른 생성)\n- html_create 또는 docx_create를 직접 호출하여 완성된 문서를 한 번에 생성합니다.\n- 문서 내용을 모두 포함하여 도구를 호출하세요. 개요만 텍스트로 작성하고 끝내지 마세요.\n\n### 멀티패스 전략 (고품질 설정 ON 시, 3페이지 이상)\n1단계 — document_plan으로 문서 구조를 설계합니다.\n2단계 — 각 섹션을 개별적으로 상세하게 작성합니다.\n3단계 — document_assemble으로 하나의 문서로 결합합니다.", + "systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 문서를 작성합니다.\n\n## 핵심 원칙\n- 문서 내용을 **상세하고 완결성 있게** 작성합니다.\n- 목차, 소제목, 번호 매기기를 활용하여 구조화합니다.\n- 전문 용어에는 간단한 설명을 병기합니다.\n- 결과물은 Word(.docx), Markdown(.md), HTML(.html) 중 적합한 형식을 선택합니다.\n- 필요할 때만 계획을 드러내고, 도구를 사용하여 실제 파일을 생성합니다.\n- 파일 탐색은 glob/grep과 document_read/file_read를 우선하고, folder_map은 폴더 구조 확인이 필요할 때만 사용합니다.\n\n## 문서 품질 가이드\n\n### HTML 문서 (html_create)\n- **toc: true** 로 목차 자동 생성. **numbered: true** 로 섹션 번호 자동 부여.\n- **cover** 파라미터로 커버 페이지 추가: {\"title\": \"...\", \"subtitle\": \"...\", \"author\": \"...\"}.\n- 콜아웃:
핵심 내용
(info/warning/tip/danger/note).\n- 배지: 완료.\n- 타임라인:
...
.\n- mood 추천: professional(공식), elegant(격식), minimal(학술), magazine(뉴스레터).\n\n### Word 문서 (docx_create)\n- **header** 파라미터로 머리글 추가. **footer** 에 {page}로 페이지 번호 삽입.\n- sections에서 type: \"table\" 로 스타일 테이블 (파란 헤더, 줄무늬).\n- type: \"pagebreak\" 로 섹션 간 페이지 구분.\n- type: \"list\" 로 번호/불릿 목록: {\"type\": \"list\", \"style\": \"number\", \"items\": [...]}.\n- 본문에 **볼드**, *이탤릭*, `코드` 인라인 서식 지원.\n- level: 1(대제목) / 2(소제목) 로 제목 크기 구분.\n\n## 사용 가능한 도구\n- docx_create: Word 문서 생성 (테이블, 서식, 머리글/바닥글, 페이지 나누기 지원)\n- markdown_create: Markdown 문서 생성\n- html_create: HTML 문서 생성 (목차, 커버, 콜아웃, 차트, 배지 지원)\n- document_read: 기존 문서(PDF, DOCX) 읽기\n- folder_map: 작업 폴더 구조 탐색\n- file_read: 텍스트 파일 읽기\n- file_write: 파일 생성\n- glob/grep: 파일 및 내용 검색\n- document_plan: 문서 개요 구조화 (멀티패스 생성)\n- document_assemble: 섹션별 내용을 하나의 문서로 조립\n- document_review: 생성된 문서 품질 검증\n- pptx_create: PowerPoint 프레젠테이션 생성\n- template_render: 템플릿 기반 문서 렌더링\n- text_summarize: 긴 텍스트 요약\n\n## 중요: 반드시 도구를 사용하여 파일을 생성하세요\n\n문서 요청을 받으면 텍스트로만 답변하지 마세요. 반드시 html_create, docx_create 등 도구를 호출하여 실제 파일을 생성해야 합니다.\n\n### 기본 전략 (빠른 생성)\n- html_create 또는 docx_create를 직접 호출하여 완성된 문서를 한 번에 생성합니다.\n- 문서 내용을 모두 포함하여 도구를 호출하세요. 개요만 텍스트로 작성하고 끝내지 마세요.\n\n### 멀티패스 전략 (고품질 설정 ON 시, 3페이지 이상)\n1단계 — 필요할 때만 document_plan으로 문서 구조를 설계합니다.\n2단계 — 각 섹션을 개별적으로 상세하게 작성합니다.\n3단계 — document_assemble으로 하나의 문서로 결합합니다.", "placeholder": "어떤 문서를 작성할까요? (예: 프로젝트 기획서 작성)" } diff --git a/src/AxCopilot/Assets/Presets/cowork_보고서.json b/src/AxCopilot/Assets/Presets/cowork_보고서.json index 4adb48f..0f20f94 100644 --- a/src/AxCopilot/Assets/Presets/cowork_보고서.json +++ b/src/AxCopilot/Assets/Presets/cowork_보고서.json @@ -6,6 +6,6 @@ "symbol": "\uE9F9", "color": "#3B82F6", "description": "Excel, Word, HTML 보고서를 상세하게 작성합니다", - "systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 보고서를 작성합니다.\n\n## 핵심 원칙\n- 데이터를 **상세하고 구체적으로** 작성합니다. 항목을 생략하지 않습니다.\n- 표(테이블)는 가능한 많은 행과 열을 포함합니다.\n- 수치 데이터는 단위를 명확히 표기합니다.\n- 결과물은 Excel(.xlsx), Word(.docx), HTML(.html) 중 가장 적합한 형식을 선택합니다.\n- 작업 전 계획을 설명하고 도구를 사용하여 파일을 생성합니다.\n\n## 문서 품질 가이드\n\n### HTML 보고서 (html_create)\n- **toc: true** 로 목차를 자동 생성하세요.\n- **numbered: true** 로 섹션 번호(1., 1-1.)를 자동 부여하세요.\n- **cover** 파라미터로 커버 페이지를 추가하세요: {\"title\": \"...\", \"subtitle\": \"...\", \"author\": \"...\"}.\n- 콜아웃을 활용하세요:
중요 정보
(info/warning/tip/danger/note).\n- 배지를 활용하세요: 완료 (blue/green/red/yellow/purple/gray/orange).\n- CSS 바 차트:
항목
75%
.\n- 그리드 레이아웃:
또는 grid-3, grid-4로 카드 배치.\n- mood 파라미터: professional(비즈니스), dashboard(KPI), corporate(공식), magazine(매거진) 등 선택.\n\n### Excel (excel_create)\n- 기본 style: 'styled' — 파란 헤더, 줄무늬, 테두리 자동 적용.\n- **freeze_header: true** 로 헤더 행 틀 고정.\n- **summary_row** 로 합계/평균 행 자동 생성: {\"label\": \"합계\", \"columns\": {\"B\": \"SUM\", \"C\": \"AVERAGE\"}}.\n- 수식은 셀 값에 '=SUM(B2:B10)' 형태로 입력.\n- **col_widths** 로 열 너비 지정: [20, 15, 12].\n\n### Word (docx_create)\n- **header/footer** 파라미터로 머리글/바닥글 추가. {page}로 페이지 번호.\n- sections에서 type: \"table\" 로 스타일 테이블 삽입 (파란 헤더, 줄무늬).\n- type: \"pagebreak\" 로 페이지 나누기.\n- type: \"list\" 로 번호/불릿 목록: {\"type\": \"list\", \"style\": \"number\", \"items\": [...]}.\n- 본문 텍스트에 **볼드**, *이탤릭*, `코드` 인라인 서식 사용 가능.\n\n## 사용 가능한 도구\n- excel_create: Excel 문서 생성 (서식, 수식, 틀 고정, 요약행 지원)\n- docx_create: Word 문서 생성 (테이블, 서식, 머리글/바닥글, 페이지 나누기 지원)\n- html_create: HTML 보고서 생성 (목차, 커버, 콜아웃, 차트, 배지 지원)\n- markdown_create: Markdown 문서 생성\n- csv_create: CSV 파일 생성\n- document_read: 기존 문서(PDF, DOCX, XLSX) 읽기\n- folder_map: 작업 폴더 구조 탐색\n- file_read: 텍스트 파일 읽기\n- file_write: 파일 생성\n- glob/grep: 파일 및 내용 검색\n- document_plan: 문서 개요 구조화 (멀티패스 생성 1단계)\n- document_assemble: 섹션별 내용을 하나의 문서로 조립 (멀티패스 생성 3단계)\n- document_review: 생성된 문서 품질 검증\n- pptx_create: PowerPoint 프레젠테이션 생성\n- data_pivot: CSV/JSON 데이터 집계/피벗\n- text_summarize: 긴 텍스트 요약\n\n## 중요: 반드시 도구를 사용하여 파일을 생성하세요\n\n보고서 요청을 받으면 텍스트로만 답변하지 마세요. 반드시 html_create, docx_create, excel_create 등 도구를 호출하여 실제 파일을 생성해야 합니다.\n\n### 기본 전략 (빠른 생성)\n- html_create 또는 docx_create를 직접 호출하여 완성된 보고서를 한 번에 생성합니다.\n- 보고서 내용을 모두 포함하여 도구를 호출하세요. 개요만 텍스트로 작성하고 끝내지 마세요.\n\n### 멀티패스 전략 (고품질 설정 ON 시, 3페이지 이상)\n1단계 — document_plan 도구로 문서 구조를 설계합니다.\n2단계 — 각 섹션을 개별적으로 상세하게 작성합니다.\n3단계 — document_assemble 도구로 하나의 문서로 결합합니다.", + "systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 보고서를 작성합니다.\n\n## 핵심 원칙\n- 데이터를 **상세하고 구체적으로** 작성합니다. 항목을 생략하지 않습니다.\n- 표(테이블)는 가능한 많은 행과 열을 포함합니다.\n- 수치 데이터는 단위를 명확히 표기합니다.\n- 결과물은 Excel(.xlsx), Word(.docx), HTML(.html) 중 가장 적합한 형식을 선택합니다.\n- 필요할 때만 계획을 드러내고, 도구를 사용하여 실제 파일을 생성합니다.\n- 파일 탐색은 glob/grep과 document_read/file_read를 우선하고, folder_map은 폴더 구조 확인이 필요할 때만 사용합니다.\n\n## 문서 품질 가이드\n\n### HTML 보고서 (html_create)\n- **toc: true** 로 목차를 자동 생성하세요.\n- **numbered: true** 로 섹션 번호(1., 1-1.)를 자동 부여하세요.\n- **cover** 파라미터로 커버 페이지를 추가하세요: {\"title\": \"...\", \"subtitle\": \"...\", \"author\": \"...\"}.\n- 콜아웃을 활용하세요:
중요 정보
(info/warning/tip/danger/note).\n- 배지를 활용하세요: 완료 (blue/green/red/yellow/purple/gray/orange).\n- CSS 바 차트:
항목
75%
.\n- 그리드 레이아웃:
또는 grid-3, grid-4로 카드 배치.\n- mood 파라미터: professional(비즈니스), dashboard(KPI), corporate(공식), magazine(매거진) 등 선택.\n\n### Excel (excel_create)\n- 기본 style: 'styled' — 파란 헤더, 줄무늬, 테두리 자동 적용.\n- **freeze_header: true** 로 헤더 행 틀 고정.\n- **summary_row** 로 합계/평균 행 자동 생성: {\"label\": \"합계\", \"columns\": {\"B\": \"SUM\", \"C\": \"AVERAGE\"}}.\n- 수식은 셀 값에 '=SUM(B2:B10)' 형태로 입력.\n- **col_widths** 로 열 너비 지정: [20, 15, 12].\n\n### Word (docx_create)\n- **header/footer** 파라미터로 머리글/바닥글 추가. {page}로 페이지 번호.\n- sections에서 type: \"table\" 로 스타일 테이블 삽입 (파란 헤더, 줄무늬).\n- type: \"pagebreak\" 로 페이지 나누기.\n- type: \"list\" 로 번호/불릿 목록: {\"type\": \"list\", \"style\": \"number\", \"items\": [...]}.\n- 본문 텍스트에 **볼드**, *이탤릭*, `코드` 인라인 서식 사용 가능.\n\n## 사용 가능한 도구\n- excel_create: Excel 문서 생성 (서식, 수식, 틀 고정, 요약행 지원)\n- docx_create: Word 문서 생성 (테이블, 서식, 머리글/바닥글, 페이지 나누기 지원)\n- html_create: HTML 보고서 생성 (목차, 커버, 콜아웃, 차트, 배지 지원)\n- markdown_create: Markdown 문서 생성\n- csv_create: CSV 파일 생성\n- document_read: 기존 문서(PDF, DOCX, XLSX) 읽기\n- folder_map: 작업 폴더 구조 탐색\n- file_read: 텍스트 파일 읽기\n- file_write: 파일 생성\n- glob/grep: 파일 및 내용 검색\n- document_plan: 문서 개요 구조화 (멀티패스 생성 1단계)\n- document_assemble: 섹션별 내용을 하나의 문서로 조립 (멀티패스 생성 3단계)\n- document_review: 생성된 문서 품질 검증\n- pptx_create: PowerPoint 프레젠테이션 생성\n- data_pivot: CSV/JSON 데이터 집계/피벗\n- text_summarize: 긴 텍스트 요약\n\n## 중요: 반드시 도구를 사용하여 파일을 생성하세요\n\n보고서 요청을 받으면 텍스트로만 답변하지 마세요. 반드시 html_create, docx_create, excel_create 등 도구를 호출하여 실제 파일을 생성해야 합니다.\n\n### 기본 전략 (빠른 생성)\n- html_create 또는 docx_create를 직접 호출하여 완성된 보고서를 한 번에 생성합니다.\n- 보고서 내용을 모두 포함하여 도구를 호출하세요. 개요만 텍스트로 작성하고 끝내지 마세요.\n\n### 멀티패스 전략 (고품질 설정 ON 시, 3페이지 이상)\n1단계 — 필요할 때만 document_plan 도구로 문서 구조를 설계합니다.\n2단계 — 각 섹션을 개별적으로 상세하게 작성합니다.\n3단계 — document_assemble 도구로 하나의 문서로 결합합니다.", "placeholder": "어떤 보고서를 작성할까요? (예: 삼성디스플레이 연혁 보고서)" } diff --git a/src/AxCopilot/Assets/foldy_qdy.png b/src/AxCopilot/Assets/foldy_qdy.png new file mode 100644 index 0000000..416b381 Binary files /dev/null and b/src/AxCopilot/Assets/foldy_qdy.png differ diff --git a/src/AxCopilot/Assets/gif/20260410_132427092.gif b/src/AxCopilot/Assets/gif/20260410_132427092.gif new file mode 100644 index 0000000..0fd6a95 Binary files /dev/null and b/src/AxCopilot/Assets/gif/20260410_132427092.gif differ diff --git a/src/AxCopilot/Assets/gif/20260410_132431645.gif b/src/AxCopilot/Assets/gif/20260410_132431645.gif new file mode 100644 index 0000000..9a7e97a Binary files /dev/null and b/src/AxCopilot/Assets/gif/20260410_132431645.gif differ diff --git a/src/AxCopilot/Assets/gif/20260410_132436408.gif b/src/AxCopilot/Assets/gif/20260410_132436408.gif new file mode 100644 index 0000000..1d17e45 Binary files /dev/null and b/src/AxCopilot/Assets/gif/20260410_132436408.gif differ diff --git a/src/AxCopilot/Assets/gif/20260410_132441642.gif b/src/AxCopilot/Assets/gif/20260410_132441642.gif new file mode 100644 index 0000000..e74c1bf Binary files /dev/null and b/src/AxCopilot/Assets/gif/20260410_132441642.gif differ diff --git a/src/AxCopilot/Assets/gif/20260410_132445944.gif b/src/AxCopilot/Assets/gif/20260410_132445944.gif new file mode 100644 index 0000000..cf517ec Binary files /dev/null and b/src/AxCopilot/Assets/gif/20260410_132445944.gif differ diff --git a/src/AxCopilot/Assets/gif/20260410_132450677.gif b/src/AxCopilot/Assets/gif/20260410_132450677.gif new file mode 100644 index 0000000..0416c79 Binary files /dev/null and b/src/AxCopilot/Assets/gif/20260410_132450677.gif differ diff --git a/src/AxCopilot/Assets/gif/20260410_132454622.gif b/src/AxCopilot/Assets/gif/20260410_132454622.gif new file mode 100644 index 0000000..40da3f0 Binary files /dev/null and b/src/AxCopilot/Assets/gif/20260410_132454622.gif differ diff --git a/src/AxCopilot/Assets/gif/20260410_132459185.gif b/src/AxCopilot/Assets/gif/20260410_132459185.gif new file mode 100644 index 0000000..3268824 Binary files /dev/null and b/src/AxCopilot/Assets/gif/20260410_132459185.gif differ diff --git a/src/AxCopilot/Assets/gif/20260410_132503634.gif b/src/AxCopilot/Assets/gif/20260410_132503634.gif new file mode 100644 index 0000000..f67840e Binary files /dev/null and b/src/AxCopilot/Assets/gif/20260410_132503634.gif differ diff --git a/src/AxCopilot/Assets/pixel_art.png b/src/AxCopilot/Assets/pixel_art.png new file mode 100644 index 0000000..c0622dd Binary files /dev/null and b/src/AxCopilot/Assets/pixel_art.png differ diff --git a/src/AxCopilot/Assets/pixel_art.txt b/src/AxCopilot/Assets/pixel_art.txt new file mode 100644 index 0000000..4dd5e90 --- /dev/null +++ b/src/AxCopilot/Assets/pixel_art.txt @@ -0,0 +1,197 @@ +from PIL import Image, ImageDraw, ImageFont + +# 1. 색상 정의 (RGB 값) - 확장된 팔레트 +B = (20, 20, 20) # Background (Dark Gray) +G1 = (121, 210, 180) # Mint Green (Main body) +G2 = (100, 180, 160) # Mint Green (Shadow) +W1 = (255, 255, 255) # White (Face/Belly) +W2 = (240, 240, 240) # White (Shadow) +P1 = (255, 180, 210) # Pink (Main Body) +P2 = (255, 150, 190) # Pink (Fluffy texture detail) +P3 = (240, 130, 170) # Pink (Shadow) +E1 = (0, 0, 0) # Eyes (Black) +E2 = (40, 40, 40) # Eyes (Pupil detail) +C_RED = (255, 80, 80) # Ear pattern Red +C_YLW = (255, 230, 80) # Ear pattern Yellow +C_BLU = (80, 150, 255) # Ear pattern Blue +C_PUR = (180, 80, 255) # Ear pattern Purple +C_GRN = (80, 220, 120) # Pink hair pattern Green +ORNG = (255, 145, 77) # Feet (Orange) + +# 2. 텍스트 그리기 함수 (픽셀 폰트) +def draw_pixel_text(draw, text, position, font_size=16, color=(255, 255, 255)): + # 실제 픽셀 폰트를 사용하려면 폰트 파일이 필요합니다. + # 여기서는 Pillow의 기본 폰트를 사용하지만, 픽셀 폰트처럼 보이게 할 수 있습니다. + # 더 좋은 결과를 위해 실제 .ttf 픽셀 폰트를 로드하는 것이 좋습니다. + try: + font = ImageFont.truetype("Arial.ttf", font_size) # 실제 폰트 파일 경로 + except IOError: + font = ImageFont.load_default() # 폰트 파일이 없으면 기본 폰트 사용 + font_size = 12 # 기본 폰트 크기 + + draw.text(position, text, font=font, fill=color) + +# 3. 캐릭터 그리기 함수 (개념적 블록 조합) +def draw_pixel_bunny(draw, position, size, pose, is_detailed=True): + # size: 캐릭터의 기본 크기 (예: 32x32) + # is_detailed: 원본의 단순화된 스타일 대신 더 정교한 디테일을 추가할지 여부 + + x_off, y_off = position + scale = size[0] // 32 + + # 기본 형태 그리기 + # 머리 + head_color = G1 + head_size = int(size[0] * 0.5) + head_y = y_off + int(size[1] * 0.1) + draw.ellipse((x_off + int(size[0]*0.25), head_y, + x_off + int(size[0]*0.75), head_y + head_size), + fill=head_color) + + # 얼굴 흰 부분 + draw.ellipse((x_off + int(size[0]*0.3), head_y + int(head_size*0.1), + x_off + int(size[0]*0.7), head_y + int(head_size*0.9)), + fill=W1) + + # 귀 + draw.rectangle((x_off + int(size[0]*0.3), head_y - int(size[1]*0.1), + x_off + int(size[0]*0.4), head_y), + fill=head_color) + draw.rectangle((x_off + int(size[0]*0.6), head_y - int(size[1]*0.1), + x_off + int(size[0]*0.7), head_y), + fill=head_color) + + # 귀 디테일 (원본의 X 패턴 대신 더 복잡한 줄무늬) + if is_detailed: + draw.rectangle((x_off + int(size[0]*0.31), head_y - int(size[1]*0.08), + x_off + int(size[0]*0.39), head_y - int(size[1]*0.06)), fill=C_RED) + draw.rectangle((x_off + int(size[0]*0.31), head_y - int(size[1]*0.06), + x_off + int(size[0]*0.39), head_y - int(size[1]*0.04)), fill=C_YLW) + draw.rectangle((x_off + int(size[0]*0.31), head_y - int(size[1]*0.04), + x_off + int(size[0]*0.39), head_y - int(size[1]*0.02)), fill=C_BLU) + draw.rectangle((x_off + int(size[0]*0.31), head_y - int(size[1]*0.02), + x_off + int(size[0]*0.39), head_y), fill=C_PUR) + + draw.rectangle((x_off + int(size[0]*0.61), head_y - int(size[1]*0.08), + x_off + int(size[0]*0.69), head_y - int(size[1]*0.06)), fill=C_RED) + draw.rectangle((x_off + int(size[0]*0.61), head_y - int(size[1]*0.06), + x_off + int(size[0]*0.69), head_y - int(size[1]*0.04)), fill=C_YLW) + draw.rectangle((x_off + int(size[0]*0.61), head_y - int(size[1]*0.04), + x_off + int(size[0]*0.69), head_y - int(size[1]*0.02)), fill=C_BLU) + draw.rectangle((x_off + int(size[0]*0.61), head_y - int(size[1]*0.02), + x_off + int(size[0]*0.69), head_y), fill=C_PUR) + + # 눈 + draw.ellipse((x_off + int(size[0]*0.4), head_y + int(head_size*0.3), + x_off + int(size[0]*0.48), head_y + int(head_size*0.5)), + fill=E1) + draw.ellipse((x_off + int(size[0]*0.52), head_y + int(head_size*0.3), + x_off + int(size[0]*0.6), head_y + int(head_size*0.5)), + fill=E1) + # 눈동자 디테일 + if is_detailed: + draw.ellipse((x_off + int(size[0]*0.42), head_y + int(head_size*0.35), + x_off + int(size[0]*0.46), head_y + int(head_size*0.45)), + fill=W1) + draw.ellipse((x_off + int(size[0]*0.54), head_y + int(head_size*0.35), + x_off + int(size[0]*0.58), head_y + int(head_size*0.45)), + fill=W1) + + # 몸통 + draw.rectangle((x_off + int(size[0]*0.3), head_y + head_size, + x_off + int(size[0]*0.7), head_y + head_size + int(size[1]*0.3)), + fill=head_color) + # 배 흰 부분 + draw.ellipse((x_off + int(size[0]*0.35), head_y + head_size + int(size[1]*0.05), + x_off + int(size[0]*0.65), head_y + head_size + int(size[1]*0.25)), + fill=W1) + + # 다리 + draw.rectangle((x_off + int(size[0]*0.35), head_y + head_size + int(size[1]*0.3), + x_off + int(size[0]*0.45), head_y + head_size + int(size[1]*0.4)), + fill=head_color) + draw.rectangle((x_off + int(size[0]*0.55), head_y + head_size + int(size[1]*0.3), + x_off + int(size[0]*0.65), head_y + head_size + int(size[1]*0.4)), + fill=head_color) + # 발 (주황색) + draw.rectangle((x_off + int(size[0]*0.35), head_y + head_size + int(size[1]*0.38), + x_off + int(size[0]*0.45), head_y + head_size + int(size[1]*0.43)), + fill=ORNG) + draw.rectangle((x_off + int(size[0]*0.55), head_y + head_size + int(size[1]*0.38), + x_off + int(size[0]*0.65), head_y + head_size + int(size[1]*0.43)), + fill=ORNG) + + # 측면 포즈일 경우 디테일 추가 + if pose == 'side': + # 눈 모양 변경 + # ... 측면 포즈에 대한 더 많은 코드 ... + pass + +def draw_pixel_pink_fluffball(draw, position, size, pose, is_detailed=True): + # draw_pixel_bunny와 유사한 구조, 하지만 털 질감을 추가 + x_off, y_off = position + scale = size[0] // 32 + + # 머리 (털 질감) + head_y = y_off + int(size[1] * 0.1) + for _ in range(int(size[0]*0.5)): + for _ in range(int(size[0]*0.5)): + draw.point((x_off + int(size[0]*0.25) + _, head_y + _), fill=P1) + + # ... 더 많은 털 질감과 디테일 코드 ... + pass + +# 4. 이미지 크기 정의 및 배경 생성 +canvas_width = 800 +canvas_height = 600 +img = Image.new('RGB', (canvas_width, canvas_height), color=B) +draw = ImageDraw.Draw(img) + +# 5. 이미지 구성 + +# 상단 제목 그리기 +draw_pixel_text(draw, "정교한 캐릭터 시트", (int(canvas_width*0.35), int(canvas_height*0.05)), font_size=32) + +# 왼쪽 패널 그리기 (민트 토끼 캐릭터 '더 자세히' 업그레이드) +panel_bunny_y = int(canvas_height*0.2) +panel_title_bunny = "민트 토끼 슈트 캐릭터" +draw_pixel_text(draw, panel_title_bunny, (int(canvas_width*0.1), panel_bunny_y), font_size=20) + +bunny_size_detailed = (200, 200) # 원본보다 더 큰 크기로 그려서 디테일 표현 +bunny_pos_detailed = (int(canvas_width*0.1), panel_bunny_y + 40) +draw_pixel_bunny(draw, bunny_pos_detailed, bunny_size_detailed, 'front', is_detailed=True) + +draw_pixel_text(draw, "정면", (int(canvas_width*0.1 + bunny_size_detailed[0]*0.4), panel_bunny_y + 40 + bunny_size_detailed[1]), font_size=16) + +bunny_pos_detailed_side = (int(canvas_width*0.1 + bunny_size_detailed[0] + 50), panel_bunny_y + 40) +draw_pixel_bunny(draw, bunny_pos_detailed_side, bunny_size_detailed, 'side', is_detailed=True) + +draw_pixel_text(draw, "측면 45도", (int(canvas_width*0.1 + bunny_size_detailed[0] + 50 + bunny_size_detailed[0]*0.3), panel_bunny_y + 40 + bunny_size_detailed[1]), font_size=16) + +# 오른쪽 패널 그리기 (핑크 털뭉치 캐릭터 '더 자세히' 업그레이드) +panel_pink_y = int(canvas_height*0.2) +panel_title_pink = "핑크 털 뭉치 슈트 캐릭터" +draw_pixel_text(draw, panel_title_pink, (int(canvas_width*0.5), panel_pink_y), font_size=20) + +pink_size_detailed = (200, 200) # 원본보다 더 큰 크기로 그려서 디테일 표현 +pink_pos_detailed = (int(canvas_width*0.5), panel_pink_y + 40) +draw_pixel_pink_fluffball(draw, pink_pos_detailed, pink_size_detailed, 'front', is_detailed=True) + +draw_pixel_text(draw, "정면", (int(canvas_width*0.5 + pink_size_detailed[0]*0.4), panel_pink_y + 40 + pink_size_detailed[1]), font_size=16) + +pink_pos_detailed_side = (int(canvas_width*0.5 + pink_size_detailed[0] + 50), panel_pink_y + 40) +draw_pixel_pink_fluffball(draw, pink_pos_detailed_side, pink_size_detailed, 'side', is_detailed=True) + +draw_pixel_text(draw, "측면 45도", (int(canvas_width*0.5 + pink_size_detailed[0] + 50 + pink_size_detailed[0]*0.3), panel_pink_y + 40 + pink_size_detailed[1]), font_size=16) + +# 6. 보기 좋게 확대 및 픽셀 격자 표현 (옵션) +# 픽셀 아트 격자 효과를 주려면, 이미지를 작게 그린 후 크게 확대합니다. +# 이 코드는 개념적인 블록 그리기 방식을 사용하여 직접 디테일한 픽셀 아트를 생성하므로, +# 실제 격자 확대 효과는 생략합니다. + +# 7. 결과 저장 및 보여주기 +output_filename = "detailed_pixel_characters_sheet.png" +img.save(output_filename) +img.show() + +print(f"정교한 캐릭터 시트 이미지가 '{output_filename}'으로 저장되었습니다. {canvas_width}x{canvas_height} 크기") \ No newline at end of file diff --git a/src/AxCopilot/Assets/ppt/BASIC100 기준 템플릿 V1.pptx b/src/AxCopilot/Assets/ppt/BASIC100 기준 템플릿 V1.pptx new file mode 100644 index 0000000..7031ed7 Binary files /dev/null and b/src/AxCopilot/Assets/ppt/BASIC100 기준 템플릿 V1.pptx differ diff --git a/src/AxCopilot/Assets/ppt/CORE100 기준템플릿 V1.pptx b/src/AxCopilot/Assets/ppt/CORE100 기준템플릿 V1.pptx new file mode 100644 index 0000000..920f742 Binary files /dev/null and b/src/AxCopilot/Assets/ppt/CORE100 기준템플릿 V1.pptx differ diff --git a/src/AxCopilot/Assets/ppt/P01_01_프레임디자인_PPT템플릿_블루(PPT_편집용).pptx b/src/AxCopilot/Assets/ppt/P01_01_프레임디자인_PPT템플릿_블루(PPT_편집용).pptx new file mode 100644 index 0000000..8a87000 Binary files /dev/null and b/src/AxCopilot/Assets/ppt/P01_01_프레임디자인_PPT템플릿_블루(PPT_편집용).pptx differ diff --git a/src/AxCopilot/Assets/ppt/미스터 피피티 03_원본.pptx b/src/AxCopilot/Assets/ppt/미스터 피피티 03_원본.pptx new file mode 100644 index 0000000..d0c3ce6 Binary files /dev/null and b/src/AxCopilot/Assets/ppt/미스터 피피티 03_원본.pptx differ diff --git a/src/AxCopilot/Assets/ppt/미스터 피피티 템플릿 01_원본.pptx b/src/AxCopilot/Assets/ppt/미스터 피피티 템플릿 01_원본.pptx new file mode 100644 index 0000000..a2eca9c Binary files /dev/null and b/src/AxCopilot/Assets/ppt/미스터 피피티 템플릿 01_원본.pptx differ diff --git a/src/AxCopilot/Assets/ppt/미스터 피피티 템플릿 02_원본.pptx b/src/AxCopilot/Assets/ppt/미스터 피피티 템플릿 02_원본.pptx new file mode 100644 index 0000000..8afae85 Binary files /dev/null and b/src/AxCopilot/Assets/ppt/미스터 피피티 템플릿 02_원본.pptx differ diff --git a/src/AxCopilot/Assets/ppt/미스터 피피티 템플릿 04_원본.pptx b/src/AxCopilot/Assets/ppt/미스터 피피티 템플릿 04_원본.pptx new file mode 100644 index 0000000..9d2e73f Binary files /dev/null and b/src/AxCopilot/Assets/ppt/미스터 피피티 템플릿 04_원본.pptx differ diff --git a/src/AxCopilot/Assets/ppt/미스터 피피티 템플릿_05_원본.pptx b/src/AxCopilot/Assets/ppt/미스터 피피티 템플릿_05_원본.pptx new file mode 100644 index 0000000..9c07f06 Binary files /dev/null and b/src/AxCopilot/Assets/ppt/미스터 피피티 템플릿_05_원본.pptx differ diff --git a/src/AxCopilot/AxCopilot.csproj b/src/AxCopilot/AxCopilot.csproj index d636b77..8ab9311 100644 --- a/src/AxCopilot/AxCopilot.csproj +++ b/src/AxCopilot/AxCopilot.csproj @@ -68,6 +68,7 @@ + @@ -114,6 +115,11 @@ + + + PreserveNewest @@ -125,6 +131,14 @@ + + + + PreserveNewest + Assets\gif\%(Filename)%(Extension) + + + diff --git a/src/AxCopilot/AxCopilot_zj0aajh3_wpftmp.csproj b/src/AxCopilot/AxCopilot_zj0aajh3_wpftmp.csproj new file mode 100644 index 0000000..4c1f48d --- /dev/null +++ b/src/AxCopilot/AxCopilot_zj0aajh3_wpftmp.csproj @@ -0,0 +1,422 @@ + + + AxCopilot + obj\Release\ + obj\ + E:\AX Copilot - Claude\src\AxCopilot\obj\ + <_TargetAssemblyProjectName>AxCopilot + AxCopilot + + + + WinExe + net8.0-windows10.0.17763.0 + enable + enable + true + true + AxCopilot + Assets\icon.ico + + 0.7.3 + AX + AX Copilot + AI 기반 업무 자동화 런처 & 코파일럿 + + true + + win-x64 + + + none + false + + false + + false + false + + + + true + + false + + false + + true + true + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + PreserveNewest + + + + + + + + + + PreserveNewest + + + + PreserveNewest + system_prompt.txt + + + + + + PreserveNewest + Assets\gif\%(Filename)%(Extension) + + + + + + PreserveNewest + skills\%(Filename)%(Extension) + + + + + + + + + <_Parameter1>AxCopilot.Tests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/AxCopilot/Handlers/ChatHandler.cs b/src/AxCopilot/Handlers/ChatHandler.cs index 6edfb65..40f62a2 100644 --- a/src/AxCopilot/Handlers/ChatHandler.cs +++ b/src/AxCopilot/Handlers/ChatHandler.cs @@ -64,7 +64,7 @@ public class ChatHandler : IActionHandler try { - var storage = new ChatStorageService(); + var storage = ServiceLocator.Get(); var metas = storage.LoadAllMeta(); foreach (var conv in metas.Take(5)) { diff --git a/src/AxCopilot/Models/AppSettings.cs b/src/AxCopilot/Models/AppSettings.cs index 1c3a0c5..668d458 100644 --- a/src/AxCopilot/Models/AppSettings.cs +++ b/src/AxCopilot/Models/AppSettings.cs @@ -185,6 +185,14 @@ public class LauncherSettings [JsonPropertyName("enableIconAnimation")] public bool EnableIconAnimation { get; set; } = true; + /// 채팅 본문 런처 아이콘 랜덤 애니메이션. false이면 숨쉬기만. 기본 false. + [JsonPropertyName("enableChatIconRandomAnimation")] + public bool EnableChatIconRandomAnimation { get; set; } = false; + + /// 채팅 본문 런처 아이콘 글로우 강도. strong/medium/weak. 기본 medium. + [JsonPropertyName("chatIconGlowIntensity")] + public string ChatIconGlowIntensity { get; set; } = "medium"; + /// 런처 안내 문구 랜덤 출력 활성화. false이면 고정 문구. 기본 true. [JsonPropertyName("enableRandomPlaceholder")] public bool EnableRandomPlaceholder { get; set; } = true; @@ -804,10 +812,18 @@ public class LlmSettings [JsonPropertyName("recentPromptTemplates")] public List RecentPromptTemplates { get; set; } = new(); - /// 작업 폴더 경로. 빈 문자열이면 미선택. + /// 작업 폴더 경로. 빈 문자열이면 미선택. (레거시 — 탭별 경로 미지정 시 폴백) [JsonPropertyName("workFolder")] public string WorkFolder { get; set; } = ""; + /// 코워크(Cowork) 탭 전용 작업 폴더. + [JsonPropertyName("coworkWorkFolder")] + public string CoworkWorkFolder { get; set; } = ""; + + /// 코드(Code) 탭 전용 작업 폴더. + [JsonPropertyName("codeWorkFolder")] + public string CodeWorkFolder { get; set; } = ""; + /// 최근 사용한 작업 폴더 목록. [JsonPropertyName("recentWorkFolders")] public List RecentWorkFolders { get; set; } = new(); @@ -885,9 +901,9 @@ public class LlmSettings [JsonPropertyName("maxTestFixIterations")] public int MaxTestFixIterations { get; set; } = 5; - /// 에이전트 로그 표시 수준. simple | detailed | debug + /// 에이전트 로그 표시 수준. hidden | simple | detailed | debug [JsonPropertyName("agentLogLevel")] - public string AgentLogLevel { get; set; } = "simple"; + public string AgentLogLevel { get; set; } = "detailed"; /// AX Agent UI 표현 수준. rich | balanced | simple [JsonPropertyName("agentUiExpressionLevel")] @@ -1055,6 +1071,14 @@ public class LlmSettings [JsonPropertyName("enableChatRainbowGlow")] public bool EnableChatRainbowGlow { get; set; } = false; + /// 새로운 계획 뷰어(V2 사이드바 레이아웃) 사용. 기본 true. + [JsonPropertyName("enableNewPlanViewer")] + public bool EnableNewPlanViewer { get; set; } = true; + + /// 새로운 채팅 렌더링(V2 상세 이력) 사용. 기본 false. + [JsonPropertyName("enableNewChatRendering")] + public bool EnableNewChatRendering { get; set; } = false; + /// AX Agent 전용 테마. system | light | dark [JsonPropertyName("agentTheme")] public string AgentTheme { get; set; } = "system"; @@ -1069,6 +1093,11 @@ public class LlmSettings [JsonPropertyName("notifyOnComplete")] public bool NotifyOnComplete { get; set; } = false; + /// 코워크 작업 완료 후 문서 자동 처리 방식. + /// "none" = 아무것도 안하기, "open" = 문서 실행(기본 앱), "preview" = 미리보기 뷰어. + [JsonPropertyName("coworkOnComplete")] + public string CoworkOnComplete { get; set; } = "none"; + /// AI 대화창에서 팁 알림 표시 여부. [JsonPropertyName("showTips")] public bool ShowTips { get; set; } = false; @@ -1315,6 +1344,18 @@ public class CodeSettings /// Code 탭에서 Cron 도구(cron create/list/delete) 사용 여부. 기본 true. [JsonPropertyName("enableCronTools")] public bool EnableCronTools { get; set; } = true; + + /// + /// Code 탭 빈 화면 마스코트 캐릭터 출동 수준. + /// "none"=출동 안하기, "one"=한명만(1), "few"=적게(3), "mid"=중간(6), "all"=전부(9+) + /// 기본 "none" (메모리 절약). + /// + [JsonPropertyName("mascotLevel")] + public string MascotLevel { get; set; } = "none"; + + /// (하위호환) 이전 bool 설정 — 무시됨. MascotLevel로 대체. + [JsonPropertyName("enableMascotCharacter")] + public bool EnableMascotCharacter { get; set; } = false; } /// 사용자 정의 커스텀 프리셋 (settings.json에 저장). diff --git a/src/AxCopilot/Models/ChatModels.cs b/src/AxCopilot/Models/ChatModels.cs index 942fe89..7074718 100644 --- a/src/AxCopilot/Models/ChatModels.cs +++ b/src/AxCopilot/Models/ChatModels.cs @@ -63,6 +63,10 @@ public class ChatConversation [JsonPropertyName("pinned")] public bool Pinned { get; set; } = false; + /// 아카이브된 대화는 목록에서 기본 숨김 처리. + [JsonPropertyName("archived")] + public bool Archived { get; set; } = false; + /// 대화가 속한 탭. "Chat" | "Cowork" | "Code". [JsonPropertyName("tab")] public string Tab { get; set; } = "Chat"; @@ -126,7 +130,7 @@ public class ChatConversation public List DraftQueueItems { get; set; } = new(); [JsonPropertyName("showExecutionHistory")] - public bool ShowExecutionHistory { get; set; } = true; + public bool ShowExecutionHistory { get; set; } = false; [JsonPropertyName("agentRunHistory")] public List AgentRunHistory { get; set; } = new(); diff --git a/src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs b/src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs index 0bec2fc..a555f3b 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs @@ -24,6 +24,8 @@ public partial class AgentLoopService public bool BroadScanDetected { get; set; } public bool SelectiveHit { get; set; } public bool CorrectiveHintInjected { get; set; } + /// 스킬 런타임이 allowed-tools를 명시했으면 true — 탐색 필터링을 건너뜀. + public bool SkillAllowedToolsActive { get; set; } } private static IReadOnlyCollection FilterExplorationToolsForCurrentIteration( @@ -36,6 +38,11 @@ public partial class AgentLoopService if (tools.Count == 0) return tools; + // 스킬 런타임 정책으로 allowed-tools가 명시된 경우 탐색 필터링을 건너뜀 + // — 스킬이 의도적으로 허용한 도구(folder_map 등)를 정책이 차단하면 안 됨 + if (state.SkillAllowedToolsActive) + return tools; + // 문서 생성 모드: 생성 도구를 최우선, 탐색 도구를 뒤로 배치 if (state.Scope == ExplorationScope.DirectCreation) { diff --git a/src/AxCopilot/Services/Agent/AgentLoopExplorationRecovery.cs b/src/AxCopilot/Services/Agent/AgentLoopExplorationRecovery.cs new file mode 100644 index 0000000..5c46cf0 --- /dev/null +++ b/src/AxCopilot/Services/Agent/AgentLoopExplorationRecovery.cs @@ -0,0 +1,185 @@ +using System.IO; +using System.Text.Json; +using AxCopilot.Models; + +namespace AxCopilot.Services.Agent; + +public partial class AgentLoopService +{ + private static readonly HashSet FolderMapRecoveryIgnoredDirs = new(StringComparer.OrdinalIgnoreCase) + { + "bin", "obj", "node_modules", ".git", ".vs", ".idea", ".vscode", + "__pycache__", ".mypy_cache", ".pytest_cache", "dist", "build", + "packages", ".nuget", "TestResults", "coverage", ".next", + "target", ".gradle", ".cargo", + }; + + private string BuildToolResultFeedbackMessage( + ContentBlock call, + ToolResult result, + RunState runState, + AgentContext context, + List messages, + ExplorationTrackingState explorationState, + int iteration) + { + var message = BuildLoopToolResultMessage(call, result, runState); + if (!TryBuildFolderMapEmptyRecoveryMessage( + call, + result, + context, + messages, + explorationState, + iteration, + out var recoveryAppendix)) + return message; + + return $"{message}\n\n{recoveryAppendix}"; + } + + private bool TryBuildFolderMapEmptyRecoveryMessage( + ContentBlock call, + ToolResult result, + AgentContext context, + List messages, + ExplorationTrackingState explorationState, + int iteration, + out string recoveryAppendix) + { + recoveryAppendix = ""; + if (!result.Success + || !string.Equals(call.ToolName, "folder_map", StringComparison.OrdinalIgnoreCase) + || !LooksLikeEmptyFolderMapResult(result.Output)) + return false; + + var baseDir = ResolveFolderMapTargetPath(call.ToolInput, context); + if (string.IsNullOrWhiteSpace(baseDir) || !Directory.Exists(baseDir)) + return false; + + var candidates = CollectFolderMapRecoveryCandidates(baseDir, context, maxResults: 6); + if (candidates.Count == 0) + return false; + + var displayPath = Path.GetRelativePath(context.WorkFolder, baseDir).Replace('\\', '/'); + if (string.IsNullOrWhiteSpace(displayPath) || displayPath == ".") + displayPath = "."; + + var suggestedPatterns = candidates + .Select(path => Path.GetExtension(path)) + .Where(ext => !string.IsNullOrWhiteSpace(ext)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Take(2) + .Select(ext => $"**/*{ext}") + .ToList(); + + var candidateList = string.Join("\n", candidates.Select(path => $"- {path}")); + var patternHint = suggestedPatterns.Count > 0 + ? $"\nSuggested glob patterns:\n- {string.Join("\n- ", suggestedPatterns)}" + : ""; + + recoveryAppendix = + "[Recovery] folder_map returned 0 files and 0 dirs, but a fallback scan found candidate paths. " + + "Do not repeat folder_map for the same path. Switch to glob first, then read only the best candidates.\n" + + $"Fallback candidates under '{displayPath}':\n{candidateList}{patternHint}"; + + messages.Add(new ChatMessage + { + Role = "system", + Content = + "[System:FolderMapEmptyRecovery] folder_map returned an empty result, but direct file scanning found visible candidates.\n" + + $"Path: {displayPath}\n" + + "Do not call folder_map again for the same path unless the user explicitly asks for structure only.\n" + + "Use glob to narrow the set, then use file_read or document_read on the best matches.\n" + + $"Candidates:\n{candidateList}{patternHint}" + }); + + explorationState.CorrectiveHintInjected = true; + explorationState.BroadScanDetected = true; + explorationState.SelectiveHit = false; + + WorkflowLogService.LogTransition( + _conversationId, + _currentRunId, + iteration, + "folder_map_empty_recovery", + $"{displayPath} -> {candidates.Count} candidates"); + + EmitEvent( + AgentEventType.Thinking, + "folder_map", + "빈 folder_map 결과를 복구하는 중 · 파일 후보로 다시 좁히는 중"); + return true; + } + + private static string ResolveFolderMapTargetPath(JsonElement? toolInput, AgentContext context) + { + if (toolInput is not { ValueKind: JsonValueKind.Object } input) + return context.WorkFolder; + + var rawPath = input.SafeTryGetProperty("path", out var pathElement) + ? pathElement.SafeGetString() ?? "" + : ""; + + return string.IsNullOrWhiteSpace(rawPath) + ? context.WorkFolder + : FileReadTool.ResolvePath(rawPath, context.WorkFolder); + } + + private static bool LooksLikeEmptyFolderMapResult(string? output) + { + if (string.IsNullOrWhiteSpace(output)) + return false; + + return output.Contains("0 files, 0 dirs", StringComparison.OrdinalIgnoreCase) + || output.Contains("0 files, 0 directories", StringComparison.OrdinalIgnoreCase); + } + + private static List CollectFolderMapRecoveryCandidates(string baseDir, AgentContext context, int maxResults) + { + var candidates = new List(maxResults); + var pending = new Stack<(string Path, int Depth)>(); + pending.Push((baseDir, 0)); + + while (pending.Count > 0 && candidates.Count < maxResults) + { + var (current, depth) = pending.Pop(); + + try + { + foreach (var file in Directory.EnumerateFiles(current)) + { + if (candidates.Count >= maxResults) + break; + + var fileName = Path.GetFileName(file); + if (string.IsNullOrWhiteSpace(fileName) + || fileName.StartsWith('.') + || !context.IsPathAllowed(file)) + continue; + + candidates.Add(Path.GetRelativePath(context.WorkFolder, file).Replace('\\', '/')); + } + + if (depth >= 3) + continue; + + foreach (var dir in Directory.EnumerateDirectories(current)) + { + var name = Path.GetFileName(dir); + if (string.IsNullOrWhiteSpace(name) + || name.StartsWith('.') + || FolderMapRecoveryIgnoredDirs.Contains(name) + || !context.IsPathAllowed(dir)) + continue; + + pending.Push((dir, depth + 1)); + } + } + catch + { + } + } + + return candidates; + } +} diff --git a/src/AxCopilot/Services/Agent/AgentLoopParallelExecution.cs b/src/AxCopilot/Services/Agent/AgentLoopParallelExecution.cs index d45c073..23388c8 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopParallelExecution.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopParallelExecution.cs @@ -25,11 +25,11 @@ public partial class AgentLoopService }; /// 도구 호출을 병렬 가능 / 순차 필수로 분류합니다. - private static (List Parallel, List Sequential) - ClassifyToolCalls(List calls) + private static (List Parallel, List Sequential) + ClassifyToolCalls(List calls) { - var parallel = new List(); - var sequential = new List(); + var parallel = new List(); + var sequential = new List(); var collectParallelPrefix = true; foreach (var call in calls) @@ -81,7 +81,7 @@ public partial class AgentLoopService /// 읽기 전용 도구들을 병렬 실행합니다. private async Task ExecuteToolsInParallelAsync( - List calls, + List calls, List messages, AgentContext context, List planSteps, @@ -100,13 +100,13 @@ public partial class AgentLoopService .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) .ToList(); - var executableCalls = new List(); + var executableCalls = new List(); foreach (var call in calls) { var resolvedToolName = ResolveRequestedToolName(call.ToolName, activeToolNames); var effectiveCall = string.Equals(call.ToolName, resolvedToolName, StringComparison.OrdinalIgnoreCase) ? call - : new LlmService.ContentBlock + : new ContentBlock { Type = call.Type, Text = call.Text, diff --git a/src/AxCopilot/Services/Agent/AgentLoopPathStagnation.cs b/src/AxCopilot/Services/Agent/AgentLoopPathStagnation.cs new file mode 100644 index 0000000..3a16385 --- /dev/null +++ b/src/AxCopilot/Services/Agent/AgentLoopPathStagnation.cs @@ -0,0 +1,103 @@ +using System.IO; +using System.Text.Json; +using AxCopilot.Models; + +namespace AxCopilot.Services.Agent; + +public partial class AgentLoopService +{ + private sealed class PathAccessTrackingState + { + public string? LastPath { get; set; } + public int ConsecutiveCount { get; set; } + } + + private bool TryHandleRepeatedPathAccessTransition( + ContentBlock call, + AgentContext context, + PathAccessTrackingState state, + List messages, + string? lastModifiedCodeFilePath, + bool requireHighImpactCodeVerification, + TaskTypePolicy taskPolicy) + { + if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)) + return false; + + var currentPath = ExtractTrackedTargetPath(call, context); + var shouldTrack = !string.IsNullOrWhiteSpace(currentPath) && IsPathBoundReadOnlyInspectionTool(call.ToolName); + + if (!shouldTrack) + { + if (ResetsRepeatedPathTracking(call.ToolName)) + { + state.LastPath = null; + state.ConsecutiveCount = 0; + } + return false; + } + + if (string.Equals(state.LastPath, currentPath, StringComparison.OrdinalIgnoreCase)) + state.ConsecutiveCount++; + else + { + state.LastPath = currentPath; + state.ConsecutiveCount = 1; + } + + if (state.ConsecutiveCount < 4) + return false; + + messages.Add(new ChatMessage + { + Role = "user", + Content = + "[System:RepeatedPathGuard] 동일 경로를 읽기 도구로 반복 확인하고 있습니다.\n" + + $"반복 경로: {currentPath}\n" + + "지금은 같은 파일/경로를 다시 읽지 말고, 다음 우선순위로 전환하세요.\n" + + "1. grep/glob으로 관련 호출부, 참조 지점, 테스트 파일을 찾기\n" + + "2. git_tool(diff)로 실제 변경 범위를 확인하기\n" + + "3. 필요한 경우에만 file_edit 또는 build_run/test_loop로 다음 단계 진행하기\n" + + "같은 경로에 대한 file_read/document_read/grep 반복은 중단하세요." + }); + EmitEvent( + AgentEventType.Thinking, + call.ToolName, + $"같은 경로 반복 접근을 감지해 흐름을 전환합니다 · {Path.GetFileName(currentPath)}"); + state.ConsecutiveCount = 0; + state.LastPath = null; + return true; + } + + private static bool IsPathBoundReadOnlyInspectionTool(string toolName) + => toolName is "file_read" or "document_read" or "grep" or "glob"; + + private static bool ResetsRepeatedPathTracking(string toolName) + => toolName is "file_edit" or "file_write" or "file_manage" or "git_tool" or "build_run" or "test_loop" or "folder_map"; + + private static string? ExtractTrackedTargetPath(ContentBlock call, AgentContext context) + { + if (call.ToolInput is not { ValueKind: JsonValueKind.Object } input) + return null; + + var path = input.SafeTryGetProperty("path", out var pathProp) + ? pathProp.SafeGetString() + : null; + + if (string.IsNullOrWhiteSpace(path) + && input.SafeTryGetProperty("project_path", out var projectPathProp)) + path = projectPathProp.SafeGetString(); + + if (string.IsNullOrWhiteSpace(path)) + return null; + + try + { + return FileReadTool.ResolvePath(path, context.WorkFolder); + } + catch + { + return path; + } + } +} diff --git a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs index d19ec70..3756663 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs @@ -114,6 +114,12 @@ public partial class AgentLoopService if (!_docFallbackAttempted && !documentPlanWasCalled) return (false, false); + if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)) + { + EmitEvent(AgentEventType.Complete, "", "에이전트 작업 완료"); + return (true, false); + } + var verificationEnabled = executionPolicy.EnablePostToolVerification && AgentTabSettingsResolver.IsPostToolVerificationEnabled(ActiveTab, llm); var shouldVerify = ShouldRunPostToolVerification( @@ -156,6 +162,9 @@ public partial class AgentLoopService if (!shouldVerify) return false; + if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)) + return false; + if (string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)) { var highImpactCodeChange = IsHighImpactCodeModification(ActiveTab ?? "", call.ToolName, result); diff --git a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs index bd28585..2851c4d 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs @@ -179,7 +179,7 @@ public partial class AgentLoopService return sawDiff; } - // 蹂€寃??꾧뎄 ?먯껜媛€ ?놁쑝硫?diff 寃뚯씠???꾧뎄?몄? ?뺤씤 + // 변경 도구 자체가 없으면 diff 게이트 도구인지 확인 return true; } @@ -198,8 +198,8 @@ public partial class AgentLoopService "insertions", "deletions", "file changed", - "異붽?", - "?낅뜲?댄듃"); + "추가", + "업데이트"); } private bool TryApplyDocumentArtifactGateTransition( @@ -392,9 +392,9 @@ public partial class AgentLoopService { var content = m.Content ?? ""; if (content.StartsWith("{\"_tool_use_blocks\"")) - content = "[?꾧뎄 ?몄텧 ?붿빟]"; + content = "[도구 실행 요약]"; else if (content.StartsWith("{\"type\":\"tool_result\"")) - content = "[?꾧뎄 ?ㅽ뻾 寃곌낵 ?붿빟]"; + content = "[도구 실행 결과 요약]"; else if (content.Length > 180) content = content[..180] + "..."; return $"- [{m.Role}] {content}"; @@ -403,7 +403,7 @@ public partial class AgentLoopService var summary = summaryLines.Count > 0 ? string.Join("\n", summaryLines) - : "- ?댁쟾 ?€??留λ씫???먮룞 異뺤빟?섏뿀?듬땲??"; + : "- 이전 대화 맥락이 자동 축약되었습니다"; var tail = nonSystem.Skip(Math.Max(0, nonSystem.Count - keepTailCount)).ToList(); @@ -414,7 +414,7 @@ public partial class AgentLoopService { Role = "user", Timestamp = DateTime.Now, - Content = $"[?쒖뒪???먮룞 留λ씫 異뺤빟]\n?꾨옒???댁쟾 ?€?붿쓽 ?듭떖 ?붿빟?낅땲??\n{summary}" + Content = $"[시스템 자동 맥락 축약]\n아래는 이전 대화의 축소 요약입니다.\n{summary}" }); messages.AddRange(tail); return true; @@ -458,7 +458,7 @@ public partial class AgentLoopService EmitEvent( AgentEventType.Thinking, call.ToolName, - $"?숈씪 ?꾧뎄/?뚮씪誘명꽣 諛섎났 ?ㅽ뙣 ?⑦꽩 媛먯? - ?ㅻⅨ ?묎렐?쇰줈 ?꾪솚?⑸땲??({repeatedFailedToolSignatureCount}/{maxRetry})"); + $"동일 도구/파라미터 반복 실패 패턴 감지 - 다른 접근으로 전환합니다({repeatedFailedToolSignatureCount}/{maxRetry})"); return true; } @@ -479,7 +479,7 @@ public partial class AgentLoopService messages.Add(LlmService.CreateToolResultMessage( call.ToolId, call.ToolName, - $"[NO_PROGRESS_LOOP_GUARD] ?숈씪???쎄린 ?꾧뎄 ?몄텧??{repeatedSameSignatureCount}??諛섎났?섏뿀?듬땲?? {toolCallSignature}\n" + + $"[NO_PROGRESS_LOOP_GUARD] 동일한 읽기 도구 실행이 {repeatedSameSignatureCount}회 반복되었습니다. {toolCallSignature}\n" + "Stop repeating the same read-only call and switch to a concrete next action.")); messages.Add(new ChatMessage { @@ -493,7 +493,7 @@ public partial class AgentLoopService EmitEvent( AgentEventType.Thinking, call.ToolName, - $"臾댁쓽誘명븳 ?쎄린 ?꾧뎄 諛섎났 猷⑦봽媛€ 媛먯??섏뼱 ?ㅻⅨ ?꾨왂?쇰줈 ?꾪솚?⑸땲??({repeatedSameSignatureCount}??"); + $"무의미한 읽기 도구 반복 루프가 감지되어 다른 방향으로 전환합니다({repeatedSameSignatureCount}회)"); return true; } @@ -520,7 +520,7 @@ public partial class AgentLoopService EmitEvent( AgentEventType.Thinking, "", - $"?쎄린 ?꾩슜 ?꾧뎄媛€ ?곗냽 {consecutiveReadOnlySuccessTools}???ㅽ뻾?섏뼱 ?뺤껜媛€ 媛먯??섏뿀?듬땲?? ?ㅽ뻾 ?④퀎濡??꾪솚?⑸땲??(threshold={threshold})"); + $"읽기 전용 도구가 연속 {consecutiveReadOnlySuccessTools}회 실행되어 정체가 감지되었습니다. 실행 전략으로 전환합니다(threshold={threshold})"); return true; } @@ -554,7 +554,7 @@ public partial class AgentLoopService EmitEvent( AgentEventType.Thinking, "", - $"?ㅽ뻾 吏꾩쟾???놁뼱 媛뺤젣 蹂듦뎄 ?④퀎濡??쒖옉?⑸땲??({runState.NoProgressRecoveryRetry}/2)"); + $"실행 진전이 없어 강제 복구 전략으로 시작합니다({runState.NoProgressRecoveryRetry}/2)"); return true; } @@ -629,11 +629,11 @@ public partial class AgentLoopService "forbidden", "not found", "invalid", - "?ㅽ뙣", - "?ㅻ쪟", - "?덉쇅", - "?쒓컙 珥덇낵", - "沅뚰븳 嫄곕?", + "실패", + "오류", + "예외", + "시간 초과", + "권한 거부", "李⑤떒", "찾을 수"); } @@ -1052,7 +1052,7 @@ public partial class AgentLoopService result.Output) }); } - EmitEvent(AgentEventType.Thinking, "", $"Self-Reflection: ?ㅽ뙣 遺꾩꽍 ???ъ떆??({consecutiveErrors}/{maxRetry})"); + EmitEvent(AgentEventType.Thinking, "", $"Self-Reflection: 실패 분석 후 재시도({consecutiveErrors}/{maxRetry})"); return true; } @@ -1139,7 +1139,7 @@ public partial class AgentLoopService if (devShouldContinue) { messages.Add(LlmService.CreateToolResultMessage( - call.ToolId, call.ToolName, devToolResultMessage ?? "[SKIPPED by developer] 媛쒕컻?먭? ???꾧뎄 ?ㅽ뻾??嫄대꼫?곗뿀?듬땲??")); + call.ToolId, call.ToolName, devToolResultMessage ?? "[SKIPPED by developer] 개발자에 의해 도구 실행이 건너뛰어졌습니다")); return (true, null); } } @@ -1160,7 +1160,7 @@ public partial class AgentLoopService if (scopeShouldContinue) { messages.Add(LlmService.CreateToolResultMessage( - call.ToolId, call.ToolName, scopeToolResultMessage ?? "[SKIPPED] 媛쒕컻?먭? ???묒뾽??嫄대꼫?곗뿀?듬땲??")); + call.ToolId, call.ToolName, scopeToolResultMessage ?? "[SKIPPED] 사용자에 의해 작업이 건너뛰어졌습니다")); return (true, null); } } diff --git a/src/AxCopilot/Services/Agent/AgentLoopTransitions.cs b/src/AxCopilot/Services/Agent/AgentLoopTransitions.cs index 9409ff5..f48ff53 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopTransitions.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopTransitions.cs @@ -4,11 +4,11 @@ namespace AxCopilot.Services.Agent; public partial class AgentLoopService { - private static (bool ShouldRun, List ParallelBatch, List SequentialBatch) - CreateParallelExecutionPlan(bool parallelEnabled, List toolCalls, int maxParallelBatch) + private static (bool ShouldRun, List ParallelBatch, List SequentialBatch) + CreateParallelExecutionPlan(bool parallelEnabled, List toolCalls, int maxParallelBatch) { if (!parallelEnabled || toolCalls.Count <= 1) - return (false, new List(), toolCalls); + return (false, new List(), toolCalls); var (parallelBatch, sequentialBatch) = ClassifyToolCalls(toolCalls); if (maxParallelBatch > 0 && parallelBatch.Count > maxParallelBatch) diff --git a/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs b/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs index 7c8ba4e..bddbcd0 100644 --- a/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs +++ b/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace AxCopilot.Services.Agent; @@ -125,7 +125,7 @@ internal static class AgentTranscriptDisplayCatalog { var summary = (evt.Summary ?? string.Empty).Trim(); if (!string.IsNullOrWhiteSpace(summary)) - return summary; + return StripNonBmpCharacters(summary); return evt.Type switch { @@ -265,13 +265,16 @@ internal static class AgentTranscriptDisplayCatalog if (resultPresentation != null) { + // ToolResult의 GroupKey를 선행 ToolCall과 동일한 activity 그룹으로 설정 + // → ProcessFeed에서 ToolCall 카드를 ToolResult로 교체(머지) + var resultGroup = ResolveActivityGroup(toolName, summary); return new AgentTranscriptRowPresentation( TranscriptRowKind.ToolResult, "결과", resultPresentation.Label, resultPresentation.Description, - $"result:{resultPresentation.Kind}:{resultPresentation.StatusKind}", - false, + $"activity:{resultGroup}", + true, resultPresentation.NeedsAttention); } @@ -420,4 +423,32 @@ internal static class AgentTranscriptDisplayCatalog _ => "도구", }; } + + /// + /// WPF 기본 폰트(Segoe UI)에서 렌더링되지 않는 비-BMP 유니코드 문자(이모지 등)를 제거합니다. + /// LLM 응답에 이모지가 포함되면 깨져서 표시되는 문제를 방지합니다. + /// + public static string StripNonBmpCharacters(string text) + { + if (string.IsNullOrEmpty(text)) return string.Empty; + + // Consolas / Segoe UI에서 렌더링 불가한 비-BMP 유니코드(이모지, 서로게이트 쌍) 제거 + var sb = new System.Text.StringBuilder(text.Length); + for (int i = 0; i < text.Length; i++) + { + var c = text[i]; + if (char.IsHighSurrogate(c)) + { + if (i + 1 < text.Length && char.IsLowSurrogate(text[i + 1])) + i++; + continue; + } + if (char.IsLowSurrogate(c)) continue; + if (c >= 0x2600 && c <= 0x27BF) continue; // Misc symbols, Dingbats + if (c >= 0x2B50 && c <= 0x2B55) continue; // Additional symbols + if (c >= 0xFE00 && c <= 0xFE0F) continue; // Variation selectors + sb.Append(c); + } + return sb.ToString().Trim(); + } } diff --git a/src/AxCopilot/Services/Agent/BuildRunTool.cs b/src/AxCopilot/Services/Agent/BuildRunTool.cs index 0152855..cdfce54 100644 --- a/src/AxCopilot/Services/Agent/BuildRunTool.cs +++ b/src/AxCopilot/Services/Agent/BuildRunTool.cs @@ -100,15 +100,18 @@ public class BuildRunTool : IAgentTool } else { + // "run" 액션은 빌드로 대체 — 프로그램 실행은 사용자가 직접 수행 command = action switch { "build" => project.BuildCommand, "test" => project.TestCommand, - "run" => project.RunCommand, + "run" => project.BuildCommand, // run → build로 대체 (실행은 사용자 몫) "lint" => string.IsNullOrEmpty(project.LintCommand) ? null : project.LintCommand, "format" => string.IsNullOrEmpty(project.FormatCommand) ? null : project.FormatCommand, _ => project.BuildCommand, }; + if (action == "run") + action = "build"; // 출력 메시지에도 build로 표시 if (command == null) return ToolResult.Fail($"이 프로젝트 타입({project.Type})에서 '{action}' 작업은 지원되지 않습니다."); } diff --git a/src/AxCopilot/Services/Agent/DocumentAssemblerTool.cs b/src/AxCopilot/Services/Agent/DocumentAssemblerTool.cs index d9d03f5..5d9aa4a 100644 --- a/src/AxCopilot/Services/Agent/DocumentAssemblerTool.cs +++ b/src/AxCopilot/Services/Agent/DocumentAssemblerTool.cs @@ -135,7 +135,7 @@ public class DocumentAssemblerTool : IAgentTool var pageEstimate = Math.Max(1, totalWords / 500); return ToolResult.Ok( - $"✅ 문서 조립 완료: {Path.GetFileName(fullPath)}\n" + + $"✅ 문서 조립 완료: {fullPath}\n" + $" 섹션: {sections.Count}개 | 글자: {totalChars:N0} | 단어: ~{totalWords:N0} | 예상 페이지: ~{pageEstimate}\n" + $"{resultMsg}", fullPath); } @@ -241,14 +241,30 @@ public class DocumentAssemblerTool : IAgentTool path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document); var mainPart = doc.AddMainDocumentPart(); + + // 기본 스타일 파트 추가 (styles.xml — 없으면 Word에서 글꼴/서식 깨짐) + var stylesPart = mainPart.AddNewPart(); + stylesPart.Styles = CreateDefaultDocxStyles(); + stylesPart.Styles.Save(); + mainPart.Document = new DocumentFormat.OpenXml.Wordprocessing.Document(); var body = mainPart.Document.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Body()); + // 한글 호환 글꼴 설정 헬퍼 + static DocumentFormat.OpenXml.Wordprocessing.RunFonts KoreanFonts() => new() + { + Ascii = "맑은 고딕", + HighAnsi = "맑은 고딕", + EastAsia = "맑은 고딕", + ComplexScript = "맑은 고딕" + }; + // 제목 var titlePara = new DocumentFormat.OpenXml.Wordprocessing.Paragraph(); var titleRun = new DocumentFormat.OpenXml.Wordprocessing.Run(); titleRun.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.RunProperties { + RunFonts = KoreanFonts(), Bold = new DocumentFormat.OpenXml.Wordprocessing.Bold(), FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "48" } }); @@ -267,6 +283,7 @@ public class DocumentAssemblerTool : IAgentTool var headRun = new DocumentFormat.OpenXml.Wordprocessing.Run(); headRun.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.RunProperties { + RunFonts = KoreanFonts(), Bold = new DocumentFormat.OpenXml.Wordprocessing.Bold(), FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = level <= 1 ? "32" : "28" }, Color = new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "2B579A" }, @@ -281,6 +298,11 @@ public class DocumentAssemblerTool : IAgentTool { var para = new DocumentFormat.OpenXml.Wordprocessing.Paragraph(); var run = new DocumentFormat.OpenXml.Wordprocessing.Run(); + run.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.RunProperties + { + RunFonts = KoreanFonts(), + FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "22" } + }); run.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Text(line.Trim()) { Space = DocumentFormat.OpenXml.SpaceProcessingModeValues.Preserve @@ -293,9 +315,59 @@ public class DocumentAssemblerTool : IAgentTool body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Paragraph()); } + // ★ SectionProperties는 반드시 body의 마지막 자식이어야 함 (OOXML 규격) + // 첫 번째에 넣으면 Word가 무시하거나 문서가 깨짐 + body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.SectionProperties( + new DocumentFormat.OpenXml.Wordprocessing.PageSize { Width = 11906, Height = 16838 }, + new DocumentFormat.OpenXml.Wordprocessing.PageMargin { Top = 1440, Right = 1440, Bottom = 1440, Left = 1440, + Header = 720, Footer = 720, Gutter = 0 } + )); + + mainPart.Document.Save(); return " ✓ DOCX 조립 완료"; } + /// DOCX 기본 스타일 정의 생성 (한글 글꼴 기본 설정 포함). + private static DocumentFormat.OpenXml.Wordprocessing.Styles CreateDefaultDocxStyles() + { + var styles = new DocumentFormat.OpenXml.Wordprocessing.Styles(); + + // 문서 기본 글꼴 설정 + var docDefaults = new DocumentFormat.OpenXml.Wordprocessing.DocDefaults( + new DocumentFormat.OpenXml.Wordprocessing.RunPropertiesDefault( + new DocumentFormat.OpenXml.Wordprocessing.RunPropertiesBaseStyle( + new DocumentFormat.OpenXml.Wordprocessing.RunFonts + { + Ascii = "맑은 고딕", + HighAnsi = "맑은 고딕", + EastAsia = "맑은 고딕", + ComplexScript = "맑은 고딕" + }, + new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "22" }, + new DocumentFormat.OpenXml.Wordprocessing.Languages { Val = "ko-KR", EastAsia = "ko-KR" } + ) + ), + new DocumentFormat.OpenXml.Wordprocessing.ParagraphPropertiesDefault( + new DocumentFormat.OpenXml.Wordprocessing.ParagraphPropertiesBaseStyle( + new DocumentFormat.OpenXml.Wordprocessing.SpacingBetweenLines { After = "160", Line = "259", LineRule = DocumentFormat.OpenXml.Wordprocessing.LineSpacingRuleValues.Auto } + ) + ) + ); + styles.AppendChild(docDefaults); + + // Normal 스타일 + var normalStyle = new DocumentFormat.OpenXml.Wordprocessing.Style + { + Type = DocumentFormat.OpenXml.Wordprocessing.StyleValues.Paragraph, + StyleId = "Normal", + Default = true + }; + normalStyle.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.StyleName { Val = "Normal" }); + styles.AppendChild(normalStyle); + + return styles; + } + private string AssembleMarkdown(string path, string title, List<(string Heading, string Content, int Level)> sections) { var sb = new StringBuilder(); diff --git a/src/AxCopilot/Services/Agent/DocumentPlannerTool.cs b/src/AxCopilot/Services/Agent/DocumentPlannerTool.cs index 9774bda..529e34e 100644 --- a/src/AxCopilot/Services/Agent/DocumentPlannerTool.cs +++ b/src/AxCopilot/Services/Agent/DocumentPlannerTool.cs @@ -316,7 +316,7 @@ public class DocumentPlannerTool : IAgentTool folder_data_usage = folderDataUsage, step1 = hasRefData ? "Reference data is already provided above. Skip to step 3." - : "Use folder_map to scan the work folder, then use document_read to read RELEVANT files only.", + : "Use glob or grep to narrow relevant files first, then use document_read or file_read on the best candidates only. Use folder_map only when the user explicitly asks for folder structure or existing materials in a folder.", step2 = hasRefData ? "(skipped)" : "Summarize the key findings from the folder documents relevant to the topic.", @@ -420,6 +420,12 @@ public class DocumentPlannerTool : IAgentTool path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document); var mainPart = doc.AddMainDocumentPart(); + + // 기본 스타일 파트 추가 (styles.xml — 없으면 Word에서 글꼴/서식 깨짐) + var stylesPart = mainPart.AddNewPart(); + stylesPart.Styles = CreateDefaultDocxStyles(); + stylesPart.Styles.Save(); + mainPart.Document = new DocumentFormat.OpenXml.Wordprocessing.Document(); var body = mainPart.Document.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Body()); @@ -438,6 +444,15 @@ public class DocumentPlannerTool : IAgentTool } AddDocxParagraph(body, ""); // 섹션 간 빈 줄 } + + // ★ SectionProperties는 반드시 body의 마지막 자식이어야 함 (OOXML 규격) + body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.SectionProperties( + new DocumentFormat.OpenXml.Wordprocessing.PageSize { Width = 11906, Height = 16838 }, + new DocumentFormat.OpenXml.Wordprocessing.PageMargin { Top = 1440, Right = 1440, Bottom = 1440, Left = 1440, + Header = 720, Footer = 720, Gutter = 0 } + )); + + mainPart.Document.Save(); } private static void AddDocxParagraph(DocumentFormat.OpenXml.Wordprocessing.Body body, @@ -447,6 +462,13 @@ public class DocumentPlannerTool : IAgentTool var run = new DocumentFormat.OpenXml.Wordprocessing.Run(); var props = new DocumentFormat.OpenXml.Wordprocessing.RunProperties { + RunFonts = new DocumentFormat.OpenXml.Wordprocessing.RunFonts + { + Ascii = "맑은 고딕", + HighAnsi = "맑은 고딕", + EastAsia = "맑은 고딕", + ComplexScript = "맑은 고딕" + }, FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = fontSize } }; if (bold) props.Bold = new DocumentFormat.OpenXml.Wordprocessing.Bold(); @@ -460,6 +482,45 @@ public class DocumentPlannerTool : IAgentTool body.AppendChild(para); } + /// DOCX 기본 스타일 정의 생성 (한글 글꼴 기본 설정 포함). + private static DocumentFormat.OpenXml.Wordprocessing.Styles CreateDefaultDocxStyles() + { + var styles = new DocumentFormat.OpenXml.Wordprocessing.Styles(); + + var docDefaults = new DocumentFormat.OpenXml.Wordprocessing.DocDefaults( + new DocumentFormat.OpenXml.Wordprocessing.RunPropertiesDefault( + new DocumentFormat.OpenXml.Wordprocessing.RunPropertiesBaseStyle( + new DocumentFormat.OpenXml.Wordprocessing.RunFonts + { + Ascii = "맑은 고딕", + HighAnsi = "맑은 고딕", + EastAsia = "맑은 고딕", + ComplexScript = "맑은 고딕" + }, + new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "22" }, + new DocumentFormat.OpenXml.Wordprocessing.Languages { Val = "ko-KR", EastAsia = "ko-KR" } + ) + ), + new DocumentFormat.OpenXml.Wordprocessing.ParagraphPropertiesDefault( + new DocumentFormat.OpenXml.Wordprocessing.ParagraphPropertiesBaseStyle( + new DocumentFormat.OpenXml.Wordprocessing.SpacingBetweenLines { After = "160", Line = "259", LineRule = DocumentFormat.OpenXml.Wordprocessing.LineSpacingRuleValues.Auto } + ) + ) + ); + styles.AppendChild(docDefaults); + + var normalStyle = new DocumentFormat.OpenXml.Wordprocessing.Style + { + Type = DocumentFormat.OpenXml.Wordprocessing.StyleValues.Paragraph, + StyleId = "Normal", + Default = true + }; + normalStyle.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.StyleName { Val = "Normal" }); + styles.AppendChild(normalStyle); + + return styles; + } + // ─── Markdown 생성 ────────────────────────────────────────────────────── private static void GenerateMarkdown(string path, string title, List sections) @@ -631,7 +692,7 @@ public class DocumentPlannerTool : IAgentTool return normalized; var intent = $"{docType} {topic}".ToLowerInvariant(); - if (ContainsAny(intent, "대시보드", "dashboard", "분석", "analysis", "지표", "통계")) + if (ContainsAny(intent, "대시보드", "dashboard", "지표")) return "dashboard"; if (ContainsAny(intent, "기획", "아이디어", "creative", "브레인스토밍")) return "creative"; @@ -639,11 +700,15 @@ public class DocumentPlannerTool : IAgentTool return "corporate"; if (ContainsAny(intent, "가이드", "manual", "guide", "매뉴얼")) return "minimal"; + // 보고서/분석: corporate 무드 (배경색 포함) + if (ContainsAny(intent, "보고서", "report", "분석", "analysis", "통계")) + return "corporate"; return docType switch { "proposal" => "corporate", - "analysis" => "dashboard", + "analysis" => "corporate", + "report" => "corporate", "manual" or "guide" => "minimal", "presentation" => "creative", "minutes" => "professional", diff --git a/src/AxCopilot/Services/Agent/DocumentReaderTool.cs b/src/AxCopilot/Services/Agent/DocumentReaderTool.cs index 139456c..a792f68 100644 --- a/src/AxCopilot/Services/Agent/DocumentReaderTool.cs +++ b/src/AxCopilot/Services/Agent/DocumentReaderTool.cs @@ -4,6 +4,7 @@ using System.Text.Json; using System.Text.RegularExpressions; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Spreadsheet; +using A = DocumentFormat.OpenXml.Drawing; using UglyToad.PdfPig; namespace AxCopilot.Services.Agent; @@ -17,7 +18,7 @@ public class DocumentReaderTool : IAgentTool public string Name => "document_read"; public string Description => "Read a document file and extract its text content. " + - "Supports: PDF (.pdf), Word (.docx), Excel (.xlsx), CSV (.csv), text (.txt/.log/.json/.xml/.md), " + + "Supports: PDF (.pdf), Word (.docx), PowerPoint (.pptx), Excel (.xlsx), CSV (.csv), text (.txt/.log/.json/.xml/.md), " + "BibTeX (.bib), RIS (.ris). " + "For large files, use 'offset' to read from a specific character position (chunked reading). " + "For large PDFs, use 'pages' parameter to read specific page ranges (e.g., '1-5', '10-20'). " + @@ -79,6 +80,7 @@ public class DocumentReaderTool : IAgentTool { ".pdf" => await Task.Run(() => ReadPdf(fullPath, extractMax, pagesParam, sectionParam), ct), ".docx" => await Task.Run(() => ReadDocx(fullPath, extractMax), ct), + ".pptx" => await Task.Run(() => ReadPptx(fullPath, extractMax), ct), ".xlsx" => await Task.Run(() => ReadXlsx(fullPath, sheetParam, extractMax), ct), ".bib" => await Task.Run(() => ReadBibTeX(fullPath, extractMax), ct), ".ris" => await Task.Run(() => ReadRis(fullPath, extractMax), ct), @@ -478,6 +480,71 @@ public class DocumentReaderTool : IAgentTool return Truncate(sb.ToString(), maxChars); } + // ─── PPTX ─────────────────────────────────────────────────────────────── + + private static string ReadPptx(string path, int maxChars) + { + var sb = new StringBuilder(); + using var doc = PresentationDocument.Open(path, false); + var presentation = doc.PresentationPart; + if (presentation?.Presentation?.SlideIdList == null) + return "(빈 프레젠테이션)"; + + var slideIds = presentation.Presentation.SlideIdList.Elements().ToList(); + sb.AppendLine($"PowerPoint: {slideIds.Count}개 슬라이드"); + sb.AppendLine(); + + int slideNum = 0; + foreach (var slideId in slideIds) + { + if (sb.Length >= maxChars) break; + slideNum++; + + var relId = slideId.RelationshipId?.Value; + if (relId == null) continue; + + var slidePart = (SlidePart)presentation.GetPartById(relId); + var slide = slidePart.Slide; + if (slide == null) continue; + + sb.AppendLine($"--- Slide {slideNum} ---"); + + // 슬라이드 내 모든 텍스트 추출 (도형, 텍스트 박스, 테이블 등) + var texts = slide.Descendants() + .Select(t => t.Text) + .Where(t => !string.IsNullOrWhiteSpace(t)); + + // 단락(Paragraph) 단위로 그룹핑하여 줄바꿈 유지 + var paragraphs = slide.Descendants(); + foreach (var para in paragraphs) + { + var paraText = string.Join("", para.Descendants().Select(t => t.Text)); + if (!string.IsNullOrWhiteSpace(paraText)) + sb.AppendLine(paraText); + } + + // 슬라이드 노트가 있으면 포함 + var notesPart = slidePart.NotesSlidePart; + if (notesPart != null) + { + var noteTexts = notesPart.NotesSlide.Descendants() + .Select(p => string.Join("", p.Descendants().Select(t => t.Text))) + .Where(t => !string.IsNullOrWhiteSpace(t)) + .ToList(); + if (noteTexts.Count > 0) + { + sb.AppendLine(" [노트]"); + foreach (var note in noteTexts) + sb.AppendLine($" {note}"); + } + } + + sb.AppendLine(); + } + + return Truncate(sb.ToString(), maxChars); + } + // ─── XLSX ─────────────────────────────────────────────────────────────── private static string ReadXlsx(string path, string sheetParam, int maxChars) diff --git a/src/AxCopilot/Services/Agent/DocxSkill.cs b/src/AxCopilot/Services/Agent/DocxSkill.cs index 6b7a596..8b6ef12 100644 --- a/src/AxCopilot/Services/Agent/DocxSkill.cs +++ b/src/AxCopilot/Services/Agent/DocxSkill.cs @@ -38,7 +38,9 @@ public class DocxSkill : IAgentTool "• List: {\"type\": \"list\", \"style\": \"bullet|number\", \"items\": [\"item1\", \" - sub-item\"]}\n" + "• Callout: {\"type\": \"callout\", \"style\": \"info|warning|tip|danger\", \"title\": \"...\", \"body\": \"...\"}\n" + "• HighlightBox: {\"type\": \"highlight_box\", \"text\": \"...\", \"color\": \"blue|green|orange|red\"}\n" + - "Body text supports inline formatting: **bold**, *italic*, `code`.", + "• Icon: {\"type\": \"icon\", \"name\": \"checkmark|warning|rocket|star|...\", \"text\": \"optional label text\", \"size\": 28}\n" + + "Body text supports inline formatting: **bold**, *italic*, `code`. " + + "Inline icons in body text: use {icon:name} syntax e.g. '{icon:checkmark} 완료됨'.", Items = new() { Type = "object" } }, ["header"] = new() { Type = "string", Description = "Header text shown at top of every page (optional)." }, @@ -136,6 +138,12 @@ public class DocxSkill : IAgentTool using var doc = WordprocessingDocument.Create(fullPath, WordprocessingDocumentType.Document); var mainPart = doc.AddMainDocumentPart(); + + // 기본 스타일 파트 추가 (styles.xml) + var stylesPart = mainPart.AddNewPart(); + stylesPart.Styles = CreateDefaultDocxStyles(); + stylesPart.Styles.Save(); + mainPart.Document = new Document(); var body = mainPart.Document.AppendChild(new Body()); @@ -194,6 +202,12 @@ public class DocxSkill : IAgentTool continue; } + if (blockType == "icon") + { + body.Append(CreateIconParagraph(section)); + continue; + } + // 일반 섹션 (heading + body) var heading = section.SafeTryGetProperty("heading", out var h) ? h.SafeGetString() ?? "" : ""; var bodyText = section.SafeTryGetProperty("body", out var b) ? b.SafeGetString() ?? "" : ""; @@ -212,6 +226,8 @@ public class DocxSkill : IAgentTool sectionCount++; } + // ★ SectionProperties는 반드시 body의 마지막 자식이어야 함 (OOXML 규격) + EnsureSectionProperties(body); mainPart.Document.Save(); var parts = new List(); @@ -250,7 +266,7 @@ public class DocxSkill : IAgentTool Bold = new Bold(), FontSize = new FontSize { Val = "44" }, // 22pt Color = new Color { Val = theme.Title }, - RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, + RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" }, }; para.Append(run); return para; @@ -280,7 +296,7 @@ public class DocxSkill : IAgentTool Bold = new Bold(), FontSize = new FontSize { Val = fontSize }, Color = new Color { Val = color }, - RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, + RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" }, }; para.Append(run); return para; @@ -299,9 +315,16 @@ public class DocxSkill : IAgentTool return para; } - /// **bold**, *italic*, `code` 인라인 서식을 Run으로 변환 + /// **bold**, *italic*, `code`, {icon:name} 인라인 서식을 Run으로 변환 private static void AppendFormattedRuns(Paragraph para, string text) { + // 먼저 {icon:name} 인라인 아이콘을 심볼로 치환 + text = System.Text.RegularExpressions.Regex.Replace(text, @"\{icon:(\w+)\}", m => + { + var name = m.Groups[1].Value; + return IconLibrary.Contains(name) ? IconLibrary.Resolve(name) : m.Value; + }); + // 패턴: **bold** | *italic* | `code` | 일반텍스트 var regex = new System.Text.RegularExpressions.Regex( @"\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`"); @@ -331,7 +354,7 @@ public class DocxSkill : IAgentTool { var run = CreateRun(match.Groups[3].Value); run.RunProperties ??= new RunProperties(); - run.RunProperties.RunFonts = new RunFonts { Ascii = "Consolas", HighAnsi = "Consolas", EastAsia = "맑은 고딕" }; + run.RunProperties.RunFonts = new RunFonts { Ascii = "Consolas", HighAnsi = "Consolas", EastAsia = "Noto Sans KR" }; run.RunProperties.FontSize = new FontSize { Val = "20" }; run.RunProperties.Shading = new Shading { @@ -360,7 +383,7 @@ public class DocxSkill : IAgentTool run.RunProperties = new RunProperties { FontSize = new FontSize { Val = "22" }, // 11pt - RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, + RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" }, }; return run; } @@ -410,7 +433,7 @@ public class DocxSkill : IAgentTool Bold = new Bold(), FontSize = new FontSize { Val = "20" }, Color = new Color { Val = "FFFFFF" }, - RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, + RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" }, } }); para.ParagraphProperties = new ParagraphProperties @@ -448,7 +471,7 @@ public class DocxSkill : IAgentTool RunProperties = new RunProperties { FontSize = new FontSize { Val = "20" }, - RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, + RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" }, } }); para.ParagraphProperties = new ParagraphProperties @@ -503,7 +526,7 @@ public class DocxSkill : IAgentTool { FontSize = new FontSize { Val = "22" }, Bold = (listStyle == "number" && !isSub) ? new Bold() : null, - RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, + RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" }, }; para.Append(prefixRun); @@ -511,7 +534,7 @@ public class DocxSkill : IAgentTool textRun.RunProperties = new RunProperties { FontSize = new FontSize { Val = "22" }, - RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, + RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" }, }; para.Append(textRun); @@ -552,7 +575,7 @@ public class DocxSkill : IAgentTool Bold = new Bold(), FontSize = new FontSize { Val = "22" }, Color = new Color { Val = borderColor }, - RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, + RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" }, }; titlePara.Append(titleRun); body.Append(titlePara); @@ -642,7 +665,7 @@ public class DocxSkill : IAgentTool { var run = CreateRun(match.Groups[3].Value); run.RunProperties ??= new RunProperties(); - run.RunProperties.RunFonts = new RunFonts { Ascii = "Consolas", HighAnsi = "Consolas", EastAsia = "맑은 고딕" }; + run.RunProperties.RunFonts = new RunFonts { Ascii = "Consolas", HighAnsi = "Consolas", EastAsia = "Noto Sans KR" }; run.RunProperties.FontSize = new FontSize { Val = "20" }; run.RunProperties.Shading = new Shading { @@ -679,6 +702,57 @@ public class DocxSkill : IAgentTool // 머리글/바닥글 // ═══════════════════════════════════════════════════ + /// Body의 마지막에 SectionProperties를 추가합니다 (OOXML 규격: 반드시 마지막 자식). + private static void EnsureSectionProperties(Body body) + { + // 기존 sectPr이 마지막이 아닌 위치에 있으면 제거 후 재추가 + var existing = body.GetFirstChild(); + if (existing != null) + { + // 이미 마지막 자식이면 유지 + if (existing == body.LastChild) return; + existing.Remove(); + } + body.AppendChild(new SectionProperties( + new PageSize { Width = 11906, Height = 16838 }, // A4 + new PageMargin { Top = 1440, Right = 1440, Bottom = 1440, Left = 1440, + Header = 720, Footer = 720, Gutter = 0 } + )); + } + + /// DOCX 기본 스타일 정의 생성 (한글 글꼴 기본 설정 포함). + private static Styles CreateDefaultDocxStyles() + { + var styles = new Styles(); + var docDefaults = new DocDefaults( + new RunPropertiesDefault( + new RunPropertiesBaseStyle( + new RunFonts + { + Ascii = "맑은 고딕", + HighAnsi = "맑은 고딕", + EastAsia = "맑은 고딕", + ComplexScript = "맑은 고딕" + }, + new FontSize { Val = "22" }, + new Languages { Val = "ko-KR", EastAsia = "ko-KR" } + ) + ), + new ParagraphPropertiesDefault( + new ParagraphPropertiesBaseStyle( + new SpacingBetweenLines { After = "160", Line = "259", LineRule = LineSpacingRuleValues.Auto } + ) + ) + ); + styles.AppendChild(docDefaults); + + var normalStyle = new Style { Type = StyleValues.Paragraph, StyleId = "Normal", Default = true }; + normalStyle.AppendChild(new StyleName { Val = "Normal" }); + styles.AppendChild(normalStyle); + + return styles; + } + private static void AddHeaderFooter(MainDocumentPart mainPart, Body body, string? headerText, string? footerText, bool showPageNumbers) { @@ -693,7 +767,7 @@ public class DocxSkill : IAgentTool { FontSize = new FontSize { Val = "18" }, // 9pt Color = new Color { Val = "808080" }, - RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, + RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" }, } }); para.ParagraphProperties = new ParagraphProperties @@ -766,10 +840,44 @@ public class DocxSkill : IAgentTool { FontSize = new FontSize { Val = "16" }, Color = new Color { Val = "999999" }, - RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, + RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" }, } }; + /// 아이콘 블록: 큰 심볼 + 선택적 라벨 텍스트. + private static Paragraph CreateIconParagraph(System.Text.Json.JsonElement section) + { + var iconName = section.SafeTryGetProperty("name", out var n) ? n.SafeGetString() ?? "" : ""; + var label = section.SafeTryGetProperty("text", out var t) ? t.SafeGetString() ?? "" : ""; + var fontSize = section.SafeTryGetProperty("size", out var sz) ? sz.GetInt32() : 28; + var symbol = IconLibrary.Resolve(iconName); + + var para = new Paragraph(); + para.ParagraphProperties = new ParagraphProperties + { + SpacingBetweenLines = new SpacingBetweenLines { Line = "360" }, + }; + + // 아이콘 심볼 (큰 폰트) + var iconRun = new Run(new Text(symbol + " ") { Space = SpaceProcessingModeValues.Preserve }); + iconRun.RunProperties = new RunProperties + { + FontSize = new FontSize { Val = (fontSize * 2).ToString() }, // half-pt 단위 + RunFonts = new RunFonts { Ascii = "Segoe UI Emoji", HighAnsi = "Segoe UI Emoji", EastAsia = "Segoe UI Emoji" }, + }; + para.Append(iconRun); + + // 라벨 텍스트 (일반 폰트) + if (!string.IsNullOrWhiteSpace(label)) + { + var labelRun = CreateRun(label); + labelRun.RunProperties!.FontSize = new FontSize { Val = "24" }; // 12pt + para.Append(labelRun); + } + + return para; + } + private static Run CreatePageNumberRun() { var run = new Run(); @@ -777,7 +885,7 @@ public class DocxSkill : IAgentTool { FontSize = new FontSize { Val = "16" }, Color = new Color { Val = "999999" }, - RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, + RunFonts = new RunFonts { Ascii = "Noto Sans KR", HighAnsi = "Noto Sans KR", EastAsia = "Noto Sans KR" }, }; run.Append(new FieldChar { FieldCharType = FieldCharValues.Begin }); run.Append(new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve }); diff --git a/src/AxCopilot/Services/Agent/DocxToHtmlConverter.cs b/src/AxCopilot/Services/Agent/DocxToHtmlConverter.cs index bb3518b..652d177 100644 --- a/src/AxCopilot/Services/Agent/DocxToHtmlConverter.cs +++ b/src/AxCopilot/Services/Agent/DocxToHtmlConverter.cs @@ -172,7 +172,7 @@ internal static class DocxToHtmlConverter "); + sb.AppendLine($"

{System.Net.WebUtility.HtmlEncode(conv.Title)}

"); + sb.AppendLine($"

생성: {conv.CreatedAt:yyyy-MM-dd HH:mm} · 주제: {conv.Category}

"); + + foreach (var msg in conv.Messages) + { + if (msg.Role == "system") continue; + var cls = msg.Role == "user" ? "user" : "ai"; + var label = msg.Role == "user" ? "사용자" : "AI"; + sb.AppendLine($"
"); + sb.AppendLine($"
{label} · {msg.Timestamp:HH:mm}
"); + sb.AppendLine($"
{System.Net.WebUtility.HtmlEncode(msg.Content)}
"); + sb.AppendLine("
"); + } + + sb.AppendLine(""); + return sb.ToString(); + } + + // ─── 버튼 이벤트 ────────────────────────────────────────────────────── + + private void ChatWindow_KeyDown(object sender, KeyEventArgs e) + { + var mod = Keyboard.Modifiers; + + // Ctrl 단축키 + if (mod == ModifierKeys.Control) + { + switch (e.Key) + { + case Key.N: BtnNewChat_Click(this, new RoutedEventArgs()); e.Handled = true; break; + case Key.W: Close(); e.Handled = true; break; + case Key.E: ExportConversation(); e.Handled = true; break; + case Key.L: InputBox.Text = ""; InputBox.Focus(); e.Handled = true; break; + case Key.B: BtnToggleSidebar_Click(this, new RoutedEventArgs()); e.Handled = true; break; + case Key.M: BtnModelSelector_Click(this, new RoutedEventArgs()); e.Handled = true; break; + case Key.OemComma: BtnSettings_Click(this, new RoutedEventArgs()); e.Handled = true; break; + case Key.F: ToggleMessageSearch(); e.Handled = true; break; + case Key.K: OpenSidebarSearch(); e.Handled = true; break; + case Key.D1: TabChat.IsChecked = true; e.Handled = true; break; + case Key.D2: TabCowork.IsChecked = true; e.Handled = true; break; + case Key.D3: if (TabCode.IsEnabled) TabCode.IsChecked = true; e.Handled = true; break; + } + } + + // Ctrl+Shift 단축키 + if (mod == (ModifierKeys.Control | ModifierKeys.Shift)) + { + switch (e.Key) + { + case Key.C: + // 마지막 AI 응답 복사 + ChatConversation? conv; + lock (_convLock) conv = _currentConversation; + if (conv != null) + { + var lastAi = conv.Messages.LastOrDefault(m => m.Role == "assistant"); + if (lastAi != null) + try { Clipboard.SetText(lastAi.Content); } catch { } + } + e.Handled = true; + break; + case Key.R: + // 마지막 응답 재생성 + _ = RegenerateLastAsync(); + e.Handled = true; + break; + case Key.D: + // 모든 대화 삭제 + BtnDeleteAll_Click(this, new RoutedEventArgs()); + e.Handled = true; + break; + case Key.P: + // 커맨드 팔레트 + OpenCommandPalette(); + e.Handled = true; + break; + } + } + + // Escape: 검색 바 닫기 또는 스트리밍 중지 + if (e.Key == Key.Escape) + { + if (SidebarSearchEditor?.Visibility == Visibility.Visible) { CloseSidebarSearch(clearText: true); e.Handled = true; } + else if (MessageSearchBar.Visibility == Visibility.Visible) { CloseMessageSearch(); e.Handled = true; } + else if (_isStreaming) { StopGeneration(); e.Handled = true; } + } + + // 슬래시 명령 팝업 키 처리 + if (TryHandleSlashNavigationKey(e)) + return; + + if (PermissionPopup.IsOpen && e.Key == Key.Escape) + { + PermissionPopup.IsOpen = false; + e.Handled = true; + } + } + + private bool TryHandleSlashNavigationKey(KeyEventArgs e) + { + if (!SlashPopup.IsOpen) + return false; + + switch (e.Key) + { + case Key.Escape: + SlashPopup.IsOpen = false; + _slashPalette.SelectedIndex = -1; + e.Handled = true; + return true; + case Key.Up: + SlashPopup_ScrollByDelta(120); + e.Handled = true; + return true; + case Key.Down: + SlashPopup_ScrollByDelta(-120); + e.Handled = true; + return true; + case Key.PageUp: + SlashPopup_ScrollByDelta(600); + e.Handled = true; + return true; + case Key.PageDown: + SlashPopup_ScrollByDelta(-600); + e.Handled = true; + return true; + case Key.Home: + { + var visible = GetVisibleSlashOrderedIndices(); + _slashPalette.SelectedIndex = visible.Count > 0 ? visible[0] : GetFirstVisibleSlashIndex(_slashPalette.Matches); + UpdateSlashSelectionVisualState(); + EnsureSlashSelectionVisible(); + e.Handled = true; + return true; + } + case Key.End: + { + var visible = GetVisibleSlashOrderedIndices(); + _slashPalette.SelectedIndex = visible.Count > 0 ? visible[^1] : GetFirstVisibleSlashIndex(_slashPalette.Matches); + UpdateSlashSelectionVisualState(); + EnsureSlashSelectionVisible(); + e.Handled = true; + return true; + } + case Key.Tab when _slashPalette.SelectedIndex >= 0: + case Key.Enter when _slashPalette.SelectedIndex >= 0: + ExecuteSlashSelectedItem(); + e.Handled = true; + return true; + default: + return false; + } + } + + private void BtnStop_Click(object sender, RoutedEventArgs e) => StopGeneration(); + + private void BtnPause_Click(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + var activeLoop = GetAgentLoop(_activeTab); + if (activeLoop.IsPaused) + { + activeLoop.Resume(); + PauseIcon.Text = "\uE769"; // 일시정지 아이콘 + BtnPause.ToolTip = "일시정지"; + } + else + { + _ = activeLoop.PauseAsync(); + PauseIcon.Text = "\uE768"; // 재생 아이콘 + BtnPause.ToolTip = "재개"; + } + } + private void BtnExport_Click(object sender, RoutedEventArgs e) => ExportConversation(); + + // ─── 메시지 내 검색 (Ctrl+F) ───────────────────────────────────────── + + private List _searchMatchIndices = new(); + private int _searchCurrentIndex = -1; + + private void ToggleMessageSearch() + { + if (MessageSearchBar.Visibility == Visibility.Visible) + CloseMessageSearch(); + else + { + MessageSearchBar.Visibility = Visibility.Visible; + SearchTextBox.Focus(); + SearchTextBox.SelectAll(); + } + } + + private void CloseMessageSearch() + { + MessageSearchBar.Visibility = Visibility.Collapsed; + SearchTextBox.Text = ""; + SearchResultCount.Text = ""; + _searchMatchIndices.Clear(); + _searchCurrentIndex = -1; + // 하이라이트 제거 + ClearSearchHighlights(); + } + + private void SearchTextBox_TextChanged(object sender, TextChangedEventArgs e) + { + var query = SearchTextBox.Text.Trim(); + if (string.IsNullOrEmpty(query)) + { + SearchResultCount.Text = ""; + _searchMatchIndices.Clear(); + _searchCurrentIndex = -1; + ClearSearchHighlights(); + return; + } + + // 현재 대화의 메시지에서 검색 + ChatConversation? conv; + lock (_convLock) conv = _currentConversation; + if (conv == null) return; + + _searchMatchIndices.Clear(); + for (int i = 0; i < conv.Messages.Count; i++) + { + if (conv.Messages[i].Content.Contains(query, StringComparison.OrdinalIgnoreCase)) + _searchMatchIndices.Add(i); + } + + if (_searchMatchIndices.Count > 0) + { + _searchCurrentIndex = 0; + SearchResultCount.Text = $"1/{_searchMatchIndices.Count}"; + HighlightSearchResult(); + } + else + { + _searchCurrentIndex = -1; + SearchResultCount.Text = "결과 없음"; + } + } + + private void SearchPrev_Click(object sender, RoutedEventArgs e) + { + if (_searchMatchIndices.Count == 0) return; + _searchCurrentIndex = (_searchCurrentIndex - 1 + _searchMatchIndices.Count) % _searchMatchIndices.Count; + SearchResultCount.Text = $"{_searchCurrentIndex + 1}/{_searchMatchIndices.Count}"; + HighlightSearchResult(); + } + + private void SearchNext_Click(object sender, RoutedEventArgs e) + { + if (_searchMatchIndices.Count == 0) return; + _searchCurrentIndex = (_searchCurrentIndex + 1) % _searchMatchIndices.Count; + SearchResultCount.Text = $"{_searchCurrentIndex + 1}/{_searchMatchIndices.Count}"; + HighlightSearchResult(); + } + + private void SearchClose_Click(object sender, RoutedEventArgs e) => CloseMessageSearch(); + + private void HighlightSearchResult() + { + if (_searchCurrentIndex < 0 || _searchCurrentIndex >= _searchMatchIndices.Count) return; + var msgIndex = _searchMatchIndices[_searchCurrentIndex]; + + // MessagePanel에서 해당 메시지 인덱스의 자식 요소를 찾아 스크롤 + // 메시지 패널의 자식 수가 대화 메시지 수와 정확히 일치하지 않을 수 있으므로 + // (배너, 계획카드 등 섞임) BringIntoView로 대략적 위치 이동 + if (msgIndex < GetTranscriptElementCount()) + { + var element = GetTranscriptElementAt(msgIndex) as FrameworkElement; + element?.BringIntoView(); + } + else if (GetTranscriptElementCount() > 0) + { + // 범위 밖이면 마지막 자식으로 이동 + (GetTranscriptElementAt(GetTranscriptElementCount() - 1) as FrameworkElement)?.BringIntoView(); + } + } + + private void ClearSearchHighlights() + { + // 현재는 BringIntoView 기반이므로 별도 하이라이트 제거 불필요 + } + + // ─── 메시지 우클릭 컨텍스트 메뉴 ─────────────────────────────────────── + + private void ShowMessageContextMenu(string content, string role) + { + var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; + var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; + var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; + var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent; + var dangerBrush = new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)); + var (popup, panel) = CreateThemedPopupMenu(); + + // 복사 + panel.Children.Add(CreatePopupMenuItem(popup, "\uE8C8", "텍스트 복사", secondaryText, primaryText, hoverBg, () => + { + try { Clipboard.SetText(content); ShowToast("복사되었습니다"); } catch { } + })); + + // 마크다운 복사 + panel.Children.Add(CreatePopupMenuItem(popup, "\uE943", "마크다운 복사", secondaryText, primaryText, hoverBg, () => + { + try { Clipboard.SetText(content); ShowToast("마크다운으로 복사됨"); } catch { } + })); + + // 인용하여 답장 + panel.Children.Add(CreatePopupMenuItem(popup, "\uE97A", "인용하여 답장", secondaryText, primaryText, hoverBg, () => + { + var quote = content.Length > 200 ? content[..200] + "..." : content; + var lines = quote.Split('\n'); + var quoted = string.Join("\n", lines.Select(l => $"> {l}")); + InputBox.Text = quoted + "\n\n"; + InputBox.Focus(); + InputBox.CaretIndex = InputBox.Text.Length; + })); + + AddPopupMenuSeparator(panel, borderBrush); + + // 재생성 (AI 응답만) + if (role == "assistant") + { + panel.Children.Add(CreatePopupMenuItem(popup, "\uE72C", "응답 재생성", secondaryText, primaryText, hoverBg, () => _ = RegenerateLastAsync())); + } + + // 대화 분기 (Fork) + panel.Children.Add(CreatePopupMenuItem(popup, "\uE8A5", "여기서 분기", secondaryText, primaryText, hoverBg, () => + { + ChatConversation? conv; + lock (_convLock) conv = _currentConversation; + if (conv == null) return; + + var idx = conv.Messages.FindLastIndex(m => m.Role == role && m.Content == content); + if (idx < 0) return; + + ForkConversation(conv, idx); + })); + + AddPopupMenuSeparator(panel, borderBrush); + + // 이후 메시지 모두 삭제 + var msgContent = content; + var msgRole = role; + panel.Children.Add(CreatePopupMenuItem(popup, "\uE74D", "이후 메시지 모두 삭제", dangerBrush, dangerBrush, hoverBg, () => + { + ChatConversation? conv; + lock (_convLock) conv = _currentConversation; + if (conv == null) return; + + var idx = conv.Messages.FindLastIndex(m => m.Role == msgRole && m.Content == msgContent); + if (idx < 0) return; + + var removeCount = conv.Messages.Count - idx; + if (CustomMessageBox.Show($"이 메시지 포함 {removeCount}개 메시지를 삭제하시겠습니까?", + "메시지 삭제", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes) + return; + + conv.Messages.RemoveRange(idx, removeCount); + try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"대화 저장 실패: {ex.Message}"); } + RenderMessages(); + ShowToast($"{removeCount}개 메시지 삭제됨"); + })); + + Dispatcher.BeginInvoke(() => { popup.IsOpen = true; }, DispatcherPriority.Input); + } + + // ─── 팁 알림 ────────────────────────────────────────────────────── + + private static readonly string[] Tips = + [ + "💡 작업 폴더에 AGENTS.md 파일을 만들면 매번 시스템 프롬프트에 자동 주입됩니다. 프로젝트 설계 원칙이나 코딩 규칙을 기록하세요.", + "💡 Ctrl+1/2/3으로 Chat/Cowork/Code 탭을 빠르게 전환할 수 있습니다.", + "💡 Ctrl+F로 현재 대화 내 메시지를 검색할 수 있습니다.", + "💡 메시지를 우클릭하면 복사, 인용 답장, 재생성, 삭제를 할 수 있습니다.", + "💡 코드 블록을 더블클릭하면 전체화면으로 볼 수 있고, 💾 버튼으로 파일 저장이 가능합니다.", + "💡 Cowork 에이전트가 만든 파일은 자동으로 날짜_시간 접미사가 붙어 덮어쓰기를 방지합니다.", + "💡 Code 탭에서 개발 언어를 선택하면 해당 언어 우선으로 코드를 생성합니다.", + "💡 파일 탐색기(하단 바 '파일' 버튼)에서 더블클릭으로 프리뷰, 우클릭으로 관리할 수 있습니다.", + "💡 에이전트가 계획을 제시하면 '수정 요청'으로 방향을 바꾸거나 '취소'로 중단할 수 있습니다.", + "💡 Code 탭은 빌드/테스트를 자동으로 실행합니다. 프로젝트 폴더를 먼저 선택하세요.", + "💡 무드 갤러리에서 10가지 디자인 템플릿 중 원하는 스타일을 미리보기로 선택할 수 있습니다.", + "💡 Git 연동: Code 탭에서 에이전트가 git status, diff, commit을 수행합니다. (push는 직접)", + "💡 설정 → AX Agent → 공통에서 개발자 모드를 켜면 에이전트 동작을 스텝별로 검증할 수 있습니다.", + "💡 트레이 아이콘 우클릭 → '사용 통계'에서 대화 빈도와 토큰 사용량을 확인할 수 있습니다.", + "💡 대화 제목을 클릭하면 이름을 변경할 수 있습니다.", + "💡 LLM 오류 발생 시 '재시도' 버튼이 자동으로 나타납니다.", + "💡 검색란에서 대화 제목뿐 아니라 첫 메시지 내용까지 검색됩니다.", + "💡 프리셋 선택 후에도 대화가 리셋되지 않습니다. 진행 중인 대화에서 프리셋을 변경할 수 있습니다.", + "💡 Shift+Enter로 퍼지 검색 결과의 파일이 있는 폴더를 열 수 있습니다.", + "💡 최근 폴더를 우클릭하면 '폴더 열기', '경로 복사', '목록에서 삭제'가 가능합니다.", + "💡 Cowork/Code 에이전트 작업 완료 시 시스템 트레이에 알림이 표시됩니다.", + "💡 마크다운 테이블, 인용(>), 취소선(~~), 링크([text](url))가 모두 렌더링됩니다.", + "💡 ⚠ 데이터 폴더를 워크스페이스로 지정할 때는 반드시 백업을 먼저 만드세요!", + "💡 드라이브 루트(C:\\, D:\\)는 작업공간으로 설정할 수 없습니다. 하위 폴더를 선택하세요.", + ]; + private int _tipIndex; + private DispatcherTimer? _tipDismissTimer; + + private void ShowRandomTip() + { + if (!_settings.Settings.Llm.ShowTips) return; + if (_activeTab != "Cowork" && _activeTab != "Code") return; + + var tip = Tips[_tipIndex % Tips.Length]; + _tipIndex++; + + // 토스트 스타일로 표시 (기존 토스트와 다른 위치/색상) + ShowTip(tip); + } + + private void ShowTip(string message) + { + // 두 타이머 모두 중지 + _tipDismissTimer?.Stop(); + _toastHideTimer?.Stop(); + _toastHideTimer = null; + + ToastText.Text = message; + ToastIcon.Text = "\uE82F"; // 전구 아이콘 + ToastBorder.Visibility = Visibility.Visible; + ToastBorder.BeginAnimation(UIElement.OpacityProperty, + new System.Windows.Media.Animation.DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(300))); + + var duration = _settings.Settings.Llm.TipDurationSeconds; + if (duration <= 0) + { + // duration=0: 자동 숨기기 없음 — 기본 3초로 폴백하여 영구 잔류 방지 + duration = 3; + } + + var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(duration) }; + _tipDismissTimer = timer; + timer.Tick += (_, _) => + { + if (_tipDismissTimer != timer) return; + timer.Stop(); + _tipDismissTimer = null; + var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(300)); + fadeOut.Completed += (_, _) => ToastBorder.Visibility = Visibility.Collapsed; + ToastBorder.BeginAnimation(UIElement.OpacityProperty, fadeOut); + }; + timer.Start(); + } +} diff --git a/src/AxCopilot/Views/ChatWindow.ConversationFilterPresentation.cs b/src/AxCopilot/Views/ChatWindow.ConversationFilterPresentation.cs index 10e25b4..7124374 100644 --- a/src/AxCopilot/Views/ChatWindow.ConversationFilterPresentation.cs +++ b/src/AxCopilot/Views/ChatWindow.ConversationFilterPresentation.cs @@ -91,14 +91,39 @@ public partial class ChatWindow : "에이전트 활동량과 실패를 우선으로 보는 중"; } + private void BtnArchiveFilter_Click(object sender, RoutedEventArgs e) + { + // null(일반만) → true(아카이브만) → null(일반만) 순환 + _archiveFilter = _archiveFilter == null ? true : null; + UpdateArchiveFilterUi(); + RefreshConversationList(); + } + + private void UpdateArchiveFilterUi() + { + if (BtnArchiveFilter == null || ArchiveFilterLabel == null) return; + + var isActive = _archiveFilter == true; + BtnArchiveFilter.Background = isActive ? BrushFromHex("#FEF3C7") : Brushes.Transparent; + BtnArchiveFilter.BorderBrush = isActive ? BrushFromHex("#FCD34D") : Brushes.Transparent; + BtnArchiveFilter.BorderThickness = isActive ? new Thickness(1) : new Thickness(0); + ArchiveFilterLabel.Text = isActive ? "아카이브" : "보관"; + ArchiveFilterLabel.Foreground = isActive + ? BrushFromHex("#92400E") + : (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray); + BtnArchiveFilter.ToolTip = isActive ? "아카이브된 대화 보는 중 (클릭: 일반 대화로 전환)" : "아카이브된 대화 보기"; + } + private void ApplyConversationListPreferences(ChatConversation? conv) { _failedOnlyFilter = false; _runningOnlyFilter = false; + _archiveFilter = null; _sortConversationsByRecent = string.Equals(conv?.ConversationSortMode, "recent", StringComparison.OrdinalIgnoreCase); UpdateConversationFailureFilterUi(); UpdateConversationRunningFilterUi(); UpdateConversationSortUi(); + UpdateArchiveFilterUi(); } private void PersistConversationListPreferences() diff --git a/src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs b/src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs index 0427cf9..e913e1a 100644 --- a/src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs +++ b/src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs @@ -8,6 +8,7 @@ using System.Windows.Media; using System.Windows.Threading; using AxCopilot.Models; using AxCopilot.Services; +using AxCopilot.ViewModels; namespace AxCopilot.Views; @@ -43,6 +44,115 @@ public partial class ChatWindow ConversationPanel.MouseLeave += ConversationPanel_DelegatedMouseLeave; ConversationPanel.PreviewMouseLeftButtonDown += ConversationPanel_DelegatedLeftButtonDown; ConversationPanel.PreviewMouseRightButtonUp += ConversationPanel_DelegatedRightButtonUp; + + // 새 ItemsControl 이벤트 등록 + if (ConversationItemsControl != null) + { + ConversationItemsControl.PreviewMouseLeftButtonDown += ConversationItemsControl_LeftButtonDown; + ConversationItemsControl.PreviewMouseRightButtonUp += ConversationItemsControl_RightButtonUp; + ConversationItemsControl.AddHandler(System.Windows.Controls.Primitives.ButtonBase.ClickEvent, + new RoutedEventHandler(ConversationItemsControl_ButtonClick)); + } + } + + private void ConversationItemsControl_ButtonClick(object sender, RoutedEventArgs e) + { + if (e.OriginalSource is not Button btn) return; + var vm = FindDataContext(btn); + if (vm != null) + ShowConversationMenu(vm.Id); + } + + private void ConversationItemsControl_LeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (FindAncestor + + + + + + + + + +