AX Agent 코워크·코드 흐름과 컨텍스트 관리를 claude-code 기준으로 대폭 정리
- 코워크·코드 프롬프트, 도구 선택, 문서 생성/검증 흐름을 claude-code 동등 품질 기준으로 재정렬함 - OpenAI/vLLM 경로의 오래된 tool history를 평탄화하고 최근 이력만 구조화해 컨텍스트 직렬화를 경량화함 - AX Agent UI를 테마 기준으로 재구성하고 플랜 승인/오버레이/이벤트 렌더링/명령 입력 상호작용을 개선함 - 파일 후보 제안, 반복 경로 정체 복구, LSP 보강, 문서·PPT 처리 개선, 설정/서비스 인터페이스 정리를 함께 반영함 - README.md 및 docs/DEVELOPMENT.md를 작업 시점별로 갱신함 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0, 오류 0)
@@ -1655,3 +1655,7 @@ MIT License
|
|||||||
- `claw-code`의 post-compact attachments 흐름을 참고해, AX도 요약/경계 메시지에 오래된 첨부 참조를 다시 실어 compact 뒤 컨텍스트 연속성을 보강했습니다.
|
- `claw-code`의 post-compact attachments 흐름을 참고해, AX도 요약/경계 메시지에 오래된 첨부 참조를 다시 실어 compact 뒤 컨텍스트 연속성을 보강했습니다.
|
||||||
- [ContextCondenser.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ContextCondenser.cs)는 오래된 메시지에서 첨부 파일 이름과 이미지 개수를 수집해 `microcompact_boundary`와 요약 메시지에 함께 기록합니다.
|
- [ContextCondenser.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ContextCondenser.cs)는 오래된 메시지에서 첨부 파일 이름과 이미지 개수를 수집해 `microcompact_boundary`와 요약 메시지에 함께 기록합니다.
|
||||||
- 요약 메시지에는 `AttachedFiles`도 같이 보존해, compact 이후에도 관련 파일 참조가 query view로 이어질 수 있게 맞췄습니다.
|
- 요약 메시지에는 `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 위험을 함께 줄였습니다.
|
||||||
|
|||||||
53
build-quick.sh
Normal file
@@ -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 ""
|
||||||
@@ -668,3 +668,14 @@ owKindCounts를 함께 남겨 %APPDATA%\\AxCopilot\\perf 기준으로 transcript
|
|||||||
- compact 이후에도 “이 대화가 어떤 파일/이미지를 참고했는지”가 요약 메시지에서 다시 드러납니다.
|
- compact 이후에도 “이 대화가 어떤 파일/이미지를 참고했는지”가 요약 메시지에서 다시 드러납니다.
|
||||||
- query view가 compact 이후 메시지를 다시 보낼 때, 파일 참조 continuity가 이전보다 더 자연스럽게 유지됩니다.
|
- 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 검사에 걸릴 가능성을 낮추면서도 최근 실행 흐름은 그대로 유지할 수 있습니다.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using AxCopilot.Core;
|
using AxCopilot.Core;
|
||||||
using AxCopilot.Handlers;
|
using AxCopilot.Handlers;
|
||||||
using AxCopilot.Services;
|
using AxCopilot.Services;
|
||||||
@@ -119,6 +120,9 @@ public partial class App : System.Windows.Application
|
|||||||
_appState.AttachChatSession(_chatSessionState);
|
_appState.AttachChatSession(_chatSessionState);
|
||||||
_appState.LoadFromSettings(settings);
|
_appState.LoadFromSettings(settings);
|
||||||
|
|
||||||
|
// ─── DI 컨테이너 초기화 (기존 인스턴스 등록, 점진적 전환) ──────────────
|
||||||
|
ConfigureDependencyInjection(settings, _memoryService, _chatSessionState, _appState);
|
||||||
|
|
||||||
_indexService = new IndexService(settings);
|
_indexService = new IndexService(settings);
|
||||||
var indexService = _indexService;
|
var indexService = _indexService;
|
||||||
// 캐시 로드를 백그라운드에서 실행 — UI 스레드 블록 방지
|
// 캐시 로드를 백그라운드에서 실행 — UI 스레드 블록 방지
|
||||||
@@ -1008,4 +1012,32 @@ public partial class App : System.Windows.Application
|
|||||||
|
|
||||||
_fileDialogWatcher.Stop();
|
_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<ISettingsService>(settings);
|
||||||
|
services.AddSingleton(settings); // 하위 호환: 구체 타입 직접 주입도 허용
|
||||||
|
services.AddSingleton<IChatStorageService, ChatStorageService>();
|
||||||
|
services.AddSingleton<IAppStateService>(appState);
|
||||||
|
services.AddSingleton(appState);
|
||||||
|
services.AddSingleton(chatSessionState);
|
||||||
|
services.AddSingleton(memoryService);
|
||||||
|
|
||||||
|
// ── Transient: 요청마다 새 인스턴스 ──────────────────────────────
|
||||||
|
services.AddTransient<ILlmService>(sp => new LlmService(sp.GetRequiredService<SettingsService>()));
|
||||||
|
services.AddSingleton<IModelRouterService>(sp => new ModelRouterService(sp.GetRequiredService<SettingsService>()));
|
||||||
|
});
|
||||||
|
|
||||||
|
LogService.Info("DI 컨테이너 초기화 완료");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"symbol": "\uE943",
|
"symbol": "\uE943",
|
||||||
"color": "#3B82F6",
|
"color": "#3B82F6",
|
||||||
"description": "새 기능 개발, 코드 작성, 프로젝트 구성",
|
"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": "어떤 기능을 개발할까요? (프로젝트 폴더를 먼저 선택하세요)"
|
"placeholder": "어떤 기능을 개발할까요? (프로젝트 폴더를 먼저 선택하세요)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"symbol": "\uE71B",
|
"symbol": "\uE71B",
|
||||||
"color": "#10B981",
|
"color": "#10B981",
|
||||||
"description": "코드 품질 분석, 모범 사례 검토, 개선 제안",
|
"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": "어떤 코드를 리뷰할까요?"
|
"placeholder": "어떤 코드를 리뷰할까요?"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"symbol": "\uE777",
|
"symbol": "\uE777",
|
||||||
"color": "#6366F1",
|
"color": "#6366F1",
|
||||||
"description": "코드 구조 개선, 중복 제거, 성능 최적화",
|
"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": "어떤 코드를 리팩터링할까요?"
|
"placeholder": "어떤 코드를 리팩터링할까요?"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"symbol": "\uE8A5",
|
"symbol": "\uE8A5",
|
||||||
"color": "#F59E0B",
|
"color": "#F59E0B",
|
||||||
"description": "Word, Markdown, HTML 문서를 작성합니다",
|
"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- 콜아웃: <div class=\"callout callout-info\">핵심 내용</div> (info/warning/tip/danger/note).\n- 배지: <span class=\"badge badge-blue\">완료</span>.\n- 타임라인: <div class=\"timeline\"><div class=\"timeline-item\">...</div></div>.\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- 콜아웃: <div class=\"callout callout-info\">핵심 내용</div> (info/warning/tip/danger/note).\n- 배지: <span class=\"badge badge-blue\">완료</span>.\n- 타임라인: <div class=\"timeline\"><div class=\"timeline-item\">...</div></div>.\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": "어떤 문서를 작성할까요? (예: 프로젝트 기획서 작성)"
|
"placeholder": "어떤 문서를 작성할까요? (예: 프로젝트 기획서 작성)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"symbol": "\uE9F9",
|
"symbol": "\uE9F9",
|
||||||
"color": "#3B82F6",
|
"color": "#3B82F6",
|
||||||
"description": "Excel, Word, HTML 보고서를 상세하게 작성합니다",
|
"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- 콜아웃을 활용하세요: <div class=\"callout callout-info\">중요 정보</div> (info/warning/tip/danger/note).\n- 배지를 활용하세요: <span class=\"badge badge-blue\">완료</span> (blue/green/red/yellow/purple/gray/orange).\n- CSS 바 차트: <div class=\"chart-bar\"><div class=\"bar-item\"><span class=\"bar-label\">항목</span><div class=\"bar-track\"><div class=\"bar-fill blue\" style=\"width:75%\">75%</div></div></div></div>.\n- 그리드 레이아웃: <div class=\"grid-2\"> 또는 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- 콜아웃을 활용하세요: <div class=\"callout callout-info\">중요 정보</div> (info/warning/tip/danger/note).\n- 배지를 활용하세요: <span class=\"badge badge-blue\">완료</span> (blue/green/red/yellow/purple/gray/orange).\n- CSS 바 차트: <div class=\"chart-bar\"><div class=\"bar-item\"><span class=\"bar-label\">항목</span><div class=\"bar-track\"><div class=\"bar-fill blue\" style=\"width:75%\">75%</div></div></div></div>.\n- 그리드 레이아웃: <div class=\"grid-2\"> 또는 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": "어떤 보고서를 작성할까요? (예: 삼성디스플레이 연혁 보고서)"
|
"placeholder": "어떤 보고서를 작성할까요? (예: 삼성디스플레이 연혁 보고서)"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/AxCopilot/Assets/foldy_qdy.png
Normal file
|
After Width: | Height: | Size: 662 KiB |
BIN
src/AxCopilot/Assets/gif/20260410_132427092.gif
Normal file
|
After Width: | Height: | Size: 4.2 MiB |
BIN
src/AxCopilot/Assets/gif/20260410_132431645.gif
Normal file
|
After Width: | Height: | Size: 4.6 MiB |
BIN
src/AxCopilot/Assets/gif/20260410_132436408.gif
Normal file
|
After Width: | Height: | Size: 6.7 MiB |
BIN
src/AxCopilot/Assets/gif/20260410_132441642.gif
Normal file
|
After Width: | Height: | Size: 5.3 MiB |
BIN
src/AxCopilot/Assets/gif/20260410_132445944.gif
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
BIN
src/AxCopilot/Assets/gif/20260410_132450677.gif
Normal file
|
After Width: | Height: | Size: 5.3 MiB |
BIN
src/AxCopilot/Assets/gif/20260410_132454622.gif
Normal file
|
After Width: | Height: | Size: 4.8 MiB |
BIN
src/AxCopilot/Assets/gif/20260410_132459185.gif
Normal file
|
After Width: | Height: | Size: 5.7 MiB |
BIN
src/AxCopilot/Assets/gif/20260410_132503634.gif
Normal file
|
After Width: | Height: | Size: 5.1 MiB |
BIN
src/AxCopilot/Assets/pixel_art.png
Normal file
|
After Width: | Height: | Size: 4.6 MiB |
197
src/AxCopilot/Assets/pixel_art.txt
Normal file
@@ -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} 크기")
|
||||||
BIN
src/AxCopilot/Assets/ppt/BASIC100 기준 템플릿 V1.pptx
Normal file
BIN
src/AxCopilot/Assets/ppt/CORE100 기준템플릿 V1.pptx
Normal file
BIN
src/AxCopilot/Assets/ppt/P01_01_프레임디자인_PPT템플릿_블루(PPT_편집용).pptx
Normal file
BIN
src/AxCopilot/Assets/ppt/미스터 피피티 03_원본.pptx
Normal file
BIN
src/AxCopilot/Assets/ppt/미스터 피피티 템플릿 01_원본.pptx
Normal file
BIN
src/AxCopilot/Assets/ppt/미스터 피피티 템플릿 02_원본.pptx
Normal file
BIN
src/AxCopilot/Assets/ppt/미스터 피피티 템플릿 04_원본.pptx
Normal file
BIN
src/AxCopilot/Assets/ppt/미스터 피피티 템플릿_05_원본.pptx
Normal file
@@ -68,6 +68,7 @@
|
|||||||
<!-- 클립보드 히스토리 DPAPI 암호화 (Windows 전용, 외부 네트워크 통신 없음) -->
|
<!-- 클립보드 히스토리 DPAPI 암호화 (Windows 전용, 외부 네트워크 통신 없음) -->
|
||||||
<!-- Windows 서비스 관리 (ServiceHandler — 로컬 전용, 외부 네트워크 통신 없음) -->
|
<!-- Windows 서비스 관리 (ServiceHandler — 로컬 전용, 외부 네트워크 통신 없음) -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||||
<PackageReference Include="DocumentFormat.OpenXml" Version="3.2.0" />
|
<PackageReference Include="DocumentFormat.OpenXml" Version="3.2.0" />
|
||||||
<PackageReference Include="Markdig" Version="0.37.0" />
|
<PackageReference Include="Markdig" Version="0.37.0" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
|
||||||
@@ -114,6 +115,11 @@
|
|||||||
<Resource Include="Assets\mascot.png" Condition="Exists('Assets\mascot.png')" />
|
<Resource Include="Assets\mascot.png" Condition="Exists('Assets\mascot.png')" />
|
||||||
<Resource Include="Assets\mascot.jpg" Condition="Exists('Assets\mascot.jpg')" />
|
<Resource Include="Assets\mascot.jpg" Condition="Exists('Assets\mascot.jpg')" />
|
||||||
<Resource Include="Assets\mascot.webp" Condition="Exists('Assets\mascot.webp')" />
|
<Resource Include="Assets\mascot.webp" Condition="Exists('Assets\mascot.webp')" />
|
||||||
|
<Resource Include="Assets\foldy_qdy.png" Condition="Exists('Assets\foldy_qdy.png')" />
|
||||||
|
<Resource Include="Assets\pixel_art.png" Condition="Exists('Assets\pixel_art.png')" />
|
||||||
|
<!-- PPT 고품질 템플릿: 빌드에 포함하지 않음 (297MB).
|
||||||
|
런타임에 Assets/ppt/ 또는 %APPDATA%/AXCopilot/templates/ppt/ 에서 검색.
|
||||||
|
개발 환경에서는 소스의 Assets/ppt/를 자동으로 찾음. -->
|
||||||
<!-- 검색 엔진 아이콘: 빌드 출력에 복사 (사내망에서 외부 favicon 다운로드 불가 대응) -->
|
<!-- 검색 엔진 아이콘: 빌드 출력에 복사 (사내망에서 외부 favicon 다운로드 불가 대응) -->
|
||||||
<Content Include="Assets\SearchEngines\*.png">
|
<Content Include="Assets\SearchEngines\*.png">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
@@ -125,6 +131,14 @@
|
|||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- 마스코트 GIF 애니메이션: 빌드 출력 Assets/gif/ 폴더에 복사 -->
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Assets\gif\*.gif">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
<Link>Assets\gif\%(Filename)%(Extension)</Link>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- 내장 스킬 파일: 빌드 출력 skills/ 폴더에 복사 -->
|
<!-- 내장 스킬 파일: 빌드 출력 skills/ 폴더에 복사 -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="skills\*.skill.md">
|
<Content Include="skills\*.skill.md">
|
||||||
|
|||||||
422
src/AxCopilot/AxCopilot_zj0aajh3_wpftmp.csproj
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<AssemblyName>AxCopilot</AssemblyName>
|
||||||
|
<IntermediateOutputPath>obj\Release\</IntermediateOutputPath>
|
||||||
|
<BaseIntermediateOutputPath>obj\</BaseIntermediateOutputPath>
|
||||||
|
<MSBuildProjectExtensionsPath>E:\AX Copilot - Claude\src\AxCopilot\obj\</MSBuildProjectExtensionsPath>
|
||||||
|
<_TargetAssemblyProjectName>AxCopilot</_TargetAssemblyProjectName>
|
||||||
|
<RootNamespace>AxCopilot</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows10.0.17763.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
<RootNamespace>AxCopilot</RootNamespace>
|
||||||
|
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
|
||||||
|
<!--
|
||||||
|
★ 버전 관리 규칙 ★
|
||||||
|
이 <Version> 값 하나만 변경하면 앱 전체에 자동 반영됩니다:
|
||||||
|
- 설정 창 하단 버전 표시 (SettingsWindow.xaml.cs → SetVersionText())
|
||||||
|
- Windows 속성 탭 파일 버전 / 제품 버전
|
||||||
|
- DEVELOPMENT.md 변경 이력은 수동으로 함께 업데이트하세요.
|
||||||
|
설정 스키마 버전(마이그레이션)은 Services/SettingsService.cs → CurrentSettingsVersion 을 별도로 관리합니다.
|
||||||
|
-->
|
||||||
|
<Version>0.7.3</Version>
|
||||||
|
<Company>AX</Company>
|
||||||
|
<Product>AX Copilot</Product>
|
||||||
|
<Description>AI 기반 업무 자동화 런처 & 코파일럿</Description>
|
||||||
|
<!-- ScreenCaptureHandler의 LockBits 포인터 연산에 필요 -->
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<!-- 배포: build.bat에서 self-contained / framework-dependent 두 종류로 publish -->
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<!-- ─── 디컴파일 방지 ─────────────────────────────────────────────── -->
|
||||||
|
<!-- 디버그 심볼 제거 (Release) -->
|
||||||
|
<DebugType Condition="'$(Configuration)'=='Release'">none</DebugType>
|
||||||
|
<DebugSymbols Condition="'$(Configuration)'=='Release'">false</DebugSymbols>
|
||||||
|
<!-- 임베디드 소스 제거 -->
|
||||||
|
<EmbedAllSources>false</EmbedAllSources>
|
||||||
|
<!-- Suppress source link -->
|
||||||
|
<EnableSourceLink>false</EnableSourceLink>
|
||||||
|
<DeterministicSourcePaths>false</DeterministicSourcePaths>
|
||||||
|
</PropertyGroup>
|
||||||
|
<!-- Release 빌드 시 추가 난독화 설정 -->
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<!-- 사용하지 않는 멤버 제거 (IL trimming) -->
|
||||||
|
<PublishTrimmed>false</PublishTrimmed>
|
||||||
|
<!-- PDB 제거 -->
|
||||||
|
<CopyOutputSymbolsToPublishDirectory>false</CopyOutputSymbolsToPublishDirectory>
|
||||||
|
<!-- 배포판 보호 수준 강화 -->
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||||
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
|
<PublishReadyToRun>true</PublishReadyToRun>
|
||||||
|
</PropertyGroup>
|
||||||
|
<!-- UseWindowsForms의 암묵적 using(System.Windows.Forms)이 WPF의
|
||||||
|
System.Windows.Input과 충돌(KeyEventArgs 모호성)하므로 전역 제거.
|
||||||
|
System.Drawing도 전역 제거: System.Windows.Media의 Color/ColorConverter/FontFamily와
|
||||||
|
모호한 참조 충돌 방지. Drawing 타입이 필요한 파일에서만 명시적 using으로 추가. -->
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Remove="System.Windows.Forms" />
|
||||||
|
<Using Remove="System.Drawing" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\AxCopilot.SDK\AxCopilot.SDK.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<!-- 클립보드 히스토리 DPAPI 암호화 (Windows 전용, 외부 네트워크 통신 없음) -->
|
||||||
|
<!-- Windows 서비스 관리 (ServiceHandler — 로컬 전용, 외부 네트워크 통신 없음) -->
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||||
|
<PackageReference Include="DocumentFormat.OpenXml" Version="3.2.0" />
|
||||||
|
<PackageReference Include="Markdig" Version="0.37.0" />
|
||||||
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
|
||||||
|
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||||
|
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
||||||
|
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" />
|
||||||
|
<PackageReference Include="UglyToad.PdfPig" Version="1.7.0-custom-5" />
|
||||||
|
</ItemGroup>
|
||||||
|
<!-- Assets -->
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- 가이드 원본 HTML: 편집용. 출력에 복사하지 않음 (암호화 버전 사용) -->
|
||||||
|
<None Include="Assets\AX Copilot 사용가이드.htm" />
|
||||||
|
<None Include="Assets\AX Copilot 개발자가이드.htm" />
|
||||||
|
<!-- 암호화된 가이드: 빌드 출력에 복사 (앱 내장 뷰어로 복호화) -->
|
||||||
|
<Content Include="Assets\guide_user.enc">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="Assets\guide_dev.enc">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<!-- 아이콘: 앱 내장 리소스 + 출력에도 복사 (트레이용) -->
|
||||||
|
<None Update="Assets\icon.ico">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<!-- about.json: 빌드 시 exe에 내장되는 리소스. 런타임 파일 수정 불가. -->
|
||||||
|
<!-- 격려/명언 문구: 빌드 시 내장. 인터넷 연결 불필요. -->
|
||||||
|
<!-- 대화 주제 프리셋: 9종 시스템 프롬프트 (빌드 시 내장) -->
|
||||||
|
<EmbeddedResource Include="Assets\Presets\*.json" />
|
||||||
|
<!-- 마스코트 이미지: 내장 리소스 (EXE에 포함됨, 교체 시 재빌드 필요) -->
|
||||||
|
<!-- PPT 고품질 템플릿: 빌드에 포함하지 않음 (297MB).
|
||||||
|
런타임에 Assets/ppt/ 또는 %APPDATA%/AXCopilot/templates/ppt/ 에서 검색.
|
||||||
|
개발 환경에서는 소스의 Assets/ppt/를 자동으로 찾음. -->
|
||||||
|
<!-- 검색 엔진 아이콘: 빌드 출력에 복사 (사내망에서 외부 favicon 다운로드 불가 대응) -->
|
||||||
|
<Content Include="Assets\SearchEngines\*.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<!-- 시스템 프롬프트: 빌드 출력에 복사 (동적 로딩) -->
|
||||||
|
<Content Include="Assets\system_prompt.txt">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
<Link>system_prompt.txt</Link>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
<!-- 마스코트 GIF 애니메이션: 빌드 출력 Assets/gif/ 폴더에 복사 -->
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Assets\gif\*.gif">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
<Link>Assets\gif\%(Filename)%(Extension)</Link>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
<!-- 내장 스킬 파일: 빌드 출력 skills/ 폴더에 복사 -->
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="skills\*.skill.md">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
<Link>skills\%(Filename)%(Extension)</Link>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
<!-- 테마 리소스 딕셔너리 -->
|
||||||
|
<ItemGroup>
|
||||||
|
</ItemGroup>
|
||||||
|
<!-- 단위 테스트 프로젝트에서 internal 멤버 접근 허용 -->
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||||
|
<_Parameter1>AxCopilot.Tests</_Parameter1>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\Accessibility.dll" />
|
||||||
|
<ReferencePath Include="E:\AX Copilot - Claude\src\AxCopilot.SDK\bin\Release\net8.0-windows\AxCopilot.SDK.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\documentformat.openxml\3.2.0\lib\net8.0\DocumentFormat.OpenXml.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\documentformat.openxml.framework\3.2.0\lib\net8.0\DocumentFormat.OpenXml.Framework.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\markdig\0.37.0\lib\net8.0\Markdig.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\Microsoft.CSharp.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.data.sqlite.core\8.0.0\lib\net8.0\Microsoft.Data.Sqlite.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.extensions.dependencyinjection.abstractions\8.0.2\lib\net8.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.extensions.dependencyinjection\8.0.1\lib\net8.0\Microsoft.Extensions.DependencyInjection.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\Microsoft.VisualBasic.Core.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\Microsoft.VisualBasic.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\Microsoft.VisualBasic.Forms.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.web.webview2\1.0.2903.40\lib_manual\netcoreapp3.0\Microsoft.Web.WebView2.Core.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.web.webview2\1.0.2903.40\lib_manual\netcoreapp3.0\Microsoft.Web.WebView2.WinForms.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.web.webview2\1.0.2903.40\lib_manual\net5.0-windows10.0.17763.0\Microsoft.Web.WebView2.Wpf.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\Microsoft.Win32.Primitives.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\Microsoft.Win32.Registry.AccessControl.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\Microsoft.Win32.Registry.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\Microsoft.Win32.SystemEvents.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.windows.sdk.net.ref\10.0.17763.57\lib\net8.0\Microsoft.Windows.SDK.NET.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\mscorlib.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\netstandard.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationCore.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.Aero.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.Aero2.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.AeroLite.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.Classic.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.Luna.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.Royale.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationUI.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\qrcoder\1.6.0\lib\net6.0-windows7.0\QRCoder.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\ReachFramework.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\sqlitepclraw.bundle_e_sqlite3\2.1.6\lib\netstandard2.0\SQLitePCLRaw.batteries_v2.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\sqlitepclraw.core\2.1.6\lib\netstandard2.0\SQLitePCLRaw.core.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\sqlitepclraw.provider.e_sqlite3\2.1.6\lib\net6.0-windows7.0\SQLitePCLRaw.provider.e_sqlite3.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.AppContext.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Buffers.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.CodeDom.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Collections.Concurrent.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Collections.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Collections.Immutable.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Collections.NonGeneric.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Collections.Specialized.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.Annotations.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.DataAnnotations.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.EventBasedAsync.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.Primitives.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.TypeConverter.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Configuration.ConfigurationManager.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Configuration.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Console.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Core.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Data.Common.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Data.DataSetExtensions.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Data.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Design.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.Contracts.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.Debug.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.DiagnosticSource.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.EventLog.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.FileVersionInfo.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.PerformanceCounter.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.Process.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.StackTrace.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.TextWriterTraceListener.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.Tools.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.TraceSource.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.Tracing.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.DirectoryServices.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Drawing.Common.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Drawing.Design.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Drawing.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Drawing.Primitives.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Dynamic.Runtime.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Formats.Asn1.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Formats.Tar.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Globalization.Calendars.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Globalization.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Globalization.Extensions.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Compression.Brotli.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Compression.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Compression.FileSystem.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Compression.ZipFile.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.FileSystem.AccessControl.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.FileSystem.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.FileSystem.DriveInfo.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.FileSystem.Primitives.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.FileSystem.Watcher.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.IsolatedStorage.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.MemoryMappedFiles.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.IO.Packaging.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Pipes.AccessControl.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Pipes.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.UnmanagedMemoryStream.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Linq.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Linq.Expressions.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Linq.Parallel.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Linq.Queryable.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Memory.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Http.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Http.Json.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.HttpListener.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Mail.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.NameResolution.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.NetworkInformation.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Ping.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Primitives.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Quic.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Requests.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Security.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.ServicePoint.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Sockets.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.WebClient.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.WebHeaderCollection.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.WebProxy.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.WebSockets.Client.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.WebSockets.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Numerics.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Numerics.Vectors.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ObjectModel.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Printing.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.DispatchProxy.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Emit.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Emit.ILGeneration.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Emit.Lightweight.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Extensions.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Metadata.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Primitives.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.TypeExtensions.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Resources.Extensions.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Resources.Reader.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Resources.ResourceManager.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Resources.Writer.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.CompilerServices.Unsafe.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.CompilerServices.VisualC.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Extensions.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Handles.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.InteropServices.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.InteropServices.JavaScript.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.InteropServices.RuntimeInformation.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Intrinsics.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Loader.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Numerics.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Serialization.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Serialization.Formatters.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Serialization.Json.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Serialization.Primitives.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Serialization.Xml.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.AccessControl.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Claims.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Algorithms.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Cng.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Csp.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Encoding.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.OpenSsl.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Pkcs.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Primitives.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.ProtectedData.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.X509Certificates.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Xml.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Security.Permissions.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Principal.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Principal.Windows.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.SecureString.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ServiceModel.Web.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ServiceProcess.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\system.serviceprocess.servicecontroller\8.0.1\lib\net8.0\System.ServiceProcess.ServiceController.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.Encoding.CodePages.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.Encoding.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.Encoding.Extensions.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.Encodings.Web.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.Json.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.RegularExpressions.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Threading.AccessControl.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Channels.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Overlapped.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Tasks.Dataflow.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Tasks.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Tasks.Extensions.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Tasks.Parallel.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Thread.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.ThreadPool.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Timer.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Transactions.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Transactions.Local.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ValueTuple.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Web.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Web.HttpUtility.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Controls.Ribbon.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Windows.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Extensions.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Forms.Design.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Forms.Design.Editors.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Forms.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Forms.Primitives.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Input.Manipulations.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Presentation.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Xaml.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.Linq.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.ReaderWriter.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.Serialization.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.XDocument.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.XmlDocument.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.XmlSerializer.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.XPath.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.XPath.XDocument.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\uglytoad.pdfpig.core\1.7.0-custom-5\lib\net6.0\UglyToad.PdfPig.Core.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\uglytoad.pdfpig\1.7.0-custom-5\lib\net6.0\UglyToad.PdfPig.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\uglytoad.pdfpig.fonts\1.7.0-custom-5\lib\net6.0\UglyToad.PdfPig.Fonts.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\uglytoad.pdfpig.tokenization\1.7.0-custom-5\lib\net6.0\UglyToad.PdfPig.Tokenization.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\uglytoad.pdfpig.tokens\1.7.0-custom-5\lib\net6.0\UglyToad.PdfPig.Tokens.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\UIAutomationClient.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\UIAutomationClientSideProviders.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\UIAutomationProvider.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\UIAutomationTypes.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\WindowsBase.dll" />
|
||||||
|
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\WindowsFormsIntegration.dll" />
|
||||||
|
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.windows.sdk.net.ref\10.0.17763.57\lib\net8.0\WinRT.Runtime.dll" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\AboutWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\AgentSettingsWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\AgentStatsDashboardWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\BatchRenameWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\ChatWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\ClipboardImagePreviewWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\ColorPickResultWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\CommandPaletteWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\DockBarWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\EyeDropperWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\GuideViewerWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\HelpDetailWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\LargeTypeWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\LauncherWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\MacroEditorWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\PreviewWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\QuickLookWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\RegionSelectWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\ReminderPopupWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\ResourceMonitorWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\ScheduleEditorWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\SessionEditorWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\SettingsWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\ShortcutHelpWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\SkillEditorWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\SkillGalleryWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\StatisticsWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\TextActionPopup.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\TrayMenuWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\Views\WorkflowAnalyzerWindow.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\App.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\AxCopilot_Content.g.cs" />
|
||||||
|
<Compile Include="E:\AX Copilot - Claude\src\AxCopilot\obj\Release\net8.0-windows10.0.17763.0\win-x64\GeneratedInternalTypeHelper.g.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Analyzer Include="C:\Program Files\dotnet\sdk\10.0.201\Sdks\Microsoft.NET.Sdk\targets\..\analyzers\Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll" />
|
||||||
|
<Analyzer Include="C:\Program Files\dotnet\sdk\10.0.201\Sdks\Microsoft.NET.Sdk\targets\..\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll" />
|
||||||
|
<Analyzer Include="C:\Users\admin\.nuget\packages\microsoft.net.illink.tasks\8.0.25\analyzers\dotnet\cs\ILLink.CodeFixProvider.dll" />
|
||||||
|
<Analyzer Include="C:\Users\admin\.nuget\packages\microsoft.net.illink.tasks\8.0.25\analyzers\dotnet\cs\ILLink.RoslynAnalyzer.dll" />
|
||||||
|
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/Microsoft.Interop.ComInterfaceGenerator.dll" />
|
||||||
|
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/Microsoft.Interop.JavaScript.JSImportGenerator.dll" />
|
||||||
|
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/Microsoft.Interop.LibraryImportGenerator.dll" />
|
||||||
|
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/Microsoft.Interop.SourceGeneration.dll" />
|
||||||
|
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/System.Text.Json.SourceGeneration.dll" />
|
||||||
|
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/System.Text.RegularExpressions.Generator.dll" />
|
||||||
|
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\analyzers/dotnet/System.Windows.Forms.Analyzers.dll" />
|
||||||
|
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\analyzers/dotnet/cs/System.Windows.Forms.Analyzers.CSharp.dll" />
|
||||||
|
<Analyzer Include="C:\Users\admin\.nuget\packages\microsoft.windows.sdk.net.ref\10.0.17763.57\analyzers/dotnet/cs/WinRT.SourceGenerator.dll" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
|
||||||
|
</Project>
|
||||||
@@ -64,7 +64,7 @@ public class ChatHandler : IActionHandler
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var storage = new ChatStorageService();
|
var storage = ServiceLocator.Get<IChatStorageService>();
|
||||||
var metas = storage.LoadAllMeta();
|
var metas = storage.LoadAllMeta();
|
||||||
foreach (var conv in metas.Take(5))
|
foreach (var conv in metas.Take(5))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -185,6 +185,14 @@ public class LauncherSettings
|
|||||||
[JsonPropertyName("enableIconAnimation")]
|
[JsonPropertyName("enableIconAnimation")]
|
||||||
public bool EnableIconAnimation { get; set; } = true;
|
public bool EnableIconAnimation { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>채팅 본문 런처 아이콘 랜덤 애니메이션. false이면 숨쉬기만. 기본 false.</summary>
|
||||||
|
[JsonPropertyName("enableChatIconRandomAnimation")]
|
||||||
|
public bool EnableChatIconRandomAnimation { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>채팅 본문 런처 아이콘 글로우 강도. strong/medium/weak. 기본 medium.</summary>
|
||||||
|
[JsonPropertyName("chatIconGlowIntensity")]
|
||||||
|
public string ChatIconGlowIntensity { get; set; } = "medium";
|
||||||
|
|
||||||
/// <summary>런처 안내 문구 랜덤 출력 활성화. false이면 고정 문구. 기본 true.</summary>
|
/// <summary>런처 안내 문구 랜덤 출력 활성화. false이면 고정 문구. 기본 true.</summary>
|
||||||
[JsonPropertyName("enableRandomPlaceholder")]
|
[JsonPropertyName("enableRandomPlaceholder")]
|
||||||
public bool EnableRandomPlaceholder { get; set; } = true;
|
public bool EnableRandomPlaceholder { get; set; } = true;
|
||||||
@@ -804,10 +812,18 @@ public class LlmSettings
|
|||||||
[JsonPropertyName("recentPromptTemplates")]
|
[JsonPropertyName("recentPromptTemplates")]
|
||||||
public List<string> RecentPromptTemplates { get; set; } = new();
|
public List<string> RecentPromptTemplates { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>작업 폴더 경로. 빈 문자열이면 미선택.</summary>
|
/// <summary>작업 폴더 경로. 빈 문자열이면 미선택. (레거시 — 탭별 경로 미지정 시 폴백)</summary>
|
||||||
[JsonPropertyName("workFolder")]
|
[JsonPropertyName("workFolder")]
|
||||||
public string WorkFolder { get; set; } = "";
|
public string WorkFolder { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>코워크(Cowork) 탭 전용 작업 폴더.</summary>
|
||||||
|
[JsonPropertyName("coworkWorkFolder")]
|
||||||
|
public string CoworkWorkFolder { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>코드(Code) 탭 전용 작업 폴더.</summary>
|
||||||
|
[JsonPropertyName("codeWorkFolder")]
|
||||||
|
public string CodeWorkFolder { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>최근 사용한 작업 폴더 목록.</summary>
|
/// <summary>최근 사용한 작업 폴더 목록.</summary>
|
||||||
[JsonPropertyName("recentWorkFolders")]
|
[JsonPropertyName("recentWorkFolders")]
|
||||||
public List<string> RecentWorkFolders { get; set; } = new();
|
public List<string> RecentWorkFolders { get; set; } = new();
|
||||||
@@ -885,9 +901,9 @@ public class LlmSettings
|
|||||||
[JsonPropertyName("maxTestFixIterations")]
|
[JsonPropertyName("maxTestFixIterations")]
|
||||||
public int MaxTestFixIterations { get; set; } = 5;
|
public int MaxTestFixIterations { get; set; } = 5;
|
||||||
|
|
||||||
/// <summary>에이전트 로그 표시 수준. simple | detailed | debug</summary>
|
/// <summary>에이전트 로그 표시 수준. hidden | simple | detailed | debug</summary>
|
||||||
[JsonPropertyName("agentLogLevel")]
|
[JsonPropertyName("agentLogLevel")]
|
||||||
public string AgentLogLevel { get; set; } = "simple";
|
public string AgentLogLevel { get; set; } = "detailed";
|
||||||
|
|
||||||
/// <summary>AX Agent UI 표현 수준. rich | balanced | simple</summary>
|
/// <summary>AX Agent UI 표현 수준. rich | balanced | simple</summary>
|
||||||
[JsonPropertyName("agentUiExpressionLevel")]
|
[JsonPropertyName("agentUiExpressionLevel")]
|
||||||
@@ -1055,6 +1071,14 @@ public class LlmSettings
|
|||||||
[JsonPropertyName("enableChatRainbowGlow")]
|
[JsonPropertyName("enableChatRainbowGlow")]
|
||||||
public bool EnableChatRainbowGlow { get; set; } = false;
|
public bool EnableChatRainbowGlow { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>새로운 계획 뷰어(V2 사이드바 레이아웃) 사용. 기본 true.</summary>
|
||||||
|
[JsonPropertyName("enableNewPlanViewer")]
|
||||||
|
public bool EnableNewPlanViewer { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>새로운 채팅 렌더링(V2 상세 이력) 사용. 기본 false.</summary>
|
||||||
|
[JsonPropertyName("enableNewChatRendering")]
|
||||||
|
public bool EnableNewChatRendering { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>AX Agent 전용 테마. system | light | dark</summary>
|
/// <summary>AX Agent 전용 테마. system | light | dark</summary>
|
||||||
[JsonPropertyName("agentTheme")]
|
[JsonPropertyName("agentTheme")]
|
||||||
public string AgentTheme { get; set; } = "system";
|
public string AgentTheme { get; set; } = "system";
|
||||||
@@ -1069,6 +1093,11 @@ public class LlmSettings
|
|||||||
[JsonPropertyName("notifyOnComplete")]
|
[JsonPropertyName("notifyOnComplete")]
|
||||||
public bool NotifyOnComplete { get; set; } = false;
|
public bool NotifyOnComplete { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>코워크 작업 완료 후 문서 자동 처리 방식.
|
||||||
|
/// "none" = 아무것도 안하기, "open" = 문서 실행(기본 앱), "preview" = 미리보기 뷰어.</summary>
|
||||||
|
[JsonPropertyName("coworkOnComplete")]
|
||||||
|
public string CoworkOnComplete { get; set; } = "none";
|
||||||
|
|
||||||
/// <summary>AI 대화창에서 팁 알림 표시 여부.</summary>
|
/// <summary>AI 대화창에서 팁 알림 표시 여부.</summary>
|
||||||
[JsonPropertyName("showTips")]
|
[JsonPropertyName("showTips")]
|
||||||
public bool ShowTips { get; set; } = false;
|
public bool ShowTips { get; set; } = false;
|
||||||
@@ -1315,6 +1344,18 @@ public class CodeSettings
|
|||||||
/// <summary>Code 탭에서 Cron 도구(cron create/list/delete) 사용 여부. 기본 true.</summary>
|
/// <summary>Code 탭에서 Cron 도구(cron create/list/delete) 사용 여부. 기본 true.</summary>
|
||||||
[JsonPropertyName("enableCronTools")]
|
[JsonPropertyName("enableCronTools")]
|
||||||
public bool EnableCronTools { get; set; } = true;
|
public bool EnableCronTools { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Code 탭 빈 화면 마스코트 캐릭터 출동 수준.
|
||||||
|
/// "none"=출동 안하기, "one"=한명만(1), "few"=적게(3), "mid"=중간(6), "all"=전부(9+)
|
||||||
|
/// 기본 "none" (메모리 절약).
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("mascotLevel")]
|
||||||
|
public string MascotLevel { get; set; } = "none";
|
||||||
|
|
||||||
|
/// <summary>(하위호환) 이전 bool 설정 — 무시됨. MascotLevel로 대체.</summary>
|
||||||
|
[JsonPropertyName("enableMascotCharacter")]
|
||||||
|
public bool EnableMascotCharacter { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>사용자 정의 커스텀 프리셋 (settings.json에 저장).</summary>
|
/// <summary>사용자 정의 커스텀 프리셋 (settings.json에 저장).</summary>
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ public class ChatConversation
|
|||||||
[JsonPropertyName("pinned")]
|
[JsonPropertyName("pinned")]
|
||||||
public bool Pinned { get; set; } = false;
|
public bool Pinned { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>아카이브된 대화는 목록에서 기본 숨김 처리.</summary>
|
||||||
|
[JsonPropertyName("archived")]
|
||||||
|
public bool Archived { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>대화가 속한 탭. "Chat" | "Cowork" | "Code".</summary>
|
/// <summary>대화가 속한 탭. "Chat" | "Cowork" | "Code".</summary>
|
||||||
[JsonPropertyName("tab")]
|
[JsonPropertyName("tab")]
|
||||||
public string Tab { get; set; } = "Chat";
|
public string Tab { get; set; } = "Chat";
|
||||||
@@ -126,7 +130,7 @@ public class ChatConversation
|
|||||||
public List<DraftQueueItem> DraftQueueItems { get; set; } = new();
|
public List<DraftQueueItem> DraftQueueItems { get; set; } = new();
|
||||||
|
|
||||||
[JsonPropertyName("showExecutionHistory")]
|
[JsonPropertyName("showExecutionHistory")]
|
||||||
public bool ShowExecutionHistory { get; set; } = true;
|
public bool ShowExecutionHistory { get; set; } = false;
|
||||||
|
|
||||||
[JsonPropertyName("agentRunHistory")]
|
[JsonPropertyName("agentRunHistory")]
|
||||||
public List<ChatAgentRunRecord> AgentRunHistory { get; set; } = new();
|
public List<ChatAgentRunRecord> AgentRunHistory { get; set; } = new();
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ public partial class AgentLoopService
|
|||||||
public bool BroadScanDetected { get; set; }
|
public bool BroadScanDetected { get; set; }
|
||||||
public bool SelectiveHit { get; set; }
|
public bool SelectiveHit { get; set; }
|
||||||
public bool CorrectiveHintInjected { get; set; }
|
public bool CorrectiveHintInjected { get; set; }
|
||||||
|
/// <summary>스킬 런타임이 allowed-tools를 명시했으면 true — 탐색 필터링을 건너뜀.</summary>
|
||||||
|
public bool SkillAllowedToolsActive { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyCollection<IAgentTool> FilterExplorationToolsForCurrentIteration(
|
private static IReadOnlyCollection<IAgentTool> FilterExplorationToolsForCurrentIteration(
|
||||||
@@ -36,6 +38,11 @@ public partial class AgentLoopService
|
|||||||
if (tools.Count == 0)
|
if (tools.Count == 0)
|
||||||
return tools;
|
return tools;
|
||||||
|
|
||||||
|
// 스킬 런타임 정책으로 allowed-tools가 명시된 경우 탐색 필터링을 건너뜀
|
||||||
|
// — 스킬이 의도적으로 허용한 도구(folder_map 등)를 정책이 차단하면 안 됨
|
||||||
|
if (state.SkillAllowedToolsActive)
|
||||||
|
return tools;
|
||||||
|
|
||||||
// 문서 생성 모드: 생성 도구를 최우선, 탐색 도구를 뒤로 배치
|
// 문서 생성 모드: 생성 도구를 최우선, 탐색 도구를 뒤로 배치
|
||||||
if (state.Scope == ExplorationScope.DirectCreation)
|
if (state.Scope == ExplorationScope.DirectCreation)
|
||||||
{
|
{
|
||||||
|
|||||||
185
src/AxCopilot/Services/Agent/AgentLoopExplorationRecovery.cs
Normal file
@@ -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<string> 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<ChatMessage> 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<ChatMessage> 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<string> CollectFolderMapRecoveryCandidates(string baseDir, AgentContext context, int maxResults)
|
||||||
|
{
|
||||||
|
var candidates = new List<string>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,11 +25,11 @@ public partial class AgentLoopService
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>도구 호출을 병렬 가능 / 순차 필수로 분류합니다.</summary>
|
/// <summary>도구 호출을 병렬 가능 / 순차 필수로 분류합니다.</summary>
|
||||||
private static (List<LlmService.ContentBlock> Parallel, List<LlmService.ContentBlock> Sequential)
|
private static (List<ContentBlock> Parallel, List<ContentBlock> Sequential)
|
||||||
ClassifyToolCalls(List<LlmService.ContentBlock> calls)
|
ClassifyToolCalls(List<ContentBlock> calls)
|
||||||
{
|
{
|
||||||
var parallel = new List<LlmService.ContentBlock>();
|
var parallel = new List<ContentBlock>();
|
||||||
var sequential = new List<LlmService.ContentBlock>();
|
var sequential = new List<ContentBlock>();
|
||||||
var collectParallelPrefix = true;
|
var collectParallelPrefix = true;
|
||||||
|
|
||||||
foreach (var call in calls)
|
foreach (var call in calls)
|
||||||
@@ -81,7 +81,7 @@ public partial class AgentLoopService
|
|||||||
|
|
||||||
/// <summary>읽기 전용 도구들을 병렬 실행합니다.</summary>
|
/// <summary>읽기 전용 도구들을 병렬 실행합니다.</summary>
|
||||||
private async Task ExecuteToolsInParallelAsync(
|
private async Task ExecuteToolsInParallelAsync(
|
||||||
List<LlmService.ContentBlock> calls,
|
List<ContentBlock> calls,
|
||||||
List<ChatMessage> messages,
|
List<ChatMessage> messages,
|
||||||
AgentContext context,
|
AgentContext context,
|
||||||
List<string> planSteps,
|
List<string> planSteps,
|
||||||
@@ -100,13 +100,13 @@ public partial class AgentLoopService
|
|||||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var executableCalls = new List<LlmService.ContentBlock>();
|
var executableCalls = new List<ContentBlock>();
|
||||||
foreach (var call in calls)
|
foreach (var call in calls)
|
||||||
{
|
{
|
||||||
var resolvedToolName = ResolveRequestedToolName(call.ToolName, activeToolNames);
|
var resolvedToolName = ResolveRequestedToolName(call.ToolName, activeToolNames);
|
||||||
var effectiveCall = string.Equals(call.ToolName, resolvedToolName, StringComparison.OrdinalIgnoreCase)
|
var effectiveCall = string.Equals(call.ToolName, resolvedToolName, StringComparison.OrdinalIgnoreCase)
|
||||||
? call
|
? call
|
||||||
: new LlmService.ContentBlock
|
: new ContentBlock
|
||||||
{
|
{
|
||||||
Type = call.Type,
|
Type = call.Type,
|
||||||
Text = call.Text,
|
Text = call.Text,
|
||||||
|
|||||||
103
src/AxCopilot/Services/Agent/AgentLoopPathStagnation.cs
Normal file
@@ -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<ChatMessage> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -114,6 +114,12 @@ public partial class AgentLoopService
|
|||||||
if (!_docFallbackAttempted && !documentPlanWasCalled)
|
if (!_docFallbackAttempted && !documentPlanWasCalled)
|
||||||
return (false, false);
|
return (false, false);
|
||||||
|
|
||||||
|
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
EmitEvent(AgentEventType.Complete, "", "에이전트 작업 완료");
|
||||||
|
return (true, false);
|
||||||
|
}
|
||||||
|
|
||||||
var verificationEnabled = executionPolicy.EnablePostToolVerification
|
var verificationEnabled = executionPolicy.EnablePostToolVerification
|
||||||
&& AgentTabSettingsResolver.IsPostToolVerificationEnabled(ActiveTab, llm);
|
&& AgentTabSettingsResolver.IsPostToolVerificationEnabled(ActiveTab, llm);
|
||||||
var shouldVerify = ShouldRunPostToolVerification(
|
var shouldVerify = ShouldRunPostToolVerification(
|
||||||
@@ -156,6 +162,9 @@ public partial class AgentLoopService
|
|||||||
if (!shouldVerify)
|
if (!shouldVerify)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
|
||||||
if (string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var highImpactCodeChange = IsHighImpactCodeModification(ActiveTab ?? "", call.ToolName, result);
|
var highImpactCodeChange = IsHighImpactCodeModification(ActiveTab ?? "", call.ToolName, result);
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ public partial class AgentLoopService
|
|||||||
return sawDiff;
|
return sawDiff;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 蹂寃??꾧뎄 ?먯껜媛 ?놁쑝硫?diff 寃뚯씠???꾧뎄?몄? ?뺤씤
|
// 변경 도구 자체가 없으면 diff 게이트 도구인지 확인
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,8 +198,8 @@ public partial class AgentLoopService
|
|||||||
"insertions",
|
"insertions",
|
||||||
"deletions",
|
"deletions",
|
||||||
"file changed",
|
"file changed",
|
||||||
"異붽?",
|
"추가",
|
||||||
"?낅뜲?댄듃");
|
"업데이트");
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryApplyDocumentArtifactGateTransition(
|
private bool TryApplyDocumentArtifactGateTransition(
|
||||||
@@ -392,9 +392,9 @@ public partial class AgentLoopService
|
|||||||
{
|
{
|
||||||
var content = m.Content ?? "";
|
var content = m.Content ?? "";
|
||||||
if (content.StartsWith("{\"_tool_use_blocks\""))
|
if (content.StartsWith("{\"_tool_use_blocks\""))
|
||||||
content = "[?꾧뎄 ?몄텧 ?붿빟]";
|
content = "[도구 실행 요약]";
|
||||||
else if (content.StartsWith("{\"type\":\"tool_result\""))
|
else if (content.StartsWith("{\"type\":\"tool_result\""))
|
||||||
content = "[?꾧뎄 ?ㅽ뻾 寃곌낵 ?붿빟]";
|
content = "[도구 실행 결과 요약]";
|
||||||
else if (content.Length > 180)
|
else if (content.Length > 180)
|
||||||
content = content[..180] + "...";
|
content = content[..180] + "...";
|
||||||
return $"- [{m.Role}] {content}";
|
return $"- [{m.Role}] {content}";
|
||||||
@@ -403,7 +403,7 @@ public partial class AgentLoopService
|
|||||||
|
|
||||||
var summary = summaryLines.Count > 0
|
var summary = summaryLines.Count > 0
|
||||||
? string.Join("\n", summaryLines)
|
? string.Join("\n", summaryLines)
|
||||||
: "- ?댁쟾 ???留λ씫???먮룞 異뺤빟?섏뿀?듬땲??";
|
: "- 이전 대화 맥락이 자동 축약되었습니다";
|
||||||
|
|
||||||
var tail = nonSystem.Skip(Math.Max(0, nonSystem.Count - keepTailCount)).ToList();
|
var tail = nonSystem.Skip(Math.Max(0, nonSystem.Count - keepTailCount)).ToList();
|
||||||
|
|
||||||
@@ -414,7 +414,7 @@ public partial class AgentLoopService
|
|||||||
{
|
{
|
||||||
Role = "user",
|
Role = "user",
|
||||||
Timestamp = DateTime.Now,
|
Timestamp = DateTime.Now,
|
||||||
Content = $"[?쒖뒪???먮룞 留λ씫 異뺤빟]\n?꾨옒???댁쟾 ??붿쓽 ?듭떖 ?붿빟?낅땲??\n{summary}"
|
Content = $"[시스템 자동 맥락 축약]\n아래는 이전 대화의 축소 요약입니다.\n{summary}"
|
||||||
});
|
});
|
||||||
messages.AddRange(tail);
|
messages.AddRange(tail);
|
||||||
return true;
|
return true;
|
||||||
@@ -458,7 +458,7 @@ public partial class AgentLoopService
|
|||||||
EmitEvent(
|
EmitEvent(
|
||||||
AgentEventType.Thinking,
|
AgentEventType.Thinking,
|
||||||
call.ToolName,
|
call.ToolName,
|
||||||
$"?숈씪 ?꾧뎄/?뚮씪誘명꽣 諛섎났 ?ㅽ뙣 ?⑦꽩 媛먯? - ?ㅻⅨ ?묎렐?쇰줈 ?꾪솚?⑸땲??({repeatedFailedToolSignatureCount}/{maxRetry})");
|
$"동일 도구/파라미터 반복 실패 패턴 감지 - 다른 접근으로 전환합니다({repeatedFailedToolSignatureCount}/{maxRetry})");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,7 +479,7 @@ public partial class AgentLoopService
|
|||||||
messages.Add(LlmService.CreateToolResultMessage(
|
messages.Add(LlmService.CreateToolResultMessage(
|
||||||
call.ToolId,
|
call.ToolId,
|
||||||
call.ToolName,
|
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."));
|
"Stop repeating the same read-only call and switch to a concrete next action."));
|
||||||
messages.Add(new ChatMessage
|
messages.Add(new ChatMessage
|
||||||
{
|
{
|
||||||
@@ -493,7 +493,7 @@ public partial class AgentLoopService
|
|||||||
EmitEvent(
|
EmitEvent(
|
||||||
AgentEventType.Thinking,
|
AgentEventType.Thinking,
|
||||||
call.ToolName,
|
call.ToolName,
|
||||||
$"臾댁쓽誘명븳 ?쎄린 ?꾧뎄 諛섎났 猷⑦봽媛 媛먯??섏뼱 ?ㅻⅨ ?꾨왂?쇰줈 ?꾪솚?⑸땲??({repeatedSameSignatureCount}??");
|
$"무의미한 읽기 도구 반복 루프가 감지되어 다른 방향으로 전환합니다({repeatedSameSignatureCount}회)");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,7 +520,7 @@ public partial class AgentLoopService
|
|||||||
EmitEvent(
|
EmitEvent(
|
||||||
AgentEventType.Thinking,
|
AgentEventType.Thinking,
|
||||||
"",
|
"",
|
||||||
$"?쎄린 ?꾩슜 ?꾧뎄媛 ?곗냽 {consecutiveReadOnlySuccessTools}???ㅽ뻾?섏뼱 ?뺤껜媛 媛먯??섏뿀?듬땲?? ?ㅽ뻾 ?④퀎濡??꾪솚?⑸땲??(threshold={threshold})");
|
$"읽기 전용 도구가 연속 {consecutiveReadOnlySuccessTools}회 실행되어 정체가 감지되었습니다. 실행 전략으로 전환합니다(threshold={threshold})");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,7 +554,7 @@ public partial class AgentLoopService
|
|||||||
EmitEvent(
|
EmitEvent(
|
||||||
AgentEventType.Thinking,
|
AgentEventType.Thinking,
|
||||||
"",
|
"",
|
||||||
$"?ㅽ뻾 吏꾩쟾???놁뼱 媛뺤젣 蹂듦뎄 ?④퀎濡??쒖옉?⑸땲??({runState.NoProgressRecoveryRetry}/2)");
|
$"실행 진전이 없어 강제 복구 전략으로 시작합니다({runState.NoProgressRecoveryRetry}/2)");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,11 +629,11 @@ public partial class AgentLoopService
|
|||||||
"forbidden",
|
"forbidden",
|
||||||
"not found",
|
"not found",
|
||||||
"invalid",
|
"invalid",
|
||||||
"?ㅽ뙣",
|
"실패",
|
||||||
"?ㅻ쪟",
|
"오류",
|
||||||
"?덉쇅",
|
"예외",
|
||||||
"?쒓컙 珥덇낵",
|
"시간 초과",
|
||||||
"沅뚰븳 嫄곕?",
|
"권한 거부",
|
||||||
"李⑤떒",
|
"李⑤떒",
|
||||||
"찾을 수");
|
"찾을 수");
|
||||||
}
|
}
|
||||||
@@ -1052,7 +1052,7 @@ public partial class AgentLoopService
|
|||||||
result.Output)
|
result.Output)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
EmitEvent(AgentEventType.Thinking, "", $"Self-Reflection: ?ㅽ뙣 遺꾩꽍 ???ъ떆??({consecutiveErrors}/{maxRetry})");
|
EmitEvent(AgentEventType.Thinking, "", $"Self-Reflection: 실패 분석 후 재시도({consecutiveErrors}/{maxRetry})");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1139,7 +1139,7 @@ public partial class AgentLoopService
|
|||||||
if (devShouldContinue)
|
if (devShouldContinue)
|
||||||
{
|
{
|
||||||
messages.Add(LlmService.CreateToolResultMessage(
|
messages.Add(LlmService.CreateToolResultMessage(
|
||||||
call.ToolId, call.ToolName, devToolResultMessage ?? "[SKIPPED by developer] 媛쒕컻?먭? ???꾧뎄 ?ㅽ뻾??嫄대꼫?곗뿀?듬땲??"));
|
call.ToolId, call.ToolName, devToolResultMessage ?? "[SKIPPED by developer] 개발자에 의해 도구 실행이 건너뛰어졌습니다"));
|
||||||
return (true, null);
|
return (true, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1160,7 +1160,7 @@ public partial class AgentLoopService
|
|||||||
if (scopeShouldContinue)
|
if (scopeShouldContinue)
|
||||||
{
|
{
|
||||||
messages.Add(LlmService.CreateToolResultMessage(
|
messages.Add(LlmService.CreateToolResultMessage(
|
||||||
call.ToolId, call.ToolName, scopeToolResultMessage ?? "[SKIPPED] 媛쒕컻?먭? ???묒뾽??嫄대꼫?곗뿀?듬땲??"));
|
call.ToolId, call.ToolName, scopeToolResultMessage ?? "[SKIPPED] 사용자에 의해 작업이 건너뛰어졌습니다"));
|
||||||
return (true, null);
|
return (true, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ namespace AxCopilot.Services.Agent;
|
|||||||
|
|
||||||
public partial class AgentLoopService
|
public partial class AgentLoopService
|
||||||
{
|
{
|
||||||
private static (bool ShouldRun, List<LlmService.ContentBlock> ParallelBatch, List<LlmService.ContentBlock> SequentialBatch)
|
private static (bool ShouldRun, List<ContentBlock> ParallelBatch, List<ContentBlock> SequentialBatch)
|
||||||
CreateParallelExecutionPlan(bool parallelEnabled, List<LlmService.ContentBlock> toolCalls, int maxParallelBatch)
|
CreateParallelExecutionPlan(bool parallelEnabled, List<ContentBlock> toolCalls, int maxParallelBatch)
|
||||||
{
|
{
|
||||||
if (!parallelEnabled || toolCalls.Count <= 1)
|
if (!parallelEnabled || toolCalls.Count <= 1)
|
||||||
return (false, new List<LlmService.ContentBlock>(), toolCalls);
|
return (false, new List<ContentBlock>(), toolCalls);
|
||||||
|
|
||||||
var (parallelBatch, sequentialBatch) = ClassifyToolCalls(toolCalls);
|
var (parallelBatch, sequentialBatch) = ClassifyToolCalls(toolCalls);
|
||||||
if (maxParallelBatch > 0 && parallelBatch.Count > maxParallelBatch)
|
if (maxParallelBatch > 0 && parallelBatch.Count > maxParallelBatch)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace AxCopilot.Services.Agent;
|
namespace AxCopilot.Services.Agent;
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ internal static class AgentTranscriptDisplayCatalog
|
|||||||
{
|
{
|
||||||
var summary = (evt.Summary ?? string.Empty).Trim();
|
var summary = (evt.Summary ?? string.Empty).Trim();
|
||||||
if (!string.IsNullOrWhiteSpace(summary))
|
if (!string.IsNullOrWhiteSpace(summary))
|
||||||
return summary;
|
return StripNonBmpCharacters(summary);
|
||||||
|
|
||||||
return evt.Type switch
|
return evt.Type switch
|
||||||
{
|
{
|
||||||
@@ -265,13 +265,16 @@ internal static class AgentTranscriptDisplayCatalog
|
|||||||
|
|
||||||
if (resultPresentation != null)
|
if (resultPresentation != null)
|
||||||
{
|
{
|
||||||
|
// ToolResult의 GroupKey를 선행 ToolCall과 동일한 activity 그룹으로 설정
|
||||||
|
// → ProcessFeed에서 ToolCall 카드를 ToolResult로 교체(머지)
|
||||||
|
var resultGroup = ResolveActivityGroup(toolName, summary);
|
||||||
return new AgentTranscriptRowPresentation(
|
return new AgentTranscriptRowPresentation(
|
||||||
TranscriptRowKind.ToolResult,
|
TranscriptRowKind.ToolResult,
|
||||||
"결과",
|
"결과",
|
||||||
resultPresentation.Label,
|
resultPresentation.Label,
|
||||||
resultPresentation.Description,
|
resultPresentation.Description,
|
||||||
$"result:{resultPresentation.Kind}:{resultPresentation.StatusKind}",
|
$"activity:{resultGroup}",
|
||||||
false,
|
true,
|
||||||
resultPresentation.NeedsAttention);
|
resultPresentation.NeedsAttention);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,4 +423,32 @@ internal static class AgentTranscriptDisplayCatalog
|
|||||||
_ => "도구",
|
_ => "도구",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WPF 기본 폰트(Segoe UI)에서 렌더링되지 않는 비-BMP 유니코드 문자(이모지 등)를 제거합니다.
|
||||||
|
/// LLM 응답에 이모지가 포함되면 깨져서 표시되는 문제를 방지합니다.
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,15 +100,18 @@ public class BuildRunTool : IAgentTool
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// "run" 액션은 빌드로 대체 — 프로그램 실행은 사용자가 직접 수행
|
||||||
command = action switch
|
command = action switch
|
||||||
{
|
{
|
||||||
"build" => project.BuildCommand,
|
"build" => project.BuildCommand,
|
||||||
"test" => project.TestCommand,
|
"test" => project.TestCommand,
|
||||||
"run" => project.RunCommand,
|
"run" => project.BuildCommand, // run → build로 대체 (실행은 사용자 몫)
|
||||||
"lint" => string.IsNullOrEmpty(project.LintCommand) ? null : project.LintCommand,
|
"lint" => string.IsNullOrEmpty(project.LintCommand) ? null : project.LintCommand,
|
||||||
"format" => string.IsNullOrEmpty(project.FormatCommand) ? null : project.FormatCommand,
|
"format" => string.IsNullOrEmpty(project.FormatCommand) ? null : project.FormatCommand,
|
||||||
_ => project.BuildCommand,
|
_ => project.BuildCommand,
|
||||||
};
|
};
|
||||||
|
if (action == "run")
|
||||||
|
action = "build"; // 출력 메시지에도 build로 표시
|
||||||
if (command == null)
|
if (command == null)
|
||||||
return ToolResult.Fail($"이 프로젝트 타입({project.Type})에서 '{action}' 작업은 지원되지 않습니다.");
|
return ToolResult.Fail($"이 프로젝트 타입({project.Type})에서 '{action}' 작업은 지원되지 않습니다.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ public class DocumentAssemblerTool : IAgentTool
|
|||||||
var pageEstimate = Math.Max(1, totalWords / 500);
|
var pageEstimate = Math.Max(1, totalWords / 500);
|
||||||
|
|
||||||
return ToolResult.Ok(
|
return ToolResult.Ok(
|
||||||
$"✅ 문서 조립 완료: {Path.GetFileName(fullPath)}\n" +
|
$"✅ 문서 조립 완료: {fullPath}\n" +
|
||||||
$" 섹션: {sections.Count}개 | 글자: {totalChars:N0} | 단어: ~{totalWords:N0} | 예상 페이지: ~{pageEstimate}\n" +
|
$" 섹션: {sections.Count}개 | 글자: {totalChars:N0} | 단어: ~{totalWords:N0} | 예상 페이지: ~{pageEstimate}\n" +
|
||||||
$"{resultMsg}", fullPath);
|
$"{resultMsg}", fullPath);
|
||||||
}
|
}
|
||||||
@@ -241,14 +241,30 @@ public class DocumentAssemblerTool : IAgentTool
|
|||||||
path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
|
path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
|
||||||
|
|
||||||
var mainPart = doc.AddMainDocumentPart();
|
var mainPart = doc.AddMainDocumentPart();
|
||||||
|
|
||||||
|
// 기본 스타일 파트 추가 (styles.xml — 없으면 Word에서 글꼴/서식 깨짐)
|
||||||
|
var stylesPart = mainPart.AddNewPart<DocumentFormat.OpenXml.Packaging.StyleDefinitionsPart>();
|
||||||
|
stylesPart.Styles = CreateDefaultDocxStyles();
|
||||||
|
stylesPart.Styles.Save();
|
||||||
|
|
||||||
mainPart.Document = new DocumentFormat.OpenXml.Wordprocessing.Document();
|
mainPart.Document = new DocumentFormat.OpenXml.Wordprocessing.Document();
|
||||||
var body = mainPart.Document.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Body());
|
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 titlePara = new DocumentFormat.OpenXml.Wordprocessing.Paragraph();
|
||||||
var titleRun = new DocumentFormat.OpenXml.Wordprocessing.Run();
|
var titleRun = new DocumentFormat.OpenXml.Wordprocessing.Run();
|
||||||
titleRun.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.RunProperties
|
titleRun.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.RunProperties
|
||||||
{
|
{
|
||||||
|
RunFonts = KoreanFonts(),
|
||||||
Bold = new DocumentFormat.OpenXml.Wordprocessing.Bold(),
|
Bold = new DocumentFormat.OpenXml.Wordprocessing.Bold(),
|
||||||
FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "48" }
|
FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = "48" }
|
||||||
});
|
});
|
||||||
@@ -267,6 +283,7 @@ public class DocumentAssemblerTool : IAgentTool
|
|||||||
var headRun = new DocumentFormat.OpenXml.Wordprocessing.Run();
|
var headRun = new DocumentFormat.OpenXml.Wordprocessing.Run();
|
||||||
headRun.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.RunProperties
|
headRun.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.RunProperties
|
||||||
{
|
{
|
||||||
|
RunFonts = KoreanFonts(),
|
||||||
Bold = new DocumentFormat.OpenXml.Wordprocessing.Bold(),
|
Bold = new DocumentFormat.OpenXml.Wordprocessing.Bold(),
|
||||||
FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = level <= 1 ? "32" : "28" },
|
FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = level <= 1 ? "32" : "28" },
|
||||||
Color = new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "2B579A" },
|
Color = new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "2B579A" },
|
||||||
@@ -281,6 +298,11 @@ public class DocumentAssemblerTool : IAgentTool
|
|||||||
{
|
{
|
||||||
var para = new DocumentFormat.OpenXml.Wordprocessing.Paragraph();
|
var para = new DocumentFormat.OpenXml.Wordprocessing.Paragraph();
|
||||||
var run = new DocumentFormat.OpenXml.Wordprocessing.Run();
|
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())
|
run.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Text(line.Trim())
|
||||||
{
|
{
|
||||||
Space = DocumentFormat.OpenXml.SpaceProcessingModeValues.Preserve
|
Space = DocumentFormat.OpenXml.SpaceProcessingModeValues.Preserve
|
||||||
@@ -293,9 +315,59 @@ public class DocumentAssemblerTool : IAgentTool
|
|||||||
body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Paragraph());
|
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 조립 완료";
|
return " ✓ DOCX 조립 완료";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>DOCX 기본 스타일 정의 생성 (한글 글꼴 기본 설정 포함).</summary>
|
||||||
|
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)
|
private string AssembleMarkdown(string path, string title, List<(string Heading, string Content, int Level)> sections)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ public class DocumentPlannerTool : IAgentTool
|
|||||||
folder_data_usage = folderDataUsage,
|
folder_data_usage = folderDataUsage,
|
||||||
step1 = hasRefData
|
step1 = hasRefData
|
||||||
? "Reference data is already provided above. Skip to step 3."
|
? "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
|
step2 = hasRefData
|
||||||
? "(skipped)"
|
? "(skipped)"
|
||||||
: "Summarize the key findings from the folder documents relevant to the topic.",
|
: "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);
|
path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
|
||||||
|
|
||||||
var mainPart = doc.AddMainDocumentPart();
|
var mainPart = doc.AddMainDocumentPart();
|
||||||
|
|
||||||
|
// 기본 스타일 파트 추가 (styles.xml — 없으면 Word에서 글꼴/서식 깨짐)
|
||||||
|
var stylesPart = mainPart.AddNewPart<DocumentFormat.OpenXml.Packaging.StyleDefinitionsPart>();
|
||||||
|
stylesPart.Styles = CreateDefaultDocxStyles();
|
||||||
|
stylesPart.Styles.Save();
|
||||||
|
|
||||||
mainPart.Document = new DocumentFormat.OpenXml.Wordprocessing.Document();
|
mainPart.Document = new DocumentFormat.OpenXml.Wordprocessing.Document();
|
||||||
var body = mainPart.Document.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Body());
|
var body = mainPart.Document.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Body());
|
||||||
|
|
||||||
@@ -438,6 +444,15 @@ public class DocumentPlannerTool : IAgentTool
|
|||||||
}
|
}
|
||||||
AddDocxParagraph(body, ""); // 섹션 간 빈 줄
|
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,
|
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 run = new DocumentFormat.OpenXml.Wordprocessing.Run();
|
||||||
var props = new DocumentFormat.OpenXml.Wordprocessing.RunProperties
|
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 }
|
FontSize = new DocumentFormat.OpenXml.Wordprocessing.FontSize { Val = fontSize }
|
||||||
};
|
};
|
||||||
if (bold) props.Bold = new DocumentFormat.OpenXml.Wordprocessing.Bold();
|
if (bold) props.Bold = new DocumentFormat.OpenXml.Wordprocessing.Bold();
|
||||||
@@ -460,6 +482,45 @@ public class DocumentPlannerTool : IAgentTool
|
|||||||
body.AppendChild(para);
|
body.AppendChild(para);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>DOCX 기본 스타일 정의 생성 (한글 글꼴 기본 설정 포함).</summary>
|
||||||
|
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 생성 ──────────────────────────────────────────────────────
|
// ─── Markdown 생성 ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static void GenerateMarkdown(string path, string title, List<SectionPlan> sections)
|
private static void GenerateMarkdown(string path, string title, List<SectionPlan> sections)
|
||||||
@@ -631,7 +692,7 @@ public class DocumentPlannerTool : IAgentTool
|
|||||||
return normalized;
|
return normalized;
|
||||||
|
|
||||||
var intent = $"{docType} {topic}".ToLowerInvariant();
|
var intent = $"{docType} {topic}".ToLowerInvariant();
|
||||||
if (ContainsAny(intent, "대시보드", "dashboard", "분석", "analysis", "지표", "통계"))
|
if (ContainsAny(intent, "대시보드", "dashboard", "지표"))
|
||||||
return "dashboard";
|
return "dashboard";
|
||||||
if (ContainsAny(intent, "기획", "아이디어", "creative", "브레인스토밍"))
|
if (ContainsAny(intent, "기획", "아이디어", "creative", "브레인스토밍"))
|
||||||
return "creative";
|
return "creative";
|
||||||
@@ -639,11 +700,15 @@ public class DocumentPlannerTool : IAgentTool
|
|||||||
return "corporate";
|
return "corporate";
|
||||||
if (ContainsAny(intent, "가이드", "manual", "guide", "매뉴얼"))
|
if (ContainsAny(intent, "가이드", "manual", "guide", "매뉴얼"))
|
||||||
return "minimal";
|
return "minimal";
|
||||||
|
// 보고서/분석: corporate 무드 (배경색 포함)
|
||||||
|
if (ContainsAny(intent, "보고서", "report", "분석", "analysis", "통계"))
|
||||||
|
return "corporate";
|
||||||
|
|
||||||
return docType switch
|
return docType switch
|
||||||
{
|
{
|
||||||
"proposal" => "corporate",
|
"proposal" => "corporate",
|
||||||
"analysis" => "dashboard",
|
"analysis" => "corporate",
|
||||||
|
"report" => "corporate",
|
||||||
"manual" or "guide" => "minimal",
|
"manual" or "guide" => "minimal",
|
||||||
"presentation" => "creative",
|
"presentation" => "creative",
|
||||||
"minutes" => "professional",
|
"minutes" => "professional",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Text.Json;
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using DocumentFormat.OpenXml.Packaging;
|
using DocumentFormat.OpenXml.Packaging;
|
||||||
using DocumentFormat.OpenXml.Spreadsheet;
|
using DocumentFormat.OpenXml.Spreadsheet;
|
||||||
|
using A = DocumentFormat.OpenXml.Drawing;
|
||||||
using UglyToad.PdfPig;
|
using UglyToad.PdfPig;
|
||||||
|
|
||||||
namespace AxCopilot.Services.Agent;
|
namespace AxCopilot.Services.Agent;
|
||||||
@@ -17,7 +18,7 @@ public class DocumentReaderTool : IAgentTool
|
|||||||
public string Name => "document_read";
|
public string Name => "document_read";
|
||||||
public string Description =>
|
public string Description =>
|
||||||
"Read a document file and extract its text content. " +
|
"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). " +
|
"BibTeX (.bib), RIS (.ris). " +
|
||||||
"For large files, use 'offset' to read from a specific character position (chunked reading). " +
|
"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'). " +
|
"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),
|
".pdf" => await Task.Run(() => ReadPdf(fullPath, extractMax, pagesParam, sectionParam), ct),
|
||||||
".docx" => await Task.Run(() => ReadDocx(fullPath, extractMax), 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),
|
".xlsx" => await Task.Run(() => ReadXlsx(fullPath, sheetParam, extractMax), ct),
|
||||||
".bib" => await Task.Run(() => ReadBibTeX(fullPath, extractMax), ct),
|
".bib" => await Task.Run(() => ReadBibTeX(fullPath, extractMax), ct),
|
||||||
".ris" => await Task.Run(() => ReadRis(fullPath, extractMax), ct),
|
".ris" => await Task.Run(() => ReadRis(fullPath, extractMax), ct),
|
||||||
@@ -478,6 +480,71 @@ public class DocumentReaderTool : IAgentTool
|
|||||||
return Truncate(sb.ToString(), maxChars);
|
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<DocumentFormat.OpenXml.Presentation.SlideId>().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<A.Text>()
|
||||||
|
.Select(t => t.Text)
|
||||||
|
.Where(t => !string.IsNullOrWhiteSpace(t));
|
||||||
|
|
||||||
|
// 단락(Paragraph) 단위로 그룹핑하여 줄바꿈 유지
|
||||||
|
var paragraphs = slide.Descendants<A.Paragraph>();
|
||||||
|
foreach (var para in paragraphs)
|
||||||
|
{
|
||||||
|
var paraText = string.Join("", para.Descendants<A.Text>().Select(t => t.Text));
|
||||||
|
if (!string.IsNullOrWhiteSpace(paraText))
|
||||||
|
sb.AppendLine(paraText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 슬라이드 노트가 있으면 포함
|
||||||
|
var notesPart = slidePart.NotesSlidePart;
|
||||||
|
if (notesPart != null)
|
||||||
|
{
|
||||||
|
var noteTexts = notesPart.NotesSlide.Descendants<A.Paragraph>()
|
||||||
|
.Select(p => string.Join("", p.Descendants<A.Text>().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 ───────────────────────────────────────────────────────────────
|
// ─── XLSX ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static string ReadXlsx(string path, string sheetParam, int maxChars)
|
private static string ReadXlsx(string path, string sheetParam, int maxChars)
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ public class DocxSkill : IAgentTool
|
|||||||
"• List: {\"type\": \"list\", \"style\": \"bullet|number\", \"items\": [\"item1\", \" - sub-item\"]}\n" +
|
"• List: {\"type\": \"list\", \"style\": \"bullet|number\", \"items\": [\"item1\", \" - sub-item\"]}\n" +
|
||||||
"• Callout: {\"type\": \"callout\", \"style\": \"info|warning|tip|danger\", \"title\": \"...\", \"body\": \"...\"}\n" +
|
"• Callout: {\"type\": \"callout\", \"style\": \"info|warning|tip|danger\", \"title\": \"...\", \"body\": \"...\"}\n" +
|
||||||
"• HighlightBox: {\"type\": \"highlight_box\", \"text\": \"...\", \"color\": \"blue|green|orange|red\"}\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" }
|
Items = new() { Type = "object" }
|
||||||
},
|
},
|
||||||
["header"] = new() { Type = "string", Description = "Header text shown at top of every page (optional)." },
|
["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);
|
using var doc = WordprocessingDocument.Create(fullPath, WordprocessingDocumentType.Document);
|
||||||
var mainPart = doc.AddMainDocumentPart();
|
var mainPart = doc.AddMainDocumentPart();
|
||||||
|
|
||||||
|
// 기본 스타일 파트 추가 (styles.xml)
|
||||||
|
var stylesPart = mainPart.AddNewPart<StyleDefinitionsPart>();
|
||||||
|
stylesPart.Styles = CreateDefaultDocxStyles();
|
||||||
|
stylesPart.Styles.Save();
|
||||||
|
|
||||||
mainPart.Document = new Document();
|
mainPart.Document = new Document();
|
||||||
var body = mainPart.Document.AppendChild(new Body());
|
var body = mainPart.Document.AppendChild(new Body());
|
||||||
|
|
||||||
@@ -194,6 +202,12 @@ public class DocxSkill : IAgentTool
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (blockType == "icon")
|
||||||
|
{
|
||||||
|
body.Append(CreateIconParagraph(section));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// 일반 섹션 (heading + body)
|
// 일반 섹션 (heading + body)
|
||||||
var heading = section.SafeTryGetProperty("heading", out var h) ? h.SafeGetString() ?? "" : "";
|
var heading = section.SafeTryGetProperty("heading", out var h) ? h.SafeGetString() ?? "" : "";
|
||||||
var bodyText = section.SafeTryGetProperty("body", out var b) ? b.SafeGetString() ?? "" : "";
|
var bodyText = section.SafeTryGetProperty("body", out var b) ? b.SafeGetString() ?? "" : "";
|
||||||
@@ -212,6 +226,8 @@ public class DocxSkill : IAgentTool
|
|||||||
sectionCount++;
|
sectionCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ SectionProperties는 반드시 body의 마지막 자식이어야 함 (OOXML 규격)
|
||||||
|
EnsureSectionProperties(body);
|
||||||
mainPart.Document.Save();
|
mainPart.Document.Save();
|
||||||
|
|
||||||
var parts = new List<string>();
|
var parts = new List<string>();
|
||||||
@@ -250,7 +266,7 @@ public class DocxSkill : IAgentTool
|
|||||||
Bold = new Bold(),
|
Bold = new Bold(),
|
||||||
FontSize = new FontSize { Val = "44" }, // 22pt
|
FontSize = new FontSize { Val = "44" }, // 22pt
|
||||||
Color = new Color { Val = theme.Title },
|
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);
|
para.Append(run);
|
||||||
return para;
|
return para;
|
||||||
@@ -280,7 +296,7 @@ public class DocxSkill : IAgentTool
|
|||||||
Bold = new Bold(),
|
Bold = new Bold(),
|
||||||
FontSize = new FontSize { Val = fontSize },
|
FontSize = new FontSize { Val = fontSize },
|
||||||
Color = new Color { Val = color },
|
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);
|
para.Append(run);
|
||||||
return para;
|
return para;
|
||||||
@@ -299,9 +315,16 @@ public class DocxSkill : IAgentTool
|
|||||||
return para;
|
return para;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>**bold**, *italic*, `code` 인라인 서식을 Run으로 변환</summary>
|
/// <summary>**bold**, *italic*, `code`, {icon:name} 인라인 서식을 Run으로 변환</summary>
|
||||||
private static void AppendFormattedRuns(Paragraph para, string text)
|
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` | 일반텍스트
|
// 패턴: **bold** | *italic* | `code` | 일반텍스트
|
||||||
var regex = new System.Text.RegularExpressions.Regex(
|
var regex = new System.Text.RegularExpressions.Regex(
|
||||||
@"\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`");
|
@"\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`");
|
||||||
@@ -331,7 +354,7 @@ public class DocxSkill : IAgentTool
|
|||||||
{
|
{
|
||||||
var run = CreateRun(match.Groups[3].Value);
|
var run = CreateRun(match.Groups[3].Value);
|
||||||
run.RunProperties ??= new RunProperties();
|
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.FontSize = new FontSize { Val = "20" };
|
||||||
run.RunProperties.Shading = new Shading
|
run.RunProperties.Shading = new Shading
|
||||||
{
|
{
|
||||||
@@ -360,7 +383,7 @@ public class DocxSkill : IAgentTool
|
|||||||
run.RunProperties = new RunProperties
|
run.RunProperties = new RunProperties
|
||||||
{
|
{
|
||||||
FontSize = new FontSize { Val = "22" }, // 11pt
|
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;
|
return run;
|
||||||
}
|
}
|
||||||
@@ -410,7 +433,7 @@ public class DocxSkill : IAgentTool
|
|||||||
Bold = new Bold(),
|
Bold = new Bold(),
|
||||||
FontSize = new FontSize { Val = "20" },
|
FontSize = new FontSize { Val = "20" },
|
||||||
Color = new Color { Val = "FFFFFF" },
|
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
|
para.ParagraphProperties = new ParagraphProperties
|
||||||
@@ -448,7 +471,7 @@ public class DocxSkill : IAgentTool
|
|||||||
RunProperties = new RunProperties
|
RunProperties = new RunProperties
|
||||||
{
|
{
|
||||||
FontSize = new FontSize { Val = "20" },
|
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
|
para.ParagraphProperties = new ParagraphProperties
|
||||||
@@ -503,7 +526,7 @@ public class DocxSkill : IAgentTool
|
|||||||
{
|
{
|
||||||
FontSize = new FontSize { Val = "22" },
|
FontSize = new FontSize { Val = "22" },
|
||||||
Bold = (listStyle == "number" && !isSub) ? new Bold() : null,
|
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);
|
para.Append(prefixRun);
|
||||||
|
|
||||||
@@ -511,7 +534,7 @@ public class DocxSkill : IAgentTool
|
|||||||
textRun.RunProperties = new RunProperties
|
textRun.RunProperties = new RunProperties
|
||||||
{
|
{
|
||||||
FontSize = new FontSize { Val = "22" },
|
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);
|
para.Append(textRun);
|
||||||
|
|
||||||
@@ -552,7 +575,7 @@ public class DocxSkill : IAgentTool
|
|||||||
Bold = new Bold(),
|
Bold = new Bold(),
|
||||||
FontSize = new FontSize { Val = "22" },
|
FontSize = new FontSize { Val = "22" },
|
||||||
Color = new Color { Val = borderColor },
|
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);
|
titlePara.Append(titleRun);
|
||||||
body.Append(titlePara);
|
body.Append(titlePara);
|
||||||
@@ -642,7 +665,7 @@ public class DocxSkill : IAgentTool
|
|||||||
{
|
{
|
||||||
var run = CreateRun(match.Groups[3].Value);
|
var run = CreateRun(match.Groups[3].Value);
|
||||||
run.RunProperties ??= new RunProperties();
|
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.FontSize = new FontSize { Val = "20" };
|
||||||
run.RunProperties.Shading = new Shading
|
run.RunProperties.Shading = new Shading
|
||||||
{
|
{
|
||||||
@@ -679,6 +702,57 @@ public class DocxSkill : IAgentTool
|
|||||||
// 머리글/바닥글
|
// 머리글/바닥글
|
||||||
// ═══════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>Body의 마지막에 SectionProperties를 추가합니다 (OOXML 규격: 반드시 마지막 자식).</summary>
|
||||||
|
private static void EnsureSectionProperties(Body body)
|
||||||
|
{
|
||||||
|
// 기존 sectPr이 마지막이 아닌 위치에 있으면 제거 후 재추가
|
||||||
|
var existing = body.GetFirstChild<SectionProperties>();
|
||||||
|
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 }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>DOCX 기본 스타일 정의 생성 (한글 글꼴 기본 설정 포함).</summary>
|
||||||
|
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,
|
private static void AddHeaderFooter(MainDocumentPart mainPart, Body body,
|
||||||
string? headerText, string? footerText, bool showPageNumbers)
|
string? headerText, string? footerText, bool showPageNumbers)
|
||||||
{
|
{
|
||||||
@@ -693,7 +767,7 @@ public class DocxSkill : IAgentTool
|
|||||||
{
|
{
|
||||||
FontSize = new FontSize { Val = "18" }, // 9pt
|
FontSize = new FontSize { Val = "18" }, // 9pt
|
||||||
Color = new Color { Val = "808080" },
|
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
|
para.ParagraphProperties = new ParagraphProperties
|
||||||
@@ -766,10 +840,44 @@ public class DocxSkill : IAgentTool
|
|||||||
{
|
{
|
||||||
FontSize = new FontSize { Val = "16" },
|
FontSize = new FontSize { Val = "16" },
|
||||||
Color = new Color { Val = "999999" },
|
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" },
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>아이콘 블록: 큰 심볼 + 선택적 라벨 텍스트.</summary>
|
||||||
|
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()
|
private static Run CreatePageNumberRun()
|
||||||
{
|
{
|
||||||
var run = new Run();
|
var run = new Run();
|
||||||
@@ -777,7 +885,7 @@ public class DocxSkill : IAgentTool
|
|||||||
{
|
{
|
||||||
FontSize = new FontSize { Val = "16" },
|
FontSize = new FontSize { Val = "16" },
|
||||||
Color = new Color { Val = "999999" },
|
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 FieldChar { FieldCharType = FieldCharValues.Begin });
|
||||||
run.Append(new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve });
|
run.Append(new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve });
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ internal static class DocxToHtmlConverter
|
|||||||
<meta charset='UTF-8'>
|
<meta charset='UTF-8'>
|
||||||
<style>
|
<style>
|
||||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||||
body {{ font-family: 'Malgun Gothic', 'Segoe UI', sans-serif; background: #fff; color: #1a1a1a;
|
body {{ font-family: 'Noto Sans KR', 'Segoe UI', sans-serif; background: #fff; color: #1a1a1a;
|
||||||
line-height: 1.7; padding: 32px 28px; font-size: 13.5px; }}
|
line-height: 1.7; padding: 32px 28px; font-size: 13.5px; }}
|
||||||
h1 {{ font-size: 22px; font-weight: 700; margin: 24px 0 10px; color: #111; }}
|
h1 {{ font-size: 22px; font-weight: 700; margin: 24px 0 10px; color: #111; }}
|
||||||
h1.title {{ font-size: 26px; text-align: center; margin-bottom: 4px; }}
|
h1.title {{ font-size: 26px; text-align: center; margin-bottom: 4px; }}
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ public class ExcelSkill : IAgentTool
|
|||||||
"Supports: header styling (bold white text on colored background), " +
|
"Supports: header styling (bold white text on colored background), " +
|
||||||
"striped rows, column auto-width, formulas (=SUM, =AVERAGE, etc), " +
|
"striped rows, column auto-width, formulas (=SUM, =AVERAGE, etc), " +
|
||||||
"cell merge, freeze panes (freeze header row), number formatting, " +
|
"cell merge, freeze panes (freeze header row), number formatting, " +
|
||||||
"themes (professional/modern/dark/minimal), column alignments, and multi-sheet workbooks.";
|
"themes (professional/modern/dark/minimal), column alignments, and multi-sheet workbooks. " +
|
||||||
|
"Inline icons: use {icon:name} in cell text (e.g. '{icon:checkmark} 완료', '{icon:warning} 주의'). " +
|
||||||
|
"170+ built-in icons: checkmark, warning, star, rocket, chart_up, chart_down, etc.";
|
||||||
|
|
||||||
public ToolParameterSchema Parameters => new()
|
public ToolParameterSchema Parameters => new()
|
||||||
{
|
{
|
||||||
@@ -340,12 +342,16 @@ public class ExcelSkill : IAgentTool
|
|||||||
|
|
||||||
var strVal = cellVal.ToString();
|
var strVal = cellVal.ToString();
|
||||||
|
|
||||||
|
// {icon:name} 인라인 아이콘 → 유니코드 심볼로 치환
|
||||||
|
if (strVal.Contains("{icon:"))
|
||||||
|
strVal = ResolveInlineIcons(strVal);
|
||||||
|
|
||||||
if (strVal.StartsWith('='))
|
if (strVal.StartsWith('='))
|
||||||
{
|
{
|
||||||
cell.CellFormula = new CellFormula(strVal);
|
cell.CellFormula = new CellFormula(strVal);
|
||||||
cell.DataType = null;
|
cell.DataType = null;
|
||||||
}
|
}
|
||||||
else if (cellVal.ValueKind == JsonValueKind.Number)
|
else if (cellVal.ValueKind == JsonValueKind.Number && !strVal.Contains("{icon:"))
|
||||||
{
|
{
|
||||||
cell.DataType = CellValues.Number;
|
cell.DataType = CellValues.Number;
|
||||||
cell.CellValue = new CellValue(cellVal.GetDouble().ToString());
|
cell.CellValue = new CellValue(cellVal.GetDouble().ToString());
|
||||||
@@ -500,18 +506,18 @@ public class ExcelSkill : IAgentTool
|
|||||||
var fonts = new Fonts(
|
var fonts = new Fonts(
|
||||||
new Font( // 0: default
|
new Font( // 0: default
|
||||||
new FontSize { Val = 11 },
|
new FontSize { Val = 11 },
|
||||||
new FontName { Val = "맑은 고딕" }
|
new FontName { Val = "Noto Sans KR" }
|
||||||
),
|
),
|
||||||
new Font( // 1: bold white (header)
|
new Font( // 1: bold white (header)
|
||||||
new Bold(),
|
new Bold(),
|
||||||
new FontSize { Val = 11 },
|
new FontSize { Val = 11 },
|
||||||
new Color { Rgb = theme.HeaderFg },
|
new Color { Rgb = theme.HeaderFg },
|
||||||
new FontName { Val = "맑은 고딕" }
|
new FontName { Val = "Noto Sans KR" }
|
||||||
),
|
),
|
||||||
new Font( // 2: bold (summary row)
|
new Font( // 2: bold (summary row)
|
||||||
new Bold(),
|
new Bold(),
|
||||||
new FontSize { Val = 11 },
|
new FontSize { Val = 11 },
|
||||||
new FontName { Val = "맑은 고딕" }
|
new FontName { Val = "Noto Sans KR" }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
stylesheet.Append(fonts);
|
stylesheet.Append(fonts);
|
||||||
@@ -799,4 +805,12 @@ public class ExcelSkill : IAgentTool
|
|||||||
|
|
||||||
private static string GetCellReference(int colIndex, int rowIndex)
|
private static string GetCellReference(int colIndex, int rowIndex)
|
||||||
=> $"{GetColumnLetter(colIndex)}{rowIndex + 1}";
|
=> $"{GetColumnLetter(colIndex)}{rowIndex + 1}";
|
||||||
|
|
||||||
|
/// <summary>{icon:name} 패턴을 유니코드 심볼로 치환합니다.</summary>
|
||||||
|
private static string ResolveInlineIcons(string 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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ public class FolderMapTool : IAgentTool
|
|||||||
public string Name => "folder_map";
|
public string Name => "folder_map";
|
||||||
public string Description =>
|
public string Description =>
|
||||||
"Generate a directory tree map of the work folder or a specified subfolder. " +
|
"Generate a directory tree map of the work folder or a specified subfolder. " +
|
||||||
"Shows folders and files in a tree structure. Use this to understand the project layout before reading or editing files. " +
|
"Shows folders and files in a tree structure. Use this only when the user explicitly asks for folder contents, directory structure, or workspace layout. " +
|
||||||
|
"Do not use it to locate a specific file when glob/grep or a targeted read would work. " +
|
||||||
"Supports sorting, size filtering, date filtering, and multi-extension filtering.";
|
"Supports sorting, size filtering, date filtering, and multi-extension filtering.";
|
||||||
|
|
||||||
public ToolParameterSchema Parameters => new()
|
public ToolParameterSchema Parameters => new()
|
||||||
@@ -22,7 +23,7 @@ public class FolderMapTool : IAgentTool
|
|||||||
{
|
{
|
||||||
["path"] = new() { Type = "string", Description = "Subdirectory to map. Optional, defaults to work folder root." },
|
["path"] = new() { Type = "string", Description = "Subdirectory to map. Optional, defaults to work folder root." },
|
||||||
["depth"] = new() { Type = "integer", Description = "Maximum depth to traverse (1-10). Default: 2." },
|
["depth"] = new() { Type = "integer", Description = "Maximum depth to traverse (1-10). Default: 2." },
|
||||||
["include_files"] = new() { Type = "boolean", Description = "Whether to include files. Default: false for conservative first-pass exploration." },
|
["include_files"] = new() { Type = "boolean", Description = "Whether to include files. Default: true." },
|
||||||
["pattern"] = new() { Type = "string", Description = "Single file extension filter (e.g. '.cs', '.py'). Optional. Use 'extensions' for multiple extensions." },
|
["pattern"] = new() { Type = "string", Description = "Single file extension filter (e.g. '.cs', '.py'). Optional. Use 'extensions' for multiple extensions." },
|
||||||
["extensions"] = new()
|
["extensions"] = new()
|
||||||
{
|
{
|
||||||
@@ -65,7 +66,7 @@ public class FolderMapTool : IAgentTool
|
|||||||
var maxDepth = Math.Min(depth, 10);
|
var maxDepth = Math.Min(depth, 10);
|
||||||
|
|
||||||
// ── include_files ─────────────────────────────────────────────────
|
// ── include_files ─────────────────────────────────────────────────
|
||||||
var includeFiles = false;
|
var includeFiles = true;
|
||||||
if (args.SafeTryGetProperty("include_files", out var inc))
|
if (args.SafeTryGetProperty("include_files", out var inc))
|
||||||
{
|
{
|
||||||
if (inc.ValueKind == JsonValueKind.True || inc.ValueKind == JsonValueKind.False)
|
if (inc.ValueKind == JsonValueKind.True || inc.ValueKind == JsonValueKind.False)
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ public class HtmlSkill : IAgentTool
|
|||||||
"'list' {style:'bullet|number', items:['item', ' - sub-item']}, " +
|
"'list' {style:'bullet|number', items:['item', ' - sub-item']}, " +
|
||||||
"'quote' {text, author}, " +
|
"'quote' {text, author}, " +
|
||||||
"'divider', " +
|
"'divider', " +
|
||||||
"'kpi' {items:[{label, value, change, positive:bool}]}. " +
|
"'kpi' {items:[{label, value, change, positive:bool}]}, " +
|
||||||
|
"'icon' {name:'checkmark|warning|rocket|...', text:'optional label', size:48}. " +
|
||||||
|
"Inline icons in any text: use {icon:name} syntax (e.g. '{icon:checkmark} Done'). " +
|
||||||
|
"170+ built-in icons available. " +
|
||||||
"When both body and sections are provided, sections are appended after body."
|
"When both body and sections are provided, sections are appended after body."
|
||||||
},
|
},
|
||||||
["mood"] = new() { Type = "string", Description = "Design template mood: modern, professional, creative, minimal, elegant, dark, colorful, corporate, magazine, dashboard. Default: modern" },
|
["mood"] = new() { Type = "string", Description = "Design template mood: modern, professional, creative, minimal, elegant, dark, colorful, corporate, magazine, dashboard. Default: modern" },
|
||||||
@@ -178,11 +181,16 @@ public class HtmlSkill : IAgentTool
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 커버가 없으면 기존 방식의 제목+메타
|
// 커버가 없으면 header-bar로 제목 표시
|
||||||
|
sb.AppendLine("<div class=\"header-bar\">");
|
||||||
sb.AppendLine($"<h1>{Escape(title)}</h1>");
|
sb.AppendLine($"<h1>{Escape(title)}</h1>");
|
||||||
sb.AppendLine($"<div class=\"meta\">생성: {DateTime.Now:yyyy-MM-dd HH:mm} | AX Copilot{moodLabel}</div>");
|
sb.AppendLine($"<div class=\"meta\">생성: {DateTime.Now:yyyy-MM-dd HH:mm} | AX Copilot{moodLabel}</div>");
|
||||||
|
sb.AppendLine("</div>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 본문을 body-content로 감싸서 좌우 여백 확보
|
||||||
|
sb.AppendLine("<div class=\"body-content\">");
|
||||||
|
|
||||||
// TOC
|
// TOC
|
||||||
if (!string.IsNullOrEmpty(tocHtml))
|
if (!string.IsNullOrEmpty(tocHtml))
|
||||||
sb.AppendLine(tocHtml);
|
sb.AppendLine(tocHtml);
|
||||||
@@ -198,7 +206,8 @@ public class HtmlSkill : IAgentTool
|
|||||||
"$1</div>");
|
"$1</div>");
|
||||||
sb.AppendLine(wrappedBody);
|
sb.AppendLine(wrappedBody);
|
||||||
|
|
||||||
sb.AppendLine("</div>");
|
sb.AppendLine("</div>"); // body-content
|
||||||
|
sb.AppendLine("</div>"); // container
|
||||||
sb.AppendLine("</body>");
|
sb.AppendLine("</body>");
|
||||||
sb.AppendLine("</html>");
|
sb.AppendLine("</html>");
|
||||||
|
|
||||||
@@ -268,6 +277,9 @@ public class HtmlSkill : IAgentTool
|
|||||||
case "kpi":
|
case "kpi":
|
||||||
sb.AppendLine(RenderKpi(section));
|
sb.AppendLine(RenderKpi(section));
|
||||||
break;
|
break;
|
||||||
|
case "icon":
|
||||||
|
sb.AppendLine(RenderIcon(section));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
@@ -549,6 +561,15 @@ public class HtmlSkill : IAgentTool
|
|||||||
// 7. 보존된 <br> 플레이스홀더를 복원
|
// 7. 보존된 <br> 플레이스홀더를 복원
|
||||||
text = text.Replace("\x00BR\x00", "<br>");
|
text = text.Replace("\x00BR\x00", "<br>");
|
||||||
|
|
||||||
|
// 8. {icon:name} 인라인 아이콘 → 유니코드 심볼 (span으로 감싸서 크기 조절)
|
||||||
|
text = Regex.Replace(text, @"\{icon:(\w+)\}", m =>
|
||||||
|
{
|
||||||
|
var name = m.Groups[1].Value;
|
||||||
|
if (!IconLibrary.Contains(name)) return m.Value;
|
||||||
|
var sym = IconLibrary.Resolve(name);
|
||||||
|
return $"<span class=\"inline-icon\" title=\"{name}\">{sym}</span>";
|
||||||
|
});
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -732,4 +753,21 @@ public class HtmlSkill : IAgentTool
|
|||||||
|
|
||||||
private static string Escape(string s) =>
|
private static string Escape(string s) =>
|
||||||
s.Replace("&", "&").Replace("<", "<").Replace(">", ">");
|
s.Replace("&", "&").Replace("<", "<").Replace(">", ">");
|
||||||
|
|
||||||
|
/// <summary>아이콘 블록: 큰 심볼 + 선택적 라벨 텍스트.</summary>
|
||||||
|
private static string RenderIcon(JsonElement s)
|
||||||
|
{
|
||||||
|
var name = s.SafeTryGetProperty("name", out var n) ? n.SafeGetString() ?? "" : "";
|
||||||
|
var label = s.SafeTryGetProperty("text", out var t) ? t.SafeGetString() ?? "" : "";
|
||||||
|
var size = s.SafeTryGetProperty("size", out var sz) ? sz.GetInt32() : 48;
|
||||||
|
var symbol = IconLibrary.Resolve(name);
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.Append($"<div class=\"icon-block\" style=\"display:flex;align-items:center;gap:12px;margin:16px 0\">");
|
||||||
|
sb.Append($"<span style=\"font-size:{size}px;line-height:1\">{symbol}</span>");
|
||||||
|
if (!string.IsNullOrWhiteSpace(label))
|
||||||
|
sb.Append($"<span style=\"font-size:1rem;color:#374151\">{MarkdownToHtml(label)}</span>");
|
||||||
|
sb.Append("</div>");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ public interface IAgentTool
|
|||||||
/// <summary>LLM function calling용 파라미터 JSON Schema.</summary>
|
/// <summary>LLM function calling용 파라미터 JSON Schema.</summary>
|
||||||
ToolParameterSchema Parameters { get; }
|
ToolParameterSchema Parameters { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 도구가 활성화되는 탭 카테고리.
|
||||||
|
/// null이면 모든 탭에서 사용 가능.
|
||||||
|
/// "Cowork" = Cowork 전용, "Code" = Code 전용, "Chat" = Chat 전용.
|
||||||
|
/// 쉼표 구분으로 복수 탭 지정 가능: "Cowork,Code".
|
||||||
|
/// </summary>
|
||||||
|
string? TabCategory => null;
|
||||||
|
|
||||||
/// <summary>도구를 실행하고 결과를 반환합니다.</summary>
|
/// <summary>도구를 실행하고 결과를 반환합니다.</summary>
|
||||||
/// <param name="args">LLM이 생성한 JSON 파라미터</param>
|
/// <param name="args">LLM이 생성한 JSON 파라미터</param>
|
||||||
/// <param name="context">실행 컨텍스트 (작업 폴더, 권한 등)</param>
|
/// <param name="context">실행 컨텍스트 (작업 폴더, 권한 등)</param>
|
||||||
@@ -171,16 +179,35 @@ public class AgentContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 작업 폴더 제한: 작업 폴더가 설정되어 있으면 하위 경로만 허용
|
// 작업 폴더 제한: 작업 폴더가 설정되어 있으면 하위 경로만 허용
|
||||||
|
// 사내 모드에서는 외부 경로도 사용자 승인 후 접근 가능 (CheckToolPermissionAsync에서 강제 승인 처리)
|
||||||
if (!string.IsNullOrEmpty(WorkFolder))
|
if (!string.IsNullOrEmpty(WorkFolder))
|
||||||
{
|
{
|
||||||
var workFull = Path.GetFullPath(WorkFolder);
|
var workFull = Path.GetFullPath(WorkFolder);
|
||||||
if (!fullPath.StartsWith(workFull, StringComparison.OrdinalIgnoreCase))
|
if (!fullPath.StartsWith(workFull, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// 사내 모드: 외부 경로는 IsPathAllowed에서 차단하지 않고 권한 검증 단계로 위임
|
||||||
|
if (AxCopilot.Services.OperationModePolicy.IsInternal(OperationMode))
|
||||||
|
return true; // CheckToolPermissionAsync에서 강제 승인 요청됨
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>경로가 워크스페이스 외부인지 확인합니다.</summary>
|
||||||
|
public bool IsOutsideWorkspace(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(WorkFolder)) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fullPath = Path.GetFullPath(path);
|
||||||
|
var workFull = Path.GetFullPath(WorkFolder);
|
||||||
|
return !fullPath.StartsWith(workFull, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
catch { return true; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 파일 경로에 타임스탬프를 추가합니다.
|
/// 파일 경로에 타임스탬프를 추가합니다.
|
||||||
/// 예: report.html → report_20260328_1430.html
|
/// 예: report.html → report_20260328_1430.html
|
||||||
@@ -241,6 +268,32 @@ public class AgentContext
|
|||||||
|
|
||||||
var effectivePerm = PermissionModeCatalog.NormalizeGlobalMode(GetEffectiveToolPermission(toolName, target));
|
var effectivePerm = PermissionModeCatalog.NormalizeGlobalMode(GetEffectiveToolPermission(toolName, target));
|
||||||
if (PermissionModeCatalog.IsDeny(effectivePerm)) return false;
|
if (PermissionModeCatalog.IsDeny(effectivePerm)) return false;
|
||||||
|
|
||||||
|
// ── 사내 모드 보안 강화: 워크스페이스 외부 경로 접근 시 무조건 사용자 승인 ──
|
||||||
|
// BypassPermissions / AcceptEdits 모드여도 외부 경로는 강제 승인 필요
|
||||||
|
if (AxCopilot.Services.OperationModePolicy.IsInternal(OperationMode)
|
||||||
|
&& !string.IsNullOrWhiteSpace(target)
|
||||||
|
&& IsOutsideWorkspace(target))
|
||||||
|
{
|
||||||
|
if (AskPermission == null) return false;
|
||||||
|
|
||||||
|
var extTarget = target.Trim();
|
||||||
|
var extCacheKey = $"ext_ws|{toolName}|{extTarget}";
|
||||||
|
lock (_permissionLock)
|
||||||
|
{
|
||||||
|
if (_approvedPermissionCache.Contains(extCacheKey))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var extAllowed = await AskPermission(toolName, extTarget);
|
||||||
|
if (extAllowed)
|
||||||
|
{
|
||||||
|
lock (_permissionLock)
|
||||||
|
_approvedPermissionCache.Add(extCacheKey);
|
||||||
|
}
|
||||||
|
return extAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
if (PermissionModeCatalog.IsAuto(effectivePerm)) return true;
|
if (PermissionModeCatalog.IsAuto(effectivePerm)) return true;
|
||||||
if (AskPermission == null) return false;
|
if (AskPermission == null) return false;
|
||||||
|
|
||||||
@@ -482,4 +535,5 @@ public enum AgentEventType
|
|||||||
StopRequested, // 중단 요청
|
StopRequested, // 중단 요청
|
||||||
Paused, // 에이전트 일시정지
|
Paused, // 에이전트 일시정지
|
||||||
Resumed, // 에이전트 재개
|
Resumed, // 에이전트 재개
|
||||||
|
UserMessage, // 실행 중 사용자 메시지 주입
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ namespace AxCopilot.Services.Agent;
|
|||||||
|
|
||||||
internal interface IToolExecutionCoordinator
|
internal interface IToolExecutionCoordinator
|
||||||
{
|
{
|
||||||
Task<LlmService.ToolPrefetchResult?> TryPrefetchReadOnlyToolAsync(
|
Task<ToolPrefetchResult?> TryPrefetchReadOnlyToolAsync(
|
||||||
LlmService.ContentBlock block,
|
ContentBlock block,
|
||||||
IReadOnlyCollection<IAgentTool> tools,
|
IReadOnlyCollection<IAgentTool> tools,
|
||||||
AgentContext context,
|
AgentContext context,
|
||||||
CancellationToken ct);
|
CancellationToken ct);
|
||||||
|
|
||||||
Task<List<LlmService.ContentBlock>> SendWithToolsWithRecoveryAsync(
|
Task<List<ContentBlock>> SendWithToolsWithRecoveryAsync(
|
||||||
List<ChatMessage> messages,
|
List<ChatMessage> messages,
|
||||||
IReadOnlyCollection<IAgentTool> tools,
|
IReadOnlyCollection<IAgentTool> tools,
|
||||||
CancellationToken ct,
|
CancellationToken ct,
|
||||||
string phaseLabel,
|
string phaseLabel,
|
||||||
AgentLoopService.RunState? runState = null,
|
AgentLoopService.RunState? runState = null,
|
||||||
bool forceToolCall = false,
|
bool forceToolCall = false,
|
||||||
Func<LlmService.ContentBlock, Task<LlmService.ToolPrefetchResult?>>? prefetchToolCallAsync = null,
|
Func<ContentBlock, Task<ToolPrefetchResult?>>? prefetchToolCallAsync = null,
|
||||||
Func<LlmService.ToolStreamEvent, Task>? onStreamEventAsync = null);
|
Func<ToolStreamEvent, Task>? onStreamEventAsync = null);
|
||||||
}
|
}
|
||||||
|
|||||||
240
src/AxCopilot/Services/Agent/IconLibrary.cs
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
namespace AxCopilot.Services.Agent;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 모든 문서 스킬(PPTX, DOCX, XLSX, HTML)에서 공유하는 내장 아이콘/심볼 라이브러리.
|
||||||
|
/// 유니코드 심볼 기반이므로 외부 파일 0바이트 — 폰트가 지원하는 한 어디서든 렌더링됩니다.
|
||||||
|
///
|
||||||
|
/// 사용 예:
|
||||||
|
/// var symbol = IconLibrary.Resolve("checkmark"); // "✔"
|
||||||
|
/// var symbol = IconLibrary.Resolve("rocket"); // "🚀"
|
||||||
|
/// var symbol = IconLibrary.Resolve("없는이름"); // "❓" (폴백)
|
||||||
|
/// </summary>
|
||||||
|
internal static class IconLibrary
|
||||||
|
{
|
||||||
|
/// <summary>아이콘 이름으로 유니코드 심볼 문자열을 반환합니다. 없으면 ❓.</summary>
|
||||||
|
public static string Resolve(string? iconName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(iconName)) return "";
|
||||||
|
return Symbols.TryGetValue(iconName.Trim(), out var symbol) ? symbol : "❓";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>아이콘 이름이 라이브러리에 존재하는지 확인합니다.</summary>
|
||||||
|
public static bool Contains(string? iconName)
|
||||||
|
=> !string.IsNullOrWhiteSpace(iconName) && Symbols.ContainsKey(iconName.Trim());
|
||||||
|
|
||||||
|
/// <summary>전체 아이콘 목록 (읽기 전용).</summary>
|
||||||
|
public static IReadOnlyDictionary<string, string> All => Symbols;
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 유니코드 심볼 매핑 (170종+)
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, string> Symbols = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
// ── 상태/알림 ──
|
||||||
|
["checkmark"] = "✔",
|
||||||
|
["check"] = "✔",
|
||||||
|
["check_box"] = "☑",
|
||||||
|
["warning"] = "⚠",
|
||||||
|
["info"] = "ℹ",
|
||||||
|
["error"] = "✖",
|
||||||
|
["question"] = "❓",
|
||||||
|
["exclamation"] = "❗",
|
||||||
|
["prohibited"] = "🚫",
|
||||||
|
["sos"] = "🆘",
|
||||||
|
["new"] = "🆕",
|
||||||
|
["free"] = "🆓",
|
||||||
|
["ok"] = "🆗",
|
||||||
|
["cool"] = "🆒",
|
||||||
|
["up"] = "🆙",
|
||||||
|
["vs"] = "🆚",
|
||||||
|
["100"] = "💯",
|
||||||
|
// ── 사람/소통 ──
|
||||||
|
["people"] = "👥",
|
||||||
|
["person"] = "👤",
|
||||||
|
["thumbs_up"] = "👍",
|
||||||
|
["thumbs_down"] = "👎",
|
||||||
|
["clap"] = "👏",
|
||||||
|
["handshake"] = "🤝",
|
||||||
|
["wave_hand"] = "👋",
|
||||||
|
["pray"] = "🙏",
|
||||||
|
["muscle"] = "💪",
|
||||||
|
["brain"] = "🧠",
|
||||||
|
["eye"] = "👁",
|
||||||
|
["speech"] = "💬",
|
||||||
|
["thought"] = "💭",
|
||||||
|
// ── 사무/업무 ──
|
||||||
|
["phone"] = "📞",
|
||||||
|
["email"] = "📧",
|
||||||
|
["inbox"] = "📥",
|
||||||
|
["outbox"] = "📤",
|
||||||
|
["document"] = "📄",
|
||||||
|
["documents"] = "📑",
|
||||||
|
["folder"] = "📁",
|
||||||
|
["folder_open"] = "📂",
|
||||||
|
["calendar"] = "📅",
|
||||||
|
["clipboard"] = "📋",
|
||||||
|
["pushpin"] = "📌",
|
||||||
|
["pin"] = "📌",
|
||||||
|
["paperclip"] = "📎",
|
||||||
|
["ruler"] = "📏",
|
||||||
|
["scissors"] = "✂",
|
||||||
|
["pen"] = "🖊",
|
||||||
|
["pencil"] = "✏",
|
||||||
|
["memo"] = "📝",
|
||||||
|
["book"] = "📖",
|
||||||
|
["books"] = "📚",
|
||||||
|
["notebook"] = "📓",
|
||||||
|
["newspaper"] = "📰",
|
||||||
|
["label"] = "🏷",
|
||||||
|
["tag"] = "🏷",
|
||||||
|
["package"] = "📦",
|
||||||
|
["mailbox"] = "📬",
|
||||||
|
["fax"] = "📠",
|
||||||
|
["printer"] = "🖨",
|
||||||
|
["computer"] = "💻",
|
||||||
|
["desktop"] = "🖥",
|
||||||
|
["keyboard"] = "⌨",
|
||||||
|
["mouse"] = "🖱",
|
||||||
|
// ── 시간/시계 ──
|
||||||
|
["clock"] = "🕐",
|
||||||
|
["hourglass"] = "⏳",
|
||||||
|
["stopwatch"] = "⏱",
|
||||||
|
["alarm"] = "⏰",
|
||||||
|
["timer"] = "⏲",
|
||||||
|
// ── 보안/잠금 ──
|
||||||
|
["lock"] = "🔒",
|
||||||
|
["unlock"] = "🔓",
|
||||||
|
["key"] = "🔑",
|
||||||
|
["shield"] = "🛡",
|
||||||
|
// ── 검색/탐색 ──
|
||||||
|
["search"] = "🔍",
|
||||||
|
["magnify_right"] = "🔎",
|
||||||
|
["binoculars"] = "🔭",
|
||||||
|
["compass"] = "🧭",
|
||||||
|
["map"] = "🗺",
|
||||||
|
["globe"] = "🌐",
|
||||||
|
["globe_asia"] = "🌏",
|
||||||
|
["globe_americas"] = "🌎",
|
||||||
|
["globe_europe"] = "🌍",
|
||||||
|
// ── 도구/설정 ──
|
||||||
|
["wrench"] = "🔧",
|
||||||
|
["hammer"] = "🔨",
|
||||||
|
["nut_bolt"] = "🔩",
|
||||||
|
["link"] = "🔗",
|
||||||
|
["chain"] = "⛓",
|
||||||
|
["refresh"] = "🔄",
|
||||||
|
["recycle"] = "♻",
|
||||||
|
["gear"] = "⚙",
|
||||||
|
// ── 성취/보상 ──
|
||||||
|
["trophy"] = "🏆",
|
||||||
|
["medal"] = "🏅",
|
||||||
|
["medal_gold"] = "🥇",
|
||||||
|
["medal_silver"] = "🥈",
|
||||||
|
["medal_bronze"] = "🥉",
|
||||||
|
["crown"] = "👑",
|
||||||
|
["gem"] = "💎",
|
||||||
|
// ── 감정/기호 ──
|
||||||
|
["lightbulb"] = "💡",
|
||||||
|
["target"] = "🎯",
|
||||||
|
["rocket"] = "🚀",
|
||||||
|
["fire"] = "🔥",
|
||||||
|
["bell"] = "🔔",
|
||||||
|
["sparkle"] = "✨",
|
||||||
|
["sparkles"] = "✨",
|
||||||
|
["rainbow"] = "🌈",
|
||||||
|
["confetti"] = "🎉",
|
||||||
|
["party"] = "🎊",
|
||||||
|
["balloon"] = "🎈",
|
||||||
|
["gift"] = "🎁",
|
||||||
|
["heart"] = "❤",
|
||||||
|
["star"] = "⭐",
|
||||||
|
["thumbs"] = "👍",
|
||||||
|
// ── 화살표/방향 ──
|
||||||
|
["arrow_right"] = "➡",
|
||||||
|
["arrow_left"] = "⬅",
|
||||||
|
["arrow_up"] = "⬆",
|
||||||
|
["arrow_down"] = "⬇",
|
||||||
|
["arrow_up_right"] = "↗",
|
||||||
|
["arrow_down_right"]= "↘",
|
||||||
|
["arrow_curved"] = "↩",
|
||||||
|
// ── 금융/비즈니스 ──
|
||||||
|
["money"] = "💰",
|
||||||
|
["money_bag"] = "💰",
|
||||||
|
["dollar"] = "💵",
|
||||||
|
["credit_card"] = "💳",
|
||||||
|
["receipt"] = "🧾",
|
||||||
|
["chart_up"] = "📈",
|
||||||
|
["chart_down"] = "📉",
|
||||||
|
["bar_chart"] = "📊",
|
||||||
|
// ── 교통/이동 ──
|
||||||
|
["car"] = "🚗",
|
||||||
|
["bus"] = "🚌",
|
||||||
|
["train"] = "🚆",
|
||||||
|
["airplane"] = "✈",
|
||||||
|
["ship"] = "🚢",
|
||||||
|
["bicycle"] = "🚲",
|
||||||
|
// ── 건물/장소 ──
|
||||||
|
["building"] = "🏢",
|
||||||
|
["hospital"] = "🏥",
|
||||||
|
["school"] = "🏫",
|
||||||
|
["factory"] = "🏭",
|
||||||
|
["house"] = "🏠",
|
||||||
|
["store"] = "🏪",
|
||||||
|
["bank"] = "🏦",
|
||||||
|
// ── 자연/과학 ──
|
||||||
|
["tree"] = "🌳",
|
||||||
|
["leaf"] = "🍃",
|
||||||
|
["flower"] = "🌸",
|
||||||
|
["seedling"] = "🌱",
|
||||||
|
["earth"] = "🌍",
|
||||||
|
["water"] = "💧",
|
||||||
|
["snowflake"] = "❄",
|
||||||
|
["atom"] = "⚛",
|
||||||
|
["dna"] = "🧬",
|
||||||
|
["microscope"] = "🔬",
|
||||||
|
["telescope"] = "🔭",
|
||||||
|
["test_tube"] = "🧪",
|
||||||
|
["petri_dish"] = "🧫",
|
||||||
|
// ── 음식 ──
|
||||||
|
["coffee"] = "☕",
|
||||||
|
["pizza"] = "🍕",
|
||||||
|
// ── 수학/기호 ──
|
||||||
|
["plus"] = "➕",
|
||||||
|
["minus"] = "➖",
|
||||||
|
["multiply"] = "✖",
|
||||||
|
["divide"] = "➗",
|
||||||
|
["infinity"] = "♾",
|
||||||
|
["copyright"] = "©",
|
||||||
|
["trademark"] = "™",
|
||||||
|
["registered"] = "®",
|
||||||
|
// ── 기타 유용 ──
|
||||||
|
["battery"] = "🔋",
|
||||||
|
["plug"] = "🔌",
|
||||||
|
["magnet"] = "🧲",
|
||||||
|
["crystal_ball"] = "🔮",
|
||||||
|
["palette"] = "🎨",
|
||||||
|
["movie"] = "🎬",
|
||||||
|
["music"] = "🎵",
|
||||||
|
["headphone"] = "🎧",
|
||||||
|
["camera"] = "📷",
|
||||||
|
["video"] = "📹",
|
||||||
|
["satellite"] = "📡",
|
||||||
|
["antenna"] = "📡",
|
||||||
|
["megaphone"] = "📢",
|
||||||
|
["loudspeaker"] = "📢",
|
||||||
|
["mute"] = "🔇",
|
||||||
|
["speaker"] = "🔊",
|
||||||
|
["zzz"] = "💤",
|
||||||
|
["smiley"] = "😊",
|
||||||
|
["cloud"] = "☁",
|
||||||
|
["sun"] = "☀",
|
||||||
|
["moon"] = "🌙",
|
||||||
|
["lightning"] = "⚡",
|
||||||
|
["flag"] = "🚩",
|
||||||
|
["home"] = "🏠",
|
||||||
|
["cross"] = "✝",
|
||||||
|
["diamond"] = "💠",
|
||||||
|
["no_symbol"] = "🚫",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -87,18 +87,18 @@ public static class ModelExecutionProfileCatalog
|
|||||||
DocumentPlanRetryMax: 2,
|
DocumentPlanRetryMax: 2,
|
||||||
PreferAggressiveDocumentFallback: false,
|
PreferAggressiveDocumentFallback: false,
|
||||||
ReduceEarlyMemoryPressure: false,
|
ReduceEarlyMemoryPressure: false,
|
||||||
EnablePostToolVerification: true,
|
EnablePostToolVerification: false,
|
||||||
EnableCodeQualityGates: true,
|
EnableCodeQualityGates: true,
|
||||||
EnableDocumentVerificationGate: true,
|
EnableDocumentVerificationGate: false,
|
||||||
EnableParallelReadBatch: true,
|
EnableParallelReadBatch: true,
|
||||||
MaxParallelReadBatch: 6,
|
MaxParallelReadBatch: 6,
|
||||||
CodeVerificationGateMaxRetries: 2,
|
CodeVerificationGateMaxRetries: 1,
|
||||||
HighImpactBuildTestGateMaxRetries: 1,
|
HighImpactBuildTestGateMaxRetries: 1,
|
||||||
FinalReportGateMaxRetries: 1,
|
FinalReportGateMaxRetries: 0,
|
||||||
CodeDiffGateMaxRetries: 1,
|
CodeDiffGateMaxRetries: 0,
|
||||||
RecentExecutionGateMaxRetries: 1,
|
RecentExecutionGateMaxRetries: 0,
|
||||||
ExecutionSuccessGateMaxRetries: 1,
|
ExecutionSuccessGateMaxRetries: 0,
|
||||||
DocumentVerificationGateMaxRetries: 1,
|
DocumentVerificationGateMaxRetries: 0,
|
||||||
TerminalEvidenceGateMaxRetries: 1),
|
TerminalEvidenceGateMaxRetries: 1),
|
||||||
"fast_readonly" => new ExecutionPolicy(
|
"fast_readonly" => new ExecutionPolicy(
|
||||||
"fast_readonly",
|
"fast_readonly",
|
||||||
@@ -128,8 +128,8 @@ public static class ModelExecutionProfileCatalog
|
|||||||
"document_heavy" => new ExecutionPolicy(
|
"document_heavy" => new ExecutionPolicy(
|
||||||
"document_heavy",
|
"document_heavy",
|
||||||
"문서 생성 우선",
|
"문서 생성 우선",
|
||||||
ForceInitialToolCall: true,
|
ForceInitialToolCall: false,
|
||||||
ForceToolCallAfterPlan: true,
|
ForceToolCallAfterPlan: false,
|
||||||
ToolTemperatureCap: 0.35,
|
ToolTemperatureCap: 0.35,
|
||||||
NoToolResponseThreshold: 1,
|
NoToolResponseThreshold: 1,
|
||||||
NoToolRecoveryMaxRetries: 1,
|
NoToolRecoveryMaxRetries: 1,
|
||||||
@@ -162,18 +162,18 @@ public static class ModelExecutionProfileCatalog
|
|||||||
DocumentPlanRetryMax: 2,
|
DocumentPlanRetryMax: 2,
|
||||||
PreferAggressiveDocumentFallback: false,
|
PreferAggressiveDocumentFallback: false,
|
||||||
ReduceEarlyMemoryPressure: false,
|
ReduceEarlyMemoryPressure: false,
|
||||||
EnablePostToolVerification: true,
|
EnablePostToolVerification: false,
|
||||||
EnableCodeQualityGates: true,
|
EnableCodeQualityGates: true,
|
||||||
EnableDocumentVerificationGate: true,
|
EnableDocumentVerificationGate: false,
|
||||||
EnableParallelReadBatch: true,
|
EnableParallelReadBatch: true,
|
||||||
MaxParallelReadBatch: 6,
|
MaxParallelReadBatch: 6,
|
||||||
CodeVerificationGateMaxRetries: 2,
|
CodeVerificationGateMaxRetries: 1,
|
||||||
HighImpactBuildTestGateMaxRetries: 1,
|
HighImpactBuildTestGateMaxRetries: 1,
|
||||||
FinalReportGateMaxRetries: 1,
|
FinalReportGateMaxRetries: 0,
|
||||||
CodeDiffGateMaxRetries: 1,
|
CodeDiffGateMaxRetries: 0,
|
||||||
RecentExecutionGateMaxRetries: 1,
|
RecentExecutionGateMaxRetries: 0,
|
||||||
ExecutionSuccessGateMaxRetries: 1,
|
ExecutionSuccessGateMaxRetries: 0,
|
||||||
DocumentVerificationGateMaxRetries: 1,
|
DocumentVerificationGateMaxRetries: 0,
|
||||||
TerminalEvidenceGateMaxRetries: 1),
|
TerminalEvidenceGateMaxRetries: 1),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ internal static class PptxToHtmlConverter
|
|||||||
<meta charset='UTF-8'>
|
<meta charset='UTF-8'>
|
||||||
<style>
|
<style>
|
||||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||||
body {{ font-family: 'Malgun Gothic', 'Segoe UI', sans-serif; background: #f3f4f6; color: #1a1a1a;
|
body {{ font-family: 'Noto Sans KR', 'Segoe UI', sans-serif; background: #f3f4f6; color: #1a1a1a;
|
||||||
line-height: 1.6; padding: 20px; font-size: 13px; }}
|
line-height: 1.6; padding: 20px; font-size: 13px; }}
|
||||||
.slide-info {{ color: #6b7280; font-size: 12px; margin-bottom: 16px; text-align: center; }}
|
.slide-info {{ color: #6b7280; font-size: 12px; margin-bottom: 16px; text-align: center; }}
|
||||||
.slide-card {{ background: #fff; border-radius: 12px; padding: 28px 32px; margin-bottom: 16px;
|
.slide-card {{ background: #fff; border-radius: 12px; padding: 28px 32px; margin-bottom: 16px;
|
||||||
|
|||||||
@@ -282,8 +282,9 @@ public static class SkillService
|
|||||||
사용자가 지정한 파일 또는 작업 폴더의 주요 파일을 읽고 상세히 설명하세요.
|
사용자가 지정한 파일 또는 작업 폴더의 주요 파일을 읽고 상세히 설명하세요.
|
||||||
|
|
||||||
다음 도구를 사용하세요:
|
다음 도구를 사용하세요:
|
||||||
1. file_read — 파일 내용 읽기
|
1. glob/grep — 관련 파일 후보 찾기
|
||||||
2. folder_map — 프로젝트 구조 파악 (필요시)
|
2. file_read — 파일 내용 읽기
|
||||||
|
3. folder_map — 프로젝트 구조가 꼭 필요할 때만 사용
|
||||||
|
|
||||||
설명 포함 사항:
|
설명 포함 사항:
|
||||||
- 파일의 역할과 책임
|
- 파일의 역할과 책임
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ public class TemplateRenderTool : IAgentTool
|
|||||||
await TextFileCodec.WriteAllTextAsync(outputPath, rendered, TextFileCodec.Utf8NoBom, ct);
|
await TextFileCodec.WriteAllTextAsync(outputPath, rendered, TextFileCodec.Utf8NoBom, ct);
|
||||||
|
|
||||||
return ToolResult.Ok(
|
return ToolResult.Ok(
|
||||||
$"✅ 템플릿 렌더링 완료: {Path.GetFileName(outputPath)} ({rendered.Length:N0}자)",
|
$"✅ 템플릿 렌더링 완료: {outputPath} ({rendered.Length:N0}자)",
|
||||||
outputPath);
|
outputPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ public static class TemplateService
|
|||||||
private const string CssModern = """
|
private const string CssModern = """
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
* { margin:0; padding:0; box-sizing:border-box; }
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
body { font-family: 'Inter', 'Segoe UI', 'Malgun Gothic', sans-serif;
|
body { font-family: 'Inter', 'Segoe UI', 'Noto Sans KR', sans-serif;
|
||||||
background: #f5f5f7; color: #1d1d1f; line-height: 1.75; padding: 48px 24px; }
|
background: #f5f5f7; color: #1d1d1f; line-height: 1.75; padding: 48px 24px; }
|
||||||
.container { max-width: 1080px; margin: 0 auto; background: #fff;
|
.container { max-width: 1080px; margin: 0 auto; background: #fff;
|
||||||
border-radius: 16px; padding: 56px 52px;
|
border-radius: 16px; padding: 56px 52px;
|
||||||
@@ -218,7 +218,7 @@ public static class TemplateService
|
|||||||
#region Professional — 전문가
|
#region Professional — 전문가
|
||||||
private const string CssProfessional = """
|
private const string CssProfessional = """
|
||||||
* { margin:0; padding:0; box-sizing:border-box; }
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
body { font-family: 'Segoe UI', 'Malgun Gothic', Arial, sans-serif;
|
body { font-family: 'Segoe UI', 'Noto Sans KR', Arial, sans-serif;
|
||||||
background: #eef1f5; color: #1e293b; line-height: 1.7; padding: 40px 20px; }
|
background: #eef1f5; color: #1e293b; line-height: 1.7; padding: 40px 20px; }
|
||||||
.container { max-width: 1080px; margin: 0 auto; background: #fff;
|
.container { max-width: 1080px; margin: 0 auto; background: #fff;
|
||||||
border-radius: 8px; padding: 48px;
|
border-radius: 8px; padding: 48px;
|
||||||
@@ -257,7 +257,7 @@ public static class TemplateService
|
|||||||
private const string CssCreative = """
|
private const string CssCreative = """
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
|
||||||
* { margin:0; padding:0; box-sizing:border-box; }
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
body { font-family: 'Poppins', 'Segoe UI', 'Malgun Gothic', sans-serif;
|
body { font-family: 'Poppins', 'Segoe UI', 'Noto Sans KR', sans-serif;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
min-height: 100vh; color: #2d3748; line-height: 1.75; padding: 48px 24px; }
|
min-height: 100vh; color: #2d3748; line-height: 1.75; padding: 48px 24px; }
|
||||||
.container { max-width: 1080px; margin: 0 auto; background: rgba(255,255,255,0.95);
|
.container { max-width: 1080px; margin: 0 auto; background: rgba(255,255,255,0.95);
|
||||||
@@ -336,7 +336,7 @@ public static class TemplateService
|
|||||||
private const string CssElegant = """
|
private const string CssElegant = """
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700&family=Source+Sans+3:wght@300;400;600&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700&family=Source+Sans+3:wght@300;400;600&display=swap');
|
||||||
* { margin:0; padding:0; box-sizing:border-box; }
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
body { font-family: 'Source Sans 3', 'Malgun Gothic', sans-serif;
|
body { font-family: 'Source Sans 3', 'Noto Sans KR', sans-serif;
|
||||||
background: #faf8f5; color: #3d3929; line-height: 1.75; padding: 48px 24px; }
|
background: #faf8f5; color: #3d3929; line-height: 1.75; padding: 48px 24px; }
|
||||||
.container { max-width: 1080px; margin: 0 auto; background: #fff;
|
.container { max-width: 1080px; margin: 0 auto; background: #fff;
|
||||||
border-radius: 4px; padding: 56px 52px;
|
border-radius: 4px; padding: 56px 52px;
|
||||||
@@ -375,7 +375,7 @@ public static class TemplateService
|
|||||||
private const string CssDark = """
|
private const string CssDark = """
|
||||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Inter:wght@300;400;500;600;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
* { margin:0; padding:0; box-sizing:border-box; }
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
body { font-family: 'Inter', 'Segoe UI', 'Malgun Gothic', sans-serif;
|
body { font-family: 'Inter', 'Segoe UI', 'Noto Sans KR', sans-serif;
|
||||||
background: #0d1117; color: #e6edf3; line-height: 1.75; padding: 48px 24px; }
|
background: #0d1117; color: #e6edf3; line-height: 1.75; padding: 48px 24px; }
|
||||||
.container { max-width: 1080px; margin: 0 auto; background: #161b22;
|
.container { max-width: 1080px; margin: 0 auto; background: #161b22;
|
||||||
border-radius: 12px; padding: 52px;
|
border-radius: 12px; padding: 52px;
|
||||||
@@ -415,7 +415,7 @@ public static class TemplateService
|
|||||||
private const string CssColorful = """
|
private const string CssColorful = """
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;600;700;800&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;600;700;800&display=swap');
|
||||||
* { margin:0; padding:0; box-sizing:border-box; }
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
body { font-family: 'Nunito', 'Segoe UI', 'Malgun Gothic', sans-serif;
|
body { font-family: 'Nunito', 'Segoe UI', 'Noto Sans KR', sans-serif;
|
||||||
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 50%, #ffecd2 100%);
|
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 50%, #ffecd2 100%);
|
||||||
min-height: 100vh; color: #2d3436; line-height: 1.75; padding: 48px 24px; }
|
min-height: 100vh; color: #2d3436; line-height: 1.75; padding: 48px 24px; }
|
||||||
.container { max-width: 1080px; margin: 0 auto; background: #fff;
|
.container { max-width: 1080px; margin: 0 auto; background: #fff;
|
||||||
@@ -455,7 +455,7 @@ public static class TemplateService
|
|||||||
#region Corporate — 기업 공식
|
#region Corporate — 기업 공식
|
||||||
private const string CssCorporate = """
|
private const string CssCorporate = """
|
||||||
* { margin:0; padding:0; box-sizing:border-box; }
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
body { font-family: 'Segoe UI', 'Malgun Gothic', Arial, sans-serif;
|
body { font-family: 'Segoe UI', 'Noto Sans KR', Arial, sans-serif;
|
||||||
background: #f2f2f2; color: #333; line-height: 1.65; padding: 32px 20px; }
|
background: #f2f2f2; color: #333; line-height: 1.65; padding: 32px 20px; }
|
||||||
.container { max-width: 1080px; margin: 0 auto; background: #fff; padding: 0;
|
.container { max-width: 1080px; margin: 0 auto; background: #fff; padding: 0;
|
||||||
box-shadow: 0 1px 4px rgba(0,0,0,0.1); }
|
box-shadow: 0 1px 4px rgba(0,0,0,0.1); }
|
||||||
@@ -464,6 +464,9 @@ public static class TemplateService
|
|||||||
.header-bar h1 { font-size: 22px; font-weight: 700; color: #fff; margin-bottom: 2px; }
|
.header-bar h1 { font-size: 22px; font-weight: 700; color: #fff; margin-bottom: 2px; }
|
||||||
.header-bar .meta { color: rgba(255,255,255,0.7); margin-bottom: 0; font-size: 12px; }
|
.header-bar .meta { color: rgba(255,255,255,0.7); margin-bottom: 0; font-size: 12px; }
|
||||||
.body-content { padding: 36px 40px 40px; }
|
.body-content { padding: 36px 40px 40px; }
|
||||||
|
.cover-page { margin: -36px -40px 40px -40px !important; border-radius: 0 !important;
|
||||||
|
width: auto !important; box-sizing: border-box !important;
|
||||||
|
left: 0; right: 0; }
|
||||||
h1 { font-size: 22px; font-weight: 700; color: #003366; margin-bottom: 4px; }
|
h1 { font-size: 22px; font-weight: 700; color: #003366; margin-bottom: 4px; }
|
||||||
h2 { font-size: 17px; font-weight: 600; margin: 28px 0 10px; color: #003366;
|
h2 { font-size: 17px; font-weight: 600; margin: 28px 0 10px; color: #003366;
|
||||||
border-left: 4px solid #ff6600; padding-left: 12px; }
|
border-left: 4px solid #ff6600; padding-left: 12px; }
|
||||||
@@ -497,7 +500,7 @@ public static class TemplateService
|
|||||||
private const string CssMagazine = """
|
private const string CssMagazine = """
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Merriweather:wght@300;400;700;900&family=Open+Sans:wght@300;400;600;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Merriweather:wght@300;400;700;900&family=Open+Sans:wght@300;400;600;700&display=swap');
|
||||||
* { margin:0; padding:0; box-sizing:border-box; }
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
body { font-family: 'Open Sans', 'Malgun Gothic', sans-serif;
|
body { font-family: 'Open Sans', 'Noto Sans KR', sans-serif;
|
||||||
background: #f0ece3; color: #2c2c2c; line-height: 1.7; padding: 48px 24px; }
|
background: #f0ece3; color: #2c2c2c; line-height: 1.7; padding: 48px 24px; }
|
||||||
.container { max-width: 1080px; margin: 0 auto; background: #fff;
|
.container { max-width: 1080px; margin: 0 auto; background: #fff;
|
||||||
border-radius: 2px; padding: 0; overflow: hidden;
|
border-radius: 2px; padding: 0; overflow: hidden;
|
||||||
@@ -548,7 +551,7 @@ public static class TemplateService
|
|||||||
private const string CssDashboard = """
|
private const string CssDashboard = """
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
* { margin:0; padding:0; box-sizing:border-box; }
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
body { font-family: 'Inter', 'Segoe UI', 'Malgun Gothic', sans-serif;
|
body { font-family: 'Inter', 'Segoe UI', 'Noto Sans KR', sans-serif;
|
||||||
background: #f0f2f5; color: #1a1a2e; line-height: 1.6; padding: 32px 24px; }
|
background: #f0f2f5; color: #1a1a2e; line-height: 1.6; padding: 32px 24px; }
|
||||||
.container { max-width: 1000px; margin: 0 auto; padding: 0; background: transparent; }
|
.container { max-width: 1000px; margin: 0 auto; padding: 0; background: transparent; }
|
||||||
h1 { font-size: 26px; font-weight: 700; color: #1a1a2e; margin-bottom: 4px; }
|
h1 { font-size: 26px; font-weight: 700; color: #1a1a2e; margin-bottom: 4px; }
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ internal static class XlsxToHtmlConverter
|
|||||||
<meta charset='UTF-8'>
|
<meta charset='UTF-8'>
|
||||||
<style>
|
<style>
|
||||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||||
body {{ font-family: 'Malgun Gothic', 'Segoe UI', sans-serif; background: #fff; color: #1a1a1a;
|
body {{ font-family: 'Noto Sans KR', 'Segoe UI', sans-serif; background: #fff; color: #1a1a1a;
|
||||||
line-height: 1.5; padding: 16px; font-size: 12.5px; }}
|
line-height: 1.5; padding: 16px; font-size: 12.5px; }}
|
||||||
.sheet-tabs {{ display: flex; gap: 2px; margin-bottom: 12px; border-bottom: 2px solid #e5e7eb; padding-bottom: 0; }}
|
.sheet-tabs {{ display: flex; gap: 2px; margin-bottom: 12px; border-bottom: 2px solid #e5e7eb; padding-bottom: 0; }}
|
||||||
.sheet-tab {{ padding: 6px 14px; border: none; background: #f3f4f6; cursor: pointer;
|
.sheet-tab {{ padding: 6px 14px; border: none; background: #f3f4f6; cursor: pointer;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace AxCopilot.Services;
|
|||||||
/// 채팅 세션 외의 스킬, MCP, 권한, 도구 카탈로그 상태를 한 곳에 모아
|
/// 채팅 세션 외의 스킬, MCP, 권한, 도구 카탈로그 상태를 한 곳에 모아
|
||||||
/// 창별 로컬 필드 의존도를 줄이는 1차 계층입니다.
|
/// 창별 로컬 필드 의존도를 줄이는 1차 계층입니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class AppStateService
|
public sealed class AppStateService : IAppStateService
|
||||||
{
|
{
|
||||||
public sealed class SkillCatalogState
|
public sealed class SkillCatalogState
|
||||||
{
|
{
|
||||||
@@ -212,7 +212,7 @@ public sealed class AppStateService
|
|||||||
NotifyStateChanged();
|
NotifyStateChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadFromSettings(SettingsService settings)
|
public void LoadFromSettings(ISettingsService settings)
|
||||||
{
|
{
|
||||||
var llm = settings.Settings.Llm;
|
var llm = settings.Settings.Llm;
|
||||||
Skills.Enabled = llm.EnableSkillSystem;
|
Skills.Enabled = llm.EnableSkillSystem;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public sealed class ChatSessionStateService
|
|||||||
return CurrentConversation;
|
return CurrentConversation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Load(SettingsService settings)
|
public void Load(ISettingsService settings)
|
||||||
{
|
{
|
||||||
var llm = settings.Settings.Llm;
|
var llm = settings.Settings.Llm;
|
||||||
ActiveTab = NormalizeTab(llm.LastActiveTab);
|
ActiveTab = NormalizeTab(llm.LastActiveTab);
|
||||||
@@ -67,7 +67,7 @@ public sealed class ChatSessionStateService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Save(SettingsService settings)
|
public void Save(ISettingsService settings)
|
||||||
{
|
{
|
||||||
var llm = settings.Settings.Llm;
|
var llm = settings.Settings.Llm;
|
||||||
llm.LastActiveTab = NormalizeTab(ActiveTab);
|
llm.LastActiveTab = NormalizeTab(ActiveTab);
|
||||||
@@ -87,7 +87,7 @@ public sealed class ChatSessionStateService
|
|||||||
TabConversationIds[NormalizeTab(tab)] = string.IsNullOrWhiteSpace(conversationId) ? null : conversationId;
|
TabConversationIds[NormalizeTab(tab)] = string.IsNullOrWhiteSpace(conversationId) ? null : conversationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChatConversation LoadOrCreateConversation(string tab, ChatStorageService storage, SettingsService settings)
|
public ChatConversation LoadOrCreateConversation(string tab, IChatStorageService storage, ISettingsService settings)
|
||||||
{
|
{
|
||||||
var normalizedTab = NormalizeTab(tab);
|
var normalizedTab = NormalizeTab(tab);
|
||||||
var rememberedId = GetConversationId(normalizedTab);
|
var rememberedId = GetConversationId(normalizedTab);
|
||||||
@@ -153,13 +153,19 @@ public sealed class ChatSessionStateService
|
|||||||
return CreateFreshConversation(normalizedTab, settings);
|
return CreateFreshConversation(normalizedTab, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChatConversation CreateFreshConversation(string tab, SettingsService settings)
|
public ChatConversation CreateFreshConversation(string tab, ISettingsService settings)
|
||||||
{
|
{
|
||||||
var normalizedTab = NormalizeTab(tab);
|
var normalizedTab = NormalizeTab(tab);
|
||||||
var created = new ChatConversation { Tab = normalizedTab };
|
var created = new ChatConversation { Tab = normalizedTab };
|
||||||
var workFolder = settings.Settings.Llm.WorkFolder;
|
|
||||||
if (!string.IsNullOrWhiteSpace(workFolder) && normalizedTab != "Chat")
|
// Code/Cowork 탭: 매 대화마다 폴더를 새로 선택하도록 빈 상태로 시작
|
||||||
created.WorkFolder = workFolder;
|
if (string.Equals(normalizedTab, "Code", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(normalizedTab, "Cowork", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
created.WorkFolder = "";
|
||||||
|
CurrentConversation = created;
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
CurrentConversation = created;
|
CurrentConversation = created;
|
||||||
return created;
|
return created;
|
||||||
@@ -239,7 +245,7 @@ public sealed class ChatSessionStateService
|
|||||||
return fork;
|
return fork;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveCurrentConversation(ChatStorageService storage, string tab)
|
public void SaveCurrentConversation(IChatStorageService storage, string tab)
|
||||||
{
|
{
|
||||||
var conv = CurrentConversation;
|
var conv = CurrentConversation;
|
||||||
if (conv == null) return;
|
if (conv == null) return;
|
||||||
@@ -261,7 +267,7 @@ public sealed class ChatSessionStateService
|
|||||||
RememberConversation(tab, null);
|
RememberConversation(tab, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChatConversation SetCurrentConversation(string tab, ChatConversation conversation, ChatStorageService? storage = null, bool remember = true)
|
public ChatConversation SetCurrentConversation(string tab, ChatConversation conversation, IChatStorageService? storage = null, bool remember = true)
|
||||||
{
|
{
|
||||||
var normalizedTab = NormalizeTab(tab);
|
var normalizedTab = NormalizeTab(tab);
|
||||||
conversation.Tab = normalizedTab;
|
conversation.Tab = normalizedTab;
|
||||||
@@ -275,7 +281,7 @@ public sealed class ChatSessionStateService
|
|||||||
return conversation;
|
return conversation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChatMessage AppendMessage(string tab, ChatMessage message, ChatStorageService? storage = null, bool useForTitle = false)
|
public ChatMessage AppendMessage(string tab, ChatMessage message, IChatStorageService? storage = null, bool useForTitle = false)
|
||||||
{
|
{
|
||||||
var conv = EnsureCurrentConversation(tab);
|
var conv = EnsureCurrentConversation(tab);
|
||||||
conv.Messages.Add(message);
|
conv.Messages.Add(message);
|
||||||
@@ -294,7 +300,7 @@ public sealed class ChatSessionStateService
|
|||||||
public ChatConversation UpdateConversationMetadata(
|
public ChatConversation UpdateConversationMetadata(
|
||||||
string tab,
|
string tab,
|
||||||
Action<ChatConversation> apply,
|
Action<ChatConversation> apply,
|
||||||
ChatStorageService? storage = null,
|
IChatStorageService? storage = null,
|
||||||
bool ensureConversation = true)
|
bool ensureConversation = true)
|
||||||
{
|
{
|
||||||
var conv = ensureConversation ? EnsureCurrentConversation(tab) : (CurrentConversation ?? new ChatConversation { Tab = NormalizeTab(tab) });
|
var conv = ensureConversation ? EnsureCurrentConversation(tab) : (CurrentConversation ?? new ChatConversation { Tab = NormalizeTab(tab) });
|
||||||
@@ -311,7 +317,7 @@ public sealed class ChatSessionStateService
|
|||||||
string? dataUsage,
|
string? dataUsage,
|
||||||
string? outputFormat,
|
string? outputFormat,
|
||||||
string? mood,
|
string? mood,
|
||||||
ChatStorageService? storage = null)
|
IChatStorageService? storage = null)
|
||||||
{
|
{
|
||||||
return UpdateConversationMetadata(tab, conv =>
|
return UpdateConversationMetadata(tab, conv =>
|
||||||
{
|
{
|
||||||
@@ -322,7 +328,7 @@ public sealed class ChatSessionStateService
|
|||||||
}, storage);
|
}, storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool RemoveLastAssistantMessage(string tab, ChatStorageService? storage = null)
|
public bool RemoveLastAssistantMessage(string tab, IChatStorageService? storage = null)
|
||||||
{
|
{
|
||||||
var conv = CurrentConversation;
|
var conv = CurrentConversation;
|
||||||
if (conv == null || conv.Messages.Count == 0)
|
if (conv == null || conv.Messages.Count == 0)
|
||||||
@@ -336,7 +342,7 @@ public sealed class ChatSessionStateService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool UpdateUserMessageAndTrim(string tab, int userMessageIndex, string newText, ChatStorageService? storage = null)
|
public bool UpdateUserMessageAndTrim(string tab, int userMessageIndex, string newText, IChatStorageService? storage = null)
|
||||||
{
|
{
|
||||||
var conv = CurrentConversation;
|
var conv = CurrentConversation;
|
||||||
if (conv == null)
|
if (conv == null)
|
||||||
@@ -353,7 +359,7 @@ public sealed class ChatSessionStateService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool UpdateMessageFeedback(string tab, ChatMessage message, string? feedback, ChatStorageService? storage = null)
|
public bool UpdateMessageFeedback(string tab, ChatMessage message, string? feedback, IChatStorageService? storage = null)
|
||||||
{
|
{
|
||||||
var conv = CurrentConversation;
|
var conv = CurrentConversation;
|
||||||
if (conv == null)
|
if (conv == null)
|
||||||
@@ -368,7 +374,7 @@ public sealed class ChatSessionStateService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChatConversation AppendExecutionEvent(string tab, Agent.AgentEvent evt, ChatStorageService? storage = null)
|
public ChatConversation AppendExecutionEvent(string tab, Agent.AgentEvent evt, IChatStorageService? storage = null)
|
||||||
{
|
{
|
||||||
var conv = EnsureCurrentConversation(tab);
|
var conv = EnsureCurrentConversation(tab);
|
||||||
conv.ExecutionEvents ??= new List<ChatExecutionEvent>();
|
conv.ExecutionEvents ??= new List<ChatExecutionEvent>();
|
||||||
@@ -417,7 +423,7 @@ public sealed class ChatSessionStateService
|
|||||||
return conv;
|
return conv;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChatConversation AppendAgentRun(string tab, Agent.AgentEvent evt, string status, string summary, ChatStorageService? storage = null)
|
public ChatConversation AppendAgentRun(string tab, Agent.AgentEvent evt, string status, string summary, IChatStorageService? storage = null)
|
||||||
{
|
{
|
||||||
var conv = EnsureCurrentConversation(tab);
|
var conv = EnsureCurrentConversation(tab);
|
||||||
conv.AgentRunHistory ??= new List<ChatAgentRunRecord>();
|
conv.AgentRunHistory ??= new List<ChatAgentRunRecord>();
|
||||||
@@ -448,7 +454,7 @@ public sealed class ChatSessionStateService
|
|||||||
return conv;
|
return conv;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DraftQueueItem? EnqueueDraft(string tab, string text, string priority = "next", ChatStorageService? storage = null, string kind = "message")
|
public DraftQueueItem? EnqueueDraft(string tab, string text, string priority = "next", IChatStorageService? storage = null, string kind = "message")
|
||||||
{
|
{
|
||||||
var trimmed = text?.Trim() ?? "";
|
var trimmed = text?.Trim() ?? "";
|
||||||
if (string.IsNullOrWhiteSpace(trimmed))
|
if (string.IsNullOrWhiteSpace(trimmed))
|
||||||
@@ -480,16 +486,16 @@ public sealed class ChatSessionStateService
|
|||||||
return (conv.DraftQueueItems ?? []).ToList();
|
return (conv.DraftQueueItems ?? []).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool MarkDraftRunning(string tab, string draftId, ChatStorageService? storage = null)
|
public bool MarkDraftRunning(string tab, string draftId, IChatStorageService? storage = null)
|
||||||
=> UpdateDraftItem(tab, draftId, item => _draftQueue.MarkRunning(item), storage);
|
=> UpdateDraftItem(tab, draftId, item => _draftQueue.MarkRunning(item), storage);
|
||||||
|
|
||||||
public bool MarkDraftCompleted(string tab, string draftId, ChatStorageService? storage = null)
|
public bool MarkDraftCompleted(string tab, string draftId, IChatStorageService? storage = null)
|
||||||
=> UpdateDraftItem(tab, draftId, item => _draftQueue.MarkCompleted(item), storage);
|
=> UpdateDraftItem(tab, draftId, item => _draftQueue.MarkCompleted(item), storage);
|
||||||
|
|
||||||
public bool MarkDraftFailed(string tab, string draftId, string? error, ChatStorageService? storage = null)
|
public bool MarkDraftFailed(string tab, string draftId, string? error, IChatStorageService? storage = null)
|
||||||
=> UpdateDraftItem(tab, draftId, item => _draftQueue.MarkFailed(item, error), storage);
|
=> UpdateDraftItem(tab, draftId, item => _draftQueue.MarkFailed(item, error), storage);
|
||||||
|
|
||||||
public bool ScheduleDraftRetry(string tab, string draftId, string? error, int maxAutoRetries = 3, ChatStorageService? storage = null)
|
public bool ScheduleDraftRetry(string tab, string draftId, string? error, int maxAutoRetries = 3, IChatStorageService? storage = null)
|
||||||
{
|
{
|
||||||
return UpdateDraftItem(tab, draftId, item =>
|
return UpdateDraftItem(tab, draftId, item =>
|
||||||
{
|
{
|
||||||
@@ -503,10 +509,10 @@ public sealed class ChatSessionStateService
|
|||||||
}, storage);
|
}, storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ResetDraftToQueued(string tab, string draftId, ChatStorageService? storage = null)
|
public bool ResetDraftToQueued(string tab, string draftId, IChatStorageService? storage = null)
|
||||||
=> UpdateDraftItem(tab, draftId, item => _draftQueue.ResetToQueued(item), storage);
|
=> UpdateDraftItem(tab, draftId, item => _draftQueue.ResetToQueued(item), storage);
|
||||||
|
|
||||||
public bool RemoveDraft(string tab, string draftId, ChatStorageService? storage = null)
|
public bool RemoveDraft(string tab, string draftId, IChatStorageService? storage = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(draftId))
|
if (string.IsNullOrWhiteSpace(draftId))
|
||||||
return false;
|
return false;
|
||||||
@@ -520,7 +526,7 @@ public sealed class ChatSessionStateService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ToggleExecutionHistory(string tab, ChatStorageService? storage = null)
|
public bool ToggleExecutionHistory(string tab, IChatStorageService? storage = null)
|
||||||
{
|
{
|
||||||
var conv = EnsureCurrentConversation(tab);
|
var conv = EnsureCurrentConversation(tab);
|
||||||
conv.ShowExecutionHistory = !conv.ShowExecutionHistory;
|
conv.ShowExecutionHistory = !conv.ShowExecutionHistory;
|
||||||
@@ -528,7 +534,7 @@ public sealed class ChatSessionStateService
|
|||||||
return conv.ShowExecutionHistory;
|
return conv.ShowExecutionHistory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveConversationListPreferences(string tab, bool failedOnly, bool runningOnly, bool sortByRecent, ChatStorageService? storage = null)
|
public void SaveConversationListPreferences(string tab, bool failedOnly, bool runningOnly, bool sortByRecent, IChatStorageService? storage = null)
|
||||||
{
|
{
|
||||||
var conv = EnsureCurrentConversation(tab);
|
var conv = EnsureCurrentConversation(tab);
|
||||||
conv.ConversationFailedOnlyFilter = failedOnly;
|
conv.ConversationFailedOnlyFilter = failedOnly;
|
||||||
@@ -758,7 +764,7 @@ public sealed class ChatSessionStateService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TouchConversation(ChatStorageService? storage, string tab)
|
private void TouchConversation(IChatStorageService? storage, string tab)
|
||||||
{
|
{
|
||||||
var conv = EnsureCurrentConversation(tab);
|
var conv = EnsureCurrentConversation(tab);
|
||||||
conv.UpdatedAt = DateTime.Now;
|
conv.UpdatedAt = DateTime.Now;
|
||||||
@@ -768,7 +774,7 @@ public sealed class ChatSessionStateService
|
|||||||
try { storage?.Save(conv); } catch { }
|
try { storage?.Save(conv); } catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool UpdateDraftItem(string tab, string draftId, Func<DraftQueueItem, bool> update, ChatStorageService? storage)
|
private bool UpdateDraftItem(string tab, string draftId, Func<DraftQueueItem, bool> update, IChatStorageService? storage)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(draftId))
|
if (string.IsNullOrWhiteSpace(draftId))
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace AxCopilot.Services;
|
|||||||
/// 스레드 안전: ReaderWriterLockSlim으로 동시 접근 보호.
|
/// 스레드 안전: ReaderWriterLockSlim으로 동시 접근 보호.
|
||||||
/// 원자적 쓰기: 임시 파일 → rename 패턴으로 크래시 시 데이터 손실 방지.
|
/// 원자적 쓰기: 임시 파일 → rename 패턴으로 크래시 시 데이터 손실 방지.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ChatStorageService
|
public class ChatStorageService : IChatStorageService
|
||||||
{
|
{
|
||||||
private static readonly string ConversationsDir =
|
private static readonly string ConversationsDir =
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
@@ -53,7 +53,7 @@ public class ChatStorageService
|
|||||||
// 원자적 교체: 기존 파일이 있으면 덮어쓰기
|
// 원자적 교체: 기존 파일이 있으면 덮어쓰기
|
||||||
if (File.Exists(path)) File.Delete(path);
|
if (File.Exists(path)) File.Delete(path);
|
||||||
File.Move(tempPath, path);
|
File.Move(tempPath, path);
|
||||||
UpdateMetaCache(conversation);
|
UpdateMetaCacheUnsafe(conversation);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -124,6 +124,20 @@ public class ChatStorageService
|
|||||||
|
|
||||||
/// <summary>메타 캐시에서 특정 항목만 업데이트합니다 (전체 재로드 없이).</summary>
|
/// <summary>메타 캐시에서 특정 항목만 업데이트합니다 (전체 재로드 없이).</summary>
|
||||||
public void UpdateMetaCache(ChatConversation conv)
|
public void UpdateMetaCache(ChatConversation conv)
|
||||||
|
{
|
||||||
|
Lock.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UpdateMetaCacheUnsafe(conv);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Lock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>WriteLock이 이미 확보된 상태에서 호출. 내부 전용.</summary>
|
||||||
|
private void UpdateMetaCacheUnsafe(ChatConversation conv)
|
||||||
{
|
{
|
||||||
if (_metaCache == null) return;
|
if (_metaCache == null) return;
|
||||||
var existing = _metaCache.FindIndex(c => c.Id == conv.Id);
|
var existing = _metaCache.FindIndex(c => c.Id == conv.Id);
|
||||||
@@ -146,6 +160,20 @@ public class ChatStorageService
|
|||||||
|
|
||||||
/// <summary>메타 캐시에서 항목을 제거합니다.</summary>
|
/// <summary>메타 캐시에서 항목을 제거합니다.</summary>
|
||||||
public void RemoveFromMetaCache(string id)
|
public void RemoveFromMetaCache(string id)
|
||||||
|
{
|
||||||
|
Lock.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RemoveFromMetaCacheUnsafe(id);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Lock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>WriteLock이 이미 확보된 상태에서 호출. 내부 전용.</summary>
|
||||||
|
private void RemoveFromMetaCacheUnsafe(string id)
|
||||||
{
|
{
|
||||||
_metaCache?.RemoveAll(c => c.Id == id);
|
_metaCache?.RemoveAll(c => c.Id == id);
|
||||||
InvalidateMetaOrderCache();
|
InvalidateMetaOrderCache();
|
||||||
@@ -220,7 +248,7 @@ public class ChatStorageService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (File.Exists(path)) File.Delete(path);
|
if (File.Exists(path)) File.Delete(path);
|
||||||
RemoveFromMetaCache(id);
|
RemoveFromMetaCacheUnsafe(id);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -293,7 +321,7 @@ public class ChatStorageService
|
|||||||
if (conv != null && !conv.Pinned && conv.UpdatedAt < cutoff)
|
if (conv != null && !conv.Pinned && conv.UpdatedAt < cutoff)
|
||||||
{
|
{
|
||||||
File.Delete(file);
|
File.Delete(file);
|
||||||
RemoveFromMetaCache(conv.Id);
|
RemoveFromMetaCacheUnsafe(conv.Id);
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ internal sealed class Cp4dTokenService
|
|||||||
Content = JsonContent.Create(body)
|
Content = JsonContent.Create(body)
|
||||||
};
|
};
|
||||||
|
|
||||||
using var resp = await _http.SendAsync(req, ct);
|
using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
|
||||||
statusCode = resp.StatusCode;
|
statusCode = resp.StatusCode;
|
||||||
var rawBody = await resp.Content.ReadAsStringAsync(ct);
|
var rawBody = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||||
if (!resp.IsSuccessStatusCode)
|
if (!resp.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
lastErrorBody = rawBody;
|
lastErrorBody = rawBody;
|
||||||
@@ -71,9 +71,9 @@ internal sealed class Cp4dTokenService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var authorized = await TryAuthorizeAsync(new { username, password }, "username+password");
|
var authorized = await TryAuthorizeAsync(new { username, password }, "username+password").ConfigureAwait(false);
|
||||||
if (!authorized && !string.IsNullOrWhiteSpace(password))
|
if (!authorized && !string.IsNullOrWhiteSpace(password))
|
||||||
authorized = await TryAuthorizeAsync(new { username, api_key = password }, "username+api_key");
|
authorized = await TryAuthorizeAsync(new { username, api_key = password }, "username+api_key").ConfigureAwait(false);
|
||||||
|
|
||||||
if (!authorized || string.IsNullOrWhiteSpace(json))
|
if (!authorized || string.IsNullOrWhiteSpace(json))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public sealed class DraftQueueProcessorService
|
|||||||
public bool CanStartNext(ChatSessionStateService? session, string tab)
|
public bool CanStartNext(ChatSessionStateService? session, string tab)
|
||||||
=> session?.GetNextQueuedDraft(tab) != null;
|
=> session?.GetNextQueuedDraft(tab) != null;
|
||||||
|
|
||||||
public DraftQueueItem? TryStartNext(ChatSessionStateService? session, string tab, ChatStorageService? storage = null, string? preferredDraftId = null, TaskRunService? taskRuns = null)
|
public DraftQueueItem? TryStartNext(ChatSessionStateService? session, string tab, IChatStorageService? storage = null, string? preferredDraftId = null, TaskRunService? taskRuns = null)
|
||||||
{
|
{
|
||||||
if (session == null)
|
if (session == null)
|
||||||
return null;
|
return null;
|
||||||
@@ -46,7 +46,7 @@ public sealed class DraftQueueProcessorService
|
|||||||
?? next;
|
?? next;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Complete(ChatSessionStateService? session, string tab, string draftId, ChatStorageService? storage = null, TaskRunService? taskRuns = null)
|
public bool Complete(ChatSessionStateService? session, string tab, string draftId, IChatStorageService? storage = null, TaskRunService? taskRuns = null)
|
||||||
{
|
{
|
||||||
var completed = session?.MarkDraftCompleted(tab, draftId, storage) ?? false;
|
var completed = session?.MarkDraftCompleted(tab, draftId, storage) ?? false;
|
||||||
if (completed)
|
if (completed)
|
||||||
@@ -54,7 +54,7 @@ public sealed class DraftQueueProcessorService
|
|||||||
return completed;
|
return completed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HandleFailure(ChatSessionStateService? session, string tab, string draftId, string? error, bool cancelled = false, int maxAutoRetries = 3, ChatStorageService? storage = null, TaskRunService? taskRuns = null)
|
public bool HandleFailure(ChatSessionStateService? session, string tab, string draftId, string? error, bool cancelled = false, int maxAutoRetries = 3, IChatStorageService? storage = null, TaskRunService? taskRuns = null)
|
||||||
{
|
{
|
||||||
if (session == null)
|
if (session == null)
|
||||||
return false;
|
return false;
|
||||||
@@ -83,7 +83,7 @@ public sealed class DraftQueueProcessorService
|
|||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int PromoteReadyBlockedItems(ChatSessionStateService? session, string tab, ChatStorageService? storage = null)
|
public int PromoteReadyBlockedItems(ChatSessionStateService? session, string tab, IChatStorageService? storage = null)
|
||||||
{
|
{
|
||||||
if (session == null)
|
if (session == null)
|
||||||
return 0;
|
return 0;
|
||||||
@@ -102,18 +102,18 @@ public sealed class DraftQueueProcessorService
|
|||||||
return promoted;
|
return promoted;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int ClearCompleted(ChatSessionStateService? session, string tab, ChatStorageService? storage = null)
|
public int ClearCompleted(ChatSessionStateService? session, string tab, IChatStorageService? storage = null)
|
||||||
=> ClearByState(session, tab, "completed", storage);
|
=> ClearByState(session, tab, "completed", storage);
|
||||||
|
|
||||||
public int ClearFailed(ChatSessionStateService? session, string tab, ChatStorageService? storage = null)
|
public int ClearFailed(ChatSessionStateService? session, string tab, IChatStorageService? storage = null)
|
||||||
=> ClearByState(session, tab, "failed", storage);
|
=> ClearByState(session, tab, "failed", storage);
|
||||||
|
|
||||||
/// <summary>대기 중인 항목을 모두 제거합니다 (중지 시 사용).</summary>
|
/// <summary>대기 중인 항목을 모두 제거합니다 (중지 시 사용).</summary>
|
||||||
public int ClearQueued(ChatSessionStateService? session, string tab, ChatStorageService? storage = null)
|
public int ClearQueued(ChatSessionStateService? session, string tab, IChatStorageService? storage = null)
|
||||||
=> ClearByState(session, tab, "queued", storage);
|
=> ClearByState(session, tab, "queued", storage);
|
||||||
|
|
||||||
/// <summary>실행 중인 항목을 실패로 전환합니다 (중지 시 사용).</summary>
|
/// <summary>실행 중인 항목을 실패로 전환합니다 (중지 시 사용).</summary>
|
||||||
public int CancelRunning(ChatSessionStateService? session, string tab, ChatStorageService? storage = null)
|
public int CancelRunning(ChatSessionStateService? session, string tab, IChatStorageService? storage = null)
|
||||||
{
|
{
|
||||||
if (session == null) return 0;
|
if (session == null) return 0;
|
||||||
int count = 0;
|
int count = 0;
|
||||||
@@ -127,7 +127,7 @@ public sealed class DraftQueueProcessorService
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int ClearByState(ChatSessionStateService? session, string tab, string state, ChatStorageService? storage)
|
private static int ClearByState(ChatSessionStateService? session, string tab, string state, IChatStorageService? storage)
|
||||||
{
|
{
|
||||||
if (session == null)
|
if (session == null)
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -42,15 +42,15 @@ internal sealed class IbmIamTokenService
|
|||||||
"application/x-www-form-urlencoded");
|
"application/x-www-form-urlencoded");
|
||||||
req.Headers.Accept.ParseAdd("application/json");
|
req.Headers.Accept.ParseAdd("application/json");
|
||||||
|
|
||||||
using var resp = await _http.SendAsync(req, ct);
|
using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
|
||||||
if (!resp.IsSuccessStatusCode)
|
if (!resp.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var errBody = await resp.Content.ReadAsStringAsync(ct);
|
var errBody = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||||
LogService.Warn($"IBM IAM 토큰 발급 실패: {resp.StatusCode} - {errBody}");
|
LogService.Warn($"IBM IAM 토큰 발급 실패: {resp.StatusCode} - {errBody}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var json = await resp.Content.ReadAsStringAsync(ct);
|
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||||
using var doc = JsonDocument.Parse(json);
|
using var doc = JsonDocument.Parse(json);
|
||||||
if (!doc.RootElement.TryGetProperty("access_token", out var tokenProp))
|
if (!doc.RootElement.TryGetProperty("access_token", out var tokenProp))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ public class IndexService : IDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task BuildAsync(CancellationToken ct = default)
|
public async Task BuildAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _rebuildLock.WaitAsync(ct);
|
await _rebuildLock.WaitAsync(ct).ConfigureAwait(false);
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -146,7 +146,7 @@ public class IndexService : IDisposable
|
|||||||
$"스캔 중: {Path.GetFileName(dir)}",
|
$"스캔 중: {Path.GetFileName(dir)}",
|
||||||
EstimateRemainingText(progressBase, sw.Elapsed)));
|
EstimateRemainingText(progressBase, sw.Elapsed)));
|
||||||
|
|
||||||
await ScanDirectoryAsync(dir, fileSystemEntries, allowedExts, indexSpeed, ct);
|
await ScanDirectoryAsync(dir, fileSystemEntries, allowedExts, indexSpeed, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
var completedProgress = paths.Count == 0 ? 1 : (double)(index + 1) / paths.Count;
|
var completedProgress = paths.Count == 0 ? 1 : (double)(index + 1) / paths.Count;
|
||||||
ReportIndexProgress(LauncherIndexProgressInfo.Running(
|
ReportIndexProgress(LauncherIndexProgressInfo.Running(
|
||||||
|
|||||||
25
src/AxCopilot/Services/Interfaces/IAppStateService.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using AxCopilot.Models;
|
||||||
|
using AxCopilot.Services.Agent;
|
||||||
|
|
||||||
|
namespace AxCopilot.Services;
|
||||||
|
|
||||||
|
/// <summary>앱 전역 상태 요약 저장소 인터페이스.</summary>
|
||||||
|
public interface IAppStateService
|
||||||
|
{
|
||||||
|
ChatSessionStateService? ChatSession { get; }
|
||||||
|
|
||||||
|
void AttachChatSession(ChatSessionStateService session);
|
||||||
|
void LoadFromSettings(ISettingsService settings);
|
||||||
|
|
||||||
|
AppStateService.SkillCatalogState Skills { get; }
|
||||||
|
AppStateService.McpCatalogState Mcp { get; }
|
||||||
|
AppStateService.PermissionPolicyState Permissions { get; }
|
||||||
|
AppStateService.AgentCatalogState AgentCatalog { get; }
|
||||||
|
AppStateService.AgentRunState AgentRun { get; }
|
||||||
|
|
||||||
|
void UpsertTask(string id, string kind, string title, string summary, string status = "running", string? filePath = null);
|
||||||
|
void CompleteTask(string id, string? summary = null, string status = "completed");
|
||||||
|
void ApplyAgentEvent(AgentEvent evt);
|
||||||
|
AppStateService.BackgroundJobSummaryState GetBackgroundJobSummary();
|
||||||
|
AppStateService.PermissionSummaryState GetPermissionSummary(ChatConversation? conversation = null);
|
||||||
|
}
|
||||||
19
src/AxCopilot/Services/Interfaces/IChatStorageService.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using AxCopilot.Models;
|
||||||
|
|
||||||
|
namespace AxCopilot.Services;
|
||||||
|
|
||||||
|
/// <summary>대화 내역 저장/로드/삭제 인터페이스.</summary>
|
||||||
|
public interface IChatStorageService
|
||||||
|
{
|
||||||
|
void Save(ChatConversation conversation);
|
||||||
|
ChatConversation? Load(string id);
|
||||||
|
List<ChatConversation> LoadAllMeta();
|
||||||
|
void InvalidateMetaCache();
|
||||||
|
void UpdateMetaCache(ChatConversation conv);
|
||||||
|
void RemoveFromMetaCache(string id);
|
||||||
|
void Delete(string id);
|
||||||
|
int DeleteAll();
|
||||||
|
int DeleteAllByTab(string tab);
|
||||||
|
int PurgeExpired(int retentionDays);
|
||||||
|
int PurgeForDiskSpace(double threshold = 0.98);
|
||||||
|
}
|
||||||
75
src/AxCopilot/Services/Interfaces/ILlmService.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using AxCopilot.Models;
|
||||||
|
using AxCopilot.Services.Agent;
|
||||||
|
|
||||||
|
namespace AxCopilot.Services;
|
||||||
|
|
||||||
|
/// <summary>런타임 연결 정보 스냅샷.</summary>
|
||||||
|
public record RuntimeConnectionSnapshot(
|
||||||
|
string Service,
|
||||||
|
string Model,
|
||||||
|
string Endpoint,
|
||||||
|
bool AllowInsecureTls,
|
||||||
|
bool HasApiKey);
|
||||||
|
|
||||||
|
/// <summary>LLM API 호출 인터페이스. 스트리밍/비스트리밍 모두 지원.</summary>
|
||||||
|
public interface ILlmService : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>현재 서비스/모델 정보 조회.</summary>
|
||||||
|
(string service, string model) GetCurrentModelInfo();
|
||||||
|
|
||||||
|
/// <summary>비스트리밍 전체 응답 요청.</summary>
|
||||||
|
Task<string> SendAsync(List<ChatMessage> messages, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>스트리밍 응답 요청 (SSE).</summary>
|
||||||
|
IAsyncEnumerable<string> StreamAsync(
|
||||||
|
List<ChatMessage> messages,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>연결 테스트.</summary>
|
||||||
|
Task<(bool ok, string message)> TestConnectionAsync();
|
||||||
|
|
||||||
|
/// <summary>자동 라우팅용 서비스/모델 오버라이드 설정.</summary>
|
||||||
|
void PushRouteOverride(string service, string model);
|
||||||
|
|
||||||
|
/// <summary>서비스/모델 오버라이드 해제.</summary>
|
||||||
|
void ClearRouteOverride();
|
||||||
|
|
||||||
|
/// <summary>모델/추론 파라미터 오버라이드 Push.</summary>
|
||||||
|
void PushInferenceOverride(
|
||||||
|
string? service = null,
|
||||||
|
string? model = null,
|
||||||
|
double? temperature = null,
|
||||||
|
string? reasoningEffort = null);
|
||||||
|
|
||||||
|
/// <summary>가장 최근 Push 상태 복원.</summary>
|
||||||
|
void PopInferenceOverride();
|
||||||
|
|
||||||
|
/// <summary>가장 최근 요청의 토큰 사용량.</summary>
|
||||||
|
TokenUsage? LastTokenUsage { get; }
|
||||||
|
|
||||||
|
/// <summary>런타임 연결 정보 스냅샷 조회.</summary>
|
||||||
|
RuntimeConnectionSnapshot GetRuntimeConnectionSnapshot();
|
||||||
|
|
||||||
|
/// <summary>도구 정의를 포함하여 LLM에 요청하고, 텍스트 + tool_use 블록을 파싱하여 반환합니다.</summary>
|
||||||
|
Task<List<ContentBlock>> SendWithToolsAsync(
|
||||||
|
List<ChatMessage> messages,
|
||||||
|
IReadOnlyCollection<IAgentTool> tools,
|
||||||
|
CancellationToken ct = default,
|
||||||
|
bool forceToolCall = false,
|
||||||
|
Func<ContentBlock, Task<ToolPrefetchResult?>>? prefetchToolCallAsync = null);
|
||||||
|
|
||||||
|
/// <summary>도구 정의를 포함하여 스트리밍 요청.</summary>
|
||||||
|
IAsyncEnumerable<ToolStreamEvent> StreamWithToolsAsync(
|
||||||
|
List<ChatMessage> messages,
|
||||||
|
IReadOnlyCollection<IAgentTool> tools,
|
||||||
|
bool forceToolCall = false,
|
||||||
|
Func<ContentBlock, Task<ToolPrefetchResult?>>? prefetchToolCallAsync = null,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>현재 활성 실행 정책 조회.</summary>
|
||||||
|
ModelExecutionProfileCatalog.ExecutionPolicy GetActiveExecutionPolicy();
|
||||||
|
|
||||||
|
/// <summary>현재 시스템 프롬프트 (컨텍스트 토큰 추정에 사용).</summary>
|
||||||
|
string? SystemPrompt { get; }
|
||||||
|
}
|
||||||
9
src/AxCopilot/Services/Interfaces/IModelRouterService.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace AxCopilot.Services;
|
||||||
|
|
||||||
|
/// <summary>사용자 메시지의 인텐트를 분석하여 최적 모델을 선택하는 라우터 인터페이스.</summary>
|
||||||
|
public interface IModelRouterService
|
||||||
|
{
|
||||||
|
/// <summary>사용자 메시지를 분석하여 최적 모델을 선택합니다.</summary>
|
||||||
|
/// <returns>라우팅 결과. null이면 기본 모델 유지.</returns>
|
||||||
|
ModelRouteResult? Route(string userMessage);
|
||||||
|
}
|
||||||
15
src/AxCopilot/Services/Interfaces/ISettingsService.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using AxCopilot.Models;
|
||||||
|
|
||||||
|
namespace AxCopilot.Services;
|
||||||
|
|
||||||
|
/// <summary>애플리케이션 설정 읽기/쓰기 인터페이스.</summary>
|
||||||
|
public interface ISettingsService
|
||||||
|
{
|
||||||
|
AppSettings Settings { get; }
|
||||||
|
string? MigrationSummary { get; }
|
||||||
|
event EventHandler? SettingsChanged;
|
||||||
|
|
||||||
|
void Load();
|
||||||
|
void Save();
|
||||||
|
Task SaveAsync();
|
||||||
|
}
|
||||||
@@ -62,9 +62,9 @@ public partial class LlmService
|
|||||||
EnsureOperationModeAllowsLlmService(activeService);
|
EnsureOperationModeAllowsLlmService(activeService);
|
||||||
return NormalizeServiceName(activeService) switch
|
return NormalizeServiceName(activeService) switch
|
||||||
{
|
{
|
||||||
"sigmoid" => await SendSigmoidWithToolsAsync(messages, tools, ct),
|
"sigmoid" => await SendSigmoidWithToolsAsync(messages, tools, ct).ConfigureAwait(false),
|
||||||
"gemini" => await SendGeminiWithToolsAsync(messages, tools, ct),
|
"gemini" => await SendGeminiWithToolsAsync(messages, tools, ct).ConfigureAwait(false),
|
||||||
"ollama" or "vllm" => await SendOpenAiWithToolsAsync(messages, tools, ct, forceToolCall, prefetchToolCallAsync),
|
"ollama" or "vllm" => await SendOpenAiWithToolsAsync(messages, tools, ct, forceToolCall, prefetchToolCallAsync).ConfigureAwait(false),
|
||||||
_ => throw new NotSupportedException($"서비스 '{activeService}'는 아직 Function Calling을 지원하지 않습니다.")
|
_ => throw new NotSupportedException($"서비스 '{activeService}'는 아직 Function Calling을 지원하지 않습니다.")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -592,7 +592,7 @@ public partial class LlmService
|
|||||||
@"\{\s*""name""\s*:\s*""(\w+)""\s*,\s*""arguments""\s*:\s*(\{[\s\S]*?\})\s*\}",
|
@"\{\s*""name""\s*:\s*""(\w+)""\s*,\s*""arguments""\s*:\s*(\{[\s\S]*?\})\s*\}",
|
||||||
System.Text.RegularExpressions.RegexOptions.Compiled);
|
System.Text.RegularExpressions.RegexOptions.Compiled);
|
||||||
|
|
||||||
private static List<ContentBlock> TryExtractToolCallsFromText(string text)
|
internal static List<ContentBlock> TryExtractToolCallsFromText(string text)
|
||||||
{
|
{
|
||||||
var results = new List<ContentBlock>();
|
var results = new List<ContentBlock>();
|
||||||
if (string.IsNullOrWhiteSpace(text)) return results;
|
if (string.IsNullOrWhiteSpace(text)) return results;
|
||||||
@@ -690,9 +690,13 @@ public partial class LlmService
|
|||||||
{
|
{
|
||||||
var llm = _settings.Settings.Llm;
|
var llm = _settings.Settings.Llm;
|
||||||
var msgs = new List<object>();
|
var msgs = new List<object>();
|
||||||
|
var structuredHistoryStart = GetStructuredToolHistoryStartIndex(messages);
|
||||||
|
|
||||||
foreach (var m in messages)
|
for (var messageIndex = 0; messageIndex < messages.Count; messageIndex++)
|
||||||
{
|
{
|
||||||
|
var m = messages[messageIndex];
|
||||||
|
var keepStructuredHistory = messageIndex >= structuredHistoryStart;
|
||||||
|
|
||||||
// tool_result 메시지 → OpenAI tool 응답 형식
|
// tool_result 메시지 → OpenAI tool 응답 형식
|
||||||
if (m.Role == "user" && m.Content.StartsWith("{\"type\":\"tool_result\""))
|
if (m.Role == "user" && m.Content.StartsWith("{\"type\":\"tool_result\""))
|
||||||
{
|
{
|
||||||
@@ -700,6 +704,16 @@ public partial class LlmService
|
|||||||
{
|
{
|
||||||
using var doc = JsonDocument.Parse(m.Content);
|
using var doc = JsonDocument.Parse(m.Content);
|
||||||
var root = doc.RootElement;
|
var root = doc.RootElement;
|
||||||
|
if (!keepStructuredHistory)
|
||||||
|
{
|
||||||
|
msgs.Add(new
|
||||||
|
{
|
||||||
|
role = "user",
|
||||||
|
content = BuildOpenAiToolResultTranscript(root),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
msgs.Add(new
|
msgs.Add(new
|
||||||
{
|
{
|
||||||
role = "tool",
|
role = "tool",
|
||||||
@@ -718,6 +732,16 @@ public partial class LlmService
|
|||||||
{
|
{
|
||||||
using var doc = JsonDocument.Parse(m.Content);
|
using var doc = JsonDocument.Parse(m.Content);
|
||||||
var blocksArr = doc.RootElement.GetProperty("_tool_use_blocks");
|
var blocksArr = doc.RootElement.GetProperty("_tool_use_blocks");
|
||||||
|
if (!keepStructuredHistory)
|
||||||
|
{
|
||||||
|
msgs.Add(new
|
||||||
|
{
|
||||||
|
role = "assistant",
|
||||||
|
content = BuildOpenAiAssistantTranscript(blocksArr),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var textContent = "";
|
var textContent = "";
|
||||||
var toolCallsList = new List<object>();
|
var toolCallsList = new List<object>();
|
||||||
foreach (var b in blocksArr.EnumerateArray())
|
foreach (var b in blocksArr.EnumerateArray())
|
||||||
@@ -766,6 +790,12 @@ public partial class LlmService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── tool_calls ↔ tool 메시지 쌍 검증 ──
|
||||||
|
// 컨텍스트 압축 후 tool_calls assistant 메시지는 남아있는데
|
||||||
|
// 대응하는 tool result 메시지가 누락되면 vLLM이 400 에러를 반환함.
|
||||||
|
// 깨진 tool_calls 메시지를 일반 assistant 텍스트로 평탄화하여 방지.
|
||||||
|
SanitizeToolCallPairs(msgs);
|
||||||
|
|
||||||
// OpenAI 도구 정의
|
// OpenAI 도구 정의
|
||||||
var toolDefs = tools.Select(t =>
|
var toolDefs = tools.Select(t =>
|
||||||
{
|
{
|
||||||
@@ -798,14 +828,20 @@ public partial class LlmService
|
|||||||
var isOllama = activeService.Equals("ollama", StringComparison.OrdinalIgnoreCase);
|
var isOllama = activeService.Equals("ollama", StringComparison.OrdinalIgnoreCase);
|
||||||
if (isOllama)
|
if (isOllama)
|
||||||
{
|
{
|
||||||
return new
|
// Ollama /api/chat 전용 바디 — stream:false로 비스트리밍 응답
|
||||||
|
// Ollama 0.5.x+ 에서 tool_choice 파라미터 지원 (미지원 버전은 무시됨)
|
||||||
|
var ollamaBody = new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
model = activeModel,
|
["model"] = activeModel,
|
||||||
messages = msgs,
|
["messages"] = msgs,
|
||||||
tools = toolDefs,
|
["tools"] = toolDefs,
|
||||||
stream = false,
|
["stream"] = false,
|
||||||
options = new { temperature = ResolveToolTemperature() }
|
["options"] = new { temperature = ResolveToolTemperature() }
|
||||||
};
|
};
|
||||||
|
// Ollama에도 tool_choice 전달 — 프로파일 ForceInitialToolCall 적용
|
||||||
|
if (forceToolCall)
|
||||||
|
ollamaBody["tool_choice"] = "required";
|
||||||
|
return ollamaBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
var body = new Dictionary<string, object?>
|
var body = new Dictionary<string, object?>
|
||||||
@@ -830,6 +866,26 @@ public partial class LlmService
|
|||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int GetStructuredToolHistoryStartIndex(IReadOnlyList<ChatMessage> messages)
|
||||||
|
{
|
||||||
|
const int protectedRecentNonSystemMessages = 8;
|
||||||
|
var nonSystemMessages = messages
|
||||||
|
.Select((message, index) => new { message, index })
|
||||||
|
.Where(x => !string.Equals(x.message.Role, "system", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (nonSystemMessages.Count <= protectedRecentNonSystemMessages)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var tentativeStart = Math.Max(0, nonSystemMessages.Count - protectedRecentNonSystemMessages);
|
||||||
|
var adjustedStart = AgentMessageInvariantHelper.AdjustStartIndexForToolPairs(
|
||||||
|
nonSystemMessages.Select(x => x.message).ToList(),
|
||||||
|
tentativeStart,
|
||||||
|
out _);
|
||||||
|
|
||||||
|
return nonSystemMessages[Math.Max(0, adjustedStart)].index;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>IBM 배포형 /text/chat 전용 도구 바디. parameters 래퍼 사용, model 필드 없음.</summary>
|
/// <summary>IBM 배포형 /text/chat 전용 도구 바디. parameters 래퍼 사용, model 필드 없음.</summary>
|
||||||
private object BuildIbmToolBody(
|
private object BuildIbmToolBody(
|
||||||
List<ChatMessage> messages,
|
List<ChatMessage> messages,
|
||||||
@@ -1006,6 +1062,51 @@ public partial class LlmService
|
|||||||
: string.Join("\n\n", parts);
|
: string.Join("\n\n", parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string BuildOpenAiAssistantTranscript(JsonElement blocksArr)
|
||||||
|
{
|
||||||
|
var textSegments = new List<string>();
|
||||||
|
var toolNames = new List<string>();
|
||||||
|
|
||||||
|
foreach (var block in blocksArr.EnumerateArray())
|
||||||
|
{
|
||||||
|
var blockType = block.GetProperty("type").SafeGetString();
|
||||||
|
if (string.Equals(blockType, "text", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var text = block.SafeTryGetProperty("text", out var textEl) ? textEl.SafeGetString() ?? "" : "";
|
||||||
|
if (!string.IsNullOrWhiteSpace(text))
|
||||||
|
textSegments.Add(text.Trim());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(blockType, "tool_use", StringComparison.OrdinalIgnoreCase))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var name = block.SafeTryGetProperty("name", out var nameEl) ? nameEl.SafeGetString() ?? "" : "";
|
||||||
|
if (!string.IsNullOrWhiteSpace(name))
|
||||||
|
toolNames.Add(name.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = new List<string>();
|
||||||
|
if (textSegments.Count > 0)
|
||||||
|
parts.Add(string.Join("\n\n", textSegments));
|
||||||
|
if (toolNames.Count > 0)
|
||||||
|
parts.Add($"[이전 도구 호출: {string.Join(", ", toolNames.Distinct(StringComparer.OrdinalIgnoreCase))}]");
|
||||||
|
|
||||||
|
return parts.Count == 0 ? "[이전 도구 호출]" : string.Join("\n\n", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildOpenAiToolResultTranscript(JsonElement root)
|
||||||
|
{
|
||||||
|
var toolName = root.SafeTryGetProperty("tool_name", out var nameEl) ? nameEl.SafeGetString() ?? "" : "";
|
||||||
|
var content = root.SafeTryGetProperty("content", out var contentEl) ? contentEl.SafeGetString() ?? "" : "";
|
||||||
|
var header = string.IsNullOrWhiteSpace(toolName)
|
||||||
|
? "[이전 도구 결과]"
|
||||||
|
: $"[이전 도구 결과: {toolName}]";
|
||||||
|
return string.IsNullOrWhiteSpace(content)
|
||||||
|
? $"{header}\n(no output)"
|
||||||
|
: $"{header}\n{content}";
|
||||||
|
}
|
||||||
|
|
||||||
private static string BuildIbmToolResultTranscript(JsonElement root)
|
private static string BuildIbmToolResultTranscript(JsonElement root)
|
||||||
{
|
{
|
||||||
var toolCallId = root.SafeTryGetProperty("tool_use_id", out var idEl) ? idEl.SafeGetString() ?? "" : "";
|
var toolCallId = root.SafeTryGetProperty("tool_use_id", out var idEl) ? idEl.SafeGetString() ?? "" : "";
|
||||||
@@ -1674,6 +1775,93 @@ public partial class LlmService
|
|||||||
|
|
||||||
// ─── 공통 헬퍼 ─────────────────────────────────────────────────────
|
// ─── 공통 헬퍼 ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// tool_calls가 포함된 assistant 메시지 뒤에 대응하는 tool 메시지가 없으면
|
||||||
|
/// 해당 assistant 메시지를 일반 텍스트로 평탄화합니다.
|
||||||
|
/// 컨텍스트 압축 후 쌍이 깨지는 경우를 방어합니다.
|
||||||
|
/// </summary>
|
||||||
|
private static void SanitizeToolCallPairs(List<object> msgs)
|
||||||
|
{
|
||||||
|
// ── 1패스: tool_calls assistant 메시지의 쌍 검증 ──
|
||||||
|
// tool_calls가 있는 assistant 인덱스를 기록하여 2패스에서 고아 tool 판별에 사용
|
||||||
|
var pairedToolIndices = new HashSet<int>();
|
||||||
|
|
||||||
|
for (int i = 0; i < msgs.Count; i++)
|
||||||
|
{
|
||||||
|
var msgType = msgs[i].GetType();
|
||||||
|
var toolCallsProp = msgType.GetProperty("tool_calls");
|
||||||
|
var roleProp = msgType.GetProperty("role");
|
||||||
|
if (toolCallsProp == null || roleProp == null) continue;
|
||||||
|
|
||||||
|
var role = roleProp.GetValue(msgs[i]) as string;
|
||||||
|
if (role != "assistant") continue;
|
||||||
|
|
||||||
|
var toolCalls = toolCallsProp.GetValue(msgs[i]);
|
||||||
|
if (toolCalls == null) continue;
|
||||||
|
|
||||||
|
int callCount = 0;
|
||||||
|
if (toolCalls is System.Collections.ICollection col) callCount = col.Count;
|
||||||
|
else if (toolCalls is System.Collections.IEnumerable en)
|
||||||
|
{
|
||||||
|
foreach (var _ in en) callCount++;
|
||||||
|
}
|
||||||
|
if (callCount == 0) continue;
|
||||||
|
|
||||||
|
// 바로 다음에 tool 역할 메시지가 callCount개 있는지 확인
|
||||||
|
int foundTools = 0;
|
||||||
|
for (int j = i + 1; j < msgs.Count && foundTools < callCount; j++)
|
||||||
|
{
|
||||||
|
var jType = msgs[j].GetType();
|
||||||
|
var jRole = jType.GetProperty("role")?.GetValue(msgs[j]) as string;
|
||||||
|
if (jRole == "tool")
|
||||||
|
{
|
||||||
|
foundTools++;
|
||||||
|
pairedToolIndices.Add(j);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundTools < callCount)
|
||||||
|
{
|
||||||
|
// 쌍이 불완전 → assistant를 일반 텍스트로 교체
|
||||||
|
var contentProp = msgType.GetProperty("content");
|
||||||
|
var contentText = contentProp?.GetValue(msgs[i]) as string ?? "";
|
||||||
|
if (string.IsNullOrWhiteSpace(contentText))
|
||||||
|
contentText = "[이전 도구 호출 — 결과 누락으로 생략됨]";
|
||||||
|
msgs[i] = new { role = "assistant", content = contentText };
|
||||||
|
|
||||||
|
// 이 assistant에 딸린 불완전 tool 메시지도 user로 변환
|
||||||
|
for (int j = i + 1; j < msgs.Count; j++)
|
||||||
|
{
|
||||||
|
var jType = msgs[j].GetType();
|
||||||
|
var jRole = jType.GetProperty("role")?.GetValue(msgs[j]) as string;
|
||||||
|
if (jRole != "tool") break;
|
||||||
|
var jContent = jType.GetProperty("content")?.GetValue(msgs[j]) as string ?? "";
|
||||||
|
msgs[j] = new { role = "user", content = $"[이전 도구 결과]\n{jContent}" };
|
||||||
|
pairedToolIndices.Remove(j);
|
||||||
|
}
|
||||||
|
|
||||||
|
LogService.Info($"[ToolUse] tool_calls/tool 쌍 불일치 감지 — assistant 메시지를 평탄화 (index={i})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2패스: 앞에 tool_calls가 없는 고아 tool 메시지를 user로 변환 ──
|
||||||
|
for (int i = 0; i < msgs.Count; i++)
|
||||||
|
{
|
||||||
|
if (pairedToolIndices.Contains(i)) continue;
|
||||||
|
|
||||||
|
var msgType = msgs[i].GetType();
|
||||||
|
var roleProp = msgType.GetProperty("role");
|
||||||
|
var role = roleProp?.GetValue(msgs[i]) as string;
|
||||||
|
if (role != "tool") continue;
|
||||||
|
|
||||||
|
var content = msgType.GetProperty("content")?.GetValue(msgs[i]) as string ?? "";
|
||||||
|
msgs[i] = new { role = "user", content = $"[이전 도구 결과]\n{content}" };
|
||||||
|
LogService.Info($"[ToolUse] 고아 tool 메시지 감지 — user로 변환 (index={i})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>ToolProperty를 LLM API용 스키마 객체로 변환. array/enum/items 포함.</summary>
|
/// <summary>ToolProperty를 LLM API용 스키마 객체로 변환. array/enum/items 포함.</summary>
|
||||||
private static object BuildPropertySchema(Agent.ToolProperty prop, bool upperCaseType)
|
private static object BuildPropertySchema(Agent.ToolProperty prop, bool upperCaseType)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public record TokenUsage(int PromptTokens, int CompletionTokens)
|
|||||||
/// LLM API 호출 서비스. Ollama / vLLM / Gemini / Claude 백엔드를 지원합니다.
|
/// LLM API 호출 서비스. Ollama / vLLM / Gemini / Claude 백엔드를 지원합니다.
|
||||||
/// 스트리밍(SSE) 및 비스트리밍 모두 지원합니다.
|
/// 스트리밍(SSE) 및 비스트리밍 모두 지원합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class LlmService : IDisposable
|
public partial class LlmService : ILlmService
|
||||||
{
|
{
|
||||||
private readonly HttpClient _http;
|
private readonly HttpClient _http;
|
||||||
private readonly HttpClient _httpInsecure;
|
private readonly HttpClient _httpInsecure;
|
||||||
@@ -148,7 +148,7 @@ public partial class LlmService : IDisposable
|
|||||||
internal string GetActiveExecutionProfileKey()
|
internal string GetActiveExecutionProfileKey()
|
||||||
=> Agent.ModelExecutionProfileCatalog.Normalize(GetActiveRegisteredModel()?.ExecutionProfile);
|
=> Agent.ModelExecutionProfileCatalog.Normalize(GetActiveRegisteredModel()?.ExecutionProfile);
|
||||||
|
|
||||||
internal Agent.ModelExecutionProfileCatalog.ExecutionPolicy GetActiveExecutionPolicy()
|
public Agent.ModelExecutionProfileCatalog.ExecutionPolicy GetActiveExecutionPolicy()
|
||||||
=> Agent.ModelExecutionProfileCatalog.Get(GetActiveExecutionProfileKey());
|
=> Agent.ModelExecutionProfileCatalog.Get(GetActiveExecutionProfileKey());
|
||||||
|
|
||||||
internal double ResolveToolTemperature()
|
internal double ResolveToolTemperature()
|
||||||
@@ -220,13 +220,6 @@ public partial class LlmService : IDisposable
|
|||||||
/// <summary>가장 최근 요청의 토큰 사용량. 스트리밍/비스트리밍 완료 후 갱신됩니다.</summary>
|
/// <summary>가장 최근 요청의 토큰 사용량. 스트리밍/비스트리밍 완료 후 갱신됩니다.</summary>
|
||||||
public TokenUsage? LastTokenUsage { get; private set; }
|
public TokenUsage? LastTokenUsage { get; private set; }
|
||||||
|
|
||||||
public record RuntimeConnectionSnapshot(
|
|
||||||
string Service,
|
|
||||||
string Model,
|
|
||||||
string Endpoint,
|
|
||||||
bool AllowInsecureTls,
|
|
||||||
bool HasApiKey);
|
|
||||||
|
|
||||||
public LlmService(SettingsService settings)
|
public LlmService(SettingsService settings)
|
||||||
{
|
{
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
@@ -342,8 +335,8 @@ public partial class LlmService : IDisposable
|
|||||||
{
|
{
|
||||||
return llm.RegisteredModels.FirstOrDefault(m =>
|
return llm.RegisteredModels.FirstOrDefault(m =>
|
||||||
m.Service.Equals(service, StringComparison.OrdinalIgnoreCase) &&
|
m.Service.Equals(service, StringComparison.OrdinalIgnoreCase) &&
|
||||||
(CryptoService.DecryptIfEnabled(m.EncryptedModelName, llm.EncryptionEnabled) == modelName ||
|
(string.Equals(CryptoService.DecryptIfEnabled(m.EncryptedModelName, llm.EncryptionEnabled), modelName, StringComparison.OrdinalIgnoreCase) ||
|
||||||
m.Alias == modelName));
|
string.Equals(m.Alias, modelName, StringComparison.OrdinalIgnoreCase)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Models.RegisteredModel? GetActiveRegisteredModel()
|
private Models.RegisteredModel? GetActiveRegisteredModel()
|
||||||
@@ -572,7 +565,7 @@ public partial class LlmService : IDisposable
|
|||||||
EnsureOperationModeAllowsLlmService(activeService);
|
EnsureOperationModeAllowsLlmService(activeService);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await SendWithServiceAsync(activeService, messages, ct);
|
return await SendWithServiceAsync(activeService, messages, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (llm.FallbackModels.Count > 0)
|
catch (Exception ex) when (llm.FallbackModels.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -587,7 +580,7 @@ public partial class LlmService : IDisposable
|
|||||||
EnsureOperationModeAllowsLlmService(fbService);
|
EnsureOperationModeAllowsLlmService(fbService);
|
||||||
LogService.Warn($"모델 폴백: {activeService} → {fbService} ({ex.Message})");
|
LogService.Warn($"모델 폴백: {activeService} → {fbService} ({ex.Message})");
|
||||||
LastFallbackInfo = $"{activeService} → {fbService}";
|
LastFallbackInfo = $"{activeService} → {fbService}";
|
||||||
return await SendWithServiceAsync(fbService, messages, ct);
|
return await SendWithServiceAsync(fbService, messages, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch { continue; }
|
catch { continue; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using AxCopilot.Services.Agent;
|
||||||
|
|
||||||
namespace AxCopilot.Services;
|
namespace AxCopilot.Services;
|
||||||
|
|
||||||
@@ -68,8 +69,15 @@ public class LspClientService : IDisposable
|
|||||||
textDocument = new
|
textDocument = new
|
||||||
{
|
{
|
||||||
definition = new { dynamicRegistration = false },
|
definition = new { dynamicRegistration = false },
|
||||||
|
implementation = new { dynamicRegistration = false },
|
||||||
references = new { dynamicRegistration = false },
|
references = new { dynamicRegistration = false },
|
||||||
documentSymbol = new { dynamicRegistration = false },
|
documentSymbol = new { dynamicRegistration = false },
|
||||||
|
hover = new { dynamicRegistration = false },
|
||||||
|
callHierarchy = new { dynamicRegistration = false },
|
||||||
|
},
|
||||||
|
workspace = new
|
||||||
|
{
|
||||||
|
symbol = new { dynamicRegistration = false },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, ct);
|
}, ct);
|
||||||
@@ -126,6 +134,79 @@ public class LspClientService : IDisposable
|
|||||||
return ParseSymbols(result);
|
return ParseSymbols(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>심볼의 hover 정보를 가져옵니다.</summary>
|
||||||
|
public async Task<string?> HoverAsync(string filePath, int line, int character, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var result = await SendRequestAsync("textDocument/hover", new
|
||||||
|
{
|
||||||
|
textDocument = new { uri = FileToUri(filePath) },
|
||||||
|
position = new { line, character }
|
||||||
|
}, ct);
|
||||||
|
|
||||||
|
return ParseHover(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>인터페이스/추상 메서드 등의 구현 위치를 찾습니다.</summary>
|
||||||
|
public async Task<List<LspLocation>> GotoImplementationAsync(string filePath, int line, int character, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var result = await SendRequestAsync("textDocument/implementation", new
|
||||||
|
{
|
||||||
|
textDocument = new { uri = FileToUri(filePath) },
|
||||||
|
position = new { line, character }
|
||||||
|
}, ct);
|
||||||
|
|
||||||
|
return ParseLocations(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>워크스페이스 전체 심볼을 검색합니다.</summary>
|
||||||
|
public async Task<List<LspWorkspaceSymbol>> SearchWorkspaceSymbolsAsync(string query, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var result = await SendRequestAsync("workspace/symbol", new { query }, ct);
|
||||||
|
return ParseWorkspaceSymbols(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>호출 계층의 기준 아이템을 준비합니다.</summary>
|
||||||
|
public async Task<List<LspCallHierarchyItem>> PrepareCallHierarchyAsync(string filePath, int line, int character, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var result = await SendRequestAsync("textDocument/prepareCallHierarchy", new
|
||||||
|
{
|
||||||
|
textDocument = new { uri = FileToUri(filePath) },
|
||||||
|
position = new { line, character }
|
||||||
|
}, ct);
|
||||||
|
|
||||||
|
return ParseCallHierarchyItems(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>해당 심볼을 호출하는 상위 호출자를 찾습니다.</summary>
|
||||||
|
public async Task<List<LspCallHierarchyEntry>> GetIncomingCallsAsync(string filePath, int line, int character, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var items = await PrepareCallHierarchyAsync(filePath, line, character, ct);
|
||||||
|
if (items.Count == 0)
|
||||||
|
return new List<LspCallHierarchyEntry>();
|
||||||
|
|
||||||
|
var result = await SendRequestAsync("callHierarchy/incomingCalls", new
|
||||||
|
{
|
||||||
|
item = items[0].RawItem
|
||||||
|
}, ct);
|
||||||
|
|
||||||
|
return ParseIncomingCalls(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>해당 심볼이 호출하는 하위 호출 대상을 찾습니다.</summary>
|
||||||
|
public async Task<List<LspCallHierarchyEntry>> GetOutgoingCallsAsync(string filePath, int line, int character, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var items = await PrepareCallHierarchyAsync(filePath, line, character, ct);
|
||||||
|
if (items.Count == 0)
|
||||||
|
return new List<LspCallHierarchyEntry>();
|
||||||
|
|
||||||
|
var result = await SendRequestAsync("callHierarchy/outgoingCalls", new
|
||||||
|
{
|
||||||
|
item = items[0].RawItem
|
||||||
|
}, ct);
|
||||||
|
|
||||||
|
return ParseOutgoingCalls(result);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── JSON-RPC 통신 ──────────────────────────────────────────────────────
|
// ─── JSON-RPC 통신 ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOpts = new()
|
private static readonly JsonSerializerOptions JsonOpts = new()
|
||||||
@@ -249,16 +330,7 @@ public class LspClientService : IDisposable
|
|||||||
var elem = result.Value;
|
var elem = result.Value;
|
||||||
if (elem.ValueKind == JsonValueKind.Array && elem.GetArrayLength() > 0)
|
if (elem.ValueKind == JsonValueKind.Array && elem.GetArrayLength() > 0)
|
||||||
elem = elem[0];
|
elem = elem[0];
|
||||||
if (elem.TryGetProperty("uri", out var uri) && elem.TryGetProperty("range", out var range))
|
return ParseLocationElement(elem);
|
||||||
{
|
|
||||||
var start = range.GetProperty("start");
|
|
||||||
return new LspLocation
|
|
||||||
{
|
|
||||||
FilePath = UriToFile(uri.GetString() ?? ""),
|
|
||||||
Line = start.GetProperty("line").GetInt32(),
|
|
||||||
Character = start.GetProperty("character").GetInt32(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
return null;
|
return null;
|
||||||
@@ -267,21 +339,56 @@ public class LspClientService : IDisposable
|
|||||||
private static List<LspLocation> ParseLocations(JsonElement? result)
|
private static List<LspLocation> ParseLocations(JsonElement? result)
|
||||||
{
|
{
|
||||||
var list = new List<LspLocation>();
|
var list = new List<LspLocation>();
|
||||||
if (result?.ValueKind != JsonValueKind.Array) return list;
|
if (result == null)
|
||||||
foreach (var elem in result.Value.EnumerateArray())
|
return list;
|
||||||
|
|
||||||
|
if (result.Value.ValueKind == JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
|
foreach (var elem in result.Value.EnumerateArray())
|
||||||
|
{
|
||||||
|
var parsed = ParseLocationElement(elem);
|
||||||
|
if (parsed != null)
|
||||||
|
list.Add(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var parsed = ParseLocationElement(result.Value);
|
||||||
|
if (parsed != null)
|
||||||
|
list.Add(parsed);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LspLocation? ParseLocationElement(JsonElement elem)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (elem.TryGetProperty("targetUri", out var targetUri) && elem.TryGetProperty("targetSelectionRange", out var targetSelectionRange))
|
||||||
|
{
|
||||||
|
var start = targetSelectionRange.GetProperty("start");
|
||||||
|
return new LspLocation
|
||||||
|
{
|
||||||
|
FilePath = UriToFile(targetUri.SafeGetString() ?? ""),
|
||||||
|
Line = start.GetProperty("line").GetInt32(),
|
||||||
|
Character = start.GetProperty("character").GetInt32(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (elem.TryGetProperty("uri", out var uri) && elem.TryGetProperty("range", out var range))
|
if (elem.TryGetProperty("uri", out var uri) && elem.TryGetProperty("range", out var range))
|
||||||
{
|
{
|
||||||
var start = range.GetProperty("start");
|
var start = range.GetProperty("start");
|
||||||
list.Add(new LspLocation
|
return new LspLocation
|
||||||
{
|
{
|
||||||
FilePath = UriToFile(uri.GetString() ?? ""),
|
FilePath = UriToFile(uri.SafeGetString() ?? ""),
|
||||||
Line = start.GetProperty("line").GetInt32(),
|
Line = start.GetProperty("line").GetInt32(),
|
||||||
Character = start.GetProperty("character").GetInt32(),
|
Character = start.GetProperty("character").GetInt32(),
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return list;
|
catch { }
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<LspSymbol> ParseSymbols(JsonElement? result)
|
private static List<LspSymbol> ParseSymbols(JsonElement? result)
|
||||||
@@ -311,6 +418,193 @@ public class LspClientService : IDisposable
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? ParseHover(JsonElement? result)
|
||||||
|
{
|
||||||
|
if (result == null || result.Value.ValueKind != JsonValueKind.Object)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!result.Value.TryGetProperty("contents", out var contents))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return contents.ValueKind switch
|
||||||
|
{
|
||||||
|
JsonValueKind.String => contents.SafeGetString(),
|
||||||
|
JsonValueKind.Object => contents.TryGetProperty("value", out var value)
|
||||||
|
? value.SafeGetString()
|
||||||
|
: contents.GetRawText(),
|
||||||
|
JsonValueKind.Array => string.Join(
|
||||||
|
"\n\n",
|
||||||
|
contents.EnumerateArray()
|
||||||
|
.Select(item => item.ValueKind == JsonValueKind.Object && item.TryGetProperty("value", out var v)
|
||||||
|
? v.SafeGetString()
|
||||||
|
: item.SafeGetString())
|
||||||
|
.Where(text => !string.IsNullOrWhiteSpace(text))),
|
||||||
|
_ => contents.SafeGetString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<LspWorkspaceSymbol> ParseWorkspaceSymbols(JsonElement? result)
|
||||||
|
{
|
||||||
|
var list = new List<LspWorkspaceSymbol>();
|
||||||
|
if (result?.ValueKind != JsonValueKind.Array)
|
||||||
|
return list;
|
||||||
|
|
||||||
|
foreach (var elem in result.Value.EnumerateArray())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var name = elem.TryGetProperty("name", out var nameEl) ? nameEl.SafeGetString() ?? "" : "";
|
||||||
|
var kind = elem.TryGetProperty("kind", out var kindEl) ? SymbolKindName(kindEl.GetInt32()) : "symbol";
|
||||||
|
var location = elem.TryGetProperty("location", out var locationEl)
|
||||||
|
? ParseLocationElement(locationEl)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
list.Add(new LspWorkspaceSymbol
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Kind = kind,
|
||||||
|
Location = location,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<LspCallHierarchyItem> ParseCallHierarchyItems(JsonElement? result)
|
||||||
|
{
|
||||||
|
var list = new List<LspCallHierarchyItem>();
|
||||||
|
if (result == null)
|
||||||
|
return list;
|
||||||
|
|
||||||
|
if (result.Value.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var elem in result.Value.EnumerateArray())
|
||||||
|
{
|
||||||
|
var item = ParseCallHierarchyItem(elem);
|
||||||
|
if (item != null)
|
||||||
|
list.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (result.Value.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
var item = ParseCallHierarchyItem(result.Value);
|
||||||
|
if (item != null)
|
||||||
|
list.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LspCallHierarchyItem? ParseCallHierarchyItem(JsonElement elem)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var uri = elem.TryGetProperty("uri", out var uriEl) ? uriEl.SafeGetString() ?? "" : "";
|
||||||
|
var name = elem.TryGetProperty("name", out var nameEl) ? nameEl.SafeGetString() ?? "" : "";
|
||||||
|
var kind = elem.TryGetProperty("kind", out var kindEl) ? SymbolKindName(kindEl.GetInt32()) : "symbol";
|
||||||
|
var selectionRange = elem.TryGetProperty("selectionRange", out var selectionRangeEl)
|
||||||
|
? selectionRangeEl
|
||||||
|
: elem.GetProperty("range");
|
||||||
|
var start = selectionRange.GetProperty("start");
|
||||||
|
|
||||||
|
return new LspCallHierarchyItem
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Kind = kind,
|
||||||
|
Location = new LspLocation
|
||||||
|
{
|
||||||
|
FilePath = UriToFile(uri),
|
||||||
|
Line = start.GetProperty("line").GetInt32(),
|
||||||
|
Character = start.GetProperty("character").GetInt32(),
|
||||||
|
},
|
||||||
|
RawItem = elem.Clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<LspCallHierarchyEntry> ParseIncomingCalls(JsonElement? result)
|
||||||
|
{
|
||||||
|
var list = new List<LspCallHierarchyEntry>();
|
||||||
|
if (result?.ValueKind != JsonValueKind.Array)
|
||||||
|
return list;
|
||||||
|
|
||||||
|
foreach (var elem in result.Value.EnumerateArray())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!elem.TryGetProperty("from", out var fromEl))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var item = ParseCallHierarchyItem(fromEl);
|
||||||
|
if (item == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var count = elem.TryGetProperty("fromRanges", out var ranges) && ranges.ValueKind == JsonValueKind.Array
|
||||||
|
? ranges.GetArrayLength()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
list.Add(new LspCallHierarchyEntry
|
||||||
|
{
|
||||||
|
Name = item.Name,
|
||||||
|
Kind = item.Kind,
|
||||||
|
Location = item.Location,
|
||||||
|
RangeCount = count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<LspCallHierarchyEntry> ParseOutgoingCalls(JsonElement? result)
|
||||||
|
{
|
||||||
|
var list = new List<LspCallHierarchyEntry>();
|
||||||
|
if (result?.ValueKind != JsonValueKind.Array)
|
||||||
|
return list;
|
||||||
|
|
||||||
|
foreach (var elem in result.Value.EnumerateArray())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!elem.TryGetProperty("to", out var toEl))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var item = ParseCallHierarchyItem(toEl);
|
||||||
|
if (item == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var count = elem.TryGetProperty("fromRanges", out var ranges) && ranges.ValueKind == JsonValueKind.Array
|
||||||
|
? ranges.GetArrayLength()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
list.Add(new LspCallHierarchyEntry
|
||||||
|
{
|
||||||
|
Name = item.Name,
|
||||||
|
Kind = item.Kind,
|
||||||
|
Location = item.Location,
|
||||||
|
RangeCount = count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
private static string SymbolKindName(int kind) => kind switch
|
private static string SymbolKindName(int kind) => kind switch
|
||||||
{
|
{
|
||||||
1 => "file", 2 => "module", 3 => "namespace", 4 => "package",
|
1 => "file", 2 => "module", 3 => "namespace", 4 => "package",
|
||||||
@@ -408,3 +702,34 @@ public class LspSymbol
|
|||||||
public int Line { get; init; }
|
public int Line { get; init; }
|
||||||
public override string ToString() => $"[{Kind}] {Name} (line {Line + 1})";
|
public override string ToString() => $"[{Kind}] {Name} (line {Line + 1})";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>워크스페이스 심볼 검색 결과.</summary>
|
||||||
|
public class LspWorkspaceSymbol
|
||||||
|
{
|
||||||
|
public string Name { get; init; } = "";
|
||||||
|
public string Kind { get; init; } = "";
|
||||||
|
public LspLocation? Location { get; init; }
|
||||||
|
public override string ToString() => Location == null
|
||||||
|
? $"[{Kind}] {Name}"
|
||||||
|
: $"[{Kind}] {Name} @ {Location}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>호출 계층 기준 아이템.</summary>
|
||||||
|
public class LspCallHierarchyItem
|
||||||
|
{
|
||||||
|
public string Name { get; init; } = "";
|
||||||
|
public string Kind { get; init; } = "";
|
||||||
|
public LspLocation Location { get; init; } = new();
|
||||||
|
public JsonElement RawItem { get; init; }
|
||||||
|
public override string ToString() => $"[{Kind}] {Name} @ {Location}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>호출 계층의 incoming/outgoing 엔트리.</summary>
|
||||||
|
public class LspCallHierarchyEntry
|
||||||
|
{
|
||||||
|
public string Name { get; init; } = "";
|
||||||
|
public string Kind { get; init; } = "";
|
||||||
|
public LspLocation Location { get; init; } = new();
|
||||||
|
public int RangeCount { get; init; }
|
||||||
|
public override string ToString() => $"[{Kind}] {Name} @ {Location} (matches: {RangeCount})";
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public record ModelRouteResult(
|
|||||||
/// - 사내 Ollama/vLLM 모델 추가: GetDefaultCapabilities()에 항목 추가
|
/// - 사내 Ollama/vLLM 모델 추가: GetDefaultCapabilities()에 항목 추가
|
||||||
/// - 확신도 조정: Route() 메서드의 confidence 체크 로직
|
/// - 확신도 조정: Route() 메서드의 confidence 체크 로직
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ModelRouterService
|
public class ModelRouterService : IModelRouterService
|
||||||
{
|
{
|
||||||
private readonly SettingsService _settings;
|
private readonly SettingsService _settings;
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ public static class PdfExportService
|
|||||||
|
|
||||||
private static string GetPrintStyles() => @"
|
private static string GetPrintStyles() => @"
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
body { font-family: 'Malgun Gothic', 'Segoe UI', sans-serif; font-size: 13px; color: #222; background: #fff; padding: 20px; }
|
body { font-family: 'Noto Sans KR', 'Segoe UI', sans-serif; font-size: 13px; color: #222; background: #fff; padding: 20px; }
|
||||||
.header { border-bottom: 2px solid #4B5EFC; padding-bottom: 12px; margin-bottom: 20px; }
|
.header { border-bottom: 2px solid #4B5EFC; padding-bottom: 12px; margin-bottom: 20px; }
|
||||||
.header h1 { font-size: 18px; font-weight: 700; color: #1a1b2e; }
|
.header h1 { font-size: 18px; font-weight: 700; color: #1a1b2e; }
|
||||||
.header .meta { font-size: 11px; color: #888; margin-top: 4px; }
|
.header .meta { font-size: 11px; color: #888; margin-top: 4px; }
|
||||||
|
|||||||
49
src/AxCopilot/Services/ServiceLocator.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace AxCopilot.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 전역 DI 서비스 로케이터.
|
||||||
|
/// 기존 수동 new 패턴에서 DI 컨테이너로의 점진적 전환을 지원합니다.
|
||||||
|
/// App.OnStartup()에서 Configure()를 호출하여 초기화합니다.
|
||||||
|
///
|
||||||
|
/// 사용법:
|
||||||
|
/// var settings = ServiceLocator.Get<ISettingsService>();
|
||||||
|
/// var storage = ServiceLocator.Get<IChatStorageService>();
|
||||||
|
/// </summary>
|
||||||
|
public static class ServiceLocator
|
||||||
|
{
|
||||||
|
private static IServiceProvider? _provider;
|
||||||
|
|
||||||
|
/// <summary>DI 컨테이너를 구성합니다. 앱 시작 시 1회만 호출합니다.</summary>
|
||||||
|
public static IServiceProvider Configure(Action<IServiceCollection> configureServices)
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
configureServices(services);
|
||||||
|
_provider = services.BuildServiceProvider(validateScopes: true);
|
||||||
|
return _provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>이미 생성된 ServiceProvider를 직접 설정합니다.</summary>
|
||||||
|
public static void SetProvider(IServiceProvider provider)
|
||||||
|
{
|
||||||
|
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>등록된 서비스를 가져옵니다.</summary>
|
||||||
|
public static T Get<T>() where T : notnull
|
||||||
|
{
|
||||||
|
if (_provider == null)
|
||||||
|
throw new InvalidOperationException("ServiceLocator가 초기화되지 않았습니다. App.OnStartup()에서 Configure()를 호출하세요.");
|
||||||
|
return _provider.GetRequiredService<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>등록된 서비스를 가져옵니다. 미등록 시 null을 반환합니다.</summary>
|
||||||
|
public static T? GetOptional<T>() where T : class
|
||||||
|
{
|
||||||
|
return _provider?.GetService<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>내부 ServiceProvider에 직접 접근합니다. (테스트/마이그레이션용)</summary>
|
||||||
|
public static IServiceProvider? Provider => _provider;
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ using AxCopilot.Services.Agent;
|
|||||||
|
|
||||||
namespace AxCopilot.Services;
|
namespace AxCopilot.Services;
|
||||||
|
|
||||||
public class SettingsService
|
public class SettingsService : ISettingsService
|
||||||
{
|
{
|
||||||
private static readonly string AppDataDir = InitAppDataDir();
|
private static readonly string AppDataDir = InitAppDataDir();
|
||||||
|
|
||||||
@@ -186,12 +186,13 @@ public class SettingsService
|
|||||||
}
|
}
|
||||||
catch (IOException) when (attempt < 2)
|
catch (IOException) when (attempt < 2)
|
||||||
{
|
{
|
||||||
Thread.Sleep(50 * (attempt + 1));
|
// SpinWait으로 짧은 지연 (Thread.Sleep보다 UI 블로킹 최소화)
|
||||||
|
Thread.SpinWait(50_000 * (attempt + 1));
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (attempt < 2)
|
catch (Exception ex) when (attempt < 2)
|
||||||
{
|
{
|
||||||
LogService.Warn($"settings.dat 저장 재시도 {attempt + 1}/3: {ex.Message}");
|
LogService.Warn($"settings.dat 저장 재시도 {attempt + 1}/3: {ex.Message}");
|
||||||
Thread.Sleep(50 * (attempt + 1));
|
Thread.SpinWait(50_000 * (attempt + 1));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -203,6 +204,42 @@ public class SettingsService
|
|||||||
SettingsChanged?.Invoke(this, EventArgs.Empty);
|
SettingsChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>비동기 설정 저장. UI 스레드에서 호출 시 블로킹 없이 저장합니다.</summary>
|
||||||
|
public async Task SaveAsync()
|
||||||
|
{
|
||||||
|
EnsureDirectories();
|
||||||
|
NormalizeRuntimeSettings();
|
||||||
|
var json = JsonSerializer.Serialize(_settings, JsonOptions);
|
||||||
|
var encrypted = CryptoService.PortableEncrypt(json);
|
||||||
|
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
lock (_saveLock)
|
||||||
|
{
|
||||||
|
var tmpPath = SettingsPath + ".tmp";
|
||||||
|
for (int attempt = 0; attempt < 3; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(tmpPath, encrypted);
|
||||||
|
File.Move(tmpPath, SettingsPath, overwrite: true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (attempt < 2)
|
||||||
|
{
|
||||||
|
LogService.Warn($"settings.dat 비동기 저장 재시도 {attempt + 1}/3: {ex.Message}");
|
||||||
|
Thread.Sleep(50 * (attempt + 1));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { if (File.Exists(tmpPath)) File.Delete(tmpPath); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
SettingsChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
private void NormalizeRuntimeSettings()
|
private void NormalizeRuntimeSettings()
|
||||||
{
|
{
|
||||||
var expressionLevel = (_settings.Llm.AgentUiExpressionLevel ?? "").Trim().ToLowerInvariant();
|
var expressionLevel = (_settings.Llm.AgentUiExpressionLevel ?? "").Trim().ToLowerInvariant();
|
||||||
|
|||||||
@@ -30,10 +30,34 @@ public static class TokenEstimator
|
|||||||
{
|
{
|
||||||
int total = 0;
|
int total = 0;
|
||||||
foreach (var m in messages)
|
foreach (var m in messages)
|
||||||
total += Estimate(m.Content) + 4; // 메시지 오버헤드
|
{
|
||||||
|
var content = m.Content;
|
||||||
|
// _tool_use_blocks JSON은 API 전송 시 구조화된 형식으로 변환되므로
|
||||||
|
// 원시 JSON 길이 대비 약 40% 할인 적용 (JSON 래퍼 오버헤드 제거)
|
||||||
|
if (m.Role == "assistant" && content != null && content.StartsWith("{\"_tool_use_blocks\""))
|
||||||
|
total += (int)(Estimate(content) * 0.6) + 4;
|
||||||
|
// tool_result JSON도 구조화 변환됨
|
||||||
|
else if (m.Role == "user" && content != null && content.StartsWith("{\"type\":\"tool_result\""))
|
||||||
|
total += (int)(Estimate(content) * 0.7) + 4;
|
||||||
|
else
|
||||||
|
total += Estimate(content ?? "") + 4;
|
||||||
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>시스템 프롬프트 + 도구 정의에 의한 기본 오버헤드 토큰을 추정합니다.</summary>
|
||||||
|
/// <param name="systemPromptLength">시스템 프롬프트 문자 수</param>
|
||||||
|
/// <param name="toolCount">등록된 도구 수</param>
|
||||||
|
/// <returns>추정 토큰 수</returns>
|
||||||
|
public static int EstimateBaseOverhead(int systemPromptLength, int toolCount)
|
||||||
|
{
|
||||||
|
// 시스템 프롬프트 토큰
|
||||||
|
var sysTokens = systemPromptLength > 0 ? (int)(systemPromptLength / 3.5) : 0;
|
||||||
|
// 도구 정의: 이름 + 설명 + 파라미터 스키마 ≈ 평균 180토큰/도구
|
||||||
|
var toolTokens = toolCount * 180;
|
||||||
|
return sysTokens + toolTokens;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>비용을 추정합니다 (USD 기준).</summary>
|
/// <summary>비용을 추정합니다 (USD 기준).</summary>
|
||||||
public static (double InputCost, double OutputCost) EstimateCost(
|
public static (double InputCost, double OutputCost) EstimateCost(
|
||||||
int promptTokens, int completionTokens, string service, string model)
|
int promptTokens, int completionTokens, string service, string model)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
<SolidColorBrush x:Key="LauncherBackground" Color="#F3F4F6"/>
|
<SolidColorBrush x:Key="LauncherBackground" Color="#FFFFFF"/>
|
||||||
<SolidColorBrush x:Key="ItemBackground" Color="#FCFCFD"/>
|
<SolidColorBrush x:Key="ItemBackground" Color="#F3F4F6"/>
|
||||||
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#E4E7EC"/>
|
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#E4E7EC"/>
|
||||||
<SolidColorBrush x:Key="ItemHoverBackground" Color="#ECEFF3"/>
|
<SolidColorBrush x:Key="ItemHoverBackground" Color="#ECEFF3"/>
|
||||||
<SolidColorBrush x:Key="PrimaryText" Color="#181A1F"/>
|
<SolidColorBrush x:Key="PrimaryText" Color="#181A1F"/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
<SolidColorBrush x:Key="LauncherBackground" Color="#FAF6EF"/>
|
<SolidColorBrush x:Key="LauncherBackground" Color="#FFFFFF"/>
|
||||||
<SolidColorBrush x:Key="ItemBackground" Color="#FFFDFC"/>
|
<SolidColorBrush x:Key="ItemBackground" Color="#FAF6EF"/>
|
||||||
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#F3E5D5"/>
|
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#F3E5D5"/>
|
||||||
<SolidColorBrush x:Key="ItemHoverBackground" Color="#F6ECE0"/>
|
<SolidColorBrush x:Key="ItemHoverBackground" Color="#F6ECE0"/>
|
||||||
<SolidColorBrush x:Key="PrimaryText" Color="#2B2118"/>
|
<SolidColorBrush x:Key="PrimaryText" Color="#2B2118"/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
<SolidColorBrush x:Key="LauncherBackground" Color="#F8FAFC"/>
|
<SolidColorBrush x:Key="LauncherBackground" Color="#FFFFFF"/>
|
||||||
<SolidColorBrush x:Key="ItemBackground" Color="#FFFFFF"/>
|
<SolidColorBrush x:Key="ItemBackground" Color="#F8FAFC"/>
|
||||||
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#E2E8F0"/>
|
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#E2E8F0"/>
|
||||||
<SolidColorBrush x:Key="ItemHoverBackground" Color="#EAF2FF"/>
|
<SolidColorBrush x:Key="ItemHoverBackground" Color="#EAF2FF"/>
|
||||||
<SolidColorBrush x:Key="PrimaryText" Color="#0F172A"/>
|
<SolidColorBrush x:Key="PrimaryText" Color="#0F172A"/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
<SolidColorBrush x:Key="LauncherBackground" Color="#ECEFF4"/>
|
<SolidColorBrush x:Key="LauncherBackground" Color="#FFFFFF"/>
|
||||||
<SolidColorBrush x:Key="ItemBackground" Color="#FFFFFF"/>
|
<SolidColorBrush x:Key="ItemBackground" Color="#ECEFF4"/>
|
||||||
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#E5E9F0"/>
|
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#E5E9F0"/>
|
||||||
<SolidColorBrush x:Key="ItemHoverBackground" Color="#E5E9F0"/>
|
<SolidColorBrush x:Key="ItemHoverBackground" Color="#E5E9F0"/>
|
||||||
<SolidColorBrush x:Key="PrimaryText" Color="#2E3440"/>
|
<SolidColorBrush x:Key="PrimaryText" Color="#2E3440"/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
<SolidColorBrush x:Key="LauncherBackground" Color="#F8FAFC"/>
|
<SolidColorBrush x:Key="LauncherBackground" Color="#FFFFFF"/>
|
||||||
<SolidColorBrush x:Key="ItemBackground" Color="#FFFFFF"/>
|
<SolidColorBrush x:Key="ItemBackground" Color="#F0F2F8"/>
|
||||||
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#E2E8F0"/>
|
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#E2E8F0"/>
|
||||||
<SolidColorBrush x:Key="ItemHoverBackground" Color="#EEF2F7"/>
|
<SolidColorBrush x:Key="ItemHoverBackground" Color="#EEF2F7"/>
|
||||||
<SolidColorBrush x:Key="PrimaryText" Color="#0F172A"/>
|
<SolidColorBrush x:Key="PrimaryText" Color="#0F172A"/>
|
||||||
|
|||||||
@@ -107,6 +107,28 @@ public class HexToColorConverter : IValueConverter
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>"#RRGGBB" 헥스 문자열 → SolidColorBrush. DataTemplate 아이콘 색상에 사용.</summary>
|
||||||
|
public class HexToBrushConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type t, object p, CultureInfo c)
|
||||||
|
{
|
||||||
|
if (value is string hex && !string.IsNullOrEmpty(hex))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var color = (Color)ColorConverter.ConvertFromString(hex);
|
||||||
|
var brush = new SolidColorBrush(color);
|
||||||
|
brush.Freeze();
|
||||||
|
return brush;
|
||||||
|
}
|
||||||
|
catch { /* 잘못된 값이면 기본 반환 */ }
|
||||||
|
}
|
||||||
|
return new SolidColorBrush(Color.FromRgb(0x6B, 0x72, 0x80));
|
||||||
|
}
|
||||||
|
public object ConvertBack(object v, Type t, object p, CultureInfo c) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>null 또는 빈 문자열이면 Collapsed, 값이 있으면 Visible.</summary>
|
/// <summary>null 또는 빈 문자열이면 Collapsed, 값이 있으면 Visible.</summary>
|
||||||
public class NullToCollapsedConverter : IValueConverter
|
public class NullToCollapsedConverter : IValueConverter
|
||||||
{
|
{
|
||||||
|
|||||||
276
src/AxCopilot/ViewModels/ChatWindowViewModel.cs
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using AxCopilot.Models;
|
||||||
|
|
||||||
|
namespace AxCopilot.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ChatWindow의 MVVM ViewModel.
|
||||||
|
/// XAML 바인딩 대상 속성을 노출합니다.
|
||||||
|
/// 기존 코드비하인드와 공존하며 점진적으로 확장합니다.
|
||||||
|
/// </summary>
|
||||||
|
public class ChatWindowViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
// ── 탭 상태 ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
private string _activeTab = "Chat";
|
||||||
|
public string ActiveTab
|
||||||
|
{
|
||||||
|
get => _activeTab;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _activeTab, value))
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(IsChat));
|
||||||
|
OnPropertyChanged(nameof(IsCowork));
|
||||||
|
OnPropertyChanged(nameof(IsCode));
|
||||||
|
OnPropertyChanged(nameof(TabDisplayName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsChat => ActiveTab == "Chat";
|
||||||
|
public bool IsCowork => ActiveTab == "Cowork";
|
||||||
|
public bool IsCode => ActiveTab == "Code";
|
||||||
|
public string TabDisplayName => ActiveTab switch
|
||||||
|
{
|
||||||
|
"Chat" => "AX Agent",
|
||||||
|
"Cowork" => "Cowork",
|
||||||
|
"Code" => "Code",
|
||||||
|
_ => ActiveTab,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 대화 제목 ────────────────────────────────────────────
|
||||||
|
|
||||||
|
private string _chatTitle = "";
|
||||||
|
public string ChatTitle
|
||||||
|
{
|
||||||
|
get => _chatTitle;
|
||||||
|
set => SetProperty(ref _chatTitle, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 스트리밍 상태 ────────────────────────────────────────
|
||||||
|
|
||||||
|
private bool _isStreaming;
|
||||||
|
public bool IsStreaming
|
||||||
|
{
|
||||||
|
get => _isStreaming;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _isStreaming, value))
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(CanSend));
|
||||||
|
OnPropertyChanged(nameof(SendButtonIcon));
|
||||||
|
OnPropertyChanged(nameof(StreamingStatusText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanSend => !IsStreaming;
|
||||||
|
public string SendButtonIcon => IsStreaming ? "\uE711" : "\uE724"; // Stop : Send
|
||||||
|
public string StreamingStatusText => IsStreaming ? "응답 생성 중..." : "";
|
||||||
|
|
||||||
|
// ── 모델 정보 ────────────────────────────────────────────
|
||||||
|
|
||||||
|
private string _modelLabel = "";
|
||||||
|
public string ModelLabel
|
||||||
|
{
|
||||||
|
get => _modelLabel;
|
||||||
|
set => SetProperty(ref _modelLabel, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _serviceLabel = "";
|
||||||
|
public string ServiceLabel
|
||||||
|
{
|
||||||
|
get => _serviceLabel;
|
||||||
|
set => SetProperty(ref _serviceLabel, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 토큰 사용량 ─────────────────────────────────────────
|
||||||
|
|
||||||
|
private int _contextTokens;
|
||||||
|
public int ContextTokens
|
||||||
|
{
|
||||||
|
get => _contextTokens;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _contextTokens, value))
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(ContextUsagePercent));
|
||||||
|
OnPropertyChanged(nameof(ContextUsageText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _maxContextTokens = 128000;
|
||||||
|
public int MaxContextTokens
|
||||||
|
{
|
||||||
|
get => _maxContextTokens;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _maxContextTokens, value))
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(ContextUsagePercent));
|
||||||
|
OnPropertyChanged(nameof(ContextUsageText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double ContextUsagePercent => MaxContextTokens > 0
|
||||||
|
? Math.Min(100.0, ContextTokens * 100.0 / MaxContextTokens)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
public string ContextUsageText => MaxContextTokens > 0
|
||||||
|
? $"{ContextTokens:N0} / {MaxContextTokens:N0}"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
// ── 대화 목록 ────────────────────────────────────────────
|
||||||
|
|
||||||
|
public ObservableCollection<ConversationItemViewModel> Conversations { get; } = new();
|
||||||
|
|
||||||
|
private int _remainingConversationCount;
|
||||||
|
public int RemainingConversationCount
|
||||||
|
{
|
||||||
|
get => _remainingConversationCount;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _remainingConversationCount, value))
|
||||||
|
OnPropertyChanged(nameof(HasMoreConversations));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasMoreConversations => RemainingConversationCount > 0;
|
||||||
|
|
||||||
|
private string _emptyConversationText = "";
|
||||||
|
public string EmptyConversationText
|
||||||
|
{
|
||||||
|
get => _emptyConversationText;
|
||||||
|
set => SetProperty(ref _emptyConversationText, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _selectedCategory = "";
|
||||||
|
public string SelectedCategory
|
||||||
|
{
|
||||||
|
get => _selectedCategory;
|
||||||
|
set => SetProperty(ref _selectedCategory, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _failedOnlyFilter;
|
||||||
|
public bool FailedOnlyFilter
|
||||||
|
{
|
||||||
|
get => _failedOnlyFilter;
|
||||||
|
set => SetProperty(ref _failedOnlyFilter, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _runningOnlyFilter;
|
||||||
|
public bool RunningOnlyFilter
|
||||||
|
{
|
||||||
|
get => _runningOnlyFilter;
|
||||||
|
set => SetProperty(ref _runningOnlyFilter, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _sortByRecent;
|
||||||
|
public bool SortByRecent
|
||||||
|
{
|
||||||
|
get => _sortByRecent;
|
||||||
|
set => SetProperty(ref _sortByRecent, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Git 상태 ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
private string _gitBranchName = "";
|
||||||
|
public string GitBranchName
|
||||||
|
{
|
||||||
|
get => _gitBranchName;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _gitBranchName, value))
|
||||||
|
OnPropertyChanged(nameof(HasGitBranch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasGitBranch => !string.IsNullOrEmpty(GitBranchName);
|
||||||
|
|
||||||
|
// ── 작업 폴더 ────────────────────────────────────────────
|
||||||
|
|
||||||
|
private string _workFolder = "";
|
||||||
|
public string WorkFolder
|
||||||
|
{
|
||||||
|
get => _workFolder;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _workFolder, value))
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(HasWorkFolder));
|
||||||
|
OnPropertyChanged(nameof(WorkFolderDisplay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasWorkFolder => !string.IsNullOrEmpty(WorkFolder);
|
||||||
|
public string WorkFolderDisplay => string.IsNullOrEmpty(WorkFolder) ? "폴더 선택" : WorkFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>대화 목록 항목 ViewModel — DataTemplate 바인딩 대상.</summary>
|
||||||
|
public class ConversationItemViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = "";
|
||||||
|
|
||||||
|
private string _title = "";
|
||||||
|
public string Title
|
||||||
|
{
|
||||||
|
get => _title;
|
||||||
|
set => SetProperty(ref _title, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _updatedAtText = "";
|
||||||
|
public string UpdatedAtText
|
||||||
|
{
|
||||||
|
get => _updatedAtText;
|
||||||
|
set => SetProperty(ref _updatedAtText, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _pinned;
|
||||||
|
public bool Pinned
|
||||||
|
{
|
||||||
|
get => _pinned;
|
||||||
|
set => SetProperty(ref _pinned, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _isSelected;
|
||||||
|
public bool IsSelected
|
||||||
|
{
|
||||||
|
get => _isSelected;
|
||||||
|
set => SetProperty(ref _isSelected, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _isRunning;
|
||||||
|
public bool IsRunning
|
||||||
|
{
|
||||||
|
get => _isRunning;
|
||||||
|
set => SetProperty(ref _isRunning, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Category { get; init; } = "general";
|
||||||
|
public string Symbol { get; init; } = "\uE8BD";
|
||||||
|
public string ColorHex { get; init; } = "#6B7280";
|
||||||
|
public string Tab { get; init; } = "Chat";
|
||||||
|
public string Preview { get; init; } = "";
|
||||||
|
public string? ParentId { get; init; }
|
||||||
|
public int AgentRunCount { get; init; }
|
||||||
|
public int FailedAgentRunCount { get; init; }
|
||||||
|
public string LastAgentRunSummary { get; init; } = "";
|
||||||
|
public string WorkFolder { get; init; } = "";
|
||||||
|
|
||||||
|
// ── 그룹 ──
|
||||||
|
public string Group { get; init; } = "오늘";
|
||||||
|
public int GroupOrder { get; init; }
|
||||||
|
|
||||||
|
// ── 표시용 computed ──
|
||||||
|
public bool IsBranch => !string.IsNullOrEmpty(ParentId);
|
||||||
|
public string IconText => Pinned ? "\uE718" : IsBranch ? "\uE8A5" : Symbol;
|
||||||
|
public string RunStatusText => FailedAgentRunCount > 0
|
||||||
|
? $"실패 {FailedAgentRunCount}"
|
||||||
|
: AgentRunCount > 0 ? $"실행 {AgentRunCount}" : "";
|
||||||
|
public bool HasRunStatus => AgentRunCount > 0 || FailedAgentRunCount > 0;
|
||||||
|
public bool HasFailed => FailedAgentRunCount > 0;
|
||||||
|
}
|
||||||
@@ -163,6 +163,8 @@ public class SettingsViewModel : INotifyPropertyChanged
|
|||||||
private bool _rememberPosition;
|
private bool _rememberPosition;
|
||||||
private bool _showPrefixBadge;
|
private bool _showPrefixBadge;
|
||||||
private bool _enableIconAnimation;
|
private bool _enableIconAnimation;
|
||||||
|
private bool _enableChatIconRandomAnimation;
|
||||||
|
private string _chatIconGlowIntensity = "medium";
|
||||||
private bool _enableRainbowGlow;
|
private bool _enableRainbowGlow;
|
||||||
private bool _enableSelectionGlow;
|
private bool _enableSelectionGlow;
|
||||||
private bool _enableRandomPlaceholder;
|
private bool _enableRandomPlaceholder;
|
||||||
@@ -849,6 +851,18 @@ public class SettingsViewModel : INotifyPropertyChanged
|
|||||||
set { _enableIconAnimation = value; OnPropertyChanged(); }
|
set { _enableIconAnimation = value; OnPropertyChanged(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool EnableChatIconRandomAnimation
|
||||||
|
{
|
||||||
|
get => _enableChatIconRandomAnimation;
|
||||||
|
set { _enableChatIconRandomAnimation = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ChatIconGlowIntensity
|
||||||
|
{
|
||||||
|
get => _chatIconGlowIntensity;
|
||||||
|
set { _chatIconGlowIntensity = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
public bool EnableRainbowGlow
|
public bool EnableRainbowGlow
|
||||||
{
|
{
|
||||||
get => _enableRainbowGlow;
|
get => _enableRainbowGlow;
|
||||||
@@ -1115,6 +1129,8 @@ public class SettingsViewModel : INotifyPropertyChanged
|
|||||||
_rememberPosition = s.Launcher.RememberPosition;
|
_rememberPosition = s.Launcher.RememberPosition;
|
||||||
_showPrefixBadge = s.Launcher.ShowPrefixBadge;
|
_showPrefixBadge = s.Launcher.ShowPrefixBadge;
|
||||||
_enableIconAnimation = s.Launcher.EnableIconAnimation;
|
_enableIconAnimation = s.Launcher.EnableIconAnimation;
|
||||||
|
_enableChatIconRandomAnimation = s.Launcher.EnableChatIconRandomAnimation;
|
||||||
|
_chatIconGlowIntensity = s.Launcher.ChatIconGlowIntensity ?? "medium";
|
||||||
_enableRainbowGlow = s.Launcher.EnableRainbowGlow;
|
_enableRainbowGlow = s.Launcher.EnableRainbowGlow;
|
||||||
_enableSelectionGlow = s.Launcher.EnableSelectionGlow;
|
_enableSelectionGlow = s.Launcher.EnableSelectionGlow;
|
||||||
_enableRandomPlaceholder = s.Launcher.EnableRandomPlaceholder;
|
_enableRandomPlaceholder = s.Launcher.EnableRandomPlaceholder;
|
||||||
@@ -1573,6 +1589,8 @@ public class SettingsViewModel : INotifyPropertyChanged
|
|||||||
s.Launcher.RememberPosition = _rememberPosition;
|
s.Launcher.RememberPosition = _rememberPosition;
|
||||||
s.Launcher.ShowPrefixBadge = _showPrefixBadge;
|
s.Launcher.ShowPrefixBadge = _showPrefixBadge;
|
||||||
s.Launcher.EnableIconAnimation = _enableIconAnimation;
|
s.Launcher.EnableIconAnimation = _enableIconAnimation;
|
||||||
|
s.Launcher.EnableChatIconRandomAnimation = _enableChatIconRandomAnimation;
|
||||||
|
s.Launcher.ChatIconGlowIntensity = _chatIconGlowIntensity;
|
||||||
s.Launcher.EnableRainbowGlow = _enableRainbowGlow;
|
s.Launcher.EnableRainbowGlow = _enableRainbowGlow;
|
||||||
s.Launcher.EnableSelectionGlow = _enableSelectionGlow;
|
s.Launcher.EnableSelectionGlow = _enableSelectionGlow;
|
||||||
s.Launcher.EnableRandomPlaceholder = _enableRandomPlaceholder;
|
s.Launcher.EnableRandomPlaceholder = _enableRandomPlaceholder;
|
||||||
|
|||||||
41
src/AxCopilot/ViewModels/ViewModelBase.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace AxCopilot.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MVVM ViewModel 기반 클래스.
|
||||||
|
/// INotifyPropertyChanged 구현과 SetProperty 헬퍼를 제공합니다.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class ViewModelBase : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
/// <summary>속성 변경 알림을 발생시킵니다.</summary>
|
||||||
|
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 백킹 필드를 업데이트하고, 값이 변경된 경우 PropertyChanged를 발생시킵니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>값이 실제로 변경되었으면 true.</returns>
|
||||||
|
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||||
|
return false;
|
||||||
|
field = value;
|
||||||
|
OnPropertyChanged(propertyName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 백킹 필드를 업데이트하고, 값이 변경된 경우 추가 콜백을 실행합니다.
|
||||||
|
/// </summary>
|
||||||
|
protected bool SetProperty<T>(ref T field, T value, Action onChanged, [CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
if (!SetProperty(ref field, value, propertyName))
|
||||||
|
return false;
|
||||||
|
onChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -87,7 +87,26 @@ public partial class ChatWindow
|
|||||||
{
|
{
|
||||||
var result = _chatEngine.AppendExecutionEvent(
|
var result = _chatEngine.AppendExecutionEvent(
|
||||||
session, _storage, _currentConversation, activeTab, eventTab, evt);
|
session, _storage, _currentConversation, activeTab, eventTab, evt);
|
||||||
_currentConversation = result.CurrentConversation;
|
// 방어: 결과 대화가 빈 대화로 교체되는 것을 방지
|
||||||
|
// EnsureCurrentConversation이 기존 대화 대신 새 빈 대화를 생성하는 경우 발생
|
||||||
|
var resultConv = result.CurrentConversation;
|
||||||
|
var currentMsgCount = _currentConversation?.Messages?.Count ?? 0;
|
||||||
|
var resultMsgCount = resultConv?.Messages?.Count ?? 0;
|
||||||
|
if (resultConv != null && resultMsgCount == 0 && currentMsgCount > 0
|
||||||
|
&& _currentConversation != null
|
||||||
|
&& string.Equals(
|
||||||
|
_currentConversation.Tab?.Trim(),
|
||||||
|
resultConv.Tab?.Trim(),
|
||||||
|
StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// 기존 대화 유지 — session에도 복원
|
||||||
|
session.CurrentConversation = _currentConversation;
|
||||||
|
LogService.Info($"[EventProc] 대화 교체 차단: 기존 msgCount={currentMsgCount}, 결과 msgCount={resultMsgCount}, convId={_currentConversation.Id?[..Math.Min(8, _currentConversation.Id?.Length ?? 0)]}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_currentConversation = resultConv;
|
||||||
|
}
|
||||||
pendingPersist = result.UpdatedConversation;
|
pendingPersist = result.UpdatedConversation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +128,17 @@ public partial class ChatWindow
|
|||||||
var summary = string.IsNullOrWhiteSpace(evt.Summary) ? "작업 완료" : evt.Summary;
|
var summary = string.IsNullOrWhiteSpace(evt.Summary) ? "작업 완료" : evt.Summary;
|
||||||
var result = _chatEngine.AppendAgentRun(
|
var result = _chatEngine.AppendAgentRun(
|
||||||
session, _storage, _currentConversation, activeTab, eventTab, evt, "completed", summary);
|
session, _storage, _currentConversation, activeTab, eventTab, evt, "completed", summary);
|
||||||
_currentConversation = result.CurrentConversation;
|
// 방어: Complete 이벤트에서도 대화 교체 보호
|
||||||
|
var completeMsgCount = result.CurrentConversation?.Messages?.Count ?? 0;
|
||||||
|
var existingMsgCount = _currentConversation?.Messages?.Count ?? 0;
|
||||||
|
if (completeMsgCount == 0 && existingMsgCount > 0 && _currentConversation != null)
|
||||||
|
{
|
||||||
|
session.CurrentConversation = _currentConversation;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_currentConversation = result.CurrentConversation;
|
||||||
|
}
|
||||||
pendingPersist = result.UpdatedConversation;
|
pendingPersist = result.UpdatedConversation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1064,6 +1064,15 @@ public partial class ChatWindow
|
|||||||
private void CollapseDecisionButtons(StackPanel outerStack, string resultText, Brush fg)
|
private void CollapseDecisionButtons(StackPanel outerStack, string resultText, Brush fg)
|
||||||
{
|
{
|
||||||
outerStack.Children.Clear();
|
outerStack.Children.Clear();
|
||||||
|
|
||||||
|
// 부모 컨테이너(Border)의 테두리·배경 제거 — 승인 후 깔끔하게
|
||||||
|
if (outerStack.Parent is Border containerBorder)
|
||||||
|
{
|
||||||
|
containerBorder.BorderThickness = new Thickness(0);
|
||||||
|
containerBorder.Background = Brushes.Transparent;
|
||||||
|
containerBorder.Padding = new Thickness(0, 2, 0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
var resultLabel = new TextBlock
|
var resultLabel = new TextBlock
|
||||||
{
|
{
|
||||||
Text = resultText,
|
Text = resultText,
|
||||||
@@ -1071,7 +1080,7 @@ public partial class ChatWindow
|
|||||||
FontWeight = FontWeights.SemiBold,
|
FontWeight = FontWeights.SemiBold,
|
||||||
Foreground = fg,
|
Foreground = fg,
|
||||||
Opacity = 0.8,
|
Opacity = 0.8,
|
||||||
Margin = new Thickness(0, 2, 0, 2),
|
Margin = new Thickness(40, 2, 0, 2),
|
||||||
};
|
};
|
||||||
outerStack.Children.Add(resultLabel);
|
outerStack.Children.Add(resultLabel);
|
||||||
}
|
}
|
||||||
|
|||||||
622
src/AxCopilot/Views/ChatWindow.CommandInteractionPresentation.cs
Normal file
@@ -0,0 +1,622 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
using AxCopilot.Models;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
using AxCopilot.Services.Agent;
|
||||||
|
|
||||||
|
namespace AxCopilot.Views;
|
||||||
|
|
||||||
|
public partial class ChatWindow
|
||||||
|
{
|
||||||
|
// ─── 대화 분기 (Fork) ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void ForkConversation(
|
||||||
|
ChatConversation source,
|
||||||
|
int atIndex,
|
||||||
|
string? branchHint = null,
|
||||||
|
string? branchContextMessage = null,
|
||||||
|
string? branchContextRunId = null)
|
||||||
|
{
|
||||||
|
var branchCount = _storage.LoadAllMeta()
|
||||||
|
.Count(m => m.ParentId == source.Id) + 1;
|
||||||
|
var fork = ChatSession?.CreateBranchConversation(source, atIndex, branchCount, branchHint, branchContextMessage, branchContextRunId)
|
||||||
|
?? new ChatConversation
|
||||||
|
{
|
||||||
|
Title = source.Title,
|
||||||
|
Tab = source.Tab,
|
||||||
|
Category = source.Category,
|
||||||
|
WorkFolder = source.WorkFolder,
|
||||||
|
SystemCommand = source.SystemCommand,
|
||||||
|
ParentId = source.Id,
|
||||||
|
BranchLabel = $"분기 {branchCount}",
|
||||||
|
BranchAtIndex = atIndex,
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_storage.Save(fork);
|
||||||
|
ShowToast($"분기 생성: {fork.Title}");
|
||||||
|
|
||||||
|
// 분기 대화로 전환
|
||||||
|
lock (_convLock)
|
||||||
|
{
|
||||||
|
_currentConversation = ChatSession?.SetCurrentConversation(_activeTab, fork, _storage) ?? fork;
|
||||||
|
SyncTabConversationIdsFromSession();
|
||||||
|
}
|
||||||
|
ViewModel.ChatTitle = fork.Title;
|
||||||
|
RenderMessages();
|
||||||
|
RefreshConversationList();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ShowToast($"분기 실패: {ex.Message}", "\uE783");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 커맨드 팔레트 ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void OpenCommandPalette()
|
||||||
|
{
|
||||||
|
var palette = new CommandPaletteWindow(ExecuteCommand) { Owner = this };
|
||||||
|
palette.ShowDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExecuteCommand(string commandId)
|
||||||
|
{
|
||||||
|
switch (commandId)
|
||||||
|
{
|
||||||
|
case "tab:chat": TabChat.IsChecked = true; break;
|
||||||
|
case "tab:cowork": TabCowork.IsChecked = true; break;
|
||||||
|
case "tab:code": if (TabCode.IsEnabled) TabCode.IsChecked = true; break;
|
||||||
|
case "new_conversation": StartNewConversation(); break;
|
||||||
|
case "search_conversation": ToggleMessageSearch(); break;
|
||||||
|
case "change_model": BtnModelSelector_Click(this, new RoutedEventArgs()); break;
|
||||||
|
case "open_settings": BtnSettings_Click(this, new RoutedEventArgs()); break;
|
||||||
|
case "open_statistics": new StatisticsWindow().Show(); break;
|
||||||
|
case "change_folder": FolderPathLabel_Click(FolderPathLabel, null!); break;
|
||||||
|
case "toggle_devmode":
|
||||||
|
var llm = _settings.Settings.Llm;
|
||||||
|
llm.DevMode = !llm.DevMode;
|
||||||
|
_settings.Save();
|
||||||
|
UpdateAnalyzerButtonVisibility();
|
||||||
|
ShowToast(llm.DevMode ? "개발자 모드 켜짐" : "개발자 모드 꺼짐");
|
||||||
|
break;
|
||||||
|
case "open_audit_log":
|
||||||
|
try { System.Diagnostics.Process.Start("explorer.exe", Services.AuditLogService.GetAuditFolder()); } catch { }
|
||||||
|
break;
|
||||||
|
case "paste_clipboard":
|
||||||
|
try { var text = Clipboard.GetText(); if (!string.IsNullOrEmpty(text)) InputBox.Text += text; } catch { }
|
||||||
|
break;
|
||||||
|
case "export_conversation": ExportConversation(); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExportConversation()
|
||||||
|
{
|
||||||
|
ChatConversation? conv;
|
||||||
|
lock (_convLock) conv = _currentConversation;
|
||||||
|
if (conv == null || conv.Messages.Count == 0) return;
|
||||||
|
|
||||||
|
var dlg = new Microsoft.Win32.SaveFileDialog
|
||||||
|
{
|
||||||
|
FileName = $"{conv.Title}",
|
||||||
|
DefaultExt = ".md",
|
||||||
|
Filter = "Markdown (*.md)|*.md|JSON (*.json)|*.json|HTML (*.html)|*.html|PDF 인쇄용 HTML (*.pdf.html)|*.pdf.html|Text (*.txt)|*.txt"
|
||||||
|
};
|
||||||
|
if (dlg.ShowDialog() != true) return;
|
||||||
|
|
||||||
|
var ext = System.IO.Path.GetExtension(dlg.FileName).ToLowerInvariant();
|
||||||
|
string content;
|
||||||
|
|
||||||
|
if (ext == ".json")
|
||||||
|
{
|
||||||
|
content = System.Text.Json.JsonSerializer.Serialize(conv, new System.Text.Json.JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (dlg.FileName.EndsWith(".pdf.html"))
|
||||||
|
{
|
||||||
|
// PDF 인쇄용 HTML — 브라우저에서 자동으로 인쇄 대화상자 표시
|
||||||
|
content = PdfExportService.BuildHtml(conv);
|
||||||
|
System.IO.File.WriteAllText(dlg.FileName, content, System.Text.Encoding.UTF8);
|
||||||
|
PdfExportService.OpenInBrowser(dlg.FileName);
|
||||||
|
ShowToast("PDF 인쇄용 HTML이 생성되어 브라우저에서 열렸습니다");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (ext == ".html")
|
||||||
|
{
|
||||||
|
content = ExportToHtml(conv);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
sb.AppendLine($"# {conv.Title}");
|
||||||
|
sb.AppendLine($"_생성: {conv.CreatedAt:yyyy-MM-dd HH:mm} · 주제: {conv.Category}_");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
foreach (var msg in conv.Messages)
|
||||||
|
{
|
||||||
|
if (msg.Role == "system") continue;
|
||||||
|
var label = msg.Role == "user" ? "**사용자**" : "**AI**";
|
||||||
|
sb.AppendLine($"{label} ({msg.Timestamp:HH:mm})");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(msg.Content);
|
||||||
|
if (msg.AttachedFiles is { Count: > 0 })
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("_첨부 파일: " + string.Join(", ", msg.AttachedFiles.Select(System.IO.Path.GetFileName)) + "_");
|
||||||
|
}
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("---");
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
content = sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
System.IO.File.WriteAllText(dlg.FileName, content, System.Text.Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExportToHtml(ChatConversation conv)
|
||||||
|
{
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
sb.AppendLine("<!DOCTYPE html><html><head><meta charset=\"utf-8\">");
|
||||||
|
sb.AppendLine($"<title>{System.Net.WebUtility.HtmlEncode(conv.Title)}</title>");
|
||||||
|
sb.AppendLine("<style>body{font-family:'Segoe UI',sans-serif;max-width:800px;margin:0 auto;padding:20px;background:#1a1a2e;color:#e0e0e0}");
|
||||||
|
sb.AppendLine(".msg{margin:12px 0;padding:12px 16px;border-radius:12px}.user{background:#2d2d5e;margin-left:60px}.ai{background:#1e1e3a;margin-right:60px}");
|
||||||
|
sb.AppendLine(".meta{font-size:11px;color:#888;margin-bottom:6px}.content{white-space:pre-wrap;line-height:1.6}");
|
||||||
|
sb.AppendLine("h1{text-align:center;color:#8b6dff}pre{background:#111;padding:12px;border-radius:8px;overflow-x:auto}</style></head><body>");
|
||||||
|
sb.AppendLine($"<h1>{System.Net.WebUtility.HtmlEncode(conv.Title)}</h1>");
|
||||||
|
sb.AppendLine($"<p style='text-align:center;color:#888'>생성: {conv.CreatedAt:yyyy-MM-dd HH:mm} · 주제: {conv.Category}</p>");
|
||||||
|
|
||||||
|
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($"<div class='msg {cls}'>");
|
||||||
|
sb.AppendLine($"<div class='meta'>{label} · {msg.Timestamp:HH:mm}</div>");
|
||||||
|
sb.AppendLine($"<div class='content'>{System.Net.WebUtility.HtmlEncode(msg.Content)}</div>");
|
||||||
|
sb.AppendLine("</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine("</body></html>");
|
||||||
|
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<int> _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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
private void ApplyConversationListPreferences(ChatConversation? conv)
|
||||||
{
|
{
|
||||||
_failedOnlyFilter = false;
|
_failedOnlyFilter = false;
|
||||||
_runningOnlyFilter = false;
|
_runningOnlyFilter = false;
|
||||||
|
_archiveFilter = null;
|
||||||
_sortConversationsByRecent = string.Equals(conv?.ConversationSortMode, "recent", StringComparison.OrdinalIgnoreCase);
|
_sortConversationsByRecent = string.Equals(conv?.ConversationSortMode, "recent", StringComparison.OrdinalIgnoreCase);
|
||||||
UpdateConversationFailureFilterUi();
|
UpdateConversationFailureFilterUi();
|
||||||
UpdateConversationRunningFilterUi();
|
UpdateConversationRunningFilterUi();
|
||||||
UpdateConversationSortUi();
|
UpdateConversationSortUi();
|
||||||
|
UpdateArchiveFilterUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PersistConversationListPreferences()
|
private void PersistConversationListPreferences()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Windows.Media;
|
|||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
using AxCopilot.Models;
|
using AxCopilot.Models;
|
||||||
using AxCopilot.Services;
|
using AxCopilot.Services;
|
||||||
|
using AxCopilot.ViewModels;
|
||||||
|
|
||||||
namespace AxCopilot.Views;
|
namespace AxCopilot.Views;
|
||||||
|
|
||||||
@@ -43,6 +44,115 @@ public partial class ChatWindow
|
|||||||
ConversationPanel.MouseLeave += ConversationPanel_DelegatedMouseLeave;
|
ConversationPanel.MouseLeave += ConversationPanel_DelegatedMouseLeave;
|
||||||
ConversationPanel.PreviewMouseLeftButtonDown += ConversationPanel_DelegatedLeftButtonDown;
|
ConversationPanel.PreviewMouseLeftButtonDown += ConversationPanel_DelegatedLeftButtonDown;
|
||||||
ConversationPanel.PreviewMouseRightButtonUp += ConversationPanel_DelegatedRightButtonUp;
|
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<ConversationItemViewModel>(btn);
|
||||||
|
if (vm != null)
|
||||||
|
ShowConversationMenu(vm.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConversationItemsControl_LeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (FindAncestor<Button>(e.OriginalSource as DependencyObject) is not null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var vm = FindDataContext<ConversationItemViewModel>(e.OriginalSource as DependencyObject);
|
||||||
|
if (vm == null) return;
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
HandleConversationItemClickById(vm.Id, vm.IsSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConversationItemsControl_RightButtonUp(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
var vm = FindDataContext<ConversationItemViewModel>(e.OriginalSource as DependencyObject);
|
||||||
|
if (vm == null) return;
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
if (!vm.IsSelected)
|
||||||
|
{
|
||||||
|
var conv = _storage.Load(vm.Id);
|
||||||
|
if (conv != null)
|
||||||
|
{
|
||||||
|
lock (_convLock)
|
||||||
|
{
|
||||||
|
_currentConversation = ChatSession?.SetCurrentConversation(_activeTab, conv, _storage) ?? conv;
|
||||||
|
SyncTabConversationIdsFromSession();
|
||||||
|
}
|
||||||
|
SaveLastConversations();
|
||||||
|
UpdateChatTitle();
|
||||||
|
RenderMessages();
|
||||||
|
RefreshDraftQueueUi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Dispatcher.BeginInvoke(new Action(() => ShowConversationMenu(vm.Id)), DispatcherPriority.Input);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleConversationItemClickById(string id, bool isSelected)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (isSelected)
|
||||||
|
{
|
||||||
|
// 선택된 항목 클릭 → 이름 변경 모드
|
||||||
|
var titleBlock = FindConversationTitleBlock(id);
|
||||||
|
if (titleBlock != null)
|
||||||
|
{
|
||||||
|
var titleColor = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||||
|
EnterTitleEditMode(titleBlock, id, titleColor);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 스트리밍 중이면 포괄적 정리 (ViewModel.IsStreaming, 글로우, 타이머 등 모두 리셋)
|
||||||
|
StopStreamingIfActive();
|
||||||
|
|
||||||
|
var conv = _storage.Load(id);
|
||||||
|
if (conv == null) return;
|
||||||
|
|
||||||
|
lock (_convLock)
|
||||||
|
{
|
||||||
|
_currentConversation = ChatSession?.SetCurrentConversation(_activeTab, conv, _storage) ?? conv;
|
||||||
|
SyncTabConversationIdsFromSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveLastConversations();
|
||||||
|
UpdateChatTitle();
|
||||||
|
ClearTranscriptElements(); // 이전 대화의 UI 요소 완전 제거
|
||||||
|
InvalidateTimelineCache(); // 타임라인 캐시 무효화 — 새 대화 데이터 반영 보장
|
||||||
|
RenderMessages();
|
||||||
|
EnsureEmptyStateConsistency(); // EmptyState 일관성 강제 검사
|
||||||
|
RefreshConversationList();
|
||||||
|
RefreshDraftQueueUi();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogService.Error($"대화 전환 오류: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>VisualTree를 올라가며 지정 타입의 DataContext를 찾습니다.</summary>
|
||||||
|
private static T? FindDataContext<T>(DependencyObject? element) where T : class
|
||||||
|
{
|
||||||
|
while (element != null)
|
||||||
|
{
|
||||||
|
if (element is FrameworkElement fe && fe.DataContext is T ctx)
|
||||||
|
return ctx;
|
||||||
|
element = VisualTreeHelper.GetParent(element);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ConversationPanel_DelegatedMouseMove(object sender, MouseEventArgs e)
|
private void ConversationPanel_DelegatedMouseMove(object sender, MouseEventArgs e)
|
||||||
@@ -119,17 +229,8 @@ public partial class ChatWindow
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_streamingTabs.Contains(_activeTab))
|
// 스트리밍 중이면 포괄적 정리 (ViewModel.IsStreaming, 글로우, 타이머 등 모두 리셋)
|
||||||
{
|
StopStreamingIfActive();
|
||||||
if (_tabStreamCts.TryGetValue(_activeTab, out var convListCts)) convListCts.Cancel();
|
|
||||||
_cursorTimer.Stop();
|
|
||||||
_typingTimer.Stop();
|
|
||||||
_elapsedTimer.Stop();
|
|
||||||
_activeStreamText = null;
|
|
||||||
_elapsedLabel = null;
|
|
||||||
_streamingTabs.Remove(_activeTab);
|
|
||||||
if (_tabStreamCts.Remove(_activeTab, out var removedCts)) removedCts.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
var conv = _storage.Load(tag.Id);
|
var conv = _storage.Load(tag.Id);
|
||||||
if (conv == null)
|
if (conv == null)
|
||||||
@@ -218,6 +319,7 @@ public partial class ChatWindow
|
|||||||
LastFailedAt = runSummary.LastFailedAt,
|
LastFailedAt = runSummary.LastFailedAt,
|
||||||
LastCompletedAt = runSummary.LastCompletedAt,
|
LastCompletedAt = runSummary.LastCompletedAt,
|
||||||
WorkFolder = c.WorkFolder ?? "",
|
WorkFolder = c.WorkFolder ?? "",
|
||||||
|
Archived = c.Archived,
|
||||||
IsRunning = _currentConversation?.Id == c.Id
|
IsRunning = _currentConversation?.Id == c.Id
|
||||||
&& !string.IsNullOrWhiteSpace(_appState.AgentRun.RunId)
|
&& !string.IsNullOrWhiteSpace(_appState.AgentRun.RunId)
|
||||||
&& !string.Equals(_appState.AgentRun.Status, "completed", StringComparison.OrdinalIgnoreCase)
|
&& !string.Equals(_appState.AgentRun.Status, "completed", StringComparison.OrdinalIgnoreCase)
|
||||||
@@ -227,15 +329,22 @@ public partial class ChatWindow
|
|||||||
|
|
||||||
// LINQ 체인을 한 번의 Where로 병합하여 중간 리스트 할당 제거
|
// LINQ 체인을 한 번의 Where로 병합하여 중간 리스트 할당 제거
|
||||||
items = items.Where(i =>
|
items = items.Where(i =>
|
||||||
string.Equals(i.Tab, _activeTab, StringComparison.OrdinalIgnoreCase)
|
{
|
||||||
&& (i.Pinned
|
if (!string.Equals(i.Tab, _activeTab, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// 아카이브 필터: null=일반만(비아카이브), true=아카이브만, false(미사용)=전체
|
||||||
|
if (_archiveFilter == null && i.Archived) return false;
|
||||||
|
if (_archiveFilter == true && !i.Archived) return false;
|
||||||
|
|
||||||
|
return i.Pinned
|
||||||
|| !string.IsNullOrWhiteSpace(i.ParentId)
|
|| !string.IsNullOrWhiteSpace(i.ParentId)
|
||||||
|| !string.Equals((i.Title ?? "").Trim(), "새 대화", StringComparison.OrdinalIgnoreCase)
|
|| !string.Equals((i.Title ?? "").Trim(), "새 대화", StringComparison.OrdinalIgnoreCase)
|
||||||
|| !string.IsNullOrWhiteSpace(i.Preview)
|
|| !string.IsNullOrWhiteSpace(i.Preview)
|
||||||
|| i.AgentRunCount > 0
|
|| i.AgentRunCount > 0
|
||||||
|| i.FailedAgentRunCount > 0
|
|| i.FailedAgentRunCount > 0
|
||||||
|| !string.Equals(i.Category, ChatCategory.General, StringComparison.OrdinalIgnoreCase))
|
|| !string.Equals(i.Category, ChatCategory.General, StringComparison.OrdinalIgnoreCase);
|
||||||
).ToList();
|
}).ToList();
|
||||||
|
|
||||||
// Count를 한 번의 루프로 계산 (3번 순회 → 1번)
|
// Count를 한 번의 루프로 계산 (3번 순회 → 1번)
|
||||||
int failedCount = 0, runningCount = 0, spotlightCount = 0;
|
int failedCount = 0, runningCount = 0, spotlightCount = 0;
|
||||||
@@ -300,7 +409,133 @@ public partial class ChatWindow
|
|||||||
.ThenByDescending(i => i.UpdatedAt))
|
.ThenByDescending(i => i.UpdatedAt))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
// 스크롤 위치 보존: 대화 전환 시 목록이 맨 위로 점프하는 현상 방지
|
||||||
|
double savedScrollOffset = 0;
|
||||||
|
bool hasScrollViewer = ConversationListScrollViewer != null;
|
||||||
|
if (hasScrollViewer)
|
||||||
|
savedScrollOffset = ConversationListScrollViewer!.VerticalOffset;
|
||||||
|
|
||||||
|
SyncConversationsToViewModel(items);
|
||||||
RenderConversationList(items);
|
RenderConversationList(items);
|
||||||
|
|
||||||
|
// 저장된 스크롤 위치 복원
|
||||||
|
if (hasScrollViewer && savedScrollOffset > 0)
|
||||||
|
{
|
||||||
|
Dispatcher.BeginInvoke(new Action(() =>
|
||||||
|
{
|
||||||
|
ConversationListScrollViewer?.ScrollToVerticalOffset(savedScrollOffset);
|
||||||
|
}), System.Windows.Threading.DispatcherPriority.Loaded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ConversationMeta>? _allFilteredItems;
|
||||||
|
|
||||||
|
private void SyncConversationsToViewModel(List<ConversationMeta> items)
|
||||||
|
{
|
||||||
|
var currentId = "";
|
||||||
|
lock (_convLock)
|
||||||
|
currentId = _currentConversation?.Id ?? "";
|
||||||
|
|
||||||
|
var spotlightIds = new HashSet<string>(
|
||||||
|
BuildConversationSpotlightItems(items).Select(s => s.Id));
|
||||||
|
|
||||||
|
ViewModel.Conversations.Clear();
|
||||||
|
|
||||||
|
if (items.Count == 0)
|
||||||
|
{
|
||||||
|
ViewModel.EmptyConversationText = _activeTab switch
|
||||||
|
{
|
||||||
|
"Cowork" => "Cowork 탭 대화가 없습니다",
|
||||||
|
"Code" => "Code 탭 대화가 없습니다",
|
||||||
|
_ => "Chat 탭 대화가 없습니다",
|
||||||
|
};
|
||||||
|
ViewModel.RemainingConversationCount = 0;
|
||||||
|
_allFilteredItems = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewModel.EmptyConversationText = "";
|
||||||
|
|
||||||
|
// 스포트라이트 항목 먼저 (GroupOrder=0)
|
||||||
|
foreach (var item in items.Where(i => spotlightIds.Contains(i.Id)))
|
||||||
|
ViewModel.Conversations.Add(BuildConversationItemVm(item, currentId, "집중 필요", 0));
|
||||||
|
|
||||||
|
// 날짜 그룹 항목 — 페이지네이션 (GroupOrder=1+)
|
||||||
|
var dateGrouped = items.Select(item =>
|
||||||
|
{
|
||||||
|
var group = GetConversationDateGroup(item.UpdatedAt);
|
||||||
|
var groupOrder = group switch { "오늘" => 1, "어제" => 2, _ => 3 };
|
||||||
|
return (item, group, groupOrder);
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
var pageItems = dateGrouped
|
||||||
|
.Where(x => !spotlightIds.Contains(x.item.Id))
|
||||||
|
.Take(ConversationPageSize).ToList();
|
||||||
|
foreach (var (item, group, groupOrder) in pageItems)
|
||||||
|
ViewModel.Conversations.Add(BuildConversationItemVm(item, currentId, group, groupOrder));
|
||||||
|
|
||||||
|
var totalNonSpotlight = dateGrouped.Count(x => !spotlightIds.Contains(x.item.Id));
|
||||||
|
var remaining = totalNonSpotlight - pageItems.Count;
|
||||||
|
ViewModel.RemainingConversationCount = remaining;
|
||||||
|
_allFilteredItems = remaining > 0 ? items : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>ViewModel의 "더 보기" 요청을 처리하여 나머지 항목을 로드합니다.</summary>
|
||||||
|
internal void LoadAllConversations()
|
||||||
|
{
|
||||||
|
if (_allFilteredItems == null) return;
|
||||||
|
|
||||||
|
var items = _allFilteredItems;
|
||||||
|
_allFilteredItems = null;
|
||||||
|
|
||||||
|
var currentId = "";
|
||||||
|
lock (_convLock)
|
||||||
|
currentId = _currentConversation?.Id ?? "";
|
||||||
|
|
||||||
|
var spotlightIds = new HashSet<string>(
|
||||||
|
BuildConversationSpotlightItems(items).Select(s => s.Id));
|
||||||
|
|
||||||
|
ViewModel.Conversations.Clear();
|
||||||
|
|
||||||
|
// 스포트라이트
|
||||||
|
foreach (var item in items.Where(i => spotlightIds.Contains(i.Id)))
|
||||||
|
ViewModel.Conversations.Add(BuildConversationItemVm(item, currentId, "집중 필요", 0));
|
||||||
|
|
||||||
|
// 전체 날짜 그룹 (스포트라이트 제외 — 이미 위에서 추가됨)
|
||||||
|
foreach (var item in items.Where(i => !spotlightIds.Contains(i.Id)))
|
||||||
|
{
|
||||||
|
var group = GetConversationDateGroup(item.UpdatedAt);
|
||||||
|
var groupOrder = group switch { "오늘" => 1, "어제" => 2, _ => 3 };
|
||||||
|
ViewModel.Conversations.Add(BuildConversationItemVm(item, currentId, group, groupOrder));
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewModel.RemainingConversationCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConversationItemViewModel BuildConversationItemVm(
|
||||||
|
ConversationMeta item, string currentId, string group, int groupOrder)
|
||||||
|
{
|
||||||
|
return new ConversationItemViewModel
|
||||||
|
{
|
||||||
|
Id = item.Id,
|
||||||
|
Title = item.Title,
|
||||||
|
UpdatedAtText = item.UpdatedAtText,
|
||||||
|
Pinned = item.Pinned,
|
||||||
|
IsSelected = item.Id == currentId,
|
||||||
|
IsRunning = item.IsRunning,
|
||||||
|
Category = item.Category,
|
||||||
|
Symbol = item.Symbol,
|
||||||
|
ColorHex = item.ColorHex,
|
||||||
|
Tab = item.Tab,
|
||||||
|
Preview = item.Preview,
|
||||||
|
ParentId = item.ParentId,
|
||||||
|
AgentRunCount = item.AgentRunCount,
|
||||||
|
FailedAgentRunCount = item.FailedAgentRunCount,
|
||||||
|
LastAgentRunSummary = item.LastAgentRunSummary,
|
||||||
|
WorkFolder = item.WorkFolder,
|
||||||
|
Group = group,
|
||||||
|
GroupOrder = groupOrder,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RenderConversationList(List<ConversationMeta> items)
|
private void RenderConversationList(List<ConversationMeta> items)
|
||||||
@@ -514,7 +749,7 @@ public partial class ChatWindow
|
|||||||
var icon = new TextBlock
|
var icon = new TextBlock
|
||||||
{
|
{
|
||||||
Text = iconText,
|
Text = iconText,
|
||||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
FontFamily = s_segoeIconFont,
|
||||||
FontSize = 10.5,
|
FontSize = 10.5,
|
||||||
Foreground = iconBrush,
|
Foreground = iconBrush,
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
@@ -587,7 +822,7 @@ public partial class ChatWindow
|
|||||||
Content = new TextBlock
|
Content = new TextBlock
|
||||||
{
|
{
|
||||||
Text = "\uE70F",
|
Text = "\uE70F",
|
||||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
FontFamily = s_segoeIconFont,
|
||||||
FontSize = 9,
|
FontSize = 9,
|
||||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||||
},
|
},
|
||||||
@@ -628,4 +863,85 @@ public partial class ChatWindow
|
|||||||
|
|
||||||
ConversationPanel.Children.Add(border);
|
ConversationPanel.Children.Add(border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LoadMoreBorder_Click(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
LoadAllConversations();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 대화 ID에 해당하는 제목 TextBlock을 찾습니다.
|
||||||
|
/// ItemsControl(새) → ConversationPanel(레거시) 순서로 탐색합니다.
|
||||||
|
/// </summary>
|
||||||
|
private TextBlock? FindConversationTitleBlock(string conversationId)
|
||||||
|
{
|
||||||
|
// 1) 새 ItemsControl에서 검색
|
||||||
|
if (ConversationItemsControl is { Visibility: Visibility.Visible })
|
||||||
|
{
|
||||||
|
var titleBlock = FindTitleBlockInItemsControl(conversationId);
|
||||||
|
if (titleBlock != null) return titleBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 레거시 ConversationPanel에서 검색
|
||||||
|
foreach (UIElement child in ConversationPanel.Children)
|
||||||
|
{
|
||||||
|
if (child is not Border b || b.Tag is not ConversationItemTag tag) continue;
|
||||||
|
if (tag.Id == conversationId && tag.TitleBlock != null)
|
||||||
|
return tag.TitleBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ItemsControl 비주얼 트리에서 특정 대화 ID의 ConvTitleBlock을 찾습니다.
|
||||||
|
/// GroupStyle 사용 시 ContainerFromIndex가 올바르게 동작하지 않으므로
|
||||||
|
/// 비주얼 트리를 직접 탐색하여 DataContext가 일치하는 ContentPresenter를 찾습니다.
|
||||||
|
/// </summary>
|
||||||
|
private TextBlock? FindTitleBlockInItemsControl(string conversationId)
|
||||||
|
{
|
||||||
|
if (ConversationItemsControl == null) return null;
|
||||||
|
|
||||||
|
// GroupStyle이 적용된 ItemsControl은 비주얼 트리를 직접 탐색해야 함
|
||||||
|
var presenters = new List<ContentPresenter>();
|
||||||
|
CollectContentPresenters(ConversationItemsControl, presenters);
|
||||||
|
|
||||||
|
foreach (var presenter in presenters)
|
||||||
|
{
|
||||||
|
if (presenter.DataContext is ConversationItemViewModel vm && vm.Id == conversationId)
|
||||||
|
{
|
||||||
|
var titleBlock = FindNamedDescendant<TextBlock>(presenter, "ConvTitleBlock");
|
||||||
|
if (titleBlock != null) return titleBlock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>비주얼 트리에서 모든 ContentPresenter를 수집합니다.</summary>
|
||||||
|
private static void CollectContentPresenters(DependencyObject parent, List<ContentPresenter> result)
|
||||||
|
{
|
||||||
|
int count = VisualTreeHelper.GetChildrenCount(parent);
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var child = VisualTreeHelper.GetChild(parent, i);
|
||||||
|
if (child is ContentPresenter cp && cp.DataContext is ConversationItemViewModel)
|
||||||
|
result.Add(cp);
|
||||||
|
else
|
||||||
|
CollectContentPresenters(child, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>비주얼 트리를 DFS로 순회하며 지정 이름의 요소를 찾습니다.</summary>
|
||||||
|
private static T? FindNamedDescendant<T>(DependencyObject parent, string name) where T : FrameworkElement
|
||||||
|
{
|
||||||
|
int count = VisualTreeHelper.GetChildrenCount(parent);
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var child = VisualTreeHelper.GetChild(parent, i);
|
||||||
|
if (child is T fe && fe.Name == name) return fe;
|
||||||
|
var result = FindNamedDescendant<T>(child, name);
|
||||||
|
if (result != null) return result;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ public partial class ChatWindow
|
|||||||
var iconTb = new TextBlock
|
var iconTb = new TextBlock
|
||||||
{
|
{
|
||||||
Text = icon,
|
Text = icon,
|
||||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
FontFamily = s_segoeIconFont,
|
||||||
FontSize = 12,
|
FontSize = 12,
|
||||||
Foreground = iconColor,
|
Foreground = iconColor,
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
@@ -218,21 +218,11 @@ public partial class ChatWindow
|
|||||||
|
|
||||||
stack.Children.Add(CreateMenuItem("\uE8AC", "이름 변경", secondaryText, () =>
|
stack.Children.Add(CreateMenuItem("\uE8AC", "이름 변경", secondaryText, () =>
|
||||||
{
|
{
|
||||||
foreach (UIElement child in ConversationPanel.Children)
|
var titleBlock = FindConversationTitleBlock(conversationId);
|
||||||
|
if (titleBlock != null)
|
||||||
{
|
{
|
||||||
if (child is not Border b || b.Child is not Grid g) continue;
|
var titleColor = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||||
foreach (UIElement gc in g.Children)
|
EnterTitleEditMode(titleBlock, conversationId, titleColor);
|
||||||
{
|
|
||||||
if (gc is StackPanel sp && sp.Children.Count > 0 && sp.Children[0] is TextBlock tb)
|
|
||||||
{
|
|
||||||
if (conv != null && tb.Text == conv.Title)
|
|
||||||
{
|
|
||||||
var titleColor = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
|
||||||
EnterTitleEditMode(tb, conversationId, titleColor);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -267,7 +257,7 @@ public partial class ChatWindow
|
|||||||
infoSp.Children.Add(new TextBlock
|
infoSp.Children.Add(new TextBlock
|
||||||
{
|
{
|
||||||
Text = catSymbol,
|
Text = catSymbol,
|
||||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
FontFamily = s_segoeIconFont,
|
||||||
FontSize = 12,
|
FontSize = 12,
|
||||||
Foreground = catBrush,
|
Foreground = catBrush,
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
@@ -324,7 +314,7 @@ public partial class ChatWindow
|
|||||||
var catIcon = new TextBlock
|
var catIcon = new TextBlock
|
||||||
{
|
{
|
||||||
Text = symbol,
|
Text = symbol,
|
||||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
FontFamily = s_segoeIconFont,
|
||||||
FontSize = 12,
|
FontSize = 12,
|
||||||
Foreground = BrushFromHex(color),
|
Foreground = BrushFromHex(color),
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
@@ -406,6 +396,26 @@ public partial class ChatWindow
|
|||||||
}
|
}
|
||||||
|
|
||||||
stack.Children.Add(CreateSeparator());
|
stack.Children.Add(CreateSeparator());
|
||||||
|
|
||||||
|
// 아카이브 토글
|
||||||
|
var isArchived = _storage.Load(conversationId)?.Archived ?? false;
|
||||||
|
stack.Children.Add(CreateMenuItem(
|
||||||
|
isArchived ? "\uE7B8" : "\uE7B7",
|
||||||
|
isArchived ? "아카이브 해제" : "아카이브 보관",
|
||||||
|
TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, () =>
|
||||||
|
{
|
||||||
|
var convToArchive = _storage.Load(conversationId);
|
||||||
|
if (convToArchive == null) return;
|
||||||
|
convToArchive.Archived = !convToArchive.Archived;
|
||||||
|
_storage.Save(convToArchive);
|
||||||
|
lock (_convLock)
|
||||||
|
{
|
||||||
|
if (_currentConversation?.Id == conversationId)
|
||||||
|
_currentConversation.Archived = convToArchive.Archived;
|
||||||
|
}
|
||||||
|
RefreshConversationList();
|
||||||
|
}));
|
||||||
|
|
||||||
stack.Children.Add(CreateMenuItem("\uE74D", "이 대화 삭제", Brushes.IndianRed, () =>
|
stack.Children.Add(CreateMenuItem("\uE74D", "이 대화 삭제", Brushes.IndianRed, () =>
|
||||||
{
|
{
|
||||||
var result = CustomMessageBox.Show("이 대화를 삭제하시겠습니까?", "대화 삭제",
|
var result = CustomMessageBox.Show("이 대화를 삭제하시겠습니까?", "대화 삭제",
|
||||||
|
|||||||
383
src/AxCopilot/Views/ChatWindow.FileMentionSuggestions.cs
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace AxCopilot.Views;
|
||||||
|
|
||||||
|
public partial class ChatWindow
|
||||||
|
{
|
||||||
|
private sealed record FileMentionQuery(string Token, int Start, int Length);
|
||||||
|
|
||||||
|
private sealed record FileMentionCandidate(string RelativePath, string FileName, int Score);
|
||||||
|
|
||||||
|
private static readonly Regex FileMentionTokenRegex = new(
|
||||||
|
"(?<token>[^\\s\\\"'`()\\[\\]{}<>]{2,})$",
|
||||||
|
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
|
private static readonly HashSet<string> FileMentionIgnoredDirs = 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 readonly object _fileMentionIndexLock = new();
|
||||||
|
private List<string> _fileMentionIndexedPaths = new();
|
||||||
|
private string? _fileMentionIndexedFolder;
|
||||||
|
private DateTime _fileMentionIndexBuiltAtUtc = DateTime.MinValue;
|
||||||
|
private CancellationTokenSource? _fileMentionIndexCts;
|
||||||
|
private bool _fileMentionIndexBuildPending;
|
||||||
|
private const int FileMentionIndexLimit = 4000;
|
||||||
|
|
||||||
|
private void RefreshFileMentionSuggestions(string text)
|
||||||
|
{
|
||||||
|
if (string.Equals(_activeTab, "Chat", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.IsNullOrWhiteSpace(GetCurrentWorkFolder()))
|
||||||
|
{
|
||||||
|
HideFileMentionSuggestions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SlashPopup?.IsOpen == true && text.StartsWith("/") && !text.Contains(' '))
|
||||||
|
{
|
||||||
|
HideFileMentionSuggestions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var query = TryExtractFileMentionQuery(text);
|
||||||
|
if (query == null)
|
||||||
|
{
|
||||||
|
HideFileMentionSuggestions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var workFolder = GetCurrentWorkFolder();
|
||||||
|
if (string.IsNullOrWhiteSpace(workFolder) || !Directory.Exists(workFolder))
|
||||||
|
{
|
||||||
|
HideFileMentionSuggestions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string> indexSnapshot;
|
||||||
|
bool canUseCurrentIndex;
|
||||||
|
lock (_fileMentionIndexLock)
|
||||||
|
{
|
||||||
|
canUseCurrentIndex =
|
||||||
|
string.Equals(_fileMentionIndexedFolder, workFolder, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& (DateTime.UtcNow - _fileMentionIndexBuiltAtUtc) < TimeSpan.FromMinutes(3)
|
||||||
|
&& _fileMentionIndexedPaths.Count > 0;
|
||||||
|
indexSnapshot = canUseCurrentIndex ? new List<string>(_fileMentionIndexedPaths) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canUseCurrentIndex)
|
||||||
|
{
|
||||||
|
RenderFileMentionSuggestions(query.Token, [], isLoading: true);
|
||||||
|
EnsureFileMentionIndexAsync(workFolder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidates = FindFileMentionCandidates(indexSnapshot, query.Token);
|
||||||
|
RenderFileMentionSuggestions(query.Token, candidates, isLoading: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureFileMentionIndexAsync(string workFolder)
|
||||||
|
{
|
||||||
|
if (_fileMentionIndexBuildPending
|
||||||
|
&& string.Equals(_fileMentionIndexedFolder, workFolder, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_fileMentionIndexCts?.Cancel();
|
||||||
|
_fileMentionIndexCts = new CancellationTokenSource();
|
||||||
|
var localCts = _fileMentionIndexCts;
|
||||||
|
|
||||||
|
_fileMentionIndexBuildPending = true;
|
||||||
|
_fileMentionIndexedFolder = workFolder;
|
||||||
|
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
|
var indexedPaths = BuildFileMentionIndex(workFolder, localCts.Token);
|
||||||
|
if (localCts.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (_fileMentionIndexLock)
|
||||||
|
{
|
||||||
|
_fileMentionIndexedPaths = indexedPaths;
|
||||||
|
_fileMentionIndexedFolder = workFolder;
|
||||||
|
_fileMentionIndexBuiltAtUtc = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dispatcher?.Invoke(() =>
|
||||||
|
{
|
||||||
|
_fileMentionIndexBuildPending = false;
|
||||||
|
RefreshFileMentionSuggestions(InputBox?.Text ?? "");
|
||||||
|
});
|
||||||
|
}, localCts.Token).ContinueWith(_ =>
|
||||||
|
{
|
||||||
|
Dispatcher?.Invoke(() => { _fileMentionIndexBuildPending = false; });
|
||||||
|
}, TaskScheduler.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> BuildFileMentionIndex(string workFolder, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var results = new List<string>(capacity: 512);
|
||||||
|
var pending = new Stack<string>();
|
||||||
|
pending.Push(workFolder);
|
||||||
|
|
||||||
|
while (pending.Count > 0 && results.Count < FileMentionIndexLimit)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
var current = pending.Pop();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var dir in Directory.EnumerateDirectories(current))
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
var name = Path.GetFileName(dir);
|
||||||
|
if (string.IsNullOrWhiteSpace(name)
|
||||||
|
|| name.StartsWith('.')
|
||||||
|
|| FileMentionIgnoredDirs.Contains(name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
pending.Push(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var file in Directory.EnumerateFiles(current))
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
var fileName = Path.GetFileName(file);
|
||||||
|
if (string.IsNullOrWhiteSpace(fileName) || fileName.StartsWith('.'))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
results.Add(Path.GetRelativePath(workFolder, file));
|
||||||
|
if (results.Count >= FileMentionIndexLimit)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FileMentionQuery? TryExtractFileMentionQuery(string text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var trimmedEnd = text.TrimEnd();
|
||||||
|
if (trimmedEnd.Length < 2)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var match = FileMentionTokenRegex.Match(trimmedEnd);
|
||||||
|
if (!match.Success)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var token = match.Groups["token"].Value.Trim();
|
||||||
|
if (token.Length < 2)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var looksFileLike = token.Contains('.')
|
||||||
|
|| token.Contains('/')
|
||||||
|
|| token.Contains('\\')
|
||||||
|
|| token.Contains('_')
|
||||||
|
|| token.Contains('-');
|
||||||
|
if (!looksFileLike)
|
||||||
|
{
|
||||||
|
var lower = trimmedEnd.ToLowerInvariant();
|
||||||
|
var fileContext = lower.Contains("파일")
|
||||||
|
|| lower.Contains("문서")
|
||||||
|
|| lower.Contains("폴더")
|
||||||
|
|| lower.Contains("file")
|
||||||
|
|| lower.Contains("document")
|
||||||
|
|| lower.Contains("folder")
|
||||||
|
|| lower.Contains("read ")
|
||||||
|
|| lower.Contains("open ");
|
||||||
|
if (!fileContext)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FileMentionQuery(
|
||||||
|
token,
|
||||||
|
match.Groups["token"].Index,
|
||||||
|
match.Groups["token"].Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<FileMentionCandidate> FindFileMentionCandidates(
|
||||||
|
IReadOnlyCollection<string> indexedPaths,
|
||||||
|
string query)
|
||||||
|
{
|
||||||
|
if (indexedPaths.Count == 0 || string.IsNullOrWhiteSpace(query))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var normalizedQuery = query.Replace('\\', '/').Trim().ToLowerInvariant();
|
||||||
|
var queryFileName = Path.GetFileName(normalizedQuery);
|
||||||
|
var candidates = new List<FileMentionCandidate>();
|
||||||
|
|
||||||
|
foreach (var relativePath in indexedPaths)
|
||||||
|
{
|
||||||
|
var normalizedPath = relativePath.Replace('\\', '/');
|
||||||
|
var lowerPath = normalizedPath.ToLowerInvariant();
|
||||||
|
var fileName = Path.GetFileName(normalizedPath);
|
||||||
|
var lowerFileName = fileName.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (!lowerPath.Contains(normalizedQuery) && !lowerFileName.Contains(queryFileName))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var score = 0;
|
||||||
|
if (string.Equals(lowerFileName, queryFileName, StringComparison.Ordinal))
|
||||||
|
score += 600;
|
||||||
|
else if (lowerFileName.StartsWith(queryFileName, StringComparison.Ordinal))
|
||||||
|
score += 420;
|
||||||
|
else if (lowerFileName.Contains(queryFileName, StringComparison.Ordinal))
|
||||||
|
score += 260;
|
||||||
|
|
||||||
|
if (string.Equals(lowerPath, normalizedQuery, StringComparison.Ordinal))
|
||||||
|
score += 520;
|
||||||
|
else if (lowerPath.EndsWith(normalizedQuery, StringComparison.Ordinal))
|
||||||
|
score += 300;
|
||||||
|
else if (lowerPath.Contains(normalizedQuery, StringComparison.Ordinal))
|
||||||
|
score += 140;
|
||||||
|
|
||||||
|
score += Math.Max(0, 80 - normalizedPath.Length);
|
||||||
|
|
||||||
|
candidates.Add(new FileMentionCandidate(normalizedPath, fileName, score));
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates
|
||||||
|
.OrderByDescending(c => c.Score)
|
||||||
|
.ThenBy(c => c.RelativePath.Length)
|
||||||
|
.ThenBy(c => c.RelativePath, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Take(6)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenderFileMentionSuggestions(
|
||||||
|
string query,
|
||||||
|
IReadOnlyList<FileMentionCandidate> candidates,
|
||||||
|
bool isLoading)
|
||||||
|
{
|
||||||
|
if (FileMentionSuggestionCard == null
|
||||||
|
|| FileMentionSuggestionTitle == null
|
||||||
|
|| FileMentionSuggestionChipPanel == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
FileMentionSuggestionChipPanel.Children.Clear();
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
{
|
||||||
|
FileMentionSuggestionTitle.Text = $"파일 후보 찾는 중 · {query}";
|
||||||
|
FileMentionSuggestionCard.Visibility = Visibility.Visible;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidates.Count == 0)
|
||||||
|
{
|
||||||
|
HideFileMentionSuggestions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMentionSuggestionTitle.Text = $"파일 후보 · {query}";
|
||||||
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
||||||
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||||
|
var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke;
|
||||||
|
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? itemBg;
|
||||||
|
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray;
|
||||||
|
var accentBrush = TryFindResource("AccentColor") as Brush ?? primaryText;
|
||||||
|
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
|
var chip = new Border
|
||||||
|
{
|
||||||
|
Background = itemBg,
|
||||||
|
BorderBrush = borderBrush,
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(10),
|
||||||
|
Padding = new Thickness(10, 6, 10, 6),
|
||||||
|
Margin = new Thickness(0, 0, 6, 6),
|
||||||
|
Cursor = Cursors.Hand,
|
||||||
|
Tag = candidate.RelativePath,
|
||||||
|
};
|
||||||
|
|
||||||
|
var stack = new StackPanel();
|
||||||
|
stack.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = candidate.FileName,
|
||||||
|
FontSize = 12,
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = primaryText,
|
||||||
|
});
|
||||||
|
stack.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = candidate.RelativePath,
|
||||||
|
Margin = new Thickness(0, 2, 0, 0),
|
||||||
|
FontSize = 10.5,
|
||||||
|
Foreground = secondaryText,
|
||||||
|
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||||
|
MaxWidth = 240,
|
||||||
|
});
|
||||||
|
chip.Child = stack;
|
||||||
|
|
||||||
|
chip.MouseEnter += (_, _) => chip.Background = hoverBg;
|
||||||
|
chip.MouseLeave += (_, _) => chip.Background = itemBg;
|
||||||
|
chip.MouseLeftButtonUp += (_, _) => ApplyFileMentionSuggestion(candidate.RelativePath);
|
||||||
|
|
||||||
|
FileMentionSuggestionChipPanel.Children.Add(chip);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMentionSuggestionCard.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HideFileMentionSuggestions()
|
||||||
|
{
|
||||||
|
if (FileMentionSuggestionCard == null || FileMentionSuggestionChipPanel == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
FileMentionSuggestionChipPanel.Children.Clear();
|
||||||
|
FileMentionSuggestionCard.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyFileMentionSuggestion(string relativePath)
|
||||||
|
{
|
||||||
|
if (InputBox == null || string.IsNullOrWhiteSpace(relativePath))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var text = InputBox.Text ?? "";
|
||||||
|
var query = TryExtractFileMentionQuery(text);
|
||||||
|
var replacement = relativePath.Replace('\\', '/');
|
||||||
|
if (query != null)
|
||||||
|
{
|
||||||
|
text = text[..query.Start] + replacement + text[(query.Start + query.Length)..];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
text = string.IsNullOrWhiteSpace(text) ? replacement : $"{text.TrimEnd()} {replacement}";
|
||||||
|
}
|
||||||
|
|
||||||
|
InputBox.Text = text;
|
||||||
|
InputBox.CaretIndex = text.Length;
|
||||||
|
InputBox.Focus();
|
||||||
|
HideFileMentionSuggestions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryAcceptTopFileMentionSuggestion()
|
||||||
|
{
|
||||||
|
if (FileMentionSuggestionCard?.Visibility != Visibility.Visible
|
||||||
|
|| FileMentionSuggestionChipPanel == null
|
||||||
|
|| FileMentionSuggestionChipPanel.Children.Count == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (FileMentionSuggestionChipPanel.Children[0] is not Border firstChip
|
||||||
|
|| firstChip.Tag is not string relativePath)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ApplyFileMentionSuggestion(relativePath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,17 +66,8 @@ public partial class ChatWindow
|
|||||||
}
|
}
|
||||||
|
|
||||||
FolderBar.Visibility = Visibility.Visible;
|
FolderBar.Visibility = Visibility.Visible;
|
||||||
var folder = GetCurrentWorkFolder();
|
ViewModel.WorkFolder = GetCurrentWorkFolder();
|
||||||
if (!string.IsNullOrEmpty(folder))
|
UpdateFolderSelectButtonStyle();
|
||||||
{
|
|
||||||
FolderPathLabel.Text = folder;
|
|
||||||
FolderPathLabel.ToolTip = folder;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
FolderPathLabel.Text = "폴더를 선택하세요.";
|
|
||||||
FolderPathLabel.ToolTip = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadConversationSettings();
|
LoadConversationSettings();
|
||||||
LoadCompactionMetricsFromConversation();
|
LoadCompactionMetricsFromConversation();
|
||||||
@@ -129,7 +120,7 @@ public partial class ChatWindow
|
|||||||
memory.Load(workFolder);
|
memory.Load(workFolder);
|
||||||
var docs = memory.InstructionDocuments;
|
var docs = memory.InstructionDocuments;
|
||||||
var learned = memory.All.Count;
|
var learned = memory.All.Count;
|
||||||
var includePolicy = _settings.Settings.Llm.AllowExternalMemoryIncludes ? "?몃? include ?덉슜" : "?몃? include 李⑤떒";
|
var includePolicy = _settings.Settings.Llm.AllowExternalMemoryIncludes ? "외부 include 허용" : "외부 include 차단";
|
||||||
var auditEnabled = _settings.Settings.Llm.EnableAuditLog;
|
var auditEnabled = _settings.Settings.Llm.EnableAuditLog;
|
||||||
var recentIncludeEntries = AuditLogService.LoadRecent("MemoryInclude", maxCount: 5, daysBack: 3);
|
var recentIncludeEntries = AuditLogService.LoadRecent("MemoryInclude", maxCount: 5, daysBack: 3);
|
||||||
|
|
||||||
@@ -143,7 +134,7 @@ public partial class ChatWindow
|
|||||||
var panel = new StackPanel { Margin = new Thickness(2) };
|
var panel = new StackPanel { Margin = new Thickness(2) };
|
||||||
panel.Children.Add(new TextBlock
|
panel.Children.Add(new TextBlock
|
||||||
{
|
{
|
||||||
Text = "硫붾え由??곹깭",
|
Text = "메모리 상태",
|
||||||
FontSize = 13,
|
FontSize = 13,
|
||||||
FontWeight = FontWeights.SemiBold,
|
FontWeight = FontWeights.SemiBold,
|
||||||
Foreground = primaryText,
|
Foreground = primaryText,
|
||||||
@@ -151,7 +142,7 @@ public partial class ChatWindow
|
|||||||
});
|
});
|
||||||
panel.Children.Add(new TextBlock
|
panel.Children.Add(new TextBlock
|
||||||
{
|
{
|
||||||
Text = $"怨꾩링??洹쒖튃 {docs.Count}媛?쨌 ?숈뒿 硫붾え由?{learned}媛?쨌 {includePolicy}",
|
Text = $"규칙 문서 {docs.Count}개 · 학습 메모리 {learned}개 · {includePolicy}",
|
||||||
FontSize = 11.5,
|
FontSize = 11.5,
|
||||||
Foreground = secondaryText,
|
Foreground = secondaryText,
|
||||||
TextWrapping = TextWrapping.Wrap,
|
TextWrapping = TextWrapping.Wrap,
|
||||||
@@ -163,7 +154,7 @@ public partial class ChatWindow
|
|||||||
panel.Children.Add(CreateSurfacePopupSeparator());
|
panel.Children.Add(CreateSurfacePopupSeparator());
|
||||||
panel.Children.Add(new TextBlock
|
panel.Children.Add(new TextBlock
|
||||||
{
|
{
|
||||||
Text = "?곸슜 以?洹쒖튃",
|
Text = "적용 중 규칙",
|
||||||
FontSize = 12,
|
FontSize = 12,
|
||||||
FontWeight = FontWeights.SemiBold,
|
FontWeight = FontWeights.SemiBold,
|
||||||
Foreground = primaryText,
|
Foreground = primaryText,
|
||||||
@@ -177,7 +168,7 @@ public partial class ChatWindow
|
|||||||
{
|
{
|
||||||
panel.Children.Add(new TextBlock
|
panel.Children.Add(new TextBlock
|
||||||
{
|
{
|
||||||
Text = $"??{docs.Count - 6}媛?洹쒖튃",
|
Text = $"외 {docs.Count - 6}개 규칙",
|
||||||
FontSize = 11,
|
FontSize = 11,
|
||||||
Foreground = secondaryText,
|
Foreground = secondaryText,
|
||||||
Margin = new Thickness(8, 2, 8, 4),
|
Margin = new Thickness(8, 2, 8, 4),
|
||||||
@@ -188,7 +179,7 @@ public partial class ChatWindow
|
|||||||
panel.Children.Add(CreateSurfacePopupSeparator());
|
panel.Children.Add(CreateSurfacePopupSeparator());
|
||||||
panel.Children.Add(new TextBlock
|
panel.Children.Add(new TextBlock
|
||||||
{
|
{
|
||||||
Text = "理쒓렐 include 媛먯궗",
|
Text = "최근 include 감사",
|
||||||
FontSize = 12,
|
FontSize = 12,
|
||||||
FontWeight = FontWeights.SemiBold,
|
FontWeight = FontWeights.SemiBold,
|
||||||
Foreground = primaryText,
|
Foreground = primaryText,
|
||||||
@@ -199,7 +190,7 @@ public partial class ChatWindow
|
|||||||
{
|
{
|
||||||
panel.Children.Add(new TextBlock
|
panel.Children.Add(new TextBlock
|
||||||
{
|
{
|
||||||
Text = "媛먯궗 濡쒓렇媛 爰쇱졇 ?덉뼱 include ?대젰? 湲곕줉?섏? ?딆뒿?덈떎.",
|
Text = "감사 로그가 꺼져 있어 include 이력이 기록되지 않습니다.",
|
||||||
FontSize = 11,
|
FontSize = 11,
|
||||||
Foreground = secondaryText,
|
Foreground = secondaryText,
|
||||||
TextWrapping = TextWrapping.Wrap,
|
TextWrapping = TextWrapping.Wrap,
|
||||||
@@ -210,7 +201,7 @@ public partial class ChatWindow
|
|||||||
{
|
{
|
||||||
panel.Children.Add(new TextBlock
|
panel.Children.Add(new TextBlock
|
||||||
{
|
{
|
||||||
Text = "理쒓렐 3?쇨컙 include 媛먯궗 湲곕줉???놁뒿?덈떎.",
|
Text = "최근 3일간 include 감사 기록이 없습니다.",
|
||||||
FontSize = 11,
|
FontSize = 11,
|
||||||
Foreground = secondaryText,
|
Foreground = secondaryText,
|
||||||
Margin = new Thickness(8, 0, 8, 6),
|
Margin = new Thickness(8, 0, 8, 6),
|
||||||
@@ -266,7 +257,7 @@ public partial class ChatWindow
|
|||||||
var stack = new StackPanel { Margin = new Thickness(8, 2, 8, 4) };
|
var stack = new StackPanel { Margin = new Thickness(8, 2, 8, 4) };
|
||||||
stack.Children.Add(new TextBlock
|
stack.Children.Add(new TextBlock
|
||||||
{
|
{
|
||||||
Text = $"[{doc.Label}] ?곗꽑?쒖쐞 {doc.Priority}",
|
Text = $"[{doc.Label}] 우선순위 {doc.Priority}",
|
||||||
FontSize = 11.5,
|
FontSize = 11.5,
|
||||||
FontWeight = FontWeights.SemiBold,
|
FontWeight = FontWeights.SemiBold,
|
||||||
Foreground = primaryText,
|
Foreground = primaryText,
|
||||||
@@ -297,7 +288,7 @@ public partial class ChatWindow
|
|||||||
private Border BuildMemoryPopupAuditRow(AuditEntry entry, Brush primaryText, Brush secondaryText, Brush okBrush, Brush warnBrush, Brush dangerBrush)
|
private Border BuildMemoryPopupAuditRow(AuditEntry entry, Brush primaryText, Brush secondaryText, Brush okBrush, Brush warnBrush, Brush dangerBrush)
|
||||||
{
|
{
|
||||||
var statusBrush = entry.Success ? okBrush : dangerBrush;
|
var statusBrush = entry.Success ? okBrush : dangerBrush;
|
||||||
var statusText = entry.Success ? "?덉슜" : "李⑤떒";
|
var statusText = entry.Success ? "허용" : "차단";
|
||||||
var resultBrush = entry.Success ? secondaryText : warnBrush;
|
var resultBrush = entry.Success ? secondaryText : warnBrush;
|
||||||
|
|
||||||
var stack = new StackPanel { Margin = new Thickness(8, 2, 8, 4) };
|
var stack = new StackPanel { Margin = new Thickness(8, 2, 8, 4) };
|
||||||
|
|||||||
@@ -16,15 +16,13 @@ public partial class ChatWindow
|
|||||||
{
|
{
|
||||||
_currentGitBranchName = branchName;
|
_currentGitBranchName = branchName;
|
||||||
_currentGitTooltip = tooltip;
|
_currentGitTooltip = tooltip;
|
||||||
|
ViewModel.GitBranchName = string.IsNullOrWhiteSpace(branchName) ? "브랜치 없음" : branchName;
|
||||||
|
|
||||||
if (BtnGitBranch != null)
|
if (BtnGitBranch != null)
|
||||||
{
|
{
|
||||||
BtnGitBranch.Visibility = visibility;
|
BtnGitBranch.Visibility = visibility;
|
||||||
BtnGitBranch.ToolTip = string.IsNullOrWhiteSpace(tooltip) ? "현재 Git 브랜치 상태" : tooltip;
|
BtnGitBranch.ToolTip = string.IsNullOrWhiteSpace(tooltip) ? "현재 Git 브랜치 상태" : tooltip;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GitBranchLabel != null)
|
|
||||||
GitBranchLabel.Text = string.IsNullOrWhiteSpace(branchName) ? "브랜치 없음" : branchName;
|
|
||||||
if (GitBranchFilesText != null)
|
if (GitBranchFilesText != null)
|
||||||
GitBranchFilesText.Text = filesText;
|
GitBranchFilesText.Text = filesText;
|
||||||
if (GitBranchAddedText != null)
|
if (GitBranchAddedText != null)
|
||||||
@@ -493,7 +491,7 @@ public partial class ChatWindow
|
|||||||
grid.Children.Add(new TextBlock
|
grid.Children.Add(new TextBlock
|
||||||
{
|
{
|
||||||
Text = icon,
|
Text = icon,
|
||||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
FontFamily = s_segoeIconFont,
|
||||||
FontSize = 11,
|
FontSize = 11,
|
||||||
Foreground = BrushFromHex(colorHex),
|
Foreground = BrushFromHex(colorHex),
|
||||||
VerticalAlignment = VerticalAlignment.Top,
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
@@ -527,7 +525,7 @@ public partial class ChatWindow
|
|||||||
var chevron = new TextBlock
|
var chevron = new TextBlock
|
||||||
{
|
{
|
||||||
Text = "\uE76C",
|
Text = "\uE76C",
|
||||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
FontFamily = s_segoeIconFont,
|
||||||
FontSize = 10,
|
FontSize = 10,
|
||||||
Foreground = secondaryText,
|
Foreground = secondaryText,
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
|||||||