diff --git a/AGENTS.md b/AGENTS.md index 8b02717..ee64955 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,6 +87,11 @@ var fg = TryFindResource("PrimaryText") as Brush ?? Brushes.Black; - 선택형: 커스텀 Popup 드롭다운 (`[라벨: 현재값 ▾]`) - AI/고급 설정 항목 옆에 `?` 도움말 아이콘 + 커스텀 다크 툴팁 (`HelpTooltipStyle`) - 설정 저장 시 `CustomMessageBox`로 완료 알림 +- 설정에서 `on/off` 또는 숫자 입력이 필요한 신규 항목은 **기존 양식에 맞춰 통일해서 추가**해야 함 +- `on/off` 항목은 예외 없이 `ToggleSwitch` 스타일을 사용하고, 기본 CheckBox/임의 토글 버튼으로 새로 만들지 않음 +- 숫자 입력 항목은 가능하면 **텍스트박스 직접 입력 대신 기존 슬라이더 + 현재값 배지 패턴**을 우선 사용하며, 범위가 명확한 값은 반드시 이 패턴을 기본으로 채택 +- 숫자 설정을 부득이하게 텍스트 입력으로 받을 경우에도, 먼저 기존 설정창/AX Agent 내부 설정에 같은 유형의 컨트롤이 있는지 확인하고 그 양식을 재사용해야 함 +- 동일 성격의 설정은 메인 설정과 AX Agent 내부 설정에서 **표현 방식이 서로 다르면 안 되며**, 기존에 쓰던 컨트롤러/레이아웃 기준으로 맞춰 추가 ### AX Agent 표현 수준 (필수) - AX Agent UI 표현 수준은 설정에서 반드시 3단계로 제공: **`풍부하게` / `적절하게` / `간단하게`** diff --git a/README.md b/README.md index 0ce0bb6..03c4314 100644 --- a/README.md +++ b/README.md @@ -1473,3 +1473,13 @@ MIT License - Cowork/Chat 하단의 프리셋 안내 카드가 실제 결과를 가리던 문제를 수정했습니다. 이제 대화에 사용자/assistant 메시지가 생기거나 실행 중일 때는 해당 카드가 자동으로 숨겨집니다. - [ChatWindow.FooterPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.FooterPresentation.cs)에 남아 있던 깨진 한글 워터마크/안내 문구를 정상 한국어로 정리했습니다. - 라이브 타이핑 속도를 조정해 SSE 및 Cowork/Code 최종 프리뷰가 한 번에 붙는 느낌을 줄이고, 더 눈에 보이게 점진적으로 출력되도록 보정했습니다. +- 업데이트: 2026-04-08 10:12 (KST) + - 등록 모델에 `동작 프로파일`을 추가했습니다. 이제 모델별로 `균형 / 도구 호출 우선 / 추론 우선 / 읽기 속도 우선 / 문서 생성 우선` 성향을 저장할 수 있고, 편집/추가 모두 내부 설정과 일반 설정에서 같은 값으로 유지됩니다. + - Cowork/Code 루프는 현재 활성 모델의 프로파일을 읽어 no-tool 감지 임계값, 도구 미호출 재시도, 문서 생성 재시도, terminal evidence gate, 읽기 도구 병렬 배치 수를 다르게 적용합니다. + - AX Agent 내부 설정의 Temperature 항목에 `자동 / 사용자 지정` 전환을 추가했습니다. 자동일 때는 등록 모델 프로파일의 temperature 정책을 따르고, 사용자 지정일 때만 슬라이더 값이 실제 tool 호출 온도로 적용됩니다. +- 업데이트: 2026-04-08 10:38 (KST) + - 모델 실행 프로파일을 Cowork/Code 후속 게이트까지 더 깊게 연결했습니다. 이제 프로파일별로 post-tool verification, 코드 품질 게이트, 문서 검증 게이트, diff/실행 증거 게이트, final report 게이트의 강도를 다르게 적용합니다. + - `document_heavy` 프로파일은 `document_plan` 이후 장기 재시도보다 fallback 산출물 생성 쪽으로 더 빨리 전환되도록 조정했습니다. + - OpenAI/vLLM tool calling 바디에 `parallel_tool_calls` 힌트를 추가해 읽기 도구 병렬 실행 성향이 모델 요청 바디에도 반영되도록 보강했습니다. + - Cowork/Code 진행 표시에는 `계획 / 도구 / 검증 / 압축 / 폴백 / 재시도` 같은 단계 메타를 더 직접적으로 붙여, 오래 걸릴 때도 현재 단계가 더 잘 읽히게 했습니다. + - [docs/AX_AGENT_REGRESSION_PROMPTS.md](/E:/AX%20Copilot%20-%20Codex/docs/AX_AGENT_REGRESSION_PROMPTS.md)를 전면 정리해 `tool_call_strict`, `fast_readonly`, `document_heavy`, `reasoning_first` 프로파일별 회귀 시나리오를 고정했습니다. diff --git a/create_html_preview.js b/create_html_preview.js new file mode 100644 index 0000000..dab1beb --- /dev/null +++ b/create_html_preview.js @@ -0,0 +1,132 @@ +const JSZip = require('jszip'); +const fs = require('fs'); +const path = require('path'); + +// EMU to inches: 1 inch = 914400 EMU +// Slide: 9144000 x 5143500 EMU = 10" x 5.625" +const EMU = 914400; +const SLIDE_W = 9144000; +const SLIDE_H = 5143500; +const SCALE = 960 / 10; // 96px per inch (at 100%) + +function emuToPx(emu) { + return (emu / EMU) * SCALE; +} + +function hexToRgb(hex) { + if (!hex || hex.length < 6) return '#888888'; + return '#' + hex.slice(-6); +} + +async function renderSlideHtml(xml, slideNum) { + // Extract background color + const bgMatch = xml.match(/p:bg>.*?(.+?)<\/p:sp>/gs; + let spMatch; + while ((spMatch = spPattern.exec(xml)) !== null) { + const spXml = spMatch[1]; + + // Get position + const offMatch = spXml.match(/([\s\S]*?)<\/a:p>/g; + let paraMatch; + while ((paraMatch = paraPattern.exec(spXml)) !== null) { + const paraXml = paraMatch[1]; + const textMatches = [...paraXml.matchAll(/]*>([^<]*)<\/a:t>/g)]; + const paraText = textMatches.map(m => m[1]).join(''); + if (paraText.trim()) texts.push(paraText.trim()); + } + + // Get font size + const szMatch = spXml.match(/sz="(\d+)"/); + const fontSize = szMatch ? parseInt(szMatch[1]) / 100 : 12; + + // Get text color + const txtColorMatch = spXml.match(/[\s\S]*?'); + shapesHtml += `
+
${textContent}
+
`; + } + + return `
+
${slideNum}
+ ${shapesHtml} +
`; +} + +async function createPreview(pptxPath, outputPath) { + const data = fs.readFileSync(pptxPath); + const zip = await JSZip.loadAsync(data); + + const slideFiles = Object.keys(zip.files) + .filter(f => f.match(/^ppt\/slides\/slide\d+\.xml$/)) + .sort((a, b) => { + const na = parseInt(a.match(/slide(\d+)/)[1]); + const nb = parseInt(b.match(/slide(\d+)/)[1]); + return na - nb; + }); + + let slidesHtml = ''; + for (const sf of slideFiles) { + const slideNum = parseInt(sf.match(/slide(\d+)/)[1]); + const xml = await zip.file(sf).async('string'); + slidesHtml += await renderSlideHtml(xml, slideNum); + } + + const html = ` + +${path.basename(pptxPath)} + + +

${path.basename(pptxPath)}

+
${slidesHtml}
+`; + + fs.writeFileSync(outputPath, html); + console.log(`Preview: ${outputPath} (${slideFiles.length} slides)`); +} + +(async () => { + await createPreview( + 'E:/test/삼성디스플레이 vs LG디스플레이 비교 분석 보고서_20260407_1958.pptx', + 'C:/Users/admin/AppData/Local/Temp/pptx_gen/preview1.html' + ); + await createPreview( + 'E:/test/삼성디스플레이 사업 영역 및 제품 강점 분석 보고서_20260407_1956.pptx', + 'C:/Users/admin/AppData/Local/Temp/pptx_gen/preview2.html' + ); +})(); diff --git a/dist/AxCopilot/Accessibility.dll b/dist/AxCopilot/Accessibility.dll deleted file mode 100644 index 718d330..0000000 Binary files a/dist/AxCopilot/Accessibility.dll and /dev/null differ diff --git a/dist/AxCopilot/Assets/icon.ico b/dist/AxCopilot/Assets/icon.ico deleted file mode 100644 index 273db74..0000000 Binary files a/dist/AxCopilot/Assets/icon.ico and /dev/null differ diff --git a/dist/AxCopilot/AxCopilot.SDK.dll b/dist/AxCopilot/AxCopilot.SDK.dll deleted file mode 100644 index bb039f0..0000000 Binary files a/dist/AxCopilot/AxCopilot.SDK.dll and /dev/null differ diff --git a/dist/AxCopilot/AxCopilot.deps.json b/dist/AxCopilot/AxCopilot.deps.json deleted file mode 100644 index 90e952a..0000000 --- a/dist/AxCopilot/AxCopilot.deps.json +++ /dev/null @@ -1,1319 +0,0 @@ -{ - "runtimeTarget": { - "name": ".NETCoreApp,Version=v8.0/win-x64", - "signature": "" - }, - "compilationOptions": {}, - "targets": { - ".NETCoreApp,Version=v8.0": {}, - ".NETCoreApp,Version=v8.0/win-x64": { - "AxCopilot/0.7.3": { - "dependencies": { - "AxCopilot.SDK": "1.0.0", - "DocumentFormat.OpenXml": "3.2.0", - "Markdig": "0.37.0", - "Microsoft.Data.Sqlite": "8.0.0", - "Microsoft.Web.WebView2": "1.0.2903.40", - "System.ServiceProcess.ServiceController": "8.0.1", - "UglyToad.PdfPig": "1.7.0-custom-5", - "Microsoft.Web.WebView2.Core": "1.0.2903.40", - "Microsoft.Web.WebView2.WinForms": "1.0.2903.40", - "Microsoft.Web.WebView2.Wpf": "1.0.2903.40", - "runtimepack.Microsoft.NETCore.App.Runtime.win-x64": "8.0.25", - "runtimepack.Microsoft.WindowsDesktop.App.Runtime.win-x64": "8.0.25" - }, - "runtime": { - "AxCopilot.dll": {} - } - }, - "runtimepack.Microsoft.NETCore.App.Runtime.win-x64/8.0.25": { - "runtime": { - "Microsoft.CSharp.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "Microsoft.VisualBasic.Core.dll": { - "assemblyVersion": "13.0.0.0", - "fileVersion": "13.0.2526.11203" - }, - "Microsoft.Win32.Primitives.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "Microsoft.Win32.Registry.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.AppContext.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Buffers.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Collections.Concurrent.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Collections.Immutable.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Collections.NonGeneric.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Collections.Specialized.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Collections.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.ComponentModel.Annotations.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.ComponentModel.DataAnnotations.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.ComponentModel.EventBasedAsync.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.ComponentModel.Primitives.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.ComponentModel.TypeConverter.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.ComponentModel.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Configuration.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Console.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Core.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Data.Common.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Data.DataSetExtensions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Data.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Diagnostics.Contracts.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Diagnostics.Debug.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Diagnostics.DiagnosticSource.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Diagnostics.FileVersionInfo.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Diagnostics.Process.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Diagnostics.StackTrace.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Diagnostics.TextWriterTraceListener.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Diagnostics.Tools.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Diagnostics.TraceSource.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Diagnostics.Tracing.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Drawing.Primitives.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Dynamic.Runtime.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Formats.Asn1.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Formats.Tar.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Globalization.Calendars.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Globalization.Extensions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Globalization.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.IO.Compression.Brotli.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.IO.Compression.FileSystem.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.IO.Compression.ZipFile.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.IO.Compression.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.IO.FileSystem.AccessControl.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.IO.FileSystem.DriveInfo.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.IO.FileSystem.Primitives.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.IO.FileSystem.Watcher.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.IO.FileSystem.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.IO.IsolatedStorage.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.IO.MemoryMappedFiles.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.IO.Pipes.AccessControl.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.IO.Pipes.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.IO.UnmanagedMemoryStream.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.IO.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Linq.Expressions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Linq.Parallel.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Linq.Queryable.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Linq.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Memory.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.Http.Json.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.Http.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.HttpListener.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.Mail.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.NameResolution.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.NetworkInformation.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.Ping.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.Primitives.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.Quic.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.Requests.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.Security.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.ServicePoint.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.Sockets.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.WebClient.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.WebHeaderCollection.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.WebProxy.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.WebSockets.Client.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.WebSockets.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Net.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Numerics.Vectors.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Numerics.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.ObjectModel.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Private.CoreLib.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Private.DataContractSerialization.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Private.Uri.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Private.Xml.Linq.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Private.Xml.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Reflection.DispatchProxy.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Reflection.Emit.ILGeneration.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Reflection.Emit.Lightweight.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Reflection.Emit.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Reflection.Extensions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Reflection.Metadata.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Reflection.Primitives.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Reflection.TypeExtensions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Reflection.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Resources.Reader.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Resources.ResourceManager.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Resources.Writer.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Runtime.CompilerServices.Unsafe.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Runtime.CompilerServices.VisualC.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Runtime.Extensions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Runtime.Handles.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Runtime.InteropServices.JavaScript.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Runtime.InteropServices.RuntimeInformation.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Runtime.InteropServices.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Runtime.Intrinsics.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Runtime.Loader.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Runtime.Numerics.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Runtime.Serialization.Formatters.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Runtime.Serialization.Json.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Runtime.Serialization.Primitives.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Runtime.Serialization.Xml.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Runtime.Serialization.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Runtime.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.AccessControl.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.Claims.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.Cryptography.Algorithms.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.Cryptography.Cng.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.Cryptography.Csp.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.Cryptography.Encoding.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.Cryptography.OpenSsl.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.Cryptography.Primitives.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.Cryptography.X509Certificates.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.Cryptography.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.Principal.Windows.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.Principal.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.SecureString.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.ServiceModel.Web.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.ServiceProcess.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Text.Encoding.CodePages.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Text.Encoding.Extensions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Text.Encoding.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Text.Encodings.Web.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Text.Json.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Text.RegularExpressions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Threading.Channels.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Threading.Overlapped.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Threading.Tasks.Dataflow.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Threading.Tasks.Extensions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Threading.Tasks.Parallel.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Threading.Tasks.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Threading.Thread.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Threading.ThreadPool.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Threading.Timer.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Threading.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Transactions.Local.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Transactions.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.ValueTuple.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Web.HttpUtility.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Web.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Windows.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Xml.Linq.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Xml.ReaderWriter.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Xml.Serialization.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Xml.XDocument.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Xml.XPath.XDocument.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Xml.XPath.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Xml.XmlDocument.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Xml.XmlSerializer.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Xml.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "mscorlib.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "netstandard.dll": { - "assemblyVersion": "2.1.0.0", - "fileVersion": "8.0.2526.11203" - } - }, - "native": { - "Microsoft.DiaSymReader.Native.amd64.dll": { - "fileVersion": "14.42.34436.0" - }, - "System.IO.Compression.Native.dll": { - "fileVersion": "8.0.2526.11203" - }, - "clretwrc.dll": { - "fileVersion": "8.0.2526.11203" - }, - "clrgc.dll": { - "fileVersion": "8.0.2526.11203" - }, - "clrjit.dll": { - "fileVersion": "8.0.2526.11203" - }, - "coreclr.dll": { - "fileVersion": "8.0.2526.11203" - }, - "createdump.exe": { - "fileVersion": "8.0.2526.11203" - }, - "hostfxr.dll": { - "fileVersion": "8.0.2526.11203" - }, - "hostpolicy.dll": { - "fileVersion": "8.0.2526.11203" - }, - "mscordaccore.dll": { - "fileVersion": "8.0.2526.11203" - }, - "mscordaccore_amd64_amd64_8.0.2526.11203.dll": { - "fileVersion": "8.0.2526.11203" - }, - "mscordbi.dll": { - "fileVersion": "8.0.2526.11203" - }, - "mscorrc.dll": { - "fileVersion": "8.0.2526.11203" - }, - "msquic.dll": { - "fileVersion": "2.4.16.0" - } - } - }, - "runtimepack.Microsoft.WindowsDesktop.App.Runtime.win-x64/8.0.25": { - "runtime": { - "Accessibility.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "DirectWriteForwarder.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "Microsoft.VisualBasic.Forms.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "Microsoft.VisualBasic.dll": { - "assemblyVersion": "10.1.0.0", - "fileVersion": "8.0.2526.11204" - }, - "Microsoft.Win32.Registry.AccessControl.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "Microsoft.Win32.SystemEvents.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "PresentationCore.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "PresentationFramework-SystemCore.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "PresentationFramework-SystemData.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "PresentationFramework-SystemDrawing.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "PresentationFramework-SystemXml.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "PresentationFramework-SystemXmlLinq.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "PresentationFramework.Aero.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "PresentationFramework.Aero2.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "PresentationFramework.AeroLite.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "PresentationFramework.Classic.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "PresentationFramework.Luna.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "PresentationFramework.Royale.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "PresentationFramework.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "PresentationUI.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "ReachFramework.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "System.CodeDom.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Configuration.ConfigurationManager.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Design.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "System.Diagnostics.EventLog.Messages.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "0.0.0.0" - }, - "System.Diagnostics.EventLog.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Diagnostics.PerformanceCounter.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.DirectoryServices.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Drawing.Common.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "System.Drawing.Design.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "System.Drawing.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "System.IO.Packaging.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Printing.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "System.Resources.Extensions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.Cryptography.Pkcs.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.Cryptography.ProtectedData.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.Cryptography.Xml.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Security.Permissions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Threading.AccessControl.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Windows.Controls.Ribbon.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "System.Windows.Extensions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11203" - }, - "System.Windows.Forms.Design.Editors.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "System.Windows.Forms.Design.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "System.Windows.Forms.Primitives.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "System.Windows.Forms.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "System.Windows.Input.Manipulations.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "System.Windows.Presentation.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "System.Xaml.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "UIAutomationClient.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "UIAutomationClientSideProviders.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "UIAutomationProvider.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "UIAutomationTypes.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "WindowsBase.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - }, - "WindowsFormsIntegration.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.2526.11204" - } - }, - "native": { - "D3DCompiler_47_cor3.dll": { - "fileVersion": "10.0.26100.6901" - }, - "PenImc_cor3.dll": { - "fileVersion": "8.0.2526.11204" - }, - "PresentationNative_cor3.dll": { - "fileVersion": "8.0.25.61506" - }, - "vcruntime140_cor3.dll": { - "fileVersion": "14.44.35211.0" - }, - "wpfgfx_cor3.dll": { - "fileVersion": "8.0.2526.11204" - } - } - }, - "DocumentFormat.OpenXml/3.2.0": { - "dependencies": { - "DocumentFormat.OpenXml.Framework": "3.2.0" - }, - "runtime": { - "lib/net8.0/DocumentFormat.OpenXml.dll": { - "assemblyVersion": "3.2.0.0", - "fileVersion": "3.2.0.0" - } - } - }, - "DocumentFormat.OpenXml.Framework/3.2.0": { - "runtime": { - "lib/net8.0/DocumentFormat.OpenXml.Framework.dll": { - "assemblyVersion": "3.2.0.0", - "fileVersion": "3.2.0.0" - } - } - }, - "Markdig/0.37.0": { - "runtime": { - "lib/net8.0/Markdig.dll": { - "assemblyVersion": "0.37.0.0", - "fileVersion": "0.37.0.0" - } - } - }, - "Microsoft.Data.Sqlite/8.0.0": { - "dependencies": { - "Microsoft.Data.Sqlite.Core": "8.0.0", - "SQLitePCLRaw.bundle_e_sqlite3": "2.1.6" - } - }, - "Microsoft.Data.Sqlite.Core/8.0.0": { - "dependencies": { - "SQLitePCLRaw.core": "2.1.6" - }, - "runtime": { - "lib/net8.0/Microsoft.Data.Sqlite.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Web.WebView2/1.0.2903.40": { - "native": { - "runtimes/win-x64/native/WebView2Loader.dll": { - "fileVersion": "1.0.2903.40" - } - } - }, - "SQLitePCLRaw.bundle_e_sqlite3/2.1.6": { - "dependencies": { - "SQLitePCLRaw.lib.e_sqlite3": "2.1.6", - "SQLitePCLRaw.provider.e_sqlite3": "2.1.6" - }, - "runtime": { - "lib/netstandard2.0/SQLitePCLRaw.batteries_v2.dll": { - "assemblyVersion": "2.1.6.2060", - "fileVersion": "2.1.6.2060" - } - } - }, - "SQLitePCLRaw.core/2.1.6": { - "runtime": { - "lib/netstandard2.0/SQLitePCLRaw.core.dll": { - "assemblyVersion": "2.1.6.2060", - "fileVersion": "2.1.6.2060" - } - } - }, - "SQLitePCLRaw.lib.e_sqlite3/2.1.6": { - "native": { - "runtimes/win-x64/native/e_sqlite3.dll": { - "fileVersion": "0.0.0.0" - } - } - }, - "SQLitePCLRaw.provider.e_sqlite3/2.1.6": { - "dependencies": { - "SQLitePCLRaw.core": "2.1.6" - }, - "runtime": { - "lib/net6.0-windows7.0/SQLitePCLRaw.provider.e_sqlite3.dll": { - "assemblyVersion": "2.1.6.2060", - "fileVersion": "2.1.6.2060" - } - } - }, - "System.ServiceProcess.ServiceController/8.0.1": { - "runtime": { - "runtimes/win/lib/net8.0/System.ServiceProcess.ServiceController.dll": { - "assemblyVersion": "8.0.0.1", - "fileVersion": "8.0.1024.46610" - } - } - }, - "UglyToad.PdfPig/1.7.0-custom-5": { - "dependencies": { - "UglyToad.PdfPig.Core": "1.7.0-custom-5", - "UglyToad.PdfPig.Fonts": "1.7.0-custom-5", - "UglyToad.PdfPig.Tokenization": "1.7.0-custom-5", - "UglyToad.PdfPig.Tokens": "1.7.0-custom-5" - }, - "runtime": { - "lib/net6.0/UglyToad.PdfPig.dll": { - "assemblyVersion": "0.1.8.0", - "fileVersion": "0.1.8.0" - } - } - }, - "UglyToad.PdfPig.Core/1.7.0-custom-5": { - "runtime": { - "lib/net6.0/UglyToad.PdfPig.Core.dll": { - "assemblyVersion": "0.1.8.0", - "fileVersion": "0.1.8.0" - } - } - }, - "UglyToad.PdfPig.Fonts/1.7.0-custom-5": { - "dependencies": { - "UglyToad.PdfPig.Core": "1.7.0-custom-5", - "UglyToad.PdfPig.Tokenization": "1.7.0-custom-5", - "UglyToad.PdfPig.Tokens": "1.7.0-custom-5" - }, - "runtime": { - "lib/net6.0/UglyToad.PdfPig.Fonts.dll": { - "assemblyVersion": "0.1.8.0", - "fileVersion": "0.1.8.0" - } - } - }, - "UglyToad.PdfPig.Tokenization/1.7.0-custom-5": { - "dependencies": { - "UglyToad.PdfPig.Core": "1.7.0-custom-5", - "UglyToad.PdfPig.Tokens": "1.7.0-custom-5" - }, - "runtime": { - "lib/net6.0/UglyToad.PdfPig.Tokenization.dll": { - "assemblyVersion": "0.1.8.0", - "fileVersion": "0.1.8.0" - } - } - }, - "UglyToad.PdfPig.Tokens/1.7.0-custom-5": { - "dependencies": { - "UglyToad.PdfPig.Core": "1.7.0-custom-5" - }, - "runtime": { - "lib/net6.0/UglyToad.PdfPig.Tokens.dll": { - "assemblyVersion": "0.1.8.0", - "fileVersion": "0.1.8.0" - } - } - }, - "AxCopilot.SDK/1.0.0": { - "runtime": { - "AxCopilot.SDK.dll": { - "assemblyVersion": "1.0.0.0", - "fileVersion": "1.0.0.0" - } - } - }, - "Microsoft.Web.WebView2.Core/1.0.2903.40": { - "runtime": { - "Microsoft.Web.WebView2.Core.dll": { - "assemblyVersion": "1.0.2903.40", - "fileVersion": "1.0.2903.40" - } - } - }, - "Microsoft.Web.WebView2.WinForms/1.0.2903.40": { - "runtime": { - "Microsoft.Web.WebView2.WinForms.dll": { - "assemblyVersion": "1.0.2903.40", - "fileVersion": "1.0.2903.40" - } - } - }, - "Microsoft.Web.WebView2.Wpf/1.0.2903.40": { - "runtime": { - "Microsoft.Web.WebView2.Wpf.dll": { - "assemblyVersion": "1.0.2903.40", - "fileVersion": "1.0.2903.40" - } - } - } - } - }, - "libraries": { - "AxCopilot/0.7.3": { - "type": "project", - "serviceable": false, - "sha512": "" - }, - "runtimepack.Microsoft.NETCore.App.Runtime.win-x64/8.0.25": { - "type": "runtimepack", - "serviceable": false, - "sha512": "" - }, - "runtimepack.Microsoft.WindowsDesktop.App.Runtime.win-x64/8.0.25": { - "type": "runtimepack", - "serviceable": false, - "sha512": "" - }, - "DocumentFormat.OpenXml/3.2.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-eDBT9G0sAWUvjgE8l8E5bGCFXgxCZXIecQ8dqUnj2PyxyMR5eBmLahqRRw3Q7uSKM3cKbysaL2mEY0JJbEEOEA==", - "path": "documentformat.openxml/3.2.0", - "hashPath": "documentformat.openxml.3.2.0.nupkg.sha512" - }, - "DocumentFormat.OpenXml.Framework/3.2.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-e1neOKqRnSHUom4JQEorAoZ67aiJOp6+Xzsu0fc6IYfFcgQn6roo+w6i2w//N2u/5ilEfvLr35bNO9zaIN7r7g==", - "path": "documentformat.openxml.framework/3.2.0", - "hashPath": "documentformat.openxml.framework.3.2.0.nupkg.sha512" - }, - "Markdig/0.37.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-biiu4MTPFjW55qw6v5Aphtj0MjDLJ14x8ndZwkJUHIeqvaSGKeqhLY7S7Vu/S3k7/c9KwhhnaCDP9hdFNUhcNA==", - "path": "markdig/0.37.0", - "hashPath": "markdig.0.37.0.nupkg.sha512" - }, - "Microsoft.Data.Sqlite/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-H+iC5IvkCCKSNHXzL3JARvDn7VpkvuJM91KVB89sKjeTF/KX/BocNNh93ZJtX5MCQKb/z4yVKgkU2sVIq+xKfg==", - "path": "microsoft.data.sqlite/8.0.0", - "hashPath": "microsoft.data.sqlite.8.0.0.nupkg.sha512" - }, - "Microsoft.Data.Sqlite.Core/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-pujbzfszX7jAl7oTbHhqx7pxd9jibeyHHl8zy1gd55XMaKWjDtc5XhhNYwQnrwWYCInNdVoArbaaAvLgW7TwuA==", - "path": "microsoft.data.sqlite.core/8.0.0", - "hashPath": "microsoft.data.sqlite.core.8.0.0.nupkg.sha512" - }, - "Microsoft.Web.WebView2/1.0.2903.40": { - "type": "package", - "serviceable": true, - "sha512": "sha512-THrzYAnJgE3+cNH+9Epr44XjoZoRELdVpXlWGPs6K9C9G6TqyDfVCeVAR/Er8ljLitIUX5gaSkPsy9wRhD1sgQ==", - "path": "microsoft.web.webview2/1.0.2903.40", - "hashPath": "microsoft.web.webview2.1.0.2903.40.nupkg.sha512" - }, - "SQLitePCLRaw.bundle_e_sqlite3/2.1.6": { - "type": "package", - "serviceable": true, - "sha512": "sha512-BmAf6XWt4TqtowmiWe4/5rRot6GerAeklmOPfviOvwLoF5WwgxcJHAxZtySuyW9r9w+HLILnm8VfJFLCUJYW8A==", - "path": "sqlitepclraw.bundle_e_sqlite3/2.1.6", - "hashPath": "sqlitepclraw.bundle_e_sqlite3.2.1.6.nupkg.sha512" - }, - "SQLitePCLRaw.core/2.1.6": { - "type": "package", - "serviceable": true, - "sha512": "sha512-wO6v9GeMx9CUngAet8hbO7xdm+M42p1XeJq47ogyRoYSvNSp0NGLI+MgC0bhrMk9C17MTVFlLiN6ylyExLCc5w==", - "path": "sqlitepclraw.core/2.1.6", - "hashPath": "sqlitepclraw.core.2.1.6.nupkg.sha512" - }, - "SQLitePCLRaw.lib.e_sqlite3/2.1.6": { - "type": "package", - "serviceable": true, - "sha512": "sha512-2ObJJLkIUIxRpOUlZNGuD4rICpBnrBR5anjyfUFQep4hMOIeqW+XGQYzrNmHSVz5xSWZ3klSbh7sFR6UyDj68Q==", - "path": "sqlitepclraw.lib.e_sqlite3/2.1.6", - "hashPath": "sqlitepclraw.lib.e_sqlite3.2.1.6.nupkg.sha512" - }, - "SQLitePCLRaw.provider.e_sqlite3/2.1.6": { - "type": "package", - "serviceable": true, - "sha512": "sha512-PQ2Oq3yepLY4P7ll145P3xtx2bX8xF4PzaKPRpw9jZlKvfe4LE/saAV82inND9usn1XRpmxXk7Lal3MTI+6CNg==", - "path": "sqlitepclraw.provider.e_sqlite3/2.1.6", - "hashPath": "sqlitepclraw.provider.e_sqlite3.2.1.6.nupkg.sha512" - }, - "System.ServiceProcess.ServiceController/8.0.1": { - "type": "package", - "serviceable": true, - "sha512": "sha512-02I0BXo1kmMBgw03E8Hu4K6nTqur4wpQdcDZrndczPzY2fEoGvlinE35AWbyzLZ2h2IksEZ6an4tVt3hi9j1oA==", - "path": "system.serviceprocess.servicecontroller/8.0.1", - "hashPath": "system.serviceprocess.servicecontroller.8.0.1.nupkg.sha512" - }, - "UglyToad.PdfPig/1.7.0-custom-5": { - "type": "package", - "serviceable": true, - "sha512": "sha512-mddnoBg+XV5YZJg+lp/LlXQ9NY9/oV/MoNjLbbLHw0uTymfyuinVePQB4ff/ELRv3s6n0G7h8q3Ycb3KYg+hgQ==", - "path": "uglytoad.pdfpig/1.7.0-custom-5", - "hashPath": "uglytoad.pdfpig.1.7.0-custom-5.nupkg.sha512" - }, - "UglyToad.PdfPig.Core/1.7.0-custom-5": { - "type": "package", - "serviceable": true, - "sha512": "sha512-bChQUAYApM6/vgBis0+fBTZyAVqjXdqshjZDCgI3dgwUplfLJxXRrnkCOdNj0a6JNcF32R4aLpnGpTc9QmmVmg==", - "path": "uglytoad.pdfpig.core/1.7.0-custom-5", - "hashPath": "uglytoad.pdfpig.core.1.7.0-custom-5.nupkg.sha512" - }, - "UglyToad.PdfPig.Fonts/1.7.0-custom-5": { - "type": "package", - "serviceable": true, - "sha512": "sha512-Z6SBBAIL8wRkJNhXGYaz0CrHnNrNeuNtmwRbBtQUA1b3TDhRQppOmHCIuhjb6Vu/Rirp6FIOtzAU1lXsGik90w==", - "path": "uglytoad.pdfpig.fonts/1.7.0-custom-5", - "hashPath": "uglytoad.pdfpig.fonts.1.7.0-custom-5.nupkg.sha512" - }, - "UglyToad.PdfPig.Tokenization/1.7.0-custom-5": { - "type": "package", - "serviceable": true, - "sha512": "sha512-U8VVH7VJjv6czP7qWyzDq6CRaiJQe7/sESUCL8H3kiEa3zi0l9TonIKlD/YidQ5DlgTumracii6zjLyKPEFKwA==", - "path": "uglytoad.pdfpig.tokenization/1.7.0-custom-5", - "hashPath": "uglytoad.pdfpig.tokenization.1.7.0-custom-5.nupkg.sha512" - }, - "UglyToad.PdfPig.Tokens/1.7.0-custom-5": { - "type": "package", - "serviceable": true, - "sha512": "sha512-m/j5RVfL4eF/OwX6ASprzK+yzD3l7xdgQ7zQPgENhjxfuXD+hj6FSeZlmxSTt9ywvWcTCjGKAILl9XTK9iQgCQ==", - "path": "uglytoad.pdfpig.tokens/1.7.0-custom-5", - "hashPath": "uglytoad.pdfpig.tokens.1.7.0-custom-5.nupkg.sha512" - }, - "AxCopilot.SDK/1.0.0": { - "type": "project", - "serviceable": false, - "sha512": "" - }, - "Microsoft.Web.WebView2.Core/1.0.2903.40": { - "type": "reference", - "serviceable": false, - "sha512": "" - }, - "Microsoft.Web.WebView2.WinForms/1.0.2903.40": { - "type": "reference", - "serviceable": false, - "sha512": "" - }, - "Microsoft.Web.WebView2.Wpf/1.0.2903.40": { - "type": "reference", - "serviceable": false, - "sha512": "" - } - }, - "runtimes": { - "win-x64": [ - "win", - "any", - "base" - ] - } -} \ No newline at end of file diff --git a/dist/AxCopilot/AxCopilot.dll b/dist/AxCopilot/AxCopilot.dll deleted file mode 100644 index bf651e3..0000000 Binary files a/dist/AxCopilot/AxCopilot.dll and /dev/null differ diff --git a/dist/AxCopilot/AxCopilot.exe b/dist/AxCopilot/AxCopilot.exe index 08b5f28..bd143e9 100644 Binary files a/dist/AxCopilot/AxCopilot.exe and b/dist/AxCopilot/AxCopilot.exe differ diff --git a/dist/AxCopilot/AxCopilot.runtimeconfig.json b/dist/AxCopilot/AxCopilot.runtimeconfig.json deleted file mode 100644 index 524daea..0000000 --- a/dist/AxCopilot/AxCopilot.runtimeconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "runtimeOptions": { - "tfm": "net8.0", - "includedFrameworks": [ - { - "name": "Microsoft.NETCore.App", - "version": "8.0.25" - }, - { - "name": "Microsoft.WindowsDesktop.App", - "version": "8.0.25" - } - ], - "configProperties": { - "System.Reflection.Metadata.MetadataUpdater.IsSupported": false, - "CSWINRT_USE_WINDOWS_UI_XAML_PROJECTIONS": false - } - } -} \ No newline at end of file diff --git a/dist/AxCopilot/D3DCompiler_47_cor3.dll b/dist/AxCopilot/D3DCompiler_47_cor3.dll deleted file mode 100644 index ef8ac8c..0000000 Binary files a/dist/AxCopilot/D3DCompiler_47_cor3.dll and /dev/null differ diff --git a/dist/AxCopilot/DirectWriteForwarder.dll b/dist/AxCopilot/DirectWriteForwarder.dll deleted file mode 100644 index f9d1933..0000000 Binary files a/dist/AxCopilot/DirectWriteForwarder.dll and /dev/null differ diff --git a/dist/AxCopilot/DocumentFormat.OpenXml.Framework.dll b/dist/AxCopilot/DocumentFormat.OpenXml.Framework.dll deleted file mode 100644 index f9f3da9..0000000 Binary files a/dist/AxCopilot/DocumentFormat.OpenXml.Framework.dll and /dev/null differ diff --git a/dist/AxCopilot/DocumentFormat.OpenXml.dll b/dist/AxCopilot/DocumentFormat.OpenXml.dll deleted file mode 100644 index fc7bada..0000000 Binary files a/dist/AxCopilot/DocumentFormat.OpenXml.dll and /dev/null differ diff --git a/dist/AxCopilot/Markdig.dll b/dist/AxCopilot/Markdig.dll deleted file mode 100644 index 4931e93..0000000 Binary files a/dist/AxCopilot/Markdig.dll and /dev/null differ diff --git a/dist/AxCopilot/Microsoft.CSharp.dll b/dist/AxCopilot/Microsoft.CSharp.dll deleted file mode 100644 index 80d592c..0000000 Binary files a/dist/AxCopilot/Microsoft.CSharp.dll and /dev/null differ diff --git a/dist/AxCopilot/Microsoft.Data.Sqlite.dll b/dist/AxCopilot/Microsoft.Data.Sqlite.dll deleted file mode 100644 index c5fef6d..0000000 Binary files a/dist/AxCopilot/Microsoft.Data.Sqlite.dll and /dev/null differ diff --git a/dist/AxCopilot/Microsoft.DiaSymReader.Native.amd64.dll b/dist/AxCopilot/Microsoft.DiaSymReader.Native.amd64.dll deleted file mode 100644 index 92b355b..0000000 Binary files a/dist/AxCopilot/Microsoft.DiaSymReader.Native.amd64.dll and /dev/null differ diff --git a/dist/AxCopilot/Microsoft.VisualBasic.Core.dll b/dist/AxCopilot/Microsoft.VisualBasic.Core.dll deleted file mode 100644 index 87b1964..0000000 Binary files a/dist/AxCopilot/Microsoft.VisualBasic.Core.dll and /dev/null differ diff --git a/dist/AxCopilot/Microsoft.VisualBasic.Forms.dll b/dist/AxCopilot/Microsoft.VisualBasic.Forms.dll deleted file mode 100644 index 12a1be7..0000000 Binary files a/dist/AxCopilot/Microsoft.VisualBasic.Forms.dll and /dev/null differ diff --git a/dist/AxCopilot/Microsoft.VisualBasic.dll b/dist/AxCopilot/Microsoft.VisualBasic.dll deleted file mode 100644 index 69c4a13..0000000 Binary files a/dist/AxCopilot/Microsoft.VisualBasic.dll and /dev/null differ diff --git a/dist/AxCopilot/Microsoft.Web.WebView2.Core.dll b/dist/AxCopilot/Microsoft.Web.WebView2.Core.dll deleted file mode 100644 index 4a27877..0000000 Binary files a/dist/AxCopilot/Microsoft.Web.WebView2.Core.dll and /dev/null differ diff --git a/dist/AxCopilot/Microsoft.Web.WebView2.WinForms.dll b/dist/AxCopilot/Microsoft.Web.WebView2.WinForms.dll deleted file mode 100644 index b2d08ad..0000000 Binary files a/dist/AxCopilot/Microsoft.Web.WebView2.WinForms.dll and /dev/null differ diff --git a/dist/AxCopilot/Microsoft.Web.WebView2.Wpf.dll b/dist/AxCopilot/Microsoft.Web.WebView2.Wpf.dll deleted file mode 100644 index ed79cd9..0000000 Binary files a/dist/AxCopilot/Microsoft.Web.WebView2.Wpf.dll and /dev/null differ diff --git a/dist/AxCopilot/Microsoft.Win32.Primitives.dll b/dist/AxCopilot/Microsoft.Win32.Primitives.dll deleted file mode 100644 index de80f4f..0000000 Binary files a/dist/AxCopilot/Microsoft.Win32.Primitives.dll and /dev/null differ diff --git a/dist/AxCopilot/Microsoft.Win32.Registry.AccessControl.dll b/dist/AxCopilot/Microsoft.Win32.Registry.AccessControl.dll deleted file mode 100644 index cb43e37..0000000 Binary files a/dist/AxCopilot/Microsoft.Win32.Registry.AccessControl.dll and /dev/null differ diff --git a/dist/AxCopilot/Microsoft.Win32.Registry.dll b/dist/AxCopilot/Microsoft.Win32.Registry.dll deleted file mode 100644 index 180bfcd..0000000 Binary files a/dist/AxCopilot/Microsoft.Win32.Registry.dll and /dev/null differ diff --git a/dist/AxCopilot/Microsoft.Win32.SystemEvents.dll b/dist/AxCopilot/Microsoft.Win32.SystemEvents.dll deleted file mode 100644 index 9da2cae..0000000 Binary files a/dist/AxCopilot/Microsoft.Win32.SystemEvents.dll and /dev/null differ diff --git a/dist/AxCopilot/PenImc_cor3.dll b/dist/AxCopilot/PenImc_cor3.dll deleted file mode 100644 index 9490687..0000000 Binary files a/dist/AxCopilot/PenImc_cor3.dll and /dev/null differ diff --git a/dist/AxCopilot/PresentationCore.dll b/dist/AxCopilot/PresentationCore.dll deleted file mode 100644 index 8da7479..0000000 Binary files a/dist/AxCopilot/PresentationCore.dll and /dev/null differ diff --git a/dist/AxCopilot/PresentationFramework-SystemCore.dll b/dist/AxCopilot/PresentationFramework-SystemCore.dll deleted file mode 100644 index 5954507..0000000 Binary files a/dist/AxCopilot/PresentationFramework-SystemCore.dll and /dev/null differ diff --git a/dist/AxCopilot/PresentationFramework-SystemData.dll b/dist/AxCopilot/PresentationFramework-SystemData.dll deleted file mode 100644 index cc53a1c..0000000 Binary files a/dist/AxCopilot/PresentationFramework-SystemData.dll and /dev/null differ diff --git a/dist/AxCopilot/PresentationFramework-SystemDrawing.dll b/dist/AxCopilot/PresentationFramework-SystemDrawing.dll deleted file mode 100644 index 0ea9322..0000000 Binary files a/dist/AxCopilot/PresentationFramework-SystemDrawing.dll and /dev/null differ diff --git a/dist/AxCopilot/PresentationFramework-SystemXml.dll b/dist/AxCopilot/PresentationFramework-SystemXml.dll deleted file mode 100644 index 3b661b8..0000000 Binary files a/dist/AxCopilot/PresentationFramework-SystemXml.dll and /dev/null differ diff --git a/dist/AxCopilot/PresentationFramework-SystemXmlLinq.dll b/dist/AxCopilot/PresentationFramework-SystemXmlLinq.dll deleted file mode 100644 index 252c653..0000000 Binary files a/dist/AxCopilot/PresentationFramework-SystemXmlLinq.dll and /dev/null differ diff --git a/dist/AxCopilot/PresentationFramework.Aero.dll b/dist/AxCopilot/PresentationFramework.Aero.dll deleted file mode 100644 index 4e6ac9b..0000000 Binary files a/dist/AxCopilot/PresentationFramework.Aero.dll and /dev/null differ diff --git a/dist/AxCopilot/PresentationFramework.Aero2.dll b/dist/AxCopilot/PresentationFramework.Aero2.dll deleted file mode 100644 index f9608ee..0000000 Binary files a/dist/AxCopilot/PresentationFramework.Aero2.dll and /dev/null differ diff --git a/dist/AxCopilot/PresentationFramework.AeroLite.dll b/dist/AxCopilot/PresentationFramework.AeroLite.dll deleted file mode 100644 index 5b58d6e..0000000 Binary files a/dist/AxCopilot/PresentationFramework.AeroLite.dll and /dev/null differ diff --git a/dist/AxCopilot/PresentationFramework.Classic.dll b/dist/AxCopilot/PresentationFramework.Classic.dll deleted file mode 100644 index 0c1bee8..0000000 Binary files a/dist/AxCopilot/PresentationFramework.Classic.dll and /dev/null differ diff --git a/dist/AxCopilot/PresentationFramework.Luna.dll b/dist/AxCopilot/PresentationFramework.Luna.dll deleted file mode 100644 index cf290b4..0000000 Binary files a/dist/AxCopilot/PresentationFramework.Luna.dll and /dev/null differ diff --git a/dist/AxCopilot/PresentationFramework.Royale.dll b/dist/AxCopilot/PresentationFramework.Royale.dll deleted file mode 100644 index 86e73c5..0000000 Binary files a/dist/AxCopilot/PresentationFramework.Royale.dll and /dev/null differ diff --git a/dist/AxCopilot/PresentationFramework.dll b/dist/AxCopilot/PresentationFramework.dll deleted file mode 100644 index 437bee9..0000000 Binary files a/dist/AxCopilot/PresentationFramework.dll and /dev/null differ diff --git a/dist/AxCopilot/PresentationNative_cor3.dll b/dist/AxCopilot/PresentationNative_cor3.dll deleted file mode 100644 index 589a8ee..0000000 Binary files a/dist/AxCopilot/PresentationNative_cor3.dll and /dev/null differ diff --git a/dist/AxCopilot/PresentationUI.dll b/dist/AxCopilot/PresentationUI.dll deleted file mode 100644 index e39b9f8..0000000 Binary files a/dist/AxCopilot/PresentationUI.dll and /dev/null differ diff --git a/dist/AxCopilot/ReachFramework.dll b/dist/AxCopilot/ReachFramework.dll deleted file mode 100644 index fa651eb..0000000 Binary files a/dist/AxCopilot/ReachFramework.dll and /dev/null differ diff --git a/dist/AxCopilot/SQLitePCLRaw.batteries_v2.dll b/dist/AxCopilot/SQLitePCLRaw.batteries_v2.dll deleted file mode 100644 index f9eb46b..0000000 Binary files a/dist/AxCopilot/SQLitePCLRaw.batteries_v2.dll and /dev/null differ diff --git a/dist/AxCopilot/SQLitePCLRaw.core.dll b/dist/AxCopilot/SQLitePCLRaw.core.dll deleted file mode 100644 index 556d40f..0000000 Binary files a/dist/AxCopilot/SQLitePCLRaw.core.dll and /dev/null differ diff --git a/dist/AxCopilot/SQLitePCLRaw.provider.e_sqlite3.dll b/dist/AxCopilot/SQLitePCLRaw.provider.e_sqlite3.dll deleted file mode 100644 index a6df9a6..0000000 Binary files a/dist/AxCopilot/SQLitePCLRaw.provider.e_sqlite3.dll and /dev/null differ diff --git a/dist/AxCopilot/System.AppContext.dll b/dist/AxCopilot/System.AppContext.dll deleted file mode 100644 index a7e6dd0..0000000 Binary files a/dist/AxCopilot/System.AppContext.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Buffers.dll b/dist/AxCopilot/System.Buffers.dll deleted file mode 100644 index 9f017b1..0000000 Binary files a/dist/AxCopilot/System.Buffers.dll and /dev/null differ diff --git a/dist/AxCopilot/System.CodeDom.dll b/dist/AxCopilot/System.CodeDom.dll deleted file mode 100644 index f6e3693..0000000 Binary files a/dist/AxCopilot/System.CodeDom.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Collections.Concurrent.dll b/dist/AxCopilot/System.Collections.Concurrent.dll deleted file mode 100644 index 5a5d56a..0000000 Binary files a/dist/AxCopilot/System.Collections.Concurrent.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Collections.Immutable.dll b/dist/AxCopilot/System.Collections.Immutable.dll deleted file mode 100644 index ea74832..0000000 Binary files a/dist/AxCopilot/System.Collections.Immutable.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Collections.NonGeneric.dll b/dist/AxCopilot/System.Collections.NonGeneric.dll deleted file mode 100644 index ac7b3f3..0000000 Binary files a/dist/AxCopilot/System.Collections.NonGeneric.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Collections.Specialized.dll b/dist/AxCopilot/System.Collections.Specialized.dll deleted file mode 100644 index 7859a23..0000000 Binary files a/dist/AxCopilot/System.Collections.Specialized.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Collections.dll b/dist/AxCopilot/System.Collections.dll deleted file mode 100644 index 8a97ac3..0000000 Binary files a/dist/AxCopilot/System.Collections.dll and /dev/null differ diff --git a/dist/AxCopilot/System.ComponentModel.Annotations.dll b/dist/AxCopilot/System.ComponentModel.Annotations.dll deleted file mode 100644 index 6f3147b..0000000 Binary files a/dist/AxCopilot/System.ComponentModel.Annotations.dll and /dev/null differ diff --git a/dist/AxCopilot/System.ComponentModel.DataAnnotations.dll b/dist/AxCopilot/System.ComponentModel.DataAnnotations.dll deleted file mode 100644 index 4fa8f4b..0000000 Binary files a/dist/AxCopilot/System.ComponentModel.DataAnnotations.dll and /dev/null differ diff --git a/dist/AxCopilot/System.ComponentModel.EventBasedAsync.dll b/dist/AxCopilot/System.ComponentModel.EventBasedAsync.dll deleted file mode 100644 index 662a7cc..0000000 Binary files a/dist/AxCopilot/System.ComponentModel.EventBasedAsync.dll and /dev/null differ diff --git a/dist/AxCopilot/System.ComponentModel.Primitives.dll b/dist/AxCopilot/System.ComponentModel.Primitives.dll deleted file mode 100644 index 0e8048f..0000000 Binary files a/dist/AxCopilot/System.ComponentModel.Primitives.dll and /dev/null differ diff --git a/dist/AxCopilot/System.ComponentModel.TypeConverter.dll b/dist/AxCopilot/System.ComponentModel.TypeConverter.dll deleted file mode 100644 index b466c2c..0000000 Binary files a/dist/AxCopilot/System.ComponentModel.TypeConverter.dll and /dev/null differ diff --git a/dist/AxCopilot/System.ComponentModel.dll b/dist/AxCopilot/System.ComponentModel.dll deleted file mode 100644 index 61445ca..0000000 Binary files a/dist/AxCopilot/System.ComponentModel.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Configuration.ConfigurationManager.dll b/dist/AxCopilot/System.Configuration.ConfigurationManager.dll deleted file mode 100644 index 4cd2a7e..0000000 Binary files a/dist/AxCopilot/System.Configuration.ConfigurationManager.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Configuration.dll b/dist/AxCopilot/System.Configuration.dll deleted file mode 100644 index 782d3bf..0000000 Binary files a/dist/AxCopilot/System.Configuration.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Console.dll b/dist/AxCopilot/System.Console.dll deleted file mode 100644 index 8b2d00c..0000000 Binary files a/dist/AxCopilot/System.Console.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Core.dll b/dist/AxCopilot/System.Core.dll deleted file mode 100644 index 64d4621..0000000 Binary files a/dist/AxCopilot/System.Core.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Data.Common.dll b/dist/AxCopilot/System.Data.Common.dll deleted file mode 100644 index 0d79229..0000000 Binary files a/dist/AxCopilot/System.Data.Common.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Data.DataSetExtensions.dll b/dist/AxCopilot/System.Data.DataSetExtensions.dll deleted file mode 100644 index 24a10a8..0000000 Binary files a/dist/AxCopilot/System.Data.DataSetExtensions.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Data.dll b/dist/AxCopilot/System.Data.dll deleted file mode 100644 index d03eba7..0000000 Binary files a/dist/AxCopilot/System.Data.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Design.dll b/dist/AxCopilot/System.Design.dll deleted file mode 100644 index be5a5dc..0000000 Binary files a/dist/AxCopilot/System.Design.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Diagnostics.Contracts.dll b/dist/AxCopilot/System.Diagnostics.Contracts.dll deleted file mode 100644 index c72d41f..0000000 Binary files a/dist/AxCopilot/System.Diagnostics.Contracts.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Diagnostics.Debug.dll b/dist/AxCopilot/System.Diagnostics.Debug.dll deleted file mode 100644 index 3204c5f..0000000 Binary files a/dist/AxCopilot/System.Diagnostics.Debug.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Diagnostics.DiagnosticSource.dll b/dist/AxCopilot/System.Diagnostics.DiagnosticSource.dll deleted file mode 100644 index 7765fff..0000000 Binary files a/dist/AxCopilot/System.Diagnostics.DiagnosticSource.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Diagnostics.EventLog.Messages.dll b/dist/AxCopilot/System.Diagnostics.EventLog.Messages.dll deleted file mode 100644 index 8352119..0000000 Binary files a/dist/AxCopilot/System.Diagnostics.EventLog.Messages.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Diagnostics.EventLog.dll b/dist/AxCopilot/System.Diagnostics.EventLog.dll deleted file mode 100644 index a6b646e..0000000 Binary files a/dist/AxCopilot/System.Diagnostics.EventLog.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Diagnostics.FileVersionInfo.dll b/dist/AxCopilot/System.Diagnostics.FileVersionInfo.dll deleted file mode 100644 index 0a96c36..0000000 Binary files a/dist/AxCopilot/System.Diagnostics.FileVersionInfo.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Diagnostics.PerformanceCounter.dll b/dist/AxCopilot/System.Diagnostics.PerformanceCounter.dll deleted file mode 100644 index ffb6dc1..0000000 Binary files a/dist/AxCopilot/System.Diagnostics.PerformanceCounter.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Diagnostics.Process.dll b/dist/AxCopilot/System.Diagnostics.Process.dll deleted file mode 100644 index 83c80bd..0000000 Binary files a/dist/AxCopilot/System.Diagnostics.Process.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Diagnostics.StackTrace.dll b/dist/AxCopilot/System.Diagnostics.StackTrace.dll deleted file mode 100644 index 190a45b..0000000 Binary files a/dist/AxCopilot/System.Diagnostics.StackTrace.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Diagnostics.TextWriterTraceListener.dll b/dist/AxCopilot/System.Diagnostics.TextWriterTraceListener.dll deleted file mode 100644 index eacf93b..0000000 Binary files a/dist/AxCopilot/System.Diagnostics.TextWriterTraceListener.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Diagnostics.Tools.dll b/dist/AxCopilot/System.Diagnostics.Tools.dll deleted file mode 100644 index 4bc3799..0000000 Binary files a/dist/AxCopilot/System.Diagnostics.Tools.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Diagnostics.TraceSource.dll b/dist/AxCopilot/System.Diagnostics.TraceSource.dll deleted file mode 100644 index 5235685..0000000 Binary files a/dist/AxCopilot/System.Diagnostics.TraceSource.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Diagnostics.Tracing.dll b/dist/AxCopilot/System.Diagnostics.Tracing.dll deleted file mode 100644 index 9d5a6f1..0000000 Binary files a/dist/AxCopilot/System.Diagnostics.Tracing.dll and /dev/null differ diff --git a/dist/AxCopilot/System.DirectoryServices.dll b/dist/AxCopilot/System.DirectoryServices.dll deleted file mode 100644 index 91201f9..0000000 Binary files a/dist/AxCopilot/System.DirectoryServices.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Drawing.Common.dll b/dist/AxCopilot/System.Drawing.Common.dll deleted file mode 100644 index c7c7349..0000000 Binary files a/dist/AxCopilot/System.Drawing.Common.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Drawing.Design.dll b/dist/AxCopilot/System.Drawing.Design.dll deleted file mode 100644 index fc2d666..0000000 Binary files a/dist/AxCopilot/System.Drawing.Design.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Drawing.Primitives.dll b/dist/AxCopilot/System.Drawing.Primitives.dll deleted file mode 100644 index 64b81ae..0000000 Binary files a/dist/AxCopilot/System.Drawing.Primitives.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Drawing.dll b/dist/AxCopilot/System.Drawing.dll deleted file mode 100644 index 1307b8b..0000000 Binary files a/dist/AxCopilot/System.Drawing.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Dynamic.Runtime.dll b/dist/AxCopilot/System.Dynamic.Runtime.dll deleted file mode 100644 index d9e24c5..0000000 Binary files a/dist/AxCopilot/System.Dynamic.Runtime.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Formats.Asn1.dll b/dist/AxCopilot/System.Formats.Asn1.dll deleted file mode 100644 index e094803..0000000 Binary files a/dist/AxCopilot/System.Formats.Asn1.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Formats.Tar.dll b/dist/AxCopilot/System.Formats.Tar.dll deleted file mode 100644 index 1230561..0000000 Binary files a/dist/AxCopilot/System.Formats.Tar.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Globalization.Calendars.dll b/dist/AxCopilot/System.Globalization.Calendars.dll deleted file mode 100644 index 9b3821b..0000000 Binary files a/dist/AxCopilot/System.Globalization.Calendars.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Globalization.Extensions.dll b/dist/AxCopilot/System.Globalization.Extensions.dll deleted file mode 100644 index 8d1e39b..0000000 Binary files a/dist/AxCopilot/System.Globalization.Extensions.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Globalization.dll b/dist/AxCopilot/System.Globalization.dll deleted file mode 100644 index aceb317..0000000 Binary files a/dist/AxCopilot/System.Globalization.dll and /dev/null differ diff --git a/dist/AxCopilot/System.IO.Compression.Brotli.dll b/dist/AxCopilot/System.IO.Compression.Brotli.dll deleted file mode 100644 index 38c445d..0000000 Binary files a/dist/AxCopilot/System.IO.Compression.Brotli.dll and /dev/null differ diff --git a/dist/AxCopilot/System.IO.Compression.FileSystem.dll b/dist/AxCopilot/System.IO.Compression.FileSystem.dll deleted file mode 100644 index 6adbf64..0000000 Binary files a/dist/AxCopilot/System.IO.Compression.FileSystem.dll and /dev/null differ diff --git a/dist/AxCopilot/System.IO.Compression.Native.dll b/dist/AxCopilot/System.IO.Compression.Native.dll deleted file mode 100644 index 2e60b93..0000000 Binary files a/dist/AxCopilot/System.IO.Compression.Native.dll and /dev/null differ diff --git a/dist/AxCopilot/System.IO.Compression.ZipFile.dll b/dist/AxCopilot/System.IO.Compression.ZipFile.dll deleted file mode 100644 index 85fada0..0000000 Binary files a/dist/AxCopilot/System.IO.Compression.ZipFile.dll and /dev/null differ diff --git a/dist/AxCopilot/System.IO.Compression.dll b/dist/AxCopilot/System.IO.Compression.dll deleted file mode 100644 index 790716f..0000000 Binary files a/dist/AxCopilot/System.IO.Compression.dll and /dev/null differ diff --git a/dist/AxCopilot/System.IO.FileSystem.AccessControl.dll b/dist/AxCopilot/System.IO.FileSystem.AccessControl.dll deleted file mode 100644 index 4c3e64d..0000000 Binary files a/dist/AxCopilot/System.IO.FileSystem.AccessControl.dll and /dev/null differ diff --git a/dist/AxCopilot/System.IO.FileSystem.DriveInfo.dll b/dist/AxCopilot/System.IO.FileSystem.DriveInfo.dll deleted file mode 100644 index 882e082..0000000 Binary files a/dist/AxCopilot/System.IO.FileSystem.DriveInfo.dll and /dev/null differ diff --git a/dist/AxCopilot/System.IO.FileSystem.Primitives.dll b/dist/AxCopilot/System.IO.FileSystem.Primitives.dll deleted file mode 100644 index 902567f..0000000 Binary files a/dist/AxCopilot/System.IO.FileSystem.Primitives.dll and /dev/null differ diff --git a/dist/AxCopilot/System.IO.FileSystem.Watcher.dll b/dist/AxCopilot/System.IO.FileSystem.Watcher.dll deleted file mode 100644 index 909de72..0000000 Binary files a/dist/AxCopilot/System.IO.FileSystem.Watcher.dll and /dev/null differ diff --git a/dist/AxCopilot/System.IO.FileSystem.dll b/dist/AxCopilot/System.IO.FileSystem.dll deleted file mode 100644 index ef3f38f..0000000 Binary files a/dist/AxCopilot/System.IO.FileSystem.dll and /dev/null differ diff --git a/dist/AxCopilot/System.IO.IsolatedStorage.dll b/dist/AxCopilot/System.IO.IsolatedStorage.dll deleted file mode 100644 index 3e73992..0000000 Binary files a/dist/AxCopilot/System.IO.IsolatedStorage.dll and /dev/null differ diff --git a/dist/AxCopilot/System.IO.MemoryMappedFiles.dll b/dist/AxCopilot/System.IO.MemoryMappedFiles.dll deleted file mode 100644 index 43756aa..0000000 Binary files a/dist/AxCopilot/System.IO.MemoryMappedFiles.dll and /dev/null differ diff --git a/dist/AxCopilot/System.IO.Packaging.dll b/dist/AxCopilot/System.IO.Packaging.dll deleted file mode 100644 index 617e201..0000000 Binary files a/dist/AxCopilot/System.IO.Packaging.dll and /dev/null differ diff --git a/dist/AxCopilot/System.IO.Pipes.AccessControl.dll b/dist/AxCopilot/System.IO.Pipes.AccessControl.dll deleted file mode 100644 index 617b9cd..0000000 Binary files a/dist/AxCopilot/System.IO.Pipes.AccessControl.dll and /dev/null differ diff --git a/dist/AxCopilot/System.IO.Pipes.dll b/dist/AxCopilot/System.IO.Pipes.dll deleted file mode 100644 index b6fa8eb..0000000 Binary files a/dist/AxCopilot/System.IO.Pipes.dll and /dev/null differ diff --git a/dist/AxCopilot/System.IO.UnmanagedMemoryStream.dll b/dist/AxCopilot/System.IO.UnmanagedMemoryStream.dll deleted file mode 100644 index 91ade0c..0000000 Binary files a/dist/AxCopilot/System.IO.UnmanagedMemoryStream.dll and /dev/null differ diff --git a/dist/AxCopilot/System.IO.dll b/dist/AxCopilot/System.IO.dll deleted file mode 100644 index 3529768..0000000 Binary files a/dist/AxCopilot/System.IO.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Linq.Expressions.dll b/dist/AxCopilot/System.Linq.Expressions.dll deleted file mode 100644 index e3f4697..0000000 Binary files a/dist/AxCopilot/System.Linq.Expressions.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Linq.Parallel.dll b/dist/AxCopilot/System.Linq.Parallel.dll deleted file mode 100644 index adc335a..0000000 Binary files a/dist/AxCopilot/System.Linq.Parallel.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Linq.Queryable.dll b/dist/AxCopilot/System.Linq.Queryable.dll deleted file mode 100644 index e276a00..0000000 Binary files a/dist/AxCopilot/System.Linq.Queryable.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Linq.dll b/dist/AxCopilot/System.Linq.dll deleted file mode 100644 index e9b9ddf..0000000 Binary files a/dist/AxCopilot/System.Linq.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Memory.dll b/dist/AxCopilot/System.Memory.dll deleted file mode 100644 index 9c836e8..0000000 Binary files a/dist/AxCopilot/System.Memory.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.Http.Json.dll b/dist/AxCopilot/System.Net.Http.Json.dll deleted file mode 100644 index f1d682c..0000000 Binary files a/dist/AxCopilot/System.Net.Http.Json.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.Http.dll b/dist/AxCopilot/System.Net.Http.dll deleted file mode 100644 index f03455b..0000000 Binary files a/dist/AxCopilot/System.Net.Http.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.HttpListener.dll b/dist/AxCopilot/System.Net.HttpListener.dll deleted file mode 100644 index bd42e7b..0000000 Binary files a/dist/AxCopilot/System.Net.HttpListener.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.Mail.dll b/dist/AxCopilot/System.Net.Mail.dll deleted file mode 100644 index c8b57ca..0000000 Binary files a/dist/AxCopilot/System.Net.Mail.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.NameResolution.dll b/dist/AxCopilot/System.Net.NameResolution.dll deleted file mode 100644 index 1235e88..0000000 Binary files a/dist/AxCopilot/System.Net.NameResolution.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.NetworkInformation.dll b/dist/AxCopilot/System.Net.NetworkInformation.dll deleted file mode 100644 index a493fa2..0000000 Binary files a/dist/AxCopilot/System.Net.NetworkInformation.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.Ping.dll b/dist/AxCopilot/System.Net.Ping.dll deleted file mode 100644 index cfb9833..0000000 Binary files a/dist/AxCopilot/System.Net.Ping.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.Primitives.dll b/dist/AxCopilot/System.Net.Primitives.dll deleted file mode 100644 index ca6eabd..0000000 Binary files a/dist/AxCopilot/System.Net.Primitives.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.Quic.dll b/dist/AxCopilot/System.Net.Quic.dll deleted file mode 100644 index cf938d8..0000000 Binary files a/dist/AxCopilot/System.Net.Quic.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.Requests.dll b/dist/AxCopilot/System.Net.Requests.dll deleted file mode 100644 index 58e3d6d..0000000 Binary files a/dist/AxCopilot/System.Net.Requests.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.Security.dll b/dist/AxCopilot/System.Net.Security.dll deleted file mode 100644 index cd7e9c6..0000000 Binary files a/dist/AxCopilot/System.Net.Security.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.ServicePoint.dll b/dist/AxCopilot/System.Net.ServicePoint.dll deleted file mode 100644 index 4c5aa19..0000000 Binary files a/dist/AxCopilot/System.Net.ServicePoint.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.Sockets.dll b/dist/AxCopilot/System.Net.Sockets.dll deleted file mode 100644 index e152432..0000000 Binary files a/dist/AxCopilot/System.Net.Sockets.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.WebClient.dll b/dist/AxCopilot/System.Net.WebClient.dll deleted file mode 100644 index a60c500..0000000 Binary files a/dist/AxCopilot/System.Net.WebClient.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.WebHeaderCollection.dll b/dist/AxCopilot/System.Net.WebHeaderCollection.dll deleted file mode 100644 index 35f2511..0000000 Binary files a/dist/AxCopilot/System.Net.WebHeaderCollection.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.WebProxy.dll b/dist/AxCopilot/System.Net.WebProxy.dll deleted file mode 100644 index 0f3fa06..0000000 Binary files a/dist/AxCopilot/System.Net.WebProxy.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.WebSockets.Client.dll b/dist/AxCopilot/System.Net.WebSockets.Client.dll deleted file mode 100644 index 712555d..0000000 Binary files a/dist/AxCopilot/System.Net.WebSockets.Client.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.WebSockets.dll b/dist/AxCopilot/System.Net.WebSockets.dll deleted file mode 100644 index afceb08..0000000 Binary files a/dist/AxCopilot/System.Net.WebSockets.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Net.dll b/dist/AxCopilot/System.Net.dll deleted file mode 100644 index 784b9a6..0000000 Binary files a/dist/AxCopilot/System.Net.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Numerics.Vectors.dll b/dist/AxCopilot/System.Numerics.Vectors.dll deleted file mode 100644 index 55ae19a..0000000 Binary files a/dist/AxCopilot/System.Numerics.Vectors.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Numerics.dll b/dist/AxCopilot/System.Numerics.dll deleted file mode 100644 index 25d0431..0000000 Binary files a/dist/AxCopilot/System.Numerics.dll and /dev/null differ diff --git a/dist/AxCopilot/System.ObjectModel.dll b/dist/AxCopilot/System.ObjectModel.dll deleted file mode 100644 index d3a65af..0000000 Binary files a/dist/AxCopilot/System.ObjectModel.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Printing.dll b/dist/AxCopilot/System.Printing.dll deleted file mode 100644 index 552ab94..0000000 Binary files a/dist/AxCopilot/System.Printing.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Private.CoreLib.dll b/dist/AxCopilot/System.Private.CoreLib.dll deleted file mode 100644 index 36bda7f..0000000 Binary files a/dist/AxCopilot/System.Private.CoreLib.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Private.DataContractSerialization.dll b/dist/AxCopilot/System.Private.DataContractSerialization.dll deleted file mode 100644 index 34e3226..0000000 Binary files a/dist/AxCopilot/System.Private.DataContractSerialization.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Private.Uri.dll b/dist/AxCopilot/System.Private.Uri.dll deleted file mode 100644 index b66b207..0000000 Binary files a/dist/AxCopilot/System.Private.Uri.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Private.Xml.Linq.dll b/dist/AxCopilot/System.Private.Xml.Linq.dll deleted file mode 100644 index 93c8df2..0000000 Binary files a/dist/AxCopilot/System.Private.Xml.Linq.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Private.Xml.dll b/dist/AxCopilot/System.Private.Xml.dll deleted file mode 100644 index 51bb5bb..0000000 Binary files a/dist/AxCopilot/System.Private.Xml.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Reflection.DispatchProxy.dll b/dist/AxCopilot/System.Reflection.DispatchProxy.dll deleted file mode 100644 index 2434e6c..0000000 Binary files a/dist/AxCopilot/System.Reflection.DispatchProxy.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Reflection.Emit.ILGeneration.dll b/dist/AxCopilot/System.Reflection.Emit.ILGeneration.dll deleted file mode 100644 index 68aca7b..0000000 Binary files a/dist/AxCopilot/System.Reflection.Emit.ILGeneration.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Reflection.Emit.Lightweight.dll b/dist/AxCopilot/System.Reflection.Emit.Lightweight.dll deleted file mode 100644 index 92d7c90..0000000 Binary files a/dist/AxCopilot/System.Reflection.Emit.Lightweight.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Reflection.Emit.dll b/dist/AxCopilot/System.Reflection.Emit.dll deleted file mode 100644 index 8fe5fa5..0000000 Binary files a/dist/AxCopilot/System.Reflection.Emit.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Reflection.Extensions.dll b/dist/AxCopilot/System.Reflection.Extensions.dll deleted file mode 100644 index c3a8857..0000000 Binary files a/dist/AxCopilot/System.Reflection.Extensions.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Reflection.Metadata.dll b/dist/AxCopilot/System.Reflection.Metadata.dll deleted file mode 100644 index d049735..0000000 Binary files a/dist/AxCopilot/System.Reflection.Metadata.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Reflection.Primitives.dll b/dist/AxCopilot/System.Reflection.Primitives.dll deleted file mode 100644 index 41e0870..0000000 Binary files a/dist/AxCopilot/System.Reflection.Primitives.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Reflection.TypeExtensions.dll b/dist/AxCopilot/System.Reflection.TypeExtensions.dll deleted file mode 100644 index 551b4a2..0000000 Binary files a/dist/AxCopilot/System.Reflection.TypeExtensions.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Reflection.dll b/dist/AxCopilot/System.Reflection.dll deleted file mode 100644 index c59a93c..0000000 Binary files a/dist/AxCopilot/System.Reflection.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Resources.Extensions.dll b/dist/AxCopilot/System.Resources.Extensions.dll deleted file mode 100644 index a700817..0000000 Binary files a/dist/AxCopilot/System.Resources.Extensions.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Resources.Reader.dll b/dist/AxCopilot/System.Resources.Reader.dll deleted file mode 100644 index 4d822aa..0000000 Binary files a/dist/AxCopilot/System.Resources.Reader.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Resources.ResourceManager.dll b/dist/AxCopilot/System.Resources.ResourceManager.dll deleted file mode 100644 index 4b23690..0000000 Binary files a/dist/AxCopilot/System.Resources.ResourceManager.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Resources.Writer.dll b/dist/AxCopilot/System.Resources.Writer.dll deleted file mode 100644 index c0fb9ba..0000000 Binary files a/dist/AxCopilot/System.Resources.Writer.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Runtime.CompilerServices.Unsafe.dll b/dist/AxCopilot/System.Runtime.CompilerServices.Unsafe.dll deleted file mode 100644 index ff0dc2f..0000000 Binary files a/dist/AxCopilot/System.Runtime.CompilerServices.Unsafe.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Runtime.CompilerServices.VisualC.dll b/dist/AxCopilot/System.Runtime.CompilerServices.VisualC.dll deleted file mode 100644 index d67ab9d..0000000 Binary files a/dist/AxCopilot/System.Runtime.CompilerServices.VisualC.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Runtime.Extensions.dll b/dist/AxCopilot/System.Runtime.Extensions.dll deleted file mode 100644 index 1833bfd..0000000 Binary files a/dist/AxCopilot/System.Runtime.Extensions.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Runtime.Handles.dll b/dist/AxCopilot/System.Runtime.Handles.dll deleted file mode 100644 index 6f38e17..0000000 Binary files a/dist/AxCopilot/System.Runtime.Handles.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Runtime.InteropServices.JavaScript.dll b/dist/AxCopilot/System.Runtime.InteropServices.JavaScript.dll deleted file mode 100644 index ad30f0b..0000000 Binary files a/dist/AxCopilot/System.Runtime.InteropServices.JavaScript.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Runtime.InteropServices.RuntimeInformation.dll b/dist/AxCopilot/System.Runtime.InteropServices.RuntimeInformation.dll deleted file mode 100644 index 795446c..0000000 Binary files a/dist/AxCopilot/System.Runtime.InteropServices.RuntimeInformation.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Runtime.InteropServices.dll b/dist/AxCopilot/System.Runtime.InteropServices.dll deleted file mode 100644 index 701d77e..0000000 Binary files a/dist/AxCopilot/System.Runtime.InteropServices.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Runtime.Intrinsics.dll b/dist/AxCopilot/System.Runtime.Intrinsics.dll deleted file mode 100644 index f581073..0000000 Binary files a/dist/AxCopilot/System.Runtime.Intrinsics.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Runtime.Loader.dll b/dist/AxCopilot/System.Runtime.Loader.dll deleted file mode 100644 index 087e3bd..0000000 Binary files a/dist/AxCopilot/System.Runtime.Loader.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Runtime.Numerics.dll b/dist/AxCopilot/System.Runtime.Numerics.dll deleted file mode 100644 index 2cc48fd..0000000 Binary files a/dist/AxCopilot/System.Runtime.Numerics.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Runtime.Serialization.Formatters.dll b/dist/AxCopilot/System.Runtime.Serialization.Formatters.dll deleted file mode 100644 index 602d383..0000000 Binary files a/dist/AxCopilot/System.Runtime.Serialization.Formatters.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Runtime.Serialization.Json.dll b/dist/AxCopilot/System.Runtime.Serialization.Json.dll deleted file mode 100644 index 3708322..0000000 Binary files a/dist/AxCopilot/System.Runtime.Serialization.Json.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Runtime.Serialization.Primitives.dll b/dist/AxCopilot/System.Runtime.Serialization.Primitives.dll deleted file mode 100644 index b2d6736..0000000 Binary files a/dist/AxCopilot/System.Runtime.Serialization.Primitives.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Runtime.Serialization.Xml.dll b/dist/AxCopilot/System.Runtime.Serialization.Xml.dll deleted file mode 100644 index 4ef2d71..0000000 Binary files a/dist/AxCopilot/System.Runtime.Serialization.Xml.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Runtime.Serialization.dll b/dist/AxCopilot/System.Runtime.Serialization.dll deleted file mode 100644 index 8190f35..0000000 Binary files a/dist/AxCopilot/System.Runtime.Serialization.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Runtime.dll b/dist/AxCopilot/System.Runtime.dll deleted file mode 100644 index ebc8ec5..0000000 Binary files a/dist/AxCopilot/System.Runtime.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.AccessControl.dll b/dist/AxCopilot/System.Security.AccessControl.dll deleted file mode 100644 index d7538ae..0000000 Binary files a/dist/AxCopilot/System.Security.AccessControl.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.Claims.dll b/dist/AxCopilot/System.Security.Claims.dll deleted file mode 100644 index a151774..0000000 Binary files a/dist/AxCopilot/System.Security.Claims.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.Cryptography.Algorithms.dll b/dist/AxCopilot/System.Security.Cryptography.Algorithms.dll deleted file mode 100644 index dbbbf89..0000000 Binary files a/dist/AxCopilot/System.Security.Cryptography.Algorithms.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.Cryptography.Cng.dll b/dist/AxCopilot/System.Security.Cryptography.Cng.dll deleted file mode 100644 index 22102fb..0000000 Binary files a/dist/AxCopilot/System.Security.Cryptography.Cng.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.Cryptography.Csp.dll b/dist/AxCopilot/System.Security.Cryptography.Csp.dll deleted file mode 100644 index a1df139..0000000 Binary files a/dist/AxCopilot/System.Security.Cryptography.Csp.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.Cryptography.Encoding.dll b/dist/AxCopilot/System.Security.Cryptography.Encoding.dll deleted file mode 100644 index 6dd3126..0000000 Binary files a/dist/AxCopilot/System.Security.Cryptography.Encoding.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.Cryptography.OpenSsl.dll b/dist/AxCopilot/System.Security.Cryptography.OpenSsl.dll deleted file mode 100644 index 4982518..0000000 Binary files a/dist/AxCopilot/System.Security.Cryptography.OpenSsl.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.Cryptography.Pkcs.dll b/dist/AxCopilot/System.Security.Cryptography.Pkcs.dll deleted file mode 100644 index 8a7904c..0000000 Binary files a/dist/AxCopilot/System.Security.Cryptography.Pkcs.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.Cryptography.Primitives.dll b/dist/AxCopilot/System.Security.Cryptography.Primitives.dll deleted file mode 100644 index 1460f3a..0000000 Binary files a/dist/AxCopilot/System.Security.Cryptography.Primitives.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.Cryptography.ProtectedData.dll b/dist/AxCopilot/System.Security.Cryptography.ProtectedData.dll deleted file mode 100644 index 92c8443..0000000 Binary files a/dist/AxCopilot/System.Security.Cryptography.ProtectedData.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.Cryptography.X509Certificates.dll b/dist/AxCopilot/System.Security.Cryptography.X509Certificates.dll deleted file mode 100644 index aa18299..0000000 Binary files a/dist/AxCopilot/System.Security.Cryptography.X509Certificates.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.Cryptography.Xml.dll b/dist/AxCopilot/System.Security.Cryptography.Xml.dll deleted file mode 100644 index 69e9ced..0000000 Binary files a/dist/AxCopilot/System.Security.Cryptography.Xml.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.Cryptography.dll b/dist/AxCopilot/System.Security.Cryptography.dll deleted file mode 100644 index ee20249..0000000 Binary files a/dist/AxCopilot/System.Security.Cryptography.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.Permissions.dll b/dist/AxCopilot/System.Security.Permissions.dll deleted file mode 100644 index 3a28e02..0000000 Binary files a/dist/AxCopilot/System.Security.Permissions.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.Principal.Windows.dll b/dist/AxCopilot/System.Security.Principal.Windows.dll deleted file mode 100644 index f94b887..0000000 Binary files a/dist/AxCopilot/System.Security.Principal.Windows.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.Principal.dll b/dist/AxCopilot/System.Security.Principal.dll deleted file mode 100644 index f23385d..0000000 Binary files a/dist/AxCopilot/System.Security.Principal.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.SecureString.dll b/dist/AxCopilot/System.Security.SecureString.dll deleted file mode 100644 index b63afdb..0000000 Binary files a/dist/AxCopilot/System.Security.SecureString.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Security.dll b/dist/AxCopilot/System.Security.dll deleted file mode 100644 index 1162897..0000000 Binary files a/dist/AxCopilot/System.Security.dll and /dev/null differ diff --git a/dist/AxCopilot/System.ServiceModel.Web.dll b/dist/AxCopilot/System.ServiceModel.Web.dll deleted file mode 100644 index 5f068f0..0000000 Binary files a/dist/AxCopilot/System.ServiceModel.Web.dll and /dev/null differ diff --git a/dist/AxCopilot/System.ServiceProcess.ServiceController.dll b/dist/AxCopilot/System.ServiceProcess.ServiceController.dll deleted file mode 100644 index 0d456dc..0000000 Binary files a/dist/AxCopilot/System.ServiceProcess.ServiceController.dll and /dev/null differ diff --git a/dist/AxCopilot/System.ServiceProcess.dll b/dist/AxCopilot/System.ServiceProcess.dll deleted file mode 100644 index 6cd3c48..0000000 Binary files a/dist/AxCopilot/System.ServiceProcess.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Text.Encoding.CodePages.dll b/dist/AxCopilot/System.Text.Encoding.CodePages.dll deleted file mode 100644 index dfdbae0..0000000 Binary files a/dist/AxCopilot/System.Text.Encoding.CodePages.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Text.Encoding.Extensions.dll b/dist/AxCopilot/System.Text.Encoding.Extensions.dll deleted file mode 100644 index fd38716..0000000 Binary files a/dist/AxCopilot/System.Text.Encoding.Extensions.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Text.Encoding.dll b/dist/AxCopilot/System.Text.Encoding.dll deleted file mode 100644 index 1692a09..0000000 Binary files a/dist/AxCopilot/System.Text.Encoding.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Text.Encodings.Web.dll b/dist/AxCopilot/System.Text.Encodings.Web.dll deleted file mode 100644 index c390b7a..0000000 Binary files a/dist/AxCopilot/System.Text.Encodings.Web.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Text.Json.dll b/dist/AxCopilot/System.Text.Json.dll deleted file mode 100644 index baa8bd8..0000000 Binary files a/dist/AxCopilot/System.Text.Json.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Text.RegularExpressions.dll b/dist/AxCopilot/System.Text.RegularExpressions.dll deleted file mode 100644 index 846c96a..0000000 Binary files a/dist/AxCopilot/System.Text.RegularExpressions.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Threading.AccessControl.dll b/dist/AxCopilot/System.Threading.AccessControl.dll deleted file mode 100644 index 0c4607b..0000000 Binary files a/dist/AxCopilot/System.Threading.AccessControl.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Threading.Channels.dll b/dist/AxCopilot/System.Threading.Channels.dll deleted file mode 100644 index a5cd341..0000000 Binary files a/dist/AxCopilot/System.Threading.Channels.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Threading.Overlapped.dll b/dist/AxCopilot/System.Threading.Overlapped.dll deleted file mode 100644 index 99aabcc..0000000 Binary files a/dist/AxCopilot/System.Threading.Overlapped.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Threading.Tasks.Dataflow.dll b/dist/AxCopilot/System.Threading.Tasks.Dataflow.dll deleted file mode 100644 index 4d54125..0000000 Binary files a/dist/AxCopilot/System.Threading.Tasks.Dataflow.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Threading.Tasks.Extensions.dll b/dist/AxCopilot/System.Threading.Tasks.Extensions.dll deleted file mode 100644 index 382799b..0000000 Binary files a/dist/AxCopilot/System.Threading.Tasks.Extensions.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Threading.Tasks.Parallel.dll b/dist/AxCopilot/System.Threading.Tasks.Parallel.dll deleted file mode 100644 index b5da743..0000000 Binary files a/dist/AxCopilot/System.Threading.Tasks.Parallel.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Threading.Tasks.dll b/dist/AxCopilot/System.Threading.Tasks.dll deleted file mode 100644 index af8605e..0000000 Binary files a/dist/AxCopilot/System.Threading.Tasks.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Threading.Thread.dll b/dist/AxCopilot/System.Threading.Thread.dll deleted file mode 100644 index dbff2e7..0000000 Binary files a/dist/AxCopilot/System.Threading.Thread.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Threading.ThreadPool.dll b/dist/AxCopilot/System.Threading.ThreadPool.dll deleted file mode 100644 index 7756839..0000000 Binary files a/dist/AxCopilot/System.Threading.ThreadPool.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Threading.Timer.dll b/dist/AxCopilot/System.Threading.Timer.dll deleted file mode 100644 index aa09082..0000000 Binary files a/dist/AxCopilot/System.Threading.Timer.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Threading.dll b/dist/AxCopilot/System.Threading.dll deleted file mode 100644 index 2ca4f95..0000000 Binary files a/dist/AxCopilot/System.Threading.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Transactions.Local.dll b/dist/AxCopilot/System.Transactions.Local.dll deleted file mode 100644 index 4933bda..0000000 Binary files a/dist/AxCopilot/System.Transactions.Local.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Transactions.dll b/dist/AxCopilot/System.Transactions.dll deleted file mode 100644 index aef014e..0000000 Binary files a/dist/AxCopilot/System.Transactions.dll and /dev/null differ diff --git a/dist/AxCopilot/System.ValueTuple.dll b/dist/AxCopilot/System.ValueTuple.dll deleted file mode 100644 index a8011a9..0000000 Binary files a/dist/AxCopilot/System.ValueTuple.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Web.HttpUtility.dll b/dist/AxCopilot/System.Web.HttpUtility.dll deleted file mode 100644 index 8c3a6df..0000000 Binary files a/dist/AxCopilot/System.Web.HttpUtility.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Web.dll b/dist/AxCopilot/System.Web.dll deleted file mode 100644 index ef0e4fb..0000000 Binary files a/dist/AxCopilot/System.Web.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Windows.Controls.Ribbon.dll b/dist/AxCopilot/System.Windows.Controls.Ribbon.dll deleted file mode 100644 index aeb56c1..0000000 Binary files a/dist/AxCopilot/System.Windows.Controls.Ribbon.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Windows.Extensions.dll b/dist/AxCopilot/System.Windows.Extensions.dll deleted file mode 100644 index 0dfdb84..0000000 Binary files a/dist/AxCopilot/System.Windows.Extensions.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Windows.Forms.Design.Editors.dll b/dist/AxCopilot/System.Windows.Forms.Design.Editors.dll deleted file mode 100644 index e138158..0000000 Binary files a/dist/AxCopilot/System.Windows.Forms.Design.Editors.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Windows.Forms.Design.dll b/dist/AxCopilot/System.Windows.Forms.Design.dll deleted file mode 100644 index ba52c7a..0000000 Binary files a/dist/AxCopilot/System.Windows.Forms.Design.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Windows.Forms.Primitives.dll b/dist/AxCopilot/System.Windows.Forms.Primitives.dll deleted file mode 100644 index 1dd9d98..0000000 Binary files a/dist/AxCopilot/System.Windows.Forms.Primitives.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Windows.Forms.dll b/dist/AxCopilot/System.Windows.Forms.dll deleted file mode 100644 index 55839c5..0000000 Binary files a/dist/AxCopilot/System.Windows.Forms.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Windows.Input.Manipulations.dll b/dist/AxCopilot/System.Windows.Input.Manipulations.dll deleted file mode 100644 index b6c6336..0000000 Binary files a/dist/AxCopilot/System.Windows.Input.Manipulations.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Windows.Presentation.dll b/dist/AxCopilot/System.Windows.Presentation.dll deleted file mode 100644 index a6c4b7c..0000000 Binary files a/dist/AxCopilot/System.Windows.Presentation.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Windows.dll b/dist/AxCopilot/System.Windows.dll deleted file mode 100644 index ed41971..0000000 Binary files a/dist/AxCopilot/System.Windows.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Xaml.dll b/dist/AxCopilot/System.Xaml.dll deleted file mode 100644 index b2ebc05..0000000 Binary files a/dist/AxCopilot/System.Xaml.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Xml.Linq.dll b/dist/AxCopilot/System.Xml.Linq.dll deleted file mode 100644 index f0e5359..0000000 Binary files a/dist/AxCopilot/System.Xml.Linq.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Xml.ReaderWriter.dll b/dist/AxCopilot/System.Xml.ReaderWriter.dll deleted file mode 100644 index 142a4b8..0000000 Binary files a/dist/AxCopilot/System.Xml.ReaderWriter.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Xml.Serialization.dll b/dist/AxCopilot/System.Xml.Serialization.dll deleted file mode 100644 index 03b1af8..0000000 Binary files a/dist/AxCopilot/System.Xml.Serialization.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Xml.XDocument.dll b/dist/AxCopilot/System.Xml.XDocument.dll deleted file mode 100644 index ec3ced5..0000000 Binary files a/dist/AxCopilot/System.Xml.XDocument.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Xml.XPath.XDocument.dll b/dist/AxCopilot/System.Xml.XPath.XDocument.dll deleted file mode 100644 index b289bf4..0000000 Binary files a/dist/AxCopilot/System.Xml.XPath.XDocument.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Xml.XPath.dll b/dist/AxCopilot/System.Xml.XPath.dll deleted file mode 100644 index 0a78e48..0000000 Binary files a/dist/AxCopilot/System.Xml.XPath.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Xml.XmlDocument.dll b/dist/AxCopilot/System.Xml.XmlDocument.dll deleted file mode 100644 index 1e89458..0000000 Binary files a/dist/AxCopilot/System.Xml.XmlDocument.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Xml.XmlSerializer.dll b/dist/AxCopilot/System.Xml.XmlSerializer.dll deleted file mode 100644 index 8a12e58..0000000 Binary files a/dist/AxCopilot/System.Xml.XmlSerializer.dll and /dev/null differ diff --git a/dist/AxCopilot/System.Xml.dll b/dist/AxCopilot/System.Xml.dll deleted file mode 100644 index 86de56b..0000000 Binary files a/dist/AxCopilot/System.Xml.dll and /dev/null differ diff --git a/dist/AxCopilot/System.dll b/dist/AxCopilot/System.dll deleted file mode 100644 index 5d47261..0000000 Binary files a/dist/AxCopilot/System.dll and /dev/null differ diff --git a/dist/AxCopilot/UIAutomationClient.dll b/dist/AxCopilot/UIAutomationClient.dll deleted file mode 100644 index c13f402..0000000 Binary files a/dist/AxCopilot/UIAutomationClient.dll and /dev/null differ diff --git a/dist/AxCopilot/UIAutomationClientSideProviders.dll b/dist/AxCopilot/UIAutomationClientSideProviders.dll deleted file mode 100644 index d6b93d9..0000000 Binary files a/dist/AxCopilot/UIAutomationClientSideProviders.dll and /dev/null differ diff --git a/dist/AxCopilot/UIAutomationProvider.dll b/dist/AxCopilot/UIAutomationProvider.dll deleted file mode 100644 index 2778581..0000000 Binary files a/dist/AxCopilot/UIAutomationProvider.dll and /dev/null differ diff --git a/dist/AxCopilot/UIAutomationTypes.dll b/dist/AxCopilot/UIAutomationTypes.dll deleted file mode 100644 index 4af390f..0000000 Binary files a/dist/AxCopilot/UIAutomationTypes.dll and /dev/null differ diff --git a/dist/AxCopilot/UglyToad.PdfPig.Core.dll b/dist/AxCopilot/UglyToad.PdfPig.Core.dll deleted file mode 100644 index d623133..0000000 Binary files a/dist/AxCopilot/UglyToad.PdfPig.Core.dll and /dev/null differ diff --git a/dist/AxCopilot/UglyToad.PdfPig.Fonts.dll b/dist/AxCopilot/UglyToad.PdfPig.Fonts.dll deleted file mode 100644 index 444918b..0000000 Binary files a/dist/AxCopilot/UglyToad.PdfPig.Fonts.dll and /dev/null differ diff --git a/dist/AxCopilot/UglyToad.PdfPig.Tokenization.dll b/dist/AxCopilot/UglyToad.PdfPig.Tokenization.dll deleted file mode 100644 index a9b3be9..0000000 Binary files a/dist/AxCopilot/UglyToad.PdfPig.Tokenization.dll and /dev/null differ diff --git a/dist/AxCopilot/UglyToad.PdfPig.Tokens.dll b/dist/AxCopilot/UglyToad.PdfPig.Tokens.dll deleted file mode 100644 index 7595fb6..0000000 Binary files a/dist/AxCopilot/UglyToad.PdfPig.Tokens.dll and /dev/null differ diff --git a/dist/AxCopilot/UglyToad.PdfPig.dll b/dist/AxCopilot/UglyToad.PdfPig.dll deleted file mode 100644 index 3645736..0000000 Binary files a/dist/AxCopilot/UglyToad.PdfPig.dll and /dev/null differ diff --git a/dist/AxCopilot/WebView2Loader.dll b/dist/AxCopilot/WebView2Loader.dll deleted file mode 100644 index 983ee32..0000000 Binary files a/dist/AxCopilot/WebView2Loader.dll and /dev/null differ diff --git a/dist/AxCopilot/WindowsBase.dll b/dist/AxCopilot/WindowsBase.dll deleted file mode 100644 index d45700b..0000000 Binary files a/dist/AxCopilot/WindowsBase.dll and /dev/null differ diff --git a/dist/AxCopilot/WindowsFormsIntegration.dll b/dist/AxCopilot/WindowsFormsIntegration.dll deleted file mode 100644 index b7ac5eb..0000000 Binary files a/dist/AxCopilot/WindowsFormsIntegration.dll and /dev/null differ diff --git a/dist/AxCopilot/clretwrc.dll b/dist/AxCopilot/clretwrc.dll deleted file mode 100644 index ce4bd4d..0000000 Binary files a/dist/AxCopilot/clretwrc.dll and /dev/null differ diff --git a/dist/AxCopilot/clrgc.dll b/dist/AxCopilot/clrgc.dll deleted file mode 100644 index b8ec919..0000000 Binary files a/dist/AxCopilot/clrgc.dll and /dev/null differ diff --git a/dist/AxCopilot/clrjit.dll b/dist/AxCopilot/clrjit.dll deleted file mode 100644 index 1bf755b..0000000 Binary files a/dist/AxCopilot/clrjit.dll and /dev/null differ diff --git a/dist/AxCopilot/coreclr.dll b/dist/AxCopilot/coreclr.dll deleted file mode 100644 index aaf3f0b..0000000 Binary files a/dist/AxCopilot/coreclr.dll and /dev/null differ diff --git a/dist/AxCopilot/createdump.exe b/dist/AxCopilot/createdump.exe deleted file mode 100644 index 5fc263f..0000000 Binary files a/dist/AxCopilot/createdump.exe and /dev/null differ diff --git a/dist/AxCopilot/cs/Microsoft.VisualBasic.Forms.resources.dll b/dist/AxCopilot/cs/Microsoft.VisualBasic.Forms.resources.dll deleted file mode 100644 index 62ad696..0000000 Binary files a/dist/AxCopilot/cs/Microsoft.VisualBasic.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/cs/PresentationCore.resources.dll b/dist/AxCopilot/cs/PresentationCore.resources.dll deleted file mode 100644 index 36d7a55..0000000 Binary files a/dist/AxCopilot/cs/PresentationCore.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/cs/PresentationFramework.resources.dll b/dist/AxCopilot/cs/PresentationFramework.resources.dll deleted file mode 100644 index 5519562..0000000 Binary files a/dist/AxCopilot/cs/PresentationFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/cs/PresentationUI.resources.dll b/dist/AxCopilot/cs/PresentationUI.resources.dll deleted file mode 100644 index dcd3c7b..0000000 Binary files a/dist/AxCopilot/cs/PresentationUI.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/cs/ReachFramework.resources.dll b/dist/AxCopilot/cs/ReachFramework.resources.dll deleted file mode 100644 index 7937899..0000000 Binary files a/dist/AxCopilot/cs/ReachFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/cs/System.Windows.Controls.Ribbon.resources.dll b/dist/AxCopilot/cs/System.Windows.Controls.Ribbon.resources.dll deleted file mode 100644 index 3f5adbf..0000000 Binary files a/dist/AxCopilot/cs/System.Windows.Controls.Ribbon.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/cs/System.Windows.Forms.Design.resources.dll b/dist/AxCopilot/cs/System.Windows.Forms.Design.resources.dll deleted file mode 100644 index 14cf7c6..0000000 Binary files a/dist/AxCopilot/cs/System.Windows.Forms.Design.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/cs/System.Windows.Forms.Primitives.resources.dll b/dist/AxCopilot/cs/System.Windows.Forms.Primitives.resources.dll deleted file mode 100644 index a92056b..0000000 Binary files a/dist/AxCopilot/cs/System.Windows.Forms.Primitives.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/cs/System.Windows.Forms.resources.dll b/dist/AxCopilot/cs/System.Windows.Forms.resources.dll deleted file mode 100644 index bfa9d56..0000000 Binary files a/dist/AxCopilot/cs/System.Windows.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/cs/System.Windows.Input.Manipulations.resources.dll b/dist/AxCopilot/cs/System.Windows.Input.Manipulations.resources.dll deleted file mode 100644 index a6501d1..0000000 Binary files a/dist/AxCopilot/cs/System.Windows.Input.Manipulations.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/cs/System.Xaml.resources.dll b/dist/AxCopilot/cs/System.Xaml.resources.dll deleted file mode 100644 index 67d647b..0000000 Binary files a/dist/AxCopilot/cs/System.Xaml.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/cs/UIAutomationClient.resources.dll b/dist/AxCopilot/cs/UIAutomationClient.resources.dll deleted file mode 100644 index ef83908..0000000 Binary files a/dist/AxCopilot/cs/UIAutomationClient.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/cs/UIAutomationClientSideProviders.resources.dll b/dist/AxCopilot/cs/UIAutomationClientSideProviders.resources.dll deleted file mode 100644 index 5298cf9..0000000 Binary files a/dist/AxCopilot/cs/UIAutomationClientSideProviders.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/cs/UIAutomationProvider.resources.dll b/dist/AxCopilot/cs/UIAutomationProvider.resources.dll deleted file mode 100644 index 256616a..0000000 Binary files a/dist/AxCopilot/cs/UIAutomationProvider.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/cs/UIAutomationTypes.resources.dll b/dist/AxCopilot/cs/UIAutomationTypes.resources.dll deleted file mode 100644 index 23230b4..0000000 Binary files a/dist/AxCopilot/cs/UIAutomationTypes.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/cs/WindowsBase.resources.dll b/dist/AxCopilot/cs/WindowsBase.resources.dll deleted file mode 100644 index c4ea203..0000000 Binary files a/dist/AxCopilot/cs/WindowsBase.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/cs/WindowsFormsIntegration.resources.dll b/dist/AxCopilot/cs/WindowsFormsIntegration.resources.dll deleted file mode 100644 index b69c9ea..0000000 Binary files a/dist/AxCopilot/cs/WindowsFormsIntegration.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/de/Microsoft.VisualBasic.Forms.resources.dll b/dist/AxCopilot/de/Microsoft.VisualBasic.Forms.resources.dll deleted file mode 100644 index a4b80de..0000000 Binary files a/dist/AxCopilot/de/Microsoft.VisualBasic.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/de/PresentationCore.resources.dll b/dist/AxCopilot/de/PresentationCore.resources.dll deleted file mode 100644 index c0d85b1..0000000 Binary files a/dist/AxCopilot/de/PresentationCore.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/de/PresentationFramework.resources.dll b/dist/AxCopilot/de/PresentationFramework.resources.dll deleted file mode 100644 index ec00067..0000000 Binary files a/dist/AxCopilot/de/PresentationFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/de/PresentationUI.resources.dll b/dist/AxCopilot/de/PresentationUI.resources.dll deleted file mode 100644 index 5916466..0000000 Binary files a/dist/AxCopilot/de/PresentationUI.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/de/ReachFramework.resources.dll b/dist/AxCopilot/de/ReachFramework.resources.dll deleted file mode 100644 index 58b499a..0000000 Binary files a/dist/AxCopilot/de/ReachFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/de/System.Windows.Controls.Ribbon.resources.dll b/dist/AxCopilot/de/System.Windows.Controls.Ribbon.resources.dll deleted file mode 100644 index 272392e..0000000 Binary files a/dist/AxCopilot/de/System.Windows.Controls.Ribbon.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/de/System.Windows.Forms.Design.resources.dll b/dist/AxCopilot/de/System.Windows.Forms.Design.resources.dll deleted file mode 100644 index effac2e..0000000 Binary files a/dist/AxCopilot/de/System.Windows.Forms.Design.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/de/System.Windows.Forms.Primitives.resources.dll b/dist/AxCopilot/de/System.Windows.Forms.Primitives.resources.dll deleted file mode 100644 index 6f74b1b..0000000 Binary files a/dist/AxCopilot/de/System.Windows.Forms.Primitives.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/de/System.Windows.Forms.resources.dll b/dist/AxCopilot/de/System.Windows.Forms.resources.dll deleted file mode 100644 index 5251820..0000000 Binary files a/dist/AxCopilot/de/System.Windows.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/de/System.Windows.Input.Manipulations.resources.dll b/dist/AxCopilot/de/System.Windows.Input.Manipulations.resources.dll deleted file mode 100644 index 9dad3c3..0000000 Binary files a/dist/AxCopilot/de/System.Windows.Input.Manipulations.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/de/System.Xaml.resources.dll b/dist/AxCopilot/de/System.Xaml.resources.dll deleted file mode 100644 index 9114dfb..0000000 Binary files a/dist/AxCopilot/de/System.Xaml.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/de/UIAutomationClient.resources.dll b/dist/AxCopilot/de/UIAutomationClient.resources.dll deleted file mode 100644 index 0de7e45..0000000 Binary files a/dist/AxCopilot/de/UIAutomationClient.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/de/UIAutomationClientSideProviders.resources.dll b/dist/AxCopilot/de/UIAutomationClientSideProviders.resources.dll deleted file mode 100644 index 32b7476..0000000 Binary files a/dist/AxCopilot/de/UIAutomationClientSideProviders.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/de/UIAutomationProvider.resources.dll b/dist/AxCopilot/de/UIAutomationProvider.resources.dll deleted file mode 100644 index 99b4612..0000000 Binary files a/dist/AxCopilot/de/UIAutomationProvider.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/de/UIAutomationTypes.resources.dll b/dist/AxCopilot/de/UIAutomationTypes.resources.dll deleted file mode 100644 index 7305c8d..0000000 Binary files a/dist/AxCopilot/de/UIAutomationTypes.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/de/WindowsBase.resources.dll b/dist/AxCopilot/de/WindowsBase.resources.dll deleted file mode 100644 index 59b20e0..0000000 Binary files a/dist/AxCopilot/de/WindowsBase.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/de/WindowsFormsIntegration.resources.dll b/dist/AxCopilot/de/WindowsFormsIntegration.resources.dll deleted file mode 100644 index cb90b33..0000000 Binary files a/dist/AxCopilot/de/WindowsFormsIntegration.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/e_sqlite3.dll b/dist/AxCopilot/e_sqlite3.dll deleted file mode 100644 index 379665c..0000000 Binary files a/dist/AxCopilot/e_sqlite3.dll and /dev/null differ diff --git a/dist/AxCopilot/es/Microsoft.VisualBasic.Forms.resources.dll b/dist/AxCopilot/es/Microsoft.VisualBasic.Forms.resources.dll deleted file mode 100644 index 8a3e906..0000000 Binary files a/dist/AxCopilot/es/Microsoft.VisualBasic.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/es/PresentationCore.resources.dll b/dist/AxCopilot/es/PresentationCore.resources.dll deleted file mode 100644 index c1de376..0000000 Binary files a/dist/AxCopilot/es/PresentationCore.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/es/PresentationFramework.resources.dll b/dist/AxCopilot/es/PresentationFramework.resources.dll deleted file mode 100644 index b5d422e..0000000 Binary files a/dist/AxCopilot/es/PresentationFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/es/PresentationUI.resources.dll b/dist/AxCopilot/es/PresentationUI.resources.dll deleted file mode 100644 index 7a06348..0000000 Binary files a/dist/AxCopilot/es/PresentationUI.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/es/ReachFramework.resources.dll b/dist/AxCopilot/es/ReachFramework.resources.dll deleted file mode 100644 index 2c67bd1..0000000 Binary files a/dist/AxCopilot/es/ReachFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/es/System.Windows.Controls.Ribbon.resources.dll b/dist/AxCopilot/es/System.Windows.Controls.Ribbon.resources.dll deleted file mode 100644 index 10431f2..0000000 Binary files a/dist/AxCopilot/es/System.Windows.Controls.Ribbon.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/es/System.Windows.Forms.Design.resources.dll b/dist/AxCopilot/es/System.Windows.Forms.Design.resources.dll deleted file mode 100644 index a3ebe05..0000000 Binary files a/dist/AxCopilot/es/System.Windows.Forms.Design.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/es/System.Windows.Forms.Primitives.resources.dll b/dist/AxCopilot/es/System.Windows.Forms.Primitives.resources.dll deleted file mode 100644 index 6fbf528..0000000 Binary files a/dist/AxCopilot/es/System.Windows.Forms.Primitives.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/es/System.Windows.Forms.resources.dll b/dist/AxCopilot/es/System.Windows.Forms.resources.dll deleted file mode 100644 index 491b5fc..0000000 Binary files a/dist/AxCopilot/es/System.Windows.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/es/System.Windows.Input.Manipulations.resources.dll b/dist/AxCopilot/es/System.Windows.Input.Manipulations.resources.dll deleted file mode 100644 index 13b6d11..0000000 Binary files a/dist/AxCopilot/es/System.Windows.Input.Manipulations.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/es/System.Xaml.resources.dll b/dist/AxCopilot/es/System.Xaml.resources.dll deleted file mode 100644 index ae10076..0000000 Binary files a/dist/AxCopilot/es/System.Xaml.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/es/UIAutomationClient.resources.dll b/dist/AxCopilot/es/UIAutomationClient.resources.dll deleted file mode 100644 index 723d0c8..0000000 Binary files a/dist/AxCopilot/es/UIAutomationClient.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/es/UIAutomationClientSideProviders.resources.dll b/dist/AxCopilot/es/UIAutomationClientSideProviders.resources.dll deleted file mode 100644 index fc3f43a..0000000 Binary files a/dist/AxCopilot/es/UIAutomationClientSideProviders.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/es/UIAutomationProvider.resources.dll b/dist/AxCopilot/es/UIAutomationProvider.resources.dll deleted file mode 100644 index 3df29f2..0000000 Binary files a/dist/AxCopilot/es/UIAutomationProvider.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/es/UIAutomationTypes.resources.dll b/dist/AxCopilot/es/UIAutomationTypes.resources.dll deleted file mode 100644 index b683290..0000000 Binary files a/dist/AxCopilot/es/UIAutomationTypes.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/es/WindowsBase.resources.dll b/dist/AxCopilot/es/WindowsBase.resources.dll deleted file mode 100644 index 824d435..0000000 Binary files a/dist/AxCopilot/es/WindowsBase.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/es/WindowsFormsIntegration.resources.dll b/dist/AxCopilot/es/WindowsFormsIntegration.resources.dll deleted file mode 100644 index 65620a9..0000000 Binary files a/dist/AxCopilot/es/WindowsFormsIntegration.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/fr/Microsoft.VisualBasic.Forms.resources.dll b/dist/AxCopilot/fr/Microsoft.VisualBasic.Forms.resources.dll deleted file mode 100644 index ce93226..0000000 Binary files a/dist/AxCopilot/fr/Microsoft.VisualBasic.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/fr/PresentationCore.resources.dll b/dist/AxCopilot/fr/PresentationCore.resources.dll deleted file mode 100644 index e8ee4db..0000000 Binary files a/dist/AxCopilot/fr/PresentationCore.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/fr/PresentationFramework.resources.dll b/dist/AxCopilot/fr/PresentationFramework.resources.dll deleted file mode 100644 index 2bc24e7..0000000 Binary files a/dist/AxCopilot/fr/PresentationFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/fr/PresentationUI.resources.dll b/dist/AxCopilot/fr/PresentationUI.resources.dll deleted file mode 100644 index b7fcd7a..0000000 Binary files a/dist/AxCopilot/fr/PresentationUI.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/fr/ReachFramework.resources.dll b/dist/AxCopilot/fr/ReachFramework.resources.dll deleted file mode 100644 index cf31c14..0000000 Binary files a/dist/AxCopilot/fr/ReachFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/fr/System.Windows.Controls.Ribbon.resources.dll b/dist/AxCopilot/fr/System.Windows.Controls.Ribbon.resources.dll deleted file mode 100644 index a2a3270..0000000 Binary files a/dist/AxCopilot/fr/System.Windows.Controls.Ribbon.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/fr/System.Windows.Forms.Design.resources.dll b/dist/AxCopilot/fr/System.Windows.Forms.Design.resources.dll deleted file mode 100644 index efe7f21..0000000 Binary files a/dist/AxCopilot/fr/System.Windows.Forms.Design.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/fr/System.Windows.Forms.Primitives.resources.dll b/dist/AxCopilot/fr/System.Windows.Forms.Primitives.resources.dll deleted file mode 100644 index 835dbaa..0000000 Binary files a/dist/AxCopilot/fr/System.Windows.Forms.Primitives.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/fr/System.Windows.Forms.resources.dll b/dist/AxCopilot/fr/System.Windows.Forms.resources.dll deleted file mode 100644 index 8fbf425..0000000 Binary files a/dist/AxCopilot/fr/System.Windows.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/fr/System.Windows.Input.Manipulations.resources.dll b/dist/AxCopilot/fr/System.Windows.Input.Manipulations.resources.dll deleted file mode 100644 index 709d93f..0000000 Binary files a/dist/AxCopilot/fr/System.Windows.Input.Manipulations.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/fr/System.Xaml.resources.dll b/dist/AxCopilot/fr/System.Xaml.resources.dll deleted file mode 100644 index 913fc8a..0000000 Binary files a/dist/AxCopilot/fr/System.Xaml.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/fr/UIAutomationClient.resources.dll b/dist/AxCopilot/fr/UIAutomationClient.resources.dll deleted file mode 100644 index d72cdb5..0000000 Binary files a/dist/AxCopilot/fr/UIAutomationClient.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/fr/UIAutomationClientSideProviders.resources.dll b/dist/AxCopilot/fr/UIAutomationClientSideProviders.resources.dll deleted file mode 100644 index 5e179c2..0000000 Binary files a/dist/AxCopilot/fr/UIAutomationClientSideProviders.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/fr/UIAutomationProvider.resources.dll b/dist/AxCopilot/fr/UIAutomationProvider.resources.dll deleted file mode 100644 index f311bb9..0000000 Binary files a/dist/AxCopilot/fr/UIAutomationProvider.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/fr/UIAutomationTypes.resources.dll b/dist/AxCopilot/fr/UIAutomationTypes.resources.dll deleted file mode 100644 index 0ff6a7e..0000000 Binary files a/dist/AxCopilot/fr/UIAutomationTypes.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/fr/WindowsBase.resources.dll b/dist/AxCopilot/fr/WindowsBase.resources.dll deleted file mode 100644 index a775e1d..0000000 Binary files a/dist/AxCopilot/fr/WindowsBase.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/fr/WindowsFormsIntegration.resources.dll b/dist/AxCopilot/fr/WindowsFormsIntegration.resources.dll deleted file mode 100644 index 3f9c44f..0000000 Binary files a/dist/AxCopilot/fr/WindowsFormsIntegration.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/hostfxr.dll b/dist/AxCopilot/hostfxr.dll deleted file mode 100644 index b68e7ea..0000000 Binary files a/dist/AxCopilot/hostfxr.dll and /dev/null differ diff --git a/dist/AxCopilot/hostpolicy.dll b/dist/AxCopilot/hostpolicy.dll deleted file mode 100644 index 2b6e13f..0000000 Binary files a/dist/AxCopilot/hostpolicy.dll and /dev/null differ diff --git a/dist/AxCopilot/it/Microsoft.VisualBasic.Forms.resources.dll b/dist/AxCopilot/it/Microsoft.VisualBasic.Forms.resources.dll deleted file mode 100644 index 9853aeb..0000000 Binary files a/dist/AxCopilot/it/Microsoft.VisualBasic.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/it/PresentationCore.resources.dll b/dist/AxCopilot/it/PresentationCore.resources.dll deleted file mode 100644 index 3cccddf..0000000 Binary files a/dist/AxCopilot/it/PresentationCore.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/it/PresentationFramework.resources.dll b/dist/AxCopilot/it/PresentationFramework.resources.dll deleted file mode 100644 index 066b32e..0000000 Binary files a/dist/AxCopilot/it/PresentationFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/it/PresentationUI.resources.dll b/dist/AxCopilot/it/PresentationUI.resources.dll deleted file mode 100644 index dab8111..0000000 Binary files a/dist/AxCopilot/it/PresentationUI.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/it/ReachFramework.resources.dll b/dist/AxCopilot/it/ReachFramework.resources.dll deleted file mode 100644 index 7d6a1bf..0000000 Binary files a/dist/AxCopilot/it/ReachFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/it/System.Windows.Controls.Ribbon.resources.dll b/dist/AxCopilot/it/System.Windows.Controls.Ribbon.resources.dll deleted file mode 100644 index 8faac17..0000000 Binary files a/dist/AxCopilot/it/System.Windows.Controls.Ribbon.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/it/System.Windows.Forms.Design.resources.dll b/dist/AxCopilot/it/System.Windows.Forms.Design.resources.dll deleted file mode 100644 index 59518ef..0000000 Binary files a/dist/AxCopilot/it/System.Windows.Forms.Design.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/it/System.Windows.Forms.Primitives.resources.dll b/dist/AxCopilot/it/System.Windows.Forms.Primitives.resources.dll deleted file mode 100644 index e120beb..0000000 Binary files a/dist/AxCopilot/it/System.Windows.Forms.Primitives.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/it/System.Windows.Forms.resources.dll b/dist/AxCopilot/it/System.Windows.Forms.resources.dll deleted file mode 100644 index 5012226..0000000 Binary files a/dist/AxCopilot/it/System.Windows.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/it/System.Windows.Input.Manipulations.resources.dll b/dist/AxCopilot/it/System.Windows.Input.Manipulations.resources.dll deleted file mode 100644 index 247330a..0000000 Binary files a/dist/AxCopilot/it/System.Windows.Input.Manipulations.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/it/System.Xaml.resources.dll b/dist/AxCopilot/it/System.Xaml.resources.dll deleted file mode 100644 index 1ec3ac1..0000000 Binary files a/dist/AxCopilot/it/System.Xaml.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/it/UIAutomationClient.resources.dll b/dist/AxCopilot/it/UIAutomationClient.resources.dll deleted file mode 100644 index 8804c89..0000000 Binary files a/dist/AxCopilot/it/UIAutomationClient.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/it/UIAutomationClientSideProviders.resources.dll b/dist/AxCopilot/it/UIAutomationClientSideProviders.resources.dll deleted file mode 100644 index a03cf91..0000000 Binary files a/dist/AxCopilot/it/UIAutomationClientSideProviders.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/it/UIAutomationProvider.resources.dll b/dist/AxCopilot/it/UIAutomationProvider.resources.dll deleted file mode 100644 index 91f2d26..0000000 Binary files a/dist/AxCopilot/it/UIAutomationProvider.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/it/UIAutomationTypes.resources.dll b/dist/AxCopilot/it/UIAutomationTypes.resources.dll deleted file mode 100644 index 71d18cc..0000000 Binary files a/dist/AxCopilot/it/UIAutomationTypes.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/it/WindowsBase.resources.dll b/dist/AxCopilot/it/WindowsBase.resources.dll deleted file mode 100644 index 34d138b..0000000 Binary files a/dist/AxCopilot/it/WindowsBase.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/it/WindowsFormsIntegration.resources.dll b/dist/AxCopilot/it/WindowsFormsIntegration.resources.dll deleted file mode 100644 index 841f6ce..0000000 Binary files a/dist/AxCopilot/it/WindowsFormsIntegration.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ja/Microsoft.VisualBasic.Forms.resources.dll b/dist/AxCopilot/ja/Microsoft.VisualBasic.Forms.resources.dll deleted file mode 100644 index 65b7dd6..0000000 Binary files a/dist/AxCopilot/ja/Microsoft.VisualBasic.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ja/PresentationCore.resources.dll b/dist/AxCopilot/ja/PresentationCore.resources.dll deleted file mode 100644 index d1d2393..0000000 Binary files a/dist/AxCopilot/ja/PresentationCore.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ja/PresentationFramework.resources.dll b/dist/AxCopilot/ja/PresentationFramework.resources.dll deleted file mode 100644 index 5753bcd..0000000 Binary files a/dist/AxCopilot/ja/PresentationFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ja/PresentationUI.resources.dll b/dist/AxCopilot/ja/PresentationUI.resources.dll deleted file mode 100644 index 369617e..0000000 Binary files a/dist/AxCopilot/ja/PresentationUI.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ja/ReachFramework.resources.dll b/dist/AxCopilot/ja/ReachFramework.resources.dll deleted file mode 100644 index 5d85611..0000000 Binary files a/dist/AxCopilot/ja/ReachFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ja/System.Windows.Controls.Ribbon.resources.dll b/dist/AxCopilot/ja/System.Windows.Controls.Ribbon.resources.dll deleted file mode 100644 index 456dc4a..0000000 Binary files a/dist/AxCopilot/ja/System.Windows.Controls.Ribbon.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ja/System.Windows.Forms.Design.resources.dll b/dist/AxCopilot/ja/System.Windows.Forms.Design.resources.dll deleted file mode 100644 index d49fab4..0000000 Binary files a/dist/AxCopilot/ja/System.Windows.Forms.Design.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ja/System.Windows.Forms.Primitives.resources.dll b/dist/AxCopilot/ja/System.Windows.Forms.Primitives.resources.dll deleted file mode 100644 index 179ce29..0000000 Binary files a/dist/AxCopilot/ja/System.Windows.Forms.Primitives.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ja/System.Windows.Forms.resources.dll b/dist/AxCopilot/ja/System.Windows.Forms.resources.dll deleted file mode 100644 index 1c7239b..0000000 Binary files a/dist/AxCopilot/ja/System.Windows.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ja/System.Windows.Input.Manipulations.resources.dll b/dist/AxCopilot/ja/System.Windows.Input.Manipulations.resources.dll deleted file mode 100644 index 448b23a..0000000 Binary files a/dist/AxCopilot/ja/System.Windows.Input.Manipulations.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ja/System.Xaml.resources.dll b/dist/AxCopilot/ja/System.Xaml.resources.dll deleted file mode 100644 index 9332199..0000000 Binary files a/dist/AxCopilot/ja/System.Xaml.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ja/UIAutomationClient.resources.dll b/dist/AxCopilot/ja/UIAutomationClient.resources.dll deleted file mode 100644 index a68085c..0000000 Binary files a/dist/AxCopilot/ja/UIAutomationClient.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ja/UIAutomationClientSideProviders.resources.dll b/dist/AxCopilot/ja/UIAutomationClientSideProviders.resources.dll deleted file mode 100644 index e5f78eb..0000000 Binary files a/dist/AxCopilot/ja/UIAutomationClientSideProviders.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ja/UIAutomationProvider.resources.dll b/dist/AxCopilot/ja/UIAutomationProvider.resources.dll deleted file mode 100644 index 9ff7b8f..0000000 Binary files a/dist/AxCopilot/ja/UIAutomationProvider.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ja/UIAutomationTypes.resources.dll b/dist/AxCopilot/ja/UIAutomationTypes.resources.dll deleted file mode 100644 index b1cdfe2..0000000 Binary files a/dist/AxCopilot/ja/UIAutomationTypes.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ja/WindowsBase.resources.dll b/dist/AxCopilot/ja/WindowsBase.resources.dll deleted file mode 100644 index ec6c503..0000000 Binary files a/dist/AxCopilot/ja/WindowsBase.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ja/WindowsFormsIntegration.resources.dll b/dist/AxCopilot/ja/WindowsFormsIntegration.resources.dll deleted file mode 100644 index 18aa0e3..0000000 Binary files a/dist/AxCopilot/ja/WindowsFormsIntegration.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ko/Microsoft.VisualBasic.Forms.resources.dll b/dist/AxCopilot/ko/Microsoft.VisualBasic.Forms.resources.dll deleted file mode 100644 index de853c1..0000000 Binary files a/dist/AxCopilot/ko/Microsoft.VisualBasic.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ko/PresentationCore.resources.dll b/dist/AxCopilot/ko/PresentationCore.resources.dll deleted file mode 100644 index 1754cbc..0000000 Binary files a/dist/AxCopilot/ko/PresentationCore.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ko/PresentationFramework.resources.dll b/dist/AxCopilot/ko/PresentationFramework.resources.dll deleted file mode 100644 index 0f0b748..0000000 Binary files a/dist/AxCopilot/ko/PresentationFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ko/PresentationUI.resources.dll b/dist/AxCopilot/ko/PresentationUI.resources.dll deleted file mode 100644 index 5a2d16f..0000000 Binary files a/dist/AxCopilot/ko/PresentationUI.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ko/ReachFramework.resources.dll b/dist/AxCopilot/ko/ReachFramework.resources.dll deleted file mode 100644 index c27c6a0..0000000 Binary files a/dist/AxCopilot/ko/ReachFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ko/System.Windows.Controls.Ribbon.resources.dll b/dist/AxCopilot/ko/System.Windows.Controls.Ribbon.resources.dll deleted file mode 100644 index e8ce016..0000000 Binary files a/dist/AxCopilot/ko/System.Windows.Controls.Ribbon.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ko/System.Windows.Forms.Design.resources.dll b/dist/AxCopilot/ko/System.Windows.Forms.Design.resources.dll deleted file mode 100644 index 0b3da3e..0000000 Binary files a/dist/AxCopilot/ko/System.Windows.Forms.Design.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ko/System.Windows.Forms.Primitives.resources.dll b/dist/AxCopilot/ko/System.Windows.Forms.Primitives.resources.dll deleted file mode 100644 index 923aae5..0000000 Binary files a/dist/AxCopilot/ko/System.Windows.Forms.Primitives.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ko/System.Windows.Forms.resources.dll b/dist/AxCopilot/ko/System.Windows.Forms.resources.dll deleted file mode 100644 index b4f036c..0000000 Binary files a/dist/AxCopilot/ko/System.Windows.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ko/System.Windows.Input.Manipulations.resources.dll b/dist/AxCopilot/ko/System.Windows.Input.Manipulations.resources.dll deleted file mode 100644 index 9b90f21..0000000 Binary files a/dist/AxCopilot/ko/System.Windows.Input.Manipulations.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ko/System.Xaml.resources.dll b/dist/AxCopilot/ko/System.Xaml.resources.dll deleted file mode 100644 index c735c85..0000000 Binary files a/dist/AxCopilot/ko/System.Xaml.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ko/UIAutomationClient.resources.dll b/dist/AxCopilot/ko/UIAutomationClient.resources.dll deleted file mode 100644 index 78a7675..0000000 Binary files a/dist/AxCopilot/ko/UIAutomationClient.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ko/UIAutomationClientSideProviders.resources.dll b/dist/AxCopilot/ko/UIAutomationClientSideProviders.resources.dll deleted file mode 100644 index fea66ed..0000000 Binary files a/dist/AxCopilot/ko/UIAutomationClientSideProviders.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ko/UIAutomationProvider.resources.dll b/dist/AxCopilot/ko/UIAutomationProvider.resources.dll deleted file mode 100644 index 583dee8..0000000 Binary files a/dist/AxCopilot/ko/UIAutomationProvider.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ko/UIAutomationTypes.resources.dll b/dist/AxCopilot/ko/UIAutomationTypes.resources.dll deleted file mode 100644 index 336f779..0000000 Binary files a/dist/AxCopilot/ko/UIAutomationTypes.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ko/WindowsBase.resources.dll b/dist/AxCopilot/ko/WindowsBase.resources.dll deleted file mode 100644 index 39bb4a1..0000000 Binary files a/dist/AxCopilot/ko/WindowsBase.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ko/WindowsFormsIntegration.resources.dll b/dist/AxCopilot/ko/WindowsFormsIntegration.resources.dll deleted file mode 100644 index ad5b588..0000000 Binary files a/dist/AxCopilot/ko/WindowsFormsIntegration.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/mscordaccore.dll b/dist/AxCopilot/mscordaccore.dll deleted file mode 100644 index 03955f5..0000000 Binary files a/dist/AxCopilot/mscordaccore.dll and /dev/null differ diff --git a/dist/AxCopilot/mscordaccore_amd64_amd64_8.0.2526.11203.dll b/dist/AxCopilot/mscordaccore_amd64_amd64_8.0.2526.11203.dll deleted file mode 100644 index 03955f5..0000000 Binary files a/dist/AxCopilot/mscordaccore_amd64_amd64_8.0.2526.11203.dll and /dev/null differ diff --git a/dist/AxCopilot/mscordbi.dll b/dist/AxCopilot/mscordbi.dll deleted file mode 100644 index f133f67..0000000 Binary files a/dist/AxCopilot/mscordbi.dll and /dev/null differ diff --git a/dist/AxCopilot/mscorlib.dll b/dist/AxCopilot/mscorlib.dll deleted file mode 100644 index 27463e0..0000000 Binary files a/dist/AxCopilot/mscorlib.dll and /dev/null differ diff --git a/dist/AxCopilot/mscorrc.dll b/dist/AxCopilot/mscorrc.dll deleted file mode 100644 index 1e092f0..0000000 Binary files a/dist/AxCopilot/mscorrc.dll and /dev/null differ diff --git a/dist/AxCopilot/msquic.dll b/dist/AxCopilot/msquic.dll deleted file mode 100644 index 9fd10ab..0000000 Binary files a/dist/AxCopilot/msquic.dll and /dev/null differ diff --git a/dist/AxCopilot/netstandard.dll b/dist/AxCopilot/netstandard.dll deleted file mode 100644 index e54daa6..0000000 Binary files a/dist/AxCopilot/netstandard.dll and /dev/null differ diff --git a/dist/AxCopilot/pl/Microsoft.VisualBasic.Forms.resources.dll b/dist/AxCopilot/pl/Microsoft.VisualBasic.Forms.resources.dll deleted file mode 100644 index b8c9e4b..0000000 Binary files a/dist/AxCopilot/pl/Microsoft.VisualBasic.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pl/PresentationCore.resources.dll b/dist/AxCopilot/pl/PresentationCore.resources.dll deleted file mode 100644 index 41b125e..0000000 Binary files a/dist/AxCopilot/pl/PresentationCore.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pl/PresentationFramework.resources.dll b/dist/AxCopilot/pl/PresentationFramework.resources.dll deleted file mode 100644 index d5f2f0e..0000000 Binary files a/dist/AxCopilot/pl/PresentationFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pl/PresentationUI.resources.dll b/dist/AxCopilot/pl/PresentationUI.resources.dll deleted file mode 100644 index 85d7a46..0000000 Binary files a/dist/AxCopilot/pl/PresentationUI.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pl/ReachFramework.resources.dll b/dist/AxCopilot/pl/ReachFramework.resources.dll deleted file mode 100644 index 34d4fe1..0000000 Binary files a/dist/AxCopilot/pl/ReachFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pl/System.Windows.Controls.Ribbon.resources.dll b/dist/AxCopilot/pl/System.Windows.Controls.Ribbon.resources.dll deleted file mode 100644 index f90e8f6..0000000 Binary files a/dist/AxCopilot/pl/System.Windows.Controls.Ribbon.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pl/System.Windows.Forms.Design.resources.dll b/dist/AxCopilot/pl/System.Windows.Forms.Design.resources.dll deleted file mode 100644 index 66ed2ce..0000000 Binary files a/dist/AxCopilot/pl/System.Windows.Forms.Design.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pl/System.Windows.Forms.Primitives.resources.dll b/dist/AxCopilot/pl/System.Windows.Forms.Primitives.resources.dll deleted file mode 100644 index 881f20f..0000000 Binary files a/dist/AxCopilot/pl/System.Windows.Forms.Primitives.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pl/System.Windows.Forms.resources.dll b/dist/AxCopilot/pl/System.Windows.Forms.resources.dll deleted file mode 100644 index 37d911d..0000000 Binary files a/dist/AxCopilot/pl/System.Windows.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pl/System.Windows.Input.Manipulations.resources.dll b/dist/AxCopilot/pl/System.Windows.Input.Manipulations.resources.dll deleted file mode 100644 index 9ef5fd3..0000000 Binary files a/dist/AxCopilot/pl/System.Windows.Input.Manipulations.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pl/System.Xaml.resources.dll b/dist/AxCopilot/pl/System.Xaml.resources.dll deleted file mode 100644 index 6c22361..0000000 Binary files a/dist/AxCopilot/pl/System.Xaml.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pl/UIAutomationClient.resources.dll b/dist/AxCopilot/pl/UIAutomationClient.resources.dll deleted file mode 100644 index 2359952..0000000 Binary files a/dist/AxCopilot/pl/UIAutomationClient.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pl/UIAutomationClientSideProviders.resources.dll b/dist/AxCopilot/pl/UIAutomationClientSideProviders.resources.dll deleted file mode 100644 index 0d4875f..0000000 Binary files a/dist/AxCopilot/pl/UIAutomationClientSideProviders.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pl/UIAutomationProvider.resources.dll b/dist/AxCopilot/pl/UIAutomationProvider.resources.dll deleted file mode 100644 index 5e569f2..0000000 Binary files a/dist/AxCopilot/pl/UIAutomationProvider.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pl/UIAutomationTypes.resources.dll b/dist/AxCopilot/pl/UIAutomationTypes.resources.dll deleted file mode 100644 index e848dc6..0000000 Binary files a/dist/AxCopilot/pl/UIAutomationTypes.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pl/WindowsBase.resources.dll b/dist/AxCopilot/pl/WindowsBase.resources.dll deleted file mode 100644 index a50f7c3..0000000 Binary files a/dist/AxCopilot/pl/WindowsBase.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pl/WindowsFormsIntegration.resources.dll b/dist/AxCopilot/pl/WindowsFormsIntegration.resources.dll deleted file mode 100644 index 52c5ac7..0000000 Binary files a/dist/AxCopilot/pl/WindowsFormsIntegration.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pt-BR/Microsoft.VisualBasic.Forms.resources.dll b/dist/AxCopilot/pt-BR/Microsoft.VisualBasic.Forms.resources.dll deleted file mode 100644 index fe4535f..0000000 Binary files a/dist/AxCopilot/pt-BR/Microsoft.VisualBasic.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pt-BR/PresentationCore.resources.dll b/dist/AxCopilot/pt-BR/PresentationCore.resources.dll deleted file mode 100644 index 53df88f..0000000 Binary files a/dist/AxCopilot/pt-BR/PresentationCore.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pt-BR/PresentationFramework.resources.dll b/dist/AxCopilot/pt-BR/PresentationFramework.resources.dll deleted file mode 100644 index 00fa7e7..0000000 Binary files a/dist/AxCopilot/pt-BR/PresentationFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pt-BR/PresentationUI.resources.dll b/dist/AxCopilot/pt-BR/PresentationUI.resources.dll deleted file mode 100644 index 7525f77..0000000 Binary files a/dist/AxCopilot/pt-BR/PresentationUI.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pt-BR/ReachFramework.resources.dll b/dist/AxCopilot/pt-BR/ReachFramework.resources.dll deleted file mode 100644 index 4100eac..0000000 Binary files a/dist/AxCopilot/pt-BR/ReachFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pt-BR/System.Windows.Controls.Ribbon.resources.dll b/dist/AxCopilot/pt-BR/System.Windows.Controls.Ribbon.resources.dll deleted file mode 100644 index 788b9a6..0000000 Binary files a/dist/AxCopilot/pt-BR/System.Windows.Controls.Ribbon.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pt-BR/System.Windows.Forms.Design.resources.dll b/dist/AxCopilot/pt-BR/System.Windows.Forms.Design.resources.dll deleted file mode 100644 index 0d06600..0000000 Binary files a/dist/AxCopilot/pt-BR/System.Windows.Forms.Design.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pt-BR/System.Windows.Forms.Primitives.resources.dll b/dist/AxCopilot/pt-BR/System.Windows.Forms.Primitives.resources.dll deleted file mode 100644 index fd6287d..0000000 Binary files a/dist/AxCopilot/pt-BR/System.Windows.Forms.Primitives.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pt-BR/System.Windows.Forms.resources.dll b/dist/AxCopilot/pt-BR/System.Windows.Forms.resources.dll deleted file mode 100644 index 73af266..0000000 Binary files a/dist/AxCopilot/pt-BR/System.Windows.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pt-BR/System.Windows.Input.Manipulations.resources.dll b/dist/AxCopilot/pt-BR/System.Windows.Input.Manipulations.resources.dll deleted file mode 100644 index f533ed3..0000000 Binary files a/dist/AxCopilot/pt-BR/System.Windows.Input.Manipulations.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pt-BR/System.Xaml.resources.dll b/dist/AxCopilot/pt-BR/System.Xaml.resources.dll deleted file mode 100644 index 31d4eef..0000000 Binary files a/dist/AxCopilot/pt-BR/System.Xaml.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pt-BR/UIAutomationClient.resources.dll b/dist/AxCopilot/pt-BR/UIAutomationClient.resources.dll deleted file mode 100644 index 1220868..0000000 Binary files a/dist/AxCopilot/pt-BR/UIAutomationClient.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pt-BR/UIAutomationClientSideProviders.resources.dll b/dist/AxCopilot/pt-BR/UIAutomationClientSideProviders.resources.dll deleted file mode 100644 index 55d4ff8..0000000 Binary files a/dist/AxCopilot/pt-BR/UIAutomationClientSideProviders.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pt-BR/UIAutomationProvider.resources.dll b/dist/AxCopilot/pt-BR/UIAutomationProvider.resources.dll deleted file mode 100644 index 0bcc724..0000000 Binary files a/dist/AxCopilot/pt-BR/UIAutomationProvider.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pt-BR/UIAutomationTypes.resources.dll b/dist/AxCopilot/pt-BR/UIAutomationTypes.resources.dll deleted file mode 100644 index 078d705..0000000 Binary files a/dist/AxCopilot/pt-BR/UIAutomationTypes.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pt-BR/WindowsBase.resources.dll b/dist/AxCopilot/pt-BR/WindowsBase.resources.dll deleted file mode 100644 index 6ccbc56..0000000 Binary files a/dist/AxCopilot/pt-BR/WindowsBase.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/pt-BR/WindowsFormsIntegration.resources.dll b/dist/AxCopilot/pt-BR/WindowsFormsIntegration.resources.dll deleted file mode 100644 index 377e3bf..0000000 Binary files a/dist/AxCopilot/pt-BR/WindowsFormsIntegration.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ru/Microsoft.VisualBasic.Forms.resources.dll b/dist/AxCopilot/ru/Microsoft.VisualBasic.Forms.resources.dll deleted file mode 100644 index 4942a60..0000000 Binary files a/dist/AxCopilot/ru/Microsoft.VisualBasic.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ru/PresentationCore.resources.dll b/dist/AxCopilot/ru/PresentationCore.resources.dll deleted file mode 100644 index 1cbb847..0000000 Binary files a/dist/AxCopilot/ru/PresentationCore.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ru/PresentationFramework.resources.dll b/dist/AxCopilot/ru/PresentationFramework.resources.dll deleted file mode 100644 index a8a62c2..0000000 Binary files a/dist/AxCopilot/ru/PresentationFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ru/PresentationUI.resources.dll b/dist/AxCopilot/ru/PresentationUI.resources.dll deleted file mode 100644 index 65218a2..0000000 Binary files a/dist/AxCopilot/ru/PresentationUI.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ru/ReachFramework.resources.dll b/dist/AxCopilot/ru/ReachFramework.resources.dll deleted file mode 100644 index 94588a8..0000000 Binary files a/dist/AxCopilot/ru/ReachFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ru/System.Windows.Controls.Ribbon.resources.dll b/dist/AxCopilot/ru/System.Windows.Controls.Ribbon.resources.dll deleted file mode 100644 index 36656a9..0000000 Binary files a/dist/AxCopilot/ru/System.Windows.Controls.Ribbon.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ru/System.Windows.Forms.Design.resources.dll b/dist/AxCopilot/ru/System.Windows.Forms.Design.resources.dll deleted file mode 100644 index feebd5d..0000000 Binary files a/dist/AxCopilot/ru/System.Windows.Forms.Design.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ru/System.Windows.Forms.Primitives.resources.dll b/dist/AxCopilot/ru/System.Windows.Forms.Primitives.resources.dll deleted file mode 100644 index 46823f9..0000000 Binary files a/dist/AxCopilot/ru/System.Windows.Forms.Primitives.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ru/System.Windows.Forms.resources.dll b/dist/AxCopilot/ru/System.Windows.Forms.resources.dll deleted file mode 100644 index ee70ba5..0000000 Binary files a/dist/AxCopilot/ru/System.Windows.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ru/System.Windows.Input.Manipulations.resources.dll b/dist/AxCopilot/ru/System.Windows.Input.Manipulations.resources.dll deleted file mode 100644 index ef8ff1a..0000000 Binary files a/dist/AxCopilot/ru/System.Windows.Input.Manipulations.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ru/System.Xaml.resources.dll b/dist/AxCopilot/ru/System.Xaml.resources.dll deleted file mode 100644 index 9591c99..0000000 Binary files a/dist/AxCopilot/ru/System.Xaml.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ru/UIAutomationClient.resources.dll b/dist/AxCopilot/ru/UIAutomationClient.resources.dll deleted file mode 100644 index 26073b0..0000000 Binary files a/dist/AxCopilot/ru/UIAutomationClient.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ru/UIAutomationClientSideProviders.resources.dll b/dist/AxCopilot/ru/UIAutomationClientSideProviders.resources.dll deleted file mode 100644 index 54884d2..0000000 Binary files a/dist/AxCopilot/ru/UIAutomationClientSideProviders.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ru/UIAutomationProvider.resources.dll b/dist/AxCopilot/ru/UIAutomationProvider.resources.dll deleted file mode 100644 index 9c39566..0000000 Binary files a/dist/AxCopilot/ru/UIAutomationProvider.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ru/UIAutomationTypes.resources.dll b/dist/AxCopilot/ru/UIAutomationTypes.resources.dll deleted file mode 100644 index 3baf0a3..0000000 Binary files a/dist/AxCopilot/ru/UIAutomationTypes.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ru/WindowsBase.resources.dll b/dist/AxCopilot/ru/WindowsBase.resources.dll deleted file mode 100644 index d58e699..0000000 Binary files a/dist/AxCopilot/ru/WindowsBase.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/ru/WindowsFormsIntegration.resources.dll b/dist/AxCopilot/ru/WindowsFormsIntegration.resources.dll deleted file mode 100644 index 3cb5e5b..0000000 Binary files a/dist/AxCopilot/ru/WindowsFormsIntegration.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/runtimes/win-x64/native/WebView2Loader.dll b/dist/AxCopilot/runtimes/win-x64/native/WebView2Loader.dll deleted file mode 100644 index 983ee32..0000000 Binary files a/dist/AxCopilot/runtimes/win-x64/native/WebView2Loader.dll and /dev/null differ diff --git a/dist/AxCopilot/tr/Microsoft.VisualBasic.Forms.resources.dll b/dist/AxCopilot/tr/Microsoft.VisualBasic.Forms.resources.dll deleted file mode 100644 index 8eea594..0000000 Binary files a/dist/AxCopilot/tr/Microsoft.VisualBasic.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/tr/PresentationCore.resources.dll b/dist/AxCopilot/tr/PresentationCore.resources.dll deleted file mode 100644 index 8bc24e8..0000000 Binary files a/dist/AxCopilot/tr/PresentationCore.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/tr/PresentationFramework.resources.dll b/dist/AxCopilot/tr/PresentationFramework.resources.dll deleted file mode 100644 index b8bdeae..0000000 Binary files a/dist/AxCopilot/tr/PresentationFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/tr/PresentationUI.resources.dll b/dist/AxCopilot/tr/PresentationUI.resources.dll deleted file mode 100644 index 113dcbb..0000000 Binary files a/dist/AxCopilot/tr/PresentationUI.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/tr/ReachFramework.resources.dll b/dist/AxCopilot/tr/ReachFramework.resources.dll deleted file mode 100644 index b873815..0000000 Binary files a/dist/AxCopilot/tr/ReachFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/tr/System.Windows.Controls.Ribbon.resources.dll b/dist/AxCopilot/tr/System.Windows.Controls.Ribbon.resources.dll deleted file mode 100644 index df07d56..0000000 Binary files a/dist/AxCopilot/tr/System.Windows.Controls.Ribbon.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/tr/System.Windows.Forms.Design.resources.dll b/dist/AxCopilot/tr/System.Windows.Forms.Design.resources.dll deleted file mode 100644 index b823ba1..0000000 Binary files a/dist/AxCopilot/tr/System.Windows.Forms.Design.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/tr/System.Windows.Forms.Primitives.resources.dll b/dist/AxCopilot/tr/System.Windows.Forms.Primitives.resources.dll deleted file mode 100644 index 19163ce..0000000 Binary files a/dist/AxCopilot/tr/System.Windows.Forms.Primitives.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/tr/System.Windows.Forms.resources.dll b/dist/AxCopilot/tr/System.Windows.Forms.resources.dll deleted file mode 100644 index cc1ab6f..0000000 Binary files a/dist/AxCopilot/tr/System.Windows.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/tr/System.Windows.Input.Manipulations.resources.dll b/dist/AxCopilot/tr/System.Windows.Input.Manipulations.resources.dll deleted file mode 100644 index 8aa0811..0000000 Binary files a/dist/AxCopilot/tr/System.Windows.Input.Manipulations.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/tr/System.Xaml.resources.dll b/dist/AxCopilot/tr/System.Xaml.resources.dll deleted file mode 100644 index f12c281..0000000 Binary files a/dist/AxCopilot/tr/System.Xaml.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/tr/UIAutomationClient.resources.dll b/dist/AxCopilot/tr/UIAutomationClient.resources.dll deleted file mode 100644 index 1aa3bd5..0000000 Binary files a/dist/AxCopilot/tr/UIAutomationClient.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/tr/UIAutomationClientSideProviders.resources.dll b/dist/AxCopilot/tr/UIAutomationClientSideProviders.resources.dll deleted file mode 100644 index b4703ef..0000000 Binary files a/dist/AxCopilot/tr/UIAutomationClientSideProviders.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/tr/UIAutomationProvider.resources.dll b/dist/AxCopilot/tr/UIAutomationProvider.resources.dll deleted file mode 100644 index e91b7a2..0000000 Binary files a/dist/AxCopilot/tr/UIAutomationProvider.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/tr/UIAutomationTypes.resources.dll b/dist/AxCopilot/tr/UIAutomationTypes.resources.dll deleted file mode 100644 index ce81902..0000000 Binary files a/dist/AxCopilot/tr/UIAutomationTypes.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/tr/WindowsBase.resources.dll b/dist/AxCopilot/tr/WindowsBase.resources.dll deleted file mode 100644 index f0d9524..0000000 Binary files a/dist/AxCopilot/tr/WindowsBase.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/tr/WindowsFormsIntegration.resources.dll b/dist/AxCopilot/tr/WindowsFormsIntegration.resources.dll deleted file mode 100644 index 68a247a..0000000 Binary files a/dist/AxCopilot/tr/WindowsFormsIntegration.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/vcruntime140_cor3.dll b/dist/AxCopilot/vcruntime140_cor3.dll deleted file mode 100644 index 5786e93..0000000 Binary files a/dist/AxCopilot/vcruntime140_cor3.dll and /dev/null differ diff --git a/dist/AxCopilot/wpfgfx_cor3.dll b/dist/AxCopilot/wpfgfx_cor3.dll deleted file mode 100644 index aa7d287..0000000 Binary files a/dist/AxCopilot/wpfgfx_cor3.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hans/Microsoft.VisualBasic.Forms.resources.dll b/dist/AxCopilot/zh-Hans/Microsoft.VisualBasic.Forms.resources.dll deleted file mode 100644 index 141280d..0000000 Binary files a/dist/AxCopilot/zh-Hans/Microsoft.VisualBasic.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hans/PresentationCore.resources.dll b/dist/AxCopilot/zh-Hans/PresentationCore.resources.dll deleted file mode 100644 index 78f3317..0000000 Binary files a/dist/AxCopilot/zh-Hans/PresentationCore.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hans/PresentationFramework.resources.dll b/dist/AxCopilot/zh-Hans/PresentationFramework.resources.dll deleted file mode 100644 index 3e3c601..0000000 Binary files a/dist/AxCopilot/zh-Hans/PresentationFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hans/PresentationUI.resources.dll b/dist/AxCopilot/zh-Hans/PresentationUI.resources.dll deleted file mode 100644 index 1a97bc1..0000000 Binary files a/dist/AxCopilot/zh-Hans/PresentationUI.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hans/ReachFramework.resources.dll b/dist/AxCopilot/zh-Hans/ReachFramework.resources.dll deleted file mode 100644 index ae6cc13..0000000 Binary files a/dist/AxCopilot/zh-Hans/ReachFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hans/System.Windows.Controls.Ribbon.resources.dll b/dist/AxCopilot/zh-Hans/System.Windows.Controls.Ribbon.resources.dll deleted file mode 100644 index 1ebf330..0000000 Binary files a/dist/AxCopilot/zh-Hans/System.Windows.Controls.Ribbon.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hans/System.Windows.Forms.Design.resources.dll b/dist/AxCopilot/zh-Hans/System.Windows.Forms.Design.resources.dll deleted file mode 100644 index 46afdf9..0000000 Binary files a/dist/AxCopilot/zh-Hans/System.Windows.Forms.Design.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hans/System.Windows.Forms.Primitives.resources.dll b/dist/AxCopilot/zh-Hans/System.Windows.Forms.Primitives.resources.dll deleted file mode 100644 index 066d961..0000000 Binary files a/dist/AxCopilot/zh-Hans/System.Windows.Forms.Primitives.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hans/System.Windows.Forms.resources.dll b/dist/AxCopilot/zh-Hans/System.Windows.Forms.resources.dll deleted file mode 100644 index 7e2f746..0000000 Binary files a/dist/AxCopilot/zh-Hans/System.Windows.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hans/System.Windows.Input.Manipulations.resources.dll b/dist/AxCopilot/zh-Hans/System.Windows.Input.Manipulations.resources.dll deleted file mode 100644 index 695fe5a..0000000 Binary files a/dist/AxCopilot/zh-Hans/System.Windows.Input.Manipulations.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hans/System.Xaml.resources.dll b/dist/AxCopilot/zh-Hans/System.Xaml.resources.dll deleted file mode 100644 index e000568..0000000 Binary files a/dist/AxCopilot/zh-Hans/System.Xaml.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hans/UIAutomationClient.resources.dll b/dist/AxCopilot/zh-Hans/UIAutomationClient.resources.dll deleted file mode 100644 index 7cd27a7..0000000 Binary files a/dist/AxCopilot/zh-Hans/UIAutomationClient.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hans/UIAutomationClientSideProviders.resources.dll b/dist/AxCopilot/zh-Hans/UIAutomationClientSideProviders.resources.dll deleted file mode 100644 index 3390c92..0000000 Binary files a/dist/AxCopilot/zh-Hans/UIAutomationClientSideProviders.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hans/UIAutomationProvider.resources.dll b/dist/AxCopilot/zh-Hans/UIAutomationProvider.resources.dll deleted file mode 100644 index ff5f6e7..0000000 Binary files a/dist/AxCopilot/zh-Hans/UIAutomationProvider.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hans/UIAutomationTypes.resources.dll b/dist/AxCopilot/zh-Hans/UIAutomationTypes.resources.dll deleted file mode 100644 index 5a9cee8..0000000 Binary files a/dist/AxCopilot/zh-Hans/UIAutomationTypes.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hans/WindowsBase.resources.dll b/dist/AxCopilot/zh-Hans/WindowsBase.resources.dll deleted file mode 100644 index 34e5fe3..0000000 Binary files a/dist/AxCopilot/zh-Hans/WindowsBase.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hans/WindowsFormsIntegration.resources.dll b/dist/AxCopilot/zh-Hans/WindowsFormsIntegration.resources.dll deleted file mode 100644 index 2aef8b7..0000000 Binary files a/dist/AxCopilot/zh-Hans/WindowsFormsIntegration.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hant/Microsoft.VisualBasic.Forms.resources.dll b/dist/AxCopilot/zh-Hant/Microsoft.VisualBasic.Forms.resources.dll deleted file mode 100644 index 2461e41..0000000 Binary files a/dist/AxCopilot/zh-Hant/Microsoft.VisualBasic.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hant/PresentationCore.resources.dll b/dist/AxCopilot/zh-Hant/PresentationCore.resources.dll deleted file mode 100644 index 172948a..0000000 Binary files a/dist/AxCopilot/zh-Hant/PresentationCore.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hant/PresentationFramework.resources.dll b/dist/AxCopilot/zh-Hant/PresentationFramework.resources.dll deleted file mode 100644 index dd559ef..0000000 Binary files a/dist/AxCopilot/zh-Hant/PresentationFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hant/PresentationUI.resources.dll b/dist/AxCopilot/zh-Hant/PresentationUI.resources.dll deleted file mode 100644 index 50606ee..0000000 Binary files a/dist/AxCopilot/zh-Hant/PresentationUI.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hant/ReachFramework.resources.dll b/dist/AxCopilot/zh-Hant/ReachFramework.resources.dll deleted file mode 100644 index 91d47af..0000000 Binary files a/dist/AxCopilot/zh-Hant/ReachFramework.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hant/System.Windows.Controls.Ribbon.resources.dll b/dist/AxCopilot/zh-Hant/System.Windows.Controls.Ribbon.resources.dll deleted file mode 100644 index 5d40c0d..0000000 Binary files a/dist/AxCopilot/zh-Hant/System.Windows.Controls.Ribbon.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hant/System.Windows.Forms.Design.resources.dll b/dist/AxCopilot/zh-Hant/System.Windows.Forms.Design.resources.dll deleted file mode 100644 index 43ff8ac..0000000 Binary files a/dist/AxCopilot/zh-Hant/System.Windows.Forms.Design.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hant/System.Windows.Forms.Primitives.resources.dll b/dist/AxCopilot/zh-Hant/System.Windows.Forms.Primitives.resources.dll deleted file mode 100644 index ec2718b..0000000 Binary files a/dist/AxCopilot/zh-Hant/System.Windows.Forms.Primitives.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hant/System.Windows.Forms.resources.dll b/dist/AxCopilot/zh-Hant/System.Windows.Forms.resources.dll deleted file mode 100644 index 19d2346..0000000 Binary files a/dist/AxCopilot/zh-Hant/System.Windows.Forms.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hant/System.Windows.Input.Manipulations.resources.dll b/dist/AxCopilot/zh-Hant/System.Windows.Input.Manipulations.resources.dll deleted file mode 100644 index d79f7ba..0000000 Binary files a/dist/AxCopilot/zh-Hant/System.Windows.Input.Manipulations.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hant/System.Xaml.resources.dll b/dist/AxCopilot/zh-Hant/System.Xaml.resources.dll deleted file mode 100644 index 1c5780f..0000000 Binary files a/dist/AxCopilot/zh-Hant/System.Xaml.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hant/UIAutomationClient.resources.dll b/dist/AxCopilot/zh-Hant/UIAutomationClient.resources.dll deleted file mode 100644 index a15b9d3..0000000 Binary files a/dist/AxCopilot/zh-Hant/UIAutomationClient.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hant/UIAutomationClientSideProviders.resources.dll b/dist/AxCopilot/zh-Hant/UIAutomationClientSideProviders.resources.dll deleted file mode 100644 index 761a785..0000000 Binary files a/dist/AxCopilot/zh-Hant/UIAutomationClientSideProviders.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hant/UIAutomationProvider.resources.dll b/dist/AxCopilot/zh-Hant/UIAutomationProvider.resources.dll deleted file mode 100644 index 52486af..0000000 Binary files a/dist/AxCopilot/zh-Hant/UIAutomationProvider.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hant/UIAutomationTypes.resources.dll b/dist/AxCopilot/zh-Hant/UIAutomationTypes.resources.dll deleted file mode 100644 index 182b9ba..0000000 Binary files a/dist/AxCopilot/zh-Hant/UIAutomationTypes.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hant/WindowsBase.resources.dll b/dist/AxCopilot/zh-Hant/WindowsBase.resources.dll deleted file mode 100644 index 78230d8..0000000 Binary files a/dist/AxCopilot/zh-Hant/WindowsBase.resources.dll and /dev/null differ diff --git a/dist/AxCopilot/zh-Hant/WindowsFormsIntegration.resources.dll b/dist/AxCopilot/zh-Hant/WindowsFormsIntegration.resources.dll deleted file mode 100644 index 9cab001..0000000 Binary files a/dist/AxCopilot/zh-Hant/WindowsFormsIntegration.resources.dll and /dev/null differ diff --git a/dist/AxCopilot_Setup.exe b/dist/AxCopilot_Setup.exe index 1cb80b6..bc87538 100644 Binary files a/dist/AxCopilot_Setup.exe and b/dist/AxCopilot_Setup.exe differ diff --git a/dist/AxKeyEncryptor/AxKeyEncryptor.deps.json b/dist/AxKeyEncryptor/AxKeyEncryptor.deps.json deleted file mode 100644 index 2faf9b9..0000000 --- a/dist/AxKeyEncryptor/AxKeyEncryptor.deps.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "runtimeTarget": { - "name": ".NETCoreApp,Version=v8.0", - "signature": "" - }, - "compilationOptions": {}, - "targets": { - ".NETCoreApp,Version=v8.0": { - "AxKeyEncryptor/1.0.0": { - "runtime": { - "AxKeyEncryptor.dll": {} - } - } - } - }, - "libraries": { - "AxKeyEncryptor/1.0.0": { - "type": "project", - "serviceable": false, - "sha512": "" - } - } -} \ No newline at end of file diff --git a/dist/AxKeyEncryptor/AxKeyEncryptor.dll b/dist/AxKeyEncryptor/AxKeyEncryptor.dll index c93a5ca..6f1d32b 100644 Binary files a/dist/AxKeyEncryptor/AxKeyEncryptor.dll and b/dist/AxKeyEncryptor/AxKeyEncryptor.dll differ diff --git a/dist/AxKeyEncryptor/AxKeyEncryptor.exe b/dist/AxKeyEncryptor/AxKeyEncryptor.exe index 0ff46b9..371c9a0 100644 Binary files a/dist/AxKeyEncryptor/AxKeyEncryptor.exe and b/dist/AxKeyEncryptor/AxKeyEncryptor.exe differ diff --git a/dist/AxKeyEncryptor/AxKeyEncryptor.runtimeconfig.json b/dist/AxKeyEncryptor/AxKeyEncryptor.runtimeconfig.json deleted file mode 100644 index b65a073..0000000 --- a/dist/AxKeyEncryptor/AxKeyEncryptor.runtimeconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "runtimeOptions": { - "tfm": "net8.0", - "frameworks": [ - { - "name": "Microsoft.NETCore.App", - "version": "8.0.0" - }, - { - "name": "Microsoft.WindowsDesktop.App", - "version": "8.0.0" - } - ], - "configProperties": { - "System.Reflection.Metadata.MetadataUpdater.IsSupported": false, - "CSWINRT_USE_WINDOWS_UI_XAML_PROJECTIONS": false - } - } -} \ No newline at end of file diff --git a/docs/AX_AGENT_REGRESSION_PROMPTS.md b/docs/AX_AGENT_REGRESSION_PROMPTS.md index 98708f4..51c70fa 100644 --- a/docs/AX_AGENT_REGRESSION_PROMPTS.md +++ b/docs/AX_AGENT_REGRESSION_PROMPTS.md @@ -1,128 +1,145 @@ # AX Agent Regression Prompts -업데이트: 2026-04-06 09:58 (KST) +업데이트: 2026-04-08 10:38 (KST) `claw-code`와 AX Agent를 같은 기준으로 비교하기 위한 공통 회귀 프롬프트 세트입니다. ## 사용 규칙 -- 런타임 동작, transcript 렌더, 권한/계획/질문 UX, queue/compact/reopen 흐름에 영향을 주는 변경 뒤에는 이 문서를 기준으로 최소 1회 점검합니다. -- 모든 항목을 매번 수동 실행할 필요는 없지만, 관련 축이 바뀌었으면 해당 묶음은 반드시 확인합니다. -- 결과는 “문장이 똑같은가”가 아니라 “실행 경로와 사용자 체감 결과가 같은가”를 봅니다. +- 루프 정책, transcript 렌더, 권한/계획/진행 UX, queue/compact/reopen 흐름에 영향을 주는 변경 뒤에는 이 문서를 기준으로 최소 1회 점검합니다. +- 모든 항목을 매번 전부 돌릴 필요는 없지만, 바뀐 영역과 맞닿은 묶음은 반드시 확인합니다. +- 결과는 “모양이 똑같은가”보다 “사용자 체감 흐름과 완료 품질이 같은가”를 기준으로 봅니다. ## 실패 분류 -- `blank-reply`: 토큰은 소비됐는데 본문이 비어 있거나 assistant 카드가 비어 있음 +- `blank-reply`: 토큰은 쓰였는데 본문이 비거나 assistant 카드가 비어 있음 - `duplicate-banner`: 같은 실행 이벤트가 transcript에 중복 표시됨 -- `bad-approval-flow`: 권한/계획/질문 요청이 inline으로 안 닫히고 popup 의존이 커짐 -- `queue-drift`: 후속 요청, retry, regenerate가 다른 실행 경로를 타거나 순서가 어긋남 -- `restore-drift`: reopen 후 상태선, queue, 최신 메시지 상태가 달라짐 -- `status-noise`: Cowork/Code 기본 상태선이 과하게 흔들리거나 debug 정보가 과노출됨 +- `bad-approval-flow`: 권한/계획/질문 요청이 inline으로 닫히지 않고 popup 의존이 큼 +- `queue-drift`: 연속 요청, retry, regenerate가 다른 실행 경로를 타거나 순서가 꼬임 +- `restore-drift`: reopen 후 queue, 최신 메시지, 진행 상태가 어긋남 +- `status-noise`: Cowork/Code 기본 상태선에 debug 느낌 정보가 과하게 노출됨 +- `no-tool-loop`: 도구 미호출 보정이 길게 반복되고 실제 tool call로 이어지지 않음 +- `slow-doc-fallback`: 문서 작업에서 `document_plan` 후 산출물 없이 오래 루프를 탐 +- `profile-drift`: 등록 모델 프로파일을 바꿨는데 실제 loop/fallback/verification 성향이 달라지지 않음 ## Chat 1. 기본 응답 -- 프롬프트: `회의 일정 조정 메일을 정중한 한국어로 써줘` +- 프롬프트: `회의 일정 조정 메일을 정중한 톤으로 써줘` - 확인: - `blank-reply` - `restore-drift` + - SSE/타이핑형 출력이 실제로 보이는지 2. 장문 설명 - 프롬프트: `RAG와 fine-tuning 차이를 실무 관점으로 7가지로 설명해줘` - 확인: - - 장문 렌더 안정성 - - compact 이후 다음 턴 문맥 유지 + - 장문 markdown 렌더 안정성 + - compact 이후 다음 턴 유지 - `blank-reply` ## Cowork -3. 문서형 작업 +3. 문서 생성 - 프롬프트: `신규 ERP 도입 제안서 초안을 작성해줘. 목적, 범위, 기대효과, 추진일정 포함` - 확인: - 작업 유형 반영 - - 계획 이후 실제 문서형 결과 흐름 - - 기본 로그 과노출 없음 - - `bad-approval-flow` + - `document_plan -> 산출물 생성` 흐름이 실제로 닫히는지 + - `slow-doc-fallback` + - 진행 줄에 `처리 중 / 문서 결과 생성 중 / 검증 중`이 보이는지 -4. 데이터형 작업 -- 프롬프트: `매출 CSV를 분석해서 월별 추세와 이상치를 요약해줘` +4. 데이터 분석 +- 프롬프트: `매출 CSV를 분석해서 분기 추세와 이상치를 요약해줘` - 확인: - - 데이터 분석 도구 선택 - - 결과 요약 일관성 - - runtime 노이즈 최소화 + - 데이터 분석 프리셋/도구 선택 + - read-only 도구 배치가 빠르게 붙는지 - `status-noise` +5. 긴 대기/압축 +- 프롬프트: `지금까지 논의한 내용을 5줄로 이어서 정리하고 다음 작업을 제안해줘` +- 확인: + - `컨텍스트 압축 중...` 라이브 표시 + - elapsed/token meta가 비정상값 없이 보이는지 + - `blank-reply` + ## Code -5. 버그 수정 -- 프롬프트: `현재 프로젝트에서 설정 저장 버그 원인 찾고 수정해줘` +6. 버그 수정 +- 프롬프트: `현재 프로젝트에서 설정 저장 관련 버그 원인 찾고 수정해줘` - 확인: - - 읽기/검색/수정 흐름 일관성 - - diff/저장/재오픈 시 transcript 보존 + - 읽기/검색/수정 흐름 + - diff 근거와 검증 근거 유도 - `restore-drift` -6. 빌드/테스트 +7. 빌드/테스트 - 프롬프트: `빌드 오류를 재현하고 수정한 뒤 다시 빌드해줘` - 확인: - - build/test 루프 - - 실패 후 재시도 - - 완료 메시지 일관성 + - build/test loop + - 실패 시 원인 표시 - `queue-drift` -## Cross-tab - -7. 후속 요청 -- 프롬프트 순서: - - `이 창 레이아웃 문제 원인 찾아줘` - - `끝나면 README도 같이 갱신해줘` +8. 코드 검토 +- 프롬프트: `현재 변경사항을 리뷰해서 위험한 점을 먼저 알려줘` - 확인: - - queue chaining - - 입력창 직접 변경 없이 다음 턴 실행 - - `queue-drift` + - 리뷰 출력 형식 + - verification/report gate + - `status-noise` -8. compact 이후 연속성 -- 프롬프트: `지금까지 논의한 내용을 5줄로 이어서 정리하고 다음 작업 제안해줘` -- 확인: - - token-only completion 없음 - - compact 후 문맥 유지 - - `queue-drift` +## 프로파일 -9. 권한 승인 -- 프롬프트: `이 파일을 수정해서 저장해줘` +9. `tool_call_strict` +- 등록 모델 프로파일: `도구 호출 우선` +- 프롬프트: `src 폴더에서 설정 저장 버그 찾아서 고쳐줘` - 확인: - - 권한 요청 transcript 표시 - - 승인/거부 결과 일관성 + - 첫 1~2턴 내 tool call 발생 + - `no-tool-loop` 감소 + - verification gate가 과도하지 않은지 + +10. `fast_readonly` +- 등록 모델 프로파일: `읽기 속도 우선` +- 프롬프트: `프로젝트 구조와 핵심 진입 파일을 빠르게 파악해줘` +- 확인: + - 읽기 도구 병렬 배치 + - 불필요한 verification gate 없음 + - 답변이 빨리 시작되는지 + +11. `document_heavy` +- 등록 모델 프로파일: `문서 생성 우선` +- 프롬프트: `영업 전략 보고서 초안을 HTML 문서로 작성해줘` +- 확인: + - `document_plan` 후 장기 재시도보다 fallback이 빨리 타는지 + - 실제 산출물 생성까지 닫히는지 + - `slow-doc-fallback` 없음 + +12. `reasoning_first` +- 등록 모델 프로파일: `추론 우선` +- 프롬프트: `현재 아키텍처에서 병목 원인을 설명하고 개선안을 비교해줘` +- 확인: + - 초반 즉시 tool call을 강요하지 않는지 + - reasoning 후 필요한 tool call로 이어지는지 + - `profile-drift` 없음 + +## 권한 / 결과 + +13. 권한 거부 후 재시도 +- 프롬프트: `설정 파일을 수정해서 저장해줘` +- 흐름: + - 첫 권한 요청은 거부 + - 같은 작업 다시 요청 +- 확인: + - `reject`와 `approval_required`가 다르게 보이는지 - `bad-approval-flow` -10. slash / skill -- 프롬프트: `/bug-hunt src 폴더 잠재 버그 찾아줘` +14. 부분 성공 +- 프롬프트: `여러 문서를 찾아 읽고 핵심만 요약해줘` - 확인: - - slash 진입과 일반 send 경로 동일성 - - skill 실행 이유/결과 표기 - - `queue-drift` + - 일부 실패가 `partial` 성격으로 읽히는지 + - 후속 권장 작업 표시가 있는지 ## 개발 루틴 고정 -- transcript, permission, tool-result, queue, compact, reopen에 영향을 주는 변경은 커밋 전 아래를 기준으로 셀프 체크합니다. - - Chat 변경: 1, 2, 8 - - Cowork 변경: 3, 4, 7, 8 - - Code 변경: 5, 6, 7, 9, 10 -- 체크 후 문서 이력에는 “어떤 묶음을 확인했는지”를 간단히 남깁니다. -## Tool / Permission Follow-up - -11. 권한 거부 후 재시도 -- 프롬프트 순서: - - `src 폴더에서 설정 파일을 수정해줘` - - 첫 권한 요청은 거부 - - 같은 작업을 다시 요청 -- 확인: - - `reject`와 `approval_required`가 같은 결과 카드처럼 보이지 않음 - - 재시도 시 권한 메시지와 도구 결과가 중복되지 않음 - - `bad-approval-flow` - -12. 부분 성공 / 후속 안내 -- 프롬프트: `여러 문서 파일을 한 번에 읽고 요약해줘` -- 확인: - - 일부 실패가 있으면 `partial` 계열 안내가 보이는지 - - 후속 안내 문구가 단순 실패와 다르게 보이는지 - - `status-noise` +- Chat 변경: 1, 2 +- Cowork 변경: 3, 4, 5, 9, 11 +- Code 변경: 6, 7, 8, 9, 10 +- 권한/도구 결과 변경: 13, 14 +- 모델 프로파일/루프 정책 변경: 9, 10, 11, 12 diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 0fe8f16..cf27a15 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -5337,3 +5337,52 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - Document update: 2026-04-07 09:19 (KST) - Restored the AX Agent footer/status total token aggregate so it no longer disappears after runs return to idle. The status strip now rehydrates totals from the current conversation message token sums when live loop counters are empty. - Document update: 2026-04-07 09:19 (KST) - Corrected context-compaction popup accuracy by switching its detail copy to the last real compaction metrics (`before -> after`, automatic/manual kind, cumulative compaction count, cumulative saved tokens) instead of only the generic trigger-threshold text. - Document update: 2026-04-07 09:19 (KST) - Prevented `total_stats` loop events from being swallowed into the generic process-feed path. AX Agent now routes those events back through the dedicated total-stats presentation so transcript summaries and footer token totals stay aligned. + +## 2026-04-08 10:12 (KST) + +- [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs) + - `RegisteredModel.ExecutionProfile`과 `LlmSettings.UseAutomaticProfileTemperature`를 추가했다. + - 등록 모델은 실행 성향을 저장하고, AX Agent 내부 설정은 프로파일 기반 temperature 자동 적용 여부를 따로 제어한다. +- [ModelExecutionProfileCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ModelExecutionProfileCatalog.cs) + - `balanced`, `tool_call_strict`, `reasoning_first`, `fast_readonly`, `document_heavy` 실행 프로파일 카탈로그를 추가했다. + - 프로파일별로 초기 도구 강제, no-tool 임계값, 문서 재시도, 병렬 읽기 배치 수, terminal evidence gate, tool temperature cap을 함께 관리한다. +- [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs) + - 등록 모델 로드/저장 시 실행 프로파일을 함께 유지하고, 모델 행에도 프로파일 레이블을 보관하도록 확장했다. +- [ModelRegistrationDialog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ModelRegistrationDialog.cs) + - 등록 모델 추가/편집 다이얼로그에 `실행 프로파일` 선택 UI를 추가했다. +- [SettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml.cs) + - 일반 설정의 등록 모델 추가/편집 흐름에서 실행 프로파일 값을 저장/수정하도록 연결했다. +- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) + - AX Agent 내부 설정의 등록 모델 추가/편집 흐름도 실행 프로파일을 저장하도록 맞췄다. + - 내부 설정 Temperature row에 `자동 / 사용자 지정` 선택을 추가하고, 자동일 때는 슬라이더를 읽기 전용처럼 비활성화해 프로파일 기반 정책이 우선되도록 했다. +- [LlmService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/LlmService.cs) + - 현재 활성 모델의 실행 프로파일을 조회하는 헬퍼와 프로파일 기반 tool temperature 계산을 추가했다. + - `UseAutomaticProfileTemperature=false`면 기존 사용자가 지정한 temperature를 그대로 사용한다. +- [LlmService.ToolUse.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/LlmService.ToolUse.cs) + - tool calling 경로의 temperature를 일반 값 대신 프로파일 기반 `ResolveToolTemperature()`로 통일했다. +- [AgentLoopTransitions.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopTransitions.cs) + - 읽기 도구 병렬 배치 계획에 `maxParallelBatch` 상한을 추가해 프로파일별 최대 동시 읽기 수를 제어할 수 있게 했다. +- [AgentLoopTransitions.Execution.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs) + - terminal evidence gate 재시도 횟수를 외부에서 주입받도록 바꿔 프로파일 기반 제어를 가능하게 했다. +- [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs) + - Cowork/Code 루프 시작 시 현재 모델의 실행 프로파일을 읽어 no-tool 감지, 도구 미호출 재시도, 문서 생성 재시도, terminal evidence gate, 읽기 병렬 실행, 초기 compaction/memory pressure 정책을 함께 적용하도록 변경했다. + - `tool_call_strict`/`fast_readonly` 같은 프로파일은 초기 memory guidance와 aggressive compaction을 더 늦게 적용해, vLLM 계열에서 불필요한 프롬프트 팽창을 줄이는 방향으로 조정했다. + +## 2026-04-08 10:38 (KST) + +- [ModelExecutionProfileCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ModelExecutionProfileCatalog.cs) + - 실행 프로파일 정책을 확장해 `post-tool verification`, `코드 품질 게이트`, `문서 검증 게이트`, `diff/실행 증거 게이트`, `final report gate`의 강도까지 함께 관리하도록 바꿨다. + - `fast_readonly`와 `document_heavy`는 속도/산출물 우선으로 검증 게이트를 더 공격적으로 줄이고, `reasoning_first`와 `balanced`는 기존 품질 게이트를 유지하도록 정리했다. +- [AgentLoopTransitions.Execution.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs) + - 코드 품질 게이트, high-impact build/test 게이트, final report 게이트, diff/실행 성공 게이트, 문서 검증 게이트를 모두 프로파일 기반 재시도 횟수로 바꿨다. + - 문서 검증 관련 깨진 문자열을 정상 한국어로 복구하고, document verification gate가 실제 최대 재시도 수를 기준으로 동작하도록 수정했다. + - terminal document completion/post-tool verification 경로도 `EnablePostToolVerification` 정책을 읽어 profile별로 verification을 생략할 수 있게 했다. +- [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs) + - `document_heavy`/`tool_call_strict` 프로파일에서는 `document_plan` 이후 장기 재시도보다 fallback 생성으로 더 빨리 전환되도록 조정했다. + - 후속 게이트 호출 시 현재 실행 프로파일을 함께 넘겨, Cowork/Code loop가 모델 성향에 맞는 검증 강도를 실제로 적용하도록 연결했다. +- [LlmService.ToolUse.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/LlmService.ToolUse.cs) + - OpenAI/vLLM tool-use request body에 `parallel_tool_calls` 힌트를 추가해 읽기 도구 병렬 실행 성향이 모델 요청 단계에도 반영되도록 보강했다. +- [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs) + - Cowork/Code 진행 카드에 `계획 / 도구 / 검증 / 압축 / 폴백 / 재시도` 단계 메타를 더 직접적으로 붙여, 오래 걸리는 작업도 어느 단계인지 읽기 쉽게 정리했다. +- [AX_AGENT_REGRESSION_PROMPTS.md](/E:/AX%20Copilot%20-%20Codex/docs/AX_AGENT_REGRESSION_PROMPTS.md) + - 회귀 프롬프트 문서를 전면 교체하고, `tool_call_strict`, `fast_readonly`, `document_heavy`, `reasoning_first` 프로파일별 검증 시나리오를 추가했다. diff --git a/src/AxCopilot/App.xaml b/src/AxCopilot/App.xaml index b546109..bd5ffc4 100644 --- a/src/AxCopilot/App.xaml +++ b/src/AxCopilot/App.xaml @@ -55,6 +55,30 @@ + + + diff --git a/src/AxCopilot/App.xaml.cs b/src/AxCopilot/App.xaml.cs index 02f818c..f53effb 100644 --- a/src/AxCopilot/App.xaml.cs +++ b/src/AxCopilot/App.xaml.cs @@ -115,9 +115,14 @@ public partial class App : System.Windows.Application _indexService = new IndexService(settings); var indexService = _indexService; - indexService.LoadCachedIndex(); - if (indexService.HasCachedIndexLoaded) - indexService.StartWatchers(); + // 캐시 로드를 백그라운드에서 실행 — UI 스레드 블록 방지 + // FuzzyEngine은 IndexEntriesChanged 이벤트를 구독하여 캐시 로드 완료 시 자동 반영됨 + _ = Task.Run(() => + { + indexService.LoadCachedIndex(); + if (indexService.HasCachedIndexLoaded) + indexService.StartWatchers(); + }); var fuzzyEngine = new FuzzyEngine(indexService); var commandResolver = new CommandResolver(fuzzyEngine, settings); var contextManager = new ContextManager(settings); @@ -283,7 +288,12 @@ public partial class App : System.Windows.Application var vm = new LauncherViewModel(commandResolver, settings); _launcher = new LauncherWindow(vm) { - OpenSettingsAction = OpenSettings + OpenSettingsAction = OpenSettings, + SendToChatAction = msg => + { + OpenAiChat(); + _chatWindow?.SendInitialMessage(msg); + } }; // ─── 클립보드 히스토리 초기화 (메시지 펌프 시작 직후 — 런처 표시 불필요) ── diff --git a/src/AxCopilot/Assets/1_1775393927824.png_260405.zip b/src/AxCopilot/Assets/1_1775393927824.png_260405.zip new file mode 100644 index 0000000..a4d6735 Binary files /dev/null and b/src/AxCopilot/Assets/1_1775393927824.png_260405.zip differ diff --git a/src/AxCopilot/Assets/폴디_큐디.png b/src/AxCopilot/Assets/폴디_큐디.png new file mode 100644 index 0000000..416b381 Binary files /dev/null and b/src/AxCopilot/Assets/폴디_큐디.png differ diff --git a/src/AxCopilot/Core/FuzzyEngine.cs b/src/AxCopilot/Core/FuzzyEngine.cs index d3555f0..cc7224d 100644 --- a/src/AxCopilot/Core/FuzzyEngine.cs +++ b/src/AxCopilot/Core/FuzzyEngine.cs @@ -18,7 +18,8 @@ public class FuzzyEngine public FuzzyEngine(IndexService index) { _index = index; - _index.IndexRebuilt += (_, _) => InvalidateQueryCache(); + // IndexRebuilt(전체 재색인)와 증분 갱신 모두에서 캐시 무효화 + _index.IndexEntriesChanged += (_, _) => InvalidateQueryCache(); } /// diff --git a/src/AxCopilot/Models/AppSettings.cs b/src/AxCopilot/Models/AppSettings.cs index 3e24538..7d90573 100644 --- a/src/AxCopilot/Models/AppSettings.cs +++ b/src/AxCopilot/Models/AppSettings.cs @@ -777,6 +777,10 @@ public class LlmSettings [JsonPropertyName("temperature")] public double Temperature { get; set; } = 0.7; + /// 등록 모델 프로파일의 자동 temperature 정책 사용 여부. 기본값 true. + [JsonPropertyName("useAutomaticProfileTemperature")] + public bool UseAutomaticProfileTemperature { get; set; } = true; + /// 사내 서비스(Ollama/vLLM)용 등록 모델 목록. 별칭 + 암호화된 모델명. [JsonPropertyName("registeredModels")] public List RegisteredModels { get; set; } = new(); @@ -1377,6 +1381,13 @@ public class RegisteredModel [JsonPropertyName("service")] public string Service { get; set; } = "ollama"; + /// + /// 실행 프로파일. balanced | tool_call_strict | reasoning_first | fast_readonly | document_heavy + /// 모델 성향에 따라 도구 호출 강도, 재시도, 메모리/압축 주입량을 조절합니다. + /// + [JsonPropertyName("executionProfile")] + public string ExecutionProfile { get; set; } = "balanced"; + /// 이 모델 전용 서버 엔드포인트. 비어있으면 LlmSettings의 기본 엔드포인트 사용. [JsonPropertyName("endpoint")] public string Endpoint { get; set; } = ""; diff --git a/src/AxCopilot/Models/ChatModels.cs b/src/AxCopilot/Models/ChatModels.cs index 1417c6e..942fe89 100644 --- a/src/AxCopilot/Models/ChatModels.cs +++ b/src/AxCopilot/Models/ChatModels.cs @@ -243,6 +243,10 @@ public class DraftQueueItem /// 대화 내 개별 메시지. public class ChatMessage { + /// 메시지 고유 ID — 버블 캐시 키로 사용. 저장된 JSON에 없으면 세션마다 새 GUID 할당. + [JsonPropertyName("msgId")] + public string MsgId { get; set; } = Guid.NewGuid().ToString("N"); + [JsonPropertyName("role")] public string Role { get; set; } = "user"; // "user" | "assistant" | "system" diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.cs b/src/AxCopilot/Services/Agent/AgentLoopService.cs index 619795a..71c7481 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopService.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopService.cs @@ -185,16 +185,19 @@ public partial class AgentLoopService string? documentPlanTitle = null; // document_plan이 제안한 문서 제목 string? documentPlanScaffold = null; // document_plan이 생성한 body 골격 HTML string? lastArtifactFilePath = null; // 생성/수정 산출물 파일 추적 + var taskType = ClassifyTaskType(userQuery, ActiveTab); + var taskPolicy = TaskTypePolicy.FromTaskType(taskType); + var executionPolicy = _llm.GetActiveExecutionPolicy(); var consecutiveNoToolResponses = 0; - var noToolResponseThreshold = GetNoToolCallResponseThreshold(); - var noToolRecoveryMaxRetries = GetNoToolCallRecoveryMaxRetries(); - var planExecutionRetryMax = GetPlanExecutionRetryMax(); + var noToolResponseThreshold = GetNoToolCallResponseThreshold(executionPolicy.NoToolResponseThreshold); + var noToolRecoveryMaxRetries = GetNoToolCallRecoveryMaxRetries(executionPolicy.NoToolRecoveryMaxRetries); + var planExecutionRetryMax = GetPlanExecutionRetryMax(executionPolicy.PlanExecutionRetryMax); + var documentPlanRetryMax = Math.Max(0, executionPolicy.DocumentPlanRetryMax); + var terminalEvidenceGateRetryMax = GetTerminalEvidenceGateMaxRetries(executionPolicy.TerminalEvidenceGateMaxRetries); var failedToolHistogram = new Dictionary(StringComparer.OrdinalIgnoreCase); var runState = new RunState(); var requireHighImpactCodeVerification = false; string? lastModifiedCodeFilePath = null; - var taskType = ClassifyTaskType(userQuery, ActiveTab); - var taskPolicy = TaskTypePolicy.FromTaskType(taskType); maxRetry = ComputeAdaptiveMaxRetry(maxRetry, taskPolicy.TaskType); var recentTaskRetryQuality = TryGetRecentTaskRetryQuality(taskPolicy.TaskType); maxRetry = ComputeQualityAwareMaxRetry(maxRetry, recentTaskRetryQuality, taskPolicy.TaskType); @@ -204,7 +207,8 @@ public partial class AgentLoopService var context = BuildContext(); InjectTaskTypeGuidance(messages, taskPolicy); - InjectRecentFailureGuidance(messages, context, userQuery, taskPolicy); + if (!executionPolicy.ReduceEarlyMemoryPressure) + InjectRecentFailureGuidance(messages, context, userQuery, taskPolicy); var runtimeOverrides = ResolveSkillRuntimeOverrides(messages); var runtimeHooks = GetRuntimeHooks(llm.AgentHooks, runtimeOverrides); var runtimeOverrideApplied = false; @@ -443,12 +447,17 @@ public partial class AgentLoopService // Context Condenser: 토큰 초과 시 이전 대화 자동 압축 // 첫 반복에서도 실행 (이전 대화 복원으로 이미 긴 경우 대비) { + var proactiveCompact = llm.EnableProactiveContextCompact + && !(executionPolicy.ReduceEarlyMemoryPressure && iteration <= 1); + var compactTriggerPercent = executionPolicy.ReduceEarlyMemoryPressure + ? Math.Min(95, llm.ContextCompactTriggerPercent + 10) + : llm.ContextCompactTriggerPercent; var compactionResult = await ContextCondenser.CondenseWithStatsAsync( messages, _llm, llm.MaxContextTokens, - llm.EnableProactiveContextCompact, - llm.ContextCompactTriggerPercent, + proactiveCompact, + compactTriggerPercent, false, ct); if (compactionResult.Changed) @@ -509,12 +518,16 @@ public partial class AgentLoopService EmitEvent(AgentEventType.Error, "", "현재 스킬 런타임 정책으로 사용 가능한 도구가 없습니다."); return "⚠ 현재 스킬 정책에서 허용된 도구가 없어 작업을 진행할 수 없습니다. allowed-tools 설정을 확인하세요."; } + // totalToolCalls == 0: 아직 한 번도 도구를 안 불렀으면 tool_choice:"required" 강제 + // → chatty 모델(Qwen 등)이 텍스트 설명만 하고 도구를 안 부르는 현상 방지 + var forceFirst = totalToolCalls == 0 && executionPolicy.ForceInitialToolCall; blocks = await SendWithToolsWithRecoveryAsync( messages, activeTools, ct, $"메인 루프 {iteration}", - runState); + runState, + forceToolCall: forceFirst); runState.ContextRecoveryAttempts = 0; runState.TransientLlmErrorRetries = 0; NotifyPostCompactionTurnIfNeeded(runState); @@ -601,6 +614,11 @@ public partial class AgentLoopService return $"⚠ LLM 오류 (도구 호출 실패 후 폴백도 실패): {fallbackEx.Message}"; } } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + // 사용자가 직접 취소한 경우 — 오류로 처리하지 않고 상위로 전파 + throw; + } catch (Exception ex) { if (TryHandleContextOverflowTransition(ex, messages, runState)) @@ -706,13 +724,27 @@ public partial class AgentLoopService .Select(t => t.Name) .Distinct(StringComparer.OrdinalIgnoreCase) .Take(10)); - messages.Add(new ChatMessage + var retryNum = runState.NoToolCallLoopRetry; + var recoveryContent = retryNum switch { - Role = "user", - Content = "[System:ToolCallRequired] 현재 응답이 연속으로 도구 호출 없이 종료되고 있습니다. " + - "설명만 하지 말고 즉시 도구를 최소 1회 이상 호출해 실행을 진행하세요. " + - $"사용 가능 도구 예시: {activeToolPreview}" - }); + 1 => + "[System:ToolCallRequired] " + + "⚠ 경고: 이전 응답에서 도구를 호출하지 않았습니다. " + + "텍스트 설명만 반환하는 것은 허용되지 않습니다. " + + "지금 즉시 도구를 1개 이상 호출하세요. " + + "할 말이 있다면 도구 호출 이후에 하세요 — 도구 호출 전 설명 금지. " + + "한 응답에서 여러 도구를 동시에 호출할 수 있고, 그렇게 해야 합니다. " + + $"지금 사용 가능한 도구: {activeToolPreview}", + _ => + "[System:ToolCallRequired] " + + "🚨 최종 경고: 도구를 계속 호출하지 않고 있습니다. 이것이 마지막 기회입니다. " + + "지금 응답은 반드시 도구 호출만 포함해야 합니다. 텍스트는 한 글자도 쓰지 마세요. " + + "작업을 완료하려면 도구를 호출하는 것 외에 다른 방법이 없습니다. " + + "도구 이름을 모른다면 아래 목록에서 골라 즉시 호출하세요. " + + "여러 도구를 한꺼번에 호출할 수 있습니다 — 지금 그렇게 하세요. " + + $"반드시 사용해야 할 도구 목록: {activeToolPreview}" + }; + messages.Add(new ChatMessage { Role = "user", Content = recoveryContent }); EmitEvent( AgentEventType.Thinking, "", @@ -725,20 +757,50 @@ public partial class AgentLoopService // 계획이 있고 도구가 아직 한 번도 실행되지 않은 경우 → LLM이 도구 대신 텍스트로만 응답한 것 // "계획이 승인됐으니 도구를 호출하라"는 메시지를 추가하여 재시도 (최대 2회) - if (planSteps.Count > 0 && totalToolCalls == 0 && planExecutionRetry < planExecutionRetryMax) + if (executionPolicy.ForceToolCallAfterPlan + && planSteps.Count > 0 + && totalToolCalls == 0 + && planExecutionRetry < planExecutionRetryMax) { planExecutionRetry++; if (!string.IsNullOrEmpty(textResponse)) messages.Add(new ChatMessage { Role = "assistant", Content = textResponse }); - messages.Add(new ChatMessage { Role = "user", - Content = "도구를 호출하지 않았습니다. 계획 1단계를 지금 즉시 도구(tool call)로 실행하세요. " + - "설명 없이 도구 호출만 하세요." }); + var planToolList = string.Join(", ", + GetRuntimeActiveTools(llm.DisabledTools, runtimeOverrides) + .Select(t => t.Name) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Take(10)); + var planRecoveryContent = planExecutionRetry switch + { + 1 => + "[System:ToolCallRequired] 계획을 세웠지만 도구를 호출하지 않았습니다. " + + "계획은 이미 수립되었으므로 지금 당장 실행 단계로 넘어가세요. " + + "텍스트 설명 없이 계획의 첫 번째 단계를 도구(tool call)로 즉시 실행하세요. " + + "한 응답에서 여러 도구를 동시에 호출할 수 있습니다. " + + $"사용 가능한 도구: {planToolList}", + _ => + "[System:ToolCallRequired] 🚨 도구 호출 없이 계획만 반복하고 있습니다. " + + "이제 계획 설명은 완전히 금지됩니다. 오직 도구 호출만 하세요. " + + "지금 이 응답에 텍스트를 포함하지 마세요. 도구만 호출하세요. " + + "독립적인 작업은 한 번에 여러 도구를 병렬 호출하세요. " + + $"사용 가능한 도구: {planToolList}" + }; + messages.Add(new ChatMessage { Role = "user", Content = planRecoveryContent }); EmitEvent(AgentEventType.Thinking, "", $"도구 미호출 감지 — 실행 재시도 {planExecutionRetry}/{planExecutionRetryMax}..."); continue; // 루프 재시작 } - // document_plan은 호출됐지만 terminal 문서 도구(html_create 등)가 미호출인 경우 → 재시도 (최대 2회) - if (documentPlanCalled && postDocumentPlanRetry < 2) + // 문서 생성 우선 프로파일은 재시도보다 빠른 fallback을 우선합니다. + if (documentPlanCalled + && executionPolicy.PreferAggressiveDocumentFallback + && !string.IsNullOrEmpty(documentPlanScaffold) + && !_docFallbackAttempted) + { + postDocumentPlanRetry = documentPlanRetryMax; + } + + // document_plan은 호출됐지만 terminal 문서 도구(html_create 등)가 미호출인 경우 → 프로파일 기준 재시도 + if (documentPlanCalled && postDocumentPlanRetry < documentPlanRetryMax) { postDocumentPlanRetry++; if (!string.IsNullOrEmpty(textResponse)) @@ -747,7 +809,7 @@ public partial class AgentLoopService Content = "html_create 도구를 호출하지 않았습니다. " + "document_plan 결과의 body 골격을 바탕으로 각 섹션에 충분한 내용을 채워서 " + "html_create 도구를 지금 즉시 호출하세요. 설명 없이 도구 호출만 하세요." }); - EmitEvent(AgentEventType.Thinking, "", $"html_create 미호출 재시도 {postDocumentPlanRetry}/2..."); + EmitEvent(AgentEventType.Thinking, "", $"html_create 미호출 재시도 {postDocumentPlanRetry}/{documentPlanRetryMax}..."); continue; // 루프 재시작 } @@ -845,36 +907,41 @@ public partial class AgentLoopService textResponse, taskPolicy, lastArtifactFilePath, - runState)) + runState, + executionPolicy)) continue; if (TryApplyCodeDiffEvidenceGateTransition( messages, textResponse, - runState)) + runState, + executionPolicy)) continue; if (TryApplyRecentExecutionEvidenceGateTransition( messages, textResponse, taskPolicy, - runState)) + runState, + executionPolicy)) continue; if (TryApplyExecutionSuccessGateTransition( messages, textResponse, taskPolicy, - runState)) + runState, + executionPolicy)) continue; - if (TryApplyCodeCompletionGateTransition( + if (executionPolicy.EnableCodeQualityGates && TryApplyCodeCompletionGateTransition( messages, textResponse, taskPolicy, requireHighImpactCodeVerification, totalToolCalls, - runState)) + runState, + executionPolicy)) continue; if (TryApplyTerminalEvidenceGateTransition( @@ -884,7 +951,8 @@ public partial class AgentLoopService userQuery, totalToolCalls, lastArtifactFilePath, - runState)) + runState, + terminalEvidenceGateRetryMax)) continue; if (!string.IsNullOrEmpty(textResponse)) @@ -904,7 +972,10 @@ public partial class AgentLoopService messages.Add(new ChatMessage { Role = "assistant", Content = assistantContent }); // 도구 실행 (병렬 모드: 읽기 전용 도구끼리 동시 실행) - var parallelPlan = CreateParallelExecutionPlan(llm.EnableParallelTools, toolCalls); + var parallelPlan = CreateParallelExecutionPlan( + llm.EnableParallelTools && executionPolicy.EnableParallelReadBatch, + toolCalls, + executionPolicy.MaxParallelReadBatch); if (parallelPlan.ShouldRun) { if (parallelPlan.ParallelBatch.Count > 1) @@ -1398,6 +1469,7 @@ public partial class AgentLoopService toolCalls, messages, llm, + executionPolicy, context, ct); if (terminalCompleted) @@ -1412,6 +1484,7 @@ public partial class AgentLoopService result, messages, llm, + executionPolicy, context, ct); if (consumedVerificationIteration) @@ -3474,29 +3547,53 @@ public partial class AgentLoopService } private static int GetNoToolCallResponseThreshold() - => ResolveNoToolCallResponseThreshold( - Environment.GetEnvironmentVariable("AXCOPILOT_NOTOOL_RESPONSE_THRESHOLD")); + => GetNoToolCallResponseThreshold(defaultValue: 2); + + private static int GetNoToolCallResponseThreshold(int defaultValue) + => ResolveThresholdValue( + Environment.GetEnvironmentVariable("AXCOPILOT_NOTOOL_RESPONSE_THRESHOLD"), + defaultValue, + min: 1, + max: 6); private static int GetNoToolCallRecoveryMaxRetries() - => ResolveNoToolCallRecoveryMaxRetries( - Environment.GetEnvironmentVariable("AXCOPILOT_NOTOOL_RECOVERY_MAX_RETRIES")); + => GetNoToolCallRecoveryMaxRetries(defaultValue: 3); + + private static int GetNoToolCallRecoveryMaxRetries(int defaultValue) + => ResolveThresholdValue( + Environment.GetEnvironmentVariable("AXCOPILOT_NOTOOL_RECOVERY_MAX_RETRIES"), + defaultValue, + min: 0, + max: 6); private static int GetPlanExecutionRetryMax() - => ResolvePlanExecutionRetryMax( - Environment.GetEnvironmentVariable("AXCOPILOT_PLAN_EXECUTION_RETRY_MAX")); + => GetPlanExecutionRetryMax(defaultValue: 2); + + private static int GetPlanExecutionRetryMax(int defaultValue) + => ResolveThresholdValue( + Environment.GetEnvironmentVariable("AXCOPILOT_PLAN_EXECUTION_RETRY_MAX"), + defaultValue, + min: 0, + max: 6); private static int ResolveNoToolCallResponseThreshold(string? envRaw) => ResolveThresholdValue(envRaw, defaultValue: 2, min: 1, max: 6); private static int ResolveNoToolCallRecoveryMaxRetries(string? envRaw) - => ResolveThresholdValue(envRaw, defaultValue: 2, min: 0, max: 6); + => ResolveThresholdValue(envRaw, defaultValue: 3, min: 0, max: 6); private static int ResolvePlanExecutionRetryMax(string? envRaw) => ResolveThresholdValue(envRaw, defaultValue: 2, min: 0, max: 6); private static int GetTerminalEvidenceGateMaxRetries() - => ResolveTerminalEvidenceGateMaxRetries( - Environment.GetEnvironmentVariable("AXCOPILOT_TERMINAL_EVIDENCE_GATE_MAX_RETRIES")); + => GetTerminalEvidenceGateMaxRetries(defaultValue: 1); + + private static int GetTerminalEvidenceGateMaxRetries(int defaultValue) + => ResolveThresholdValue( + Environment.GetEnvironmentVariable("AXCOPILOT_TERMINAL_EVIDENCE_GATE_MAX_RETRIES"), + defaultValue, + min: 0, + max: 3); private static int ResolveTerminalEvidenceGateMaxRetries(string? envRaw) => ResolveThresholdValue(envRaw, defaultValue: 1, min: 0, max: 3); @@ -3957,6 +4054,10 @@ public partial class AgentLoopService } } } + catch (OperationCanceledException) + { + throw; + } catch (Exception ex) { EmitEvent(AgentEventType.Error, "", $"검증 LLM 호출 실패: {ex.Message}"); @@ -4592,9 +4693,15 @@ public partial class AgentLoopService } // 문서 생성 (Excel, Word, HTML 등) - if (toolName is "excel_create" or "docx_create" or "html_create" or "csv_create" or "script_create") + // Cowork 탭에서 문서 생성은 핵심 목적이므로 승인 불필요 + if (toolName is "excel_create" or "docx_create" or "html_create" or "csv_create" or "script_create" + or "markdown_create" or "pptx_create") { - var path = input?.TryGetProperty("file_path", out var p) == true ? p.GetString() : ""; + if (string.Equals(ActiveTab, "Cowork", StringComparison.OrdinalIgnoreCase)) + return null; + // "path" 파라미터 우선, 없으면 "file_path" 순으로 추출 + var path = (input?.TryGetProperty("path", out var p1) == true ? p1.GetString() : null) + ?? (input?.TryGetProperty("file_path", out var p2) == true ? p2.GetString() : ""); return $"문서를 생성하시겠습니까?\n\n도구: {toolName}\n경로: {path}"; } @@ -4833,4 +4940,3 @@ public partial class AgentLoopService } } - diff --git a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs index 24d3508..3ebaf96 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs @@ -61,7 +61,8 @@ public partial class AgentLoopService TaskTypePolicy taskPolicy, bool requireHighImpactCodeVerification, int totalToolCalls, - RunState runState) + RunState runState, + ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy) { if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase) || totalToolCalls <= 0) return false; @@ -69,7 +70,9 @@ public partial class AgentLoopService var hasCodeVerificationEvidence = HasCodeVerificationEvidenceAfterLastModification( messages, requireHighImpactCodeVerification); - if (!hasCodeVerificationEvidence && runState.CodeVerificationGateRetry < 2) + if (executionPolicy.CodeVerificationGateMaxRetries > 0 + && !hasCodeVerificationEvidence + && runState.CodeVerificationGateRetry < executionPolicy.CodeVerificationGateMaxRetries) { runState.CodeVerificationGateRetry++; if (!string.IsNullOrEmpty(textResponse)) @@ -78,18 +81,19 @@ public partial class AgentLoopService { Role = "user", Content = requireHighImpactCodeVerification - ? "[System:CodeQualityGate] 怨듭슜/?듭떖 肄붾뱶 ?섏젙 ?댄썑 寃€利?洹쇨굅媛€ 遺€議깊빀?덈떎. 醫낅즺?섏? 留먭퀬 file_read, grep/glob, git diff, build/test源뚯? ?뺣낫???ㅼ뿉留?留덈Т由ы븯?몄슂." - : "[System:CodeQualityGate] 留덉?留?肄붾뱶 ?섏젙 ?댄썑 build/test/file_read/diff 洹쇨굅媛€ 遺€議깊빀?덈떎. 醫낅즺?섏? 留먭퀬 寃€利?洹쇨굅瑜??뺣낫???ㅼ뿉留?留덈Т由ы븯?몄슂." + ? "[System:CodeQualityGate] 공용/핵심 코드 변경 이후 검증 근거가 부족합니다. 종료하지 말고 file_read, grep/glob, git diff, build/test까지 확인한 뒤에만 마무리하세요." + : "[System:CodeQualityGate] 마지막 코드 수정 이후 build/test/file_read/diff 근거가 부족합니다. 종료하지 말고 검증 근거를 보강한 뒤에만 마무리하세요." }); EmitEvent(AgentEventType.Thinking, "", requireHighImpactCodeVerification - ? "怨좎쁺??肄붾뱶 蹂€寃쎌쓽 寃€利?洹쇨굅媛€ 遺€議깊빐 異붽? 寃€利앹쓣 吏꾪뻾?⑸땲??.." - : "肄붾뱶 寃곌낵 寃€利?洹쇨굅媛€ 遺€議깊빐 異붽? 寃€利앹쓣 吏꾪뻾?⑸땲??.."); + ? "핵심 코드 변경의 검증 근거가 부족해 추가 검증을 진행합니다..." + : "코드 결과 검증 근거가 부족해 추가 검증을 진행합니다..."); return true; } if (requireHighImpactCodeVerification + && executionPolicy.HighImpactBuildTestGateMaxRetries > 0 && !HasSuccessfulBuildAndTestAfterLastModification(messages) - && runState.HighImpactBuildTestGateRetry < 1) + && runState.HighImpactBuildTestGateRetry < executionPolicy.HighImpactBuildTestGateMaxRetries) { runState.HighImpactBuildTestGateRetry++; if (!string.IsNullOrEmpty(textResponse)) @@ -97,15 +101,15 @@ public partial class AgentLoopService messages.Add(new ChatMessage { Role = "user", - Content = "[System:HighImpactBuildTestGate] 怨좎쁺??肄붾뱶 蹂€寃쎌엯?덈떎. " + - "醫낅즺?섏? 留먭퀬 build_run怨?test_loop瑜?紐⑤몢 ?ㅽ뻾???깃났 洹쇨굅瑜??뺣낫???ㅼ뿉留?留덈Т由ы븯?몄슂." + Content = "[System:HighImpactBuildTestGate] 핵심 코드 변경입니다. 종료하지 말고 build_run과 test_loop를 모두 실행해 성공 근거를 확보한 뒤에만 마무리하세요." }); - EmitEvent(AgentEventType.Thinking, "", "怨좎쁺??蹂€寃쎌씠??build+test ?깃났 洹쇨굅瑜?紐⑤몢 ?뺣낫???뚭퉴吏€ 吏꾪뻾?⑸땲??.."); + EmitEvent(AgentEventType.Thinking, "", "핵심 변경이라 build+test 성공 근거를 모두 확보할 때까지 진행합니다..."); return true; } - if (!HasSufficientFinalReportEvidence(textResponse, taskPolicy, requireHighImpactCodeVerification, messages) - && runState.FinalReportGateRetry < 1) + if (executionPolicy.FinalReportGateMaxRetries > 0 + && !HasSufficientFinalReportEvidence(textResponse, taskPolicy, requireHighImpactCodeVerification, messages) + && runState.FinalReportGateRetry < executionPolicy.FinalReportGateMaxRetries) { runState.FinalReportGateRetry++; if (!string.IsNullOrEmpty(textResponse)) @@ -115,7 +119,7 @@ public partial class AgentLoopService Role = "user", Content = BuildFinalReportQualityPrompt(taskPolicy, requireHighImpactCodeVerification) }); - EmitEvent(AgentEventType.Thinking, "", "理쒖쥌 蹂닿퀬??蹂€寃?寃€利?由ъ뒪???붿빟??遺€議깊빐 ??踰????뺣━?⑸땲??.."); + EmitEvent(AgentEventType.Thinking, "", "최종 보고에 변경·검증·리스크 요약이 부족해 한 번 더 정리합니다..."); return true; } @@ -125,12 +129,13 @@ public partial class AgentLoopService private bool TryApplyCodeDiffEvidenceGateTransition( List messages, string? textResponse, - RunState runState) + RunState runState, + ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy) { if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)) return false; - if (runState.CodeDiffGateRetry >= 1) + if (executionPolicy.CodeDiffGateMaxRetries <= 0 || runState.CodeDiffGateRetry >= executionPolicy.CodeDiffGateMaxRetries) return false; if (HasDiffEvidenceAfterLastModification(messages)) @@ -154,12 +159,13 @@ public partial class AgentLoopService List messages, string? textResponse, TaskTypePolicy taskPolicy, - RunState runState) + RunState runState, + ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy) { if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)) return false; - if (runState.RecentExecutionGateRetry >= 1) + if (executionPolicy.RecentExecutionGateMaxRetries <= 0 || runState.RecentExecutionGateRetry >= executionPolicy.RecentExecutionGateMaxRetries) return false; if (!HasAnyBuildOrTestEvidence(messages)) @@ -184,12 +190,13 @@ public partial class AgentLoopService List messages, string? textResponse, TaskTypePolicy taskPolicy, - RunState runState) + RunState runState, + ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy) { if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase)) return false; - if (runState.ExecutionSuccessGateRetry >= 1) + if (executionPolicy.ExecutionSuccessGateMaxRetries <= 0 || runState.ExecutionSuccessGateRetry >= executionPolicy.ExecutionSuccessGateMaxRetries) return false; if (!HasAnyBuildOrTestAttempt(messages)) @@ -217,7 +224,8 @@ public partial class AgentLoopService string userQuery, int totalToolCalls, string? lastArtifactFilePath, - RunState runState) + RunState runState, + int retryMax) { if (totalToolCalls <= 0) return false; @@ -225,7 +233,6 @@ public partial class AgentLoopService if (ShouldSkipTerminalEvidenceGateForAnalysisQuery(userQuery, taskPolicy)) return false; - var retryMax = GetTerminalEvidenceGateMaxRetries(); if (runState.TerminalEvidenceGateRetry >= retryMax) return false; @@ -426,9 +433,18 @@ public partial class AgentLoopService string? textResponse, TaskTypePolicy taskPolicy, string? lastArtifactFilePath, - RunState runState) + RunState runState, + ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy) { - if (!ShouldRequestDocumentVerification(taskPolicy.TaskType, lastArtifactFilePath, messages, runState.DocumentVerificationGateRetry)) + if (!executionPolicy.EnableDocumentVerificationGate) + return false; + + if (!ShouldRequestDocumentVerification( + taskPolicy.TaskType, + lastArtifactFilePath, + messages, + runState.DocumentVerificationGateRetry, + executionPolicy.DocumentVerificationGateMaxRetries)) return false; runState.DocumentVerificationGateRetry++; @@ -437,13 +453,13 @@ public partial class AgentLoopService messages.Add(new ChatMessage { Role = "user", - Content = "[System:DocumentVerificationGate] 臾몄꽌 ?뚯씪?€ ?앹꽦?섏뿀吏€留?寃€利?洹쇨굅媛€ 遺€議깊빀?덈떎. " + - "file_read ?먮뒗 document_read濡?諛⑷툑 ?앹꽦???뚯씪???ㅼ떆 ?쎄퀬, 臾몄젣 ?놁쓬/?섏젙 ?꾩슂瑜?紐낆떆??蹂닿퀬?섏꽭??" + Content = "[System:DocumentVerificationGate] 문서 파일은 생성됐지만 검증 근거가 부족합니다. " + + "file_read 또는 document_read로 방금 생성한 파일을 다시 확인하고, 문제 없음/수정 필요 여부를 명시해 보고하세요." }); EmitEvent( AgentEventType.Thinking, "", - $"臾몄꽌 寃€利?洹쇨굅媛€ 遺€議깊빐 ?ш?利앹쓣 ?붿껌?⑸땲??({runState.DocumentVerificationGateRetry}/1)"); + $"문서 검증 근거가 부족해 결과 검증을 요청합니다 ({runState.DocumentVerificationGateRetry}/{Math.Max(1, executionPolicy.DocumentVerificationGateMaxRetries)})"); return true; } @@ -461,12 +477,14 @@ public partial class AgentLoopService string taskType, string? lastArtifactFilePath, List messages, - int verificationGateRetry) + int verificationGateRetry, + int maxRetries) { return string.Equals(taskType, "docs", StringComparison.OrdinalIgnoreCase) && HasMaterializedArtifact(lastArtifactFilePath) && !HasDocumentVerificationEvidenceAfterLastArtifact(messages) - && verificationGateRetry < 1; + && maxRetries > 0 + && verificationGateRetry < maxRetries; } private static bool HasDocumentVerificationEvidenceAfterLastArtifact(List messages) @@ -758,9 +776,9 @@ public partial class AgentLoopService { if (string.Equals(taskPolicy.TaskType, "docs", StringComparison.OrdinalIgnoreCase)) { - var fileHint = string.IsNullOrWhiteSpace(documentPlanPath) ? "寃곌낵 臾몄꽌 ?뚯씪" : $"'{documentPlanPath}'"; - return "[System:NoProgressExecutionRecovery] ?쎄린/遺꾩꽍留?諛섎났?섍퀬 ?덉뒿?덈떎. " + - $"?댁젣 ?ㅻ챸??硫덉텛怨?{fileHint}???ㅼ젣濡??앹꽦?섎뒗 ?꾧뎄(html_create/markdown_create/document_assemble/file_write)瑜?利됱떆 ?몄텧?섏꽭??"; + var fileHint = string.IsNullOrWhiteSpace(documentPlanPath) ? "결과 문서 파일" : $"'{documentPlanPath}'"; + return "[System:NoProgressExecutionRecovery] 계획/분석만 반복되고 있습니다. " + + $"지금 즉시 설명을 멈추고 {fileHint}을(를) 실제로 생성하는 도구(html_create/markdown_create/document_assemble/file_write)를 호출하세요."; } return BuildFailureNextToolPriorityPrompt( @@ -961,9 +979,9 @@ public partial class AgentLoopService { if (string.Equals(taskPolicy.TaskType, "docs", StringComparison.OrdinalIgnoreCase)) { - return "[System:NoProgressRecovery] ?숈씪???쎄린 ?몄텧??諛섎났?섏뿀?듬땲?? " + - "?댁젣 臾몄꽌 ?앹꽦/?섏젙 ?꾧뎄(html_create/markdown_create/document_assemble/file_write) 以??섎굹瑜??ㅼ젣濡??몄텧??吏꾪뻾?섏꽭?? " + - "?ㅻ챸留??섏? 留먭퀬 利됱떆 ?꾧뎄瑜??몄텧?섏꽭??"; + return "[System:NoProgressRecovery] 동일한 읽기 도구 호출이 반복되고 있습니다. " + + "지금 즉시 문서 생성/수정 도구(html_create/markdown_create/document_assemble/file_write) 중 하나를 호출하세요. " + + "설명 없이 바로 도구를 호출하세요. 여러 도구를 한 번에 호출할 수 있습니다."; } return BuildFailureNextToolPriorityPrompt( @@ -978,6 +996,8 @@ public partial class AgentLoopService RunState runState, CancellationToken ct) { + // 사용자 취소는 재시도하지 않음 + if (ct.IsCancellationRequested) return false; if (!IsTransientLlmError(ex) || runState.TransientLlmErrorRetries >= 3) return false; @@ -986,7 +1006,7 @@ public partial class AgentLoopService EmitEvent( AgentEventType.Thinking, "", - $"?쇱떆??LLM ?ㅻ쪟濡??ъ떆?꾪빀?덈떎 ({runState.TransientLlmErrorRetries}/3, {delayMs}ms ?€湲?"); + $"일시적 LLM 오류로 {delayMs}ms 후 재시도합니다 ({runState.TransientLlmErrorRetries}/3)"); await Task.Delay(delayMs, ct); return true; } @@ -1092,7 +1112,8 @@ public partial class AgentLoopService IReadOnlyCollection tools, CancellationToken ct, string phaseLabel, - RunState? runState = null) + RunState? runState = null, + bool forceToolCall = false) { var transientRetries = runState?.TransientLlmErrorRetries ?? 0; var contextRecoveryRetries = runState?.ContextRecoveryAttempts ?? 0; @@ -1100,7 +1121,7 @@ public partial class AgentLoopService { try { - return await _llm.SendWithToolsAsync(messages, tools, ct); + return await _llm.SendWithToolsAsync(messages, tools, ct, forceToolCall); } catch (Exception ex) { @@ -1114,10 +1135,13 @@ public partial class AgentLoopService EmitEvent( AgentEventType.Thinking, "", - $"{phaseLabel}: 而⑦뀓?ㅽ듃 珥덇낵瑜?媛먯???蹂듦뎄 ???ъ떆?꾪빀?덈떎 ({contextRecoveryRetries}/2)"); + $"{phaseLabel}: 컨텍스트 한도 초과로 대화를 압축한 후 재시도합니다 ({contextRecoveryRetries}/2)"); continue; } + // 사용자 취소(ct)인 경우 재시도하지 않고 즉시 전파 + if (ct.IsCancellationRequested) throw; + if (IsTransientLlmError(ex) && transientRetries < 3) { transientRetries++; @@ -1127,7 +1151,7 @@ public partial class AgentLoopService EmitEvent( AgentEventType.Thinking, "", - $"{phaseLabel}: ?쇱떆??LLM ?ㅻ쪟濡??ъ떆?꾪빀?덈떎 ({transientRetries}/3, {delayMs}ms ?€湲?"); + $"{phaseLabel}: 일시적 LLM 오류로 {delayMs}ms 후 재시도합니다 ({transientRetries}/3)"); await Task.Delay(delayMs, ct); continue; } @@ -1444,13 +1468,15 @@ public partial class AgentLoopService List toolCalls, List messages, Models.LlmSettings llm, + ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy, AgentContext context, CancellationToken ct) { if (!result.Success || !IsTerminalDocumentTool(call.ToolName) || toolCalls.Count != 1) return (false, false); - var verificationEnabled = AgentTabSettingsResolver.IsPostToolVerificationEnabled(ActiveTab, llm); + var verificationEnabled = executionPolicy.EnablePostToolVerification + && AgentTabSettingsResolver.IsPostToolVerificationEnabled(ActiveTab, llm); var shouldVerify = ShouldRunPostToolVerification( ActiveTab, call.ToolName, @@ -1464,7 +1490,7 @@ public partial class AgentLoopService consumedExtraIteration = true; } - EmitEvent(AgentEventType.Complete, "", "?먯씠?꾪듃 ?묒뾽 ?꾨즺"); + EmitEvent(AgentEventType.Complete, "", "에이전트 작업 완료"); return (true, consumedExtraIteration); } @@ -1473,13 +1499,15 @@ public partial class AgentLoopService ToolResult result, List messages, Models.LlmSettings llm, + ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy, AgentContext context, CancellationToken ct) { if (!result.Success) return false; - var verificationEnabled = AgentTabSettingsResolver.IsPostToolVerificationEnabled(ActiveTab, llm); + var verificationEnabled = executionPolicy.EnablePostToolVerification + && AgentTabSettingsResolver.IsPostToolVerificationEnabled(ActiveTab, llm); var shouldVerify = ShouldRunPostToolVerification( ActiveTab, call.ToolName, @@ -1501,19 +1529,19 @@ public partial class AgentLoopService if (context.DevModeStepApproval && UserDecisionCallback != null) { var decision = await UserDecisionCallback( - $"[DEV] ?꾧뎄 '{call.ToolName}' ?ㅽ뻾???뱀씤?섏떆寃좎뒿?덇퉴?\n{FormatToolCallSummary(call)}", - new List { "?뱀씤", "嫄대꼫?곌린", "以묐떒" }); + $"[DEV] 도구 '{call.ToolName}' 실행을 확인하시겠습니까?\n{FormatToolCallSummary(call)}", + new List { "확인", "건너뛰기", "중단" }); var (devShouldContinue, devTerminalResponse, devToolResultMessage) = EvaluateDevStepDecision(decision); if (!string.IsNullOrEmpty(devTerminalResponse)) { - EmitEvent(AgentEventType.Complete, "", "[DEV] ?ъ슜?먭? ?ㅽ뻾??以묐떒?덉뒿?덈떎"); + EmitEvent(AgentEventType.Complete, "", "[DEV] 사용자가 실행을 중단했습니다"); return (false, devTerminalResponse); } if (devShouldContinue) { messages.Add(LlmService.CreateToolResultMessage( - call.ToolId, call.ToolName, devToolResultMessage ?? "[SKIPPED by developer] ?ъ슜?먭? ???꾧뎄 ?ㅽ뻾??嫄대꼫?곗뿀?듬땲??")); + call.ToolId, call.ToolName, devToolResultMessage ?? "[SKIPPED by developer] 사용자가 이 도구 실행을 건너뛰었습니다.")); return (true, null); } } @@ -1523,18 +1551,18 @@ public partial class AgentLoopService { var decision = await UserDecisionCallback( decisionRequired, - new List { "?뱀씤", "嫄대꼫?곌린", "痍⑥냼" }); + new List { "확인", "건너뛰기", "취소" }); var (scopeShouldContinue, scopeTerminalResponse, scopeToolResultMessage) = EvaluateScopeDecision(decision); if (!string.IsNullOrEmpty(scopeTerminalResponse)) { - EmitEvent(AgentEventType.Complete, "", "?ъ슜?먭? ?묒뾽??痍⑥냼?덉뒿?덈떎"); + EmitEvent(AgentEventType.Complete, "", "사용자가 작업을 취소했습니다"); return (false, scopeTerminalResponse); } if (scopeShouldContinue) { messages.Add(LlmService.CreateToolResultMessage( - call.ToolId, call.ToolName, scopeToolResultMessage ?? "[SKIPPED] ?ъ슜?먭? ???묒뾽??嫄대꼫?곗뿀?듬땲??")); + call.ToolId, call.ToolName, scopeToolResultMessage ?? "[SKIPPED] 사용자가 이 작업을 건너뛰었습니다.")); return (true, null); } } diff --git a/src/AxCopilot/Services/Agent/AgentLoopTransitions.cs b/src/AxCopilot/Services/Agent/AgentLoopTransitions.cs index 90de02a..9409ff5 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopTransitions.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopTransitions.cs @@ -5,12 +5,19 @@ namespace AxCopilot.Services.Agent; public partial class AgentLoopService { private static (bool ShouldRun, List ParallelBatch, List SequentialBatch) - CreateParallelExecutionPlan(bool parallelEnabled, List toolCalls) + CreateParallelExecutionPlan(bool parallelEnabled, List toolCalls, int maxParallelBatch) { if (!parallelEnabled || toolCalls.Count <= 1) return (false, new List(), toolCalls); var (parallelBatch, sequentialBatch) = ClassifyToolCalls(toolCalls); + if (maxParallelBatch > 0 && parallelBatch.Count > maxParallelBatch) + { + var overflow = parallelBatch.Skip(maxParallelBatch).ToList(); + parallelBatch = parallelBatch.Take(maxParallelBatch).ToList(); + sequentialBatch = overflow.Concat(sequentialBatch).ToList(); + } + return (true, parallelBatch, sequentialBatch); } diff --git a/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs b/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs index ddb7ded..317ae9d 100644 --- a/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs +++ b/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs @@ -147,7 +147,19 @@ public sealed class AxAgentExecutionEngine if (session != null) { - session.AppendMessage(tab, assistant, storage); + // session.CurrentConversation이 전달된 conversation과 다른 경우 (새 대화 시작 등), + // session을 통하지 않고 conversation에 직접 추가하여 새 대화가 오염되지 않도록 함. + if (session.CurrentConversation == null || + string.Equals(session.CurrentConversation.Id, conversation.Id, StringComparison.Ordinal)) + { + session.AppendMessage(tab, assistant, storage); + } + else + { + conversation.Messages.Add(assistant); + conversation.UpdatedAt = DateTime.Now; + try { storage?.Save(conversation); } catch { } + } return assistant; } @@ -197,23 +209,7 @@ public sealed class AxAgentExecutionEngine if (!string.IsNullOrWhiteSpace(content)) return content; - var latestEventSummary = conversation.ExecutionEvents? - .Where(evt => !string.IsNullOrWhiteSpace(evt.Summary)) - .OrderByDescending(evt => evt.Timestamp) - .Select(evt => evt.Summary.Trim()) - .FirstOrDefault(); - - if (!string.IsNullOrWhiteSpace(latestEventSummary)) - { - return runTab switch - { - "Cowork" => $"코워크 작업이 완료되었습니다.\n\n{latestEventSummary}", - "Code" => $"코드 작업이 완료되었습니다.\n\n{latestEventSummary}", - _ => latestEventSummary, - }; - } - - return "(빈 응답)"; + return BuildFallbackCompletionMessage(conversation, runTab); } public FinalizedContent FinalizeExecutionContent(string? currentContent, Exception? error = null, bool cancelled = false) @@ -276,23 +272,59 @@ public sealed class AxAgentExecutionEngine if (!string.IsNullOrWhiteSpace(content)) return content; - var latestEventSummary = conversation.ExecutionEvents? - .Where(evt => !string.IsNullOrWhiteSpace(evt.Summary)) + return BuildFallbackCompletionMessage(conversation, runTab); + } + + /// + /// LLM 응답이 비어있을 때 실행 이벤트에서 의미 있는 완료 메시지를 구성합니다. + /// UserPromptSubmit/Paused/Resumed 같은 내부 운영 이벤트는 제외합니다. + /// + private static string BuildFallbackCompletionMessage(ChatConversation conversation, string runTab) + { + static bool IsSignificantEventType(string t) + => !string.Equals(t, "UserPromptSubmit", StringComparison.OrdinalIgnoreCase) + && !string.Equals(t, "Paused", StringComparison.OrdinalIgnoreCase) + && !string.Equals(t, "Resumed", StringComparison.OrdinalIgnoreCase) + && !string.Equals(t, "SessionStart", StringComparison.OrdinalIgnoreCase); + + var completionLine = runTab switch + { + "Cowork" => "코워크 작업이 완료되었습니다.", + "Code" => "코드 작업이 완료되었습니다.", + _ => null, + }; + + // 파일 경로가 있는 이벤트를 최우선으로 — 산출물 파일을 명시적으로 표시 + var artifactEvent = conversation.ExecutionEvents? + .Where(evt => !string.IsNullOrWhiteSpace(evt.FilePath) && IsSignificantEventType(evt.Type)) + .OrderByDescending(evt => evt.Timestamp) + .FirstOrDefault(); + + if (artifactEvent != null) + { + var fileLine = string.IsNullOrWhiteSpace(artifactEvent.Summary) + ? $"생성된 파일: {artifactEvent.FilePath}" + : $"{artifactEvent.Summary}\n경로: {artifactEvent.FilePath}"; + return completionLine != null + ? $"{completionLine}\n\n{fileLine}" + : fileLine; + } + + // 파일 없으면 가장 최근 의미 있는 이벤트 요약 사용 + var latestSummary = conversation.ExecutionEvents? + .Where(evt => !string.IsNullOrWhiteSpace(evt.Summary) && IsSignificantEventType(evt.Type)) .OrderByDescending(evt => evt.Timestamp) .Select(evt => evt.Summary.Trim()) .FirstOrDefault(); - if (!string.IsNullOrWhiteSpace(latestEventSummary)) + if (!string.IsNullOrWhiteSpace(latestSummary)) { - return runTab switch - { - "Cowork" => $"코워크 작업이 완료되었습니다.\n\n{latestEventSummary}", - "Code" => $"코드 작업이 완료되었습니다.\n\n{latestEventSummary}", - _ => latestEventSummary, - }; + return completionLine != null + ? $"{completionLine}\n\n{latestSummary}" + : latestSummary; } - return "(빈 응답)"; + return completionLine ?? "(빈 응답)"; } private static ChatMessage CloneMessage(ChatMessage source) diff --git a/src/AxCopilot/Services/Agent/ChartSkill.cs b/src/AxCopilot/Services/Agent/ChartSkill.cs index c202fa8..d83aa10 100644 --- a/src/AxCopilot/Services/Agent/ChartSkill.cs +++ b/src/AxCopilot/Services/Agent/ChartSkill.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Text; using System.Text.Json; @@ -6,14 +6,14 @@ namespace AxCopilot.Services.Agent; /// /// CSS/SVG 기반 차트를 HTML 파일로 생성하는 스킬. -/// bar, line, pie(donut), radar, area 차트를 지원하며 TemplateService 무드 스타일을 적용합니다. +/// bar, line, pie(donut), radar, area, scatter, heatmap, gauge 차트를 지원하며 TemplateService 무드 스타일을 적용합니다. /// public class ChartSkill : IAgentTool { public string Name => "chart_create"; public string Description => "Create a styled HTML chart document with CSS/SVG-based charts. " + - "Supports chart types: bar, horizontal_bar, stacked_bar, line, area, pie, donut, radar, progress, comparison. " + + "Supports chart types: bar, horizontal_bar, stacked_bar, line, area, pie, donut, radar, progress, comparison, scatter, heatmap, gauge. " + "Multiple charts can be placed in one document using the 'charts' array. " + "Applies design mood from TemplateService (modern, professional, creative, etc.)."; @@ -22,22 +22,29 @@ public class ChartSkill : IAgentTool Properties = new() { ["path"] = new() { Type = "string", Description = "Output file path (.html). Relative to work folder." }, - ["title"] = new() { Type = "string", Description = "Document title" }, + ["title"] = new() { Type = "string", Description = "Document title." }, ["charts"] = new() { Type = "array", Description = "Array of chart objects. Each chart: " + - "{\"type\": \"bar|horizontal_bar|stacked_bar|line|area|pie|donut|radar|progress|comparison\", " + + "{\"type\": \"bar|horizontal_bar|stacked_bar|line|area|pie|donut|radar|progress|comparison|scatter|heatmap|gauge\", " + "\"title\": \"Chart Title\", " + "\"labels\": [\"A\",\"B\",\"C\"], " + "\"datasets\": [{\"name\": \"Series1\", \"values\": [10,20,30], \"color\": \"#4B5EFC\"}], " + - "\"unit\": \"%\"}", + "\"unit\": \"%\", " + + "\"accent_color\": \"#4B5EFC\", " + + "\"show_values\": true}. " + + "For scatter: datasets[].points=[{x,y}]. " + + "For heatmap: y_labels, values (2D array), color_from, color_to. " + + "For gauge: value, min, max, thresholds=[{at, color}].", Items = new ToolProperty { Type = "object" }, }, - ["mood"] = new() { Type = "string", Description = "Design mood: modern, professional, creative, dark, dashboard, etc. Default: dashboard" }, - ["layout"] = new() { Type = "string", Description = "Chart layout: 'single' (one per row) or 'grid' (2-column grid). Default: single" }, + ["mood"] = new() { Type = "string", Description = "Design mood: modern, professional, creative, dark, dashboard, etc. Default: dashboard." }, + ["layout"] = new() { Type = "string", Description = "Chart layout: 'single' (one per row) or 'grid' (2-column grid). Default: single." }, + ["accent_color"] = new() { Type = "string", Description = "Global accent color override for single-color charts." }, + ["show_values"] = new() { Type = "boolean", Description = "Show value labels on bars/points globally. Default: true." }, }, - Required = ["path", "title", "charts"] + Required = ["title", "charts"] }; // 기본 차트 팔레트 @@ -49,10 +56,25 @@ public class ChartSkill : IAgentTool public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { - var path = args.GetProperty("path").GetString() ?? "chart.html"; var title = args.GetProperty("title").GetString() ?? "Chart"; + string path; + if (args.TryGetProperty("path", out var pathEl) && pathEl.ValueKind == JsonValueKind.String + && !string.IsNullOrWhiteSpace(pathEl.GetString())) + { + path = pathEl.GetString()!; + } + else + { + var safe = System.Text.RegularExpressions.Regex.Replace(title, @"[\\/:*?""<>|]", "_").Trim().TrimEnd('.'); + if (safe.Length > 60) safe = safe[..60].TrimEnd(); + path = (string.IsNullOrWhiteSpace(safe) ? "chart" : safe) + ".html"; + } var mood = args.TryGetProperty("mood", out var m) ? m.GetString() ?? "dashboard" : "dashboard"; var layout = args.TryGetProperty("layout", out var l) ? l.GetString() ?? "single" : "single"; + var globalAccent = args.TryGetProperty("accent_color", out var ga) ? ga.GetString() : null; + var globalShowValues = true; + if (args.TryGetProperty("show_values", out var sv)) + globalShowValues = sv.ValueKind != JsonValueKind.False && !string.Equals(sv.GetString(), "false", StringComparison.OrdinalIgnoreCase); var fullPath = FileReadTool.ResolvePath(path, context.WorkFolder); if (context.ActiveTab == "Cowork") fullPath = AgentContext.EnsureTimestampedPath(fullPath); @@ -81,8 +103,8 @@ public class ChartSkill : IAgentTool int chartIdx = 0; foreach (var chartEl in chartsEl.EnumerateArray()) { - var chartHtml = RenderChart(chartEl, chartIdx); - body.AppendLine("
"); + var chartHtml = RenderChart(chartEl, chartIdx, globalAccent, globalShowValues); + body.AppendLine("
"); body.AppendLine(chartHtml); body.AppendLine("
"); chartIdx++; @@ -114,7 +136,7 @@ public class ChartSkill : IAgentTool return ToolResult.Ok($"차트 문서 생성 완료: {fullPath} ({chartCount}개 차트)", fullPath); } - private string RenderChart(JsonElement chart, int idx) + private string RenderChart(JsonElement chart, int idx, string? globalAccent, bool globalShowValues) { var type = chart.TryGetProperty("type", out var t) ? t.GetString() ?? "bar" : "bar"; var chartTitle = chart.TryGetProperty("title", out var ct) ? ct.GetString() ?? "" : ""; @@ -122,17 +144,27 @@ public class ChartSkill : IAgentTool var labels = ParseStringArray(chart, "labels"); var datasets = ParseDatasets(chart); + // Per-chart overrides + var accentColor = chart.TryGetProperty("accent_color", out var ac) ? ac.GetString() : globalAccent; + var showValues = globalShowValues; + if (chart.TryGetProperty("show_values", out var sv)) + showValues = sv.ValueKind != JsonValueKind.False && !string.Equals(sv.GetString(), "false", StringComparison.OrdinalIgnoreCase); + + // Apply accent color to first dataset if single-color context + if (!string.IsNullOrEmpty(accentColor) && datasets.Count > 0 && datasets[0].Color == Palette[0]) + datasets[0] = datasets[0] with { Color = accentColor }; + var sb = new StringBuilder(); if (!string.IsNullOrEmpty(chartTitle)) - sb.AppendLine($"

{Escape(chartTitle)}

"); + sb.AppendLine($"

{Escape(chartTitle)}

"); switch (type) { case "bar": - sb.Append(RenderBarChart(labels, datasets, unit, false)); + sb.Append(RenderBarChart(labels, datasets, unit, false, showValues)); break; case "horizontal_bar": - sb.Append(RenderBarChart(labels, datasets, unit, true)); + sb.Append(RenderBarChart(labels, datasets, unit, true, showValues)); break; case "stacked_bar": sb.Append(RenderStackedBar(labels, datasets, unit)); @@ -154,8 +186,17 @@ public class ChartSkill : IAgentTool case "radar": sb.Append(RenderRadarChart(labels, datasets)); break; + case "scatter": + sb.Append(RenderScatterChart(chart, datasets, unit)); + break; + case "heatmap": + sb.Append(RenderHeatmap(chart, labels)); + break; + case "gauge": + sb.Append(RenderGauge(chart, unit)); + break; default: - sb.Append(RenderBarChart(labels, datasets, unit, false)); + sb.Append(RenderBarChart(labels, datasets, unit, false, showValues)); break; } @@ -173,7 +214,7 @@ public class ChartSkill : IAgentTool // ─── Bar Chart ─────────────────────────────────────────────────────── - private static string RenderBarChart(List labels, List datasets, string unit, bool horizontal) + private static string RenderBarChart(List labels, List datasets, string unit, bool horizontal, bool showValues) { var maxVal = datasets.SelectMany(d => d.Values).DefaultIfEmpty(1).Max(); if (maxVal <= 0) maxVal = 1; @@ -204,7 +245,8 @@ public class ChartSkill : IAgentTool { var val = i < ds.Values.Count ? ds.Values[i] : 0; var pct = (int)(val / maxVal * 100); - sb.AppendLine($"
"); + var valLabel = showValues ? $"{val:G}{unit}" : ""; + sb.AppendLine($"
{valLabel}
"); } sb.AppendLine($"
{Escape(labels[i])}
"); sb.AppendLine("
"); @@ -238,7 +280,7 @@ public class ChartSkill : IAgentTool return sb.ToString(); } - // ─── Line / Area Chart (SVG) ───────────────────────────────────────── + // ─── Line / Area Chart (SVG, smooth bezier) ────────────────────────── private static string RenderLineChart(List labels, List datasets, string unit, bool isArea) { @@ -253,9 +295,10 @@ public class ChartSkill : IAgentTool var n = labels.Count; var sb = new StringBuilder(); - sb.AppendLine($""); + sb.AppendLine($""); + sb.AppendLine($"{Escape(isArea ? "Area Chart" : "Line Chart")}"); - // Y축 그리드 + // Y-axis grid for (int i = 0; i <= 4; i++) { var y = padT + chartH - (chartH * i / 4.0); @@ -264,14 +307,14 @@ public class ChartSkill : IAgentTool sb.AppendLine($"{val:G3}{unit}"); } - // X축 라벨 + // X-axis labels for (int i = 0; i < n; i++) { var x = padL + (n > 1 ? chartW * i / (double)(n - 1) : chartW / 2.0); sb.AppendLine($"{Escape(labels[i])}"); } - // 데이터셋 + // Datasets with smooth cubic bezier foreach (var ds in datasets) { var points = new List<(double x, double y)>(); @@ -282,17 +325,18 @@ public class ChartSkill : IAgentTool points.Add((x, y)); } - var pathData = string.Join(" ", points.Select((p, i) => $"{(i == 0 ? "M" : "L")}{p.x:F1},{p.y:F1}")); + if (points.Count == 0) continue; + + var pathData = BuildSmoothPath(points); if (isArea && points.Count > 1) { - var areaPath = pathData + $" L{points.Last().x:F1},{padT + chartH} L{points.First().x:F1},{padT + chartH} Z"; + var areaPath = pathData + $" L{points.Last().x:F1},{padT + chartH:F0} L{points.First().x:F1},{padT + chartH:F0} Z"; sb.AppendLine($""); } sb.AppendLine($""); - // 데이터 포인트 foreach (var (px, py) in points) sb.AppendLine($""); } @@ -301,6 +345,22 @@ public class ChartSkill : IAgentTool return sb.ToString(); } + private static string BuildSmoothPath(List<(double x, double y)> pts) + { + if (pts.Count == 1) return $"M{pts[0].x:F1},{pts[0].y:F1}"; + var sb = new StringBuilder(); + sb.Append($"M{pts[0].x:F1},{pts[0].y:F1}"); + for (int i = 1; i < pts.Count; i++) + { + var prev = pts[i - 1]; + var curr = pts[i]; + var cpTension = 0.35; + var dx = (curr.x - prev.x) * cpTension; + sb.Append($" C{prev.x + dx:F1},{prev.y:F1} {curr.x - dx:F1},{curr.y:F1} {curr.x:F1},{curr.y:F1}"); + } + return sb.ToString(); + } + // ─── Pie / Donut Chart (SVG) ───────────────────────────────────────── private static string RenderPieChart(List labels, List datasets, bool isDonut) @@ -311,39 +371,54 @@ public class ChartSkill : IAgentTool int cx = 150, cy = 150, r = 120; var sb = new StringBuilder(); - sb.AppendLine($"
"); - sb.AppendLine($""); + sb.AppendLine("
"); + sb.AppendLine($""); + sb.AppendLine($"{Escape(isDonut ? "Donut Chart" : "Pie Chart")}"); double startAngle = -90; + var slices = new List<(double start, double end, string color, string label, double pct)>(); + for (int i = 0; i < Math.Min(values.Count, labels.Count); i++) { var pct = values[i] / total; var angle = pct * 360; var endAngle = startAngle + angle; - - var x1 = cx + r * Math.Cos(startAngle * Math.PI / 180); - var y1 = cy + r * Math.Sin(startAngle * Math.PI / 180); - var x2 = cx + r * Math.Cos(endAngle * Math.PI / 180); - var y2 = cy + r * Math.Sin(endAngle * Math.PI / 180); - var largeArc = angle > 180 ? 1 : 0; - var color = i < Palette.Length ? Palette[i] : Palette[i % Palette.Length]; - sb.AppendLine($""); + slices.Add((startAngle, endAngle, color, labels[i], pct * 100)); startAngle = endAngle; } + foreach (var (start, end, color, label, pct) in slices) + { + var x1 = cx + r * Math.Cos(start * Math.PI / 180); + var y1 = cy + r * Math.Sin(start * Math.PI / 180); + var x2 = cx + r * Math.Cos(end * Math.PI / 180); + var y2 = cy + r * Math.Sin(end * Math.PI / 180); + var largeArc = (end - start) > 180 ? 1 : 0; + sb.AppendLine($"{Escape(label)}: {pct:F1}%"); + + // Percentage label inside slice (skip tiny slices) + if (pct >= 8) + { + var midAngle = (start + end) / 2 * Math.PI / 180; + var lr = isDonut ? r * 0.78 : r * 0.65; + var lx = cx + lr * Math.Cos(midAngle); + var ly = cy + lr * Math.Sin(midAngle); + sb.AppendLine($"{pct:F0}%"); + } + } + if (isDonut) - sb.AppendLine($""); + sb.AppendLine($""); sb.AppendLine(""); - // 범례 + // Legend sb.AppendLine("
"); - for (int i = 0; i < Math.Min(values.Count, labels.Count); i++) + for (int i = 0; i < slices.Count; i++) { - var color = i < Palette.Length ? Palette[i] : Palette[i % Palette.Length]; - var pct = values[i] / total * 100; - sb.AppendLine($"
{Escape(labels[i])} ({pct:F1}%)
"); + var (_, _, color, label, pct) = slices[i]; + sb.AppendLine($"
{Escape(label)} ({pct:F1}%)
"); } sb.AppendLine("
"); @@ -401,9 +476,10 @@ public class ChartSkill : IAgentTool if (n < 3) return "

레이더 차트는 최소 3개 항목이 필요합니다.

"; var sb = new StringBuilder(); - sb.AppendLine($""); + sb.AppendLine($""); + sb.AppendLine("Radar Chart"); - // 그리드 + // Grid for (int level = 1; level <= 4; level++) { var lr = r * level / 4.0; @@ -415,19 +491,19 @@ public class ChartSkill : IAgentTool sb.AppendLine($""); } - // 축선 + 라벨 + // Axes + labels for (int i = 0; i < n; i++) { var angle = (360.0 / n * i - 90) * Math.PI / 180; var x = cx + r * Math.Cos(angle); var y = cy + r * Math.Sin(angle); sb.AppendLine($""); - var lx = cx + (r + 16) * Math.Cos(angle); - var ly = cy + (r + 16) * Math.Sin(angle); + var lx = cx + (r + 18) * Math.Cos(angle); + var ly = cy + (r + 18) * Math.Sin(angle); sb.AppendLine($"{Escape(labels[i])}"); } - // 데이터 + // Data var maxVal = datasets.SelectMany(d => d.Values).DefaultIfEmpty(1).Max(); if (maxVal <= 0) maxVal = 1; foreach (var ds in datasets) @@ -446,8 +522,236 @@ public class ChartSkill : IAgentTool return sb.ToString(); } + // ─── Scatter Chart (SVG) ───────────────────────────────────────────── + + private static string RenderScatterChart(JsonElement chart, List datasets, string unit) + { + // Gather all points across all datasets to determine axis bounds + var allX = new List(); + var allY = new List(); + var dsPoints = new List<(Dataset ds, List<(double x, double y)> pts)>(); + + foreach (var ds in datasets) + { + var pts = new List<(double x, double y)>(); + if (chart.TryGetProperty("datasets", out var dsArr) && dsArr.ValueKind == JsonValueKind.Array) + { + foreach (var dEl in dsArr.EnumerateArray()) + { + var dName = dEl.TryGetProperty("name", out var nn) ? nn.GetString() : null; + if (dName != ds.Name) continue; + if (dEl.TryGetProperty("points", out var ptsEl) && ptsEl.ValueKind == JsonValueKind.Array) + { + foreach (var pEl in ptsEl.EnumerateArray()) + { + if (pEl.TryGetProperty("x", out var px) && pEl.TryGetProperty("y", out var py) + && px.TryGetDouble(out var xd) && py.TryGetDouble(out var yd)) + pts.Add((xd, yd)); + } + } + } + } + dsPoints.Add((ds, pts)); + allX.AddRange(pts.Select(p => p.x)); + allY.AddRange(pts.Select(p => p.y)); + } + + if (allX.Count == 0) return "

Scatter chart requires datasets with points [{x, y}].

"; + + int w = 600, h = 320, padL = 55, padR = 20, padT = 20, padB = 40; + var chartW = w - padL - padR; + var chartH = h - padT - padB; + + var minX = allX.Min(); var maxX = allX.Max(); + var minY = allY.Min(); var maxY = allY.Max(); + if (maxX <= minX) maxX = minX + 1; + if (maxY <= minY) maxY = minY + 1; + + var sb = new StringBuilder(); + sb.AppendLine($""); + sb.AppendLine("Scatter Plot"); + + // Grid + for (int i = 0; i <= 4; i++) + { + var gy = padT + chartH - chartH * i / 4.0; + var gVal = minY + (maxY - minY) * i / 4.0; + sb.AppendLine($""); + sb.AppendLine($"{gVal:G3}{unit}"); + + var gx = padL + chartW * i / 4.0; + var gxVal = minX + (maxX - minX) * i / 4.0; + sb.AppendLine($""); + sb.AppendLine($"{gxVal:G3}"); + } + + // Axis lines + sb.AppendLine($""); + sb.AppendLine($""); + + // Points + foreach (var (ds, pts) in dsPoints) + { + foreach (var (px, py) in pts) + { + var cx = padL + (px - minX) / (maxX - minX) * chartW; + var cy = padT + chartH - (py - minY) / (maxY - minY) * chartH; + sb.AppendLine($"{ds.Name}: ({px:G}, {py:G})"); + } + } + + sb.AppendLine(""); + return sb.ToString(); + } + + // ─── Heatmap (SVG) ─────────────────────────────────────────────────── + + private static string RenderHeatmap(JsonElement chart, List xLabels) + { + var yLabels = ParseStringArray(chart, "y_labels"); + var colorFrom = chart.TryGetProperty("color_from", out var cf) ? cf.GetString() ?? "#EFF6FF" : "#EFF6FF"; + var colorTo = chart.TryGetProperty("color_to", out var ct2) ? ct2.GetString() ?? "#1D4ED8" : "#1D4ED8"; + + // Parse 2D values array + var grid = new List>(); + if (chart.TryGetProperty("values", out var valsEl) && valsEl.ValueKind == JsonValueKind.Array) + { + foreach (var row in valsEl.EnumerateArray()) + { + var r = new List(); + if (row.ValueKind == JsonValueKind.Array) + foreach (var cell in row.EnumerateArray()) + r.Add(cell.TryGetDouble(out var dv) ? dv : 0); + grid.Add(r); + } + } + + if (grid.Count == 0 || xLabels.Count == 0 || yLabels.Count == 0) + return "

Heatmap requires labels, y_labels, and a 2D values array.

"; + + var allCells = grid.SelectMany(r => r).ToList(); + var minV = allCells.Min(); + var maxV = allCells.Max(); + if (maxV <= minV) maxV = minV + 1; + + int cellW = 60, cellH = 36, labelW = 80, labelH = 28, padTop = labelH + 8; + int svgW = labelW + xLabels.Count * cellW + 20; + int svgH = padTop + yLabels.Count * cellH + 10; + + var sb = new StringBuilder(); + sb.AppendLine($""); + sb.AppendLine("Heatmap"); + + // X labels + for (int xi = 0; xi < xLabels.Count; xi++) + { + var lx = labelW + xi * cellW + cellW / 2; + sb.AppendLine($"{Escape(xLabels[xi])}"); + } + + // Rows + for (int yi = 0; yi < Math.Min(yLabels.Count, grid.Count); yi++) + { + var row = grid[yi]; + var ry = padTop + yi * cellH; + + // Y label + sb.AppendLine($"{Escape(yLabels[yi])}"); + + for (int xi = 0; xi < Math.Min(xLabels.Count, row.Count); xi++) + { + var val = row[xi]; + var t = (val - minV) / (maxV - minV); + var cellColor = InterpolateHex(colorFrom, colorTo, t); + var textColor = t > 0.55 ? "white" : "#1F2937"; + var rx = labelW + xi * cellW; + sb.AppendLine($"{Escape(xLabels[xi])} / {Escape(yLabels[yi])}: {val:G}"); + sb.AppendLine($"{val:G}"); + } + } + + sb.AppendLine(""); + return sb.ToString(); + } + + // ─── Gauge (SVG semi-circle) ───────────────────────────────────────── + + private static string RenderGauge(JsonElement chart, string unit) + { + var value = chart.TryGetProperty("value", out var vEl) && vEl.TryGetDouble(out var vd) ? vd : 0; + var min = chart.TryGetProperty("min", out var minEl) && minEl.TryGetDouble(out var mind) ? mind : 0; + var max = chart.TryGetProperty("max", out var maxEl) && maxEl.TryGetDouble(out var maxd) ? maxd : 100; + if (max <= min) max = min + 1; + + // Determine arc color from thresholds (highest threshold below value wins) + var arcColor = Palette[0]; + if (chart.TryGetProperty("thresholds", out var thEl) && thEl.ValueKind == JsonValueKind.Array) + { + var thresholds = thEl.EnumerateArray() + .Select(t => ( + At: t.TryGetProperty("at", out var a) && a.TryGetDouble(out var ad) ? ad : 0, + Color: t.TryGetProperty("color", out var c) ? c.GetString() ?? Palette[0] : Palette[0])) + .OrderBy(t => t.At) + .ToList(); + foreach (var (at, color) in thresholds) + if (value >= at) arcColor = color; + } + + int cx = 150, cy = 155, r = 110; + var ratio = Math.Clamp((value - min) / (max - min), 0, 1); + // Semi-circle: -180deg (left) to 0deg (right), spanning 180 degrees + var endDeg = 180.0 + ratio * 180.0; + + var sb = new StringBuilder(); + sb.AppendLine($""); + sb.AppendLine("Gauge Chart"); + + // Background track arc + sb.AppendLine(ArcPath(cx, cy, r, 180, 360, "#E5E7EB", 18, false)); + // Value arc + if (ratio > 0) + sb.AppendLine(ArcPath(cx, cy, r, 180, 180 + ratio * 180, arcColor, 18, false)); + + // Center value text + sb.AppendLine($"{value:G}{unit}"); + sb.AppendLine($"{min:G} – {max:G}"); + + sb.AppendLine(""); + return sb.ToString(); + } + + private static string ArcPath(int cx, int cy, int r, double startDeg, double endDeg, string color, int strokeWidth, bool fill) + { + var startRad = startDeg * Math.PI / 180; + var endRad = endDeg * Math.PI / 180; + var x1 = cx + r * Math.Cos(startRad); + var y1 = cy + r * Math.Sin(startRad); + var x2 = cx + r * Math.Cos(endRad); + var y2 = cy + r * Math.Sin(endRad); + var largeArc = (endDeg - startDeg) > 180 ? 1 : 0; + var fillAttr = fill ? color : "none"; + return $""; + } + // ─── Helpers ───────────────────────────────────────────────────────── + private static string InterpolateHex(string from, string to, double t) + { + t = Math.Clamp(t, 0, 1); + static (int r, int g, int b) Parse(string hex) + { + hex = hex.TrimStart('#'); + if (hex.Length == 3) hex = $"{hex[0]}{hex[0]}{hex[1]}{hex[1]}{hex[2]}{hex[2]}"; + return (Convert.ToInt32(hex[..2], 16), Convert.ToInt32(hex[2..4], 16), Convert.ToInt32(hex[4..6], 16)); + } + var (r1, g1, b1) = Parse(from); + var (r2, g2, b2) = Parse(to); + var ri = (int)(r1 + (r2 - r1) * t); + var gi = (int)(g1 + (g2 - g1) * t); + var bi = (int)(b1 + (b2 - b1) * t); + return $"#{ri:X2}{gi:X2}{bi:X2}"; + } + private static List ParseStringArray(JsonElement parent, string prop) { if (!parent.TryGetProperty(prop, out var arr) || arr.ValueKind != JsonValueKind.Array) @@ -459,7 +763,6 @@ public class ChartSkill : IAgentTool { if (!chart.TryGetProperty("datasets", out var dsArr) || dsArr.ValueKind != JsonValueKind.Array) { - // datasets 없으면 values 배열에서 단일 데이터셋 생성 if (chart.TryGetProperty("values", out var vals) && vals.ValueKind == JsonValueKind.Array) { return new() @@ -493,10 +796,7 @@ public class ChartSkill : IAgentTool private static string Escape(string s) => s.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """); - private static string FormatSize(long bytes) => - bytes switch { < 1024 => $"{bytes}B", < 1048576 => $"{bytes / 1024.0:F1}KB", _ => $"{bytes / 1048576.0:F1}MB" }; - - private sealed class Dataset + private sealed record Dataset { public string Name { get; init; } = ""; public List Values { get; init; } = new(); @@ -506,15 +806,34 @@ public class ChartSkill : IAgentTool // ─── Chart CSS ─────────────────────────────────────────────────────── private const string ChartCss = @" -/* Vertical Bar Chart */ +/* ── Card ── */ +.card { + background: #fff; + border-radius: 12px; + padding: 20px 24px; + box-shadow: 0 1px 4px rgba(0,0,0,.06), 0 4px 16px rgba(0,0,0,.06); +} + +/* ── Chart title ── */ +.chart-title { margin: 0 0 14px; font-size: 15px; font-weight: 700; color: #111827; } + +/* ── Animations ── */ +@keyframes slideUp { + from { transform: scaleY(0); transform-origin: bottom; opacity: 0; } + to { transform: scaleY(1); transform-origin: bottom; opacity: 1; } +} + +/* ── Vertical Bar Chart ── */ .vbar-chart { margin: 16px 0; } .vbar-bars { display: flex; align-items: flex-end; gap: 8px; height: 220px; padding: 0 8px; border-bottom: 2px solid #E5E7EB; } .vbar-group { flex: 1; display: flex; gap: 3px; align-items: flex-end; position: relative; } -.vbar-bar { flex: 1; min-width: 18px; border-radius: 4px 4px 0 0; transition: opacity 0.2s; cursor: default; } +.vbar-bar-wrap { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; min-width: 18px; animation: slideUp 0.5s ease both; } +.vbar-bar { width: 100%; flex: 1; border-radius: 4px 4px 0 0; transition: opacity 0.2s; cursor: default; } .vbar-bar:hover { opacity: 0.8; } +.vbar-value-label { font-size: 10px; font-weight: 600; color: #374151; margin-bottom: 3px; white-space: nowrap; } .vbar-label { text-align: center; font-size: 11px; color: #6B7280; margin-top: 6px; position: absolute; bottom: -24px; left: 0; right: 0; } -/* Horizontal Bar Chart */ +/* ── Horizontal Bar Chart ── */ .hbar-chart { margin: 12px 0; } .hbar-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; } .hbar-label { min-width: 80px; text-align: right; font-size: 12px; color: #374151; font-weight: 500; } @@ -522,16 +841,20 @@ public class ChartSkill : IAgentTool .hbar-fill { height: 100%; border-radius: 6px; transition: width 0.6s ease; } .hbar-value { min-width: 50px; font-size: 12px; color: #6B7280; font-weight: 600; } -/* Line/Area Chart */ -.line-chart-svg { width: 100%; max-width: 600px; height: auto; } +/* ── Line / Area / Scatter SVG ── */ +.line-chart-svg { width: 100%; max-width: 100%; height: auto; } -/* Legend */ +/* ── Legend ── */ .chart-legend { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 12px; padding-top: 8px; border-top: 1px solid #F3F4F6; } .legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #374151; } -.legend-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; } +.legend-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; flex-shrink: 0; } -/* Pie Legend */ +/* ── Pie Legend ── */ .pie-legend { display: flex; flex-direction: column; gap: 6px; } .pie-legend-item { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #374151; } + +/* ── Progress ── */ +.progress { height: 10px; background: #F3F4F6; border-radius: 99px; overflow: hidden; } +.progress-fill { height: 100%; border-radius: 99px; transition: width 0.6s ease; } "; } diff --git a/src/AxCopilot/Services/Agent/ContextCondenser.cs b/src/AxCopilot/Services/Agent/ContextCondenser.cs index 8c65555..1e95698 100644 --- a/src/AxCopilot/Services/Agent/ContextCondenser.cs +++ b/src/AxCopilot/Services/Agent/ContextCondenser.cs @@ -52,7 +52,10 @@ public static class ContextCondenser _ when key.Contains("gemini-2.0") => 900_000, _ when key.Contains("gemini") => 900_000, _ when key.Contains("gpt-4") => 120_000, // GPT-4 128K - _ => 16_000, // Ollama/vLLM 로컬 모델 기본값 + _ when key.Contains("deepseek") => 128_000, // DeepSeek-V3/R1 128K + _ when key.Contains("qwen") => 32_000, // Qwen 계열 32K + _ when key.Contains("llama") => 32_000, // LLaMA 계열 32K + _ => 32_000, // vLLM/Ollama 알 수 없는 모델 기본값 (보수적으로 32K) }; } @@ -90,8 +93,9 @@ public static class ContextCondenser // 현재 모델의 입력 토큰 한도 var settings = llm.GetCurrentModelInfo(); + // 사용자가 설정한 컨텍스트 크기를 우선 사용. 미설정 시 모델별 기본값 적용. var inputLimit = GetModelInputLimit(settings.service, settings.model); - var effectiveMax = maxOutputTokens > 0 ? Math.Min(inputLimit, maxOutputTokens) : inputLimit; + var effectiveMax = maxOutputTokens > 0 ? maxOutputTokens : inputLimit; var percent = Math.Clamp(triggerPercent, 50, 95); var threshold = (int)(effectiveMax * (percent / 100.0)); // 설정 임계치에서 압축 시작 diff --git a/src/AxCopilot/Services/Agent/CsvSkill.cs b/src/AxCopilot/Services/Agent/CsvSkill.cs index f36cfe0..e3704ab 100644 --- a/src/AxCopilot/Services/Agent/CsvSkill.cs +++ b/src/AxCopilot/Services/Agent/CsvSkill.cs @@ -1,4 +1,5 @@ -using System.IO; +using System.Globalization; +using System.IO; using System.Text; using System.Text.Json; @@ -7,29 +8,81 @@ namespace AxCopilot.Services.Agent; /// /// CSV (.csv) 파일을 생성하는 내장 스킬. /// LLM이 헤더와 데이터 행을 전달하면 CSV 파일을 생성합니다. +/// delimiter, bom, summary, col_types 옵션을 지원합니다. /// public class CsvSkill : IAgentTool { public string Name => "csv_create"; - public string Description => "Create a CSV (.csv) file with structured data. Provide headers and rows as JSON arrays."; + public string Description => + "Create a CSV (.csv) file with structured data. " + + "Supports custom delimiters (comma/tab/semicolon), UTF-8 BOM for Excel compatibility, " + + "optional summary row with counts/sums, and column type hints for proper formatting."; public ToolParameterSchema Parameters => new() { Properties = new() { - ["path"] = new() { Type = "string", Description = "Output file path (.csv). Relative to work folder." }, - ["headers"] = new() { Type = "array", Description = "Column headers as JSON array of strings.", Items = new() { Type = "string" } }, - ["rows"] = new() { Type = "array", Description = "Data rows as JSON array of arrays.", Items = new() { Type = "array", Items = new() { Type = "string" } } }, - ["encoding"] = new() { Type = "string", Description = "File encoding: 'utf-8' (default) or 'euc-kr'." }, + ["path"] = new() { Type = "string", Description = "출력 파일 경로 (.csv). 작업 폴더 기준 상대 경로." }, + ["headers"] = new() { Type = "array", Description = "열 헤더 배열 (문자열 배열).", Items = new() { Type = "string" } }, + ["rows"] = new() { Type = "array", Description = "데이터 행 배열 (배열의 배열).", Items = new() { Type = "array", Items = new() { Type = "string" } } }, + ["encoding"] = new() { Type = "string", Description = "파일 인코딩: 'utf-8' (기본값) 또는 'euc-kr'." }, + ["delimiter"] = new() { Type = "string", Description = "열 구분자: 'comma' (기본값, ','), 'tab' ('\\t'), 'semicolon' (';')." }, + ["bom"] = new() { Type = "boolean", Description = "UTF-8 BOM 추가 여부 (기본값: true). Excel 호환성을 위해 utf-8 인코딩 시 BOM을 추가합니다." }, + ["summary"] = new() { Type = "boolean", Description = "true이면 파일 하단에 수치 열의 합계/개수를 포함한 요약 행을 추가합니다." }, + ["col_types"] = new() + { + Type = "array", + Description = "각 열의 타입 힌트 배열: 'text', 'number', 'date', 'percent'. 생략 시 자동 감지.", + Items = new() { Type = "string" } + }, }, - Required = ["path", "headers", "rows"] + Required = ["headers", "rows"] }; public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { - var path = args.GetProperty("path").GetString() ?? ""; - var encodingName = args.TryGetProperty("encoding", out var enc) ? enc.GetString() ?? "utf-8" : "utf-8"; + // ── 필수 파라미터 검증 ────────────────────────────────────────────── + if (!args.TryGetProperty("headers", out var headersEl) || headersEl.ValueKind != JsonValueKind.Array) + return ToolResult.Fail("필수 파라미터 누락: 'headers' (문자열 배열)가 필요합니다."); + if (!args.TryGetProperty("rows", out var rowsEl) || rowsEl.ValueKind != JsonValueKind.Array) + return ToolResult.Fail("필수 파라미터 누락: 'rows' (배열의 배열)가 필요합니다."); + // path 미제공 시 첫 번째 헤더로 파일명 자동 생성 + string path; + if (args.TryGetProperty("path", out var pathEl) && pathEl.ValueKind == JsonValueKind.String + && !string.IsNullOrWhiteSpace(pathEl.GetString())) + { + path = pathEl.GetString()!; + } + else + { + var hint = headersEl.GetArrayLength() > 0 + ? headersEl[0].GetString() ?? "data" + : "data"; + var safe = System.Text.RegularExpressions.Regex.Replace(hint, @"[\\/:*?""<>|]", "_").Trim().TrimEnd('.'); + if (safe.Length > 40) safe = safe[..40].TrimEnd(); + path = (string.IsNullOrWhiteSpace(safe) ? "data" : safe) + ".csv"; + } + var encodingName = args.TryGetProperty("encoding", out var encEl) ? encEl.GetString() ?? "utf-8" : "utf-8"; + var delimiterKey = args.TryGetProperty("delimiter", out var delimEl) ? delimEl.GetString() ?? "comma" : "comma"; + var useBom = !args.TryGetProperty("bom", out var bomEl) || bomEl.ValueKind != JsonValueKind.False; // default true + var useSummary = args.TryGetProperty("summary", out var sumEl) && sumEl.ValueKind == JsonValueKind.True; + + // ── 구분자 해석 ──────────────────────────────────────────────────── + var delimiter = delimiterKey.ToLowerInvariant() switch + { + "tab" => "\t", + "semicolon" => ";", + _ => "," + }; + + // ── 열 타입 힌트 ─────────────────────────────────────────────────── + var colTypeHints = new List(); + if (args.TryGetProperty("col_types", out var colTypesEl) && colTypesEl.ValueKind == JsonValueKind.Array) + foreach (var ct2 in colTypesEl.EnumerateArray()) + colTypeHints.Add(ct2.GetString()?.ToLowerInvariant() ?? "text"); + + // ── 경로 처리 ────────────────────────────────────────────────────── var fullPath = FileReadTool.ResolvePath(path, context.WorkFolder); if (context.ActiveTab == "Cowork") fullPath = AgentContext.EnsureTimestampedPath(fullPath); if (!fullPath.EndsWith(".csv", StringComparison.OrdinalIgnoreCase)) @@ -43,51 +96,230 @@ public class CsvSkill : IAgentTool try { - var headers = args.GetProperty("headers"); - var rows = args.GetProperty("rows"); + // ── 인코딩 결정 ──────────────────────────────────────────────── + Encoding fileEncoding; + try + { + if (encodingName.Equals("utf-8", StringComparison.OrdinalIgnoreCase) || + encodingName.Equals("utf8", StringComparison.OrdinalIgnoreCase)) + { + fileEncoding = useBom ? new UTF8Encoding(true) : new UTF8Encoding(false); + } + else + { + fileEncoding = Encoding.GetEncoding(encodingName); + } + } + catch + { + fileEncoding = useBom ? new UTF8Encoding(true) : new UTF8Encoding(false); + } + // ── 헤더 수집 ────────────────────────────────────────────────── + var headerList = new List(); + foreach (var h in headersEl.EnumerateArray()) + headerList.Add(h.GetString() ?? ""); + + int colCount = headerList.Count; + + // ── 행 데이터 수집 (원본 문자열) ─────────────────────────────── + var allRows = new List>(); + foreach (var row in rowsEl.EnumerateArray()) + { + var fields = new List(); + if (row.ValueKind == JsonValueKind.Array) + foreach (var cell in row.EnumerateArray()) + fields.Add(cell.ValueKind == JsonValueKind.Null ? "" : cell.ToString()); + allRows.Add(fields); + } + + // ── 열 타입 자동 감지 ────────────────────────────────────────── + var resolvedTypes = ResolveColumnTypes(colTypeHints, colCount, allRows); + + // ── CSV 빌드 ─────────────────────────────────────────────────── + var sb = new StringBuilder(); + + // 헤더 행 + sb.AppendLine(string.Join(delimiter, + headerList.Select(h => EscapeField(h, delimiter)))); + + // 데이터 행 + foreach (var row in allRows) + { + var escaped = new List(); + for (int i = 0; i < colCount; i++) + { + var raw = i < row.Count ? row[i] : ""; + escaped.Add(FormatAndEscape(raw, i < resolvedTypes.Count ? resolvedTypes[i] : "text", delimiter)); + } + sb.AppendLine(string.Join(delimiter, escaped)); + } + + // 요약 행 + if (useSummary && allRows.Count > 0) + { + var summaryFields = BuildSummaryRow(headerList, allRows, resolvedTypes, delimiter); + sb.AppendLine(string.Join(delimiter, summaryFields)); + } + + // ── 파일 쓰기 ────────────────────────────────────────────────── var dir = Path.GetDirectoryName(fullPath); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); - Encoding fileEncoding; - try { fileEncoding = Encoding.GetEncoding(encodingName); } - catch { fileEncoding = new UTF8Encoding(true); } - - var sb = new StringBuilder(); - - // 헤더 - var headerValues = new List(); - foreach (var h in headers.EnumerateArray()) - headerValues.Add(EscapeCsvField(h.GetString() ?? "")); - sb.AppendLine(string.Join(",", headerValues)); - - // 데이터 - int rowCount = 0; - foreach (var row in rows.EnumerateArray()) - { - var fields = new List(); - foreach (var cell in row.EnumerateArray()) - fields.Add(EscapeCsvField(cell.ToString())); - sb.AppendLine(string.Join(",", fields)); - rowCount++; - } - await File.WriteAllTextAsync(fullPath, sb.ToString(), fileEncoding, ct); + var delimLabel = delimiterKey.ToLowerInvariant() switch + { + "tab" => "탭", + "semicolon" => "세미콜론", + _ => "쉼표" + }; + var bomLabel = (useBom && fileEncoding is UTF8Encoding) ? ", BOM 포함" : ""; return ToolResult.Ok( - $"CSV 파일 생성 완료: {fullPath}\n열: {headerValues.Count}, 행: {rowCount}, 인코딩: {encodingName}", + $"CSV 파일 생성 완료: {fullPath}\n열: {colCount}, 행: {allRows.Count}, 구분자: {delimLabel}, 인코딩: {encodingName}{bomLabel}" + + (useSummary ? ", 요약 행 포함" : ""), fullPath); } catch (Exception ex) { - return ToolResult.Fail($"CSV 생성 실패: {ex.Message}"); + return ToolResult.Fail($"CSV 파일 생성 중 오류가 발생했습니다: {ex.Message}"); } } - private static string EscapeCsvField(string field) + // ── 열 타입 해석 ────────────────────────────────────────────────────────── + private static List ResolveColumnTypes(List hints, int colCount, List> rows) { - if (field.Contains(',') || field.Contains('"') || field.Contains('\n') || field.Contains('\r')) + var types = new List(colCount); + for (int i = 0; i < colCount; i++) + { + if (i < hints.Count && hints[i] != "text") + { + types.Add(hints[i]); + continue; + } + + // 자동 감지: 모든 비어있지 않은 값이 숫자인지 확인 + bool allNumeric = true; + bool hasAny = false; + foreach (var row in rows) + { + var val = i < row.Count ? row[i].Trim() : ""; + if (string.IsNullOrEmpty(val)) continue; + hasAny = true; + if (!IsNumericString(val)) { allNumeric = false; break; } + } + + types.Add(hasAny && allNumeric ? "number" : "text"); + } + return types; + } + + // ── 숫자 여부 판별 ──────────────────────────────────────────────────────── + private static bool IsNumericString(string s) + { + // 선행/후행 공백 제거, 선택적 음수 부호, 선택적 소수점, 선택적 퍼센트 부호 허용 + var trimmed = s.Trim().TrimEnd('%'); + return double.TryParse(trimmed, NumberStyles.Any, CultureInfo.InvariantCulture, out _); + } + + // ── 값 포매팅 및 이스케이프 ────────────────────────────────────────────── + private static string FormatAndEscape(string raw, string colType, string delimiter) + { + if (string.IsNullOrEmpty(raw)) + return ""; + + switch (colType) + { + case "number": + { + var trimmed = raw.Trim(); + if (double.TryParse(trimmed, NumberStyles.Any, CultureInfo.InvariantCulture, out var num)) + { + // 정수면 소수점 없이, 소수면 그대로 + var formatted = num == Math.Floor(num) && !trimmed.Contains('.') + ? num.ToString("0", CultureInfo.InvariantCulture) + : num.ToString("G", CultureInfo.InvariantCulture); + // 숫자는 구분자/큰따옴표가 없으면 인용 불필요 + return NeedsQuoting(formatted, delimiter) ? $"\"{formatted.Replace("\"", "\"\"")}\"" : formatted; + } + return EscapeField(raw, delimiter); + } + case "percent": + { + var trimmed = raw.Trim().TrimEnd('%'); + if (double.TryParse(trimmed, NumberStyles.Any, CultureInfo.InvariantCulture, out var pct)) + { + var formatted = pct.ToString("G", CultureInfo.InvariantCulture) + "%"; + return NeedsQuoting(formatted, delimiter) ? $"\"{formatted.Replace("\"", "\"\"")}\"" : formatted; + } + return EscapeField(raw, delimiter); + } + default: + return EscapeField(raw, delimiter); + } + } + + // ── 인용 필요 여부 판별 ─────────────────────────────────────────────────── + private static bool NeedsQuoting(string value, string delimiter) + => value.Contains(delimiter) || value.Contains('"') || + value.Contains('\n') || value.Contains('\r'); + + // ── 텍스트 필드 이스케이프 ─────────────────────────────────────────────── + private static string EscapeField(string field, string delimiter) + { + if (NeedsQuoting(field, delimiter)) return $"\"{field.Replace("\"", "\"\"")}\""; return field; } + + // ── 요약 행 생성 ────────────────────────────────────────────────────────── + private static List BuildSummaryRow( + List headers, List> rows, + List types, string delimiter) + { + int colCount = headers.Count; + var summary = new List(colCount); + + for (int i = 0; i < colCount; i++) + { + var colType = i < types.Count ? types[i] : "text"; + + if (colType == "number" || colType == "percent") + { + // 합계 계산 + double sum = 0; + int count = 0; + bool valid = true; + foreach (var row in rows) + { + var raw = i < row.Count ? row[i].Trim().TrimEnd('%') : ""; + if (string.IsNullOrEmpty(raw)) continue; + if (double.TryParse(raw, NumberStyles.Any, CultureInfo.InvariantCulture, out var v)) + { sum += v; count++; } + else { valid = false; break; } + } + if (valid && count > 0) + { + var sumStr = sum == Math.Floor(sum) + ? sum.ToString("0", CultureInfo.InvariantCulture) + : sum.ToString("G", CultureInfo.InvariantCulture); + summary.Add(colType == "percent" ? sumStr + "%" : sumStr); + } + else + { + summary.Add(EscapeField($"(개수: {rows.Count})", delimiter)); + } + } + else if (i == 0) + { + // 첫 번째 텍스트 열에 "합계" 레이블 + summary.Add(EscapeField("합계", delimiter)); + } + else + { + summary.Add(""); + } + } + return summary; + } } diff --git a/src/AxCopilot/Services/Agent/DocxSkill.cs b/src/AxCopilot/Services/Agent/DocxSkill.cs index e4432fe..0b7ec62 100644 --- a/src/AxCopilot/Services/Agent/DocxSkill.cs +++ b/src/AxCopilot/Services/Agent/DocxSkill.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Text.Json; using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; @@ -8,16 +8,18 @@ namespace AxCopilot.Services.Agent; /// /// Word (.docx) 문서를 생성하는 내장 스킬. -/// 테이블, 텍스트 스타일링, 머리글/바닥글, 페이지 나누기 등 고급 기능을 지원합니다. +/// 테마, 테이블, 콜아웃, 하이라이트박스, 텍스트 스타일링, 머리글/바닥글, 페이지 나누기 등 고급 기능을 지원합니다. /// public class DocxSkill : IAgentTool { public string Name => "docx_create"; public string Description => "Create a rich Word (.docx) document. " + - "Supports: sections with heading+body, tables with optional header styling, " + + "Supports: themes (professional/modern/dark/minimal/creative), " + + "sections with heading+body, tables with optional header styling, " + + "callout blocks (info/warning/tip/danger), highlight boxes, " + "text formatting (bold, italic, color, highlight, shading), " + "headers/footers with page numbers, page breaks between sections, " + - "and numbered/bulleted lists."; + "and numbered/bulleted lists with sub-bullets."; public ToolParameterSchema Parameters => new() { @@ -25,6 +27,7 @@ public class DocxSkill : IAgentTool { ["path"] = new() { Type = "string", Description = "Output file path (.docx). Relative to work folder." }, ["title"] = new() { Type = "string", Description = "Document title (optional)." }, + ["theme"] = new() { Type = "string", Description = "Visual theme: professional (default), modern, dark, minimal, creative." }, ["sections"] = new() { Type = "array", @@ -32,7 +35,9 @@ public class DocxSkill : IAgentTool "• Section: {\"heading\": \"...\", \"body\": \"...\", \"level\": 1|2}\n" + "• Table: {\"type\": \"table\", \"headers\": [\"A\",\"B\"], \"rows\": [[\"1\",\"2\"]], \"style\": \"striped|plain\"}\n" + "• PageBreak: {\"type\": \"pagebreak\"}\n" + - "• List: {\"type\": \"list\", \"style\": \"bullet|number\", \"items\": [\"item1\", \"item2\"]}\n" + + "• List: {\"type\": \"list\", \"style\": \"bullet|number\", \"items\": [\"item1\", \" - sub-item\"]}\n" + + "• Callout: {\"type\": \"callout\", \"style\": \"info|warning|tip|danger\", \"title\": \"...\", \"body\": \"...\"}\n" + + "• HighlightBox: {\"type\": \"highlight_box\", \"text\": \"...\", \"color\": \"blue|green|orange|red\"}\n" + "Body text supports inline formatting: **bold**, *italic*, `code`.", Items = new() { Type = "object" } }, @@ -40,18 +45,77 @@ public class DocxSkill : IAgentTool ["footer"] = new() { Type = "string", Description = "Footer text. Use {page} for page number. Default: 'AX Copilot · {page}' if header is set." }, ["page_numbers"] = new() { Type = "boolean", Description = "Show page numbers in footer. Default: true if header or footer is set." }, }, - Required = ["path", "sections"] + Required = ["sections"] }; + // ═══════════════════════════════════════════════════ + // 테마 색상 정의 + // ═══════════════════════════════════════════════════ + + private record ThemeColors( + string Title, + string H1, + string H2, + string TableHeader, + string BorderColor); + + private static readonly Dictionary Themes = new(StringComparer.OrdinalIgnoreCase) + { + ["professional"] = new("1F3864", "2E74B5", "404040", "2E74B5", "B4C6E7"), + ["modern"] = new("065F46", "059669", "374151", "059669", "A7F3D0"), + ["dark"] = new("1E293B", "6366F1", "94A3B8", "374151", "334155"), + ["minimal"] = new("111827", "374151", "6B7280", "6B7280", "E5E7EB"), + ["creative"] = new("7C3AED", "8B5CF6", "374151", "7C3AED", "EDE9FE"), + }; + + // Callout border/fill by style + private static readonly Dictionary CalloutColors = + new(StringComparer.OrdinalIgnoreCase) + { + ["info"] = ("2563EB", "EFF6FF"), + ["warning"] = ("D97706", "FFFBEB"), + ["danger"] = ("DC2626", "FEF2F2"), + ["tip"] = ("059669", "F0FDF4"), + }; + + // HighlightBox colors (fill, bottom-border) + private static readonly Dictionary HighlightBoxColors = + new(StringComparer.OrdinalIgnoreCase) + { + ["blue"] = ("DBEAFE", "2563EB"), + ["green"] = ("DCFCE7", "059669"), + ["orange"] = ("FEF3C7", "D97706"), + ["red"] = ("FEE2E2", "DC2626"), + }; + + // ═══════════════════════════════════════════════════ + // ExecuteAsync + // ═══════════════════════════════════════════════════ + public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { - var path = args.GetProperty("path").GetString() ?? ""; var title = args.TryGetProperty("title", out var t) ? t.GetString() ?? "" : ""; + string path; + if (args.TryGetProperty("path", out var pathEl) && pathEl.ValueKind == JsonValueKind.String + && !string.IsNullOrWhiteSpace(pathEl.GetString())) + { + path = pathEl.GetString()!; + } + else + { + var safe = System.Text.RegularExpressions.Regex.Replace( + string.IsNullOrWhiteSpace(title) ? "document" : title, @"[\\/:*?""<>|]", "_").Trim().TrimEnd('.'); + if (safe.Length > 60) safe = safe[..60].TrimEnd(); + path = (string.IsNullOrWhiteSpace(safe) ? "document" : safe) + ".docx"; + } var headerText = args.TryGetProperty("header", out var hdr) ? hdr.GetString() : null; var footerText = args.TryGetProperty("footer", out var ftr) ? ftr.GetString() : null; var showPageNumbers = args.TryGetProperty("page_numbers", out var pn) ? pn.GetBoolean() : (headerText != null || footerText != null); + var themeName = args.TryGetProperty("theme", out var th) ? th.GetString() ?? "professional" : "professional"; + var theme = Themes.TryGetValue(themeName, out var tc) ? tc : Themes["professional"]; + var fullPath = FileReadTool.ResolvePath(path, context.WorkFolder); if (context.ActiveTab == "Cowork") fullPath = AgentContext.EnsureTimestampedPath(fullPath); if (!fullPath.EndsWith(".docx", StringComparison.OrdinalIgnoreCase)) @@ -82,13 +146,13 @@ public class DocxSkill : IAgentTool // 제목 if (!string.IsNullOrEmpty(title)) { - body.Append(CreateTitleParagraph(title)); + body.Append(CreateTitleParagraph(title, theme)); // 제목 아래 구분선 body.Append(new Paragraph( new ParagraphProperties { ParagraphBorders = new ParagraphBorders( - new BottomBorder { Val = BorderValues.Single, Size = 6, Color = "4472C4", Space = 1 }), + new BottomBorder { Val = BorderValues.Single, Size = 6, Color = theme.BorderColor, Space = 1 }), SpacingBetweenLines = new SpacingBetweenLines { After = "300" }, })); } @@ -107,7 +171,7 @@ public class DocxSkill : IAgentTool if (blockType == "table") { - body.Append(CreateTable(section)); + body.Append(CreateTable(section, theme)); tableCount++; continue; } @@ -118,13 +182,25 @@ public class DocxSkill : IAgentTool continue; } + if (blockType == "callout") + { + AppendCallout(body, section); + continue; + } + + if (blockType == "highlight_box") + { + body.Append(CreateHighlightBox(section)); + continue; + } + // 일반 섹션 (heading + body) var heading = section.TryGetProperty("heading", out var h) ? h.GetString() ?? "" : ""; var bodyText = section.TryGetProperty("body", out var b) ? b.GetString() ?? "" : ""; var level = section.TryGetProperty("level", out var lv) ? lv.GetInt32() : 1; if (!string.IsNullOrEmpty(heading)) - body.Append(CreateHeadingParagraph(heading, level)); + body.Append(CreateHeadingParagraph(heading, level, theme)); if (!string.IsNullOrEmpty(bodyText)) { @@ -144,6 +220,7 @@ public class DocxSkill : IAgentTool if (tableCount > 0) parts.Add($"테이블: {tableCount}개"); if (headerText != null) parts.Add("머리글"); if (showPageNumbers) parts.Add("페이지번호"); + parts.Add($"테마: {themeName}"); return ToolResult.Ok( $"Word 문서 생성 완료: {fullPath}\n{string.Join(", ", parts)}", @@ -159,7 +236,7 @@ public class DocxSkill : IAgentTool // 제목/소제목/본문 단락 생성 // ═══════════════════════════════════════════════════ - private static Paragraph CreateTitleParagraph(string text) + private static Paragraph CreateTitleParagraph(string text, ThemeColors theme) { var para = new Paragraph(); para.ParagraphProperties = new ParagraphProperties @@ -172,17 +249,18 @@ public class DocxSkill : IAgentTool { Bold = new Bold(), FontSize = new FontSize { Val = "44" }, // 22pt - Color = new Color { Val = "1F3864" }, + Color = new Color { Val = theme.Title }, + RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, }; para.Append(run); return para; } - private static Paragraph CreateHeadingParagraph(string text, int level) + private static Paragraph CreateHeadingParagraph(string text, int level, ThemeColors theme) { var para = new Paragraph(); var fontSize = level <= 1 ? "32" : "26"; // 16pt / 13pt - var color = level <= 1 ? "2E74B5" : "404040"; + var color = level <= 1 ? theme.H1 : theme.H2; para.ParagraphProperties = new ParagraphProperties { @@ -193,7 +271,7 @@ public class DocxSkill : IAgentTool if (level <= 1) { para.ParagraphProperties.ParagraphBorders = new ParagraphBorders( - new BottomBorder { Val = BorderValues.Single, Size = 4, Color = "B4C6E7", Space = 1 }); + new BottomBorder { Val = BorderValues.Single, Size = 4, Color = theme.BorderColor, Space = 1 }); } var run = new Run(new Text(text)); @@ -202,6 +280,7 @@ public class DocxSkill : IAgentTool Bold = new Bold(), FontSize = new FontSize { Val = fontSize }, Color = new Color { Val = color }, + RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, }; para.Append(run); return para; @@ -252,7 +331,7 @@ public class DocxSkill : IAgentTool { var run = CreateRun(match.Groups[3].Value); run.RunProperties ??= new RunProperties(); - run.RunProperties.RunFonts = new RunFonts { Ascii = "Consolas", HighAnsi = "Consolas" }; + run.RunProperties.RunFonts = new RunFonts { Ascii = "Consolas", HighAnsi = "Consolas", EastAsia = "맑은 고딕" }; run.RunProperties.FontSize = new FontSize { Val = "20" }; run.RunProperties.Shading = new Shading { @@ -281,6 +360,7 @@ public class DocxSkill : IAgentTool run.RunProperties = new RunProperties { FontSize = new FontSize { Val = "22" }, // 11pt + RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, }; return run; } @@ -289,7 +369,7 @@ public class DocxSkill : IAgentTool // 테이블 생성 // ═══════════════════════════════════════════════════ - private static Table CreateTable(JsonElement section) + private static Table CreateTable(JsonElement section, ThemeColors theme) { var headers = section.TryGetProperty("headers", out var hArr) ? hArr : default; var rows = section.TryGetProperty("rows", out var rArr) ? rArr : default; @@ -320,7 +400,7 @@ public class DocxSkill : IAgentTool var cell = new TableCell(); cell.TableCellProperties = new TableCellProperties { - Shading = new Shading { Val = ShadingPatternValues.Clear, Fill = "2E74B5", Color = "auto" }, + Shading = new Shading { Val = ShadingPatternValues.Clear, Fill = theme.TableHeader, Color = "auto" }, TableCellVerticalAlignment = new TableCellVerticalAlignment { Val = TableVerticalAlignmentValues.Center }, }; var para = new Paragraph(new Run(new Text(h.GetString() ?? "")) @@ -330,6 +410,7 @@ public class DocxSkill : IAgentTool Bold = new Bold(), FontSize = new FontSize { Val = "20" }, Color = new Color { Val = "FFFFFF" }, + RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, } }); para.ParagraphProperties = new ParagraphProperties @@ -364,7 +445,11 @@ public class DocxSkill : IAgentTool var para = new Paragraph(new Run(new Text(cellVal.ToString()) { Space = SpaceProcessingModeValues.Preserve }) { - RunProperties = new RunProperties { FontSize = new FontSize { Val = "20" } } + RunProperties = new RunProperties + { + FontSize = new FontSize { Val = "20" }, + RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, + } }); para.ParagraphProperties = new ParagraphProperties { @@ -382,7 +467,7 @@ public class DocxSkill : IAgentTool } // ═══════════════════════════════════════════════════ - // 리스트 (번호/불릿) + // 리스트 (번호/불릿) — 서브불릿 지원 // ═══════════════════════════════════════════════════ private static void AppendList(Body body, JsonElement section) @@ -395,13 +480,21 @@ public class DocxSkill : IAgentTool int idx = 1; foreach (var item in items.EnumerateArray()) { - var text = item.GetString() ?? item.ToString(); - var prefix = listStyle == "number" ? $"{idx}. " : "• "; + var rawText = item.GetString() ?? item.ToString(); + + // 서브불릿 감지: 앞에 공백/탭 또는 "- " 시작 + bool isSub = rawText.StartsWith(" ") || rawText.StartsWith("\t") || rawText.TrimStart().StartsWith("- "); + var text = isSub ? rawText.TrimStart().TrimStart('-').TrimStart() : rawText; + var indentLeft = isSub ? "1440" : "720"; // 서브: 1 inch, 일반: 0.5 inch + + var prefix = isSub + ? "◦ " // 서브불릿 기호 + : (listStyle == "number" ? $"{idx}. " : "• "); var para = new Paragraph(); para.ParagraphProperties = new ParagraphProperties { - Indentation = new Indentation { Left = "720" }, // 0.5 inch + Indentation = new Indentation { Left = indentLeft }, SpacingBetweenLines = new SpacingBetweenLines { Line = "320" }, }; @@ -409,19 +502,167 @@ public class DocxSkill : IAgentTool prefixRun.RunProperties = new RunProperties { FontSize = new FontSize { Val = "22" }, - Bold = listStyle == "number" ? new Bold() : null, + Bold = (listStyle == "number" && !isSub) ? new Bold() : null, + RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, }; para.Append(prefixRun); var textRun = new Run(new Text(text) { Space = SpaceProcessingModeValues.Preserve }); - textRun.RunProperties = new RunProperties { FontSize = new FontSize { Val = "22" } }; + textRun.RunProperties = new RunProperties + { + FontSize = new FontSize { Val = "22" }, + RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, + }; para.Append(textRun); body.Append(para); - idx++; + if (!isSub) idx++; } } + // ═══════════════════════════════════════════════════ + // 콜아웃 블록 + // ═══════════════════════════════════════════════════ + + private static void AppendCallout(Body body, JsonElement section) + { + var style = section.TryGetProperty("style", out var sv) ? sv.GetString() ?? "info" : "info"; + var calloutTitle = section.TryGetProperty("title", out var tv) ? tv.GetString() ?? "" : ""; + var calloutBody = section.TryGetProperty("body", out var bv) ? bv.GetString() ?? "" : ""; + + var (borderColor, fillColor) = CalloutColors.TryGetValue(style, out var cc) + ? cc + : CalloutColors["info"]; + + // 제목 단락 (있을 경우) + if (!string.IsNullOrEmpty(calloutTitle)) + { + var titlePara = new Paragraph(); + titlePara.ParagraphProperties = new ParagraphProperties + { + ParagraphBorders = new ParagraphBorders( + new LeftBorder { Val = BorderValues.Single, Size = 16, Color = borderColor, Space = 4 }), + Shading = new Shading { Val = ShadingPatternValues.Clear, Fill = fillColor, Color = "auto" }, + Indentation = new Indentation { Left = "360", Right = "360" }, + SpacingBetweenLines = new SpacingBetweenLines { Before = "60", After = "60" }, + }; + var titleRun = new Run(new Text(calloutTitle) { Space = SpaceProcessingModeValues.Preserve }); + titleRun.RunProperties = new RunProperties + { + Bold = new Bold(), + FontSize = new FontSize { Val = "22" }, + Color = new Color { Val = borderColor }, + RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, + }; + titlePara.Append(titleRun); + body.Append(titlePara); + } + + // 본문 단락 + if (!string.IsNullOrEmpty(calloutBody)) + { + foreach (var line in calloutBody.Split('\n')) + { + var bodyPara = new Paragraph(); + bodyPara.ParagraphProperties = new ParagraphProperties + { + ParagraphBorders = new ParagraphBorders( + new LeftBorder { Val = BorderValues.Single, Size = 16, Color = borderColor, Space = 4 }), + Shading = new Shading { Val = ShadingPatternValues.Clear, Fill = fillColor, Color = "auto" }, + Indentation = new Indentation { Left = "360", Right = "360" }, + SpacingBetweenLines = new SpacingBetweenLines { Before = "40", After = "40" }, + }; + AppendFormattedRunsWithFont(bodyPara, line); + body.Append(bodyPara); + } + } + + // 콜아웃 뒤 여백 단락 + body.Append(new Paragraph(new ParagraphProperties( + new SpacingBetweenLines { After = "120" }))); + } + + // ═══════════════════════════════════════════════════ + // 하이라이트 박스 + // ═══════════════════════════════════════════════════ + + private static Paragraph CreateHighlightBox(JsonElement section) + { + var text = section.TryGetProperty("text", out var tv) ? tv.GetString() ?? "" : ""; + var color = section.TryGetProperty("color", out var cv) ? cv.GetString() ?? "blue" : "blue"; + + var (fillColor, borderColor) = HighlightBoxColors.TryGetValue(color, out var hc) + ? hc + : HighlightBoxColors["blue"]; + + var para = new Paragraph(); + para.ParagraphProperties = new ParagraphProperties + { + ParagraphBorders = new ParagraphBorders( + new BottomBorder { Val = BorderValues.Single, Size = 8, Color = borderColor, Space = 1 }), + Shading = new Shading { Val = ShadingPatternValues.Clear, Fill = fillColor, Color = "auto" }, + Indentation = new Indentation { Left = "240", Right = "240" }, + SpacingBetweenLines = new SpacingBetweenLines { Before = "80", After = "80" }, + }; + AppendFormattedRunsWithFont(para, text); + return para; + } + + // ═══════════════════════════════════════════════════ + // 서식 있는 런 추가 (폰트 포함) — 콜아웃/하이라이트 박스용 + // ═══════════════════════════════════════════════════ + + /// AppendFormattedRuns와 동일하나 모든 일반 런에 한/영 폰트를 적용합니다. + private static void AppendFormattedRunsWithFont(Paragraph para, string text) + { + var regex = new System.Text.RegularExpressions.Regex( + @"\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`"); + int lastIndex = 0; + + foreach (System.Text.RegularExpressions.Match match in regex.Matches(text)) + { + if (match.Index > lastIndex) + para.Append(CreateRun(text[lastIndex..match.Index])); + + if (match.Groups[1].Success) + { + var run = CreateRun(match.Groups[1].Value); + run.RunProperties ??= new RunProperties(); + run.RunProperties.Bold = new Bold(); + para.Append(run); + } + else if (match.Groups[2].Success) + { + var run = CreateRun(match.Groups[2].Value); + run.RunProperties ??= new RunProperties(); + run.RunProperties.Italic = new Italic(); + para.Append(run); + } + else if (match.Groups[3].Success) + { + var run = CreateRun(match.Groups[3].Value); + run.RunProperties ??= new RunProperties(); + run.RunProperties.RunFonts = new RunFonts { Ascii = "Consolas", HighAnsi = "Consolas", EastAsia = "맑은 고딕" }; + run.RunProperties.FontSize = new FontSize { Val = "20" }; + run.RunProperties.Shading = new Shading + { + Val = ShadingPatternValues.Clear, + Fill = "F2F2F2", + Color = "auto" + }; + para.Append(run); + } + + lastIndex = match.Index + match.Length; + } + + if (lastIndex < text.Length) + para.Append(CreateRun(text[lastIndex..])); + + if (lastIndex == 0 && text.Length == 0) + para.Append(CreateRun("")); + } + // ═══════════════════════════════════════════════════ // 페이지 나누기 // ═══════════════════════════════════════════════════ @@ -452,6 +693,7 @@ public class DocxSkill : IAgentTool { FontSize = new FontSize { Val = "18" }, // 9pt Color = new Color { Val = "808080" }, + RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, } }); para.ParagraphProperties = new ParagraphProperties @@ -524,6 +766,7 @@ public class DocxSkill : IAgentTool { FontSize = new FontSize { Val = "16" }, Color = new Color { Val = "999999" }, + RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, } }; @@ -534,6 +777,7 @@ public class DocxSkill : IAgentTool { FontSize = new FontSize { Val = "16" }, Color = new Color { Val = "999999" }, + RunFonts = new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri", EastAsia = "맑은 고딕" }, }; run.Append(new FieldChar { FieldCharType = FieldCharValues.Begin }); run.Append(new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve }); diff --git a/src/AxCopilot/Services/Agent/ExcelSkill.cs b/src/AxCopilot/Services/Agent/ExcelSkill.cs index ebb84c3..497de60 100644 --- a/src/AxCopilot/Services/Agent/ExcelSkill.cs +++ b/src/AxCopilot/Services/Agent/ExcelSkill.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Text.Json; using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; @@ -8,15 +8,16 @@ namespace AxCopilot.Services.Agent; /// /// Excel (.xlsx) 문서를 생성하는 내장 스킬. -/// 셀 서식(색상/테두리/볼드), 수식, 열 너비, 셀 병합, 틀 고정 등을 지원합니다. +/// 셀 서식(색상/테두리/볼드), 수식, 열 너비, 셀 병합, 틀 고정, 테마, 숫자 형식, 멀티시트, 정렬 등을 지원합니다. /// public class ExcelSkill : IAgentTool { public string Name => "excel_create"; public string Description => "Create a styled Excel (.xlsx) file. " + - "Supports: header styling (bold white text on blue background), " + + "Supports: header styling (bold white text on colored background), " + "striped rows, column auto-width, formulas (=SUM, =AVERAGE, etc), " + - "cell merge, freeze panes (freeze header row), and number formatting."; + "cell merge, freeze panes (freeze header row), number formatting, " + + "themes (professional/modern/dark/minimal), column alignments, and multi-sheet workbooks."; public ToolParameterSchema Parameters => new() { @@ -26,22 +27,77 @@ public class ExcelSkill : IAgentTool ["sheet_name"] = new() { Type = "string", Description = "Sheet name. Default: 'Sheet1'." }, ["headers"] = new() { Type = "array", Description = "Column headers as JSON array of strings.", Items = new() { Type = "string" } }, ["rows"] = new() { Type = "array", Description = "Data rows as JSON array of arrays. Use string starting with '=' for formulas (e.g. '=SUM(A2:A10)').", Items = new() { Type = "array", Items = new() { Type = "string" } } }, - ["style"] = new() { Type = "string", Description = "Table style: 'styled' (blue header, striped rows, borders) or 'plain'. Default: 'styled'" }, + ["style"] = new() { Type = "string", Description = "Table style: 'styled' (colored header, striped rows, borders) or 'plain'. Default: 'styled'" }, + ["theme"] = new() { Type = "string", Description = "Color theme: 'professional' (blue), 'modern' (green), 'dark' (slate), 'minimal' (gray). Default: 'professional'." }, ["col_widths"] = new() { Type = "array", Description = "Column widths as JSON array of numbers (in characters). e.g. [15, 10, 20]. Auto-fit if omitted.", Items = new() { Type = "number" } }, ["freeze_header"] = new() { Type = "boolean", Description = "Freeze the header row. Default: true for styled." }, ["merges"] = new() { Type = "array", Description = "Cell merge ranges. e.g. [\"A1:C1\", \"D5:D8\"]", Items = new() { Type = "string" } }, ["summary_row"] = new() { Type = "object", Description = "Auto-generate summary row. {\"label\": \"합계\", \"columns\": {\"B\": \"SUM\", \"C\": \"AVERAGE\"}}. Adds formulas at bottom." }, + ["number_formats"] = new() { Type = "array", Description = "Number format per column index. Supported: 'currency' (#,##0\"원\"), 'percent' (0.00%), 'decimal' (#,##0.00), 'integer' (#,##0), 'date' (yyyy-mm-dd), or any custom Excel format string. e.g. [\"text\",\"integer\",\"currency\",\"percent\"]", Items = new() { Type = "string" } }, + ["col_alignments"] = new() { Type = "array", Description = "Horizontal alignment per column: 'left', 'center', 'right'. Headers always center-aligned.", Items = new() { Type = "string" } }, + ["sheets"] = new() { Type = "array", Description = "Multi-sheet mode: array of sheet objects [{name, headers, rows, style?, theme?, col_widths?, freeze_header?, number_formats?, col_alignments?, merges?, summary_row?}]. When present, overrides top-level headers/rows/sheet_name.", Items = new() { Type = "object" } }, }, - Required = ["path", "headers", "rows"] + Required = [] }; + // ═══════════════════════════════════════════════════ + // Theme definitions + // ═══════════════════════════════════════════════════ + + private record ThemeColors(string HeaderBg, string HeaderFg, string StripeBg); + + private static ThemeColors GetTheme(string? theme) => theme?.ToLower() switch + { + "modern" => new("FF065F46", "FFFFFFFF", "FFF0FDF4"), + "dark" => new("FF1E293B", "FFFFFFFF", "FFF1F5F9"), + "minimal"=> new("FF374151", "FFFFFFFF", "FFF9FAFB"), + _ => new("FF1F4E79", "FFFFFFFF", "FFF2F7FC"), // professional (default) + }; + + // ═══════════════════════════════════════════════════ + // Number format resolution + // ═══════════════════════════════════════════════════ + + // Returns (isBuiltIn, numFmtId, formatString) + // Built-in IDs we use: 0=General, 164+ = custom + private static (bool isBuiltIn, uint numFmtId, string? formatCode) ResolveNumberFormat(string? fmt) + { + if (string.IsNullOrEmpty(fmt) || fmt == "text") + return (true, 0, null); // General / no special format + + return fmt.ToLower() switch + { + "percent" => (true, 10, null), // 0.00% (built-in id 10) + "decimal" => (true, 4, null), // #,##0.00 (built-in id 4) + "integer" => (true, 3, null), // #,##0 (built-in id 3) + "date" => (true, 14, null), // m/d/yyyy (built-in id 14) + "currency" => (false, 0, "#,##0\"원\""), // custom + _ => (false, 0, fmt), // user-supplied custom string + }; + } + + // ═══════════════════════════════════════════════════ + // Execute + // ═══════════════════════════════════════════════════ + public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { - var path = args.GetProperty("path").GetString() ?? ""; - var sheetName = args.TryGetProperty("sheet_name", out var sn) ? sn.GetString() ?? "Sheet1" : "Sheet1"; - var tableStyle = args.TryGetProperty("style", out var st) ? st.GetString() ?? "styled" : "styled"; - var isStyled = tableStyle != "plain"; - var freezeHeader = args.TryGetProperty("freeze_header", out var fh) ? fh.GetBoolean() : isStyled; + // path 미제공 시 title 또는 sheet_name에서 자동 생성 + string path; + if (args.TryGetProperty("path", out var pathEl) && pathEl.ValueKind == JsonValueKind.String + && !string.IsNullOrWhiteSpace(pathEl.GetString())) + { + path = pathEl.GetString()!; + } + else + { + var hint = (args.TryGetProperty("title", out var tEl) ? tEl.GetString() : null) + ?? (args.TryGetProperty("sheet_name", out var snEl) ? snEl.GetString() : null) + ?? "workbook"; + var safe = System.Text.RegularExpressions.Regex.Replace(hint, @"[\\/:*?""<>|]", "_").Trim().TrimEnd('.'); + if (safe.Length > 60) safe = safe[..60].TrimEnd(); + path = (string.IsNullOrWhiteSpace(safe) ? "workbook" : safe) + ".xlsx"; + } var fullPath = FileReadTool.ResolvePath(path, context.WorkFolder); if (context.ActiveTab == "Cowork") fullPath = AgentContext.EnsureTimestampedPath(fullPath); @@ -56,157 +112,18 @@ public class ExcelSkill : IAgentTool try { - var headers = args.GetProperty("headers"); - var rows = args.GetProperty("rows"); - var dir = Path.GetDirectoryName(fullPath); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); - using var spreadsheet = SpreadsheetDocument.Create(fullPath, SpreadsheetDocumentType.Workbook); + // Determine if we are in multi-sheet mode + var multiSheetMode = args.TryGetProperty("sheets", out var sheetsArr) + && sheetsArr.ValueKind == JsonValueKind.Array + && sheetsArr.GetArrayLength() > 0; - var workbookPart = spreadsheet.AddWorkbookPart(); - workbookPart.Workbook = new Workbook(); - - // Stylesheet 추가 (서식용) - var stylesPart = workbookPart.AddNewPart(); - stylesPart.Stylesheet = CreateStylesheet(isStyled); - stylesPart.Stylesheet.Save(); - - var worksheetPart = workbookPart.AddNewPart(); - worksheetPart.Worksheet = new Worksheet(); - - // 열 너비 설정 - var colCount = headers.GetArrayLength(); - var columns = CreateColumns(args, colCount); - if (columns != null) - worksheetPart.Worksheet.Append(columns); - - var sheetData = new SheetData(); - worksheetPart.Worksheet.Append(sheetData); - - var sheets = workbookPart.Workbook.AppendChild(new Sheets()); - sheets.Append(new Sheet - { - Id = workbookPart.GetIdOfPart(worksheetPart), - SheetId = 1, - Name = sheetName, - }); - - // 헤더 행 (styleIndex 1 = 볼드 흰색 + 파란배경) - var headerRow = new Row { RowIndex = 1 }; - int colIdx = 0; - foreach (var h in headers.EnumerateArray()) - { - var cellRef = GetCellReference(colIdx, 0); - var cell = new Cell - { - CellReference = cellRef, - DataType = CellValues.String, - CellValue = new CellValue(h.GetString() ?? ""), - StyleIndex = isStyled ? (uint)1 : 0, - }; - headerRow.Append(cell); - colIdx++; - } - sheetData.Append(headerRow); - - // 데이터 행 - int rowCount = 0; - uint rowNum = 2; - foreach (var row in rows.EnumerateArray()) - { - var dataRow = new Row { RowIndex = rowNum }; - int ci = 0; - foreach (var cellVal in row.EnumerateArray()) - { - var cellRef = GetCellReference(ci, (int)rowNum - 1); - var cell = new Cell { CellReference = cellRef }; - - // striped 스타일: 짝수행 - if (isStyled && rowCount % 2 == 0) - cell.StyleIndex = 2; // 연한 파란 배경 - - var strVal = cellVal.ToString(); - - // 수식 (=으로 시작) - if (strVal.StartsWith('=')) - { - cell.CellFormula = new CellFormula(strVal); - cell.DataType = null; // 수식은 DataType 없음 - } - else if (cellVal.ValueKind == JsonValueKind.Number) - { - cell.DataType = CellValues.Number; - cell.CellValue = new CellValue(cellVal.GetDouble().ToString()); - } - else - { - cell.DataType = CellValues.String; - cell.CellValue = new CellValue(strVal); - } - - dataRow.Append(cell); - ci++; - } - sheetData.Append(dataRow); - rowCount++; - rowNum++; - } - - // 요약 행 (summary_row) - if (args.TryGetProperty("summary_row", out var summary)) - AddSummaryRow(sheetData, summary, rowNum, colCount, rowCount, isStyled); - - // 셀 병합 - if (args.TryGetProperty("merges", out var merges) && merges.ValueKind == JsonValueKind.Array) - { - var mergeCells = new MergeCells(); - foreach (var merge in merges.EnumerateArray()) - { - var range = merge.GetString(); - if (!string.IsNullOrEmpty(range)) - mergeCells.Append(new MergeCell { Reference = range }); - } - if (mergeCells.HasChildren) - worksheetPart.Worksheet.InsertAfter(mergeCells, sheetData); - } - - // 틀 고정 (헤더 행) - if (freezeHeader) - { - var sheetViews = new SheetViews(new SheetView( - new Pane - { - VerticalSplit = 1, - TopLeftCell = "A2", - ActivePane = PaneValues.BottomLeft, - State = PaneStateValues.Frozen - }, - new Selection - { - Pane = PaneValues.BottomLeft, - ActiveCell = "A2", - SequenceOfReferences = new ListValue { InnerText = "A2" } - }) - { TabSelected = true, WorkbookViewId = 0 }); - - var insertBefore = (OpenXmlElement?)worksheetPart.Worksheet.GetFirstChild() - ?? worksheetPart.Worksheet.GetFirstChild(); - worksheetPart.Worksheet.InsertBefore(sheetViews, insertBefore); - } - - workbookPart.Workbook.Save(); - - var features = new List(); - if (isStyled) features.Add("스타일 적용"); - if (freezeHeader) features.Add("틀 고정"); - if (args.TryGetProperty("merges", out _)) features.Add("셀 병합"); - if (args.TryGetProperty("summary_row", out _)) features.Add("요약행"); - var featureStr = features.Count > 0 ? $" [{string.Join(", ", features)}]" : ""; - - return ToolResult.Ok( - $"Excel 파일 생성 완료: {fullPath}\n시트: {sheetName}, 열: {colCount}, 행: {rowCount}{featureStr}", - fullPath); + if (multiSheetMode) + return GenerateMultiSheetWorkbook(args, sheetsArr, fullPath); + else + return GenerateSingleSheetWorkbook(args, fullPath); } catch (Exception ex) { @@ -215,26 +132,383 @@ public class ExcelSkill : IAgentTool } // ═══════════════════════════════════════════════════ - // Stylesheet (셀 서식) + // Single-sheet workbook (backward-compatible path) // ═══════════════════════════════════════════════════ - private static Stylesheet CreateStylesheet(bool isStyled) + private static ToolResult GenerateSingleSheetWorkbook(JsonElement args, string fullPath) + { + if (!args.TryGetProperty("headers", out var headers) || headers.ValueKind != JsonValueKind.Array) + return ToolResult.Fail("필수 파라미터 누락: 'headers'"); + if (!args.TryGetProperty("rows", out var rows) || rows.ValueKind != JsonValueKind.Array) + return ToolResult.Fail("필수 파라미터 누락: 'rows'"); + + var sheetName = args.TryGetProperty("sheet_name", out var sn) ? sn.GetString() ?? "Sheet1" : "Sheet1"; + var tableStyle = args.TryGetProperty("style", out var st) ? st.GetString() ?? "styled" : "styled"; + var themeName = args.TryGetProperty("theme", out var th) ? th.GetString() : null; + var isStyled = tableStyle != "plain"; + var freezeHeader= args.TryGetProperty("freeze_header", out var fh) ? fh.GetBoolean() : isStyled; + + var theme = GetTheme(themeName); + var numFmts = ParseNumberFormats(args, "number_formats"); + var alignments = ParseAlignments(args, "col_alignments"); + + // Collect custom number formats for stylesheet + var customFmts = CollectCustomFormats(numFmts); + + using var spreadsheet = SpreadsheetDocument.Create(fullPath, SpreadsheetDocumentType.Workbook); + var workbookPart = spreadsheet.AddWorkbookPart(); + workbookPart.Workbook = new Workbook(); + + var stylesPart = workbookPart.AddNewPart(); + stylesPart.Stylesheet = CreateStylesheet(isStyled, theme, customFmts, numFmts, alignments); + stylesPart.Stylesheet.Save(); + + var worksheetPart = workbookPart.AddNewPart(); + worksheetPart.Worksheet = new Worksheet(); + + var colCount = headers.GetArrayLength(); + + var summaryArg = args.TryGetProperty("summary_row", out var sumEl) ? sumEl : default; + var mergesArg = args.TryGetProperty("merges", out var mergeEl) ? mergeEl : default; + + var rowCount = WriteSheetContent(worksheetPart, args, sheetName, headers, rows, + isStyled, freezeHeader, theme, numFmts, alignments, customFmts, + summaryArg, mergesArg, colCount); + + var wbSheets = workbookPart.Workbook.AppendChild(new Sheets()); + wbSheets.Append(new Sheet + { + Id = workbookPart.GetIdOfPart(worksheetPart), + SheetId = 1, + Name = sheetName, + }); + + workbookPart.Workbook.Save(); + + var features = BuildFeatureList(isStyled, freezeHeader, + mergesArg.ValueKind == JsonValueKind.Array, + summaryArg.ValueKind == JsonValueKind.Object, + numFmts.Count > 0, alignments.Count > 0, themeName); + + return ToolResult.Ok( + $"Excel 파일 생성 완료: {fullPath}\n시트: {sheetName}, 열: {colCount}, 행: {rowCount}{features}", + fullPath); + } + + // ═══════════════════════════════════════════════════ + // Multi-sheet workbook + // ═══════════════════════════════════════════════════ + + private static ToolResult GenerateMultiSheetWorkbook(JsonElement args, JsonElement sheetsArr, string fullPath) + { + using var spreadsheet = SpreadsheetDocument.Create(fullPath, SpreadsheetDocumentType.Workbook); + var workbookPart = spreadsheet.AddWorkbookPart(); + workbookPart.Workbook = new Workbook(); + + // Build a combined set of custom formats and alignments across all sheets for the stylesheet. + // We create one shared stylesheet (using the first sheet's theme if not overridden). + var allNumFmts = new List(); + var allAligns = new List(); + foreach (var sheetDef in sheetsArr.EnumerateArray()) + { + var nf = ParseNumberFormats(sheetDef, "number_formats"); + var al = ParseAlignments(sheetDef, "col_alignments"); + allNumFmts.AddRange(nf); + allAligns.AddRange(al); + } + + // Use first sheet's theme for the shared stylesheet + var firstThemeName = sheetsArr[0].TryGetProperty("theme", out var ft) ? ft.GetString() : null; + var firstTheme = GetTheme(firstThemeName); + + var combinedCustomFmts = CollectCustomFormats(allNumFmts); + + var stylesPart = workbookPart.AddNewPart(); + stylesPart.Stylesheet = CreateStylesheet(true, firstTheme, combinedCustomFmts, allNumFmts, allAligns); + stylesPart.Stylesheet.Save(); + + var wbSheets = workbookPart.Workbook.AppendChild(new Sheets()); + + uint sheetId = 1; + var totalSheets = 0; + var totalRows = 0; + + foreach (var sheetDef in sheetsArr.EnumerateArray()) + { + var sheetName = sheetDef.TryGetProperty("name", out var snEl) ? snEl.GetString() ?? $"Sheet{sheetId}" : $"Sheet{sheetId}"; + var tableStyle = sheetDef.TryGetProperty("style", out var stEl) ? stEl.GetString() ?? "styled" : "styled"; + var themeName = sheetDef.TryGetProperty("theme", out var thEl) ? thEl.GetString() : firstThemeName; + var isStyled = tableStyle != "plain"; + var freezeHeader = sheetDef.TryGetProperty("freeze_header", out var fhEl) ? fhEl.GetBoolean() : isStyled; + var theme = GetTheme(themeName); + var numFmts = ParseNumberFormats(sheetDef, "number_formats"); + var alignments = ParseAlignments(sheetDef, "col_alignments"); + + if (!sheetDef.TryGetProperty("headers", out var headers) || headers.ValueKind != JsonValueKind.Array) continue; + if (!sheetDef.TryGetProperty("rows", out var rows) || rows.ValueKind != JsonValueKind.Array) continue; + + var colCount = headers.GetArrayLength(); + var summaryArg = sheetDef.TryGetProperty("summary_row", out var sumEl) ? sumEl : default; + var mergesArg = sheetDef.TryGetProperty("merges", out var mergeEl) ? mergeEl : default; + + var worksheetPart = workbookPart.AddNewPart(); + worksheetPart.Worksheet = new Worksheet(); + + var rowCount = WriteSheetContent(worksheetPart, sheetDef, sheetName, headers, rows, + isStyled, freezeHeader, theme, numFmts, alignments, combinedCustomFmts, + summaryArg, mergesArg, colCount); + + wbSheets.Append(new Sheet + { + Id = workbookPart.GetIdOfPart(worksheetPart), + SheetId = sheetId, + Name = sheetName, + }); + + sheetId++; + totalSheets++; + totalRows += rowCount; + } + + workbookPart.Workbook.Save(); + + return ToolResult.Ok( + $"Excel 파일 생성 완료: {fullPath}\n시트: {totalSheets}개, 총 데이터 행: {totalRows}", + fullPath); + } + + // ═══════════════════════════════════════════════════ + // Core sheet writer (shared by single + multi) + // ═══════════════════════════════════════════════════ + + private static int WriteSheetContent( + WorksheetPart worksheetPart, + JsonElement args, + string sheetName, + JsonElement headers, + JsonElement rows, + bool isStyled, + bool freezeHeader, + ThemeColors theme, + List numFmts, + List alignments, + List<(string code, uint id)> customFmtRegistry, + JsonElement summaryArg, + JsonElement mergesArg, + int colCount) + { + // Column widths + var columns = CreateColumns(args, colCount); + if (columns != null) + worksheetPart.Worksheet.Append(columns); + + var sheetData = new SheetData(); + worksheetPart.Worksheet.Append(sheetData); + + // Header row + var headerRow = new Row { RowIndex = 1 }; + for (int ci = 0; ci < colCount; ci++) + { + var h = headers.EnumerateArray().ElementAtOrDefault(ci); + var cellRef = GetCellReference(ci, 0); + var cell = new Cell + { + CellReference = cellRef, + DataType = CellValues.String, + CellValue = new CellValue(h.ValueKind != JsonValueKind.Undefined ? h.GetString() ?? "" : ""), + StyleIndex = isStyled ? (uint)1 : 0, + }; + headerRow.Append(cell); + } + sheetData.Append(headerRow); + + // Data rows + int rowCount = 0; + uint rowNum = 2; + foreach (var row in rows.EnumerateArray()) + { + var dataRow = new Row { RowIndex = rowNum }; + int ci = 0; + foreach (var cellVal in row.EnumerateArray()) + { + var cellRef = GetCellReference(ci, (int)rowNum - 1); + var cell = new Cell { CellReference = cellRef }; + + // Determine style index + cell.StyleIndex = ResolveDataStyleIndex( + ci, rowCount, isStyled, numFmts, alignments, customFmtRegistry); + + var strVal = cellVal.ToString(); + + if (strVal.StartsWith('=')) + { + cell.CellFormula = new CellFormula(strVal); + cell.DataType = null; + } + else if (cellVal.ValueKind == JsonValueKind.Number) + { + cell.DataType = CellValues.Number; + cell.CellValue = new CellValue(cellVal.GetDouble().ToString()); + } + else + { + cell.DataType = CellValues.String; + cell.CellValue = new CellValue(strVal); + } + + dataRow.Append(cell); + ci++; + } + sheetData.Append(dataRow); + rowCount++; + rowNum++; + } + + // Summary row + if (summaryArg.ValueKind == JsonValueKind.Object) + AddSummaryRow(sheetData, summaryArg, rowNum, colCount, rowCount, isStyled); + + // Cell merges + if (mergesArg.ValueKind == JsonValueKind.Array) + { + var mergeCells = new MergeCells(); + foreach (var merge in mergesArg.EnumerateArray()) + { + var range = merge.GetString(); + if (!string.IsNullOrEmpty(range)) + mergeCells.Append(new MergeCell { Reference = range }); + } + if (mergeCells.HasChildren) + worksheetPart.Worksheet.InsertAfter(mergeCells, sheetData); + } + + // Freeze header + if (freezeHeader) + { + var sheetViews = new SheetViews(new SheetView( + new Pane + { + VerticalSplit = 1, + TopLeftCell = "A2", + ActivePane = PaneValues.BottomLeft, + State = PaneStateValues.Frozen + }, + new Selection + { + Pane = PaneValues.BottomLeft, + ActiveCell = "A2", + SequenceOfReferences = new ListValue { InnerText = "A2" } + }) + { TabSelected = true, WorkbookViewId = 0 }); + + var insertBefore = (OpenXmlElement?)worksheetPart.Worksheet.GetFirstChild() + ?? worksheetPart.Worksheet.GetFirstChild(); + worksheetPart.Worksheet.InsertBefore(sheetViews, insertBefore); + } + + return rowCount; + } + + // ═══════════════════════════════════════════════════ + // Style index resolution for data cells + // ═══════════════════════════════════════════════════ + + // StyleIndex layout (base indices built by CreateStylesheet): + // 0 = default (no style) + // 1 = header (bold, theme header bg) + // 2 = stripe (theme stripe bg) + // 3 = summary (bold, gray bg) + // 4..3+N = number-format variants of default (plain, even-row): base=4 + // + // For each unique (numFmt, isStripe, alignment) we build extra CellFormats. + // To keep it simple, we encode them linearly after index 3. + // BuildExtraFormats returns a mapping used both in stylesheet creation and here. + // + // ExtraFormat key: (colIndex, isStripe) + // We pre-compute these in CreateStylesheet and expose via static registry pattern. + // + // To avoid complex cross-method state, we compute style index inline using the + // same deterministic formula used when building CellFormats in CreateStylesheet. + + private static uint ResolveDataStyleIndex( + int colIndex, + int dataRowIndex, + bool isStyled, + List numFmts, + List alignments, + List<(string code, uint id)> customFmtRegistry) + { + if (!isStyled && numFmts.Count == 0 && alignments.Count == 0) + return 0; + + // Base: 4 reserved indices (0=default, 1=header, 2=stripe, 3=summary) + // Extra formats start at index 4. + // Layout: for each col (0..maxCol-1), two entries: [plain, stripe] + // combined (colIndex, isStripe) into a single ordinal. + + bool isStripe = isStyled && dataRowIndex % 2 == 0; + + var maxCol = Math.Max(numFmts.Count, alignments.Count); + if (maxCol == 0) + { + // No per-column formats, just use built-in stripes + if (!isStyled) return 0; + return isStripe ? (uint)2 : (uint)0; + } + + if (colIndex >= maxCol) + { + // Column beyond per-column format definitions — fall back to default/stripe + if (!isStyled) return 0; + return isStripe ? (uint)2 : (uint)0; + } + + // Extra style index = 4 + colIndex * 2 + (isStripe ? 1 : 0) + return (uint)(4 + colIndex * 2 + (isStripe ? 1 : 0)); + } + + // ═══════════════════════════════════════════════════ + // Stylesheet + // ═══════════════════════════════════════════════════ + + private static Stylesheet CreateStylesheet( + bool isStyled, + ThemeColors theme, + List<(string code, uint id)> customFmtRegistry, + List allNumFmts, + List? allAlignments = null) { var stylesheet = new Stylesheet(); - // Fonts + // ── NumberingFormats (must come first) ─────────────────── + if (customFmtRegistry.Count > 0) + { + var nfSection = new NumberingFormats(); + foreach (var (code, id) in customFmtRegistry) + { + nfSection.Append(new NumberingFormat + { + NumberFormatId = id, + FormatCode = code, + }); + } + nfSection.Count = (uint)customFmtRegistry.Count; + stylesheet.Append(nfSection); + } + + // ── Fonts ───────────────────────────────────────────────── var fonts = new Fonts( - new Font( // 0: 기본 + new Font( // 0: default new FontSize { Val = 11 }, new FontName { Val = "맑은 고딕" } ), - new Font( // 1: 볼드 흰색 (헤더용) + new Font( // 1: bold white (header) new Bold(), new FontSize { Val = 11 }, - new Color { Rgb = "FFFFFFFF" }, + new Color { Rgb = theme.HeaderFg }, new FontName { Val = "맑은 고딕" } ), - new Font( // 2: 볼드 (요약행용) + new Font( // 2: bold (summary row) new Bold(), new FontSize { Val = 11 }, new FontName { Val = "맑은 고딕" } @@ -242,23 +516,23 @@ public class ExcelSkill : IAgentTool ); stylesheet.Append(fonts); - // Fills + // ── Fills ───────────────────────────────────────────────── var fills = new Fills( - new Fill(new PatternFill { PatternType = PatternValues.None }), // 0: none (필수) - new Fill(new PatternFill { PatternType = PatternValues.Gray125 }), // 1: gray125 (필수) - new Fill(new PatternFill // 2: 파란 헤더 배경 + new Fill(new PatternFill { PatternType = PatternValues.None }), // 0: none (required) + new Fill(new PatternFill { PatternType = PatternValues.Gray125 }), // 1: gray125 (required) + new Fill(new PatternFill // 2: theme header bg { PatternType = PatternValues.Solid, - ForegroundColor = new ForegroundColor { Rgb = "FF2E74B5" }, + ForegroundColor = new ForegroundColor { Rgb = theme.HeaderBg }, BackgroundColor = new BackgroundColor { Indexed = 64 } }), - new Fill(new PatternFill // 3: 연한 파란 (striped) + new Fill(new PatternFill // 3: theme stripe bg { PatternType = PatternValues.Solid, - ForegroundColor = new ForegroundColor { Rgb = "FFF2F7FB" }, + ForegroundColor = new ForegroundColor { Rgb = theme.StripeBg }, BackgroundColor = new BackgroundColor { Indexed = 64 } }), - new Fill(new PatternFill // 4: 연한 회색 (요약행) + new Fill(new PatternFill // 4: light gray (summary) { PatternType = PatternValues.Solid, ForegroundColor = new ForegroundColor { Rgb = "FFE8E8E8" }, @@ -267,13 +541,13 @@ public class ExcelSkill : IAgentTool ); stylesheet.Append(fills); - // Borders + // ── Borders ─────────────────────────────────────────────── var borders = new Borders( - new Border( // 0: 테두리 없음 + new Border( // 0: no border new LeftBorder(), new RightBorder(), new TopBorder(), new BottomBorder(), new DiagonalBorder() ), - new Border( // 1: 얇은 테두리 + new Border( // 1: thin border new LeftBorder(new Color { Rgb = "FFD9D9D9" }) { Style = BorderStyleValues.Thin }, new RightBorder(new Color { Rgb = "FFD9D9D9" }) { Style = BorderStyleValues.Thin }, new TopBorder(new Color { Rgb = "FFD9D9D9" }) { Style = BorderStyleValues.Thin }, @@ -283,47 +557,133 @@ public class ExcelSkill : IAgentTool ); stylesheet.Append(borders); - // CellFormats - var cellFormats = new CellFormats( - new CellFormat // 0: 기본 + // ── CellFormats ─────────────────────────────────────────── + // Indices 0-3: reserved base formats (same as before) + // Indices 4+: per-column extra formats (plain + stripe per column) + + var cellFormats = new CellFormats(); + + // 0: default + cellFormats.Append(new CellFormat + { + FontId = 0, FillId = 0, BorderId = isStyled ? (uint)1 : 0, + ApplyBorder = isStyled + }); + + // 1: header (bold, theme header bg, center) + cellFormats.Append(new CellFormat + { + FontId = 1, FillId = 2, BorderId = 1, + ApplyFont = true, ApplyFill = true, ApplyBorder = true, + Alignment = new Alignment { - FontId = 0, FillId = 0, BorderId = isStyled ? (uint)1 : 0, ApplyBorder = isStyled - }, - new CellFormat // 1: 헤더 (볼드 흰색 + 파란배경 + 테두리) - { - FontId = 1, FillId = 2, BorderId = 1, - ApplyFont = true, ApplyFill = true, ApplyBorder = true, - Alignment = new Alignment { Horizontal = HorizontalAlignmentValues.Center, Vertical = VerticalAlignmentValues.Center } - }, - new CellFormat // 2: striped 행 (연한 파란 배경) - { - FontId = 0, FillId = 3, BorderId = 1, - ApplyFill = true, ApplyBorder = true - }, - new CellFormat // 3: 요약행 (볼드 + 회색 배경) - { - FontId = 2, FillId = 4, BorderId = 1, - ApplyFont = true, ApplyFill = true, ApplyBorder = true + Horizontal = HorizontalAlignmentValues.Center, + Vertical = VerticalAlignmentValues.Center } - ); + }); + + // 2: stripe row (theme stripe bg) + cellFormats.Append(new CellFormat + { + FontId = 0, FillId = 3, BorderId = 1, + ApplyFill = true, ApplyBorder = true + }); + + // 3: summary row (bold + gray bg) + cellFormats.Append(new CellFormat + { + FontId = 2, FillId = 4, BorderId = 1, + ApplyFont = true, ApplyFill = true, ApplyBorder = true + }); + + // 4+: per-column formats (plain + stripe), two entries per column + int maxCol = Math.Max(allNumFmts.Count, allAlignments?.Count ?? 0); + for (int ci = 0; ci < maxCol; ci++) + { + var fmtStr = ci < allNumFmts.Count ? allNumFmts[ci] : null; + var (isBuiltIn, builtInId, customCode) = ResolveNumberFormat(fmtStr); + uint numFmtId = isBuiltIn ? builtInId : GetCustomFmtId(customCode!, customFmtRegistry); + bool applyNumFmt = numFmtId != 0; + + var alignStr = (allAlignments != null && ci < allAlignments.Count) ? allAlignments[ci] : null; + var horizAlign = alignStr switch + { + "right" => HorizontalAlignmentValues.Right, + "center" => HorizontalAlignmentValues.Center, + _ => HorizontalAlignmentValues.Left, + }; + bool applyAlign = !string.IsNullOrEmpty(alignStr); + + // plain (odd rows) + var plainFmt = new CellFormat + { + FontId = 0, FillId = 0, + BorderId = isStyled ? (uint)1 : 0, + NumberFormatId = numFmtId, + ApplyBorder = isStyled, + ApplyNumberFormat = applyNumFmt, + ApplyAlignment = applyAlign, + }; + if (applyAlign) + plainFmt.Alignment = new Alignment { Horizontal = horizAlign }; + cellFormats.Append(plainFmt); + + // stripe (even rows) + var stripeFmt = new CellFormat + { + FontId = 0, FillId = isStyled ? (uint)3 : 0, + BorderId = isStyled ? (uint)1 : 0, + NumberFormatId = numFmtId, + ApplyFill = isStyled, + ApplyBorder = isStyled, + ApplyNumberFormat = applyNumFmt, + ApplyAlignment = applyAlign, + }; + if (applyAlign) + stripeFmt.Alignment = new Alignment { Horizontal = horizAlign }; + cellFormats.Append(stripeFmt); + } + stylesheet.Append(cellFormats); return stylesheet; } + private static uint GetCustomFmtId(string code, List<(string code, uint id)> registry) + { + foreach (var (c, id) in registry) + if (c == code) return id; + return 0; + } + + // Build registry of custom number formats (deduplicated), assigning IDs from 164+ + private static List<(string code, uint id)> CollectCustomFormats(IEnumerable numFmts) + { + var registry = new List<(string code, uint id)>(); + uint nextId = 164; + foreach (var fmt in numFmts) + { + if (string.IsNullOrEmpty(fmt) || fmt == "text") continue; + var (isBuiltIn, _, customCode) = ResolveNumberFormat(fmt); + if (isBuiltIn || customCode == null) continue; + if (registry.Any(r => r.code == customCode)) continue; + registry.Add((customCode, nextId++)); + } + return registry; + } + // ═══════════════════════════════════════════════════ - // 열 너비 + // Column widths // ═══════════════════════════════════════════════════ private static Columns? CreateColumns(JsonElement args, int colCount) { var hasWidths = args.TryGetProperty("col_widths", out var widthsArr) && widthsArr.ValueKind == JsonValueKind.Array; - // col_widths가 없으면 기본 너비 15 적용 var columns = new Columns(); for (int i = 0; i < colCount; i++) { - double width = 15; // 기본 너비 + double width = 15; if (hasWidths && i < widthsArr.GetArrayLength()) width = widthsArr[i].GetDouble(); @@ -340,7 +700,7 @@ public class ExcelSkill : IAgentTool } // ═══════════════════════════════════════════════════ - // 요약 행 + // Summary row // ═══════════════════════════════════════════════════ private static void AddSummaryRow(SheetData sheetData, JsonElement summary, @@ -351,7 +711,6 @@ public class ExcelSkill : IAgentTool var summaryRow = new Row { RowIndex = rowNum }; - // 첫 번째 열에 라벨 var labelCell = new Cell { CellReference = GetCellReference(0, (int)rowNum - 1), @@ -361,7 +720,6 @@ public class ExcelSkill : IAgentTool }; summaryRow.Append(labelCell); - // 나머지 열에 수식 또는 빈 셀 for (int ci = 1; ci < colCount; ci++) { var colLetter = GetColumnLetter(ci); @@ -387,7 +745,45 @@ public class ExcelSkill : IAgentTool } // ═══════════════════════════════════════════════════ - // 유틸리티 + // Parameter parsing helpers + // ═══════════════════════════════════════════════════ + + private static List ParseNumberFormats(JsonElement args, string key) + { + var result = new List(); + if (!args.TryGetProperty(key, out var arr) || arr.ValueKind != JsonValueKind.Array) + return result; + foreach (var el in arr.EnumerateArray()) + result.Add(el.ValueKind == JsonValueKind.Null ? null : el.GetString()); + return result; + } + + private static List ParseAlignments(JsonElement args, string key) + { + var result = new List(); + if (!args.TryGetProperty(key, out var arr) || arr.ValueKind != JsonValueKind.Array) + return result; + foreach (var el in arr.EnumerateArray()) + result.Add(el.ValueKind == JsonValueKind.Null ? null : el.GetString()?.ToLower()); + return result; + } + + private static string BuildFeatureList(bool isStyled, bool freezeHeader, + bool hasMerges, bool hasSummary, bool hasNumFmts, bool hasAlignments, string? theme) + { + var features = new List(); + if (isStyled) features.Add("스타일 적용"); + if (theme != null) features.Add($"테마:{theme}"); + if (freezeHeader) features.Add("틀 고정"); + if (hasMerges) features.Add("셀 병합"); + if (hasSummary) features.Add("요약행"); + if (hasNumFmts) features.Add("숫자형식"); + if (hasAlignments) features.Add("정렬"); + return features.Count > 0 ? $" [{string.Join(", ", features)}]" : ""; + } + + // ═══════════════════════════════════════════════════ + // Utilities // ═══════════════════════════════════════════════════ private static string GetColumnLetter(int colIndex) diff --git a/src/AxCopilot/Services/Agent/FileWriteTool.cs b/src/AxCopilot/Services/Agent/FileWriteTool.cs index 305ec91..d2a5b72 100644 --- a/src/AxCopilot/Services/Agent/FileWriteTool.cs +++ b/src/AxCopilot/Services/Agent/FileWriteTool.cs @@ -1,30 +1,60 @@ -using System.IO; +using System.IO; +using System.Text; using System.Text.Json; namespace AxCopilot.Services.Agent; -/// 파일 전체를 새로 쓰는 도구. 새 파일 생성 또는 기존 파일 덮어쓰기. +/// 파일 전체를 새로 쓰거나 추가 쓰기하는 도구. 다양한 인코딩 지원. public class FileWriteTool : IAgentTool { public string Name => "file_write"; - public string Description => "Write content to a file. Creates new file or overwrites existing. Parent directories are created automatically."; + public string Description => + "Write content to a file. Creates a new file or overwrites an existing one. " + + "Supports append mode, automatic parent directory creation, and multiple encodings " + + "(utf-8, utf-8-bom, utf-16le, utf-16be, euc-kr, cp949)."; public ToolParameterSchema Parameters => new() { Properties = new() { - ["path"] = new() { Type = "string", Description = "File path to write (absolute or relative to work folder)" }, - ["content"] = new() { Type = "string", Description = "Content to write to the file" }, + ["path"] = new() + { + Type = "string", + Description = "File path to write (absolute or relative to work folder)" + }, + ["content"] = new() + { + Type = "string", + Description = "Content to write to the file" + }, + ["encoding"] = new() + { + Type = "string", + Description = "Text encoding to use. Options: \"utf-8\" (default, no BOM), \"utf-8-bom\", \"utf-16le\", \"utf-16be\", \"euc-kr\", \"cp949\"" + }, + ["append"] = new() + { + Type = "boolean", + Description = "If true, appends content to the file instead of overwriting. Creates the file if it does not exist. Default: false" + }, + ["create_dirs"] = new() + { + Type = "boolean", + Description = "If true (default), automatically creates any missing parent directories before writing" + }, }, Required = ["path", "content"] }; public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { - var path = args.GetProperty("path").GetString() ?? ""; - var content = args.GetProperty("content").GetString() ?? ""; + var rawPath = args.GetProperty("path").GetString() ?? ""; + var content = args.GetProperty("content").GetString() ?? ""; + var encName = args.TryGetProperty("encoding", out var encEl) ? encEl.GetString() ?? "utf-8" : "utf-8"; + var append = args.TryGetProperty("append", out var appEl) && appEl.GetBoolean(); + var mkDirs = !args.TryGetProperty("create_dirs", out var mkEl) || mkEl.GetBoolean(); - var fullPath = FileReadTool.ResolvePath(path, context.WorkFolder); + var fullPath = FileReadTool.ResolvePath(rawPath, context.WorkFolder); if (!context.IsPathAllowed(fullPath)) return ToolResult.Fail($"경로 접근 차단: {fullPath}"); @@ -32,15 +62,90 @@ public class FileWriteTool : IAgentTool if (!await context.CheckWritePermissionAsync(Name, fullPath)) return ToolResult.Fail($"쓰기 권한 거부: {fullPath}"); + // Resolve encoding + Encoding encoding; + string encodingLabel; try { - await TextFileCodec.WriteAllTextAsync(fullPath, content, TextFileCodec.Utf8NoBom, ct); - var lines = content.Split('\n').Length; - return ToolResult.Ok($"파일 저장 완료: {fullPath} ({lines} lines, {content.Length} chars)", fullPath); + (encoding, encodingLabel) = ResolveEncoding(encName.Trim().ToLowerInvariant()); + } + catch (Exception ex) + { + return ToolResult.Fail($"지원하지 않는 인코딩: '{encName}'. ({ex.Message})"); + } + + try + { + if (mkDirs) + { + var dir = Path.GetDirectoryName(fullPath); + if (!string.IsNullOrEmpty(dir)) + Directory.CreateDirectory(dir); + } + + if (append) + { + // Append path: use StreamWriter so we respect the requested encoding + await using var sw = new StreamWriter(fullPath, append: true, encoding: encoding); + await sw.WriteAsync(content.AsMemory(), ct); + } + else + { + // Overwrite path: prefer TextFileCodec for UTF-8 variants, raw StreamWriter otherwise + if (encoding is UTF8Encoding) + { + await TextFileCodec.WriteAllTextAsync(fullPath, content, encoding, ct); + } + else + { + await File.WriteAllTextAsync(fullPath, content, encoding, ct); + } + } + + // Build success stats + var fileInfo = new FileInfo(fullPath); + var lines = TextFileCodec.SplitLines(content).Length; + var charCount = content.Length; + var sizeKb = fileInfo.Exists ? fileInfo.Length / 1024.0 : 0.0; + var mode = append ? "추가" : "덮어쓰기"; + + var summary = + $"파일 {mode} 완료\n" + + $" 경로 : {fullPath}\n" + + $" 줄 수 : {lines:N0} lines\n" + + $" 문자 수 : {charCount:N0} chars\n" + + $" 파일 크기: {sizeKb:F1} KB\n" + + $" 인코딩 : {encodingLabel}"; + + return ToolResult.Ok(summary, fullPath); } catch (Exception ex) { return ToolResult.Fail($"파일 쓰기 실패: {ex.Message}"); } } + + /// + /// 인코딩 이름 문자열을 System.Text.Encoding 인스턴스로 변환합니다. + /// + private static (Encoding encoding, string label) ResolveEncoding(string name) => name switch + { + "utf-8" => (TextFileCodec.Utf8NoBom, "UTF-8 (no BOM)"), + "utf-8-bom" => (new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), "UTF-8 BOM"), + "utf-16le" => (new UnicodeEncoding(bigEndian: false, byteOrderMark: true), "UTF-16 LE"), + "utf-16be" => (new UnicodeEncoding(bigEndian: true, byteOrderMark: true), "UTF-16 BE"), + "euc-kr" => (CodePagesEncoding("euc-kr"), "EUC-KR"), + "cp949" => (CodePagesEncoding("ks_c_5601-1987"), "CP949 (ks_c_5601-1987)"), + _ => throw new NotSupportedException($"알 수 없는 인코딩: {name}") + }; + + /// + /// .NET 8에서 코드 페이지 인코딩을 가져옵니다. EncodingProvider 등록을 보장합니다. + /// + private static Encoding CodePagesEncoding(string name) + { + // CodePagesEncodingProvider를 최초 1회 등록 + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + return Encoding.GetEncoding(name); + } } diff --git a/src/AxCopilot/Services/Agent/FolderMapTool.cs b/src/AxCopilot/Services/Agent/FolderMapTool.cs index 5196a84..1353c40 100644 --- a/src/AxCopilot/Services/Agent/FolderMapTool.cs +++ b/src/AxCopilot/Services/Agent/FolderMapTool.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Text; using System.Text.Json; @@ -13,7 +13,8 @@ public class FolderMapTool : IAgentTool public string Name => "folder_map"; public string Description => "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 to understand the project layout before reading or editing files. " + + "Supports sorting, size filtering, date filtering, and multi-extension filtering."; public ToolParameterSchema Parameters => new() { @@ -22,7 +23,17 @@ public class FolderMapTool : IAgentTool ["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: 3." }, ["include_files"] = new() { Type = "boolean", Description = "Whether to include files. Default: true." }, - ["pattern"] = new() { Type = "string", Description = "File extension filter (e.g. '.cs', '.py'). Optional, shows all files if omitted." }, + ["pattern"] = new() { Type = "string", Description = "Single file extension filter (e.g. '.cs', '.py'). Optional. Use 'extensions' for multiple extensions." }, + ["extensions"] = new() + { + Type = "array", + Description = "Filter by multiple extensions, e.g. [\".cs\", \".json\"]. Takes precedence over 'pattern' if both are provided.", + Items = new ToolProperty { Type = "string" }, + }, + ["sort_by"] = new() { Type = "string", Description = "Sort files/dirs within each level: 'name' (default), 'size' (descending), 'modified' (newest first)." }, + ["show_dir_sizes"] = new() { Type = "boolean", Description = "If true, show the total size of each directory in parentheses. Default: false." }, + ["modified_after"] = new() { Type = "string", Description = "ISO date string (e.g. '2024-01-01'). Only show files modified after this date." }, + ["max_file_size"] = new() { Type = "string", Description = "Only show files smaller than this size, e.g. '1MB', '500KB', '2048B'." }, }, Required = [] }; @@ -40,14 +51,20 @@ public class FolderMapTool : IAgentTool public Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { + // ── path ────────────────────────────────────────────────────────── var subPath = args.TryGetProperty("path", out var p) ? p.GetString() ?? "" : ""; + + // ── depth ───────────────────────────────────────────────────────── var depth = 3; if (args.TryGetProperty("depth", out var d)) { if (d.ValueKind == JsonValueKind.Number) depth = d.GetInt32(); else if (d.ValueKind == JsonValueKind.String && int.TryParse(d.GetString(), out var dv)) depth = dv; } - var depthStr = depth.ToString(); + if (depth < 1) depth = 1; + var maxDepth = Math.Min(depth, 10); + + // ── include_files ───────────────────────────────────────────────── var includeFiles = true; if (args.TryGetProperty("include_files", out var inc)) { @@ -56,12 +73,51 @@ public class FolderMapTool : IAgentTool else includeFiles = !string.Equals(inc.GetString(), "false", StringComparison.OrdinalIgnoreCase); } - var extFilter = args.TryGetProperty("pattern", out var pat) ? pat.GetString() ?? "" : ""; - if (!int.TryParse(depthStr, out var maxDepth) || maxDepth < 1) - maxDepth = 3; - maxDepth = Math.Min(maxDepth, 10); + // ── extensions / pattern ────────────────────────────────────────── + HashSet? extSet = null; + if (args.TryGetProperty("extensions", out var extsEl) && extsEl.ValueKind == JsonValueKind.Array) + { + var list = extsEl.EnumerateArray() + .Select(e => e.GetString() ?? "") + .Where(s => !string.IsNullOrWhiteSpace(s)) + .Select(s => s.StartsWith('.') ? s : "." + s) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + if (list.Count > 0) extSet = list; + } + // Fall back to single pattern if extensions not provided + string extFilter = ""; + if (extSet == null) + extFilter = args.TryGetProperty("pattern", out var pat) ? pat.GetString() ?? "" : ""; + // ── sort_by ─────────────────────────────────────────────────────── + var sortBy = args.TryGetProperty("sort_by", out var sb2) ? sb2.GetString() ?? "name" : "name"; + if (sortBy != "size" && sortBy != "modified") sortBy = "name"; + + // ── show_dir_sizes ──────────────────────────────────────────────── + var showDirSizes = false; + if (args.TryGetProperty("show_dir_sizes", out var sds)) + { + if (sds.ValueKind == JsonValueKind.True || sds.ValueKind == JsonValueKind.False) + showDirSizes = sds.GetBoolean(); + else + showDirSizes = string.Equals(sds.GetString(), "true", StringComparison.OrdinalIgnoreCase); + } + + // ── modified_after ──────────────────────────────────────────────── + DateTime? modifiedAfter = null; + if (args.TryGetProperty("modified_after", out var maEl) && maEl.ValueKind == JsonValueKind.String) + { + if (DateTime.TryParse(maEl.GetString(), out var mdt)) + modifiedAfter = mdt; + } + + // ── max_file_size ───────────────────────────────────────────────── + long? maxFileSizeBytes = null; + if (args.TryGetProperty("max_file_size", out var mfsEl) && mfsEl.ValueKind == JsonValueKind.String) + maxFileSizeBytes = ParseSizeString(mfsEl.GetString() ?? ""); + + // ── resolve base directory ──────────────────────────────────────── var baseDir = string.IsNullOrEmpty(subPath) ? context.WorkFolder : FileReadTool.ResolvePath(subPath, context.WorkFolder); @@ -74,18 +130,31 @@ public class FolderMapTool : IAgentTool try { + var options = new TreeOptions(maxDepth, includeFiles, extFilter, extSet, + sortBy, showDirSizes, modifiedAfter, maxFileSizeBytes); + var sb = new StringBuilder(); var dirName = Path.GetFileName(baseDir); if (string.IsNullOrEmpty(dirName)) dirName = baseDir; - sb.AppendLine($"{dirName}/"); + + long rootTotalSize = 0; + sb.Append($"{dirName}/"); int entryCount = 0; - BuildTree(sb, baseDir, "", 0, maxDepth, includeFiles, extFilter, context, ref entryCount); + int totalFiles = 0; + int totalDirs = 0; + BuildTree(sb, baseDir, "", 0, options, context, + ref entryCount, ref totalFiles, ref totalDirs, ref rootTotalSize); + + if (showDirSizes) + sb.Insert(sb.ToString().IndexOf('/') + 1, $" ({FormatSize(rootTotalSize)})"); + + sb.AppendLine(); // newline after root if (entryCount >= MaxEntries) - sb.AppendLine($"\n... ({MaxEntries}개 항목 제한 도달, depth 또는 pattern을 조정하세요)"); + sb.AppendLine($"\n... ({MaxEntries} entry limit reached; adjust depth or filters)"); - var summary = $"폴더 맵 생성 완료 ({entryCount}개 항목, 깊이 {maxDepth})"; + var summary = $"Folder map complete — {totalFiles} files, {totalDirs} dirs, {FormatSize(rootTotalSize)} total (depth {maxDepth})"; return Task.FromResult(ToolResult.Ok($"{summary}\n\n{sb}")); } catch (Exception ex) @@ -94,44 +163,58 @@ public class FolderMapTool : IAgentTool } } - private static void BuildTree( - StringBuilder sb, string dir, string prefix, int currentDepth, int maxDepth, - bool includeFiles, string extFilter, AgentContext context, ref int entryCount) - { - if (currentDepth >= maxDepth || entryCount >= MaxEntries) return; + // ─── Tree builder ──────────────────────────────────────────────────── - // 하위 디렉토리 + private static void BuildTree( + StringBuilder sb, string dir, string prefix, int currentDepth, + TreeOptions opts, AgentContext context, + ref int entryCount, ref int totalFiles, ref int totalDirs, ref long accumSize) + { + if (currentDepth >= opts.MaxDepth || entryCount >= MaxEntries) return; + + // ── Collect subdirectories ──────────────────────────────────────── List subDirs; try { subDirs = new DirectoryInfo(dir).GetDirectories() .Where(d => !d.Attributes.HasFlag(FileAttributes.Hidden) && !IgnoredDirs.Contains(d.Name)) - .OrderBy(d => d.Name) .ToList(); } - catch { return; } // 접근 불가 디렉토리 무시 + catch { return; } - // 하위 파일 + // ── Collect files ───────────────────────────────────────────────── List files = []; - if (includeFiles) + if (opts.IncludeFiles) { try { files = new DirectoryInfo(dir).GetFiles() - .Where(f => !f.Attributes.HasFlag(FileAttributes.Hidden) - && (string.IsNullOrEmpty(extFilter) - || f.Extension.Equals(extFilter, StringComparison.OrdinalIgnoreCase))) - .OrderBy(f => f.Name) + .Where(f => !f.Attributes.HasFlag(FileAttributes.Hidden)) + .Where(f => MatchesExtension(f, opts.ExtFilter, opts.ExtSet)) + .Where(f => opts.ModifiedAfter == null || f.LastWriteTime > opts.ModifiedAfter.Value) + .Where(f => opts.MaxFileSizeBytes == null || f.Length <= opts.MaxFileSizeBytes.Value) .ToList(); } catch { /* ignore */ } } + // ── Sort ────────────────────────────────────────────────────────── + subDirs = opts.SortBy == "modified" + ? subDirs.OrderByDescending(d => d.LastWriteTime).ToList() + : subDirs.OrderBy(d => d.Name).ToList(); + + files = opts.SortBy switch + { + "size" => files.OrderByDescending(f => f.Length).ToList(), + "modified" => files.OrderByDescending(f => f.LastWriteTime).ToList(), + _ => files.OrderBy(f => f.Name).ToList(), + }; + var totalItems = subDirs.Count + files.Count; var index = 0; - // 디렉토리 출력 + // ── Render subdirectories ───────────────────────────────────────── foreach (var sub in subDirs) { if (entryCount >= MaxEntries) break; @@ -140,15 +223,41 @@ public class FolderMapTool : IAgentTool var connector = isLast ? "└── " : "├── "; var childPrefix = isLast ? " " : "│ "; - sb.AppendLine($"{prefix}{connector}{sub.Name}/"); - entryCount++; + long subSize = 0; + int subFiles = 0, subDirsCount = 0; + _ = subFiles; _ = subDirsCount; // suppress unused warnings (reserved for future use) if (context.IsPathAllowed(sub.FullName)) - BuildTree(sb, sub.FullName, prefix + childPrefix, currentDepth + 1, maxDepth, - includeFiles, extFilter, context, ref entryCount); + { + // We always recurse to gather sizes; count only when rendered + if (opts.ShowDirSizes) + { + // Pre-compute directory size (best-effort, no error propagation) + try { subSize = ComputeDirSize(sub.FullName); } catch { } + } + + totalDirs++; + entryCount++; + + var dirLabel = opts.ShowDirSizes + ? $"{sub.Name}/ ({FormatSize(subSize)})" + : $"{sub.Name}/"; + sb.AppendLine($"{prefix}{connector}{dirLabel}"); + + BuildTree(sb, sub.FullName, prefix + childPrefix, currentDepth + 1, opts, context, + ref entryCount, ref totalFiles, ref totalDirs, ref subSize); + + accumSize += subSize; + } + else + { + totalDirs++; + entryCount++; + sb.AppendLine($"{prefix}{connector}{sub.Name}/ [access denied]"); + } } - // 파일 출력 + // ── Render files ────────────────────────────────────────────────── foreach (var file in files) { if (entryCount >= MaxEntries) break; @@ -156,16 +265,73 @@ public class FolderMapTool : IAgentTool var isLast = index == totalItems; var connector = isLast ? "└── " : "├── "; - var sizeStr = FormatSize(file.Length); - sb.AppendLine($"{prefix}{connector}{file.Name} ({sizeStr})"); + string annotation = opts.SortBy switch + { + "modified" => $"({file.LastWriteTime:yyyy-MM-dd}, {FormatSize(file.Length)})", + "size" => $"({FormatSize(file.Length)})", + _ => $"({FormatSize(file.Length)})", + }; + + sb.AppendLine($"{prefix}{connector}{file.Name} {annotation}"); + accumSize += file.Length; + totalFiles++; entryCount++; } } + // ─── Helpers ────────────────────────────────────────────────────────── + + private static bool MatchesExtension(FileInfo f, string extFilter, HashSet? extSet) + { + if (extSet != null) return extSet.Contains(f.Extension); + if (!string.IsNullOrEmpty(extFilter)) + return f.Extension.Equals(extFilter, StringComparison.OrdinalIgnoreCase); + return true; + } + + private static long ComputeDirSize(string dir) + { + long total = 0; + foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories)) + { + try { total += new FileInfo(f).Length; } catch { } + } + return total; + } + + private static long? ParseSizeString(string s) + { + if (string.IsNullOrWhiteSpace(s)) return null; + s = s.Trim(); + if (s.EndsWith("GB", StringComparison.OrdinalIgnoreCase) && double.TryParse(s[..^2], out var gb)) + return (long)(gb * 1024 * 1024 * 1024); + if (s.EndsWith("MB", StringComparison.OrdinalIgnoreCase) && double.TryParse(s[..^2], out var mb)) + return (long)(mb * 1024 * 1024); + if (s.EndsWith("KB", StringComparison.OrdinalIgnoreCase) && double.TryParse(s[..^2], out var kb)) + return (long)(kb * 1024); + if (s.EndsWith("B", StringComparison.OrdinalIgnoreCase) && double.TryParse(s[..^1], out var b)) + return (long)b; + if (long.TryParse(s, out var raw)) return raw; + return null; + } + private static string FormatSize(long bytes) => bytes switch { < 1024 => $"{bytes} B", < 1024 * 1024 => $"{bytes / 1024.0:F1} KB", - _ => $"{bytes / (1024.0 * 1024.0):F1} MB", + < 1024L * 1024 * 1024 => $"{bytes / (1024.0 * 1024.0):F1} MB", + _ => $"{bytes / (1024.0 * 1024.0 * 1024.0):F2} GB", }; + + // ─── Options record ──────────────────────────────────────────────────── + + private sealed record TreeOptions( + int MaxDepth, + bool IncludeFiles, + string ExtFilter, + HashSet? ExtSet, + string SortBy, + bool ShowDirSizes, + DateTime? ModifiedAfter, + long? MaxFileSizeBytes); } diff --git a/src/AxCopilot/Services/Agent/GlobTool.cs b/src/AxCopilot/Services/Agent/GlobTool.cs index 6ed96d8..75eba13 100644 --- a/src/AxCopilot/Services/Agent/GlobTool.cs +++ b/src/AxCopilot/Services/Agent/GlobTool.cs @@ -1,5 +1,7 @@ -using System.IO; +using System.IO; +using System.Text; using System.Text.Json; +using System.Text.RegularExpressions; namespace AxCopilot.Services.Agent; @@ -7,22 +9,30 @@ namespace AxCopilot.Services.Agent; public class GlobTool : IAgentTool { public string Name => "glob"; - public string Description => "Find files matching a glob pattern (e.g. '**/*.cs', 'src/**/*.json'). Returns matching file paths."; + public string Description => "Find files matching a glob pattern (e.g. '**/*.cs', 'src/**/*.json', '*.txt'). Returns matching file paths relative to the search directory."; public ToolParameterSchema Parameters => new() { Properties = new() { - ["pattern"] = new() { Type = "string", Description = "Glob pattern to match files (e.g. '**/*.cs', '*.txt')" }, - ["path"] = new() { Type = "string", Description = "Directory to search in. Optional, defaults to work folder." }, + ["pattern"] = new() { Type = "string", Description = "Glob pattern to match files. Supports ** (recursive), * (any chars), ? (single char). E.g. '**/*.cs', 'src/models/*.json', '*.txt'." }, + ["path"] = new() { Type = "string", Description = "Directory to search in. Optional, defaults to work folder." }, + ["sort_by"] = new() { Type = "string", Description = "'name' (default), 'size', or 'modified'. When size/modified, shows that info next to each file." }, + ["max_results"] = new() { Type = "integer", Description = "Maximum number of results to return. Default 200, max 2000." }, + ["include_hidden"] = new() { Type = "boolean", Description = "Include hidden files/directories (starting with '.'). Default false." }, + ["exclude_pattern"] = new() { Type = "string", Description = "Glob pattern for files to exclude (e.g. '*.min.js', '*.g.cs'). Matched against file name only." }, }, Required = ["pattern"] }; public Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { - var pattern = args.GetProperty("pattern").GetString() ?? ""; - var searchPath = args.TryGetProperty("path", out var p) ? p.GetString() ?? "" : ""; + var pattern = args.GetProperty("pattern").GetString() ?? ""; + var searchPath = args.TryGetProperty("path", out var p) ? p.GetString() ?? "" : ""; + var sortBy = args.TryGetProperty("sort_by", out var sb) ? sb.GetString() ?? "name" : "name"; + var maxResults = args.TryGetProperty("max_results", out var mr) ? Math.Clamp(mr.GetInt32(), 1, 2000) : 200; + var includeHidden = args.TryGetProperty("include_hidden", out var ih) && ih.GetBoolean(); + var excludePattern = args.TryGetProperty("exclude_pattern", out var ep) ? ep.GetString() ?? "" : ""; var baseDir = string.IsNullOrEmpty(searchPath) ? context.WorkFolder @@ -36,22 +46,89 @@ public class GlobTool : IAgentTool try { - // glob 패턴을 Directory.EnumerateFiles용으로 변환 - var searchPattern = ExtractSearchPattern(pattern); - var recursive = pattern.Contains("**") || pattern.Contains('/') || pattern.Contains('\\'); + var (searchDir, filePattern, recursive) = DecomposePattern(pattern, baseDir); + + if (!Directory.Exists(searchDir)) + return Task.FromResult(ToolResult.Ok($"패턴 '{pattern}'에 일치하는 파일이 없습니다. (디렉토리 없음: {Path.GetRelativePath(baseDir, searchDir)})")); + var option = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; - var files = Directory.EnumerateFiles(baseDir, searchPattern, option) - .Where(f => context.IsPathAllowed(f)) - .OrderBy(f => f) - .Take(200) + // Build a regex from the file-name portion of the glob for exact matching + var fileNameRegex = GlobSegmentToRegex(filePattern); + + // Build exclude regex if provided (matched against filename only) + Regex? excludeRegex = string.IsNullOrEmpty(excludePattern) + ? null + : new Regex(GlobSegmentToRegex(excludePattern), RegexOptions.IgnoreCase | RegexOptions.Compiled); + + var files = Directory.EnumerateFiles(searchDir, "*", option) + .Where(f => + { + if (!context.IsPathAllowed(f)) return false; + + var fileName = Path.GetFileName(f); + + // Hidden file/dir check + if (!includeHidden) + { + // Check every path segment from searchDir downward + var rel = Path.GetRelativePath(searchDir, f); + if (rel.Split(Path.DirectorySeparatorChar) + .Any(seg => seg.StartsWith('.'))) + return false; + } + + // File-name pattern match + if (!Regex.IsMatch(fileName, fileNameRegex, RegexOptions.IgnoreCase)) + return false; + + // Exclude pattern + if (excludeRegex != null && excludeRegex.IsMatch(fileName)) + return false; + + return true; + }) .ToList(); if (files.Count == 0) return Task.FromResult(ToolResult.Ok($"패턴 '{pattern}'에 일치하는 파일이 없습니다.")); - var result = string.Join("\n", files.Select(f => Path.GetRelativePath(baseDir, f))); - return Task.FromResult(ToolResult.Ok($"{files.Count}개 파일 발견:\n{result}")); + // Sort + IEnumerable sorted = sortBy switch + { + "size" => files.OrderBy(f => new FileInfo(f).Length), + "modified" => files.OrderByDescending(f => new FileInfo(f).LastWriteTime), + _ => files.OrderBy(f => f, StringComparer.OrdinalIgnoreCase) + }; + + var taken = sorted.Take(maxResults).ToList(); + var truncated = files.Count > maxResults; + + var sb2 = new StringBuilder(); + foreach (var f in taken) + { + var rel = Path.GetRelativePath(baseDir, f); + if (sortBy == "size") + { + var size = new FileInfo(f).Length; + sb2.AppendLine($"{rel} ({FormatSize(size)})"); + } + else if (sortBy == "modified") + { + var ts = new FileInfo(f).LastWriteTime; + sb2.AppendLine($"{rel} ({ts:yyyy-MM-dd HH:mm:ss})"); + } + else + { + sb2.AppendLine(rel); + } + } + + var summary = truncated + ? $"{taken.Count}개 파일 표시 (전체 {files.Count}개, 제한 {maxResults}개):" + : $"{taken.Count}개 파일 발견:"; + + return Task.FromResult(ToolResult.Ok($"{summary}\n{sb2.ToString().TrimEnd()}")); } catch (Exception ex) { @@ -59,11 +136,81 @@ public class GlobTool : IAgentTool } } - private static string ExtractSearchPattern(string globPattern) + /// + /// Decomposes a glob pattern into (searchDirectory, fileNamePattern, recursive). + /// Examples: + /// "**/*.cs" → (baseDir, "*.cs", true) + /// "*.txt" → (baseDir, "*.txt", false) + /// "src/**/*.json" → (baseDir/src, "*.json", true) + /// "src/models/*.cs" → (baseDir/src/models, "*.cs", false) + /// "src/models/Foo.cs" → (baseDir/src/models, "Foo.cs", false) + /// + private static (string searchDir, string filePattern, bool recursive) DecomposePattern( + string pattern, string baseDir) { - // **/*.cs → *.cs, src/**/*.json → *.json - var parts = globPattern.Replace('/', '\\').Split('\\'); - var last = parts[^1]; - return string.IsNullOrEmpty(last) || last == "**" ? "*" : last; + // Normalise separators + var normalised = pattern.Replace('\\', '/'); + var segments = normalised.Split('/', StringSplitOptions.RemoveEmptyEntries); + + if (segments.Length == 0) + return (baseDir, "*", true); + + // The last segment is always the file-name pattern (may contain * or ?) + var filePattern = segments[^1]; + var dirSegments = segments[..^1]; // everything before the last segment + + bool recursive = false; + var pathParts = new List(); + + foreach (var seg in dirSegments) + { + if (seg == "**") + { + recursive = true; + // Do not add ** itself to the path; it means "any depth" + } + else + { + pathParts.Add(seg); + } + } + + // If the file pattern itself is **, treat as recursive wildcard + if (filePattern == "**") + { + recursive = true; + filePattern = "*"; + } + + var searchDir = pathParts.Count > 0 + ? Path.Combine(new[] { baseDir }.Concat(pathParts).ToArray()) + : baseDir; + + return (searchDir, filePattern, recursive); + } + + /// Converts a simple glob segment (*, ?, literals) to a full-match regex string. + private static string GlobSegmentToRegex(string glob) + { + var sb = new StringBuilder("^"); + foreach (var ch in glob) + { + switch (ch) + { + case '*': sb.Append(".*"); break; + case '?': sb.Append('.'); break; + case '.': sb.Append("\\."); break; + default: sb.Append(Regex.Escape(ch.ToString())); break; + } + } + sb.Append('$'); + return sb.ToString(); + } + + private static string FormatSize(long bytes) + { + if (bytes >= 1_048_576) return $"{bytes / 1_048_576.0:F1} MB"; + if (bytes >= 1_024) return $"{bytes / 1_024.0:F1} KB"; + return $"{bytes} B"; } } diff --git a/src/AxCopilot/Services/Agent/GrepTool.cs b/src/AxCopilot/Services/Agent/GrepTool.cs index d1cea57..29a2ae2 100644 --- a/src/AxCopilot/Services/Agent/GrepTool.cs +++ b/src/AxCopilot/Services/Agent/GrepTool.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; @@ -15,22 +15,38 @@ public class GrepTool : IAgentTool { Properties = new() { - ["pattern"] = new() { Type = "string", Description = "Search pattern (regex supported)" }, - ["path"] = new() { Type = "string", Description = "File or directory to search in. Optional, defaults to work folder." }, - ["glob"] = new() { Type = "string", Description = "File pattern filter (e.g. '*.cs', '*.json'). Optional." }, - ["context_lines"] = new() { Type = "integer", Description = "Number of context lines before/after each match (0-5). Default 0." }, - ["case_sensitive"] = new() { Type = "boolean", Description = "Case-sensitive search. Default false (case-insensitive)." }, + ["pattern"] = new() { Type = "string", Description = "Search pattern (regex supported)" }, + ["path"] = new() { Type = "string", Description = "File or directory to search in. Optional, defaults to work folder." }, + ["glob"] = new() { Type = "string", Description = "File pattern filter (e.g. '*.cs', '*.json'). Optional." }, + ["context_lines"] = new() { Type = "integer", Description = "Number of context lines before AND after each match (0-10). Shorthand for setting both before_lines and after_lines. Default 0." }, + ["before_lines"] = new() { Type = "integer", Description = "Lines to show before each match (0-10). Default 0. Overrides context_lines for the before side." }, + ["after_lines"] = new() { Type = "integer", Description = "Lines to show after each match (0-10). Default 0. Overrides context_lines for the after side." }, + ["case_sensitive"] = new() { Type = "boolean", Description = "Case-sensitive search. Default false (case-insensitive)." }, + ["max_matches"] = new() { Type = "integer", Description = "Maximum total matches to return. Default 100, max 500." }, + ["files_only"] = new() { Type = "boolean", Description = "If true, return only file paths (no content). Default false." }, + ["whole_word"] = new() { Type = "boolean", Description = "If true, match whole words only (wraps pattern in \\b...\\b). Default false." }, + ["invert"] = new() { Type = "boolean", Description = "If true, return lines NOT matching the pattern. Default false." }, + ["max_file_size_kb"] = new() { Type = "integer", Description = "Skip files larger than this size in KB. Default 1000." }, }, Required = ["pattern"] }; public Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { - var pattern = args.GetProperty("pattern").GetString() ?? ""; - var searchPath = args.TryGetProperty("path", out var p) ? p.GetString() ?? "" : ""; - var globFilter = args.TryGetProperty("glob", out var g) ? g.GetString() ?? "" : ""; - var contextLines = args.TryGetProperty("context_lines", out var cl) ? Math.Clamp(cl.GetInt32(), 0, 5) : 0; - var caseSensitive = args.TryGetProperty("case_sensitive", out var cs) && cs.GetBoolean(); + var pattern = args.GetProperty("pattern").GetString() ?? ""; + var searchPath = args.TryGetProperty("path", out var p) ? p.GetString() ?? "" : ""; + var globFilter = args.TryGetProperty("glob", out var g) ? g.GetString() ?? "" : ""; + var caseSensitive = args.TryGetProperty("case_sensitive", out var cs) && cs.GetBoolean(); + var maxMatches = args.TryGetProperty("max_matches", out var mm) ? Math.Clamp(mm.GetInt32(), 1, 500) : 100; + var filesOnly = args.TryGetProperty("files_only", out var fo) && fo.GetBoolean(); + var wholeWord = args.TryGetProperty("whole_word", out var ww) && ww.GetBoolean(); + var invert = args.TryGetProperty("invert", out var inv) && inv.GetBoolean(); + var maxFileSizeKb = args.TryGetProperty("max_file_size_kb", out var mfs) ? Math.Max(1, mfs.GetInt32()) : 1000; + + // context_lines is the base for both sides; before/after_lines override individually + var contextLines = args.TryGetProperty("context_lines", out var cl) ? Math.Clamp(cl.GetInt32(), 0, 10) : 0; + var beforeLines = args.TryGetProperty("before_lines", out var bl) ? Math.Clamp(bl.GetInt32(), 0, 10) : contextLines; + var afterLines = args.TryGetProperty("after_lines", out var al) ? Math.Clamp(al.GetInt32(), 0, 10) : contextLines; var baseDir = string.IsNullOrEmpty(searchPath) ? context.WorkFolder @@ -41,8 +57,10 @@ public class GrepTool : IAgentTool try { + // Build effective regex pattern + var effectivePattern = wholeWord ? $@"\b{pattern}\b" : pattern; var regexOpts = RegexOptions.Compiled | (caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase); - var regex = new Regex(pattern, regexOpts, TimeSpan.FromSeconds(5)); + var regex = new Regex(effectivePattern, regexOpts, TimeSpan.FromSeconds(5)); var filePattern = string.IsNullOrEmpty(globFilter) ? "*" : globFilter; @@ -54,52 +72,110 @@ public class GrepTool : IAgentTool else return Task.FromResult(ToolResult.Fail($"경로가 존재하지 않습니다: {baseDir}")); - var sb = new StringBuilder(); + long maxFileSizeBytes = (long)maxFileSizeKb * 1024; + + // files_only mode: collect matching file paths only + if (filesOnly) + { + var matchingFiles = new List(); + + foreach (var file in files) + { + if (ct.IsCancellationRequested) break; + if (!context.IsPathAllowed(file)) continue; + if (IsBinaryFile(file)) continue; + if (new FileInfo(file).Length > maxFileSizeBytes) continue; + + try + { + var read = TextFileCodec.ReadAllText(file); + var lines = TextFileCodec.SplitLines(read.Text); + bool hit = lines.Any(line => + invert ? !regex.IsMatch(line) : regex.IsMatch(line)); + + if (hit) + { + var rel = Directory.Exists(context.WorkFolder) + ? Path.GetRelativePath(context.WorkFolder, file) + : file; + matchingFiles.Add(rel); + } + } + catch { /* 읽기 실패 파일 무시 */ } + } + + if (matchingFiles.Count == 0) + return Task.FromResult(ToolResult.Ok($"패턴 '{pattern}'에 일치하는 파일이 없습니다.")); + + var fileList = string.Join("\n", matchingFiles); + return Task.FromResult(ToolResult.Ok($"{matchingFiles.Count}개 파일 발견:\n{fileList}")); + } + + // Normal mode: show matching lines with context + var sb = new StringBuilder(); int matchCount = 0; - int fileCount = 0; - const int maxMatches = 100; + int fileCount = 0; foreach (var file in files) { if (ct.IsCancellationRequested) break; if (!context.IsPathAllowed(file)) continue; if (IsBinaryFile(file)) continue; + if (new FileInfo(file).Length > maxFileSizeBytes) continue; try { - var read = TextFileCodec.ReadAllText(file); + var read = TextFileCodec.ReadAllText(file); var lines = TextFileCodec.SplitLines(read.Text); - bool fileHit = false; - for (int i = 0; i < lines.Length && matchCount < maxMatches; i++) + + // First pass: find all matching line indices in this file + var hitIndices = new List(); + for (int i = 0; i < lines.Length; i++) { - if (regex.IsMatch(lines[i])) - { - if (!fileHit) - { - var rel = Directory.Exists(context.WorkFolder) - ? Path.GetRelativePath(context.WorkFolder, file) - : file; - sb.AppendLine($"\n{rel}:"); - fileHit = true; - fileCount++; - } - // 컨텍스트 라인 (before) - if (contextLines > 0) - { - for (int c = Math.Max(0, i - contextLines); c < i; c++) - sb.AppendLine($" {c + 1} {lines[c].TrimEnd()}"); - } - sb.AppendLine($" {i + 1}: {lines[i].TrimEnd()}"); - // 컨텍스트 라인 (after) - if (contextLines > 0) - { - for (int c = i + 1; c <= Math.Min(lines.Length - 1, i + contextLines); c++) - sb.AppendLine($" {c + 1} {lines[c].TrimEnd()}"); - sb.AppendLine(" ---"); - } - matchCount++; - } + bool isMatch = regex.IsMatch(lines[i]); + if (invert ? !isMatch : isMatch) + hitIndices.Add(i); } + + if (hitIndices.Count == 0) continue; + + var rel = Directory.Exists(context.WorkFolder) + ? Path.GetRelativePath(context.WorkFolder, file) + : file; + + // File header with per-file match count + sb.AppendLine(); + sb.AppendLine($"── {rel} ({hitIndices.Count}개 일치) ──"); + + int fileMatchCount = 0; + + // Second pass: emit hits with context, avoiding duplicate lines + int lastEmittedLine = -1; + + foreach (int idx in hitIndices) + { + if (matchCount >= maxMatches) break; + + int from = Math.Max(0, idx - beforeLines); + int to = Math.Min(lines.Length - 1, idx + afterLines); + + // If there's a gap between the previous context block and this one, add separator + if (lastEmittedLine >= 0 && from > lastEmittedLine + 1) + sb.AppendLine(" ···"); + + for (int c = Math.Max(from, lastEmittedLine + 1); c <= to; c++) + { + bool isHit = invert ? !regex.IsMatch(lines[c]) : regex.IsMatch(lines[c]); + var marker = isHit ? ">" : " "; + sb.AppendLine($" {marker} {c + 1,5}: {lines[c].TrimEnd()}"); + } + + lastEmittedLine = to; + matchCount++; + fileMatchCount++; + } + + fileCount++; } catch { /* 읽기 실패 파일 무시 */ } @@ -109,7 +185,10 @@ public class GrepTool : IAgentTool if (matchCount == 0) return Task.FromResult(ToolResult.Ok($"패턴 '{pattern}'에 일치하는 결과가 없습니다.")); - var header = $"{fileCount}개 파일에서 {matchCount}개 일치{(matchCount >= maxMatches ? " (제한 도달)" : "")}:"; + var limitNote = matchCount >= maxMatches ? $" (제한 {maxMatches}개 도달)" : ""; + var invertNote = invert ? " [반전 검색]" : ""; + var header = $"{fileCount}개 파일에서 {matchCount}개 일치{limitNote}{invertNote}:"; + return Task.FromResult(ToolResult.Ok(header + sb)); } catch (RegexParseException) diff --git a/src/AxCopilot/Services/Agent/HtmlSkill.cs b/src/AxCopilot/Services/Agent/HtmlSkill.cs index dd96cb4..4b781dc 100644 --- a/src/AxCopilot/Services/Agent/HtmlSkill.cs +++ b/src/AxCopilot/Services/Agent/HtmlSkill.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; @@ -9,6 +9,7 @@ namespace AxCopilot.Services.Agent; /// HTML (.html) 보고서를 생성하는 내장 스킬. /// 테마 무드(mood)를 선택하면 TemplateService에서 해당 CSS를 가져와 적용합니다. /// TOC, 커버 페이지, 섹션 번호 등 고급 문서 기능을 지원합니다. +/// sections 파라미터로 구조화된 콘텐츠 블록(heading/paragraph/callout/table/chart/cards/list/quote/divider/kpi)을 지원합니다. ///
public class HtmlSkill : IAgentTool { @@ -18,6 +19,8 @@ public class HtmlSkill : IAgentTool "badges (.badge-blue/green/red/yellow/purple), CSS bar charts (.chart-bar), " + "progress bars (.progress), timelines (.timeline), grid layouts (.grid-2/3/4), " + "and auto section numbering. " + + "Use 'sections' array for structured content (heading/paragraph/callout/table/chart/cards/list/quote/divider/kpi) " + + "instead of raw HTML body. Use 'accent_color' to override theme accent. Use 'print' for print-ready output. " + "Available moods: modern, professional, creative, minimal, elegant, dark, colorful, corporate, magazine, dashboard."; public ToolParameterSchema Parameters => new() @@ -30,29 +33,76 @@ public class HtmlSkill : IAgentTool "div.callout-info/warning/tip/danger for callouts, span.badge-blue/green/red for badges, " + "div.chart-bar>div.bar-item for charts, div.grid-2/3/4 for grid layouts, " + "div.timeline>div.timeline-item for timelines, div.progress for progress bars." }, + ["sections"] = new() + { + Type = "array", + Description = "Structured content blocks. Each object has a 'type' field. " + + "Types: 'heading' {level:1-4, text}, " + + "'paragraph' {text} — supports **bold** *italic* `code` [link](url) inline markdown, " + + "'callout' {style:'info|warning|tip|danger', title, text}, " + + "'table' {headers:[], rows:[[]]}, " + + "'chart' {kind:'bar|horizontal_bar', title, data:[{label, value, color}]}, " + + "'cards' {items:[{title, body, badge, icon}]}, " + + "'list' {style:'bullet|number', items:['item', ' - sub-item']}, " + + "'quote' {text, author}, " + + "'divider', " + + "'kpi' {items:[{label, value, change, positive:bool}]}. " + + "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" }, + ["accent_color"] = new() { Type = "string", Description = "Hex color string (e.g. '#2E75B6') that overrides the CSS primary/accent color. Affects buttons, headings, borders, chart bars." }, ["style"] = new() { Type = "string", Description = "Optional additional CSS. Appended after mood+shared CSS." }, ["toc"] = new() { Type = "boolean", Description = "Auto-generate table of contents from h2/h3 headings. Default: false" }, ["numbered"] = new() { Type = "boolean", Description = "Auto-number h2/h3 sections (1., 1-1., etc). Default: false" }, + ["print"] = new() { Type = "boolean", Description = "If true, adds @media print CSS: removes shadows, adds borders, forces white background, page-breaks before h2, adjusts font sizes. Default: false" }, ["cover"] = new() { Type = "object", Description = "Cover page config: {\"title\": \"...\", \"subtitle\": \"...\", \"author\": \"...\", \"date\": \"...\", \"gradient\": \"#hex1,#hex2\"}. Omit to skip cover page." }, }, - Required = ["path", "title", "body"] + Required = ["title"] }; public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { - var path = args.GetProperty("path").GetString() ?? ""; - var title = args.GetProperty("title").GetString() ?? "Report"; - var body = args.GetProperty("body").GetString() ?? ""; + // 필수 파라미터 안전 추출 + if (!args.TryGetProperty("title", out var titleEl) || titleEl.ValueKind == JsonValueKind.Null) + return ToolResult.Fail("필수 파라미터 누락: 'title'"); + + // path 미제공 시 title로 자동 생성 + args.TryGetProperty("path", out var pathEl); + if (pathEl.ValueKind == JsonValueKind.Null || string.IsNullOrWhiteSpace(pathEl.GetString())) + pathEl = default; // 아래에서 title 기반으로 생성 + + // body와 sections 둘 다 없으면 오류 + bool hasBody = args.TryGetProperty("body", out var bodyEl) && bodyEl.ValueKind != JsonValueKind.Null; + bool hasSections = args.TryGetProperty("sections", out var sectionsEl) && sectionsEl.ValueKind == JsonValueKind.Array; + if (!hasBody && !hasSections) + return ToolResult.Fail("필수 파라미터 누락: 'body' 또는 'sections' 중 하나는 반드시 제공해야 합니다."); + + var title = titleEl.GetString() ?? "Report"; + // path가 없으면 title에서 안전한 파일명 생성 + string path; + if (pathEl.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(pathEl.GetString())) + { + path = pathEl.GetString()!; + } + else + { + var safeName = Regex.Replace(title, @"[\\/:*?""<>|]", "_").Trim().TrimEnd('.'); + if (safeName.Length > 60) safeName = safeName[..60].TrimEnd(); + if (string.IsNullOrWhiteSpace(safeName)) safeName = "report"; + path = safeName + ".html"; + } + var body = hasBody ? (bodyEl.GetString() ?? "") : ""; var customStyle = args.TryGetProperty("style", out var s) ? s.GetString() : null; var mood = args.TryGetProperty("mood", out var m) ? m.GetString() ?? "modern" : "modern"; - var useToc = args.TryGetProperty("toc", out var tocVal) && tocVal.GetBoolean(); - var useNumbered = args.TryGetProperty("numbered", out var numVal) && numVal.GetBoolean(); + var useToc = args.TryGetProperty("toc", out var tocVal) && tocVal.ValueKind == JsonValueKind.True; + var useNumbered = args.TryGetProperty("numbered", out var numVal) && numVal.ValueKind == JsonValueKind.True; + var usePrint = args.TryGetProperty("print", out var printVal) && printVal.ValueKind == JsonValueKind.True; var hasCover = args.TryGetProperty("cover", out var coverVal) && coverVal.ValueKind == JsonValueKind.Object; + var accentColor = args.TryGetProperty("accent_color", out var accentEl) ? accentEl.GetString() : null; var fullPath = FileReadTool.ResolvePath(path, context.WorkFolder); if (context.ActiveTab == "Cowork") fullPath = AgentContext.EnsureTimestampedPath(fullPath); @@ -73,12 +123,29 @@ public class HtmlSkill : IAgentTool // 스타일 결정: mood CSS + shared CSS + custom var style = TemplateService.GetCss(mood); + + // accent_color 주입 + if (!string.IsNullOrEmpty(accentColor)) + style = InjectAccentColor(style, accentColor); + + // print CSS 추가 + if (usePrint) + style += "\n" + GeneratePrintCss(); + if (!string.IsNullOrEmpty(customStyle)) style += "\n" + customStyle; var moodInfo = TemplateService.GetMood(mood); var moodLabel = moodInfo != null ? $" · {moodInfo.Icon} {moodInfo.Label}" : ""; + // sections → HTML 변환 + if (hasSections) + { + var sectionsHtml = RenderSections(sectionsEl, accentColor); + // body + sections 합산 (body 있으면 뒤에 붙이기) + body = hasBody ? body + "\n" + sectionsHtml : sectionsHtml; + } + // 섹션 번호 자동 부여 — h2, h3에 class="numbered" 추가 if (useNumbered) body = AddNumberedClass(body); @@ -133,6 +200,9 @@ public class HtmlSkill : IAgentTool if (useToc) features.Add("목차"); if (useNumbered) features.Add("섹션번호"); if (hasCover) features.Add("커버페이지"); + if (hasSections) features.Add("구조화섹션"); + if (!string.IsNullOrEmpty(accentColor)) features.Add($"색상:{accentColor}"); + if (usePrint) features.Add("인쇄최적화"); var featureStr = features.Count > 0 ? $" [{string.Join(", ", features)}]" : ""; return ToolResult.Ok( @@ -145,6 +215,407 @@ public class HtmlSkill : IAgentTool } } + // ───────────────────────────────────────────────────────────────────────── + // sections 렌더링 + // ───────────────────────────────────────────────────────────────────────── + + /// sections 배열을 순회하여 HTML 문자열로 변환 + private static string RenderSections(JsonElement sections, string? accentColor) + { + var sb = new StringBuilder(); + foreach (var section in sections.EnumerateArray()) + { + if (!section.TryGetProperty("type", out var typeEl)) continue; + var type = typeEl.GetString() ?? ""; + + switch (type.ToLowerInvariant()) + { + case "heading": + sb.AppendLine(RenderHeading(section)); + break; + case "paragraph": + sb.AppendLine(RenderParagraph(section)); + break; + case "callout": + sb.AppendLine(RenderCallout(section)); + break; + case "table": + sb.AppendLine(RenderTable(section)); + break; + case "chart": + sb.AppendLine(RenderChart(section, accentColor)); + break; + case "cards": + sb.AppendLine(RenderCards(section)); + break; + case "list": + sb.AppendLine(RenderList(section)); + break; + case "quote": + sb.AppendLine(RenderQuote(section)); + break; + case "divider": + sb.AppendLine("
"); + break; + case "kpi": + sb.AppendLine(RenderKpi(section)); + break; + } + } + return sb.ToString(); + } + + private static string RenderHeading(JsonElement s) + { + var level = s.TryGetProperty("level", out var lv) ? Math.Clamp(lv.GetInt32(), 1, 4) : 2; + var text = s.TryGetProperty("text", out var t) ? t.GetString() ?? "" : ""; + return $"{Escape(text)}"; + } + + private static string RenderParagraph(JsonElement s) + { + var text = s.TryGetProperty("text", out var t) ? t.GetString() ?? "" : ""; + return $"

{MarkdownToHtml(text)}

"; + } + + private static string RenderCallout(JsonElement s) + { + var style = s.TryGetProperty("style", out var st) ? st.GetString() ?? "info" : "info"; + var title = s.TryGetProperty("title", out var ti) ? ti.GetString() ?? "" : ""; + var text = s.TryGetProperty("text", out var tx) ? tx.GetString() ?? "" : ""; + var icon = style switch { "warning" => "⚠️", "tip" => "💡", "danger" => "🚨", _ => "ℹ️" }; + var sb = new StringBuilder(); + sb.AppendLine($"
"); + if (!string.IsNullOrEmpty(title)) + sb.AppendLine($"{icon} {Escape(title)}"); + sb.AppendLine($"

{MarkdownToHtml(text)}

"); + sb.AppendLine("
"); + return sb.ToString(); + } + + private static string RenderTable(JsonElement s) + { + var sb = new StringBuilder(); + sb.AppendLine("
"); + sb.AppendLine(""); + + if (s.TryGetProperty("headers", out var headers) && headers.ValueKind == JsonValueKind.Array) + { + sb.AppendLine(""); + foreach (var h in headers.EnumerateArray()) + sb.Append($""); + sb.AppendLine(""); + } + + if (s.TryGetProperty("rows", out var rows) && rows.ValueKind == JsonValueKind.Array) + { + sb.AppendLine(""); + foreach (var row in rows.EnumerateArray()) + { + sb.Append(""); + if (row.ValueKind == JsonValueKind.Array) + foreach (var cell in row.EnumerateArray()) + sb.Append($""); + sb.AppendLine(""); + } + sb.AppendLine(""); + } + + sb.AppendLine("
{Escape(h.GetString() ?? "")}
{MarkdownToHtml(cell.GetString() ?? "")}
"); + sb.AppendLine("
"); + return sb.ToString(); + } + + private static string RenderChart(JsonElement s, string? accentColor) + { + var kind = s.TryGetProperty("kind", out var k) ? k.GetString() ?? "bar" : "bar"; + var chartTitle = s.TryGetProperty("title", out var t) ? t.GetString() ?? "" : ""; + + if (!s.TryGetProperty("data", out var data) || data.ValueKind != JsonValueKind.Array) + return ""; + + var items = new List<(string label, double value, string color)>(); + double maxVal = 0; + foreach (var item in data.EnumerateArray()) + { + var label = item.TryGetProperty("label", out var lb) ? lb.GetString() ?? "" : ""; + var value = item.TryGetProperty("value", out var vl) ? vl.GetDouble() : 0; + var color = item.TryGetProperty("color", out var cl) ? cl.GetString() ?? "#2E75B6" : "#2E75B6"; + items.Add((label, value, color)); + if (value > maxVal) maxVal = value; + } + + if (items.Count == 0) return ""; + + // Horizontal bar chart SVG + var defaultAccent = string.IsNullOrEmpty(accentColor) ? "#2E75B6" : accentColor; + int rowHeight = 44; + int paddingTop = string.IsNullOrEmpty(chartTitle) ? 12 : 38; + int paddingBottom = 20; + int svgHeight = paddingTop + items.Count * rowHeight + paddingBottom; + int labelWidth = 120; + int valueWidth = 56; + int barAreaWidth = 560; // nominal; SVG is 100% wide so we use viewBox + int totalWidth = labelWidth + barAreaWidth + valueWidth; + + var sb = new StringBuilder(); + sb.AppendLine($"
"); + if (!string.IsNullOrEmpty(chartTitle)) + sb.AppendLine($"
{Escape(chartTitle)}
"); + sb.AppendLine($""); + + // background grid lines (light) + int gridSteps = 5; + for (int i = 0; i <= gridSteps; i++) + { + int x = labelWidth + (int)(barAreaWidth * i / gridSteps); + sb.AppendLine($" "); + } + + // bars + for (int i = 0; i < items.Count; i++) + { + var (label, value, color) = items[i]; + int y = paddingTop + i * rowHeight; + int barHeight = 24; + int barY = y + (rowHeight - barHeight) / 2; + double ratio = maxVal > 0 ? value / maxVal : 0; + int barW = Math.Max(4, (int)(barAreaWidth * ratio)); + string barColor = color == "#2E75B6" && !string.IsNullOrEmpty(accentColor) ? defaultAccent : color; + + // label (right-aligned in label area) + sb.AppendLine($" {Escape(label)}"); + // bar + sb.AppendLine($" "); + // value label + var valStr = value == Math.Floor(value) ? ((long)value).ToString("N0") : value.ToString("G4"); + sb.AppendLine($" {Escape(valStr)}"); + } + + sb.AppendLine(""); + sb.AppendLine("
"); + return sb.ToString(); + } + + private static string RenderCards(JsonElement s) + { + if (!s.TryGetProperty("items", out var items) || items.ValueKind != JsonValueKind.Array) + return ""; + + var sb = new StringBuilder(); + sb.AppendLine("
"); + foreach (var item in items.EnumerateArray()) + { + var cardTitle = item.TryGetProperty("title", out var ti) ? ti.GetString() ?? "" : ""; + var cardBody = item.TryGetProperty("body", out var bd) ? bd.GetString() ?? "" : ""; + var badge = item.TryGetProperty("badge", out var bg) ? bg.GetString() : null; + var icon = item.TryGetProperty("icon", out var ic) ? ic.GetString() : null; + + sb.AppendLine("
"); + sb.Append("
"); + if (!string.IsNullOrEmpty(icon)) + sb.Append($"{icon}"); + sb.Append($"{Escape(cardTitle)}"); + if (!string.IsNullOrEmpty(badge)) + sb.Append($"{Escape(badge)}"); + sb.AppendLine("
"); + if (!string.IsNullOrEmpty(cardBody)) + sb.AppendLine($"
{MarkdownToHtml(cardBody)}
"); + sb.AppendLine("
"); + } + sb.AppendLine("
"); + return sb.ToString(); + } + + private static string RenderList(JsonElement s) + { + var style = s.TryGetProperty("style", out var st) ? st.GetString() ?? "bullet" : "bullet"; + if (!s.TryGetProperty("items", out var items) || items.ValueKind != JsonValueKind.Array) + return ""; + + var tag = style == "number" ? "ol" : "ul"; + var sb = new StringBuilder(); + + // Support simple indentation via leading spaces/dashes + // Items starting with " - " or " * " are treated as sub-items + sb.AppendLine($"<{tag}>"); + bool inSubList = false; + foreach (var item in items.EnumerateArray()) + { + var text = item.GetString() ?? ""; + bool isSub = text.StartsWith(" "); + if (isSub && !inSubList) + { + sb.AppendLine($"<{tag} style=\"margin-top:4px\">"); + inSubList = true; + } + else if (!isSub && inSubList) + { + sb.AppendLine($""); + inSubList = false; + } + var cleaned = isSub ? Regex.Replace(text.TrimStart(), @"^[-*]\s*", "") : text; + sb.AppendLine($"
  • {MarkdownToHtml(cleaned)}
  • "); + } + if (inSubList) sb.AppendLine($""); + sb.AppendLine($""); + return sb.ToString(); + } + + private static string RenderQuote(JsonElement s) + { + var text = s.TryGetProperty("text", out var t) ? t.GetString() ?? "" : ""; + var author = s.TryGetProperty("author", out var a) ? a.GetString() : null; + var sb = new StringBuilder(); + sb.AppendLine("
    "); + sb.AppendLine($"

    {MarkdownToHtml(text)}

    "); + if (!string.IsNullOrEmpty(author)) + sb.AppendLine($"
    — {Escape(author)}
    "); + sb.AppendLine("
    "); + return sb.ToString(); + } + + private static string RenderKpi(JsonElement s) + { + if (!s.TryGetProperty("items", out var items) || items.ValueKind != JsonValueKind.Array) + return ""; + + var sb = new StringBuilder(); + sb.AppendLine("
    "); + foreach (var item in items.EnumerateArray()) + { + var label = item.TryGetProperty("label", out var lb) ? lb.GetString() ?? "" : ""; + var value = item.TryGetProperty("value", out var vl) ? vl.GetString() ?? "" : ""; + var change = item.TryGetProperty("change", out var ch) ? ch.GetString() : null; + var positive = item.TryGetProperty("positive", out var pos) ? pos.GetBoolean() : true; + var changeColor = positive ? "#16a34a" : "#dc2626"; + var changeArrow = positive ? "▲" : "▼"; + + sb.AppendLine("
    "); + sb.AppendLine($"
    {Escape(label)}
    "); + sb.AppendLine($"
    {Escape(value)}
    "); + if (!string.IsNullOrEmpty(change)) + sb.AppendLine($"
    {changeArrow} {Escape(change)}
    "); + sb.AppendLine("
    "); + } + sb.AppendLine("
    "); + return sb.ToString(); + } + + // ───────────────────────────────────────────────────────────────────────── + // Markdown 인라인 변환 + // ───────────────────────────────────────────────────────────────────────── + + /// + /// 인라인 마크다운을 HTML로 변환합니다. + /// 지원: **bold**, *italic*, `code`, [text](url), \n → br + /// HTML 특수문자는 먼저 이스케이프한 뒤 변환합니다. + /// + private static string MarkdownToHtml(string text) + { + if (string.IsNullOrEmpty(text)) return text; + + // 1. HTML 이스케이프 (먼저 처리해서 XSS 방지) + text = text.Replace("&", "&").Replace("<", "<").Replace(">", ">"); + + // 2. [text](url) → — 이스케이프 후 처리 (url의 & 등 고려) + text = Regex.Replace(text, + @"\[([^\]]+)\]\((https?://[^\)]+)\)", + m => $"{m.Groups[1].Value}"); + + // 3. **bold** + text = Regex.Replace(text, @"\*\*(.+?)\*\*", "$1"); + + // 4. *italic* (single asterisk, not preceded/followed by another) + text = Regex.Replace(text, @"(?$1"); + + // 5. `code` + text = Regex.Replace(text, @"`([^`]+)`", "$1"); + + // 6. newline →
    + text = text.Replace("\n", "
    "); + + return text; + } + + // ───────────────────────────────────────────────────────────────────────── + // accent_color 주입 + // ───────────────────────────────────────────────────────────────────────── + + /// + /// CSS의 :root { } 블록에 --accent 및 --accent-light CSS 변수를 주입합니다. + /// :root가 없으면 맨 앞에 새로 추가합니다. + /// + private static string InjectAccentColor(string css, string hexColor) + { + // Compute a lighter version by mixing with white at 80% + var accentLight = LightenHex(hexColor, 0.82); + + var varBlock = $"--accent: {hexColor}; --accent-light: {accentLight};"; + + // Try to inject into existing :root { } + if (Regex.IsMatch(css, @":root\s*\{")) + { + return Regex.Replace(css, @"(:root\s*\{)", $"$1\n {varBlock}"); + } + else + { + // Prepend :root block + return $":root {{\n {varBlock}\n}}\n" + css; + } + } + + /// hex 색상을 white와 mix하여 밝은 버전 생성 (ratio=0~1, 높을수록 더 밝음) + private static string LightenHex(string hex, double ratio) + { + hex = hex.TrimStart('#'); + if (hex.Length == 3) hex = string.Concat(hex[0], hex[0], hex[1], hex[1], hex[2], hex[2]); + if (hex.Length != 6) return hex; + try + { + int r = Convert.ToInt32(hex[..2], 16); + int g = Convert.ToInt32(hex[2..4], 16); + int b = Convert.ToInt32(hex[4..6], 16); + r = (int)Math.Round(r + (255 - r) * ratio); + g = (int)Math.Round(g + (255 - g) * ratio); + b = (int)Math.Round(b + (255 - b) * ratio); + return $"#{r:X2}{g:X2}{b:X2}"; + } + catch { return "#" + hex; } + } + + // ───────────────────────────────────────────────────────────────────────── + // print CSS + // ───────────────────────────────────────────────────────────────────────── + + private static string GeneratePrintCss() => @" +@media print { + body { background: #fff !important; font-size: 11pt; } + .container { max-width: 100% !important; padding: 0 !important; } + * { box-shadow: none !important; text-shadow: none !important; } + a { color: inherit; text-decoration: underline; } + a[href]::after { content: "" ("" attr(href) "")""; font-size: 0.8em; color: #555; } + a[href^=""#""]::after { content: """"; } + h2 { page-break-before: always; } + h1, h2, h3, h4 { page-break-after: avoid; } + table { border-collapse: collapse !important; } + th, td { border: 1px solid #999 !important; } + tr { page-break-inside: avoid; } + blockquote { border: 1px solid #bbb !important; page-break-inside: avoid; } + .callout-info, .callout-warning, .callout-tip, .callout-danger { + border: 1px solid #bbb !important; + background: #fff !important; + page-break-inside: avoid; + } + .kpi-grid > div, .cards-grid > div { border: 1px solid #ccc !important; } + nav.toc { page-break-after: always; } + .cover-page { page-break-after: always; background: none !important; color: #000 !important; } +}"; + + // ───────────────────────────────────────────────────────────────────────── + // 기존 헬퍼 메서드 (변경 없음) + // ───────────────────────────────────────────────────────────────────────── + /// h2, h3 태그에 id 속성이 없으면 자동 부여 private static string EnsureHeadingIds(string html) { diff --git a/src/AxCopilot/Services/Agent/MarkdownSkill.cs b/src/AxCopilot/Services/Agent/MarkdownSkill.cs index 89c5cc9..ae75ae1 100644 --- a/src/AxCopilot/Services/Agent/MarkdownSkill.cs +++ b/src/AxCopilot/Services/Agent/MarkdownSkill.cs @@ -1,35 +1,88 @@ -using System.IO; +using System.IO; using System.Text; using System.Text.Json; +using System.Text.RegularExpressions; namespace AxCopilot.Services.Agent; /// /// Markdown (.md) 문서를 생성하는 내장 스킬. -/// LLM이 마크다운 내용을 전달하면 파일로 저장합니다. +/// sections 배열로 구조화된 콘텐츠 블록(heading/paragraph/table/list/callout/code/quote/divider/toc)을 지원합니다. +/// frontmatter, toc 옵션으로 메타데이터와 목차를 자동 생성할 수 있습니다. +/// content 파라미터로 기존 방식의 원시 마크다운도 계속 지원합니다. /// public class MarkdownSkill : IAgentTool { public string Name => "markdown_create"; - public string Description => "Create a Markdown (.md) document file. Provide the content in Markdown format."; + public string Description => + "Create a Markdown (.md) document. " + + "Use 'sections' for structured content (heading/paragraph/table/list/callout/code/quote/divider/toc). " + + "Use 'frontmatter' for YAML metadata. Use 'toc' to auto-generate a table of contents. " + + "Use 'content' for raw markdown (backward compatible)."; public ToolParameterSchema Parameters => new() { Properties = new() { - ["path"] = new() { Type = "string", Description = "Output file path (.md). Relative to work folder." }, - ["content"] = new() { Type = "string", Description = "Markdown content to write" }, - ["title"] = new() { Type = "string", Description = "Optional document title. If provided, prepends '# title' at the top." }, + ["path"] = new() { Type = "string", Description = "출력 파일 경로 (.md). 작업 폴더 기준 상대 경로." }, + ["title"] = new() { Type = "string", Description = "문서 제목. 제공 시 최상단에 '# 제목' 헤딩을 추가합니다." }, + ["content"] = new() { Type = "string", Description = "원시 마크다운 내용 (하위 호환). sections가 없을 때 사용합니다." }, + ["sections"] = new() + { + Type = "array", + Description = + "구조화된 콘텐츠 블록 배열. 각 객체는 'type' 필드를 가집니다. " + + "타입: " + + "'heading' {level:1-6, text} — 제목, " + + "'paragraph' {text} — 단락, " + + "'table' {headers:[], rows:[[]]} — 표, " + + "'list' {items:[], ordered:false} — 목록, " + + "'callout' {style:'info|warning|tip|danger', text} — 콜아웃 블록, " + + "'code' {language:'python', code:'...'} — 코드 블록, " + + "'quote' {text, author} — 인용구, " + + "'divider' — 구분선, " + + "'toc' — 이 위치에 목차 삽입." + }, + ["frontmatter"] = new() + { + Type = "object", + Description = "YAML 프론트매터 메타데이터 키-값 쌍. 예: {\"author\": \"홍길동\", \"date\": \"2026-04-07\"}." + }, + ["toc"] = new() { Type = "boolean", Description = "true이면 문서 상단(제목 다음)에 목차를 자동 생성합니다." }, + ["encoding"] = new() { Type = "string", Description = "파일 인코딩: 'utf-8' (기본값) 또는 'euc-kr'." }, }, - Required = ["path", "content"] + Required = [] }; public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { - var path = args.GetProperty("path").GetString() ?? ""; - var content = args.GetProperty("content").GetString() ?? ""; - var title = args.TryGetProperty("title", out var t) ? t.GetString() : null; + // ── 필수 파라미터 ────────────────────────────────────────────────── + var hasSections = args.TryGetProperty("sections", out var sectionsEl) && sectionsEl.ValueKind == JsonValueKind.Array; + var hasContent = args.TryGetProperty("content", out var contentEl) && contentEl.ValueKind != JsonValueKind.Null; + var hasFrontmatter= args.TryGetProperty("frontmatter",out var frontEl) && frontEl.ValueKind == JsonValueKind.Object; + if (!hasSections && !hasContent) + return ToolResult.Fail("필수 파라미터 누락: 'sections' 또는 'content' 중 하나는 반드시 제공해야 합니다."); + + // path 미제공 시 title에서 자동 생성 + string path; + if (args.TryGetProperty("path", out var pathEl) && pathEl.ValueKind == JsonValueKind.String + && !string.IsNullOrWhiteSpace(pathEl.GetString())) + { + path = pathEl.GetString()!; + } + else + { + var baseTitle = (args.TryGetProperty("title", out var autoT) ? autoT.GetString() : null) ?? "document"; + var safe = System.Text.RegularExpressions.Regex.Replace(baseTitle, @"[\\/:*?""<>|]", "_").Trim().TrimEnd('.'); + if (safe.Length > 60) safe = safe[..60].TrimEnd(); + path = (string.IsNullOrWhiteSpace(safe) ? "document" : safe) + ".md"; + } + var title = args.TryGetProperty("title", out var titleEl) ? titleEl.GetString() : null; + var useToc = args.TryGetProperty("toc", out var tocEl) && tocEl.ValueKind == JsonValueKind.True; + var encodingName = args.TryGetProperty("encoding", out var encEl) ? encEl.GetString() ?? "utf-8" : "utf-8"; + + // ── 경로 처리 ────────────────────────────────────────────────────── var fullPath = FileReadTool.ResolvePath(path, context.WorkFolder); if (context.ActiveTab == "Cowork") fullPath = AgentContext.EnsureTimestampedPath(fullPath); if (!fullPath.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) @@ -43,27 +96,285 @@ public class MarkdownSkill : IAgentTool try { - var dir = Path.GetDirectoryName(fullPath); - if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); + Encoding fileEncoding; + try { fileEncoding = Encoding.GetEncoding(encodingName); } + catch { fileEncoding = new UTF8Encoding(false); } var sb = new StringBuilder(); + + // ── YAML 프론트매터 ──────────────────────────────────────────── + if (hasFrontmatter) + { + sb.AppendLine("---"); + foreach (var prop in frontEl.EnumerateObject()) + { + var val = prop.Value.ValueKind == JsonValueKind.String + ? prop.Value.GetString() ?? "" + : prop.Value.ToString(); + // 값에 특수 문자가 있으면 인용 + if (val.Contains(':') || val.Contains('#') || val.StartsWith('"')) + val = $"\"{val.Replace("\"", "\\\"")}\""; + sb.AppendLine($"{prop.Name}: {val}"); + } + sb.AppendLine("---"); + sb.AppendLine(); + } + + // ── 제목 ─────────────────────────────────────────────────────── if (!string.IsNullOrEmpty(title)) { sb.AppendLine($"# {title}"); sb.AppendLine(); } - sb.Append(content); - await File.WriteAllTextAsync(fullPath, sb.ToString(), Encoding.UTF8, ct); + if (hasSections) + { + // ── sections 모드 ────────────────────────────────────────── + // 먼저 heading 목록 수집 (TOC 생성용) + var headings = CollectHeadings(sectionsEl); + + // 문서 상단 TOC (toc:true 옵션) + if (useToc && headings.Count > 0) + { + RenderToc(sb, headings); + sb.AppendLine(); + } + + // 섹션 렌더링 + foreach (var section in sectionsEl.EnumerateArray()) + { + if (!section.TryGetProperty("type", out var typeEl)) continue; + var type = typeEl.GetString()?.ToLowerInvariant() ?? ""; + + switch (type) + { + case "heading": + RenderHeading(sb, section); + break; + case "paragraph": + RenderParagraph(sb, section); + break; + case "table": + RenderTable(sb, section); + break; + case "list": + RenderList(sb, section); + break; + case "callout": + RenderCallout(sb, section); + break; + case "code": + RenderCode(sb, section); + break; + case "quote": + RenderQuote(sb, section); + break; + case "divider": + sb.AppendLine("---"); + sb.AppendLine(); + break; + case "toc": + // 인라인 TOC 삽입 + if (headings.Count > 0) RenderToc(sb, headings); + sb.AppendLine(); + break; + } + } + } + else + { + // ── 하위 호환: 원시 content 모드 ────────────────────────── + if (useToc) + { + // content에서 헤딩 파싱하여 TOC 생성 + var raw = contentEl.GetString() ?? ""; + var headings = ParseHeadingsFromContent(raw); + if (headings.Count > 0) + { + RenderToc(sb, headings); + sb.AppendLine(); + } + } + sb.Append(contentEl.GetString() ?? ""); + } + + // ── 파일 쓰기 ────────────────────────────────────────────────── + var dir = Path.GetDirectoryName(fullPath); + if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); + + await File.WriteAllTextAsync(fullPath, sb.ToString(), fileEncoding, ct); var lines = sb.ToString().Split('\n').Length; return ToolResult.Ok( - $"Markdown 문서 저장 완료: {fullPath} ({lines} lines)", + $"Markdown 문서 저장 완료: {fullPath} ({lines}줄, 인코딩: {encodingName})", fullPath); } catch (Exception ex) { - return ToolResult.Fail($"Markdown 저장 실패: {ex.Message}"); + return ToolResult.Fail($"Markdown 문서 저장 중 오류가 발생했습니다: {ex.Message}"); } } + + // ── 헤딩 수집 (sections 배열에서) ──────────────────────────────────────── + private static List<(int Level, string Text)> CollectHeadings(JsonElement sections) + { + var list = new List<(int, string)>(); + foreach (var s in sections.EnumerateArray()) + { + if (!s.TryGetProperty("type", out var t) || t.GetString() != "heading") continue; + var level = s.TryGetProperty("level", out var lEl) ? lEl.GetInt32() : 2; + var text = s.TryGetProperty("text", out var xEl) ? xEl.GetString() ?? "" : ""; + if (!string.IsNullOrEmpty(text)) + list.Add((Math.Clamp(level, 1, 6), text)); + } + return list; + } + + // ── 헤딩 파싱 (원시 마크다운에서) ──────────────────────────────────────── + private static List<(int Level, string Text)> ParseHeadingsFromContent(string content) + { + var list = new List<(int, string)>(); + foreach (var line in content.Split('\n')) + { + var m = Regex.Match(line, @"^(#{1,6})\s+(.+)"); + if (m.Success) + list.Add((m.Groups[1].Length, m.Groups[2].Value.Trim())); + } + return list; + } + + // ── TOC 렌더링 ──────────────────────────────────────────────────────────── + private static void RenderToc(StringBuilder sb, List<(int Level, string Text)> headings) + { + sb.AppendLine("## 목차"); + sb.AppendLine(); + foreach (var (level, text) in headings) + { + // level 1은 들여쓰기 없음, level 2 → 2스페이스, 이후 2씩 추가 + var indent = level <= 1 ? "" : new string(' ', (level - 1) * 2); + var anchor = HeadingToAnchor(text); + sb.AppendLine($"{indent}- [{text}](#{anchor})"); + } + sb.AppendLine(); + } + + // ── GitHub-style 앵커 생성 ──────────────────────────────────────────────── + private static string HeadingToAnchor(string text) + { + // 소문자 변환, 영문/숫자/한글 이외 공백 → '-', 연속 '-' 정리 + var anchor = text.ToLowerInvariant(); + anchor = Regex.Replace(anchor, @"[^\w\uAC00-\uD7A3\s-]", ""); + anchor = Regex.Replace(anchor, @"\s+", "-"); + anchor = Regex.Replace(anchor, @"-+", "-").Trim('-'); + return anchor; + } + + // ── 개별 섹션 렌더러 ────────────────────────────────────────────────────── + + private static void RenderHeading(StringBuilder sb, JsonElement s) + { + var level = s.TryGetProperty("level", out var lEl) ? Math.Clamp(lEl.GetInt32(), 1, 6) : 2; + var text = s.TryGetProperty("text", out var tEl) ? tEl.GetString() ?? "" : ""; + sb.AppendLine($"{new string('#', level)} {text}"); + sb.AppendLine(); + } + + private static void RenderParagraph(StringBuilder sb, JsonElement s) + { + var text = s.TryGetProperty("text", out var tEl) ? tEl.GetString() ?? "" : ""; + if (!string.IsNullOrWhiteSpace(text)) + { + sb.AppendLine(text); + sb.AppendLine(); + } + } + + private static void RenderTable(StringBuilder sb, JsonElement s) + { + if (!s.TryGetProperty("headers", out var headersEl) || headersEl.ValueKind != JsonValueKind.Array) + return; + if (!s.TryGetProperty("rows", out var rowsEl) || rowsEl.ValueKind != JsonValueKind.Array) + return; + + var headers = headersEl.EnumerateArray().Select(h => EscapeTableCell(h.GetString() ?? "")).ToList(); + if (headers.Count == 0) return; + + // 헤더 행 + sb.AppendLine("| " + string.Join(" | ", headers) + " |"); + // 구분 행 + sb.AppendLine("| " + string.Join(" | ", headers.Select(_ => "---")) + " |"); + // 데이터 행 + foreach (var row in rowsEl.EnumerateArray()) + { + if (row.ValueKind != JsonValueKind.Array) continue; + var cells = row.EnumerateArray() + .Select(c => EscapeTableCell(c.ValueKind == JsonValueKind.Null ? "" : c.ToString())) + .ToList(); + // 열 수 맞추기 + while (cells.Count < headers.Count) cells.Add(""); + sb.AppendLine("| " + string.Join(" | ", cells.Take(headers.Count)) + " |"); + } + sb.AppendLine(); + } + + private static string EscapeTableCell(string value) + => value.Replace("|", "\\|").Replace("\n", " ").Replace("\r", ""); + + private static void RenderList(StringBuilder sb, JsonElement s) + { + if (!s.TryGetProperty("items", out var itemsEl) || itemsEl.ValueKind != JsonValueKind.Array) + return; + + var ordered = s.TryGetProperty("ordered", out var ordEl) && ordEl.ValueKind == JsonValueKind.True; + int idx = 1; + foreach (var item in itemsEl.EnumerateArray()) + { + var text = item.ValueKind == JsonValueKind.String ? item.GetString() ?? "" : item.ToString(); + // 들여쓰기 보존 (앞에 공백/탭이 있으면 그대로) + var prefix = ordered ? $"{idx++}. " : "- "; + if (text.StartsWith(" ") || text.StartsWith("\t")) + sb.AppendLine(text); // 하위 항목: 이미 들여쓰기 포함 + else + sb.AppendLine($"{prefix}{text}"); + } + sb.AppendLine(); + } + + private static void RenderCallout(StringBuilder sb, JsonElement s) + { + var style = s.TryGetProperty("style", out var stEl) ? stEl.GetString()?.ToUpperInvariant() ?? "INFO" : "INFO"; + var text = s.TryGetProperty("text", out var tEl) ? tEl.GetString() ?? "" : ""; + + // 표준 GitHub/Obsidian 콜아웃 형식 + sb.AppendLine($"> [!{style}]"); + foreach (var line in text.Split('\n')) + sb.AppendLine($"> {line}"); + sb.AppendLine(); + } + + private static void RenderCode(StringBuilder sb, JsonElement s) + { + var lang = s.TryGetProperty("language", out var lEl) ? lEl.GetString() ?? "" : ""; + var code = s.TryGetProperty("code", out var cEl) ? cEl.GetString() ?? "" : ""; + + sb.AppendLine($"```{lang}"); + sb.AppendLine(code); + sb.AppendLine("```"); + sb.AppendLine(); + } + + private static void RenderQuote(StringBuilder sb, JsonElement s) + { + var text = s.TryGetProperty("text", out var tEl) ? tEl.GetString() ?? "" : ""; + var author = s.TryGetProperty("author", out var aEl) ? aEl.GetString() : null; + + foreach (var line in text.Split('\n')) + sb.AppendLine($"> {line}"); + + if (!string.IsNullOrEmpty(author)) + sb.AppendLine($">"); + sb.AppendLine($"> — {author}"); + + sb.AppendLine(); + } } diff --git a/src/AxCopilot/Services/Agent/ModelExecutionProfileCatalog.cs b/src/AxCopilot/Services/Agent/ModelExecutionProfileCatalog.cs new file mode 100644 index 0000000..58f7e0b --- /dev/null +++ b/src/AxCopilot/Services/Agent/ModelExecutionProfileCatalog.cs @@ -0,0 +1,173 @@ +namespace AxCopilot.Services.Agent; + +public static class ModelExecutionProfileCatalog +{ + public sealed record ExecutionPolicy( + string Key, + string Label, + bool ForceInitialToolCall, + bool ForceToolCallAfterPlan, + double? ToolTemperatureCap, + int NoToolResponseThreshold, + int NoToolRecoveryMaxRetries, + int PlanExecutionRetryMax, + int DocumentPlanRetryMax, + bool PreferAggressiveDocumentFallback, + bool ReduceEarlyMemoryPressure, + bool EnablePostToolVerification, + bool EnableCodeQualityGates, + bool EnableDocumentVerificationGate, + bool EnableParallelReadBatch, + int MaxParallelReadBatch, + int CodeVerificationGateMaxRetries, + int HighImpactBuildTestGateMaxRetries, + int FinalReportGateMaxRetries, + int CodeDiffGateMaxRetries, + int RecentExecutionGateMaxRetries, + int ExecutionSuccessGateMaxRetries, + int DocumentVerificationGateMaxRetries, + int TerminalEvidenceGateMaxRetries); + + public static string Normalize(string? key) + { + var normalized = (key ?? "").Trim().ToLowerInvariant(); + return normalized switch + { + "tool_call_strict" => "tool_call_strict", + "reasoning_first" => "reasoning_first", + "fast_readonly" => "fast_readonly", + "document_heavy" => "document_heavy", + _ => "balanced", + }; + } + + public static ExecutionPolicy Get(string? key) + => Normalize(key) switch + { + "tool_call_strict" => new ExecutionPolicy( + "tool_call_strict", + "도구 호출 우선", + ForceInitialToolCall: true, + ForceToolCallAfterPlan: true, + ToolTemperatureCap: 0.2, + NoToolResponseThreshold: 1, + NoToolRecoveryMaxRetries: 1, + PlanExecutionRetryMax: 1, + DocumentPlanRetryMax: 1, + PreferAggressiveDocumentFallback: true, + ReduceEarlyMemoryPressure: true, + EnablePostToolVerification: false, + EnableCodeQualityGates: true, + EnableDocumentVerificationGate: false, + EnableParallelReadBatch: true, + MaxParallelReadBatch: 8, + CodeVerificationGateMaxRetries: 1, + HighImpactBuildTestGateMaxRetries: 1, + FinalReportGateMaxRetries: 1, + CodeDiffGateMaxRetries: 1, + RecentExecutionGateMaxRetries: 0, + ExecutionSuccessGateMaxRetries: 0, + DocumentVerificationGateMaxRetries: 0, + TerminalEvidenceGateMaxRetries: 1), + "reasoning_first" => new ExecutionPolicy( + "reasoning_first", + "추론 우선", + ForceInitialToolCall: false, + ForceToolCallAfterPlan: false, + ToolTemperatureCap: 0.45, + NoToolResponseThreshold: 2, + NoToolRecoveryMaxRetries: 2, + PlanExecutionRetryMax: 2, + DocumentPlanRetryMax: 2, + PreferAggressiveDocumentFallback: false, + ReduceEarlyMemoryPressure: false, + EnablePostToolVerification: true, + EnableCodeQualityGates: true, + EnableDocumentVerificationGate: true, + EnableParallelReadBatch: true, + MaxParallelReadBatch: 6, + CodeVerificationGateMaxRetries: 2, + HighImpactBuildTestGateMaxRetries: 1, + FinalReportGateMaxRetries: 1, + CodeDiffGateMaxRetries: 1, + RecentExecutionGateMaxRetries: 1, + ExecutionSuccessGateMaxRetries: 1, + DocumentVerificationGateMaxRetries: 1, + TerminalEvidenceGateMaxRetries: 1), + "fast_readonly" => new ExecutionPolicy( + "fast_readonly", + "읽기 속도 우선", + ForceInitialToolCall: true, + ForceToolCallAfterPlan: false, + ToolTemperatureCap: 0.25, + NoToolResponseThreshold: 1, + NoToolRecoveryMaxRetries: 1, + PlanExecutionRetryMax: 1, + DocumentPlanRetryMax: 1, + PreferAggressiveDocumentFallback: false, + ReduceEarlyMemoryPressure: true, + EnablePostToolVerification: false, + EnableCodeQualityGates: false, + EnableDocumentVerificationGate: false, + EnableParallelReadBatch: true, + MaxParallelReadBatch: 10, + CodeVerificationGateMaxRetries: 0, + HighImpactBuildTestGateMaxRetries: 0, + FinalReportGateMaxRetries: 0, + CodeDiffGateMaxRetries: 0, + RecentExecutionGateMaxRetries: 0, + ExecutionSuccessGateMaxRetries: 0, + DocumentVerificationGateMaxRetries: 0, + TerminalEvidenceGateMaxRetries: 0), + "document_heavy" => new ExecutionPolicy( + "document_heavy", + "문서 생성 우선", + ForceInitialToolCall: true, + ForceToolCallAfterPlan: true, + ToolTemperatureCap: 0.35, + NoToolResponseThreshold: 1, + NoToolRecoveryMaxRetries: 1, + PlanExecutionRetryMax: 1, + DocumentPlanRetryMax: 0, + PreferAggressiveDocumentFallback: true, + ReduceEarlyMemoryPressure: true, + EnablePostToolVerification: false, + EnableCodeQualityGates: false, + EnableDocumentVerificationGate: false, + EnableParallelReadBatch: true, + MaxParallelReadBatch: 6, + CodeVerificationGateMaxRetries: 0, + HighImpactBuildTestGateMaxRetries: 0, + FinalReportGateMaxRetries: 0, + CodeDiffGateMaxRetries: 0, + RecentExecutionGateMaxRetries: 0, + ExecutionSuccessGateMaxRetries: 0, + DocumentVerificationGateMaxRetries: 0, + TerminalEvidenceGateMaxRetries: 1), + _ => new ExecutionPolicy( + "balanced", + "균형", + ForceInitialToolCall: true, + ForceToolCallAfterPlan: true, + ToolTemperatureCap: 0.35, + NoToolResponseThreshold: 2, + NoToolRecoveryMaxRetries: 2, + PlanExecutionRetryMax: 2, + DocumentPlanRetryMax: 2, + PreferAggressiveDocumentFallback: false, + ReduceEarlyMemoryPressure: false, + EnablePostToolVerification: true, + EnableCodeQualityGates: true, + EnableDocumentVerificationGate: true, + EnableParallelReadBatch: true, + MaxParallelReadBatch: 6, + CodeVerificationGateMaxRetries: 2, + HighImpactBuildTestGateMaxRetries: 1, + FinalReportGateMaxRetries: 1, + CodeDiffGateMaxRetries: 1, + RecentExecutionGateMaxRetries: 1, + ExecutionSuccessGateMaxRetries: 1, + DocumentVerificationGateMaxRetries: 1, + TerminalEvidenceGateMaxRetries: 1), + }; +} diff --git a/src/AxCopilot/Services/Agent/MultiReadTool.cs b/src/AxCopilot/Services/Agent/MultiReadTool.cs index e276f1c..c1a214c 100644 --- a/src/AxCopilot/Services/Agent/MultiReadTool.cs +++ b/src/AxCopilot/Services/Agent/MultiReadTool.cs @@ -1,16 +1,21 @@ -using System.IO; +using System.IO; using System.Text; using System.Text.Json; namespace AxCopilot.Services.Agent; -/// 여러 파일을 한 번에 읽어 결합 반환하는 도구. +/// 여러 파일을 한 번에 읽어 결합 반환하는 도구 (최대 20개). public class MultiReadTool : IAgentTool { + private const int MaxFiles = 20; + private const int DefaultMaxLines = 300; + private const int HardMaxLines = 2000; + public string Name => "multi_read"; public string Description => - "Read multiple files in a single call (max 10). " + + $"Read multiple files in a single call (max {MaxFiles}). " + "Returns concatenated contents with file headers. " + + "Supports line offset, per-file line limits, and optional encoding display. " + "More efficient than calling file_read multiple times."; public ToolParameterSchema Parameters => new() @@ -19,14 +24,24 @@ public class MultiReadTool : IAgentTool { ["paths"] = new() { - Type = "array", - Description = "List of file paths to read (max 10)", - Items = new() { Type = "string", Description = "File path" }, + Type = "array", + Description = $"List of file paths to read (max {MaxFiles})", + Items = new() { Type = "string", Description = "File path (absolute or relative to work folder)" }, }, ["max_lines"] = new() { - Type = "integer", - Description = "Max lines per file (default 200)", + Type = "integer", + Description = $"Max lines to read per file (default {DefaultMaxLines}, max {HardMaxLines})", + }, + ["offset"] = new() + { + Type = "integer", + Description = "1-based line offset: skip this many lines from the start of each file before reading (default 1 = start from first line)", + }, + ["show_encoding"] = new() + { + Type = "boolean", + Description = "If true, include the detected encoding in each file's header (default false)", }, }, Required = ["paths"], @@ -34,61 +49,126 @@ public class MultiReadTool : IAgentTool public Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default) { - var maxLines = args.TryGetProperty("max_lines", out var ml) ? ml.GetInt32() : 200; - if (maxLines <= 0) maxLines = 200; + // --- Parse parameters --- + var maxLines = DefaultMaxLines; + if (args.TryGetProperty("max_lines", out var mlEl)) + { + maxLines = mlEl.GetInt32(); + if (maxLines <= 0) maxLines = DefaultMaxLines; + if (maxLines > HardMaxLines) maxLines = HardMaxLines; + } + // offset is 1-based; convert to 0-based skip count + var offsetParam = 1; + if (args.TryGetProperty("offset", out var offEl)) + { + offsetParam = offEl.GetInt32(); + if (offsetParam < 1) offsetParam = 1; + } + var skipLines = offsetParam - 1; // number of lines to skip (0 = start from line 1) + + var showEncoding = args.TryGetProperty("show_encoding", out var seEl) && seEl.GetBoolean(); + + // --- Validate paths array --- if (!args.TryGetProperty("paths", out var pathsEl) || pathsEl.ValueKind != JsonValueKind.Array) return Task.FromResult(ToolResult.Fail("'paths'는 문자열 배열이어야 합니다.")); - var paths = new List(); + var rawPaths = new List(); foreach (var p in pathsEl.EnumerateArray()) { var s = p.GetString(); - if (!string.IsNullOrEmpty(s)) paths.Add(s); + if (!string.IsNullOrEmpty(s)) rawPaths.Add(s); } - if (paths.Count == 0) + if (rawPaths.Count == 0) return Task.FromResult(ToolResult.Fail("읽을 파일이 없습니다.")); - if (paths.Count > 10) - return Task.FromResult(ToolResult.Fail("최대 10개 파일만 지원합니다.")); + if (rawPaths.Count > MaxFiles) + return Task.FromResult(ToolResult.Fail($"최대 {MaxFiles}개 파일만 지원합니다. (요청: {rawPaths.Count}개)")); - var sb = new StringBuilder(); + // --- Read each file --- + var sb = new StringBuilder(); var readCount = 0; - foreach (var rawPath in paths) + foreach (var rawPath in rawPaths) { - var path = Path.IsPathRooted(rawPath) ? rawPath : Path.Combine(context.WorkFolder, rawPath); - - sb.AppendLine($"═══ {Path.GetFileName(path)} ═══"); - sb.AppendLine($"Path: {path}"); + var path = FileReadTool.ResolvePath(rawPath, context.WorkFolder); + var fileName = Path.GetFileName(path); if (!context.IsPathAllowed(path)) { + AppendHeader(sb, fileName, path, encodingLabel: null, showEncoding, totalLines: null); sb.AppendLine("[접근 차단됨]"); + sb.AppendLine(); + continue; } - else if (!File.Exists(path)) + + if (!File.Exists(path)) { + AppendHeader(sb, fileName, path, encodingLabel: null, showEncoding, totalLines: null); sb.AppendLine("[파일 없음]"); + sb.AppendLine(); + continue; } - else + + try { - try + var read = TextFileCodec.ReadAllText(path); + var allLines = TextFileCodec.SplitLines(read.Text); + var totalLines = allLines.Length; + var encLabel = showEncoding ? read.Encoding.WebName : null; + + AppendHeader(sb, fileName, path, encLabel, showEncoding, totalLines); + + // Apply offset (skip) and max_lines limit + var startIdx = Math.Min(skipLines, totalLines); + var available = totalLines - startIdx; + var takeCount = Math.Min(available, maxLines); + var truncated = available > maxLines; + + for (var i = 0; i < takeCount; i++) { - var lines = File.ReadLines(path).Take(maxLines).ToList(); - for (var i = 0; i < lines.Count; i++) - sb.AppendLine($"{i + 1}\t{lines[i]}"); - if (lines.Count >= maxLines) - sb.AppendLine($"... (이후 생략, max_lines={maxLines})"); - readCount++; - } - catch (Exception ex) - { - sb.AppendLine($"[읽기 오류: {ex.Message}]"); + var lineNum = startIdx + i + 1; // 1-based line number in the original file + sb.Append(lineNum); + sb.Append('\t'); + sb.AppendLine(allLines[startIdx + i]); } + + if (truncated) + sb.AppendLine($"... (이후 생략: {available - takeCount}줄 더 있음, max_lines={maxLines})"); + + readCount++; } + catch (Exception ex) + { + sb.AppendLine($"[읽기 오류: {ex.Message}]"); + } + sb.AppendLine(); } - return Task.FromResult(ToolResult.Ok($"{readCount}/{paths.Count}개 파일 읽기 완료.\n\n{sb}")); + var summary = $"{readCount}/{rawPaths.Count}개 파일 읽기 완료 " + + $"(max_lines={maxLines}, offset={offsetParam})\n\n{sb}"; + return Task.FromResult(ToolResult.Ok(summary)); + } + + /// + /// 파일 헤더 블록을 StringBuilder에 추가합니다. + /// + private static void AppendHeader( + StringBuilder sb, + string fileName, + string fullPath, + string? encodingLabel, + bool showEncoding, + int? totalLines) + { + sb.AppendLine($"═══ {fileName} ═══"); + sb.AppendLine($"Path: {fullPath}"); + + if (totalLines.HasValue) + sb.AppendLine($"Total lines: {totalLines.Value:N0}"); + + if (showEncoding && encodingLabel != null) + sb.AppendLine($"Encoding: {encodingLabel}"); } } diff --git a/src/AxCopilot/Services/Agent/PptxSkill.cs b/src/AxCopilot/Services/Agent/PptxSkill.cs index 3c32128..7981f62 100644 --- a/src/AxCopilot/Services/Agent/PptxSkill.cs +++ b/src/AxCopilot/Services/Agent/PptxSkill.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Text.Json; using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; @@ -9,61 +9,310 @@ using A = DocumentFormat.OpenXml.Drawing; namespace AxCopilot.Services.Agent; /// -/// PowerPoint (.pptx) 프레젠테이션을 네이티브 생성하는 스킬. +/// 고품질 PowerPoint (.pptx) 프레젠테이션을 네이티브 생성하는 스킬. /// OpenXML SDK를 사용하여 Python/Node 의존 없이 PPTX를 생성합니다. +/// +/// 개선 사항 (v3): +/// - ThemeStyle 시스템: 색상 + 레이아웃/구도를 함께 정의 +/// - 타이틀 슬라이드 5가지 변형: bar_left, top_band, center_bold, diagonal, minimal +/// - 콘텐츠 슬라이드 4가지 변형: header_bar, card, minimal, split_accent +/// - 섹션 슬라이드 3가지 변형: full_color, centered_line, side_accent +/// - 장식 원형 도형, 카드 박스 지원 +/// - 한글 폰트 (맑은 고딕) + 영문 폰트 (Calibri) +/// - 진짜 OpenXML 테이블, 불릿 포인트, 16:9 와이드스크린 +/// - 20가지 테마 (각각 고유한 색상 + 레이아웃 조합) /// public class PptxSkill : IAgentTool { public string Name => "pptx_create"; public string Description => - "Create a PowerPoint (.pptx) presentation. " + - "Supports slide layouts: title (title+subtitle), content (title+body text), " + - "two_column (title+left+right), table (title+headers+rows), blank. " + - "No external runtime required (native OpenXML)."; + "Create a high-quality PowerPoint (.pptx) presentation. " + + "Layouts: title (title+subtitle), content (title+bullet body), " + + "section (chapter divider), two_column (title+left+right), " + + "table (title+real styled table), quote (styled quotation slide), blank. " + + "Built-in themes (each controls BOTH colors AND layout/composition): " + + "professional (bar_left), modern (top_band+card), dark (center_bold), " + + "minimal (minimal), vibrant (diagonal+split_accent), navy (bar_left+side_accent), " + + "ocean (top_band+card, centered), forest (bar_left+header_bar), " + + "earth (top_band+card), nordic (bar_left+minimal), " + + "sunset (diagonal+split_accent), rose (top_band+card), " + + "amber (bar_left+header_bar), crimson (center_bold), " + + "slate (minimal), indigo (center_bold+card), " + + "emerald (bar_left+header_bar), corporate (top_band+header_bar), " + + "pastel (minimal+card), midnight (center_bold+split_accent). " + + "Custom theme: set theme='custom' and provide custom_colors object. " + + "Template theme: set theme_file to an existing .pptx path to extract its colors. " + + "No external runtime required (native OpenXML). Korean font (맑은 고딕) + Calibri."; public ToolParameterSchema Parameters => new() { Properties = new() { - ["path"] = new() { Type = "string", Description = "Output file path (.pptx). Relative to work folder." }, - ["title"] = new() { Type = "string", Description = "Presentation title (used on first slide if no explicit title slide)." }, + ["path"] = new() { Type = "string", Description = "Output file path (.pptx). Relative to work folder." }, + ["title"] = new() { Type = "string", Description = "Presentation title." }, ["slides"] = new() { Type = "array", - Description = "Array of slide objects. Each slide: " + - "{\"layout\": \"title|content|two_column|table|blank\", " + - "\"title\": \"Slide Title\", " + - "\"subtitle\": \"...\", " + // title layout - "\"body\": \"...\", " + // content layout - "\"left\": \"...\", \"right\": \"...\", " + // two_column - "\"headers\": [...], \"rows\": [[...]], " + // table + Description = + "Array of slide objects. Each slide: " + + "{\"layout\": \"title|content|section|two_column|table|quote|blank\", " + + "\"title\": \"...\", \"subtitle\": \"...\", " + + "\"body\": \"bullet1\\nbullet2\\n - sub-bullet\", " + + "\"left\": \"...\", \"right\": \"...\", " + + "\"headers\": [\"col1\",\"col2\"], \"rows\": [[\"a\",\"b\"]], " + + "\"quote\": \"Quote text\", \"author\": \"Author\", " + "\"notes\": \"Speaker notes\"}", Items = new() { Type = "object" } }, - ["theme"] = new() + ["theme"] = new() { Type = "string", - Description = "Color theme: professional (blue), modern (teal), dark (dark gray), minimal (light). Default: professional", - Enum = ["professional", "modern", "dark", "minimal"] + Description = "Built-in theme name (controls colors AND layout/composition), or 'custom' to use custom_colors. " + + "Built-in: professional, modern, dark, minimal, vibrant, navy, " + + "ocean, forest, sunset, rose, slate, amber, indigo, emerald, crimson, " + + "corporate, pastel, midnight, earth, nordic. Default: professional", + }, + ["theme_file"] = new() + { + Type = "string", + Description = "Path to an existing .pptx file. Its theme colors will be extracted and applied. " + + "Overrides 'theme' if both are specified.", + }, + ["custom_colors"] = new() + { + Type = "object", + Description = "Custom color set when theme='custom'. All values are hex strings WITHOUT '#'. " + + "Fields: primary, accent, text_dark, text_light, bg, bg_alt, header_bg, header_text. " + + "Example: {\"primary\":\"1F4E79\",\"accent\":\"2E75B6\",\"text_dark\":\"1A1A2E\",...}", + }, + ["aspect"] = new() + { + Type = "string", + Description = "Slide aspect ratio. Default: widescreen (16:9)", + Enum = ["widescreen", "standard"] }, }, - Required = ["path", "slides"] + Required = ["slides"] }; - // 테마별 색상 정의 - private static readonly Dictionary Themes = new() + // ── 색상 레코드 ──────────────────────────────────────────────────────────── + // Primary, Accent, TextDark, TextLight, Bg, BgAlt, HeaderBg, HeaderText + private record ThemeColors( + string Primary, string Accent, + string TextDark, string TextLight, + string Bg, string BgAlt, + string HeaderBg, string HeaderText); + + // ── 레이아웃/구도 레코드 ────────────────────────────────────────────────── + private record ThemeLayout( + // 타이틀 슬라이드 변형 + string TitleVariant, // "bar_left" | "top_band" | "center_bold" | "diagonal" | "minimal" + // 내용 슬라이드 변형 + string ContentVariant, // "header_bar" | "card" | "minimal" | "split_accent" + // 섹션 슬라이드 변형 + string SectionVariant, // "full_color" | "centered_line" | "side_accent" + // 텍스트 정렬 + string TitleAlign, // "l" | "ctr" | "r" + // 액센트 바 위치 + string AccentPos, // "left" | "top" | "bottom" | "right" | "none" + int AccentThickPct, // 두께 % of slide dimension (1~15) + // 타이포그래피 (100분의 1 pt 단위) + int TitlePt, // 타이틀 슬라이드 제목 (기본 4400 = 44pt) + int SubtitlePt, // 부제목 (기본 2000) + int SlideTitlePt, // 슬라이드 헤더 제목 (기본 2800) + int BodyPt, // 본문 텍스트 (기본 1600) + // 장식 + bool HasDecorCircle, // 장식 원형 도형 추가 여부 + bool ContentHasCard // 콘텐츠 영역에 박스 카드 여부 + ); + + // ── 통합 테마 레코드 ────────────────────────────────────────────────────── + private record FullTheme(ThemeColors Colors, ThemeLayout Layout); + + // ── 20가지 테마 사전 ────────────────────────────────────────────────────── + private static readonly Dictionary FullThemes = new(StringComparer.OrdinalIgnoreCase) { - ["professional"] = ("2B579A", "4B5EFC", "1A1A2E", "FFFFFF", "FFFFFF"), - ["modern"] = ("0D9488", "06B6D4", "1A1A2E", "FFFFFF", "FFFFFF"), - ["dark"] = ("374151", "6366F1", "F9FAFB", "FFFFFF", "1F2937"), - ["minimal"] = ("6B7280", "3B82F6", "111827", "FFFFFF", "FAFAFA"), + // ── 1. professional ─ 좌측 바, 헤더 바, 전통적인 비즈니스 + ["professional"] = new( + new("1F4E79", "2E75B6", "1A1A2E", "FFFFFF", "FFFFFF", "F2F7FC", "1F4E79", "FFFFFF"), + new(TitleVariant: "bar_left", ContentVariant: "header_bar", SectionVariant: "full_color", + TitleAlign: "l", AccentPos: "left", AccentThickPct: 5, + TitlePt: 4400, SubtitlePt: 2000, SlideTitlePt: 2800, BodyPt: 1600, + HasDecorCircle: false, ContentHasCard: false)), + + // ── 2. modern ─ 상단 밴드, 카드 콘텐츠, 청록색 계열 + ["modern"] = new( + new("0D9488", "0891B2", "1A1A2E", "FFFFFF", "FFFFFF", "F0FDFA", "0D9488", "FFFFFF"), + new(TitleVariant: "top_band", ContentVariant: "card", SectionVariant: "centered_line", + TitleAlign: "l", AccentPos: "top", AccentThickPct: 4, + TitlePt: 4200, SubtitlePt: 1900, SlideTitlePt: 2600, BodyPt: 1600, + HasDecorCircle: false, ContentHasCard: true)), + + // ── 3. dark ─ 중앙 굵은 제목, 어두운 배경, 장식 원 + ["dark"] = new( + new("1E293B", "6366F1", "F1F5F9", "FFFFFF", "0F172A", "1E293B", "374151", "F1F5F9"), + new(TitleVariant: "center_bold", ContentVariant: "minimal", SectionVariant: "full_color", + TitleAlign: "ctr", AccentPos: "bottom", AccentThickPct: 3, + TitlePt: 4600, SubtitlePt: 2000, SlideTitlePt: 2800, BodyPt: 1700, + HasDecorCircle: true, ContentHasCard: false)), + + // ── 4. minimal ─ 최소한의 장식, 깔끔한 흰 배경 + ["minimal"] = new( + new("374151", "3B82F6", "111827", "FFFFFF", "FFFFFF", "F9FAFB", "F3F4F6", "111827"), + new(TitleVariant: "minimal", ContentVariant: "minimal", SectionVariant: "centered_line", + TitleAlign: "l", AccentPos: "bottom", AccentThickPct: 2, + TitlePt: 5000, SubtitlePt: 2000, SlideTitlePt: 2800, BodyPt: 1600, + HasDecorCircle: false, ContentHasCard: false)), + + // ── 5. vibrant ─ 대각선 분할, 사이드 액센트, 생동감 있는 색상 + ["vibrant"] = new( + new("7C3AED", "EC4899", "1A1A2E", "FFFFFF", "FFFFFF", "FAF5FF", "7C3AED", "FFFFFF"), + new(TitleVariant: "diagonal", ContentVariant: "split_accent", SectionVariant: "full_color", + TitleAlign: "l", AccentPos: "left", AccentThickPct: 8, + TitlePt: 4400, SubtitlePt: 2000, SlideTitlePt: 2600, BodyPt: 1600, + HasDecorCircle: true, ContentHasCard: false)), + + // ── 6. navy ─ 좌측 바, 사이드 액센트 섹션, 네이비 블루 + ["navy"] = new( + new("0F2744", "1E6FBF", "1A1A2E", "FFFFFF", "FFFFFF", "EEF4FB", "0F2744", "FFFFFF"), + new(TitleVariant: "bar_left", ContentVariant: "header_bar", SectionVariant: "side_accent", + TitleAlign: "l", AccentPos: "left", AccentThickPct: 6, + TitlePt: 4200, SubtitlePt: 1900, SlideTitlePt: 2800, BodyPt: 1600, + HasDecorCircle: false, ContentHasCard: false)), + + // ── 7. ocean ─ 상단 밴드, 카드, 중앙 정렬, 청색 계열 + ["ocean"] = new( + new("0369A1", "06B6D4", "0C4A6E", "FFFFFF", "FFFFFF", "F0F9FF", "0369A1", "FFFFFF"), + new(TitleVariant: "top_band", ContentVariant: "card", SectionVariant: "centered_line", + TitleAlign: "ctr", AccentPos: "top", AccentThickPct: 4, + TitlePt: 4200, SubtitlePt: 1900, SlideTitlePt: 2600, BodyPt: 1600, + HasDecorCircle: false, ContentHasCard: true)), + + // ── 8. forest ─ 좌측 바, 헤더 바, 초록 자연 + ["forest"] = new( + new("166534", "22C55E", "14532D", "FFFFFF", "FFFFFF", "F0FDF4", "166534", "FFFFFF"), + new(TitleVariant: "bar_left", ContentVariant: "header_bar", SectionVariant: "full_color", + TitleAlign: "l", AccentPos: "left", AccentThickPct: 5, + TitlePt: 4400, SubtitlePt: 2000, SlideTitlePt: 2800, BodyPt: 1600, + HasDecorCircle: false, ContentHasCard: false)), + + // ── 9. earth ─ 상단 밴드, 카드, 따뜻한 흙색 + ["earth"] = new( + new("78350F", "D97706", "1C1917", "FFFFFF", "FEFCE8", "FEF3C7", "78350F", "FFFFFF"), + new(TitleVariant: "top_band", ContentVariant: "card", SectionVariant: "full_color", + TitleAlign: "l", AccentPos: "top", AccentThickPct: 4, + TitlePt: 4200, SubtitlePt: 1900, SlideTitlePt: 2600, BodyPt: 1600, + HasDecorCircle: false, ContentHasCard: true)), + + // ── 10. nordic ─ 좌측 바, 미니멀 콘텐츠, 북유럽 스타일 + ["nordic"] = new( + new("1E3A5F", "4FC3F7", "1A2332", "FFFFFF", "F0F4F8", "E1EAF2", "1E3A5F", "FFFFFF"), + new(TitleVariant: "bar_left", ContentVariant: "minimal", SectionVariant: "centered_line", + TitleAlign: "l", AccentPos: "left", AccentThickPct: 4, + TitlePt: 4400, SubtitlePt: 2000, SlideTitlePt: 2800, BodyPt: 1600, + HasDecorCircle: false, ContentHasCard: false)), + + // ── 11. sunset ─ 대각선 분할, 사이드 액센트, 따뜻한 오렌지 + ["sunset"] = new( + new("C2410C", "F59E0B", "1A1A2E", "FFFFFF", "FFFFFF", "FFF7ED", "C2410C", "FFFFFF"), + new(TitleVariant: "diagonal", ContentVariant: "split_accent", SectionVariant: "full_color", + TitleAlign: "l", AccentPos: "left", AccentThickPct: 7, + TitlePt: 4400, SubtitlePt: 2000, SlideTitlePt: 2600, BodyPt: 1600, + HasDecorCircle: true, ContentHasCard: false)), + + // ── 12. rose ─ 상단 밴드, 카드, 로즈 핑크 + ["rose"] = new( + new("9D174D", "F43F5E", "1A1A2E", "FFFFFF", "FFFFFF", "FFF1F2", "9D174D", "FFFFFF"), + new(TitleVariant: "top_band", ContentVariant: "card", SectionVariant: "centered_line", + TitleAlign: "l", AccentPos: "top", AccentThickPct: 4, + TitlePt: 4200, SubtitlePt: 1900, SlideTitlePt: 2600, BodyPt: 1600, + HasDecorCircle: false, ContentHasCard: true)), + + // ── 13. amber ─ 좌측 바, 헤더 바, 황금 호박색 + ["amber"] = new( + new("92400E", "F59E0B", "1C1917", "FFFFFF", "FFFBEB", "FEF3C7", "92400E", "FFFFFF"), + new(TitleVariant: "bar_left", ContentVariant: "header_bar", SectionVariant: "full_color", + TitleAlign: "l", AccentPos: "left", AccentThickPct: 5, + TitlePt: 4200, SubtitlePt: 2000, SlideTitlePt: 2800, BodyPt: 1600, + HasDecorCircle: false, ContentHasCard: false)), + + // ── 14. crimson ─ 중앙 굵은 제목, 장식 원, 크림슨 레드 + ["crimson"] = new( + new("7F1D1D", "EF4444", "1A1A2E", "FFFFFF", "FFFFFF", "FEF2F2", "7F1D1D", "FFFFFF"), + new(TitleVariant: "center_bold", ContentVariant: "minimal", SectionVariant: "full_color", + TitleAlign: "ctr", AccentPos: "bottom", AccentThickPct: 3, + TitlePt: 4600, SubtitlePt: 2000, SlideTitlePt: 2800, BodyPt: 1700, + HasDecorCircle: true, ContentHasCard: false)), + + // ── 15. slate ─ 미니멀, 심플한 슬레이트 그레이 + ["slate"] = new( + new("334155", "64748B", "0F172A", "FFFFFF", "FFFFFF", "F8FAFC", "334155", "FFFFFF"), + new(TitleVariant: "minimal", ContentVariant: "minimal", SectionVariant: "centered_line", + TitleAlign: "l", AccentPos: "bottom", AccentThickPct: 2, + TitlePt: 4800, SubtitlePt: 2000, SlideTitlePt: 2800, BodyPt: 1600, + HasDecorCircle: false, ContentHasCard: false)), + + // ── 16. indigo ─ 중앙 굵은 제목, 카드 콘텐츠, 인디고 블루 + ["indigo"] = new( + new("3730A3", "6366F1", "1E1B4B", "FFFFFF", "FFFFFF", "EEF2FF", "3730A3", "FFFFFF"), + new(TitleVariant: "center_bold", ContentVariant: "card", SectionVariant: "full_color", + TitleAlign: "ctr", AccentPos: "bottom", AccentThickPct: 3, + TitlePt: 4600, SubtitlePt: 2000, SlideTitlePt: 2800, BodyPt: 1600, + HasDecorCircle: true, ContentHasCard: true)), + + // ── 17. emerald ─ 좌측 바, 헤더 바, 에메랄드 그린 + ["emerald"] = new( + new("065F46", "10B981", "022C22", "FFFFFF", "FFFFFF", "ECFDF5", "065F46", "FFFFFF"), + new(TitleVariant: "bar_left", ContentVariant: "header_bar", SectionVariant: "side_accent", + TitleAlign: "l", AccentPos: "left", AccentThickPct: 5, + TitlePt: 4400, SubtitlePt: 2000, SlideTitlePt: 2800, BodyPt: 1600, + HasDecorCircle: false, ContentHasCard: false)), + + // ── 18. corporate ─ 상단 밴드, 헤더 바, 골드 포인트 기업 스타일 + ["corporate"] = new( + new("1E3A5F", "B8860B", "1A1A2E", "FFFFFF", "FFFFFF", "F5F5F0", "1E3A5F", "FFFFFF"), + new(TitleVariant: "top_band", ContentVariant: "header_bar", SectionVariant: "side_accent", + TitleAlign: "l", AccentPos: "top", AccentThickPct: 4, + TitlePt: 4200, SubtitlePt: 1900, SlideTitlePt: 2800, BodyPt: 1600, + HasDecorCircle: false, ContentHasCard: false)), + + // ── 19. pastel ─ 미니멀, 카드 콘텐츠, 밝고 부드러운 파스텔 + ["pastel"] = new( + new("6B7280", "A78BFA", "374151", "FFFFFF", "FAFAFA", "F3F4F6", "E5E7EB", "374151"), + new(TitleVariant: "minimal", ContentVariant: "card", SectionVariant: "centered_line", + TitleAlign: "l", AccentPos: "bottom", AccentThickPct: 2, + TitlePt: 4600, SubtitlePt: 2000, SlideTitlePt: 2600, BodyPt: 1600, + HasDecorCircle: false, ContentHasCard: true)), + + // ── 20. midnight ─ 중앙 굵은 제목, 사이드 액센트, 미드나잇 다크 + ["midnight"] = new( + new("0F0F1A", "818CF8", "E2E8F0", "FFFFFF", "0A0A14", "13131F", "1E1E35", "E2E8F0"), + new(TitleVariant: "center_bold", ContentVariant: "split_accent", SectionVariant: "full_color", + TitleAlign: "ctr", AccentPos: "bottom", AccentThickPct: 3, + TitlePt: 4600, SubtitlePt: 2000, SlideTitlePt: 2800, BodyPt: 1700, + HasDecorCircle: true, ContentHasCard: false)), }; + private const string FontKo = "맑은 고딕"; + private const string FontEn = "Calibri"; + public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { - var path = args.GetProperty("path").GetString() ?? ""; var presTitle = args.TryGetProperty("title", out var tt) ? tt.GetString() ?? "Presentation" : "Presentation"; - var theme = args.TryGetProperty("theme", out var th) ? th.GetString() ?? "professional" : "professional"; + string path; + if (args.TryGetProperty("path", out var pathEl) && pathEl.ValueKind == JsonValueKind.String + && !string.IsNullOrWhiteSpace(pathEl.GetString())) + { + path = pathEl.GetString()!; + } + else + { + var safe = System.Text.RegularExpressions.Regex.Replace(presTitle, @"[\\/:*?""<>|]", "_").Trim().TrimEnd('.'); + if (safe.Length > 60) safe = safe[..60].TrimEnd(); + path = (string.IsNullOrWhiteSpace(safe) ? "presentation" : safe) + ".pptx"; + } + var theme = args.TryGetProperty("theme", out var th) ? th.GetString() ?? "professional" : "professional"; + var aspect = args.TryGetProperty("aspect", out var asp) ? asp.GetString() ?? "widescreen" : "widescreen"; if (!args.TryGetProperty("slides", out var slidesEl) || slidesEl.ValueKind != JsonValueKind.Array) return ToolResult.Fail("slides 배열이 필요합니다."); @@ -81,94 +330,164 @@ public class PptxSkill : IAgentTool var dir = Path.GetDirectoryName(fullPath); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); - if (!Themes.TryGetValue(theme, out var colors)) - colors = Themes["professional"]; + // ── 테마 결정 우선순위: theme_file > custom_colors > theme 이름 ────── + FullTheme fullTheme; + + if (args.TryGetProperty("theme_file", out var tfEl) && !string.IsNullOrEmpty(tfEl.GetString())) + { + // 기존 PPTX에서 테마 색상 추출 → default layout (professional) + var tfPath = FileReadTool.ResolvePath(tfEl.GetString()!, context.WorkFolder); + var extracted = ExtractThemeFromPptx(tfPath); + var baseLayout = FullThemes["professional"].Layout; + fullTheme = extracted != null + ? new FullTheme(extracted, baseLayout) + : FullThemes["professional"]; + } + else if (string.Equals(theme, "custom", StringComparison.OrdinalIgnoreCase) + && args.TryGetProperty("custom_colors", out var ccEl) + && ccEl.ValueKind == JsonValueKind.Object) + { + // 사용자 지정 색상 → default layout (professional) + static string Hex(JsonElement obj, string key, string fallback) => + obj.TryGetProperty(key, out var v) ? (v.GetString() ?? fallback).TrimStart('#') : fallback; + var customColors = new ThemeColors( + Primary: Hex(ccEl, "primary", "1F4E79"), + Accent: Hex(ccEl, "accent", "2E75B6"), + TextDark: Hex(ccEl, "text_dark", "1A1A2E"), + TextLight: Hex(ccEl, "text_light", "FFFFFF"), + Bg: Hex(ccEl, "bg", "FFFFFF"), + BgAlt: Hex(ccEl, "bg_alt", "F2F7FC"), + HeaderBg: Hex(ccEl, "header_bg", "1F4E79"), + HeaderText: Hex(ccEl, "header_text", "FFFFFF")); + fullTheme = new FullTheme(customColors, FullThemes["professional"].Layout); + } + else + { + if (!FullThemes.TryGetValue(theme ?? "professional", out fullTheme!)) + fullTheme = FullThemes["professional"]; + } + + // 슬라이드 치수 (EMU: 1 inch = 914,400 EMU) + var isWide = !string.Equals(aspect, "standard", StringComparison.OrdinalIgnoreCase); + var slideW = isWide ? 12192000L : 9144000L; + var slideH = 6858000L; try { using var pres = PresentationDocument.Create(fullPath, PresentationDocumentType.Presentation); var presPart = pres.AddPresentationPart(); presPart.Presentation = new P.Presentation(); - presPart.Presentation.SlideIdList = new SlideIdList(); - presPart.Presentation.SlideSize = new SlideSize - { - Cx = 12192000, // 10 inches - Cy = 6858000, // 7.5 inches - }; - presPart.Presentation.NotesSize = new NotesSize { Cx = 6858000, Cy = 9144000 }; - uint slideId = 256; - int slideCount = 0; - - foreach (var slideEl in slidesEl.EnumerateArray()) - { - var layout = slideEl.TryGetProperty("layout", out var lay) ? lay.GetString() ?? "content" : "content"; - var slideTitle = slideEl.TryGetProperty("title", out var st) ? st.GetString() ?? "" : ""; - var subtitle = slideEl.TryGetProperty("subtitle", out var sub) ? sub.GetString() ?? "" : ""; - var body = slideEl.TryGetProperty("body", out var bd) ? bd.GetString() ?? "" : ""; - var left = slideEl.TryGetProperty("left", out var lf) ? lf.GetString() ?? "" : ""; - var right = slideEl.TryGetProperty("right", out var rt) ? rt.GetString() ?? "" : ""; - var notes = slideEl.TryGetProperty("notes", out var nt) ? nt.GetString() ?? "" : ""; - - var slidePart = presPart.AddNewPart(); - slidePart.Slide = new Slide(new CommonSlideData(new ShapeTree( + // ── SlideMaster → SlideLayout → Slide 체인 구성 ──────────────── + var masterPart = presPart.AddNewPart(); + masterPart.SlideMaster = new SlideMaster( + new CommonSlideData(new ShapeTree( new P.NonVisualGroupShapeProperties( new P.NonVisualDrawingProperties { Id = 1, Name = "" }, new P.NonVisualGroupShapeDrawingProperties(), new ApplicationNonVisualDrawingProperties()), - new GroupShapeProperties(new A.TransformGroup()) - ))); + new GroupShapeProperties(new A.TransformGroup()))), + new P.ColorMap + { + Background1 = A.ColorSchemeIndexValues.Light1, + Text1 = A.ColorSchemeIndexValues.Dark1, + Background2 = A.ColorSchemeIndexValues.Light2, + Text2 = A.ColorSchemeIndexValues.Dark2, + Accent1 = A.ColorSchemeIndexValues.Accent1, + Accent2 = A.ColorSchemeIndexValues.Accent2, + Accent3 = A.ColorSchemeIndexValues.Accent3, + Accent4 = A.ColorSchemeIndexValues.Accent4, + Accent5 = A.ColorSchemeIndexValues.Accent5, + Accent6 = A.ColorSchemeIndexValues.Accent6, + Hyperlink = A.ColorSchemeIndexValues.Hyperlink, + FollowedHyperlink = A.ColorSchemeIndexValues.FollowedHyperlink, + }, + new SlideLayoutIdList()); - var shapeTree = slidePart.Slide.CommonSlideData!.ShapeTree!; - uint shapeId = 2; + var layoutPart = masterPart.AddNewPart(); + layoutPart.SlideLayout = new SlideLayout( + new CommonSlideData(new ShapeTree( + new P.NonVisualGroupShapeProperties( + new P.NonVisualDrawingProperties { Id = 1, Name = "" }, + new P.NonVisualGroupShapeDrawingProperties(), + new ApplicationNonVisualDrawingProperties()), + new GroupShapeProperties(new A.TransformGroup())))) + { Type = SlideLayoutValues.Blank, Preserve = true }; + layoutPart.AddPart(masterPart); - switch (layout) + masterPart.SlideMaster.SlideLayoutIdList!.AppendChild(new SlideLayoutId + { + Id = 2049U, + RelationshipId = masterPart.GetIdOfPart(layoutPart) + }); + presPart.Presentation.SlideMasterIdList = new SlideMasterIdList( + new SlideMasterId { Id = 2147483648U, RelationshipId = presPart.GetIdOfPart(masterPart) }); + + presPart.Presentation.SlideIdList = new SlideIdList(); + presPart.Presentation.SlideSize = new SlideSize + { + Cx = (Int32Value)(int)slideW, + Cy = (Int32Value)(int)slideH, + Type = isWide ? SlideSizeValues.Screen16x9 : SlideSizeValues.Screen4x3, + }; + presPart.Presentation.NotesSize = new NotesSize { Cx = 6858000, Cy = 9144000 }; + + masterPart.SlideMaster.Save(); + layoutPart.SlideLayout.Save(); + + // ── 슬라이드 생성 ──────────────────────────────────────────────── + uint slideId = 256; + int slideCount = 0; + + foreach (var slideEl in slidesEl.EnumerateArray()) + { + var layout = slideEl.TryGetProperty("layout", out var lay) ? lay.GetString() ?? "content" : "content"; + var slidePart = presPart.AddNewPart(); + slidePart.AddPart(layoutPart); + + var shapeTree = new ShapeTree( + new P.NonVisualGroupShapeProperties( + new P.NonVisualDrawingProperties { Id = 1, Name = "" }, + new P.NonVisualGroupShapeDrawingProperties(), + new ApplicationNonVisualDrawingProperties()), + new GroupShapeProperties(new A.TransformGroup())); + + slidePart.Slide = new Slide(new CommonSlideData(shapeTree)); + uint sid = 2; + + switch (layout.ToLowerInvariant()) { case "title": - // 배경 색상 박스 - AddRectangle(shapeTree, ref shapeId, 0, 0, 12192000, 6858000, colors.Primary); - // 타이틀 - AddTextBox(shapeTree, ref shapeId, 600000, 2000000, 10992000, 1400000, - slideTitle, 3600, colors.TextLight, true); - // 서브타이틀 - if (!string.IsNullOrEmpty(subtitle)) - AddTextBox(shapeTree, ref shapeId, 600000, 3600000, 10992000, 800000, - subtitle, 2000, colors.TextLight, false); + BuildTitleSlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid); + break; + case "section": + BuildSectionSlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid); + break; + case "quote": + BuildQuoteSlide(shapeTree, slideEl, fullTheme.Colors, slideW, slideH, ref sid); break; - case "two_column": - AddTextBox(shapeTree, ref shapeId, 600000, 300000, 10992000, 800000, - slideTitle, 2800, colors.Primary, true); - // 왼쪽 컬럼 - AddTextBox(shapeTree, ref shapeId, 600000, 1300000, 5200000, 5000000, - left, 1600, colors.TextDark, false); - // 오른쪽 컬럼 - AddTextBox(shapeTree, ref shapeId, 6400000, 1300000, 5200000, 5000000, - right, 1600, colors.TextDark, false); + BuildTwoColumnSlide(shapeTree, slideEl, fullTheme.Colors, slideW, slideH, ref sid); break; - case "table": - AddTextBox(shapeTree, ref shapeId, 600000, 300000, 10992000, 800000, - slideTitle, 2800, colors.Primary, true); - // 테이블은 텍스트로 시뮬레이션 (OpenXML 테이블은 매우 복잡) - var tableText = FormatTableAsText(slideEl, colors); - AddTextBox(shapeTree, ref shapeId, 600000, 1300000, 10992000, 5000000, - tableText, 1400, colors.TextDark, false); + BuildTableSlide(shapeTree, slideEl, fullTheme.Colors, slideW, slideH, ref sid); break; - case "blank": - // 빈 슬라이드 break; - default: // content - AddTextBox(shapeTree, ref shapeId, 600000, 300000, 10992000, 800000, - slideTitle, 2800, colors.Primary, true); - AddTextBox(shapeTree, ref shapeId, 600000, 1300000, 10992000, 5000000, - body, 1600, colors.TextDark, false); + BuildContentSlide(shapeTree, slideEl, fullTheme, slideW, slideH, ref sid); break; } - // 슬라이드 등록 + // 발표자 노트 + if (slideEl.TryGetProperty("notes", out var notesEl) && + notesEl.ValueKind == JsonValueKind.String && + !string.IsNullOrWhiteSpace(notesEl.GetString())) + { + AddNotesSlide(slidePart, notesEl.GetString()!); + } + + slidePart.Slide.Save(); presPart.Presentation.SlideIdList.AppendChild(new SlideId { Id = slideId++, @@ -178,93 +497,841 @@ public class PptxSkill : IAgentTool } presPart.Presentation.Save(); - return ToolResult.Ok( - $"✅ PPTX 생성 완료: {Path.GetFileName(fullPath)} ({slideCount}슬라이드, 테마: {theme})", + $"✅ PPTX 생성 완료: {Path.GetFileName(fullPath)} ({slideCount}슬라이드, {theme} 테마, {(isWide ? "16:9" : "4:3")})", fullPath); } catch (Exception ex) { + if (File.Exists(fullPath)) try { File.Delete(fullPath); } catch { } return ToolResult.Fail($"PPTX 생성 실패: {ex.Message}"); } } - private static void AddTextBox(ShapeTree tree, ref uint id, long x, long y, long cx, long cy, - string text, int fontSize, string color, bool bold) + // ══════════════════════════════════════════════════════════════════════════ + // 슬라이드 레이아웃 빌더 + // ══════════════════════════════════════════════════════════════════════════ + + private static void BuildTitleSlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id) { - var shape = new Shape(); - shape.NonVisualShapeProperties = new P.NonVisualShapeProperties( - new P.NonVisualDrawingProperties { Id = id++, Name = $"TextBox{id}" }, - new P.NonVisualShapeDrawingProperties(new A.ShapeLocks { NoGrouping = true }), - new ApplicationNonVisualDrawingProperties()); - shape.ShapeProperties = new ShapeProperties( - new A.Transform2D( - new A.Offset { X = x, Y = y }, - new A.Extents { Cx = cx, Cy = cy }), - new A.PresetGeometry(new A.AdjustValueList()) { Preset = A.ShapeTypeValues.Rectangle }); + var c = theme.Colors; + var lay = theme.Layout; + var title = Str(s, "title"); + var subtitle = Str(s, "subtitle"); - var txBody = new TextBody( - new A.BodyProperties { Wrap = A.TextWrappingValues.Square }, - new A.ListStyle()); - - // 텍스트를 줄 단위로 분리 - var lines = text.Split('\n'); - foreach (var line in lines) + switch (lay.TitleVariant) { - var para = new A.Paragraph(); - var run = new A.Run(); - var runProps = new A.RunProperties { Language = "ko-KR", FontSize = fontSize, Dirty = false }; - runProps.AppendChild(new A.SolidFill(new A.RgbColorModelHex { Val = color })); - if (bold) runProps.Bold = true; - run.AppendChild(runProps); - run.AppendChild(new A.Text(line)); - para.AppendChild(run); - txBody.AppendChild(para); + case "top_band": + BuildTitleTopBand(t, ref id, c, lay, title, subtitle, W, H); + break; + case "center_bold": + BuildTitleCenterBold(t, ref id, c, lay, title, subtitle, W, H); + break; + case "diagonal": + BuildTitleDiagonal(t, ref id, c, lay, title, subtitle, W, H); + break; + case "minimal": + BuildTitleMinimal(t, ref id, c, lay, title, subtitle, W, H); + break; + default: // bar_left + BuildTitleBarLeft(t, ref id, c, lay, title, subtitle, W, H); + break; } - - shape.TextBody = txBody; - tree.AppendChild(shape); } - private static void AddRectangle(ShapeTree tree, ref uint id, long x, long y, long cx, long cy, string fillColor) + // ── 타이틀 변형: bar_left ───────────────────────────────────────────────── + // 전체 Primary 배경 + 좌측 세로 Accent 바 + 하단 미세 라인 + private static void BuildTitleBarLeft(ShapeTree t, ref uint id, + ThemeColors c, ThemeLayout lay, string title, string subtitle, long W, long H) { - var shape = new Shape(); - shape.NonVisualShapeProperties = new P.NonVisualShapeProperties( + AddRect(t, ref id, 0, 0, W, H, c.Primary); + long barW = W * lay.AccentThickPct / 100; + AddRect(t, ref id, 0, 0, barW, H, c.Accent); + AddRect(t, ref id, barW, H - 8000, W - barW, 8000, c.Accent); + + long textX = barW + 250000; + long textW = W - barW - 400000; + long textY = H * 28 / 100; + + AddText(t, ref id, textX, textY, textW, H / 5, + title, lay.TitlePt, c.TextLight, bold: true, align: lay.TitleAlign); + if (!string.IsNullOrWhiteSpace(subtitle)) + AddText(t, ref id, textX, textY + H / 5 + 120000, textW, H / 6, + subtitle, lay.SubtitlePt, c.Accent, bold: false, align: lay.TitleAlign); + } + + // ── 타이틀 변형: top_band ───────────────────────────────────────────────── + // 흰/Bg 배경 + 상단 컬러 밴드 (20% 높이) + 밴드 하단 Accent 라인 + 제목 아래 위치 + private static void BuildTitleTopBand(ShapeTree t, ref uint id, + ThemeColors c, ThemeLayout lay, string title, string subtitle, long W, long H) + { + // 배경 + AddRect(t, ref id, 0, 0, W, H, c.Bg); + // 상단 밴드 + long bandH = H * 20 / 100; + AddRect(t, ref id, 0, 0, W, bandH, c.Primary); + // 밴드 하단 Accent 라인 + long lineH = (long)(H * lay.AccentThickPct / 100.0 * 0.3); + if (lineH < 6000) lineH = 6000; + AddRect(t, ref id, 0, bandH, W, lineH, c.Accent); + + long textY = bandH + lineH + H * 5 / 100; + long textX = W / 12; + long textW = W * 10 / 12; + + AddText(t, ref id, textX, textY, textW, H / 4, + title, lay.TitlePt, c.Primary, bold: true, align: lay.TitleAlign); + if (!string.IsNullOrWhiteSpace(subtitle)) + AddText(t, ref id, textX, textY + H / 4 + 80000, textW, H / 6, + subtitle, lay.SubtitlePt, c.Accent, bold: false, align: lay.TitleAlign); + } + + // ── 타이틀 변형: center_bold ────────────────────────────────────────────── + // 전체 Primary 배경 + 선택적 장식 원 + 중앙 큰 제목 + Accent 구분선 + 부제목 + private static void BuildTitleCenterBold(ShapeTree t, ref uint id, + ThemeColors c, ThemeLayout lay, string title, string subtitle, long W, long H) + { + AddRect(t, ref id, 0, 0, W, H, c.Primary); + + if (lay.HasDecorCircle) + { + // 우측 상단 큰 반투명 장식 원 + long circSize = H * 90 / 100; + AddEllipse(t, ref id, W - circSize / 2, -circSize / 4, circSize, circSize, c.Accent, alpha: 15); + } + + long accentLineW = W * 40 / 100; + long accentLineX = (W - accentLineW) / 2; + long centerY = H * 35 / 100; + + AddText(t, ref id, W / 8, centerY - H / 10, W * 6 / 8, H / 4, + title, lay.TitlePt, c.TextLight, bold: true, align: "ctr"); + + // Accent 하단 수평선 (중앙, 40% 너비) + long lineH = H * lay.AccentThickPct / 100 / 3; + if (lineH < 6000) lineH = 6000; + AddRect(t, ref id, accentLineX, centerY + H / 7, accentLineW, lineH, c.Accent); + + if (!string.IsNullOrWhiteSpace(subtitle)) + AddText(t, ref id, W / 8, centerY + H / 7 + lineH + 80000, W * 6 / 8, H / 6, + subtitle, lay.SubtitlePt, c.Accent, bold: false, align: "ctr"); + } + + // ── 타이틀 변형: diagonal ───────────────────────────────────────────────── + // 좌측 40% Primary 채움 + 우측 Bg 배경 + 좌측 장식 원 + 우측에 제목/부제목 + private static void BuildTitleDiagonal(ShapeTree t, ref uint id, + ThemeColors c, ThemeLayout lay, string title, string subtitle, long W, long H) + { + // 전체 배경 + AddRect(t, ref id, 0, 0, W, H, c.Bg); + // 좌측 컬러 블록 (40%) + long leftW = W * 40 / 100; + AddRect(t, ref id, 0, 0, leftW, H, c.Primary); + + // 대각 스트라이프 효과: 얇고 기울어진 느낌을 근사하는 직사각형 오버랩 + // 실제 OpenXML 회전은 spPr.Transform2D.Rotation으로 표현(단위: 1/60000도) + // 여기서는 삼각형 모양의 사각형을 오른쪽으로 약간 확장하여 경계를 만듦 + long accentW = W * lay.AccentThickPct / 100; + AddRect(t, ref id, leftW - accentW / 2, 0, accentW, H, c.Accent); + + if (lay.HasDecorCircle) + { + // 좌측 중앙 장식 원 + long circSize = leftW * 60 / 100; + AddEllipse(t, ref id, leftW / 2 - circSize / 2, H / 2 - circSize / 2, circSize, circSize, c.Accent, alpha: 20); + } + + long textX = leftW + accentW / 2 + 200000; + long textW = W - textX - 200000; + long textY = H * 28 / 100; + + AddText(t, ref id, textX, textY, textW, H / 4, + title, lay.TitlePt, c.Primary, bold: true, align: "l"); + if (!string.IsNullOrWhiteSpace(subtitle)) + AddText(t, ref id, textX, textY + H / 4 + 100000, textW, H / 6, + subtitle, lay.SubtitlePt, c.Accent, bold: false, align: "l"); + } + + // ── 타이틀 변형: minimal ────────────────────────────────────────────────── + // 흰 배경 + 하단 Accent 라인만 + 대형 굵은 제목 + 부제목 + private static void BuildTitleMinimal(ShapeTree t, ref uint id, + ThemeColors c, ThemeLayout lay, string title, string subtitle, long W, long H) + { + AddRect(t, ref id, 0, 0, W, H, c.Bg); + + long lineH = H * lay.AccentThickPct / 100; + if (lineH < 8000) lineH = 8000; + AddRect(t, ref id, 0, H - lineH, W, lineH, c.Accent); + + long textX = W / 12; + long textW = W * 10 / 12; + long textY = H * 22 / 100; + + AddText(t, ref id, textX, textY, textW, H * 35 / 100, + title, lay.TitlePt, c.Primary, bold: true, align: lay.TitleAlign); + if (!string.IsNullOrWhiteSpace(subtitle)) + AddText(t, ref id, textX, textY + H * 35 / 100 + 80000, textW, H / 6, + subtitle, lay.SubtitlePt, c.TextDark, bold: false, align: lay.TitleAlign); + } + + // ══════════════════════════════════════════════════════════════════════════ + // 섹션 슬라이드 빌더 + // ══════════════════════════════════════════════════════════════════════════ + + private static void BuildSectionSlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id) + { + var c = theme.Colors; + var lay = theme.Layout; + var title = Str(s, "title"); + var subtitle = Str(s, "subtitle"); + + switch (lay.SectionVariant) + { + case "centered_line": + BuildSectionCenteredLine(t, ref id, c, lay, title, subtitle, W, H); + break; + case "side_accent": + BuildSectionSideAccent(t, ref id, c, lay, title, subtitle, W, H); + break; + default: // full_color + BuildSectionFullColor(t, ref id, c, lay, title, subtitle, W, H); + break; + } + } + + // ── 섹션 변형: full_color ───────────────────────────────────────────────── + // 전체 Primary 배경 + 가로 Accent 스트라이프 + 중앙 제목/부제목 + private static void BuildSectionFullColor(ShapeTree t, ref uint id, + ThemeColors c, ThemeLayout lay, string title, string subtitle, long W, long H) + { + AddRect(t, ref id, 0, 0, W, H, c.Primary); + AddRect(t, ref id, W / 8, H / 2 - 5000, W * 3 / 4, 10000, c.Accent); + + AddText(t, ref id, W / 8, H / 4, W * 3 / 4, H / 3, + title, lay.SlideTitlePt + 800, c.TextLight, bold: true, align: "ctr"); + if (!string.IsNullOrWhiteSpace(subtitle)) + AddText(t, ref id, W / 8, H * 58 / 100, W * 3 / 4, H / 6, + subtitle, lay.SubtitlePt, c.Accent, bold: false, align: "ctr"); + } + + // ── 섹션 변형: centered_line ────────────────────────────────────────────── + // 흰 배경 + 중앙 큰 Primary 색 제목 + Accent 언더라인 + 부제목 + private static void BuildSectionCenteredLine(ShapeTree t, ref uint id, + ThemeColors c, ThemeLayout lay, string title, string subtitle, long W, long H) + { + AddRect(t, ref id, 0, 0, W, H, c.Bg); + + long textY = H * 28 / 100; + long lineW = W * 30 / 100; + long lineX = (W - lineW) / 2; + long lineH = 10000; + + AddText(t, ref id, W / 8, textY, W * 6 / 8, H / 4, + title, lay.SlideTitlePt + 800, c.Primary, bold: true, align: "ctr"); + + AddRect(t, ref id, lineX, textY + H / 4 + 40000, lineW, lineH, c.Accent); + + if (!string.IsNullOrWhiteSpace(subtitle)) + AddText(t, ref id, W / 8, textY + H / 4 + lineH + 120000, W * 6 / 8, H / 6, + subtitle, lay.SubtitlePt, c.TextDark, bold: false, align: "ctr"); + } + + // ── 섹션 변형: side_accent ──────────────────────────────────────────────── + // 흰 배경 + 좌측 10% Primary 스트라이프 + 우측에 큰 제목/부제목 + private static void BuildSectionSideAccent(ShapeTree t, ref uint id, + ThemeColors c, ThemeLayout lay, string title, string subtitle, long W, long H) + { + AddRect(t, ref id, 0, 0, W, H, c.Bg); + long stripW = W / 10; + AddRect(t, ref id, 0, 0, stripW, H, c.Primary); + + long textX = stripW + 200000; + long textW = W - textX - 200000; + long textY = H * 30 / 100; + + AddText(t, ref id, textX, textY, textW, H / 3, + title, lay.SlideTitlePt + 1000, c.Primary, bold: true, align: "l"); + if (!string.IsNullOrWhiteSpace(subtitle)) + AddText(t, ref id, textX, textY + H / 3 + 80000, textW, H / 6, + subtitle, lay.SubtitlePt, c.Accent, bold: false, align: "l"); + } + + // ══════════════════════════════════════════════════════════════════════════ + // 콘텐츠 슬라이드 빌더 + // ══════════════════════════════════════════════════════════════════════════ + + private static void BuildContentSlide(ShapeTree t, JsonElement s, FullTheme theme, long W, long H, ref uint id) + { + var c = theme.Colors; + var lay = theme.Layout; + var title = Str(s, "title"); + var body = Str(s, "body"); + + switch (lay.ContentVariant) + { + case "card": + BuildContentCard(t, ref id, c, lay, title, body, W, H); + break; + case "split_accent": + BuildContentSplitAccent(t, ref id, c, lay, title, body, W, H); + break; + case "minimal": + BuildContentMinimal(t, ref id, c, lay, title, body, W, H); + break; + default: // header_bar + BuildContentHeaderBar(t, ref id, c, lay, title, body, W, H); + break; + } + } + + // ── 콘텐츠 변형: header_bar ─────────────────────────────────────────────── + // 상단 헤더 바 (Primary 색) + 제목 텍스트 + 하단 본문 + private static void BuildContentHeaderBar(ShapeTree t, ref uint id, + ThemeColors c, ThemeLayout lay, string title, string body, long W, long H) + { + const long M = 450000; + long hdrH = 1100000L; + + // 헤더 배경 바 + AddRect(t, ref id, 0, 0, W, hdrH, c.Primary); + // 헤더 하단 Accent 라인 + AddRect(t, ref id, 0, hdrH, W, 12000, c.Accent); + + AddText(t, ref id, M, 0, W - M * 2, hdrH, + title, lay.SlideTitlePt, c.TextLight, bold: true, align: "l"); + + long bodyY = hdrH + 12000 + 80000; + AddBulletBody(t, ref id, M, bodyY, W - M * 2, H - bodyY - M, + body, lay.BodyPt, c.TextDark, c.Accent); + } + + // ── 콘텐츠 변형: card ───────────────────────────────────────────────────── + // 흰 배경 + 제목 + Accent 언더라인 + 내용을 BgAlt 카드 박스 안에 + private static void BuildContentCard(ShapeTree t, ref uint id, + ThemeColors c, ThemeLayout lay, string title, string body, long W, long H) + { + const long M = 450000; + + AddRect(t, ref id, 0, 0, W, H, c.Bg); + + // 제목 + AddText(t, ref id, M, 180000, W - M * 2, 840000, + title, lay.SlideTitlePt, c.Primary, bold: true, align: "l"); + // 제목 하단 Accent 언더라인 + long underlineW = W * 35 / 100; + AddRect(t, ref id, M, 1060000, underlineW, 10000, c.Accent); + + // 콘텐츠 카드 박스 (BgAlt 배경의 둥근 모서리 사각형) + long cardY = 1120000; + long cardH = H - cardY - M; + AddRoundedRect(t, ref id, M, cardY, W - M * 2, cardH, c.BgAlt); + + // 카드 안 본문 + AddBulletBody(t, ref id, M + 150000, cardY + 120000, W - M * 2 - 300000, cardH - 240000, + body, lay.BodyPt, c.TextDark, c.Accent); + } + + // ── 콘텐츠 변형: minimal ────────────────────────────────────────────────── + // 흰 배경 + 제목 (어두운 색) + 하단 Accent 라인 + 본문 바로 아래 + private static void BuildContentMinimal(ShapeTree t, ref uint id, + ThemeColors c, ThemeLayout lay, string title, string body, long W, long H) + { + const long M = 450000; + + AddRect(t, ref id, 0, 0, W, H, c.Bg); + + AddText(t, ref id, M, 200000, W - M * 2, 900000, + title, lay.SlideTitlePt, c.Primary, bold: true, align: "l"); + AddRect(t, ref id, M, 1140000, W - M * 2, 8000, c.Accent); + + AddBulletBody(t, ref id, M, 1200000, W - M * 2, H - 1200000 - M, + body, lay.BodyPt, c.TextDark, c.Accent); + } + + // ── 콘텐츠 변형: split_accent ───────────────────────────────────────────── + // 흰 배경 + 좌측 Accent 스트라이프 + 우측에 제목/본문 + private static void BuildContentSplitAccent(ShapeTree t, ref uint id, + ThemeColors c, ThemeLayout lay, string title, string body, long W, long H) + { + const long M = 450000; + + AddRect(t, ref id, 0, 0, W, H, c.Bg); + + long stripW = W * lay.AccentThickPct / 100; + AddRect(t, ref id, M, M, stripW, H - M * 2, c.Accent); + + long textX = M + stripW + 160000; + long textW = W - textX - M; + + AddText(t, ref id, textX, 200000, textW, 900000, + title, lay.SlideTitlePt, c.Primary, bold: true, align: "l"); + AddRect(t, ref id, textX, 1140000, textW, 8000, c.Primary); + + AddBulletBody(t, ref id, textX, 1200000, textW, H - 1200000 - M, + body, lay.BodyPt, c.TextDark, c.Accent); + } + + // ══════════════════════════════════════════════════════════════════════════ + // 기타 레이아웃 빌더 (두 컬럼, 인용구, 테이블) + // ══════════════════════════════════════════════════════════════════════════ + + private static void BuildTwoColumnSlide(ShapeTree t, JsonElement s, ThemeColors c, long W, long H, ref uint id) + { + var title = Str(s, "title"); + var left = Str(s, "left"); + var right = Str(s, "right"); + const long M = 450000; + const long gap = 250000; + long colW = (W - M * 2 - gap) / 2; + long colY = 1200000; + long hdrH = 380000; + + AddRect(t, ref id, M, 1150000, W - M * 2, 10000, c.Accent); + AddText(t, ref id, M, 220000, W - M * 2, 900000, + title, 2800, c.Primary, bold: true, align: "l"); + + // 왼쪽 컬럼 헤더 배경 + AddRect(t, ref id, M, colY, colW, hdrH, c.Primary); + // 오른쪽 컬럼 헤더 배경 + AddRect(t, ref id, M + colW + gap, colY, colW, hdrH, c.Accent); + + // 왼쪽 불릿 본문 + AddBulletBody(t, ref id, M, colY + hdrH + 60000, colW, H - colY - hdrH - M - 60000, + left, 1500, c.TextDark, c.Primary); + // 오른쪽 불릿 본문 + AddBulletBody(t, ref id, M + colW + gap, colY + hdrH + 60000, colW, H - colY - hdrH - M - 60000, + right, 1500, c.TextDark, c.Accent); + } + + private static void BuildQuoteSlide(ShapeTree t, JsonElement s, ThemeColors c, long W, long H, ref uint id) + { + var quote = Str(s, "quote"); + var author = Str(s, "author"); + + AddRect(t, ref id, 0, 0, W, H, c.BgAlt); + AddRect(t, ref id, W / 10, H / 5, W / 100, H * 3 / 5, c.Accent); + + AddText(t, ref id, W / 10 + 60000, H / 10, W / 5, H / 4, + "\u201C", 9600, c.Primary, bold: true, align: "l"); + + AddTextEx(t, ref id, W / 10 + 180000, H / 4, W * 7 / 10, H * 2 / 4, + quote, 2200, c.TextDark, bold: false, italic: true, align: "l"); + + if (!string.IsNullOrWhiteSpace(author)) + { + AddRect(t, ref id, W / 10 + 180000, H * 74 / 100, W / 8, 8000, c.Accent); + AddText(t, ref id, W / 10 + 180000 + W / 8 + 80000, H * 73 / 100, W / 2, 300000, + $"— {author}", 1600, c.Primary, bold: true, align: "l"); + } + } + + private static void BuildTableSlide(ShapeTree t, JsonElement s, ThemeColors c, long W, long H, ref uint id) + { + var title = Str(s, "title"); + var headers = s.TryGetProperty("headers", out var hEl) + ? hEl.EnumerateArray().Select(x => x.GetString() ?? "").ToList() + : new List(); + var rows = s.TryGetProperty("rows", out var rEl) + ? rEl.EnumerateArray().Select(r => r.EnumerateArray().Select(c2 => c2.GetString() ?? "").ToList()).ToList() + : new List>(); + const long M = 450000; + + AddRect(t, ref id, M, 1150000, W - M * 2, 10000, c.Accent); + AddText(t, ref id, M, 220000, W - M * 2, 900000, + title, 2800, c.Primary, bold: true, align: "l"); + + if (headers.Count > 0) + AddOpenXmlTable(t, ref id, headers, rows, c, M, 1260000, W - M * 2, H - 1260000 - M); + } + + // ══════════════════════════════════════════════════════════════════════════ + // Shape 헬퍼 + // ══════════════════════════════════════════════════════════════════════════ + + /// 단색 채운 직사각형 (배경, 구분선, 액센트 바 등) + private static void AddRect(ShapeTree t, ref uint id, long x, long y, long w, long h, string hex) + { + var sp = new Shape(); + sp.NonVisualShapeProperties = new P.NonVisualShapeProperties( new P.NonVisualDrawingProperties { Id = id++, Name = $"Rect{id}" }, new P.NonVisualShapeDrawingProperties(), new ApplicationNonVisualDrawingProperties()); - shape.ShapeProperties = new ShapeProperties( - new A.Transform2D( - new A.Offset { X = x, Y = y }, - new A.Extents { Cx = cx, Cy = cy }), + sp.ShapeProperties = new ShapeProperties( + new A.Transform2D(new A.Offset { X = x, Y = y }, new A.Extents { Cx = w, Cy = h }), new A.PresetGeometry(new A.AdjustValueList()) { Preset = A.ShapeTypeValues.Rectangle }, - new A.SolidFill(new A.RgbColorModelHex { Val = fillColor })); - tree.AppendChild(shape); + new A.SolidFill(new A.RgbColorModelHex { Val = hex.TrimStart('#') }), + new A.Outline(new A.NoFill())); + sp.TextBody = new TextBody(new A.BodyProperties(), new A.ListStyle(), new A.Paragraph()); + t.AppendChild(sp); } - private static string FormatTableAsText(JsonElement slideEl, (string Primary, string Accent, string TextDark, string TextLight, string Bg) colors) + /// 단색 채운 둥근 모서리 사각형 (카드 박스) + private static void AddRoundedRect(ShapeTree t, ref uint id, long x, long y, long w, long h, string hex) { - var sb = new System.Text.StringBuilder(); - if (slideEl.TryGetProperty("headers", out var headers)) + var sp = new Shape(); + sp.NonVisualShapeProperties = new P.NonVisualShapeProperties( + new P.NonVisualDrawingProperties { Id = id++, Name = $"RRect{id}" }, + new P.NonVisualShapeDrawingProperties(), + new ApplicationNonVisualDrawingProperties()); + // roundRect preset with adj (corner radius ~5%) + var geom = new A.PresetGeometry( + new A.AdjustValueList(new A.ShapeGuide { Name = "adj", Formula = "val 20000" })) + { Preset = A.ShapeTypeValues.RoundRectangle }; + sp.ShapeProperties = new ShapeProperties( + new A.Transform2D(new A.Offset { X = x, Y = y }, new A.Extents { Cx = w, Cy = h }), + geom, + new A.SolidFill(new A.RgbColorModelHex { Val = hex.TrimStart('#') }), + new A.Outline(new A.NoFill())); + sp.TextBody = new TextBody(new A.BodyProperties(), new A.ListStyle(), new A.Paragraph()); + t.AppendChild(sp); + } + + /// 단색 타원 (장식 원 등). alpha: 0~100 (불투명도 %) + private static void AddEllipse(ShapeTree t, ref uint id, long x, long y, long w, long h, string hex, int alpha = 100) + { + var sp = new Shape(); + sp.NonVisualShapeProperties = new P.NonVisualShapeProperties( + new P.NonVisualDrawingProperties { Id = id++, Name = $"Ellipse{id}" }, + new P.NonVisualShapeDrawingProperties(), + new ApplicationNonVisualDrawingProperties()); + + DocumentFormat.OpenXml.OpenXmlElement fill; + if (alpha < 100) { - var headerTexts = new List(); - foreach (var h in headers.EnumerateArray()) - headerTexts.Add(h.GetString() ?? ""); - sb.AppendLine(string.Join(" | ", headerTexts)); - sb.AppendLine(new string('─', headerTexts.Sum(h => h.Length + 5))); + // 알파값 포함 (OpenXML alpha: 0=완전투명, 100000=완전불투명) + var rgbClr = new A.RgbColorModelHex { Val = hex.TrimStart('#') }; + rgbClr.AppendChild(new A.Alpha { Val = alpha * 1000 }); + fill = new A.SolidFill(rgbClr); + } + else + { + fill = new A.SolidFill(new A.RgbColorModelHex { Val = hex.TrimStart('#') }); } - if (slideEl.TryGetProperty("rows", out var rows)) + sp.ShapeProperties = new ShapeProperties( + new A.Transform2D(new A.Offset { X = x, Y = y }, new A.Extents { Cx = w, Cy = h }), + new A.PresetGeometry(new A.AdjustValueList()) { Preset = A.ShapeTypeValues.Ellipse }, + fill, + new A.Outline(new A.NoFill())); + sp.TextBody = new TextBody(new A.BodyProperties(), new A.ListStyle(), new A.Paragraph()); + t.AppendChild(sp); + } + + /// 텍스트 박스 (일반 텍스트, 줄바꿈 지원) + private static void AddText(ShapeTree t, ref uint id, + long x, long y, long w, long h, + string text, int fs100, string hex, bool bold, string align = "l") + => AddTextEx(t, ref id, x, y, w, h, text, fs100, hex, bold, italic: false, align: align); + + private static void AddTextEx(ShapeTree t, ref uint id, + long x, long y, long w, long h, + string text, int fs100, string hex, bool bold, bool italic, string align = "l") + { + var alignVal = align switch { - foreach (var row in rows.EnumerateArray()) + "ctr" => A.TextAlignmentTypeValues.Center, + "r" => A.TextAlignmentTypeValues.Right, + _ => A.TextAlignmentTypeValues.Left, + }; + + var txBody = new TextBody( + new A.BodyProperties { - var cells = new List(); - foreach (var cell in row.EnumerateArray()) - cells.Add(cell.GetString() ?? cell.ToString()); - sb.AppendLine(string.Join(" | ", cells)); - } + Wrap = A.TextWrappingValues.Square, + LeftInset = 91440, + RightInset = 91440, + TopInset = 45720, + BottomInset = 45720, + Anchor = A.TextAnchoringTypeValues.Center, + }, + new A.ListStyle()); + + foreach (var line in text.Split('\n')) + { + var para = new A.Paragraph(new A.ParagraphProperties { Alignment = alignVal }); + var rPr = MakeRunProps(fs100, hex, bold, italic); + para.AppendChild(new A.Run(rPr, new A.Text { Text = line })); + txBody.AppendChild(para); } - return sb.ToString(); + var sp = new Shape(); + sp.NonVisualShapeProperties = new P.NonVisualShapeProperties( + new P.NonVisualDrawingProperties { Id = id++, Name = $"Txt{id}" }, + new P.NonVisualShapeDrawingProperties(), + new ApplicationNonVisualDrawingProperties()); + sp.ShapeProperties = new ShapeProperties( + new A.Transform2D(new A.Offset { X = x, Y = y }, new A.Extents { Cx = w, Cy = h }), + new A.PresetGeometry(new A.AdjustValueList()) { Preset = A.ShapeTypeValues.Rectangle }, + new A.NoFill(), + new A.Outline(new A.NoFill())); + sp.TextBody = txBody; + t.AppendChild(sp); + } + + /// 불릿 포인트 텍스트 박스 — 줄 단위로 불릿 적용, 들여쓰기 서브불릿 지원 + private static void AddBulletBody(ShapeTree t, ref uint id, + long x, long y, long w, long h, + string text, int fs100, string textHex, string bulletHex) + { + var txBody = new TextBody( + new A.BodyProperties + { + Wrap = A.TextWrappingValues.Square, + LeftInset = 91440, + RightInset = 91440, + TopInset = 45720, + BottomInset = 45720, + Anchor = A.TextAnchoringTypeValues.Top, + }, + new A.ListStyle()); + + foreach (var rawLine in text.Split('\n')) + { + if (string.IsNullOrWhiteSpace(rawLine)) + { + txBody.AppendChild(new A.Paragraph()); + continue; + } + + var isSub = rawLine.StartsWith(" ") || rawLine.StartsWith("\t"); + var trimmed = rawLine.TrimStart().TrimStart('-').TrimStart(); + var bulletFs = isSub ? fs100 - 200 : fs100; + var bChar = isSub ? "–" : "•"; + var marL = isSub ? 685800 : 457200; + var indent = isSub ? -228600 : -342900; + + var pPr = new A.ParagraphProperties + { + LeftMargin = marL, + Indent = indent, + }; + pPr.AppendChild(new A.SpaceBefore(new A.SpacingPercent { Val = isSub ? 80 : 100 })); + pPr.AppendChild(new A.SpaceAfter(new A.SpacingPoints { Val = 0 })); + pPr.AppendChild(new A.BulletColor(new A.RgbColorModelHex { Val = bulletHex.TrimStart('#') })); + pPr.AppendChild(new A.BulletFont { Typeface = FontEn }); + pPr.AppendChild(new A.CharacterBullet { Char = bChar }); + + var para = new A.Paragraph(pPr, new A.Run(MakeRunProps(bulletFs, textHex, false, false), new A.Text { Text = trimmed })); + txBody.AppendChild(para); + } + + var sp = new Shape(); + sp.NonVisualShapeProperties = new P.NonVisualShapeProperties( + new P.NonVisualDrawingProperties { Id = id++, Name = $"Body{id}" }, + new P.NonVisualShapeDrawingProperties(), + new ApplicationNonVisualDrawingProperties()); + sp.ShapeProperties = new ShapeProperties( + new A.Transform2D(new A.Offset { X = x, Y = y }, new A.Extents { Cx = w, Cy = h }), + new A.PresetGeometry(new A.AdjustValueList()) { Preset = A.ShapeTypeValues.Rectangle }, + new A.NoFill(), + new A.Outline(new A.NoFill())); + sp.TextBody = txBody; + t.AppendChild(sp); + } + + /// 진짜 OpenXML 테이블 — 헤더 행 별도 색상, 짝수/홀수 행 교대 배경 + private static void AddOpenXmlTable(ShapeTree t, ref uint id, + List headers, List> rows, + ThemeColors c, long x, long y, long w, long h) + { + int cols = headers.Count; + if (cols == 0) return; + long colW = w / cols; + long rowH = Math.Max(Math.Min(h / (rows.Count + 1), 650000), 400000); + + TextBody MakeCell(string txt, string hexFg, bool bold) + { + var tb = new TextBody( + new A.BodyProperties { LeftInset = 91440, RightInset = 91440, TopInset = 45720, BottomInset = 45720 }, + new A.ListStyle()); + var para = new A.Paragraph(new A.ParagraphProperties()); + para.AppendChild(new A.Run(MakeRunProps(1300, hexFg, bold, false), new A.Text { Text = txt })); + tb.AppendChild(para); + return tb; + } + + var table = new A.Table(); + var tblPr = new A.TableProperties { FirstRow = true, BandRow = true }; + table.AppendChild(tblPr); + + var tblGrid = new A.TableGrid(); + for (int i = 0; i < cols; i++) + tblGrid.AppendChild(new A.GridColumn { Width = colW }); + table.AppendChild(tblGrid); + + // 헤더 행 + var hRow = new A.TableRow { Height = rowH }; + foreach (var hdr in headers) + { + var tc = new A.TableCell(); + tc.AppendChild(MakeCell(hdr, c.HeaderText, bold: true)); + var tcPr = new A.TableCellProperties(); + tcPr.AppendChild(new A.SolidFill(new A.RgbColorModelHex { Val = c.HeaderBg.TrimStart('#') })); + tc.AppendChild(tcPr); + hRow.AppendChild(tc); + } + table.AppendChild(hRow); + + // 데이터 행 + for (int ri = 0; ri < rows.Count; ri++) + { + var rowData = rows[ri]; + var rowBg = ri % 2 == 0 ? c.Bg : c.BgAlt; + var dRow = new A.TableRow { Height = rowH }; + for (int ci = 0; ci < cols; ci++) + { + var cellTxt = ci < rowData.Count ? rowData[ci] : ""; + var tc = new A.TableCell(); + tc.AppendChild(MakeCell(cellTxt, c.TextDark, bold: false)); + var tcPr = new A.TableCellProperties(); + tcPr.AppendChild(new A.SolidFill(new A.RgbColorModelHex { Val = rowBg.TrimStart('#') })); + tc.AppendChild(tcPr); + dRow.AppendChild(tc); + } + table.AppendChild(dRow); + } + + var gf = new P.GraphicFrame( + new P.NonVisualGraphicFrameProperties( + new P.NonVisualDrawingProperties { Id = id++, Name = "Table" }, + new P.NonVisualGraphicFrameDrawingProperties(new A.GraphicFrameLocks()), + new ApplicationNonVisualDrawingProperties()), + new P.Transform( + new A.Offset { X = x, Y = y }, + new A.Extents { Cx = w, Cy = rowH * (rows.Count + 1) }), + new A.Graphic( + new A.GraphicData(table) + { + Uri = "http://schemas.openxmlformats.org/drawingml/2006/table" + })); + t.AppendChild(gf); + } + + // ══════════════════════════════════════════════════════════════════════════ + // RunProperties 생성 헬퍼 + // ══════════════════════════════════════════════════════════════════════════ + + private static A.RunProperties MakeRunProps(int fs100, string hex, bool bold, bool italic) + { + var rPr = new A.RunProperties + { + Language = "ko-KR", + AlternativeLanguage = "en-US", + FontSize = fs100, + Bold = bold, + Italic = italic, + Dirty = false, + }; + rPr.AppendChild(new A.SolidFill(new A.RgbColorModelHex { Val = hex.TrimStart('#') })); + rPr.AppendChild(new A.LatinFont { Typeface = FontEn }); + rPr.AppendChild(new A.EastAsianFont { Typeface = FontKo }); + return rPr; + } + + // ══════════════════════════════════════════════════════════════════════════ + // 기타 헬퍼 + // ══════════════════════════════════════════════════════════════════════════ + + private static string Str(JsonElement e, string key) + => e.TryGetProperty(key, out var v) && v.ValueKind == JsonValueKind.String + ? v.GetString() ?? "" + : ""; + + private static void AddNotesSlide(SlidePart slidePart, string notes) + { + var notesPart = slidePart.AddNewPart(); + var nsSp = new Shape(); + nsSp.NonVisualShapeProperties = new P.NonVisualShapeProperties( + new P.NonVisualDrawingProperties { Id = 2, Name = "Notes" }, + new P.NonVisualShapeDrawingProperties(), + new ApplicationNonVisualDrawingProperties()); + nsSp.ShapeProperties = new ShapeProperties( + new A.Transform2D(new A.Offset { X = 457200, Y = 1143000 }, new A.Extents { Cx = 5486400, Cy = 3886200 }), + new A.PresetGeometry(new A.AdjustValueList()) { Preset = A.ShapeTypeValues.Rectangle }, + new A.NoFill()); + var ntBody = new TextBody( + new A.BodyProperties { Wrap = A.TextWrappingValues.Square }, + new A.ListStyle()); + var ntPara = new A.Paragraph(); + var ntRpr = new A.RunProperties { Language = "ko-KR", FontSize = 1200, Dirty = false }; + ntRpr.AppendChild(new A.LatinFont { Typeface = FontEn }); + ntRpr.AppendChild(new A.EastAsianFont { Typeface = FontKo }); + ntPara.AppendChild(new A.Run(ntRpr, new A.Text { Text = notes })); + ntBody.AppendChild(ntPara); + nsSp.TextBody = ntBody; + + notesPart.NotesSlide = new NotesSlide( + new CommonSlideData(new ShapeTree( + new P.NonVisualGroupShapeProperties( + new P.NonVisualDrawingProperties { Id = 1, Name = "" }, + new P.NonVisualGroupShapeDrawingProperties(), + new ApplicationNonVisualDrawingProperties()), + new GroupShapeProperties(new A.TransformGroup()), + nsSp))); + notesPart.NotesSlide.Save(); + } + + // ── 기존 PPTX에서 테마 색상 추출 ──────────────────────────────────────────── + /// + /// 기존 .pptx 파일의 슬라이드 마스터 테마에서 색상을 읽어 ThemeColors로 반환합니다. + /// 추출할 수 없는 경우 null을 반환합니다. + /// + private static ThemeColors? ExtractThemeFromPptx(string pptxPath) + { + if (!File.Exists(pptxPath)) return null; + try + { + using var doc = PresentationDocument.Open(pptxPath, isEditable: false); + var master = doc.PresentationPart?.SlideMasterParts?.FirstOrDefault(); + var scheme = master?.ThemePart?.Theme?.ThemeElements?.ColorScheme; + if (scheme == null) return null; + + static string? ColorHex(A.Color2Type? el) + { + if (el == null) return null; + var srgb = el.GetFirstChild(); + if (srgb?.Val?.Value != null) return srgb.Val.Value; + var sys = el.GetFirstChild(); + if (sys?.LastColor?.Value != null) return sys.LastColor.Value; + return null; + } + + var dk1 = ColorHex(scheme.Dark1Color) ?? "1A1A2E"; + var lt1 = ColorHex(scheme.Light1Color) ?? "FFFFFF"; + var dk2 = ColorHex(scheme.Dark2Color) ?? "1A1A2E"; + var lt2 = ColorHex(scheme.Light2Color) ?? "F2F7FC"; + var acc1 = ColorHex(scheme.Accent1Color) ?? "2E75B6"; + + var isDarkBg = IsDarkColor(lt1); + return new ThemeColors( + Primary: dk2, + Accent: acc1, + TextDark: dk1, + TextLight: lt1, + Bg: lt1, + BgAlt: lt2, + HeaderBg: dk2, + HeaderText: isDarkBg ? dk1 : "FFFFFF"); + } + catch + { + return null; + } + } + + /// hex 색상이 어두운지 판단 (명도 기준). + private static bool IsDarkColor(string hex) + { + try + { + var h = hex.TrimStart('#'); + if (h.Length < 6) return false; + var r = Convert.ToInt32(h[..2], 16); + var g = Convert.ToInt32(h[2..4], 16); + var b = Convert.ToInt32(h[4..6], 16); + var luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b; + return luminance < 128; + } + catch { return false; } } } diff --git a/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs b/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs index 2b48b0e..5e1f53d 100644 --- a/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs +++ b/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs @@ -122,6 +122,10 @@ internal static class ToolResultPresentationCatalog return "build_test"; if (tool.Contains("git") || tool.Contains("diff")) return "git"; + if (tool is "html_create" or "docx_create" or "excel_create" or "xlsx_create" + or "csv_create" or "markdown_create" or "md_create" or "script_create" + or "pptx_create") + return "document"; if (tool.Contains("document") || tool.Contains("format") || tool.Contains("template")) return "document"; if (tool.Contains("skill")) diff --git a/src/AxCopilot/Services/ChatSessionStateService.cs b/src/AxCopilot/Services/ChatSessionStateService.cs index 2339224..0259fb8 100644 --- a/src/AxCopilot/Services/ChatSessionStateService.cs +++ b/src/AxCopilot/Services/ChatSessionStateService.cs @@ -118,9 +118,10 @@ public sealed class ChatSessionStateService // 새 대화를 시작한 직후처럼 아직 저장되지 않은 현재 대화가 있으면, // 최신 저장 대화로 되돌아가지 말고 그 임시 세션을 그대로 유지합니다. + // HasActualContent를 사용하여 WorkFolder/Permission 같은 설정값만 있는 경우도 새 대화로 인식. if (CurrentConversation != null && string.Equals(NormalizeTab(CurrentConversation.Tab), normalizedTab, StringComparison.OrdinalIgnoreCase) - && !HasPersistableContent(CurrentConversation)) + && !HasActualContent(CurrentConversation)) { return CurrentConversation; } @@ -607,6 +608,20 @@ public sealed class ChatSessionStateService return "Chat"; } + /// + /// 실제 사용자 상호작용 데이터가 있는지 확인합니다. WorkFolder/Permission 같은 설정값은 제외. + /// LoadOrCreateConversation의 새 대화 보호 가드에서 사용합니다. + /// + private static bool HasActualContent(ChatConversation conv) + { + if ((conv.Messages?.Count ?? 0) > 0) return true; + if ((conv.ExecutionEvents?.Count ?? 0) > 0) return true; + if ((conv.AgentRunHistory?.Count ?? 0) > 0) return true; + if ((conv.DraftQueueItems?.Count ?? 0) > 0) return true; + if (conv.Pinned) return true; + return false; + } + private static bool HasPersistableContent(ChatConversation conv) { if ((conv.Messages?.Count ?? 0) > 0) return true; diff --git a/src/AxCopilot/Services/ChatStorageService.cs b/src/AxCopilot/Services/ChatStorageService.cs index 3316600..4891fd2 100644 --- a/src/AxCopilot/Services/ChatStorageService.cs +++ b/src/AxCopilot/Services/ChatStorageService.cs @@ -92,10 +92,35 @@ public class ChatStorageService // ── 메타 캐시 ───────────────────────────────────────────────────────── private List? _metaCache; + private List? _metaOrderedCache; private bool _metaDirty = true; + private bool _metaOrderDirty = true; /// 메타 캐시를 무효화합니다. 다음 LoadAllMeta 호출 시 디스크에서 다시 읽습니다. - public void InvalidateMetaCache() => _metaDirty = true; + public void InvalidateMetaCache() + { + _metaDirty = true; + _metaOrderDirty = true; + } + + private void InvalidateMetaOrderCache() => _metaOrderDirty = true; + + private List BuildOrderedMetaCache() + { + if (_metaCache == null) + return new List(); + + if (_metaOrderDirty || _metaOrderedCache == null) + { + _metaOrderedCache = _metaCache + .OrderByDescending(c => c.Pinned) + .ThenByDescending(c => c.UpdatedAt) + .ToList(); + _metaOrderDirty = false; + } + + return new List(_metaOrderedCache); + } /// 메타 캐시에서 특정 항목만 업데이트합니다 (전체 재로드 없이). public void UpdateMetaCache(ChatConversation conv) @@ -116,23 +141,21 @@ public class ChatStorageService _metaCache[existing] = meta; else _metaCache.Add(meta); + InvalidateMetaOrderCache(); } /// 메타 캐시에서 항목을 제거합니다. public void RemoveFromMetaCache(string id) { _metaCache?.RemoveAll(c => c.Id == id); + InvalidateMetaOrderCache(); } /// 모든 대화의 메타 정보(메시지 미포함)를 로드합니다. 캐시를 사용합니다. public List LoadAllMeta() { if (!_metaDirty && _metaCache != null) - { - return _metaCache.OrderByDescending(c => c.Pinned) - .ThenByDescending(c => c.UpdatedAt) - .ToList(); - } + return BuildOrderedMetaCache(); var result = new List(); if (!Directory.Exists(ConversationsDir)) @@ -184,10 +207,9 @@ public class ChatStorageService _metaCache = result; _metaDirty = false; + _metaOrderDirty = true; - return result.OrderByDescending(c => c.Pinned) - .ThenByDescending(c => c.UpdatedAt) - .ToList(); + return BuildOrderedMetaCache(); } /// 특정 대화를 삭제합니다. diff --git a/src/AxCopilot/Services/IndexService.cs b/src/AxCopilot/Services/IndexService.cs index f0e05ca..32c9cff 100644 --- a/src/AxCopilot/Services/IndexService.cs +++ b/src/AxCopilot/Services/IndexService.cs @@ -62,6 +62,8 @@ public class IndexService : IDisposable public IReadOnlyList Entries => _index; public event EventHandler? IndexRebuilt; + /// 전체 재색인 및 증분 갱신 모두에서 발생 — 쿼리 캐시 무효화 용도. + public event EventHandler? IndexEntriesChanged; public event EventHandler? IndexProgressChanged; public TimeSpan LastIndexDuration { get; private set; } public int LastIndexCount { get; private set; } @@ -109,6 +111,7 @@ public class IndexService : IDisposable LastIndexDuration = TimeSpan.Zero; LastIndexCount = _index.Count; LogService.Info($"런처 인덱스 캐시 로드: {entries.Count}개 파일 시스템 항목"); + IndexEntriesChanged?.Invoke(this, EventArgs.Empty); // FuzzyEngine 캐시 무효화 } catch (Exception ex) { @@ -171,6 +174,7 @@ public class IndexService : IDisposable $"최근 색인 완료 · {LastIndexCount:N0}개 항목 · {LastIndexDuration.TotalSeconds:F1}초")); LogService.Info($"런처 인덱싱 완료: {fileSystemEntries.Count}개 파일 시스템 항목 ({sw.Elapsed.TotalSeconds:F1}초)"); IndexRebuilt?.Invoke(this, EventArgs.Empty); + IndexEntriesChanged?.Invoke(this, EventArgs.Empty); } finally { @@ -329,6 +333,7 @@ public class IndexService : IDisposable LastIndexCount, LastIndexDuration, $"최근 색인 완료 · {LastIndexCount:N0}개 항목"); + IndexEntriesChanged?.Invoke(this, EventArgs.Empty); // 증분 갱신도 캐시 무효화 필요 LogService.Info($"런처 인덱스 증분 갱신: {triggerPath}"); } @@ -516,7 +521,12 @@ public class IndexService : IDisposable } RegisterBuiltInApps(entries); - ComputeAllSearchCaches(entries); + // FS 항목은 이미 캐시 계산 완료 — 별칭·내장앱처럼 새로 추가된 항목만 계산 + foreach (var entry in entries) + { + if (string.IsNullOrEmpty(entry.NameLower)) + ComputeSearchCache(entry); + } _index = entries; } @@ -798,6 +808,10 @@ public class IndexService : IDisposable if (name.StartsWith(".", StringComparison.Ordinal) || IgnoredDirectoryNames.Contains(name)) continue; + // 파일과 동일하게 숨김·시스템 폴더도 색인에서 제외 + if (ShouldIgnorePath(subDir)) + continue; + entries.Add(CreateFolderEntry(subDir)); } } diff --git a/src/AxCopilot/Services/LlmService.ToolUse.cs b/src/AxCopilot/Services/LlmService.ToolUse.cs index 6629783..0e3bcdc 100644 --- a/src/AxCopilot/Services/LlmService.ToolUse.cs +++ b/src/AxCopilot/Services/LlmService.ToolUse.cs @@ -25,10 +25,16 @@ public partial class LlmService } /// 도구 정의를 포함하여 LLM에 요청하고, 텍스트 + tool_use 블록을 파싱하여 반환합니다. + /// + /// true이면 tool_choice: "required"를 요청에 추가하여 모델이 반드시 도구를 호출하도록 강제합니다. + /// 아직 한 번도 도구를 호출하지 않은 첫 번째 요청에 사용하세요. + /// Claude/Gemini는 지원하지 않으므로 vLLM/Ollama/OpenAI 계열에만 적용됩니다. + /// public async Task> SendWithToolsAsync( List messages, IReadOnlyCollection tools, - CancellationToken ct = default) + CancellationToken ct = default, + bool forceToolCall = false) { var activeService = ResolveService(); EnsureOperationModeAllowsLlmService(activeService); @@ -36,7 +42,7 @@ public partial class LlmService { "sigmoid" => await SendSigmoidWithToolsAsync(messages, tools, ct), "gemini" => await SendGeminiWithToolsAsync(messages, tools, ct), - "ollama" or "vllm" => await SendOpenAiWithToolsAsync(messages, tools, ct), + "ollama" or "vllm" => await SendOpenAiWithToolsAsync(messages, tools, ct, forceToolCall), _ => throw new NotSupportedException($"서비스 '{activeService}'는 아직 Function Calling을 지원하지 않습니다.") }; } @@ -428,7 +434,8 @@ public partial class LlmService // ─── OpenAI Compatible (Ollama / vLLM) Function Calling ────────── private async Task> SendOpenAiWithToolsAsync( - List messages, IReadOnlyCollection tools, CancellationToken ct) + List messages, IReadOnlyCollection tools, CancellationToken ct, + bool forceToolCall = false) { var activeService = ResolveService(); @@ -438,17 +445,19 @@ public partial class LlmService ? ResolveEndpointForService(activeService) : resolvedEp; var registered = GetActiveRegisteredModel(); - if (UsesIbmDeploymentChatApi(activeService, registered, endpoint)) - { - throw new ToolCallNotSupportedException( - "IBM 배포형 vLLM 연결은 OpenAI 도구 호출 형식과 다를 수 있어 일반 대화 경로로 폴백합니다."); - } + var isIbmDeployment = UsesIbmDeploymentChatApi(activeService, registered, endpoint); - var body = BuildOpenAiToolBody(messages, tools); + var body = isIbmDeployment + ? BuildIbmToolBody(messages, tools, forceToolCall) + : BuildOpenAiToolBody(messages, tools, forceToolCall); - var url = activeService.ToLowerInvariant() == "ollama" - ? endpoint.TrimEnd('/') + "/api/chat" - : endpoint.TrimEnd('/') + "/v1/chat/completions"; + string url; + if (isIbmDeployment) + url = BuildIbmDeploymentChatUrl(endpoint, stream: false); + else if (activeService.ToLowerInvariant() == "ollama") + url = endpoint.TrimEnd('/') + "/api/chat"; + else + url = endpoint.TrimEnd('/') + "/v1/chat/completions"; var json = JsonSerializer.Serialize(body); using var req = new HttpRequestMessage(HttpMethod.Post, url) @@ -473,7 +482,19 @@ public partial class LlmService throw new HttpRequestException($"{activeService} API 오류 ({resp.StatusCode}): {detail}"); } - var respJson = await resp.Content.ReadAsStringAsync(ct); + var rawResp = await resp.Content.ReadAsStringAsync(ct); + + // SSE 형식 응답 사전 처리 (stream:false 요청에도 SSE로 응답하는 경우) + var respJson = ExtractJsonFromSseIfNeeded(rawResp); + + // 비-JSON 응답(IBM 도구 호출 미지원 등) → ToolCallNotSupportedException으로 폴백 트리거 + { + var trimmedResp = respJson.TrimStart(); + if (!trimmedResp.StartsWith('{') && !trimmedResp.StartsWith('[')) + throw new ToolCallNotSupportedException( + $"vLLM 응답이 JSON이 아닙니다 (도구 호출 미지원 가능성): {respJson[..Math.Min(120, respJson.Length)]}"); + } + using var doc = JsonDocument.Parse(respJson); var root = doc.RootElement; @@ -540,7 +561,7 @@ public partial class LlmService return blocks; } - private object BuildOpenAiToolBody(List messages, IReadOnlyCollection tools) + private object BuildOpenAiToolBody(List messages, IReadOnlyCollection tools, bool forceToolCall = false) { var llm = _settings.Settings.Llm; var msgs = new List(); @@ -648,6 +669,7 @@ public partial class LlmService var activeService = ResolveService(); var activeModel = ResolveModel(); + var executionPolicy = GetActiveExecutionPolicy(); var isOllama = activeService.Equals("ollama", StringComparison.OrdinalIgnoreCase); if (isOllama) { @@ -657,7 +679,7 @@ public partial class LlmService messages = msgs, tools = toolDefs, stream = false, - options = new { temperature = ResolveTemperature() } + options = new { temperature = ResolveToolTemperature() } }; } @@ -667,15 +689,150 @@ public partial class LlmService ["messages"] = msgs, ["tools"] = toolDefs, ["stream"] = false, - ["temperature"] = ResolveTemperature(), + ["temperature"] = ResolveToolTemperature(), ["max_tokens"] = ResolveOpenAiCompatibleMaxTokens(), + ["parallel_tool_calls"] = executionPolicy.EnableParallelReadBatch, }; + // tool_choice: "required" — 모델이 반드시 도구를 호출하도록 강제 + // 아직 한 번도 도구를 호출하지 않은 첫 번째 요청에서만 사용 (chatty 모델 대응) + if (forceToolCall) + body["tool_choice"] = "required"; var effort = ResolveReasoningEffort(); if (!string.IsNullOrWhiteSpace(effort)) body["reasoning_effort"] = effort; return body; } + /// IBM 배포형 /text/chat 전용 도구 바디. parameters 래퍼 사용, model 필드 없음. + private object BuildIbmToolBody(List messages, IReadOnlyCollection tools, bool forceToolCall = false) + { + var msgs = new List(); + + // 시스템 프롬프트 + var systemPrompt = messages.FirstOrDefault(m => m.Role == "system")?.Content ?? _systemPrompt; + if (!string.IsNullOrWhiteSpace(systemPrompt)) + msgs.Add(new { role = "system", content = systemPrompt }); + + foreach (var m in messages) + { + if (m.Role == "system") continue; + + // tool_result → OpenAI role:"tool" 형식 (watsonx /text/chat 지원) + if (m.Role == "user" && m.Content.StartsWith("{\"type\":\"tool_result\"")) + { + try + { + using var doc = JsonDocument.Parse(m.Content); + var root = doc.RootElement; + msgs.Add(new + { + role = "tool", + tool_call_id = root.GetProperty("tool_use_id").GetString(), + content = root.GetProperty("content").GetString(), + }); + continue; + } + catch { } + } + + // _tool_use_blocks → OpenAI tool_calls 형식 + if (m.Role == "assistant" && m.Content.StartsWith("{\"_tool_use_blocks\"")) + { + try + { + using var doc = JsonDocument.Parse(m.Content); + var blocksArr = doc.RootElement.GetProperty("_tool_use_blocks"); + var textContent = ""; + var toolCallsList = new List(); + foreach (var b in blocksArr.EnumerateArray()) + { + var bType = b.GetProperty("type").GetString(); + if (bType == "text") + textContent = b.GetProperty("text").GetString() ?? ""; + else if (bType == "tool_use") + { + var argsJson = b.TryGetProperty("input", out var inp) ? inp.GetRawText() : "{}"; + toolCallsList.Add(new + { + id = b.GetProperty("id").GetString() ?? "", + type = "function", + function = new + { + name = b.GetProperty("name").GetString() ?? "", + arguments = argsJson, + } + }); + } + } + msgs.Add(new + { + role = "assistant", + content = string.IsNullOrEmpty(textContent) ? (string?)null : textContent, + tool_calls = toolCallsList, + }); + continue; + } + catch { } + } + + msgs.Add(new { role = m.Role == "assistant" ? "assistant" : "user", content = m.Content }); + } + + // OpenAI 호환 도구 정의 (형식 동일, watsonx에서 tools 필드 지원) + var toolDefs = tools.Select(t => + { + var paramDict = new Dictionary + { + ["type"] = "object", + ["properties"] = t.Parameters.Properties.ToDictionary( + kv => kv.Key, + kv => BuildPropertySchema(kv.Value, false)), + }; + if (t.Parameters.Required is { Count: > 0 }) + paramDict["required"] = t.Parameters.Required; + + return new + { + type = "function", + function = new + { + name = t.Name, + description = t.Description, + parameters = paramDict, + } + }; + }).ToArray(); + + // IBM watsonx: parameters 래퍼 사용, model 필드 없음 + // tool_choice: "required" 지원 여부는 배포 버전마다 다를 수 있으므로 + // forceToolCall=true일 때 추가하되, 오류 시 상위에서 ToolCallNotSupportedException으로 폴백됨 + if (forceToolCall) + { + return new + { + messages = msgs, + tools = toolDefs, + tool_choice = "required", + parameters = new + { + temperature = ResolveTemperature(), + max_new_tokens = ResolveOpenAiCompatibleMaxTokens() + } + }; + } + + return new + { + messages = msgs, + tools = toolDefs, + parameters = new + { + temperature = ResolveTemperature(), + max_new_tokens = ResolveOpenAiCompatibleMaxTokens() + } + }; + } + // ─── 공통 헬퍼 ───────────────────────────────────────────────────── /// ToolProperty를 LLM API용 스키마 객체로 변환. array/enum/items 포함. diff --git a/src/AxCopilot/Services/LlmService.cs b/src/AxCopilot/Services/LlmService.cs index 93eb1c5..42193a0 100644 --- a/src/AxCopilot/Services/LlmService.cs +++ b/src/AxCopilot/Services/LlmService.cs @@ -25,7 +25,10 @@ public partial class LlmService : IDisposable private string? _systemPrompt; private const int MaxRetries = 2; - private static readonly TimeSpan ChunkTimeout = TimeSpan.FromSeconds(30); + // 첫 청크: 모델이 컨텍스트를 처리하는 시간 (대용량 컨텍스트에서 3분까지 허용) + private static readonly TimeSpan FirstChunkTimeout = TimeSpan.FromSeconds(180); + // 이후 청크: 스트리밍이 시작된 후 청크 간 최대 간격 + private static readonly TimeSpan SubsequentChunkTimeout = TimeSpan.FromSeconds(45); private static readonly string SigmoidApiHost = string.Concat("api.", "an", "thr", "opic.com"); private static readonly string SigmoidApiVersionHeader = string.Concat("an", "thr", "opic-version"); private const string SigmoidApiVersion = "2023-06-01"; @@ -93,7 +96,12 @@ public partial class LlmService : IDisposable public (string service, string model) GetCurrentModelInfo() => (ResolveService(), ResolveModel()); /// 오버라이드를 고려한 실제 서비스명. - private string ResolveService() => NormalizeServiceName(_serviceOverride ?? _settings.Settings.Llm.Service); + private string ResolveService() + { + string? svc; + lock (_overrideLock) svc = _serviceOverride; + return NormalizeServiceName(svc ?? _settings.Settings.Llm.Service); + } private static bool IsExternalLlmService(string normalizedService) => normalizedService is "gemini" or "sigmoid"; @@ -129,12 +137,29 @@ public partial class LlmService : IDisposable /// 오버라이드를 고려한 실제 모델명. private string ResolveModel() { - if (_modelOverride != null) return _modelOverride; - return ResolveModelName(); + string? mdl; + lock (_overrideLock) mdl = _modelOverride; + return mdl ?? ResolveModelName(); } private double ResolveTemperature() => _temperatureOverride ?? _settings.Settings.Llm.Temperature; + internal string GetActiveExecutionProfileKey() + => Agent.ModelExecutionProfileCatalog.Normalize(GetActiveRegisteredModel()?.ExecutionProfile); + + internal Agent.ModelExecutionProfileCatalog.ExecutionPolicy GetActiveExecutionPolicy() + => Agent.ModelExecutionProfileCatalog.Get(GetActiveExecutionProfileKey()); + + internal double ResolveToolTemperature() + { + var resolved = ResolveTemperature(); + if (!_settings.Settings.Llm.UseAutomaticProfileTemperature) + return resolved; + + var cap = GetActiveExecutionPolicy().ToolTemperatureCap; + return cap.HasValue ? Math.Min(resolved, cap.Value) : resolved; + } + private string? ResolveReasoningEffort() => _reasoningEffortOverride; private static bool LooksLikeEncryptedPayload(string value) @@ -371,6 +396,49 @@ public partial class LlmService : IDisposable if (m.Role == "system") continue; + // assistant 메시지에 _tool_use_blocks 포함 시 텍스트만 추출 + // (IBM vLLM은 OpenAI tool_use 형식을 이해하지 못함) + if (m.Role == "assistant" && m.Content.Contains("_tool_use_blocks")) + { + try + { + using var doc = JsonDocument.Parse(m.Content); + if (doc.RootElement.TryGetProperty("_tool_use_blocks", out var blocks)) + { + var parts = new List(); + foreach (var block in blocks.EnumerateArray()) + { + if (!block.TryGetProperty("type", out var typeEl)) continue; + var type = typeEl.GetString(); + if (type == "text" && block.TryGetProperty("text", out var textEl)) + parts.Add(textEl.GetString() ?? ""); + else if (type == "tool_use" && block.TryGetProperty("name", out var nameEl)) + parts.Add($"[도구 호출: {nameEl.GetString()}]"); + } + var content = string.Join("\n", parts).Trim(); + if (!string.IsNullOrEmpty(content)) + msgs.Add(new { role = "assistant", content }); + continue; + } + } + catch { /* 파싱 실패 시 아래에서 원본 사용 */ } + } + + // user 메시지에 tool_result JSON 포함 시 평문으로 변환 + if (m.Role == "user" && m.Content.StartsWith("{\"type\":\"tool_result\"", StringComparison.Ordinal)) + { + try + { + using var doc = JsonDocument.Parse(m.Content); + var root = doc.RootElement; + var toolName = root.TryGetProperty("tool_name", out var tn) ? tn.GetString() ?? "tool" : "tool"; + var toolContent = root.TryGetProperty("content", out var tc) ? tc.GetString() ?? "" : ""; + msgs.Add(new { role = "user", content = $"[{toolName} 결과]\n{toolContent}" }); + continue; + } + catch { /* 파싱 실패 시 아래에서 원본 사용 */ } + } + msgs.Add(new { role = m.Role == "assistant" ? "assistant" : "user", @@ -656,10 +724,20 @@ public partial class LlmService : IDisposable using var stream = await resp.Content.ReadAsStreamAsync(ct); using var reader = new StreamReader(stream); + var firstChunkReceived = false; while (!reader.EndOfStream && !ct.IsCancellationRequested) { - var line = await ReadLineWithTimeoutAsync(reader, ct); - if (line == null) break; + var timeout = firstChunkReceived ? SubsequentChunkTimeout : FirstChunkTimeout; + var line = await ReadLineWithTimeoutAsync(reader, ct, timeout); + if (line == null) + { + if (!firstChunkReceived) + LogService.Warn($"Ollama 첫 청크 타임아웃 ({(int)FirstChunkTimeout.TotalSeconds}초) — 모델이 응답하지 않습니다"); + else + yield return "\n\n*(응답이 중간에 끊겼습니다 — 연결 시간 초과)*"; + break; + } + firstChunkReceived = true; if (string.IsNullOrEmpty(line)) continue; string? text = null; @@ -669,7 +747,6 @@ public partial class LlmService : IDisposable if (doc.RootElement.TryGetProperty("message", out var msg) && msg.TryGetProperty("content", out var c)) text = c.GetString(); - // Ollama: done=true 시 토큰 사용량 포함 if (doc.RootElement.TryGetProperty("done", out var done) && done.GetBoolean()) TryParseOllamaUsage(doc.RootElement); } @@ -721,11 +798,16 @@ public partial class LlmService : IDisposable using var resp = await SendWithErrorClassificationAsync(req, allowInsecureTls, ct); var respBody = await resp.Content.ReadAsStringAsync(ct); - return SafeParseJson(respBody, root => + + // IBM vLLM이 stream:false 요청에도 SSE 형식(id:/event/data: 라인)으로 응답하는 경우 처리 + var effectiveBody = ExtractJsonFromSseIfNeeded(respBody); + + return SafeParseJson(effectiveBody, root => { TryParseOpenAiUsage(root); if (usesIbmDeploymentApi) { + // SSE에서 누적된 텍스트가 이미 하나의 JSON이 아닐 수 있으므로 재추출 var parsed = ExtractIbmDeploymentText(root); return string.IsNullOrWhiteSpace(parsed) ? "(빈 응답)" : parsed; } @@ -736,6 +818,81 @@ public partial class LlmService : IDisposable }, "vLLM 응답"); } + /// + /// IBM vLLM이 stream:false 요청에도 SSE 포맷(id:/event/data: 라인)을 반환할 때 + /// "data: {...}" 라인에서 JSON만 추출합니다. 일반 JSON이면 그대로 반환합니다. + /// + private static string ExtractJsonFromSseIfNeeded(string raw) + { + if (string.IsNullOrWhiteSpace(raw)) return raw; + var trimmed = raw.TrimStart(); + + // 일반 JSON이면 그대로 + if (trimmed.StartsWith('{') || trimmed.StartsWith('[')) + return raw; + + // SSE 포맷: "data: {...}" 라인 중 마지막 유효한 것 사용 + // (stream:false지만 SSE로 오면 보통 단일 data 라인 + [DONE]) + string? lastDataJson = null; + var sb = new System.Text.StringBuilder(); + bool collectingChunks = false; + + foreach (var line in raw.Split('\n')) + { + var l = line.TrimEnd('\r').Trim(); + if (!l.StartsWith("data: ", StringComparison.Ordinal)) continue; + var data = l["data: ".Length..].Trim(); + if (data == "[DONE]") break; + if (string.IsNullOrEmpty(data)) continue; + + // choices[].delta.content 형식(스트리밍 청크)인 경우 텍스트를 누적 + // 단일 완성 응답(choices[].message)이면 바로 반환 + lastDataJson = data; + try + { + using var doc = JsonDocument.Parse(data); + // 스트리밍 청크(delta) → content 누적 + if (doc.RootElement.TryGetProperty("choices", out var ch) && ch.GetArrayLength() > 0) + { + var first = ch[0]; + if (first.TryGetProperty("delta", out var delta) + && delta.TryGetProperty("content", out var cnt)) + { + var txt = cnt.GetString(); + if (!string.IsNullOrEmpty(txt)) { sb.Append(txt); collectingChunks = true; } + } + else if (first.TryGetProperty("message", out _)) + { + // 완성 응답 → 이 JSON을 그대로 사용 + return data; + } + } + // IBM results[] 형식 + else if (doc.RootElement.TryGetProperty("results", out var res) && res.GetArrayLength() > 0) + { + return data; + } + } + catch { /* 파싱 실패 라인 무시 */ } + } + + // 청크를 누적한 경우 OpenAI message 형식으로 재조립 + if (collectingChunks && sb.Length > 0) + { + var assembled = System.Text.Json.JsonSerializer.Serialize(new + { + choices = new[] + { + new { message = new { content = sb.ToString() } } + } + }); + return assembled; + } + + // 마지막 data 라인을 그대로 사용 + return lastDataJson ?? raw; + } + private async IAsyncEnumerable StreamOpenAiCompatibleAsync( List messages, [EnumeratorCancellation] CancellationToken ct) @@ -759,10 +916,20 @@ public partial class LlmService : IDisposable using var stream = await resp.Content.ReadAsStreamAsync(ct); using var reader = new StreamReader(stream); + var firstChunkReceived = false; while (!reader.EndOfStream && !ct.IsCancellationRequested) { - var line = await ReadLineWithTimeoutAsync(reader, ct); - if (line == null) break; + var timeout = firstChunkReceived ? SubsequentChunkTimeout : FirstChunkTimeout; + var line = await ReadLineWithTimeoutAsync(reader, ct, timeout); + if (line == null) + { + if (!firstChunkReceived) + LogService.Warn($"vLLM 첫 청크 타임아웃 ({(int)FirstChunkTimeout.TotalSeconds}초) — 모델이 응답하지 않습니다"); + else + yield return "\n\n*(응답이 중간에 끊겼습니다 — 연결 시간 초과)*"; + break; + } + firstChunkReceived = true; if (string.IsNullOrEmpty(line) || !line.StartsWith("data: ")) continue; var data = line["data: ".Length..]; if (data == "[DONE]") break; @@ -882,10 +1049,20 @@ public partial class LlmService : IDisposable using var stream = await resp.Content.ReadAsStreamAsync(ct); using var reader = new StreamReader(stream); + var firstChunkReceived = false; while (!reader.EndOfStream && !ct.IsCancellationRequested) { - var line = await ReadLineWithTimeoutAsync(reader, ct); - if (line == null) break; + var timeout = firstChunkReceived ? SubsequentChunkTimeout : FirstChunkTimeout; + var line = await ReadLineWithTimeoutAsync(reader, ct, timeout); + if (line == null) + { + if (!firstChunkReceived) + LogService.Warn($"Gemini 첫 청크 타임아웃 ({(int)FirstChunkTimeout.TotalSeconds}초)"); + else + yield return "\n\n*(응답이 중간에 끊겼습니다 — 연결 시간 초과)*"; + break; + } + firstChunkReceived = true; if (string.IsNullOrEmpty(line) || !line.StartsWith("data: ")) continue; var data = line["data: ".Length..]; string? parsed = null; @@ -1016,10 +1193,20 @@ public partial class LlmService : IDisposable using var stream = await resp.Content.ReadAsStreamAsync(ct); using var reader = new StreamReader(stream); + var firstChunkReceived = false; while (!reader.EndOfStream && !ct.IsCancellationRequested) { - var line = await ReadLineWithTimeoutAsync(reader, ct); - if (line == null) break; + var timeout = firstChunkReceived ? SubsequentChunkTimeout : FirstChunkTimeout; + var line = await ReadLineWithTimeoutAsync(reader, ct, timeout); + if (line == null) + { + if (!firstChunkReceived) + LogService.Warn($"Claude 첫 청크 타임아웃 ({(int)FirstChunkTimeout.TotalSeconds}초)"); + else + yield return "\n\n*(응답이 중간에 끊겼습니다 — 연결 시간 초과)*"; + break; + } + firstChunkReceived = true; if (string.IsNullOrEmpty(line) || !line.StartsWith("data: ")) continue; var data = line["data: ".Length..]; @@ -1201,19 +1388,18 @@ public partial class LlmService : IDisposable return resp; } - /// 스트리밍 ReadLine에 청크 타임아웃 적용 - private static async Task ReadLineWithTimeoutAsync(StreamReader reader, CancellationToken ct) + /// 스트리밍 ReadLine에 청크 타임아웃 적용. 타임아웃 시 null 반환. + private static async Task ReadLineWithTimeoutAsync(StreamReader reader, CancellationToken ct, TimeSpan timeout) { using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); - cts.CancelAfter(ChunkTimeout); + cts.CancelAfter(timeout); try { return await reader.ReadLineAsync(cts.Token); } catch (OperationCanceledException) when (!ct.IsCancellationRequested) { - LogService.Warn("스트리밍 청크 타임아웃 (30초 무응답)"); - return null; // 타임아웃 시 스트림 종료 + return null; } } diff --git a/src/AxCopilot/Services/MarkdownRenderer.cs b/src/AxCopilot/Services/MarkdownRenderer.cs index 68172ad..2cba5d1 100644 --- a/src/AxCopilot/Services/MarkdownRenderer.cs +++ b/src/AxCopilot/Services/MarkdownRenderer.cs @@ -1,7 +1,10 @@ -using System.Text.RegularExpressions; +using System.Diagnostics; +using System.IO; +using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; +using System.Windows.Input; using System.Windows.Media; namespace AxCopilot.Services; @@ -313,12 +316,19 @@ public static class MarkdownRenderer if (pm.Index > lastIndex) AddPlainTextWithCodeSymbols(inlines, text[lastIndex..pm.Index]); - // 파일 경로 — 파란색 강조 - inlines.Add(new Run(pm.Value) + // 파일 경로 — 파란색 강조 + 클릭 시 파일 열기 + var matchedPath = pm.Value; + var hyperlink = new Hyperlink(new Run(matchedPath)) { Foreground = FilePathBrush, FontWeight = FontWeights.Medium, - }); + TextDecorations = null, + Cursor = Cursors.Hand, + }; + hyperlink.MouseEnter += (_, _) => hyperlink.TextDecorations = TextDecorations.Underline; + hyperlink.MouseLeave += (_, _) => hyperlink.TextDecorations = null; + hyperlink.Click += (_, _) => OpenFilePath(matchedPath); + inlines.Add(hyperlink); lastIndex = pm.Index + pm.Length; } @@ -327,6 +337,19 @@ public static class MarkdownRenderer AddPlainTextWithCodeSymbols(inlines, text[lastIndex..]); } + private static void OpenFilePath(string path) + { + try + { + var expanded = Environment.ExpandEnvironmentVariables(path); + if (File.Exists(expanded)) + Process.Start(new ProcessStartInfo(expanded) { UseShellExecute = true }); + else if (Directory.Exists(expanded)) + Process.Start("explorer.exe", $"\"{expanded}\""); + } + catch { /* 경로가 잘못됐거나 앱이 없으면 무시 */ } + } + private static void AddPlainTextWithCodeSymbols(InlineCollection inlines, string text) { if (string.IsNullOrEmpty(text)) diff --git a/src/AxCopilot/Services/TokenEstimator.cs b/src/AxCopilot/Services/TokenEstimator.cs index 494ff87..9e439a6 100644 --- a/src/AxCopilot/Services/TokenEstimator.cs +++ b/src/AxCopilot/Services/TokenEstimator.cs @@ -63,11 +63,11 @@ public static class TokenEstimator }; } - /// 토큰 수를 읽기 쉬운 문자열로 포맷합니다. + /// 토큰 수를 읽기 쉬운 문자열로 포맷합니다. 이진 단위(1K=1024) 사용. public static string Format(int count) => count switch { - >= 1_000_000 => $"{count / 1_000_000.0:0.#}M", - >= 1_000 => $"{count / 1_000.0:0.#}K", + >= 1_048_576 => $"{count / 1_048_576.0:0.#}M", // 1024*1024 + >= 1_024 => $"{count / 1_024.0:0.#}K", // 1024 _ => count.ToString(), }; diff --git a/src/AxCopilot/ViewModels/LauncherViewModel.Widgets.cs b/src/AxCopilot/ViewModels/LauncherViewModel.Widgets.cs index 031e0d7..b1bf20c 100644 --- a/src/AxCopilot/ViewModels/LauncherViewModel.Widgets.cs +++ b/src/AxCopilot/ViewModels/LauncherViewModel.Widgets.cs @@ -76,20 +76,37 @@ public partial class LauncherViewModel private int _widgetRefreshTick; public void UpdateWidgets() + => UpdateWidgets(true, true, true, true, true); + + public void UpdateWidgets( + bool includePerf, + bool includePomo, + bool includeNote, + bool includeCalendar, + bool includeServerStatus) { _widgetRefreshTick++; - if (_widgetRefreshTick % 5 == 0) + if (includeNote && _widgetRefreshTick % 5 == 0) _widgetNoteCount = GetNoteCount(); - OnPropertyChanged(nameof(Widget_PerfText)); - OnPropertyChanged(nameof(Widget_PomoText)); - OnPropertyChanged(nameof(Widget_PomoRunning)); - OnPropertyChanged(nameof(Widget_NoteText)); - OnPropertyChanged(nameof(Widget_OllamaOnline)); - OnPropertyChanged(nameof(Widget_LlmOnline)); - OnPropertyChanged(nameof(Widget_McpOnline)); - OnPropertyChanged(nameof(Widget_McpName)); - OnPropertyChanged(nameof(Widget_CalText)); + if (includePerf) + OnPropertyChanged(nameof(Widget_PerfText)); + if (includePomo) + { + OnPropertyChanged(nameof(Widget_PomoText)); + OnPropertyChanged(nameof(Widget_PomoRunning)); + } + if (includeNote) + OnPropertyChanged(nameof(Widget_NoteText)); + if (includeServerStatus) + { + OnPropertyChanged(nameof(Widget_OllamaOnline)); + OnPropertyChanged(nameof(Widget_LlmOnline)); + OnPropertyChanged(nameof(Widget_McpOnline)); + OnPropertyChanged(nameof(Widget_McpName)); + } + if (includeCalendar) + OnPropertyChanged(nameof(Widget_CalText)); } private static int GetNoteCount() diff --git a/src/AxCopilot/ViewModels/SettingsViewModel.cs b/src/AxCopilot/ViewModels/SettingsViewModel.cs index 4f75bf9..4661e47 100644 --- a/src/AxCopilot/ViewModels/SettingsViewModel.cs +++ b/src/AxCopilot/ViewModels/SettingsViewModel.cs @@ -1245,6 +1245,7 @@ public class SettingsViewModel : INotifyPropertyChanged Alias = rm.Alias, EncryptedModelName = rm.EncryptedModelName, Service = rm.Service, + ExecutionProfile = rm.ExecutionProfile ?? "balanced", Endpoint = rm.Endpoint, ApiKey = rm.ApiKey, AllowInsecureTls = rm.AllowInsecureTls, @@ -1679,6 +1680,7 @@ public class SettingsViewModel : INotifyPropertyChanged Alias = rm.Alias, EncryptedModelName = rm.EncryptedModelName, Service = rm.Service, + ExecutionProfile = rm.ExecutionProfile ?? "balanced", Endpoint = rm.Endpoint, ApiKey = rm.ApiKey, AllowInsecureTls = rm.AllowInsecureTls, @@ -1998,6 +2000,7 @@ public class RegisteredModelRow : INotifyPropertyChanged private string _alias = ""; private string _encryptedModelName = ""; private string _service = "ollama"; + private string _executionProfile = "balanced"; private string _endpoint = ""; private string _apiKey = ""; private bool _allowInsecureTls; @@ -2023,6 +2026,12 @@ public class RegisteredModelRow : INotifyPropertyChanged set { _service = value; OnPropertyChanged(); OnPropertyChanged(nameof(ServiceLabel)); } } + public string ExecutionProfile + { + get => _executionProfile; + set { _executionProfile = value; OnPropertyChanged(); OnPropertyChanged(nameof(ProfileLabel)); } + } + /// 이 모델 전용 서버 엔드포인트. 비어있으면 기본 엔드포인트 사용. public string Endpoint { @@ -2098,6 +2107,15 @@ public class RegisteredModelRow : INotifyPropertyChanged /// 서비스 라벨 public string ServiceLabel => _service == "vllm" ? "vLLM" : "Ollama"; + public string ProfileLabel => (_executionProfile ?? "balanced").Trim().ToLowerInvariant() switch + { + "tool_call_strict" => "도구 호출 우선", + "reasoning_first" => "추론 우선", + "fast_readonly" => "읽기 속도 우선", + "document_heavy" => "문서 생성 우선", + _ => "균형", + }; + public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string? n = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); diff --git a/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs b/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs index 4fb6487..78417b4 100644 --- a/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs +++ b/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System.Windows.Input; @@ -309,6 +310,10 @@ public partial class ChatWindow private static string BuildReadableProcessFeedSummary(AgentEvent evt, string transcriptBadgeLabel, string itemDisplayName) { + var phaseLabel = ResolveProgressPhaseLabel(evt); + if (!string.IsNullOrWhiteSpace(phaseLabel)) + return phaseLabel; + return evt.Type switch { AgentEventType.Thinking when string.Equals(evt.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase) @@ -412,6 +417,10 @@ public partial class ChatWindow { var parts = new List(); + var phaseMeta = ResolveProgressPhaseMeta(evt); + if (!string.IsNullOrWhiteSpace(phaseMeta)) + parts.Add(phaseMeta); + var normalizedElapsedMs = NormalizeProgressElapsedMs(evt.ElapsedMs); if (normalizedElapsedMs > 0) { @@ -431,6 +440,61 @@ public partial class ChatWindow return string.Join(" · ", parts); } + private static string? ResolveProgressPhaseLabel(AgentEvent evt) + { + var summary = (evt.Summary ?? string.Empty).Trim(); + var toolName = (evt.ToolName ?? string.Empty).Trim(); + + if (string.Equals(toolName, "context_compaction", StringComparison.OrdinalIgnoreCase)) + return "컨텍스트 압축 중..."; + if (string.Equals(toolName, "agent_wait", StringComparison.OrdinalIgnoreCase)) + return "처리 중..."; + if (summary.Contains("html_create", StringComparison.OrdinalIgnoreCase) + || summary.Contains("document_assemble", StringComparison.OrdinalIgnoreCase) + || summary.Contains("docx_create", StringComparison.OrdinalIgnoreCase)) + return "문서 결과 생성 중..."; + if (summary.Contains("검증", StringComparison.OrdinalIgnoreCase) + || summary.Contains("verification", StringComparison.OrdinalIgnoreCase)) + return "결과 검증 중..."; + if (summary.Contains("diff", StringComparison.OrdinalIgnoreCase)) + return "변경 내용 확인 중..."; + if (evt.Type == AgentEventType.ToolCall && !string.IsNullOrWhiteSpace(toolName)) + { + return toolName switch + { + "file_read" or "directory_list" or "glob" or "grep" or "folder_map" or "multi_read" => "파일 탐색 중...", + "file_edit" or "file_write" or "html_create" or "docx_create" or "markdown_create" => "산출물 생성 중...", + "build_run" or "test_loop" => "실행 결과 확인 중...", + _ => null, + }; + } + + return null; + } + + private static string? ResolveProgressPhaseMeta(AgentEvent evt) + { + var summary = evt.Summary ?? string.Empty; + var toolName = evt.ToolName ?? string.Empty; + + if (evt.Type == AgentEventType.Planning) + return "계획"; + if (evt.Type == AgentEventType.StepStart || evt.Type == AgentEventType.StepDone) + return "단계"; + if (string.Equals(toolName, "context_compaction", StringComparison.OrdinalIgnoreCase)) + return "압축"; + if (summary.Contains("검증", StringComparison.OrdinalIgnoreCase) || summary.Contains("verification", StringComparison.OrdinalIgnoreCase)) + return "검증"; + if (summary.Contains("fallback", StringComparison.OrdinalIgnoreCase) || summary.Contains("자동 생성", StringComparison.OrdinalIgnoreCase)) + return "폴백"; + if (summary.Contains("재시도", StringComparison.OrdinalIgnoreCase) || summary.Contains("retry", StringComparison.OrdinalIgnoreCase)) + return "재시도"; + if (evt.Type == AgentEventType.ToolCall) + return "도구"; + + return null; + } + private Border CreateReadableProgressFeedCard( string summary, Brush primaryText, @@ -909,6 +973,161 @@ public partial class ChatWindow } } + // ─── 에이전트 실행 통합 진행 카드 ───────────────────────────────────────── + + private void AddLiveRunProgressCard(IReadOnlyList steps) + { + if (steps.Count == 0) return; + + var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black; + var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray; + var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; + var accentColor = (accentBrush as SolidColorBrush)?.Color ?? Color.FromRgb(0x59, 0xA5, 0xF5); + + var cardBg = new SolidColorBrush(Color.FromArgb(0x0D, accentColor.R, accentColor.G, accentColor.B)); + var cardBorder = new SolidColorBrush(Color.FromArgb(0x2A, accentColor.R, accentColor.G, accentColor.B)); + var doneBulletColor = new SolidColorBrush(Color.FromRgb(0x22, 0xC5, 0x5E)); + + var outerStack = new StackPanel(); + + // 헤더 행: 펄스 점 + "작업 진행 중" + var headerRow = new StackPanel + { + Orientation = Orientation.Horizontal, + Margin = new Thickness(14, 8, 14, 6), + }; + + var headerDot = new Border + { + Width = 8, + Height = 8, + CornerRadius = new CornerRadius(999), + Background = accentBrush, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + }; + ApplyLiveWaitingPulseToMarker(headerDot); + headerRow.Children.Add(headerDot); + + headerRow.Children.Add(new TextBlock + { + Text = "작업 진행 중", + FontSize = 11.5, + FontWeight = FontWeights.SemiBold, + Foreground = accentBrush, + VerticalAlignment = VerticalAlignment.Center, + }); + + outerStack.Children.Add(headerRow); + + // 구분선 + outerStack.Children.Add(new Border + { + Height = 1, + Background = cardBorder, + Margin = new Thickness(14, 0, 14, 6), + }); + + // 스텝 목록 + var stepsPanel = new StackPanel + { + Margin = new Thickness(14, 0, 14, 8), + }; + + for (var i = 0; i < steps.Count; i++) + { + var step = steps[i]; + var isLast = i == steps.Count - 1; + + var itemGrid = new Grid { Margin = new Thickness(0, 2, 0, 2) }; + itemGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(18) }); + itemGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + itemGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + // 불릿: 완료는 ✓, 진행 중은 펄스 점 + if (isLast) + { + var liveDot = new Border + { + Width = 7, + Height = 7, + CornerRadius = new CornerRadius(999), + Background = accentBrush, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 0, 0), + }; + ApplyLiveWaitingPulseToMarker(liveDot); + itemGrid.Children.Add(liveDot); + } + else + { + itemGrid.Children.Add(new TextBlock + { + Text = "✓", + FontSize = 9, + Foreground = doneBulletColor, + VerticalAlignment = VerticalAlignment.Center, + }); + } + + // 스텝 레이블 + var transcriptLabel = GetTranscriptBadgeLabel(step); + var itemDisplayName = GetAgentItemDisplayName(step.ToolName); + var label = BuildReadableProcessFeedSummary(step, transcriptLabel, itemDisplayName); + if (string.IsNullOrWhiteSpace(label)) label = transcriptLabel; + + var labelBlock = new TextBlock + { + Text = label, + FontSize = isLast ? 12 : 11.5, + FontWeight = isLast ? FontWeights.SemiBold : FontWeights.Normal, + Foreground = isLast ? primaryText : secondaryText, + VerticalAlignment = VerticalAlignment.Center, + TextTrimming = TextTrimming.CharacterEllipsis, + Margin = new Thickness(4, 0, 0, 0), + }; + Grid.SetColumn(labelBlock, 1); + itemGrid.Children.Add(labelBlock); + + // 경과 시간 메타 (완료된 스텝만) + if (!isLast) + { + var meta = BuildReadableProgressMetaText(step); + if (!string.IsNullOrWhiteSpace(meta)) + { + var metaBlock = new TextBlock + { + Text = meta, + FontSize = 10, + Foreground = secondaryText, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(8, 0, 0, 0), + Opacity = 0.8, + }; + Grid.SetColumn(metaBlock, 2); + itemGrid.Children.Add(metaBlock); + } + } + + stepsPanel.Children.Add(itemGrid); + } + + outerStack.Children.Add(stepsPanel); + + var card = new Border + { + Background = cardBg, + BorderBrush = cardBorder, + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(12), + Margin = new Thickness(12, 4, 12, 4), + HorizontalAlignment = HorizontalAlignment.Stretch, + Child = outerStack, + }; + + MessagePanel.Children.Add(card); + } + private void AddAgentEventBanner(AgentEvent evt) { var logLevel = _settings.Settings.Llm.AgentLogLevel; diff --git a/src/AxCopilot/Views/ChatWindow.ComposerQueuePresentation.cs b/src/AxCopilot/Views/ChatWindow.ComposerQueuePresentation.cs index 1ba128c..d7ad7cc 100644 --- a/src/AxCopilot/Views/ChatWindow.ComposerQueuePresentation.cs +++ b/src/AxCopilot/Views/ChatWindow.ComposerQueuePresentation.cs @@ -81,7 +81,8 @@ public partial class ChatWindow if (string.IsNullOrWhiteSpace(text)) return; - if (_isStreaming && string.Equals(priority, "now", StringComparison.OrdinalIgnoreCase)) + // 현재 탭이 스트리밍 중일 때만 우선순위를 "next"로 낮춤 — 다른 탭 스트리밍은 무관 + if (_streamingTabs.Contains(_activeTab) && string.Equals(priority, "now", StringComparison.OrdinalIgnoreCase)) priority = "next"; HideSlashChip(restoreText: false); @@ -97,19 +98,30 @@ public partial class ChatWindow if (queuedItem == null) return; - if (!_isStreaming && startImmediatelyWhenIdle) + if (!_streamingTabs.Contains(_activeTab) && startImmediatelyWhenIdle) { StartNextQueuedDraftIfAny(queuedItem.Id); return; } + var runningTab = _streamRunTab; + var runningLabel = runningTab switch + { + "Cowork" => "코워크", + "Code" => "코드", + "Chat" => "채팅", + _ => runningTab ?? "다른 탭", + }; + var suffix = !string.IsNullOrEmpty(runningTab) && !string.Equals(runningTab, _activeTab, StringComparison.OrdinalIgnoreCase) + ? $" ({runningLabel} 실행 완료 후 자동 실행)" + : " (실행 완료 후 자동 실행)"; var toast = queuedItem.Kind switch { - "command" => "명령이 대기열에 추가되었습니다.", - "direct" => "직접 실행 요청이 대기열에 추가되었습니다.", - "steering" => "조정 요청이 대기열에 추가되었습니다.", - "followup" => "후속 작업이 대기열에 추가되었습니다.", - _ => "메시지가 대기열에 추가되었습니다.", + "command" => "명령이 대기열에 추가되었습니다." + suffix, + "direct" => "직접 실행 요청이 대기열에 추가되었습니다." + suffix, + "steering" => "조정 요청이 대기열에 추가되었습니다." + suffix, + "followup" => "후속 작업이 대기열에 추가되었습니다." + suffix, + _ => "메시지가 대기열에 추가되었습니다." + suffix, }; ShowToast(toast); } @@ -135,16 +147,7 @@ public partial class ChatWindow RebuildDraftQueuePanel(items); } - private bool IsDraftQueueExpanded() - => _expandedDraftQueueTabs.Contains(_activeTab); - - private void ToggleDraftQueueExpanded() - { - if (!_expandedDraftQueueTabs.Add(_activeTab)) - _expandedDraftQueueTabs.Remove(_activeTab); - - RefreshDraftQueueUi(); - } + // --- Queue panel (Codex style) --- private void RebuildDraftQueuePanel(IReadOnlyList items) { @@ -154,6 +157,8 @@ public partial class ChatWindow DraftQueuePanel.Children.Clear(); var visibleItems = items + .Where(x => !string.Equals(x.State, "completed", StringComparison.OrdinalIgnoreCase) // 완료 항목 제거 + && !string.Equals(x.State, "running", StringComparison.OrdinalIgnoreCase)) // 수행 중인 항목은 채팅창에서 이미 표시됨 .OrderBy(GetDraftStateRank) .ThenBy(GetDraftPriorityRank) .ThenBy(x => x.CreatedAt) @@ -165,206 +170,142 @@ public partial class ChatWindow return; } - var summary = _appState.GetDraftQueueSummary(_activeTab); - var shouldShowQueue = - IsDraftQueueExpanded() - || summary.RunningCount > 0 - || summary.QueuedCount > 0 - || summary.FailedCount > 0; - - if (!shouldShowQueue) - { - DraftQueuePanel.Visibility = Visibility.Collapsed; - return; - } - DraftQueuePanel.Visibility = Visibility.Visible; - DraftQueuePanel.Children.Add(CreateDraftQueueSummaryStrip(summary, IsDraftQueueExpanded())); - if (!IsDraftQueueExpanded()) + + // 단일 통합 컨테이너 + var containerBorder = new Border { - DraftQueuePanel.Children.Add(CreateCompactDraftQueuePanel(visibleItems, summary)); - return; - } - - const int maxPerSection = 3; - var runningItems = visibleItems - .Where(item => string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase)) - .Take(maxPerSection) - .ToList(); - var queuedItems = visibleItems - .Where(item => string.Equals(item.State, "queued", StringComparison.OrdinalIgnoreCase) && !IsDraftBlocked(item)) - .Take(maxPerSection) - .ToList(); - var blockedItems = visibleItems - .Where(IsDraftBlocked) - .Take(maxPerSection) - .ToList(); - var completedItems = visibleItems - .Where(item => string.Equals(item.State, "completed", StringComparison.OrdinalIgnoreCase)) - .Take(maxPerSection) - .ToList(); - var failedItems = visibleItems - .Where(item => string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase)) - .Take(maxPerSection) - .ToList(); - - AddDraftQueueSection("실행 중", runningItems, summary.RunningCount); - AddDraftQueueSection("다음 작업", queuedItems, summary.QueuedCount); - AddDraftQueueSection("보류", blockedItems, summary.BlockedCount); - AddDraftQueueSection("완료", completedItems, summary.CompletedCount); - AddDraftQueueSection("실패", failedItems, summary.FailedCount); - - if (summary.CompletedCount > 0 || summary.FailedCount > 0) - { - var footer = new StackPanel - { - Orientation = Orientation.Horizontal, - Margin = new Thickness(0, 2, 0, 0), - }; - - if (summary.CompletedCount > 0) - footer.Children.Add(CreateDraftQueueActionButton($"완료 정리 {summary.CompletedCount}", ClearCompletedDrafts, BrushFromHex("#ECFDF5"))); - - if (summary.FailedCount > 0) - footer.Children.Add(CreateDraftQueueActionButton($"실패 정리 {summary.FailedCount}", ClearFailedDrafts, BrushFromHex("#FEF2F2"))); - - DraftQueuePanel.Children.Add(footer); - } - } - - private UIElement CreateCompactDraftQueuePanel(IReadOnlyList items, AppStateService.DraftQueueSummaryState summary) - { - var primaryText = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#111827"); - var secondaryText = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#6B7280"); - var background = TryFindResource("ItemBackground") as Brush ?? BrushFromHex("#F7F7F8"); - var borderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E4E4E7"); - var focusItem = items.FirstOrDefault(item => string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase)) - ?? items.FirstOrDefault(item => string.Equals(item.State, "queued", StringComparison.OrdinalIgnoreCase) && !IsDraftBlocked(item)) - ?? items.FirstOrDefault(IsDraftBlocked) - ?? items.FirstOrDefault(item => string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase)) - ?? items.FirstOrDefault(item => string.Equals(item.State, "completed", StringComparison.OrdinalIgnoreCase)); - - var container = new Border - { - Background = background, - BorderBrush = borderBrush, + Background = TryFindResource("ItemBackground") as Brush ?? BrushFromHex("#1E1E2A"), + BorderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#3A3A4A"), BorderThickness = new Thickness(1), - CornerRadius = new CornerRadius(14), - Padding = new Thickness(12, 10, 12, 10), + CornerRadius = new CornerRadius(10), Margin = new Thickness(0, 0, 0, 4), }; - var root = new Grid(); - root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); - root.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); - container.Child = root; + var innerStack = new StackPanel(); + containerBorder.Child = innerStack; - var left = new StackPanel(); - left.Children.Add(new TextBlock + for (int i = 0; i < visibleItems.Count; i++) { - Text = focusItem == null - ? "대기열 항목이 준비되면 여기에서 요약됩니다." - : $"{GetDraftStateLabel(focusItem)} · {GetDraftKindLabel(focusItem)}", - FontSize = 11, - FontWeight = FontWeights.SemiBold, - Foreground = primaryText, - }); - left.Children.Add(new TextBlock - { - Text = focusItem?.Text ?? BuildDraftQueueCompactSummaryText(summary), - FontSize = 10.5, - Foreground = secondaryText, - TextTrimming = TextTrimming.CharacterEllipsis, - Margin = new Thickness(0, 4, 0, 0), - MaxWidth = 520, - }); - Grid.SetColumn(left, 0); - root.Children.Add(left); + innerStack.Children.Add(CreateDraftQueueRow(visibleItems[i])); - var action = CreateDraftQueueActionButton("상세", ToggleDraftQueueExpanded); - action.Margin = new Thickness(12, 0, 0, 0); - Grid.SetColumn(action, 1); - root.Children.Add(action); - - return container; - } - - private static string BuildDraftQueueCompactSummaryText(AppStateService.DraftQueueSummaryState summary) - { - var parts = new List(); - if (summary.RunningCount > 0) parts.Add($"실행 {summary.RunningCount}"); - if (summary.QueuedCount > 0) parts.Add($"다음 {summary.QueuedCount}"); - if (summary.FailedCount > 0) parts.Add($"실패 {summary.FailedCount}"); - return parts.Count == 0 ? "대기열 0" : string.Join(" · ", parts); - } - - private void AddDraftQueueSection(string label, IReadOnlyList items, int totalCount) - { - if (DraftQueuePanel == null || totalCount <= 0) - return; - - DraftQueuePanel.Children.Add(CreateDraftQueueSectionLabel($"{label} · {totalCount}")); - foreach (var item in items) - DraftQueuePanel.Children.Add(CreateDraftQueueCard(item)); - - if (totalCount > items.Count) - { - DraftQueuePanel.Children.Add(new TextBlock + // 구분선 (마지막 항목 제외) + if (i < visibleItems.Count - 1) { - Text = $"추가 항목 {totalCount - items.Count}개", - Margin = new Thickness(8, -2, 0, 8), - FontSize = 10.5, - Foreground = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#7A7F87"), - }); + innerStack.Children.Add(new Border + { + Height = 1, + Background = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#2E2E3E"), + Margin = new Thickness(10, 0, 10, 0), + }); + } + } + + DraftQueuePanel.Children.Add(containerBorder); + + // 실패 항목 정리 버튼 (실패만 — 완료는 자동 제거됨) + var summary = _appState.GetDraftQueueSummary(_activeTab); + if (summary.FailedCount > 0) + { + var failBtn = CreateQueueFooterButton($"실패 정리 ({summary.FailedCount})", ClearFailedDrafts); + DraftQueuePanel.Children.Add(failBtn); } } - private UIElement CreateDraftQueueSummaryStrip(AppStateService.DraftQueueSummaryState summary, bool isExpanded) + // --- Codex-style row --- + + private Border CreateDraftQueueRow(DraftQueueItem item) { - var root = new Grid + var isRunning = string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase); + var isFailed = string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase); + var primaryText = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#E5E5EA"); + var secondaryText = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#8E8E9A"); + + var row = new Border { - Margin = new Thickness(0, 0, 0, 8), + Padding = new Thickness(10, 7, 8, 7), }; - root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); - root.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); - var wrap = new WrapPanel(); + var grid = new Grid(); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // state icon + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); // text + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // actions + row.Child = grid; - if (summary.RunningCount > 0) - wrap.Children.Add(CreateQueueSummaryPill("실행 중", summary.RunningCount.ToString(), "#EFF6FF", "#BFDBFE", "#1D4ED8")); - if (summary.QueuedCount > 0) - wrap.Children.Add(CreateQueueSummaryPill("다음", summary.QueuedCount.ToString(), "#F5F3FF", "#DDD6FE", "#6D28D9")); - if (isExpanded && summary.BlockedCount > 0) - wrap.Children.Add(CreateQueueSummaryPill("보류", summary.BlockedCount.ToString(), "#FFF7ED", "#FDBA74", "#C2410C")); - if (isExpanded && summary.CompletedCount > 0) - wrap.Children.Add(CreateQueueSummaryPill("완료", summary.CompletedCount.ToString(), "#ECFDF5", "#BBF7D0", "#166534")); - if (summary.FailedCount > 0) - wrap.Children.Add(CreateQueueSummaryPill("실패", summary.FailedCount.ToString(), "#FEF2F2", "#FECACA", "#991B1B")); + // State icon + var stateIcon = new TextBlock + { + Text = GetDraftStateIcon(item), + FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontSize = 11, + Foreground = GetDraftStateIconBrush(item), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + Width = 14, + }; + Grid.SetColumn(stateIcon, 0); + grid.Children.Add(stateIcon); - if (wrap.Children.Count == 0) - wrap.Children.Add(CreateQueueSummaryPill("대기열", "0", "#F8FAFC", "#E2E8F0", "#475569")); + // Message text + var msgText = new TextBlock + { + Text = item.Text, + FontSize = 12, + Foreground = isFailed ? (TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#8E8E9A")) : primaryText, + TextTrimming = TextTrimming.CharacterEllipsis, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + }; + Grid.SetColumn(msgText, 1); + grid.Children.Add(msgText); - Grid.SetColumn(wrap, 0); - root.Children.Add(wrap); + // Right actions + var actions = new StackPanel + { + Orientation = Orientation.Horizontal, + VerticalAlignment = VerticalAlignment.Center, + }; + Grid.SetColumn(actions, 2); + grid.Children.Add(actions); - var toggle = CreateDraftQueueActionButton(isExpanded ? "간단히" : "상세 보기", ToggleDraftQueueExpanded); - toggle.Margin = new Thickness(10, 0, 0, 0); - Grid.SetColumn(toggle, 1); - root.Children.Add(toggle); + if (!isRunning) + { + // Kind chip (↪ 조정 style) + actions.Children.Add(CreateKindChip(item, secondaryText)); + } - return root; + if (!isRunning && !isFailed) + { + // Run now button + actions.Children.Add(CreateRowIconButton("\uE768", "지금 실행", () => QueueDraftForImmediateRun(item.Id))); + } + + if (!isRunning) + { + // Edit button + actions.Children.Add(CreateRowIconButton("\uE70F", "편집", () => PopDraftToEditor(item.Id))); + } + + // Delete + actions.Children.Add(CreateRowIconButton("\uE74D", isRunning ? "취소" : "삭제", () => RemoveDraftFromQueue(item.Id))); + + return row; } - private Border CreateQueueSummaryPill(string label, string value, string bgHex, string borderHex, string fgHex) + // Kind chip on the right — "↪ 조정" style + private Border CreateKindChip(DraftQueueItem item, Brush defaultForeground) { + var (kindIcon, kindLabel) = GetDraftKindChipContent(item); + var foreground = GetDraftKindChipColor(item); + return new Border { - Background = BrushFromHex(bgHex), - BorderBrush = BrushFromHex(borderHex), + BorderBrush = foreground, BorderThickness = new Thickness(1), - CornerRadius = new CornerRadius(999), - Padding = new Thickness(8, 3, 8, 3), - Margin = new Thickness(0, 0, 6, 0), + CornerRadius = new CornerRadius(5), + Padding = new Thickness(5, 2, 6, 2), + Margin = new Thickness(0, 0, 4, 0), + VerticalAlignment = VerticalAlignment.Center, Child = new StackPanel { Orientation = Orientation.Horizontal, @@ -372,169 +313,108 @@ public partial class ChatWindow { new TextBlock { - Text = label, + Text = kindIcon, + FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 10, - Foreground = BrushFromHex(fgHex), + Foreground = foreground, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 3, 0), }, new TextBlock { - Text = $" {value}", - FontSize = 10, - FontWeight = FontWeights.SemiBold, - Foreground = BrushFromHex(fgHex), - } + Text = kindLabel, + FontSize = 10.5, + Foreground = foreground, + VerticalAlignment = VerticalAlignment.Center, + }, } } }; } - private TextBlock CreateDraftQueueSectionLabel(string text) + // Row icon button — flat, no border + private Button CreateRowIconButton(string icon, string tooltip, Action onClick) { - return new TextBlock + var btn = new Button { - Text = text, - FontSize = 10.5, - FontWeight = FontWeights.SemiBold, - Margin = new Thickness(8, 0, 8, 6), - Foreground = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#64748B"), - }; - } - - private Border CreateDraftQueueCard(DraftQueueItem item) - { - var background = TryFindResource("ItemBackground") as Brush ?? BrushFromHex("#F7F7F8"); - var borderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E4E4E7"); - var primaryText = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#111827"); - var secondaryText = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#6B7280"); - var neutralSurface = BrushFromHex("#F5F6F8"); - var (kindIcon, kindForeground) = GetDraftKindVisual(item); - var (stateBackground, stateBorder, stateForeground) = GetDraftStateBadgeColors(item); - var (priorityBackground, priorityBorder, priorityForeground) = GetDraftPriorityBadgeColors(item.Priority); - - var container = new Border - { - Background = background, - BorderBrush = borderBrush, - BorderThickness = new Thickness(1), - CornerRadius = new CornerRadius(14), - Padding = new Thickness(12, 10, 12, 10), - Margin = new Thickness(0, 0, 0, 8), - }; - - var root = new Grid(); - root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); - root.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); - container.Child = root; - - var left = new StackPanel(); - Grid.SetColumn(left, 0); - root.Children.Add(left); - - var header = new StackPanel - { - Orientation = Orientation.Horizontal, - }; - header.Children.Add(new TextBlock - { - Text = kindIcon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), - FontSize = 11, - Foreground = kindForeground, - VerticalAlignment = VerticalAlignment.Center, - Margin = new Thickness(0, 0, 6, 0), - }); - header.Children.Add(CreateDraftQueueBadge(GetDraftKindLabel(item), BrushFromHex("#F8FAFC"), borderBrush, kindForeground)); - header.Children.Add(CreateDraftQueueBadge(GetDraftStateLabel(item), stateBackground, stateBorder, stateForeground)); - header.Children.Add(CreateDraftQueueBadge(GetDraftPriorityLabel(item.Priority), priorityBackground, priorityBorder, priorityForeground)); - left.Children.Add(header); - - left.Children.Add(new TextBlock - { - Text = item.Text, - FontSize = 12.5, - Foreground = primaryText, - Margin = new Thickness(0, 6, 0, 0), - TextWrapping = TextWrapping.Wrap, - TextTrimming = TextTrimming.CharacterEllipsis, - MaxWidth = 520, - }); - - var meta = $"{item.CreatedAt:HH:mm}"; - if (item.AttemptCount > 0) - meta += $" · 시도 {item.AttemptCount}"; - if (item.NextRetryAt.HasValue && item.NextRetryAt.Value > DateTime.Now) - meta += $" · 재시도 {item.NextRetryAt.Value:HH:mm:ss}"; - if (!string.IsNullOrWhiteSpace(item.LastError)) - meta += $" · {TruncateForStatus(item.LastError, 36)}"; - - left.Children.Add(new TextBlock - { - Text = meta, - FontSize = 10.5, - Foreground = secondaryText, - Margin = new Thickness(0, 6, 0, 0), - }); - - var actions = new StackPanel - { - Orientation = Orientation.Horizontal, - VerticalAlignment = VerticalAlignment.Top, - }; - Grid.SetColumn(actions, 1); - root.Children.Add(actions); - - if (!string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase)) - actions.Children.Add(CreateDraftQueueActionButton("실행", () => QueueDraftForImmediateRun(item.Id))); - - if (string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase) || - string.Equals(item.State, "completed", StringComparison.OrdinalIgnoreCase)) - { - actions.Children.Add(CreateDraftQueueActionButton("대기", () => ResetDraftInQueue(item.Id), neutralSurface)); - } - - actions.Children.Add(CreateDraftQueueActionButton("삭제", () => RemoveDraftFromQueue(item.Id), neutralSurface)); - return container; - } - - private Border CreateDraftQueueBadge(string text, Brush background, Brush borderBrush, Brush foreground) - { - return new Border - { - Background = background, - BorderBrush = borderBrush, - BorderThickness = new Thickness(1), - CornerRadius = new CornerRadius(999), - Padding = new Thickness(7, 2, 7, 2), - Margin = new Thickness(0, 0, 6, 0), - Child = new TextBlock + Content = new TextBlock { - Text = text, - FontSize = 10, - FontWeight = FontWeights.SemiBold, - Foreground = foreground, - } + Text = icon, + FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontSize = 11, + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center, + }, + Width = 24, + Height = 24, + Padding = new Thickness(0), + Background = Brushes.Transparent, + BorderThickness = new Thickness(0), + Foreground = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#8E8E9A"), + Cursor = Cursors.Hand, + ToolTip = tooltip, }; + btn.Click += (_, _) => onClick(); + return btn; } - private Button CreateDraftQueueActionButton(string label, Action onClick, Brush? background = null) + // Footer button (failed clear) + private Button CreateQueueFooterButton(string label, Action onClick) { var btn = new Button { Content = label, - Margin = new Thickness(6, 0, 0, 0), - Padding = new Thickness(10, 5, 10, 5), - MinWidth = 48, + Margin = new Thickness(0, 2, 0, 0), + Padding = new Thickness(10, 4, 10, 4), FontSize = 11, - Background = background ?? BrushFromHex("#EEF2FF"), - BorderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#D4D4D8"), + Background = Brushes.Transparent, + BorderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#3A3A4A"), BorderThickness = new Thickness(1), - Foreground = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#111827"), + Foreground = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#8E8E9A"), + HorizontalAlignment = HorizontalAlignment.Left, Cursor = Cursors.Hand, }; btn.Click += (_, _) => onClick(); return btn; } + // Pop queued draft back into the InputBox for editing + private void PopDraftToEditor(string draftId) + { + string? text = null; + lock (_convLock) + { + var session = ChatSession; + if (session != null) + { + var item = session.GetDraftQueueItems(_activeTab) + .FirstOrDefault(x => string.Equals(x.Id, draftId, StringComparison.OrdinalIgnoreCase)); + if (item != null) + { + text = item.Text; + if (session.RemoveDraft(_activeTab, draftId, _storage)) + _currentConversation = session.CurrentConversation ?? _currentConversation; + } + } + } + + if (InputBox != null && text != null) + { + InputBox.Text = text; + InputBox.CaretIndex = text.Length; + InputBox.Focus(); + UpdateInputBoxHeight(); + } + + RefreshDraftQueueUi(); + } + + // --- Icon-only button (kept for compatibility) --- + private Button CreateIconButton(string icon, string tooltip, Action onClick) + => CreateRowIconButton(icon, tooltip, onClick); + + // --- Ranking helpers --- + private static int GetDraftStateRank(DraftQueueItem item) => string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase) ? 0 : IsDraftBlocked(item) ? 1 @@ -550,52 +430,74 @@ public partial class ChatWindow _ => 2, }; - private static string GetDraftPriorityLabel(string? priority) - => priority?.ToLowerInvariant() switch + // --- State icon --- + + private static string GetDraftStateIcon(DraftQueueItem item) + { + if (IsDraftBlocked(item)) return "\uE9F5"; // 시계 + return item.State?.ToLowerInvariant() switch { - "now" => "지금", - "later" => "나중", - _ => "다음", + "running" => "\uE895", // 회전 (재생 아이콘) + "failed" => "\uE783", // 경고 + "completed" => "\uE73E", // 체크 + _ => "\uE76C", // 대기 점 }; + } + + private Brush GetDraftStateIconBrush(DraftQueueItem item) + { + if (IsDraftBlocked(item)) return BrushFromHex("#C2410C"); + return item.State?.ToLowerInvariant() switch + { + "running" => TryFindResource("AccentColor") as Brush ?? BrushFromHex("#5B8AF5"), + "failed" => BrushFromHex("#DC2626"), + "completed" => BrushFromHex("#16A34A"), + _ => TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#8E8E9A"), + }; + } + + // --- Kind chip --- + + private static (string Icon, string Label) GetDraftKindChipContent(DraftQueueItem item) + => item.Kind?.ToLowerInvariant() switch + { + "followup" => ("\uE8A5", "후속"), + "steering" => ("\uE7C3", "조정"), + "command" => ("\uE756", "명령"), + "direct" => ("\uE8A7", "직접"), + _ => ("\uE8BD", "메시지"), + }; + + private Brush GetDraftKindChipColor(DraftQueueItem item) + => item.Kind?.ToLowerInvariant() switch + { + "followup" => BrushFromHex("#0F766E"), + "steering" => BrushFromHex("#B45309"), + "command" => BrushFromHex("#7C3AED"), + "direct" => BrushFromHex("#2563EB"), + _ => TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#6B7280"), + }; + + // --- Legacy helpers (used by other partial classes) --- private static string GetDraftKindLabel(DraftQueueItem item) => item.Kind?.ToLowerInvariant() switch { - "followup" => "후속 작업", + "followup" => "후속", "steering" => "조정", - "command" => "명령", - "direct" => "직접 실행", + "command" => "명령", + "direct" => "직접", _ => "메시지", }; - private (string Icon, Brush Foreground) GetDraftKindVisual(DraftQueueItem item) - => item.Kind?.ToLowerInvariant() switch - { - "followup" => ("\uE8A5", BrushFromHex("#0F766E")), - "steering" => ("\uE7C3", BrushFromHex("#B45309")), - "command" => ("\uE756", BrushFromHex("#7C3AED")), - "direct" => ("\uE8A7", BrushFromHex("#2563EB")), - _ => ("\uE8BD", BrushFromHex("#475569")), - }; - private static string GetDraftStateLabel(DraftQueueItem item) => IsDraftBlocked(item) ? "재시도 대기" : item.State?.ToLowerInvariant() switch { - "running" => "실행 중", - "failed" => "실패", + "running" => "실행 중", + "failed" => "실패", "completed" => "완료", - _ => "대기", - }; - - private Brush GetDraftStateBrush(DraftQueueItem item) - => IsDraftBlocked(item) ? BrushFromHex("#B45309") - : item.State?.ToLowerInvariant() switch - { - "running" => BrushFromHex("#2563EB"), - "failed" => BrushFromHex("#DC2626"), - "completed" => BrushFromHex("#059669"), - _ => BrushFromHex("#7C3AED"), + _ => "대기", }; private (Brush Background, Brush Border, Brush Foreground) GetDraftStateBadgeColors(DraftQueueItem item) @@ -603,22 +505,17 @@ public partial class ChatWindow ? (BrushFromHex("#FFF7ED"), BrushFromHex("#FDBA74"), BrushFromHex("#C2410C")) : item.State?.ToLowerInvariant() switch { - "running" => (BrushFromHex("#EFF6FF"), BrushFromHex("#BFDBFE"), BrushFromHex("#1D4ED8")), - "failed" => (BrushFromHex("#FEF2F2"), BrushFromHex("#FECACA"), BrushFromHex("#991B1B")), + "running" => (BrushFromHex("#EFF6FF"), BrushFromHex("#BFDBFE"), BrushFromHex("#1D4ED8")), + "failed" => (BrushFromHex("#FEF2F2"), BrushFromHex("#FECACA"), BrushFromHex("#991B1B")), "completed" => (BrushFromHex("#ECFDF5"), BrushFromHex("#BBF7D0"), BrushFromHex("#166534")), - _ => (BrushFromHex("#F5F3FF"), BrushFromHex("#DDD6FE"), BrushFromHex("#6D28D9")), + _ => (BrushFromHex("#F5F3FF"), BrushFromHex("#DDD6FE"), BrushFromHex("#6D28D9")), }; - private static (Brush Background, Brush Border, Brush Foreground) GetDraftPriorityBadgeColors(string? priority) - => priority?.ToLowerInvariant() switch - { - "now" => (BrushFromHex("#EEF2FF"), BrushFromHex("#C7D2FE"), BrushFromHex("#3730A3")), - "later" => (BrushFromHex("#F8FAFC"), BrushFromHex("#E2E8F0"), BrushFromHex("#475569")), - _ => (BrushFromHex("#FEF3C7"), BrushFromHex("#FDE68A"), BrushFromHex("#92400E")), - }; - private static bool IsDraftBlocked(DraftQueueItem item) => string.Equals(item.State, "queued", StringComparison.OrdinalIgnoreCase) && item.NextRetryAt.HasValue && item.NextRetryAt.Value > DateTime.Now; + + private Button CreateDraftQueueActionButton(string label, Action onClick, Brush? background = null) + => CreateQueueFooterButton(label, onClick); } diff --git a/src/AxCopilot/Views/ChatWindow.ContextUsagePresentation.cs b/src/AxCopilot/Views/ChatWindow.ContextUsagePresentation.cs index 0221e18..adb3fad 100644 --- a/src/AxCopilot/Views/ChatWindow.ContextUsagePresentation.cs +++ b/src/AxCopilot/Views/ChatWindow.ContextUsagePresentation.cs @@ -10,7 +10,7 @@ public partial class ChatWindow { if (TokenUsageCard == null || TokenUsageArc == null || TokenUsagePercentText == null || TokenUsageSummaryText == null || TokenUsageHintText == null - || TokenUsageThresholdMarker == null || CompactNowLabel == null) + || CompactNowLabel == null) return; var showContextUsage = _activeTab is "Cowork" or "Code"; @@ -79,7 +79,6 @@ public partial class ChatWindow } TokenUsageArc.Stroke = progressBrush; - TokenUsageThresholdMarker.Fill = progressBrush; var percentText = $"{Math.Round(usageRatio * 100):0}%"; TokenUsagePercentText.Text = percentText; TokenUsageSummaryText.Text = $"컨텍스트 {percentText}"; @@ -101,8 +100,7 @@ public partial class ChatWindow TokenUsageCard.ToolTip = null; - UpdateCircularUsageArc(TokenUsageArc, usageRatio, 14, 14, 11); - PositionThresholdMarker(TokenUsageThresholdMarker, triggerRatio, 14, 14, 11, 2.5); + UpdateCircularUsageArc(TokenUsageArc, usageRatio, 15, 15, 11); } private void TokenUsageCard_MouseEnter(object sender, MouseEventArgs e) diff --git a/src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs b/src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs index f7dc5dc..4385ca9 100644 --- a/src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs +++ b/src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs @@ -464,15 +464,16 @@ public partial class ChatWindow return; } - if (_isStreaming) + if (_streamingTabs.Contains(_activeTab)) { - _streamCts?.Cancel(); + if (_tabStreamCts.TryGetValue(_activeTab, out var convListCts)) convListCts.Cancel(); _cursorTimer.Stop(); _typingTimer.Stop(); _elapsedTimer.Stop(); _activeStreamText = null; _elapsedLabel = null; - _isStreaming = false; + _streamingTabs.Remove(_activeTab); + if (_tabStreamCts.Remove(_activeTab, out var removedCts)) removedCts.Dispose(); } var conv = _storage.Load(item.Id); diff --git a/src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs b/src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs index b2c57fd..b166252 100644 --- a/src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs +++ b/src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs @@ -37,13 +37,14 @@ public partial class ChatWindow var bubble = new Border { - Background = userBubbleBg, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(12), Padding = new Thickness(11, 7, 11, 7), HorizontalAlignment = HorizontalAlignment.Right, }; + // DynamicResource 방식으로 바인딩 — 테마 전환 시 기존 버블도 자동 업데이트 + bubble.SetResourceReference(Border.BackgroundProperty, "HintBackground"); if (string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase) || string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase)) @@ -120,6 +121,7 @@ public partial class ChatWindow if (animate) ApplyMessageEntryAnimation(wrapper); + if (message?.MsgId != null) _elementCache[$"m_{message.MsgId}"] = wrapper; MessagePanel.Children.Add(wrapper); return; } @@ -129,6 +131,7 @@ public partial class ChatWindow var compactCard = CreateCompactionMetaCard(message, primaryText, secondaryText, hintBg, borderBrush, accentBrush); if (animate) ApplyMessageEntryAnimation(compactCard); + if (message.MsgId != null) _elementCache[$"m_{message.MsgId}"] = compactCard; MessagePanel.Children.Add(compactCard); return; } @@ -146,14 +149,7 @@ public partial class ChatWindow var (agentName, _, _) = GetAgentIdentity(); var header = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(2, 0, 0, 1.5) }; - header.Children.Add(new TextBlock - { - Text = "\uE945", - FontFamily = new FontFamily("Segoe MDL2 Assets"), - FontSize = 10, - Foreground = secondaryText, - VerticalAlignment = VerticalAlignment.Center, - }); + header.Children.Add(CreateMiniLauncherIcon(pixelSize: 4.0)); header.Children.Add(new TextBlock { Text = agentName, @@ -167,12 +163,13 @@ public partial class ChatWindow var contentCard = new Border { - Background = assistantBubbleBg, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(12), Padding = new Thickness(11, 8, 11, 8), }; + // DynamicResource 방식으로 바인딩 — 테마 전환 시 기존 버블도 자동 업데이트 + contentCard.SetResourceReference(Border.BackgroundProperty, "ItemBackground"); var contentStack = new StackPanel(); var app = System.Windows.Application.Current as App; @@ -337,6 +334,7 @@ public partial class ChatWindow ShowMessageContextMenu(aiContent, "assistant"); }; + if (message?.MsgId != null) _elementCache[$"m_{message.MsgId}"] = container; MessagePanel.Children.Add(container); } } diff --git a/src/AxCopilot/Views/ChatWindow.MessageInteractions.cs b/src/AxCopilot/Views/ChatWindow.MessageInteractions.cs index 6ec98a5..1bd1bfc 100644 --- a/src/AxCopilot/Views/ChatWindow.MessageInteractions.cs +++ b/src/AxCopilot/Views/ChatWindow.MessageInteractions.cs @@ -11,9 +11,57 @@ namespace AxCopilot.Views; public partial class ChatWindow { + /// 런처 아이콘과 동일한 2×2 컬러 픽셀 다이아몬드를 생성합니다. + internal static FrameworkElement CreateMiniLauncherIcon(double pixelSize = 4.0) + { + const double gap = 0.75; + var total = pixelSize * 2 + gap; + + var canvas = new System.Windows.Controls.Canvas + { + Width = total, + Height = total, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + RenderTransformOrigin = new Point(0.5, 0.5), + RenderTransform = new RotateTransform(45), + }; + + void AddPixel(double left, double top, string colorHex) + { + var rect = new System.Windows.Shapes.Rectangle + { + Width = pixelSize, + Height = pixelSize, + RadiusX = 1.0, + RadiusY = 1.0, + Fill = new SolidColorBrush((Color)System.Windows.Media.ColorConverter.ConvertFromString(colorHex)), + }; + System.Windows.Controls.Canvas.SetLeft(rect, left); + System.Windows.Controls.Canvas.SetTop(rect, top); + canvas.Children.Add(rect); + } + + AddPixel(0, 0, "#4488FF"); + AddPixel(pixelSize + gap, 0, "#44DD66"); + AddPixel(0, pixelSize + gap, "#44DD66"); + AddPixel(pixelSize + gap, pixelSize + gap, "#FF4466"); + + // Wrap in a container so the rotated canvas doesn't disturb layout + var host = new Grid + { + Width = total, + Height = total, + VerticalAlignment = VerticalAlignment.Center, + }; + host.Children.Add(canvas); + return host; + } + /// 좋아요/싫어요 피드백 버튼을 생성합니다. private Button CreateFeedbackButton( string iconGlyph, + string activeGlyph, string tooltip, Brush normalColor, Brush activeColor, @@ -54,12 +102,18 @@ public partial class ChatWindow BorderThickness = new Thickness(0), Cursor = Cursors.Hand, Padding = new Thickness(0), - ToolTip = tooltip + ToolTip = tooltip, + // Remove WPF default hover chrome — visual states handled entirely by chip/icon + Template = (ControlTemplate)System.Windows.Markup.XamlReader.Parse( + "" + + "" + + ""), }; void RefreshVisual() { var active = isActive(); + icon.Text = active ? activeGlyph : iconGlyph; icon.Foreground = active ? activeColor : normalColor; chip.Background = active ? activeBackground : Brushes.Transparent; chip.BorderBrush = active ? activeColor : Brushes.Transparent; @@ -67,15 +121,22 @@ public partial class ChatWindow RefreshVisual(); + var hoverBackground = new SolidColorBrush(Color.FromArgb(18, 128, 128, 128)); btn.MouseEnter += (_, _) => { if (!isActive()) + { icon.Foreground = hoverBrush; + chip.Background = hoverBackground; + } }; btn.MouseLeave += (_, _) => { if (!isActive()) + { icon.Foreground = normalColor; + chip.Background = Brushes.Transparent; + } }; btn.Click += (_, _) => { @@ -140,7 +201,8 @@ public partial class ChatWindow } var likeBtn = CreateFeedbackButton( - "\uE8E1", + "\uE8E1", // 좋아요 아웃라인 + "\uEB51", // 좋아요 채움 (활성) "좋아요", btnColor, new SolidColorBrush(Color.FromRgb(0x38, 0xA1, 0x69)), @@ -154,7 +216,8 @@ public partial class ChatWindow }); var dislikeBtn = CreateFeedbackButton( - "\uE8E0", + "\uE8E0", // 싫어요 아웃라인 + "\uEB50", // 싫어요 채움 (활성) "싫어요", btnColor, new SolidColorBrush(Color.FromRgb(0xE5, 0x3E, 0x3E)), @@ -416,6 +479,13 @@ public partial class ChatWindow } } + // 수정된 메시지(및 이후 잘린 메시지) 캐시 무효화 + lock (_convLock) + { + for (var i = userMsgIdx; i < conv.Messages.Count; i++) + _elementCache.Remove($"m_{conv.Messages[i].MsgId}"); + } + RenderMessages(preserveViewport: true); AutoScrollIfNeeded(); diff --git a/src/AxCopilot/Views/ChatWindow.PlanApprovalPresentation.cs b/src/AxCopilot/Views/ChatWindow.PlanApprovalPresentation.cs index efa3179..e11924e 100644 --- a/src/AxCopilot/Views/ChatWindow.PlanApprovalPresentation.cs +++ b/src/AxCopilot/Views/ChatWindow.PlanApprovalPresentation.cs @@ -46,6 +46,8 @@ public partial class ChatWindow } else if (!string.Equals(result, "취소", StringComparison.OrdinalIgnoreCase) && !string.Equals(result, "확인", StringComparison.OrdinalIgnoreCase) + && !string.Equals(result, "건너뛰기", StringComparison.OrdinalIgnoreCase) + && !string.Equals(result, "중단", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(result)) { agentDecision = $"수정 요청: {result.Trim()}"; diff --git a/src/AxCopilot/Views/ChatWindow.PreviewPresentation.cs b/src/AxCopilot/Views/ChatWindow.PreviewPresentation.cs index 1d111a6..e5d1bbf 100644 --- a/src/AxCopilot/Views/ChatWindow.PreviewPresentation.cs +++ b/src/AxCopilot/Views/ChatWindow.PreviewPresentation.cs @@ -56,7 +56,7 @@ public partial class ChatWindow PreviewPanel.Visibility = Visibility.Visible; PreviewSplitter.Visibility = Visibility.Visible; - BtnPreviewToggle.Visibility = Visibility.Visible; + if (PreviewDot != null) PreviewDot.Fill = System.Windows.Media.Brushes.LimeGreen; RebuildPreviewTabs(); LoadPreviewContent(filePath); @@ -434,6 +434,7 @@ public partial class ChatWindow { _previewTabs.Clear(); _activePreviewTab = null; + if (PreviewDot != null) PreviewDot.Fill = System.Windows.Media.Brushes.Gray; if (PreviewHeaderTitle != null) PreviewHeaderTitle.Text = "미리보기"; if (PreviewHeaderSubtitle != null) PreviewHeaderSubtitle.Text = "선택한 파일이 여기에 표시됩니다"; if (PreviewHeaderMeta != null) PreviewHeaderMeta.Text = "파일 메타"; @@ -466,7 +467,7 @@ public partial class ChatWindow private void BtnClosePreview_Click(object sender, RoutedEventArgs e) { HidePreviewPanel(); - BtnPreviewToggle.Visibility = Visibility.Collapsed; + // BtnPreviewToggle은 항상 표시 — 패널만 닫힘 } private void BtnPreviewToggle_Click(object sender, RoutedEventArgs e) @@ -477,6 +478,7 @@ public partial class ChatWindow PreviewSplitter.Visibility = Visibility.Collapsed; PreviewColumn.Width = new GridLength(0); SplitterColumn.Width = new GridLength(0); + if (PreviewDot != null) PreviewDot.Fill = System.Windows.Media.Brushes.Gray; } else if (_previewTabs.Count > 0) { @@ -484,6 +486,7 @@ public partial class ChatWindow PreviewSplitter.Visibility = Visibility.Visible; PreviewColumn.Width = new GridLength(420); SplitterColumn.Width = new GridLength(5); + if (PreviewDot != null) PreviewDot.Fill = System.Windows.Media.Brushes.LimeGreen; RebuildPreviewTabs(); if (_activePreviewTab != null) LoadPreviewContent(_activePreviewTab); diff --git a/src/AxCopilot/Views/ChatWindow.StatusPresentation.cs b/src/AxCopilot/Views/ChatWindow.StatusPresentation.cs index e781e50..f7370bc 100644 --- a/src/AxCopilot/Views/ChatWindow.StatusPresentation.cs +++ b/src/AxCopilot/Views/ChatWindow.StatusPresentation.cs @@ -161,8 +161,8 @@ public partial class ChatWindow private void RefreshStatusTokenAggregate() { - var promptTokens = Math.Max(0, _agentCumulativeInputTokens); - var completionTokens = Math.Max(0, _agentCumulativeOutputTokens); + var promptTokens = (int)Math.Max(0, _tabCumulativeInputTokens.GetValueOrDefault(_activeTab)); + var completionTokens = (int)Math.Max(0, _tabCumulativeOutputTokens.GetValueOrDefault(_activeTab)); if (promptTokens == 0 && completionTokens == 0) { diff --git a/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs b/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs index 252e9a8..5bea8ff 100644 --- a/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs +++ b/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Windows; @@ -32,9 +32,7 @@ public partial class ChatWindow if (conversation?.ShowExecutionHistory ?? true) return events; - return events - .Where(ShouldShowCollapsedProgressEvent) - .ToList(); + return events.Where(ShouldShowCollapsedProgressEvent).ToList(); } private static bool ShouldShowCollapsedProgressEvent(ChatExecutionEvent executionEvent) @@ -53,9 +51,19 @@ public partial class ChatWindow return true; } + // 문서 생성 ToolResult 성공 시 항상 표시 (미리보기 카드용) + if (restoredEvent.Type == AgentEventType.ToolResult && restoredEvent.Success + && IsDocumentCreationTool(restoredEvent.ToolName)) + return true; + return IsProcessFeedEvent(restoredEvent); } + private static bool IsDocumentCreationTool(string? toolName) => + toolName is "html_create" or "docx_create" or "excel_create" or "xlsx_create" + or "csv_create" or "markdown_create" or "md_create" or "script_create" + or "pptx_create"; + private List<(DateTime Timestamp, int Order, Action Render)> BuildTimelineRenderActions( IReadOnlyCollection visibleMessages, IReadOnlyCollection visibleEvents) @@ -63,22 +71,52 @@ public partial class ChatWindow var timeline = new List<(DateTime Timestamp, int Order, Action Render)>(visibleMessages.Count + visibleEvents.Count); foreach (var msg in visibleMessages) - timeline.Add((msg.Timestamp, 0, () => AddMessageBubble(msg.Role, msg.Content, animate: false, message: msg))); + { + var capturedMsg = msg; + var cacheKey = $"m_{msg.MsgId}"; + timeline.Add((msg.Timestamp, 0, () => + { + // 캐시된 버블이 있으면 재생성 없이 재사용 (O(1) Add vs 전체 재생성) + if (_elementCache.TryGetValue(cacheKey, out var cached)) + MessagePanel.Children.Add(cached); + else + AddMessageBubble(capturedMsg.Role, capturedMsg.Content, animate: false, message: capturedMsg); + })); + } + + // 현재 실행 중인 run의 process feed 이벤트를 통합 카드로 대체 (히스토리 접힘 모드) + var showFullHistory = _currentConversation?.ShowExecutionHistory ?? true; + var activeRunId = _isStreaming ? (_appState.AgentRun.RunId ?? "") : ""; foreach (var executionEvent in visibleEvents) { + // 스트리밍 중이고 히스토리 접힘 상태일 때, 현재 run의 process feed 이벤트는 통합 카드에서 표시 + if (!showFullHistory && _isStreaming + && !string.IsNullOrEmpty(activeRunId) + && string.Equals(executionEvent.RunId, activeRunId, StringComparison.Ordinal)) + { + var restoredCheck = ToAgentEvent(executionEvent); + if (IsProcessFeedEvent(restoredCheck)) + continue; // 통합 카드로 대체 — 개별 pill 스킵 + } + var restoredEvent = ToAgentEvent(executionEvent); timeline.Add((executionEvent.Timestamp, 1, () => AddAgentEventBanner(restoredEvent))); } + // 스트리밍 중 + 히스토리 접힘: 통합 진행 카드 삽입 (개별 pill 대체) + if (!showFullHistory && _isStreaming && _currentRunProgressSteps.Count > 0) + { + var capturedSteps = _currentRunProgressSteps.ToList(); + var cardTimestamp = capturedSteps[^1].Timestamp; + timeline.Add((cardTimestamp, 1, () => AddLiveRunProgressCard(capturedSteps))); + } + var liveProgressHint = GetLiveAgentProgressHint(); if (liveProgressHint != null) timeline.Add((liveProgressHint.Timestamp, 2, () => AddAgentEventBanner(liveProgressHint))); - return timeline - .OrderBy(x => x.Timestamp) - .ThenBy(x => x.Order) - .ToList(); + return timeline.OrderBy(x => x.Timestamp).ThenBy(x => x.Order).ToList(); } private Border CreateTimelineLoadMoreCard(int hiddenCount) @@ -198,11 +236,7 @@ public partial class ChatWindow }; var stack = new StackPanel(); - var header = new StackPanel - { - Orientation = Orientation.Horizontal, - Margin = new Thickness(0, 0, 0, 6), - }; + var header = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 6) }; header.Children.Add(new TextBlock { Text = icon, @@ -222,12 +256,8 @@ public partial class ChatWindow }); stack.Children.Add(header); - var lines = (message.Content ?? "") - .Replace("\r\n", "\n") - .Split('\n', StringSplitOptions.RemoveEmptyEntries) - .Select(line => line.Trim()) - .Where(line => !string.IsNullOrWhiteSpace(line)) - .ToList(); + var lines = (message.Content ?? "").Replace("\r\n", "\n").Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()).Where(line => !string.IsNullOrWhiteSpace(line)).ToList(); foreach (var line in lines) { diff --git a/src/AxCopilot/Views/ChatWindow.xaml b/src/AxCopilot/Views/ChatWindow.xaml index 835f9c5..4b3192a 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml +++ b/src/AxCopilot/Views/ChatWindow.xaml @@ -1055,10 +1055,10 @@