에이전트 진행 표시 구조를 claude-code식 row 기반으로 재정리
Some checks are pending
Release Gate / gate (push) Waiting to run

- AgentTranscriptDisplayCatalog를 row presentation 중심으로 재구성해 thinking/waiting/compact/tool activity/permission/tool result/status를 타입별로 분리함
- PermissionRequestPresentationCatalog와 ToolResultPresentationCatalog를 정리해 권한 요청과 결과 상태를 행위/상태 기준으로 더 명확하게 표현함
- ChatWindow.AgentEventRendering에서 process feed 계열 이벤트를 GroupKey 기준으로 병합해 append 수를 줄이고 진행 흐름이 기본 transcript에 남도록 조정함
- FooterPresentation에서 Cowork/Chat 프리셋 안내 카드가 execution event 이후 자동으로 숨겨지도록 하고 입력 워터마크와 footer 기본 문구를 정리함
- render_messages 성능 로그에 processFeed append/merge 수치와 rowKindCounts를 추가해 %APPDATA%\\AxCopilot\\perf 기준 실검증이 가능하도록 함
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 기준 경고 0 / 오류 0 확인
This commit is contained in:
2026-04-09 14:49:53 +09:00
parent 33c1db4dae
commit 227f5ab0d3
9 changed files with 867 additions and 548 deletions

View File

@@ -7,6 +7,11 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
개발 참고: Claw Code 동등성 작업 추적 문서 개발 참고: Claw Code 동등성 작업 추적 문서
`docs/claw-code-parity-plan.md` `docs/claw-code-parity-plan.md`
- 업데이트: 2026-04-09 13:05 (KST)
- AX Agent transcript 표시를 `claude-code`식 row 타입 기반으로 재정리했습니다. `thinking / waiting / compact / tool activity / permission / tool result / status`를 별도 row 의미로 정규화하고, process feed는 같은 활동 그룹이면 이어붙여 append 수를 줄이도록 바꿨습니다.
- 권한 요청과 도구 결과 카탈로그를 다시 정리해 Cowork/Code 진행 중 무엇을 하는지, 어떤 결과 상태인지가 기본 transcript에서 더 읽히도록 맞췄습니다. 동시에 render 성능 로그에 row kind 분포와 process feed append/merge 수치를 남겨 이후 실검증 때 구조 개선 효과를 바로 판단할 수 있게 했습니다.
- Cowork/Chat 하단 프리셋 안내 카드는 실제 메시지뿐 아니라 execution event가 생긴 뒤에도 자동으로 숨겨 결과/진행 화면을 가리지 않도록 조정했고, 입력 워터마크와 footer 기본 문구도 정상 한국어 기준으로 다시 정리했습니다.
- 업데이트: 2026-04-09 01:33 (KST) - 업데이트: 2026-04-09 01:33 (KST)
- AX Agent transcript 호스트를 `ObservableCollection<UIElement>` 직접 주입 구조에서 `TranscriptVisualItem + TranscriptVisualHost` 기반의 지연 materialization 구조로 올렸습니다. `MessageList`는 virtualization 설정을 유지한 채 필요한 시점에만 실제 버블 UI를 생성할 수 있는 기반을 갖습니다. - AX Agent transcript 호스트를 `ObservableCollection<UIElement>` 직접 주입 구조에서 `TranscriptVisualItem + TranscriptVisualHost` 기반의 지연 materialization 구조로 올렸습니다. `MessageList`는 virtualization 설정을 유지한 채 필요한 시점에만 실제 버블 UI를 생성할 수 있는 기반을 갖습니다.
- `StreamingToolExecutionCoordinator``IToolExecutionCoordinator` 인터페이스 뒤로 숨겨 Cowork/Code tool streaming executor를 루프 구현에서 구조적으로 분리했습니다. 이후 executor 교체, 테스트 더블 주입, 모델별 executor 분기를 더 쉽게 할 수 있는 상태입니다. - `StreamingToolExecutionCoordinator``IToolExecutionCoordinator` 인터페이스 뒤로 숨겨 Cowork/Code tool streaming executor를 루프 구현에서 구조적으로 분리했습니다. 이후 executor 교체, 테스트 더블 주입, 모델별 executor 분기를 더 쉽게 할 수 있는 상태입니다.
@@ -1548,3 +1553,9 @@ MIT License
- [MultiReadTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/MultiReadTool.cs)는 한 번에 읽을 수 있는 최대 파일 수를 20개에서 8개로 낮춰 초기 과탐색 토큰 낭비를 줄이도록 했습니다. - [MultiReadTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/MultiReadTool.cs)는 한 번에 읽을 수 있는 최대 파일 수를 20개에서 8개로 낮춰 초기 과탐색 토큰 낭비를 줄이도록 했습니다.
- [AgentLoopExplorationPolicy.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs)와 [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)에 탐색 범위 분류기와 broad-scan corrective hint를 추가해, 좁은 질문에서 반복적인 `folder_map`/대량 `multi_read`가 나오면 관련 파일만 다시 고르도록 교정합니다. - [AgentLoopExplorationPolicy.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs)와 [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)에 탐색 범위 분류기와 broad-scan corrective hint를 추가해, 좁은 질문에서 반복적인 `folder_map`/대량 `multi_read`가 나오면 관련 파일만 다시 고르도록 교정합니다.
- [AgentPerformanceLogService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AgentPerformanceLogService.cs)는 `%APPDATA%\\AxCopilot\\perf``exploration_breadth` 로그를 남겨 `folder_map` 호출 수, 총 읽은 파일 수, broad scan 여부를 실사용 기준으로 확인할 수 있게 했습니다. - [AgentPerformanceLogService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AgentPerformanceLogService.cs)는 `%APPDATA%\\AxCopilot\\perf``exploration_breadth` 로그를 남겨 `folder_map` 호출 수, 총 읽은 파일 수, broad scan 여부를 실사용 기준으로 확인할 수 있게 했습니다.
- 업데이트: 2026-04-09 11:12 (KST)
- `claude-code``Messages.tsx`, `MessageRow.tsx`, `GroupedToolUseContent.tsx`, `UserToolResultMessage`, `PermissionRequest` 구조를 다시 대조해 AX transcript 표시 계약을 row 중심으로 정리했습니다.
- [AgentTranscriptDisplayCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs)에 `TranscriptRowKind``AgentTranscriptRowPresentation`을 도입해 thinking, waiting, compact, tool activity, permission, tool result, status를 한 번에 정규화하도록 바꿨습니다.
- [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs)는 이 row 메타를 사용해 고빈도 process feed를 같은 group key 기준으로 교체 렌더하도록 바꿨고, 연속 읽기/검색/단계 이벤트를 더 적은 row 수로 보여주게 했습니다.
- [ToolResultPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs)와 [PermissionRequestPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs)를 정리해 실패/거부/취소/승인 필요와 권한 행위를 유형별 카드 메타로 다시 맞췄습니다.
- [ChatWindow.TranscriptHost.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptHost.cs), [ChatWindow.TranscriptRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptRendering.cs)는 grouped process feed append/merge 수를 추적하도록 바꿔, `claude-code`식 activity grouping이 실제 렌더 수를 얼마나 줄였는지 성능 로그로 확인할 수 있게 했습니다.

View File

@@ -1,506 +1,474 @@
# AX Copilot - 개발 문서 # AX Copilot - 媛쒕컻 臾몄꽌
> 최종 업데이트: 2026-04-09 · 버전 0.7.3
## claude-code식 transcript 표시 구조 정리
- 업데이트: 2026-04-09 13:05 (KST)
- AgentTranscriptDisplayCatalog를 row presentation 중심으로 재구성해 thinking / waiting / compact / tool activity / permission / tool result / status를 개별 transcript row 의미로 다룰 수 있게 정리했습니다.
- ChatWindow.AgentEventRendering은 process feed 계열 이벤트를 같은 GroupKey 단위로 병합해, 긴 Cowork/Code 실행 중 append 수를 줄이면서도 주요 활동 흐름이 기본 transcript에 남도록 조정했습니다.
- PermissionRequestPresentationCatalog, ToolResultPresentationCatalog를 다시 정리해 권한 요청과 도구 결과를 행위/상태 기준으로 구분하고, transcript 렌더와 popup이 같은 메타를 공유하도록 맞췄습니다.
- ChatWindow.FooterPresentation은 execution event가 생긴 뒤에는 프리셋 안내 카드를 자동으로 숨기도록 바꿔 결과/진행 화면을 덮지 않게 했고, Cowork/Code 입력 워터마크와 footer 기본 문구도 정상 한국어 기준으로 다시 정리했습니다.
- render_messages 성능 로그에는 processFeedAppends, processFeedMerges, rowKindCounts를 함께 남겨 %APPDATA%\\AxCopilot\\perf 기준으로 transcript grouping 효과를 수치로 비교할 수 있게 했습니다.
## 1. ?꾨줈?앺듃 媛쒖슂
AX Copilot?€ Windows???앹궛???곗쿂 + AI ?먯씠?꾪듃 ?곗뒪?ы넲 ?깆엯?덈떎.
- **?곗쿂**: Alfred/Raycast ?ㅽ??쇱쓽 ?쇱? 寃€?? 紐낅졊 ?ㅽ뻾, ?꾩젽
- **?먯씠?꾪듃**: LLM 湲곕컲 ?€?뷀삎 肄붾뱶/臾몄꽌 ?묒뾽 ?먮룞??(?꾧뎄 ?몄텧 猷⑦봽)
- **??諛?*: ?쒖뒪??由ъ냼?? ?대┰蹂대뱶, ?ㅽ겕由곗꺑 ??鍮좊Ⅸ ?묎렐
--- ---
## 1. 프로젝트 개요 ## 2. 湲곗닠 ?ㅽ깮
AX Copilot은 Windows용 생산성 런처 + AI 에이전트 데스크톱 앱입니다. | ??ぉ | 媛?|
- **런처**: Alfred/Raycast 스타일의 퍼지 검색, 명령 실행, 위젯
- **에이전트**: LLM 기반 대화형 코드/문서 작업 자동화 (도구 호출 루프)
- **독 바**: 시스템 리소스, 클립보드, 스크린샷 등 빠른 접근
---
## 2. 기술 스택
| 항목 | 값 |
|------|-----| |------|-----|
| 프레임워크 | .NET 8 (net8.0-windows10.0.17763.0) | | ?꾨젅?꾩썙??| .NET 8 (net8.0-windows10.0.17763.0) |
| UI | WPF + Windows Forms (하이브리드) | | UI | WPF + Windows Forms (?섏씠釉뚮━?? |
| 언어 | C# 12 | | ?몄뼱 | C# 12 |
| 패턴 | MVVM, 이벤트 기반, 싱글톤 서비스 | | ?⑦꽩 | MVVM, ?대깽??湲곕컲, ?깃????쒕퉬??|
| 테스트 | xUnit 2.9 + FluentAssertions 6.12 | | ?뚯뒪??| xUnit 2.9 + FluentAssertions 6.12 |
| 빌드 | dotnet CLI, PublishSingleFile | | 鍮뚮뱶 | dotnet CLI, PublishSingleFile |
### 주요 NuGet 패키지 ### 二쇱슂 NuGet ?⑦궎吏€
| 패키지 | 용도 | | ?⑦궎吏€ | ?⑸룄 |
|--------|------| |--------|------|
| DocumentFormat.OpenXml 3.2.0 | DOCX/XLSX/PPTX 생성 | | DocumentFormat.OpenXml 3.2.0 | DOCX/XLSX/PPTX ?앹꽦 |
| Markdig 0.37.0 | Markdown HTML 렌더링 | | Markdig 0.37.0 | Markdown ??HTML ?뚮뜑留?|
| Microsoft.Data.Sqlite 8.0 | SQLite (대화 저장소) | | Microsoft.Data.Sqlite 8.0 | SQLite (?€???€?μ냼) |
| Microsoft.Web.WebView2 | HTML 미리보기, 가이드 뷰어 | | Microsoft.Web.WebView2 | HTML 誘몃━蹂닿린, 媛€?대뱶 酉곗뼱 |
| QRCoder 1.6.0 | QR 코드 생성 | | QRCoder 1.6.0 | QR 肄붾뱶 ?앹꽦 |
| System.Security.Cryptography.ProtectedData | DPAPI 암호화 | | System.Security.Cryptography.ProtectedData | DPAPI ?뷀샇??|
| UglyToad.PdfPig | PDF 읽기 | | UglyToad.PdfPig | PDF ?쎄린 |
--- ---
## 3. 솔루션 구조 ## 3. ?붾(??援ъ“
``` ```
src/ src/
├── AxCopilot/ # 메인 WPF (v0.7.3) ?쒋??€ AxCopilot/ # 硫붿씤 WPF ??(v0.7.3)
│ ├── Assets/ # 아이콘, 프리셋 JSON, 암호화된 가이드, 마스코트 ?? ?쒋??€ Assets/ # ?꾩씠肄? ?꾨━??JSON, ?뷀샇?붾맂 媛€?대뱶, 留덉뒪肄뷀듃
│ ├── Core/ # FuzzyEngine, CommandResolver, InputListener, PluginHost ?? ?쒋??€ Core/ # FuzzyEngine, CommandResolver, InputListener, PluginHost
│ ├── Handlers/ # 136개 빌트인 명령 핸들러 ?? ?쒋??€ Handlers/ # 136媛?鍮뚰듃??紐낅졊 ?몃뱾???? ?쒋??€ Models/ # AppSettings, ChatModels, McpSettings
│ ├── Models/ # AppSettings, ChatModels, McpSettings ?? ?쒋??€ Security/ # AntiTamper (?붾쾭嫄??붿뺨?뚯씪???먯?)
│ ├── Security/ # AntiTamper (디버거/디컴파일러 탐지) ?? ?쒋??€ Services/ # 60媛??쒕퉬???? ?? ?붴??€ Agent/ # ?먯씠?꾪듃 猷⑦봽 + 114媛??꾧뎄
│ ├── Services/ # 60개 서비스 ?? ?쒋??€ Themes/ # 9媛??뚮쭏 (Dark, Light, OLED, Nord, Monokai ??
│ │ └── Agent/ # 에이전트 루프 + 114개 도구 ?? ?쒋??€ ViewModels/ # LauncherViewModel, SettingsViewModel, StatisticsViewModel
│ ├── Themes/ # 9개 테마 (Dark, Light, OLED, Nord, Monokai 등) ?? ?붴??€ Views/ # 30媛?XAML ?덈룄???쒋??€ AxCopilot.SDK/ # ?뚮윭洹몄씤 SDK (IActionHandler ?명꽣?섏씠??
│ ├── ViewModels/ # LauncherViewModel, SettingsViewModel, StatisticsViewModel ?쒋??€ AxCopilot.Installer/ # Windows Forms ?ㅼ튂 ?꾨줈洹몃옩 (.NET Framework 4.8)
│ └── Views/ # 30개 XAML 윈도우 ?쒋??€ AxCopilot.Tests/ # xUnit ?⑥쐞/?듯빀 ?뚯뒪???붴??€ AxKeyEncryptor/ # API ??DPAPI ?뷀샇???좏떥由ы떚
├── AxCopilot.SDK/ # 플러그인 SDK (IActionHandler 인터페이스)
├── AxCopilot.Installer/ # Windows Forms 설치 프로그램 (.NET Framework 4.8)
├── AxCopilot.Tests/ # xUnit 단위/통합 테스트
└── AxKeyEncryptor/ # API 키 DPAPI 암호화 유틸리티
``` ```
--- ---
## 4. 앱 시작 흐름 (App.xaml.cs) ## 4. ???쒖옉 ?먮쫫 (App.xaml.cs)
``` ```
OnStartup() OnStartup()
├─ AntiTamper 디버거 감지 (Release 빌드) ?쒋? AntiTamper ?붾쾭嫄?媛먯? (Release 鍮뚮뱶)
├─ 단일 인스턴스 뮤텍스 확인 ?쒋? ?⑥씪 ?몄뒪?댁뒪 裕ㅽ뀓???뺤씤
├─ SettingsService 초기화 + 설정 로드 ?쒋? SettingsService 珥덇린??+ ?ㅼ젙 濡쒕뱶
├─ ChatStorageService 보관 정책 실행 (만료 대화 정리) ?쒋? ChatStorageService 蹂닿? ?뺤콉 ?ㅽ뻾 (留뚮즺 ?€???뺣━)
├─ L10n 언어 초기화 ?쒋? L10n ?몄뼱 珥덇린?? ?쒋? ?쒕퉬??珥덇린?? ?? ?쒋? AgentMemoryService
├─ 서비스 초기화 ?? ?쒋? ChatSessionStateService
│ ├─ AgentMemoryService ?? ?쒋? AppStateService
│ ├─ ChatSessionStateService ?? ?쒋? IndexService (諛깃렇?쇱슫???뚯씪 ?몃뜳??
│ ├─ AppStateService ?? ?쒋? FuzzyEngine + CommandResolver
│ ├─ IndexService (백그라운드 파일 인덱싱) ?? ?쒋? ContextManager
│ ├─ FuzzyEngine + CommandResolver ?? ?쒋? SessionTrackingService
│ ├─ ContextManager ?? ?쒋? WorktimeReminderService
│ ├─ SessionTrackingService ?? ?붴? ClipboardHistoryService
│ ├─ WorktimeReminderService ?쒋? 鍮뚰듃???몃뱾???깅줉 (136媛?
│ └─ ClipboardHistoryService ?쒋? SchedulerService + PluginHost 珥덇린?? ?쒋? InputListener ?쒖옉 (湲€濡쒕쾶 ?ロ궎)
├─ 빌트인 핸들러 등록 (136개) ?붴? ?곗쿂/?ㅼ젙/?몃젅???덈룄???앹꽦
├─ SchedulerService + PluginHost 초기화
├─ InputListener 시작 (글로벌 핫키)
└─ 런처/설정/트레이 윈도우 생성
``` ```
--- ---
## 5. 핵심 아키텍처 ## 5. ?듭떖 ?꾪궎?띿쿂
### 5.1 런처 (Launcher) ### 5.1 ?곗쿂 (Launcher)
**검색 파이프라인**: 사용자 입력 → `CommandResolver` (접두어 매칭) → `FuzzyEngine` (퍼지 검색) → 결과 정렬 → UI 렌더링 **寃€???뚯씠?꾨씪??*: ?ъ슜???낅젰 ??`CommandResolver` (?묐몢??留ㅼ묶) ??`FuzzyEngine` (?쇱? 寃€?? ??寃곌낵 ?뺣젹 ??UI ?뚮뜑留?
- `FuzzyEngine`: ?뚯씪 ?몃뜳??湲곕컲 ?쇱? 留ㅼ묶, ?먯닔 ?쒖쐞
- `CommandResolver`: ?몃뱾???쇱슦??(?묐몢??`@`, `!`, `#`, `~`, `>`, `$` ??
- `IndexService`: 諛깃렇?쇱슫???뚯씪 ?몃뜳??(`.git`, `node_modules` ???쒖쇅)
- `FuzzyEngine`: 파일 인덱스 기반 퍼지 매칭, 점수 순위 **?꾩젽**: ?깅뒫 紐⑤땲?? ?щえ?꾨줈, 硫붾え, ?좎뵪, 罹섎┛?? 諛고꽣由?
- `CommandResolver`: 핸들러 라우팅 (접두어 `@`, `!`, `#`, `~`, `>`, `$`) ### 5.2 ?먯씠?꾪듃 (Agent Loop)
- `IndexService`: 백그라운드 파일 인덱싱 (`.git`, `node_modules` 등 제외)
**위젯**: 성능 모니터, 포모도로, 메모, 날씨, 캘린더, 배터리
### 5.2 에이전트 (Agent Loop)
``` ```
사용자 메시지 ?ъ슜??硫붿떆吏€
LlmService.StreamAsync() (LLM API 호출) ??LlmService.StreamAsync() (LLM API ?몄텧)
→ 응답 스트리밍 수신 ???묐떟 ?ㅽ듃由щ컢 ?섏떊
→ 도구 호출 감지 시: ???꾧뎄 ?몄텧 媛먯? ??
ToolRegistry에서 도구 조회 ??ToolRegistry?먯꽌 ?꾧뎄 議고쉶
→ 권한 확인 (AskPermissionCallback) ??沅뚰븳 ?뺤씤 (AskPermissionCallback)
→ 도구 실행 ???꾧뎄 ?ㅽ뻾
→ 결과를 컨텍스트에 추가 ??寃곌낵瑜?而⑦뀓?ㅽ듃??異붽?
LLM 재호출 (반복) ??LLM ?ы샇異?(諛섎났)
→ 최종 텍스트 응답 반환 ??理쒖쥌 ?띿뒪???묐떟 諛섑솚
``` ```
**핵심 클래스**: **?듭떖 ?대옒??*:
- `AgentLoopService` — 루프 엔진 (반복, 일시정지/재개, 이벤트 발행) - `AgentLoopService` ??猷⑦봽 ?붿쭊 (諛섎났, ?쇱떆?뺤?/?ш컻, ?대깽??諛쒗뻾)
- `AxAgentExecutionEngine` — 도구 실행 조율 - `AxAgentExecutionEngine` ???꾧뎄 ?ㅽ뻾 議곗쑉
- `AgentLoopParallelExecution` — 병렬 도구 실행 - `AgentLoopParallelExecution` ??蹂묐젹 ?꾧뎄 ?ㅽ뻾
- `AgentLoopTransitions` / `.Execution` — 상태 전이 로직 - `AgentLoopTransitions` / `.Execution` ???곹깭 ?꾩씠 濡쒖쭅
- `ToolRegistry` — 도구 등록/조회 - `ToolRegistry` ???꾧뎄 ?깅줉/議고쉶
- `ContextCondenser` — 컨텍스트 압축 (토큰 관리) - `ContextCondenser` ??而⑦뀓?ㅽ듃 ?뺤텞 (?좏겙 愿€由?
**도구 카테고리** (114개): **?꾧뎄 移댄뀒怨좊━** (114媛?:
| 카테고리 | 예시 | | 移댄뀒怨좊━ | ?덉떆 |
|---------|------| |---------|------|
| 파일 I/O | FileReadTool, FileEditTool, FileManageTool, FileWriteTool | | ?뚯씪 I/O | FileReadTool, FileEditTool, FileManageTool, FileWriteTool |
| 검색 | GlobTool, GrepTool, CodeSearchTool, FileSearchTool | | 寃€??| GlobTool, GrepTool, CodeSearchTool, FileSearchTool |
| 문서 | DocumentReaderTool, ExcelSkill, DocxSkill, PptxSkill, CsvSkill, HtmlSkill | | 臾몄꽌 | DocumentReaderTool, ExcelSkill, DocxSkill, PptxSkill, CsvSkill, HtmlSkill |
| 코드 | BuildRunTool, SnippetRunnerTool, CodeReviewTool, TestLoopTool, LspTool | | 肄붾뱶 | BuildRunTool, SnippetRunnerTool, CodeReviewTool, TestLoopTool, LspTool |
| 데이터 | JsonTool, XmlTool, SqlTool, DataPivotTool, RegexTool | | ?곗씠??| JsonTool, XmlTool, SqlTool, DataPivotTool, RegexTool |
| 시스템 | ProcessTool, EnvTool, ZipTool, ClipboardTool | | ?쒖뒪??| ProcessTool, EnvTool, ZipTool, ClipboardTool |
| 계획/추적 | TodoWriteTool, TaskTrackerTool, CheckpointTool, PlaybookTool | | 怨꾪쉷/異붿쟻 | TodoWriteTool, TaskTrackerTool, CheckpointTool, PlaybookTool |
| 사용자 | UserAskTool, SuggestActionsTool, NotifyTool | | ?ъ슜??| UserAskTool, SuggestActionsTool, NotifyTool |
| MCP | McpTool, McpListResourcesTool, McpReadResourceTool | | MCP | McpTool, McpListResourcesTool, McpReadResourceTool |
### 5.3 LLM 서비스 ### 5.3 LLM ?쒕퉬??
**吏€??怨듦툒??*:
**지원 공급자**: | ?쒕퉬??| ?ㅻ챸 |
| 서비스 | 설명 |
|--------|------| |--------|------|
| `claude` / `sigmoid` | Anthropic Claude (Sigmoid API 경유) | | `claude` / `sigmoid` | Anthropic Claude (Sigmoid API 寃쎌쑀) |
| `gemini` | Google Gemini API | | `gemini` | Google Gemini API |
| `vllm` | OpenAI 호환 vLLM (IBM CP4D 지원 포함) | | `vllm` | OpenAI ?명솚 vLLM (IBM CP4D 吏€???ы븿) |
| `ollama` | 로컬 Ollama 모델 | | `ollama` | 濡쒖뺄 Ollama 紐⑤뜽 |
**모델 라우팅**: `ModelRouterService`를 통한 오버라이드 스택 — 대화 중 모델/서비스를 동적으로 전환 가능 **紐⑤뜽 ?쇱슦??*: `ModelRouterService`瑜??듯븳 ?ㅻ쾭?쇱씠???ㅽ깮 ???€??以?紐⑤뜽/?쒕퉬?ㅻ? ?숈쟻?쇰줈 ?꾪솚 媛€??
**?좏겙 愿€由?*: `TokenEstimator`濡?而⑦뀓?ㅽ듃 湲몄씠 異붿젙, ?ㅻ쾭?뚮줈????`ContextCondenser`媛€ ?먮룞 ?뺤텞
**토큰 관리**: `TokenEstimator`로 컨텍스트 길이 추정, 오버플로우 시 `ContextCondenser`가 자동 압축 ### 5.4 ?€???€?μ냼
### 5.4 대화 저장소
- `ChatStorageService`: SQLite 기반 대화 영속화
- `ChatSessionStateService`: 메모리 내 세션 상태 관리
- `ChatConversation`: 메시지 목록 + 실행 이벤트 타임라인
- `ChatStorageService`: SQLite 湲곕컲 ?€???곸냽??- `ChatSessionStateService`: 硫붾え由????몄뀡 ?곹깭 愿€由?- `ChatConversation`: 硫붿떆吏€ 紐⑸줉 + ?ㅽ뻾 ?대깽???€?꾨씪??
--- ---
## 6. UI 계층 ## 6. UI 怨꾩링
### 주요 윈도우 ### 二쇱슂 ?덈룄??
| ?덈룄??| ??븷 |
| 윈도우 | 역할 |
|--------|------| |--------|------|
| `LauncherWindow` | 메인 런처 (검색, 위젯, 결과 목록) | | `LauncherWindow` | 硫붿씤 ?곗쿂 (寃€?? ?꾩젽, 寃곌낵 紐⑸줉) |
| `ChatWindow` | AI 에이전트 대화 (채팅/Cowork/코드 탭) | | `ChatWindow` | AI ?먯씠?꾪듃 ?€??(梨꾪똿/Cowork/肄붾뱶 ?? |
| `DockBarWindow` | 독 바 (시스템 리소스, 빠른 접근) | | `DockBarWindow` | ??諛?(?쒖뒪??由ъ냼?? 鍮좊Ⅸ ?묎렐) |
| `SettingsWindow` | 설정 관리 | | `SettingsWindow` | ?ㅼ젙 愿€由?|
| `AgentSettingsWindow` | 에이전트 전용 설정 | | `AgentSettingsWindow` | ?먯씠?꾪듃 ?꾩슜 ?ㅼ젙 |
| `AgentStatsDashboardWindow` | 에이전트 통계 대시보드 | | `AgentStatsDashboardWindow` | ?먯씠?꾪듃 ?듦퀎 ?€?쒕낫??|
| `SkillEditorWindow` | 스킬 편집기 | | `SkillEditorWindow` | ?ㅽ궗 ?몄쭛湲?|
| `SkillGalleryWindow` | 스킬 갤러리 | | `SkillGalleryWindow` | ?ㅽ궗 媛ㅻ윭由?|
| `TrayMenuWindow` | 시스템 트레이 메뉴 | | `TrayMenuWindow` | ?쒖뒪???몃젅??硫붾돱 |
| `PreviewWindow` | 문서 미리보기 (WebView2) | | `PreviewWindow` | 臾몄꽌 誘몃━蹂닿린 (WebView2) |
### ChatWindow 분할 구조 ### ChatWindow 遺꾪븷 援ъ“
`ChatWindow.xaml.cs`partial class로 기능별 분할: `ChatWindow.xaml.cs`??partial class濡?湲곕뒫蹂?遺꾪븷:
| 파일 | 역할 | | ?뚯씪 | ??븷 |
|------|------| |------|------|
| `ChatWindow.xaml.cs` | 메인 오케스트레이션, 스트리밍, 입력 처리 | | `ChatWindow.xaml.cs` | 硫붿씤 ?ㅼ??ㅽ듃?덉씠?? ?ㅽ듃由щ컢, ?낅젰 泥섎━ |
| `ChatWindow.AgentEventProcessor.cs` | 에이전트 이벤트 수신/라우팅 | | `ChatWindow.AgentEventProcessor.cs` | ?먯씠?꾪듃 ?대깽???섏떊/?쇱슦??|
| `ChatWindow.AgentEventRendering.cs` | 에이전트 이벤트 배너/카드 렌더링 | | `ChatWindow.AgentEventRendering.cs` | ?먯씠?꾪듃 ?대깽??諛곕꼫/移대뱶 ?뚮뜑留?|
| `ChatWindow.ComposerQueuePresentation.cs` | 작성기 큐 UI | | `ChatWindow.ComposerQueuePresentation.cs` | ?묒꽦湲???UI |
| `ChatWindow.ContextUsagePresentation.cs` | 컨텍스트 사용량 링/팝업 | | `ChatWindow.ContextUsagePresentation.cs` | 而⑦뀓?ㅽ듃 ?ъ슜??留??앹뾽 |
| `ChatWindow.ConversationFilterPresentation.cs` | 대화 필터링 | | `ChatWindow.ConversationFilterPresentation.cs` | ?€???꾪꽣留?|
| `ChatWindow.ConversationListPresentation.cs` | 사이드바 대화 목록 | | `ChatWindow.ConversationListPresentation.cs` | ?ъ씠?쒕컮 ?€??紐⑸줉 |
| `ChatWindow.ConversationManagementPresentation.cs` | 대화 생성/삭제/관리 | | `ChatWindow.ConversationManagementPresentation.cs` | ?€???앹꽦/??젣/愿€由?|
| `ChatWindow.FileBrowserPresentation.cs` | 파일 브라우저 UI | | `ChatWindow.FileBrowserPresentation.cs` | ?뚯씪 釉뚮씪?곗? UI |
| `ChatWindow.FooterPresentation.cs` | 하단 바 (폴더, 권한) | | `ChatWindow.FooterPresentation.cs` | ?섎떒 諛?(?대뜑, 沅뚰븳) |
| `ChatWindow.GitBranchPresentation.cs` | Git 브랜치 표시/전환 | | `ChatWindow.GitBranchPresentation.cs` | Git 釉뚮옖移??쒖떆/?꾪솚 |
| `ChatWindow.LiveProgressPresentation.cs` | 실시간 진행 상태 | | `ChatWindow.LiveProgressPresentation.cs` | ?ㅼ떆媛?吏꾪뻾 ?곹깭 |
| `ChatWindow.MessageBubblePresentation.cs` | 메시지 버블 렌더링 | | `ChatWindow.MessageBubblePresentation.cs` | 硫붿떆吏€ 踰꾨툝 ?뚮뜑留?|
| `ChatWindow.MessageInteractions.cs` | 메시지 복사/편집/재전송 | | `ChatWindow.MessageInteractions.cs` | 硫붿떆吏€ 蹂듭궗/?몄쭛/?ъ쟾??|
| `ChatWindow.PermissionPresentation.cs` | 권한 팝업/배너 UI | | `ChatWindow.PermissionPresentation.cs` | 沅뚰븳 ?앹뾽/諛곕꼫 UI |
| `ChatWindow.PlanApprovalPresentation.cs` | 계획 승인 카드 | | `ChatWindow.PlanApprovalPresentation.cs` | 怨꾪쉷 ?뱀씤 移대뱶 |
| `ChatWindow.PopupPresentation.cs` | 공통 팝업 구성 | | `ChatWindow.PopupPresentation.cs` | 怨듯넻 ?앹뾽 援ъ꽦 |
| `ChatWindow.PreviewPresentation.cs` | 파일 미리보기 탭 | | `ChatWindow.PreviewPresentation.cs` | ?뚯씪 誘몃━蹂닿린 ??|
| `ChatWindow.SelectionPopupPresentation.cs` | 워크트리 선택 팝업 | | `ChatWindow.SelectionPopupPresentation.cs` | ?뚰겕?몃━ ?좏깮 ?앹뾽 |
| `ChatWindow.SidebarInteractionPresentation.cs` | 사이드바 상호작용 | | `ChatWindow.SidebarInteractionPresentation.cs` | ?ъ씠?쒕컮 ?곹샇?묒슜 |
| `ChatWindow.StatusPresentation.cs` | 상태 배지/스트립 | | `ChatWindow.StatusPresentation.cs` | ?곹깭 諛곗?/?ㅽ듃由?|
| `ChatWindow.SurfaceVisualPresentation.cs` | 시각 효과 (글로우, 펄스 등) | | `ChatWindow.SurfaceVisualPresentation.cs` | ?쒓컖 ?④낵 (湲€濡쒖슦, ?꾩뒪 ?? |
| `ChatWindow.TaskSummary.cs` | 작업 요약 카드 | | `ChatWindow.TaskSummary.cs` | ?묒뾽 ?붿빟 移대뱶 |
| `ChatWindow.TimelinePresentation.cs` | 타임라인 정렬, 캐시, 이벤트 필터링 | | `ChatWindow.TimelinePresentation.cs` | ?€?꾨씪???뺣젹, 罹먯떆, ?대깽???꾪꽣留?|
| `ChatWindow.TopicPresetPresentation.cs` | 주제 프리셋 UI | | `ChatWindow.TopicPresetPresentation.cs` | 二쇱젣 ?꾨━??UI |
| `ChatWindow.TranscriptHost.cs` | 트랜스크립트 호스트 컨테이너 | | `ChatWindow.TranscriptHost.cs` | ?몃옖?ㅽ겕由쏀듃 ?몄뒪??而⑦뀒?대꼫 |
| `ChatWindow.TranscriptPolicy.cs` | 트랜스크립트 표시 정책 | | `ChatWindow.TranscriptPolicy.cs` | ?몃옖?ㅽ겕由쏀듃 ?쒖떆 ?뺤콉 |
| `ChatWindow.TranscriptRenderExecution.cs` | 트랜스크립트 렌더 실행 | | `ChatWindow.TranscriptRenderExecution.cs` | ?몃옖?ㅽ겕由쏀듃 ?뚮뜑 ?ㅽ뻾 |
| `ChatWindow.TranscriptRenderPlanner.cs` | 트랜스크립트 렌더 계획 | | `ChatWindow.TranscriptRenderPlanner.cs` | ?몃옖?ㅽ겕由쏀듃 ?뚮뜑 怨꾪쉷 |
| `ChatWindow.TranscriptRendering.cs` | 트랜스크립트 렌더링 | | `ChatWindow.TranscriptRendering.cs` | ?몃옖?ㅽ겕由쏀듃 ?뚮뜑留?|
| `ChatWindow.TranscriptVirtualization.cs` | 트랜스크립트 가상화 (대규모 대화) | | `ChatWindow.TranscriptVirtualization.cs` | ?몃옖?ㅽ겕由쏀듃 媛€?곹솕 (?€洹쒕え ?€?? |
| `ChatWindow.UserAskPresentation.cs` | 사용자 질문 인라인 카드 | | `ChatWindow.UserAskPresentation.cs` | ?ъ슜??吏덈Ц ?몃씪??移대뱶 |
| `ChatWindow.VisualInteractionHelpers.cs` | 시각 상호작용 헬퍼 | | `ChatWindow.VisualInteractionHelpers.cs` | ?쒓컖 ?곹샇?묒슜 ?ы띁 |
### 테마 시스템 ### ?뚮쭏 ?쒖뒪??
9媛??뚮쭏 XAML 由ъ냼???뺤뀛?덈━: `Dark`, `Light`, `OLED`, `Nord`, `Monokai`, `Catppuccin`, `Sepia`, `Alfred`, `AlfredLight`
9개 테마 XAML 리소스 딕셔너리: `Dark`, `Light`, `OLED`, `Nord`, `Monokai`, `Catppuccin`, `Sepia`, `Alfred`, `AlfredLight` ?고????뚮쭏 ?꾪솚: `SettingsService.Settings.Launcher.Theme` 蹂€寃???由ъ냼???뺤뀛?덈━ 援먯껜
런타임 테마 전환: `SettingsService.Settings.Launcher.Theme` 변경 → 리소스 딕셔너리 교체
--- ---
## 7. 설정 구조 (AppSettings) ## 7. ?ㅼ젙 援ъ“ (AppSettings)
### 최상위 설정 ### 理쒖긽???ㅼ젙
| 속성 | 기본값 | 설명 | | ?띿꽦 | 湲곕낯媛?| ?ㅻ챸 |
|------|--------|------| |------|--------|------|
| `AiEnabled` | true | AI 기능 활성화 | | `AiEnabled` | true | AI 湲곕뒫 ?쒖꽦??|
| `OperationMode` | "internal" | 운영 모드 (internal/external) | | `OperationMode` | "internal" | ?댁쁺 紐⑤뱶 (internal/external) |
| `Hotkey` | "Alt+Space" | 런처 단축키 | | `Hotkey` | "Alt+Space" | ?곗쿂 ?⑥텞??|
| `CleanupPeriodDays` | 30 | 대화 보관 기간 (일) | | `CleanupPeriodDays` | 30 | ?€??蹂닿? 湲곌컙 (?? |
| `InternalModeEnabled` | true | 사내 모드 여부 | | `InternalModeEnabled` | true | ?щ궡 紐⑤뱶 ?щ? |
### LauncherSettings (중첩) ### LauncherSettings (以묒꺽)
| 그룹 | 주요 속성 | | 洹몃9 | 二쇱슂 ?띿꽦 |
|------|----------| |------|----------|
| 표시 | `Theme`, `Opacity`, `Position`, `Width`, `MaxResults` | | ?쒖떆 | `Theme`, `Opacity`, `Position`, `Width`, `MaxResults` |
| 글로우 | `EnableRainbowGlow`, `EnableSelectionGlow`, `ShowLauncherBorder` | | 湲€濡쒖슦 | `EnableRainbowGlow`, `EnableSelectionGlow`, `ShowLauncherBorder` |
| 위젯 | `ShowWidgetPerf`, `ShowWidgetPomo`, `ShowWidgetNote`, `ShowWidgetWeather`, `ShowWidgetCalendar`, `ShowWidgetBattery` | | ?꾩젽 | `ShowWidgetPerf`, `ShowWidgetPomo`, `ShowWidgetNote`, `ShowWidgetWeather`, `ShowWidgetCalendar`, `ShowWidgetBattery` |
| 독 바 | `DockBarItems`, `DockBarAutoShow`, `DockBarOpacity`, `DockBarRainbowGlow` | | ??諛?| `DockBarItems`, `DockBarAutoShow`, `DockBarOpacity`, `DockBarRainbowGlow` |
| 기능 | `EnableFavorites`, `EnableRecent`, `EnableActionMode`, `EnableClipboardAutoCategory` | | 湲곕뒫 | `EnableFavorites`, `EnableRecent`, `EnableActionMode`, `EnableClipboardAutoCategory` |
### LlmSettings (중첩) ### LlmSettings (以묒꺽)
에이전트의 LLM 연결 설정: 서비스 선택, 모델, API (DPAPI 암호화), 엔드포인트, 온도, 최대 토큰 등 ?먯씠?꾪듃??LLM ?곌껐 ?ㅼ젙: ?쒕퉬???좏깮, 紐⑤뜽, API ??(DPAPI ?뷀샇??, ?붾뱶?ъ씤?? ?⑤룄, 理쒕? ?좏겙 ??
| ?띿꽦 | 湲곕낯媛?| ?ㅻ챸 |
| 속성 | 기본값 | 설명 |
|------|--------|------| |------|--------|------|
| `UseAutomaticProfileTemperature` | true | 등록 모델 프로파일의 자동 temperature 정책 | | `UseAutomaticProfileTemperature` | true | ?깅줉 紐⑤뜽 ?꾨줈?뚯씪???먮룞 temperature ?뺤콉 |
| `EnableDetailedLog` | false | 워크플로우 상세 로그 (LLM 요청/응답, 도구 이력) | | `EnableDetailedLog` | false | ?뚰겕?뚮줈???곸꽭 濡쒓렇 (LLM ?붿껌/?묐떟, ?꾧뎄 ?대젰) |
| `DetailedLogRetentionDays` | 3 | 상세 로그 보관 기간 (일) | | `DetailedLogRetentionDays` | 3 | ?곸꽭 濡쒓렇 蹂닿? 湲곌컙 (?? |
| `EnableRawLlmLog` | false | LLM 요청/응답 원문 기록 (디버깅용) | | `EnableRawLlmLog` | false | LLM ?붿껌/?묐떟 ?먮Ц 湲곕줉 (?붾쾭源낆슜) |
### RegisteredModel 실행 프로파일 ### RegisteredModel ?ㅽ뻾 ?꾨줈?뚯씪
모델별 `ExecutionProfile`로 도구 호출 강도, 재시도, 메모리/압축 주입량을 조절: 紐⑤뜽蹂?`ExecutionProfile`濡??꾧뎄 ?몄텧 媛뺣룄, ?ъ떆?? 硫붾え由??뺤텞 二쇱엯?됱쓣 議곗젅:
| 프로파일 | 설명 | | ?꾨줈?뚯씪 | ?ㅻ챸 |
|---------|------| |---------|------|
| `balanced` | 기본 균형 모드 | | `balanced` | 湲곕낯 洹좏삎 紐⑤뱶 |
| `tool_call_strict` | 도구 호출 강제/엄격 모드 | | `tool_call_strict` | ?꾧뎄 ?몄텧 媛뺤젣/?꾧꺽 紐⑤뱶 |
| `reasoning_first` | 추론 우선 모드 | | `reasoning_first` | 異붾줎 ?곗꽑 紐⑤뱶 |
| `fast_readonly` | 빠른 읽기 전용 모드 | | `fast_readonly` | 鍮좊Ⅸ ?쎄린 ?꾩슜 紐⑤뱶 |
| `document_heavy` | 문서 처리 집중 모드 | | `document_heavy` | 臾몄꽌 泥섎━ 吏묒쨷 紐⑤뱶 |
--- ---
## 8. 플러그인 시스템 ## 8. ?뚮윭洹몄씤 ?쒖뒪??
### SDK (AxCopilot.SDK) ### SDK (AxCopilot.SDK)
```csharp ```csharp
public interface IActionHandler public interface IActionHandler
{ {
string? Prefix { get; } // 접두어 (null이면 퍼지 검색만) string? Prefix { get; } // ?묐몢??(null?대㈃ ?쇱? 寃€?됰쭔)
PluginMetadata Metadata { get; } PluginMetadata Metadata { get; }
Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct); Task<IEnumerable<LauncherItem>
Task ExecuteAsync(LauncherItem item, CancellationToken ct); ## claude-code식 transcript
}
```
### 개발 방법 - : 2026-04-09 13:05 (KST)
- AgentTranscriptDisplayCatalog를 row presentation hinking / waiting / compact / tool activity / permission / tool result / status를 transcript row .
- ChatWindow.AgentEventRendering은 process feed GroupKey , Cowork/Code append transcript에 .
- PermissionRequestPresentationCatalog, ToolResultPresentationCatalog를 / , transcript popup이 .
- ChatWindow.FooterPresentation은 execution event가 / , Cowork/Code footer .
-
ender_messages processFeedAppends, processFeedMerges,
owKindCounts를 %APPDATA%\\AxCopilot\\perf transcript grouping .
1. `AxCopilot.SDK` 참조하여 `IActionHandler` 구현 ## 9. ??
2. 빌드된 `.dll``settings.json``Plugins` 배열에 경로 등록
3. `PluginHost`가 앱 시작 시 동적 로드
--- ###
## 9. 빌드 및 실행
### 개발 빌드
```bash ```bash
dotnet build src/AxCopilot/AxCopilot.csproj dotnet build src/AxCopilot/AxCopilot.csproj
``` ```
### 릴리스 빌드 (단일 파일) ### 由대━??鍮뚮뱶 (?⑥씪 ?뚯씪)
```bash ```bash
dotnet publish src/AxCopilot/AxCopilot.csproj -c Release -r win-x64 --self-contained dotnet publish src/AxCopilot/AxCopilot.csproj -c Release -r win-x64 --self-contained
``` ```
릴리스 빌드 옵션: 由대━??鍮뚮뱶 ?듭뀡:
- `PublishSingleFile`: 단일 실행 파일 - `PublishSingleFile`: ?⑥씪 ?ㅽ뻾 ?뚯씪
- `EnableCompressionInSingleFile`: 압축 적용 - `EnableCompressionInSingleFile`: ?뺤텞 ?곸슜
- `PublishReadyToRun`: AOT 프리컴파일 - `PublishReadyToRun`: AOT ?꾨━而댄뙆??- `DebugType=none`: ?붾쾭洹??щ낵 ?쒓굅
- `DebugType=none`: 디버그 심볼 제거 - `TrimMode=partial`: IL ?몃━諛?
- `TrimMode=partial`: IL 트리밍 ### ?뚯뒪??
### 테스트
```bash ```bash
dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj
``` ```
--- ---
## 10. 버전 관리 ## 10. 踰꾩쟾 愿€由?
- `AxCopilot.csproj`??`<Version>` ?쒓렇 ?섎굹留?蹂€寃쏀븯硫????꾩껜??諛섏쁺
- `AxCopilot.csproj``<Version>` 태그 하나만 변경하면 앱 전체에 반영 - ?ㅼ젙 ?ㅽ궎留?踰꾩쟾?€ `SettingsService.cs` ??`CurrentSettingsVersion`?먯꽌 蹂꾨룄 愿€由?- 留덉씠洹몃젅?댁뀡: `SettingsService`媛€ ?댁쟾 踰꾩쟾 ?ㅼ젙 ?뚯씪???먮룞 ?낃렇?덉씠??
- 설정 스키마 버전은 `SettingsService.cs``CurrentSettingsVersion`에서 별도 관리
- 마이그레이션: `SettingsService`가 이전 버전 설정 파일을 자동 업그레이드
--- ---
## 11. 보안 ## 11. 蹂댁븞
| 항목 | 구현 | | ??ぉ | 援ы쁽 |
|------|------| |------|------|
| API 키 저장 | DPAPI 암호화 (System.Security.Cryptography.ProtectedData) | | API ???€??| DPAPI ?뷀샇??(System.Security.Cryptography.ProtectedData) |
| 키 관리 도구 | AxKeyEncryptor (별도 유틸리티) | | ??愿€由??꾧뎄 | AxKeyEncryptor (蹂꾨룄 ?좏떥由ы떚) |
| 안티 탬퍼 | 디버거/디컴파일러 감지 (Release 빌드, `Security/AntiTamper.cs`) | | ?덊떚 ?ы띁 | ?붾쾭嫄??붿뺨?뚯씪??媛먯? (Release 鍮뚮뱶, `Security/AntiTamper.cs`) |
| Unsafe 코드 | `AllowUnsafeBlocks=true` (ScreenCaptureHandler 포인터 연산용) | | Unsafe 肄붾뱶 | `AllowUnsafeBlocks=true` (ScreenCaptureHandler ?ъ씤???곗궛?? |
--- ---
## 12. 성능 최적화 내역 ## 12. ?깅뒫 理쒖쟻???댁뿭
### 유휴 CPU 최적화 (2026-04-09) ### ?좏쑕 CPU 理쒖쟻??(2026-04-09)
| 대상 | 변경 전 | 변경 후 | | ?€??| 蹂€寃???| 蹂€寃???|
|------|---------|---------| |------|---------|---------|
| PerformanceMonitorService 폴링 | 2| 5| | PerformanceMonitorService ?대쭅 | 2珥?| 5珥?|
| 위젯 타이머 | 1| 3| | ?꾩젽 ?€?대㉧ | 1珥?| 3珥?|
| 레인보우 글로우 타이머 | 150ms | 300ms | | ?덉씤蹂댁슦 湲€濡쒖슦 ?€?대㉧ | 150ms | 300ms |
| ServerStatusService | 15| 60| | ServerStatusService ??| 15珥?| 60珥?|
### 스트리밍 렌더링 최적화 (2026-04-09) ### ?ㅽ듃由щ컢 ?뚮뜑留?理쒖쟻??(2026-04-09)
- **TypingTimer**: 50ms 80ms, `string.Concat` `char[]` 버퍼 재사용 - **TypingTimer**: 50ms ??80ms, `string.Concat` ??`char[]` 踰꾪띁 ?ъ궗??- **CursorTimer**: ?꾩껜 臾몄옄???ъ깮????留덉?留?臾몄옄留?援먯껜
- **CursorTimer**: 전체 문자열 재생성 → 마지막 문자만 교체 - **StringBuilder.ToString()**: 30ms 理쒖냼 媛꾧꺽 ?곕줈?€留?- **RenderMessages**: ?ㅽ듃由щ컢 以?遺덊븘?뷀븳 ?꾩껜 ?щ젋?붾쭅 諛⑹? (議곌린 諛섑솚)
- **StringBuilder.ToString()**: 30ms 최소 간격 쓰로틀링 - **?€?꾨씪???대깽??*: ?묓엺 紐⑤뱶?먯꽌 ?곗냽 ?숈씪 ToolCall 蹂묓빀
- **RenderMessages**: 스트리밍 중 불필요한 전체 재렌더링 방지 (조기 반환)
- **타임라인 이벤트**: 접힌 모드에서 연속 동일 ToolCall 병합
### 런타임 안정성 수정 (2026-04-09) ### ?고????덉젙???섏젙 (2026-04-09)
| 파일 | 수정 내용 | | ?뚯씪 | ?섏젙 ?댁슜 |
|------|----------| |------|----------|
| `CsvSkill.cs` | JSON 배열 첫 요소 `ValueKind` 검증 추가 | | `CsvSkill.cs` | JSON 諛곗뿴 泥??붿냼 `ValueKind` 寃€利?異붽? |
| `HtmlSkill.cs` | gradient `Split(',')` 결과 `Length >= 2` 가드 추가 | | `HtmlSkill.cs` | gradient `Split(',')` 寃곌낵 `Length >= 2` 媛€??異붽? |
| `ChatWindow.xaml.cs` | `ParseGenericAction` 빈 배열 가드, `ShowDropActionMenu` null 가드, `GetAgentLoop` `.FirstOrDefault()` 전환 | | `ChatWindow.xaml.cs` | `ParseGenericAction` 鍮?諛곗뿴 媛€?? `ShowDropActionMenu` null 媛€?? `GetAgentLoop` `.FirstOrDefault()` ?꾪솚 |
| `ChatWindow.GitBranchPresentation.cs` | async void 핸들러 try/catch 보호 | | `ChatWindow.GitBranchPresentation.cs` | async void ?몃뱾??try/catch 蹂댄샇 |
| `ChatWindow.xaml.cs` (BtnGitBranch_Click) | async void 핸들러 try/catch 보호 | | `ChatWindow.xaml.cs` (BtnGitBranch_Click) | async void ?몃뱾??try/catch 蹂댄샇 |
### UI 스레드 부하 최적화 2차 (2026-04-09) ### UI ?ㅻ젅??遺€??理쒖쟻??2李?(2026-04-09)
| 대상 | 변경 전 | 변경 후 | 효과 | | ?€??| 蹂€寃???| 蹂€寃???| ?④낵 |
|------|---------|---------|------| |------|---------|---------|------|
| 스크롤 애니메이션 | 매번 새 16ms 타이머 생성 | 재사용 32ms 타이머 1개 | GC 압력 + 타이머 누적 해소 | | ?ㅽ겕濡??좊땲硫붿씠??| 留ㅻ쾲 ??16ms ?€?대㉧ ?앹꽦 | ?ъ궗??32ms ?€?대㉧ 1媛?| GC ?뺣젰 + ?€?대㉧ ?꾩쟻 ?댁냼 |
| 사이드바 애니메이션 | 매번 새 10ms 타이머 생성 | 재사용 32ms 타이머 1개 | 동일 | | ?ъ씠?쒕컮 ?좊땲硫붿씠??| 留ㅻ쾲 ??10ms ?€?대㉧ ?앹꽦 | ?ъ궗??32ms ?€?대㉧ 1媛?| ?숈씪 |
| Git 브랜치 UI | `Dispatcher.Invoke` (블로킹) | `Dispatcher.InvokeAsync` (논블로킹) | UI 스레드 차단 해소 | | Git 釉뚮옖移?UI | `Dispatcher.Invoke` (釉붾줈?? | `Dispatcher.InvokeAsync` (?쇰툝濡쒗궧) | UI ?ㅻ젅??李⑤떒 ?댁냼 |
| 토큰 사용량 호 | 매 250ms PathGeometry 재생성 | 1% 미만 변화 시 렌더링 생략 | 불필요한 레이아웃 연산 제거 | | ?좏겙 ?ъ슜????| 留?250ms PathGeometry ?ъ깮??| 1% 誘몃쭔 蹂€?????뚮뜑留??앸왂 | 遺덊븘?뷀븳 ?덉씠?꾩썐 ?곗궛 ?쒓굅 |
| 대화 검색 타이머 | 140ms | 300ms | 초당 7회 → 3회 | | ?€??寃€???€?대㉧ | 140ms | 300ms | 珥덈떦 7????3??|
| 에이전트 이벤트 타이머 | 140ms (스트리밍: 300/420) | 200ms (스트리밍: 350/500) | 이벤트 처리 빈도 완화 | | ?먯씠?꾪듃 ?대깽???€?대㉧ | 140ms (?ㅽ듃由щ컢: 300/420) | 200ms (?ㅽ듃由щ컢: 350/500) | ?대깽??泥섎━ 鍮덈룄 ?꾪솕 |
| 반응형 레이아웃 타이머 | 120ms | 250ms | 리사이즈 디바운스 강화 | | 諛섏쓳???덉씠?꾩썐 ?€?대㉧ | 120ms | 250ms | 由ъ궗?댁쫰 ?붾컮?댁뒪 媛뺥솕 |
| 대화 목록 LINQ | Where×2 + Count×3 = 리스트 5회 순회 | Where 1회 병합 + 단일 루프 카운트 | 할당/순회 대폭 감소 | | ?€??紐⑸줉 LINQ | Where2 + Count3 = 由ъ뒪??5???쒗쉶 | Where 1??蹂묓빀 + ?⑥씪 猷⑦봽 移댁슫??| ?좊떦/?쒗쉶 ?€??媛먯냼 |
### 구조적 메모리/안정성 수정 (2026-04-09) ### 援ъ“??硫붾え由??덉젙???섏젙 (2026-04-09)
| 문제 | 위치 | 수정 | | 臾몄젣 | ?꾩튂 | ?섏젙 |
|------|------|------| |------|------|------|
| Events 컬렉션 무한 성장 | `AgentLoopService.cs` | 500개 초과 시 오래된 이벤트 자동 제거 | | Events 而щ젆??臾댄븳 ?깆옣 | `AgentLoopService.cs` | 500媛?珥덇낵 ???ㅻ옒???대깽???먮룞 ?쒓굅 |
| 파일 브라우저 타이머 좀비 | `ChatWindow.FileBrowserPresentation.cs` | 매번 새 타이머 생성 → 재사용 패턴 | | ?뚯씪 釉뚮씪?곗? ?€?대㉧ 醫€鍮?| `ChatWindow.FileBrowserPresentation.cs` | 留ㅻ쾲 ???€?대㉧ ?앹꽦 ???ъ궗???⑦꽩 |
| 엘리먼트 캐시 미정리 | `ChatWindow.TranscriptVirtualization.cs` | 보유 한도 240→120, 1.5배 초과 시 정리 | | ?섎━癒쇳듃 罹먯떆 誘몄젙由?| `ChatWindow.TranscriptVirtualization.cs` | 蹂댁쑀 ?쒕룄 240??20, 1.5諛?珥덇낵 ???뺣━ |
| WorkflowAnalyzer UI 블로킹 | `WorkflowAnalyzerWindow.xaml.cs` | `Dispatcher.Invoke` `InvokeAsync` | | WorkflowAnalyzer UI 釉붾줈??| `WorkflowAnalyzerWindow.xaml.cs` | `Dispatcher.Invoke` ??`InvokeAsync` |
### 구조적 리팩토링 P1 (2026-04-09) ### 援ъ“??由ы뙥?좊쭅 P1 (2026-04-09)
| 대상 | 파일 | 변경 | | ?€??| ?뚯씪 | 蹂€寃?|
|------|------|------| |------|------|------|
| 인크리멘탈 렌더 hiddenCount 안정화 | `ChatWindow.TranscriptRenderPlanner.cs` | 스트리밍 중 hiddenCount 감소 차단 → prefix 키 불일치로 인한 전체 재빌드 폴백 방지 | | ?명겕由щ찘???뚮뜑 hiddenCount ?덉젙??| `ChatWindow.TranscriptRenderPlanner.cs` | ?ㅽ듃由щ컢 以?hiddenCount 媛먯냼 李⑤떒 ??prefix ??遺덉씪移섎줈 ?명븳 ?꾩껜 ?щ퉴???대갚 諛⑹? |
| 비가시 렌더 차단 | `ChatWindow.TranscriptRendering.cs` | 최소화/숨김 상태에서 RenderMessages 즉시 반환 → 불필요한 UI 재구축 제거 | | 鍮꾧????뚮뜑 李⑤떒 | `ChatWindow.TranscriptRendering.cs` | 理쒖냼???④? ?곹깭?먯꽌 RenderMessages 利됱떆 諛섑솚 ??遺덊븘?뷀븳 UI ?ш뎄異??쒓굅 |
| ConversationList 이벤트 위임 | `ChatWindow.ConversationListPresentation.cs` | 항목당 5개 람다 핸들러 → ConversationPanel에 단일 위임 핸들러 (Tag 기반 분기). 탭 전환 시 250개 핸들러 누적 해소 | | ConversationList ?대깽???꾩엫 | `ChatWindow.ConversationListPresentation.cs` | ??ぉ??5媛??뚮떎 ?몃뱾????ConversationPanel???⑥씪 ?꾩엫 ?몃뱾??(Tag 湲곕컲 遺꾧린). ???꾪솚 ??250媛??몃뱾???꾩쟻 ?댁냼 |
| TopicPreset 이벤트 위임 | `ChatWindow.TopicPresetPresentation.cs` | 카드당 3개 람다 핸들러 → TopicButtonPanel에 단일 위임 핸들러. 탭 전환 시 45개 핸들러 누적 해소 | | TopicPreset ?대깽???꾩엫 | `ChatWindow.TopicPresetPresentation.cs` | 移대뱶??3媛??뚮떎 ?몃뱾????TopicButtonPanel???⑥씪 ?꾩엫 ?몃뱾?? ???꾪솚 ??45媛??몃뱾???꾩쟻 ?댁냼 |
| 공통 VisualTree 헬퍼 | `ChatWindow.VisualInteractionHelpers.cs` | `FindAncestorWithTag<T>`, `FindAncestor<T>` 유틸 추가 | | 怨듯넻 VisualTree ?ы띁 | `ChatWindow.VisualInteractionHelpers.cs` | `FindAncestorWithTag<T>`, `FindAncestor<T>` ?좏떥 異붽? |
### 구조적 리팩토링 P2 (2026-04-09) ### 援ъ“??由ы뙥?좊쭅 P2 (2026-04-09)
| 대상 | 파일 | 변경 | | ?€??| ?뚯씪 | 蹂€寃?|
|------|------|------| |------|------|------|
| _agentLiveContainer 인크리멘탈 허용 | `TranscriptRenderPlanner.cs`, `TranscriptRenderExecution.cs` | 라이브 컨테이너를 expectedChildCount에 포함, 인크리멘탈 시 임시 분리/재삽입 → `hasExternalChildren` 차단 해소 | | _agentLiveContainer ?명겕由щ찘???덉슜 | `TranscriptRenderPlanner.cs`, `TranscriptRenderExecution.cs` | ?쇱씠釉?而⑦뀒?대꼫瑜?expectedChildCount???ы븿, ?명겕由щ찘?????꾩떆 遺꾨━/?ъ궫????`hasExternalChildren` 李⑤떒 ?댁냼 |
| 스트리밍 append-only 렌더 | `TranscriptRenderExecution.cs`, `TranscriptRendering.cs` | prefix 비교 우회하는 `TryApplyStreamingAppendRender` 추가 — stable 키 부분집합 관계만 확인, 새 항목만 추가 | | ?ㅽ듃由щ컢 append-only ?뚮뜑 | `TranscriptRenderExecution.cs`, `TranscriptRendering.cs` | prefix 鍮꾧탳 ?고쉶?섎뒗 `TryApplyStreamingAppendRender` 異붽? ??stable ??遺€遺꾩쭛??愿€怨꾨쭔 ?뺤씤, ????ぉ留?異붽? |
| Permission 이벤트 위임 | `ChatWindow.PermissionPresentation.cs` | 행당 4개 람다 → PermissionItems에 단일 위임 핸들러 + `PermissionItemTag` | | Permission ?대깽???꾩엫 | `ChatWindow.PermissionPresentation.cs` | ?됰떦 4媛??뚮떎 ??PermissionItems???⑥씪 ?꾩엫 ?몃뱾??+ `PermissionItemTag` |
| Preview 탭 이벤트 위임 | `ChatWindow.PreviewPresentation.cs` | 탭당 7개 람다 → PreviewTabPanel에 단일 위임 핸들러 + `PreviewTabTag` | | Preview ???대깽???꾩엫 | `ChatWindow.PreviewPresentation.cs` | ??떦 7媛??뚮떎 ??PreviewTabPanel???⑥씪 ?꾩엫 ?몃뱾??+ `PreviewTabTag` |
| GitBranch 이벤트 위임 | `ChatWindow.GitBranchPresentation.cs`, `SelectionPopupPresentation.cs` | `CreateFlatPopupRow`/`CreatePopupMenuRow` 행의 람다 → GitBranchItems에 단일 위임 + `PopupRowTag` | | GitBranch ?대깽???꾩엫 | `ChatWindow.GitBranchPresentation.cs`, `SelectionPopupPresentation.cs` | `CreateFlatPopupRow`/`CreatePopupMenuRow` ?됱쓽 ?뚮떎 ??GitBranchItems???⑥씪 ?꾩엫 + `PopupRowTag` |
### 구조적 리팩토링 P3 (2026-04-09) ### 援ъ“??由ы뙥?좊쭅 P3 (2026-04-09)
| 대상 | 파일 | 변경 | | ?€??| ?뚯씪 | 蹂€寃?|
|------|------|------| |------|------|------|
| FileBrowser 명시적 해제 | `ChatWindow.FileBrowserPresentation.cs` | TreeViewItem 람다→명명 메서드(`FileTreeItem_Expanded/DoubleClick/RightClick`) 전환. `BuildFileTree()` `DetachFileTreeHandlers()` 재귀 호출로 Clear 전 핸들러 해제. 트리 재구축당 300개 핸들러 누적 해소 | | FileBrowser 紐낆떆???댁젣 | `ChatWindow.FileBrowserPresentation.cs` | TreeViewItem ?뚮떎?믩챸紐?硫붿꽌??`FileTreeItem_Expanded/DoubleClick/RightClick`) ?꾪솚. `BuildFileTree()` ??`DetachFileTreeHandlers()` ?ш? ?몄텧濡?Clear ???몃뱾???댁젣. ?몃━ ?ш뎄異뺣떦 300媛??몃뱾???꾩쟻 ?댁냼 |
> 전체 계획 완료. `docs/STRUCTURAL_REFACTORING_PLAN.md` 참조.
### 런처 · 에이전트 리소스/안정성 수정 (2026-04-09) ## claude-code식 transcript 표시 구조 정리
| 대상 | 파일 | 변경 | - 업데이트: 2026-04-09 13:05 (KST)
|------|------|------| - AgentTranscriptDisplayCatalog를 row presentation 중심으로 재구성해 hinking / waiting / compact / tool activity / permission / tool result / status를 개별 transcript row 의미로 다룰 수 있게 정리했습니다.
| LauncherWindow 이벤트 누수 | `LauncherWindow.xaml.cs` | `vm.CloseRequested`, `vm.PropertyChanged`, `app.IndexService.IndexRebuilt` 핸들러를 필드 저장 → `OnClosed`에서 `-=` 해제. ViewModel보다 Window가 먼저 닫힐 때 GC 누수 방지 | - ChatWindow.AgentEventRendering은 process feed 계열 이벤트를 같은 GroupKey 단위로 병합해, 긴 Cowork/Code 실행 중 append 수를 줄이면서도 주요 활동 흐름이 기본 transcript에 남도록 조정했습니다.
| ChatWindow 타이머 정리 | `ChatWindow.xaml.cs` | `Closed` 핸들러에 누락된 8개 타이머 명시적 `Stop()` 추가 + `StopAgentEventProcessor()` 호출 | - PermissionRequestPresentationCatalog, ToolResultPresentationCatalog를 다시 정리해 권한 요청과 도구 결과를 행위/상태 기준으로 구분하고, transcript 렌더와 popup이 같은 메타를 공유하도록 맞췄습니다.
| Events 스레드 안전 | `AgentLoopService.cs` | Dispatcher 없을 때 `Events` 접근에 `lock(Events)` 추가 — 동시 EmitEvent 호출 시 IndexOutOfRange 크래시 방지 | - ChatWindow.FooterPresentation은 execution event가 생긴 뒤에는 프리셋 안내 카드를 자동으로 숨기도록 바꿔 결과/진행 화면을 덮지 않게 했고, Cowork/Code 입력 워터마크와 footer 기본 문구도 정상 한국어 기준으로 다시 정리했습니다.
| NotifyTool 타이머 누적 | `NotifyTool.cs` | 알림당 `new DispatcherTimer``DoubleAnimation.Completed` 콜백으로 대체. 100개 알림 시 100개 타이머 동시 존재 해소 | -
| LauncherWindow 토스트 타이머 | `LauncherWindow.xaml.cs` | `ShowToast()` 매 호출 `new DispatcherTimer` → 재사용 패턴 + 명명 메서드(`ToastTimer_Tick`) | ender_messages 성능 로그에는 processFeedAppends, processFeedMerges,
| LauncherWindow 타이머 정리 | `LauncherWindow.xaml.cs` | `OnClosed``_toastTimer?.Stop()`, `_indexStatusTimer?.Stop()` 추가 | owKindCounts를 함께 남겨 %APPDATA%\\AxCopilot\\perf 기준으로 transcript grouping 효과를 수치로 비교할 수 있게 했습니다.
### Hot path · 리소스 추가 최적화 (2026-04-09) ## 13. ?붾젆?좊━蹂?媛€?대뱶
| 대상 | 파일 | 변경 | | ?붾젆?좊━ | ?섏젙 ??二쇱쓽?ы빆 |
|------|------|------|
| GetRuntimeActiveTools 캐시 | `AgentLoopService.cs` | 반복당 1~4회 호출 → `cachedActiveTools` 로컬 변수로 1회 캐시. foreach 내 `activeToolNames` 계산도 루프 밖으로 호이스트 |
| SubAgentTool 취소 전파 | `SubAgentTool.cs` | `CancellationTokenSource.CreateLinkedTokenSource(ct)` 연동. Task.Run + loop.RunAsync에 토큰 전달. 부모 중지 시 자식 즉시 취소 |
| 아이콘 애니메이션 재귀 제어 | `LauncherWindow.xaml.cs` | `sb.Completed`에서 즉시 재귀 → `_iconAnimationDelayTimer` 8초 딜레이. 할당 빈도 75% 감소. 클릭 시 딜레이 취소 후 즉시 전환 |
| JsonSerializerOptions 공유 | `AgentLoopService.cs` | `s_jsonOpts` 정적 필드 추가, 4개 `JsonSerializer.Serialize` 호출에 적용. L4096 `System.Text.Json.` 접두사 정규화 |
### Claude Desktop 스타일 UI 개선 (2026-04-09)
| 항목 | 파일 | 수정 내용 |
|------|------|----------|
| 미리보기 Split Button | `ChatWindow.xaml` | 기존 `BtnPreviewToggle` (Ellipse 점 + "프리뷰") → `[▶ 미리보기 | ]` Split Button으로 교체. 좌측 토글, 우측 셰브론 드롭다운 |
| 미리보기 드롭다운 | `ChatWindow.PreviewPresentation.cs` | `ShowPreviewTabDropdown()` — 열린 탭 목록 팝업, 파일 확장자별 아이콘, 활성 탭 하이라이트 |
| PreviewDot → PreviewIcon | `ChatWindow.PreviewPresentation.cs` | `PreviewDot.Fill` 4곳 → `PreviewIcon.Foreground` (AccentColor/SecondaryText) 전환 |
| 셰브론 동기화 | `ChatWindow.PreviewPresentation.cs` | `UpdatePreviewChevronState()``_previewTabs.Count` 기반 IsHitTestVisible/Opacity 제어 |
| 계획 버튼 이동 | `ChatWindow.xaml` | MoodIconPanel 동적 주입 → StatusBar XAML 선언 요소 `BtnPlanViewer`로 이동 |
| ShowPlanButton 리팩토링 | `ChatWindow.PlanApprovalPresentation.cs` | 동적 Add/Remove → `Visibility` 토글 단순화 + 레거시 정리 유지 |
### 에이전트 루프 문서 생성 흐름 수정 (2026-04-09)
| 파일 | 수정 내용 |
|------|----------|
| `AgentLoopTransitions.Documents.cs` | `TryHandleTerminalDocumentCompletionTransitionAsync`에서 `document_plan` 없이 바로 문서 도구 호출 시 조기 종료 방지 — LLM이 추가 반복으로 내용을 보강할 수 있도록 허용 |
| `HtmlSkill.cs` | `MarkdownToHtml`에서 LLM이 삽입한 `<br>` 태그가 이스케이프되는 버그 수정 — 이스케이프 전 플레이스홀더로 보존 후 복원 |
---
## 13. 디렉토리별 가이드
| 디렉토리 | 수정 시 주의사항 |
|---------|----------------| |---------|----------------|
| `Core/` | `FuzzyEngine` 점수 공식 변경 시 검색 품질에 직접 영향 | | `Core/` | `FuzzyEngine` ?먯닔 怨듭떇 蹂€寃???寃€???덉쭏??吏곸젒 ?곹뼢 |
| `Handlers/` | 새 핸들러 추가 시 `App.xaml.cs`에 등록 필요 | | `Handlers/` | ???몃뱾??異붽? ??`App.xaml.cs`???깅줉 ?꾩슂 |
| `Services/Agent/` | 새 도구 추가 시 `ToolRegistry`에 등록 + 스킬 파일(`.skill.md`) 작성 | | `Services/Agent/` | ???꾧뎄 異붽? ??`ToolRegistry`???깅줉 + ?ㅽ궗 ?뚯씪(`.skill.md`) ?묒꽦 |
| `Themes/` | 리소스 키 변경 시 모든 테마에 동일하게 적용 필요 | | `Themes/` | 由ъ냼????蹂€寃???紐⑤뱺 ?뚮쭏???숈씪?섍쾶 ?곸슜 ?꾩슂 |
| `Models/AppSettings.cs` | 속성 추가 시 `SettingsService` 마이그레이션 고려 | | `Models/AppSettings.cs` | ?띿꽦 異붽? ??`SettingsService` 留덉씠洹몃젅?댁뀡 怨좊젮 |
| `Views/ChatWindow.*` | partial class 분할 — 관련 기능은 해당 파일에서 수정 | | `Views/ChatWindow.*` | partial class 遺꾪븷 ??愿€??湲곕뒫?€ ?대떦 ?뚯씪?먯꽌 ?섏젙 |
--- ---
## 14. 관련 문서 ## 14. 愿€??臾몄꽌
| 문서 | 내용 | | 臾몄꽌 | ?댁슜 |
|------|------| |------|------|
| `docs/AGENT_ROADMAP.md` | 에이전트 기능 로드맵 | | `docs/AGENT_ROADMAP.md` | ?먯씠?꾪듃 湲곕뒫 濡쒕뱶留?|
| `docs/LAUNCHER_ROADMAP.md` | 런처 기능 로드맵 | | `docs/LAUNCHER_ROADMAP.md` | ?곗쿂 湲곕뒫 濡쒕뱶留?|
| `docs/CLAW_CODE_PARITY_PLAN.md` | Claude Code 기능 대응 계획 | | `docs/CLAW_CODE_PARITY_PLAN.md` | Claude Code 湲곕뒫 ?€??怨꾪쉷 |
| `docs/TOOL_PARITY_REPORT.md` | 도구 호환성 리포트 | | `docs/TOOL_PARITY_REPORT.md` | ?꾧뎄 ?명솚??由ы룷??|
| `docs/AX_AGENT_UI_CHECKLIST.md` | 에이전트 UI 체크리스트 | | `docs/AX_AGENT_UI_CHECKLIST.md` | ?먯씠?꾪듃 UI 泥댄겕由ъ뒪??|
| `docs/UI_UX_CHECKLIST.md` | UI/UX 체크리스트 | | `docs/UI_UX_CHECKLIST.md` | UI/UX 泥댄겕由ъ뒪??|
--- ---
### 선택적 탐색 구조 개선 (2026-04-09 10:36 KST) ### ?좏깮???먯깋 援ъ“ 媛쒖꽑 (2026-04-09 10:36 KST)
- `claude-code``Glob/Grep/FileRead` 프롬프트와 `toolOrchestration.ts` 흐름을 다시 대조한 결과, AX`folder_map`을 너무 쉽게 먼저 호출하도록 유도하는 규칙 때문에 질문과 무관한 전체 워크스페이스를 훑는 경향이 있었습니다. - `claude-code`??`Glob/Grep/FileRead` ?꾨\?꾪듃?€ `toolOrchestration.ts` ?먮쫫???ㅼ떆 ?€議고븳 寃곌낵, AX??`folder_map`???덈Т ?쎄쾶 癒쇱? ?몄텧?섎룄濡??좊룄?섎뒗 洹쒖튃 ?뚮Ц??吏덈Ц怨?臾닿????꾩껜 ?뚰겕?ㅽ럹?댁뒪瑜??묐뒗 寃쏀뼢???덉뿀?듬땲??
- `src/AxCopilot/Views/ChatWindow.xaml.cs` - `src/AxCopilot/Views/ChatWindow.xaml.cs`
- Cowork/Code 시스템 프롬프트에서 `folder_map`을 항상 첫 단계로 요구하던 문구를 완화했습니다. - Cowork/Code ?쒖뒪???꾨\?꾪듃?먯꽌 `folder_map`????긽 泥??④퀎濡??붽뎄?섎뜕 臾멸뎄瑜??꾪솕?덉뒿?덈떎.
- 좁은 범위의 질문은 `glob/grep + targeted file_read`를 우선하고, 저장소 전체 구조가 정말 필요할 때만 `folder_map`을 쓰도록 바꿨습니다. - 醫곸? 踰붿쐞??吏덈Ц?€ `glob/grep + targeted file_read`瑜??곗꽑?섍퀬, ?€?μ냼 ?꾩껜 援ъ“媛€ ?뺣쭚 ?꾩슂???뚮쭔 `folder_map`???곕룄濡?諛붽엥?듬땲??
- `src/AxCopilot/Services/Agent/FolderMapTool.cs` - `src/AxCopilot/Services/Agent/FolderMapTool.cs`
- 기본 탐색 depth`3 -> 2`로 낮췄습니다. - 湲곕낯 ?먯깋 depth瑜?`3 -
- `include_files` 기본값을 `true -> false`로 바꿔 첫 패스에서 구조 확인 위주로 동작하게 했습니다. ## claude-code식 transcript 표시 구조 정리
- `src/AxCopilot/Services/Agent/MultiReadTool.cs`
- 한 번에 읽을 수 있는 최대 파일 수를 `20 -> 8`로 낮춰 초기 과탐색과 토큰 낭비를 줄였습니다. - 업데이트: 2026-04-09 13:05 (KST)
- `src/AxCopilot/Services/Agent/AgentLoopExplorationPolicy.cs` - AgentTranscriptDisplayCatalog를 row presentation 중심으로 재구성해 hinking / waiting / compact / tool activity / permission / tool result / status를 개별 transcript row 의미로 다룰 수 있게 정리했습니다.
- 새 partial 파일을 추가해 탐색 범위를 `Localized / TopicBased / RepoWide / OpenEnded`로 분류하는 정책을 도입했습니다. - ChatWindow.AgentEventRendering은 process feed 계열 이벤트를 같은 GroupKey 단위로 병합해, 긴 Cowork/Code 실행 중 append 수를 줄이면서도 주요 활동 흐름이 기본 transcript에 남도록 조정했습니다.
- `folder_map` 반복, 대량 `multi_read`, broad scan 패턴이 나오면 관련 파일만 다시 고르도록 corrective hint를 주입하는 규칙을 넣었습니다. - PermissionRequestPresentationCatalog, ToolResultPresentationCatalog를 다시 정리해 권한 요청과 도구 결과를 행위/상태 기준으로 구분하고, transcript 렌더와 popup이 같은 메타를 공유하도록 맞췄습니다.
- `src/AxCopilot/Services/Agent/AgentLoopService.cs` - ChatWindow.FooterPresentation은 execution event가 생긴 뒤에는 프리셋 안내 카드를 자동으로 숨기도록 바꿔 결과/진행 화면을 덮지 않게 했고, Cowork/Code 입력 워터마크와 footer 기본 문구도 정상 한국어 기준으로 다시 정리했습니다.
- 루프 시작 시 탐색 범위 가이드를 시스템 메시지로 주입합니다. -
- 도구 실행 중 `folder_map` 호출 수, `multi_read` 파일 수, 총 읽은 파일 수를 추적합니다. ender_messages 성능 로그에는 processFeedAppends, processFeedMerges,
- 좁은 범위 질문에서 broad scan이 감지되면 `탐색 범위를 좁히는 중 · 관련 파일만 다시 선택합니다` 진행 메시지를 남기고 선택적 탐색으로 되돌립니다. owKindCounts를 함께 남겨 %APPDATA%\\AxCopilot\\perf 기준으로 transcript grouping 효과를 수치로 비교할 수 있게 했습니다.
- `src/AxCopilot/Services/AgentPerformanceLogService.cs`
- `%APPDATA%\\AxCopilot\\perf``exploration_breadth` 성능 로그를 남겨 broad scan 여부와 selective hit 여부를 실사용 기준으로 검증할 수 있게 했습니다. ### transcript row 怨꾩빟 諛?activity grouping ?뺣━ (2026-04-09 11:12 KST)
- `claude-code`??`Messages.tsx`, `MessageRow.tsx`, `GroupedToolUseContent.tsx`, `UserToolResultMessage`, `PermissionRequest`瑜??ㅼ떆 ?€議고빐 AX transcript瑜?row ?€??以묒떖?쇰줈 ?뺣━?덉뒿?덈떎.
- `src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs`
- `TranscriptRowKind`, `AgentTranscriptRowPresentation`??異붽??덉뒿?덈떎.
- thinking / waiting / compact / tool activity / permission / tool result / status瑜??섎굹??移댄깉濡쒓렇?먯꽌 ?뺢퇋?뷀븯?꾨줉 諛붽엥?듬땲??
- process feed row??`GroupKey`, `CanGroup`, `Emphasize` 硫뷀?瑜??④퍡 怨꾩궛???뚮뜑?ш? 怨좊퉰???대깽?몃? ???곸? ?됱쑝濡?臾띠쓣 ???덇쾶 ?덉뒿?덈떎.
- `src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs`
- process feed ?뚮뜑媛€ row presentation??吏곸젒 諛쏆븘 ?쒕ぉ/?ㅻ챸/媛뺤“ ?щ?瑜?援ъ꽦?섎룄濡?諛붽엥?듬땲??
- 媛숈? 醫낅쪟??read/search/step ?대깽?몃뒗 留덉?留?grouped row瑜?援먯껜?섎뒗 諛⑹떇?쇰줈 merge?섏뿬 append ?섎? 以꾩??듬땲??
- permission/result card??row 硫뷀??€ presentation catalog瑜???吏곸젒?곸쑝濡??ъ슜?섎룄濡??곌껐?덉뒿?덈떎.
- `src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs`
- ?깃났/?ㅽ뙣/嫄곕?/痍⑥냼/?뱀씤 ?꾩슂/遺€遺??꾨즺瑜?clean??硫뷀? 援ъ“濡??ㅼ떆 ?뺣━?덉뒿?덈떎.
- `src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs`
- bash / powershell / command / web / mcp / skill / ask / file edit / file write / git / document / filesystem???됱쐞蹂?沅뚰븳 移대뱶 硫뷀?濡??ъ젙?섑뻽?듬땲??
- `src/AxCopilot/Views/ChatWindow.TranscriptHost.cs`
- transcript 珥덇린????grouped process feed ?곹깭?€ 移댁슫?곕룄 ?④퍡 由ъ뀑?섎룄濡?蹂닿컯?덉뒿?덈떎.
- `src/AxCopilot/Views/ChatWindow.TranscriptRendering.cs`
- performance log detail??`processFeedAppends`, `processFeedMerges`瑜?異붽???grouped activity row???④낵瑜??ㅼ궗??濡쒓렇?먯꽌 ?뺤씤?????덇쾶 ?덉뒿?덈떎.

View File

@@ -2,6 +2,28 @@ using System;
namespace AxCopilot.Services.Agent; namespace AxCopilot.Services.Agent;
internal enum TranscriptRowKind
{
AssistantText,
Thinking,
ToolActivity,
Permission,
ToolResult,
CompactBoundary,
Waiting,
PlanApproval,
Status,
}
internal sealed record AgentTranscriptRowPresentation(
TranscriptRowKind Kind,
string BadgeLabel,
string Title,
string Description,
string GroupKey,
bool CanGroup,
bool Emphasize);
internal static class AgentTranscriptDisplayCatalog internal static class AgentTranscriptDisplayCatalog
{ {
public static string GetDisplayName(string? rawName, bool slashPrefix = false) public static string GetDisplayName(string? rawName, bool slashPrefix = false)
@@ -15,20 +37,27 @@ internal static class AgentTranscriptDisplayCatalog
{ {
"file_read" => "파일 읽기", "file_read" => "파일 읽기",
"file_write" => "파일 쓰기", "file_write" => "파일 쓰기",
"file_edit" => "파일 편집", "file_edit" => "파일 수정",
"file_watch" => "파일 변경 감시", "file_watch" => "파일 감시",
"file_info" => "파일 정보", "file_info" => "파일 정보",
"file_manage" => "파일 관리", "file_manage" => "파일 관리",
"glob" => "파일 찾기", "glob" => "파일 찾기",
"grep" => "내용 검색", "grep" => "내용 검색",
"folder_map" => "폴더 구조", "folder_map" => "폴더 구조",
"multi_read" => "다중 파일 읽기",
"document_reader" => "문서 읽기", "document_read" or "document_reader" => "문서 읽기",
"document_planner" => "문서 계획", "document_plan" or "document_planner" => "문서 계획",
"document_assembler" => "문서 조합", "document_assemble" or "document_assembler" => "문서 조합",
"document_review" => "문서 검토", "document_review" => "문서 검토",
"format_convert" => "형식 변환", "format_convert" => "형식 변환",
"template_render" => "템플릿 렌더", "template_render" => "템플릿 렌더",
"html_create" => "HTML 생성",
"docx_create" => "Word 생성",
"markdown_create" or "md_create" => "Markdown 생성",
"excel_create" or "xlsx_create" => "Excel 생성",
"csv_create" => "CSV 생성",
"pptx_create" => "PowerPoint 생성",
"build_run" => "빌드/실행", "build_run" => "빌드/실행",
"test_loop" => "테스트 루프", "test_loop" => "테스트 루프",
@@ -42,11 +71,11 @@ internal static class AgentTranscriptDisplayCatalog
"powershell" => "PowerShell", "powershell" => "PowerShell",
"web_fetch" => "웹 요청", "web_fetch" => "웹 요청",
"http" => "HTTP 요청", "http" => "HTTP 요청",
"user_ask" => "의견 요청", "user_ask" => "질문 요청",
"suggest_actions" => "다음 작업 제안", "suggest_actions" => "다음 작업 제안",
"task_create" => "작업 생성", "task_create" => "작업 생성",
"task_update" => "작업 업데이트", "task_update" => "작업 갱신",
"task_list" => "작업 목록", "task_list" => "작업 목록",
"task_get" => "작업 조회", "task_get" => "작업 조회",
"task_stop" => "작업 중지", "task_stop" => "작업 중지",
@@ -104,7 +133,7 @@ internal static class AgentTranscriptDisplayCatalog
AgentEventType.ToolResult => evt.Success ? $"{displayName} 실행 완료" : $"{displayName} 실행 실패", AgentEventType.ToolResult => evt.Success ? $"{displayName} 실행 완료" : $"{displayName} 실행 실패",
AgentEventType.SkillCall => $"{displayName} 실행", AgentEventType.SkillCall => $"{displayName} 실행",
AgentEventType.PermissionRequest => $"{displayName} 실행 전에 권한 확인이 필요합니다.", AgentEventType.PermissionRequest => $"{displayName} 실행 전에 권한 확인이 필요합니다.",
AgentEventType.PermissionGranted => $"{displayName} 실행 권한이 승인되었습니다.", AgentEventType.PermissionGranted => $"{displayName} 실행 권한을 확인했습니다.",
AgentEventType.PermissionDenied => $"{displayName} 실행 권한이 거부되었습니다.", AgentEventType.PermissionDenied => $"{displayName} 실행 권한이 거부되었습니다.",
AgentEventType.Complete => "에이전트 작업이 완료되었습니다.", AgentEventType.Complete => "에이전트 작업이 완료되었습니다.",
AgentEventType.Error => "에이전트 실행 중 오류가 발생했습니다.", AgentEventType.Error => "에이전트 실행 중 오류가 발생했습니다.",
@@ -112,6 +141,257 @@ internal static class AgentTranscriptDisplayCatalog
}; };
} }
public static AgentTranscriptRowPresentation ResolveRowPresentation(AgentEvent evt, string itemDisplayName, string transcriptBadgeLabel)
{
var summary = (evt.Summary ?? string.Empty).Trim();
var toolName = (evt.ToolName ?? string.Empty).Trim().ToLowerInvariant();
var resultPresentation = evt.Type == AgentEventType.ToolResult
? ToolResultPresentationCatalog.Resolve(evt, transcriptBadgeLabel)
: null;
var permissionPresentation = evt.Type switch
{
AgentEventType.PermissionRequest => PermissionRequestPresentationCatalog.Resolve(evt.ToolName, pending: true),
AgentEventType.PermissionGranted => PermissionRequestPresentationCatalog.Resolve(evt.ToolName, pending: false),
_ => null
};
if (string.Equals(toolName, "agent_wait", StringComparison.OrdinalIgnoreCase))
{
return new AgentTranscriptRowPresentation(
TranscriptRowKind.Waiting,
"대기",
"처리 중...",
string.IsNullOrWhiteSpace(summary) ? "작업을 계속 진행하기 위한 응답을 기다리는 중입니다." : summary,
"waiting:agent",
true,
true);
}
if (string.Equals(toolName, "context_compaction", StringComparison.OrdinalIgnoreCase))
{
return new AgentTranscriptRowPresentation(
TranscriptRowKind.CompactBoundary,
"압축",
"컨텍스트 압축 중...",
string.IsNullOrWhiteSpace(summary) ? "긴 대화를 계속 진행하기 위해 컨텍스트를 정리하고 있습니다." : summary,
"compact:context",
true,
true);
}
if (evt.Type == AgentEventType.Planning)
{
var title = evt.Steps is { Count: > 0 }
? $"계획 {evt.Steps.Count}단계 정리"
: "작업 계획 정리 중";
return new AgentTranscriptRowPresentation(
TranscriptRowKind.Thinking,
"계획",
title,
string.IsNullOrWhiteSpace(summary) ? "실행 순서와 필요한 도구를 정리하고 있습니다." : summary,
"planning",
true,
false);
}
if (evt.Type == AgentEventType.Decision)
{
return new AgentTranscriptRowPresentation(
TranscriptRowKind.PlanApproval,
"확인",
"계획 확인 필요",
string.IsNullOrWhiteSpace(summary) ? "사용자 확인이 필요한 계획 단계입니다." : summary,
"plan:approval",
false,
true);
}
if (evt.Type is AgentEventType.StepStart or AgentEventType.StepDone)
{
var title = evt.StepTotal > 0
? evt.Type == AgentEventType.StepStart
? $"{evt.StepCurrent}/{evt.StepTotal} 단계 진행"
: $"{evt.StepCurrent}/{evt.StepTotal} 단계 완료"
: evt.Type == AgentEventType.StepStart ? "단계 진행" : "단계 완료";
return new AgentTranscriptRowPresentation(
TranscriptRowKind.ToolActivity,
"단계",
title,
string.IsNullOrWhiteSpace(summary) ? title : summary,
$"step:{evt.StepCurrent}:{evt.StepTotal}:{evt.Type}",
false,
false);
}
if (evt.Type == AgentEventType.Thinking)
{
var title = ResolveThinkingTitle(summary, toolName);
return new AgentTranscriptRowPresentation(
TranscriptRowKind.Thinking,
"생각",
title,
string.IsNullOrWhiteSpace(summary) ? title : summary,
$"thinking:{ResolveActivityGroup(toolName, summary)}",
true,
false);
}
if (evt.Type == AgentEventType.ToolCall || evt.Type == AgentEventType.SkillCall)
{
var group = ResolveActivityGroup(toolName, summary);
var title = BuildActivityTitle(toolName, itemDisplayName, summary);
var badge = evt.Type == AgentEventType.SkillCall ? "스킬" : "도구";
return new AgentTranscriptRowPresentation(
TranscriptRowKind.ToolActivity,
badge,
title,
BuildActivityDescription(group, summary),
$"activity:{group}",
true,
false);
}
if (permissionPresentation != null)
{
return new AgentTranscriptRowPresentation(
TranscriptRowKind.Permission,
"권한",
permissionPresentation.Label,
permissionPresentation.Description,
$"permission:{permissionPresentation.Kind}",
false,
true);
}
if (resultPresentation != null)
{
return new AgentTranscriptRowPresentation(
TranscriptRowKind.ToolResult,
"결과",
resultPresentation.Label,
resultPresentation.Description,
$"result:{resultPresentation.Kind}:{resultPresentation.StatusKind}",
false,
resultPresentation.NeedsAttention);
}
if (evt.Type == AgentEventType.Error)
{
return new AgentTranscriptRowPresentation(
TranscriptRowKind.Status,
"오류",
"실행 중 오류 발생",
string.IsNullOrWhiteSpace(summary) ? "에이전트 실행 중 오류가 발생했습니다." : summary,
"status:error",
false,
true);
}
if (evt.Type == AgentEventType.Complete)
{
return new AgentTranscriptRowPresentation(
TranscriptRowKind.Status,
"완료",
"작업 완료",
string.IsNullOrWhiteSpace(summary) ? "에이전트 작업이 완료되었습니다." : summary,
"status:complete",
false,
false);
}
return new AgentTranscriptRowPresentation(
TranscriptRowKind.Status,
transcriptBadgeLabel,
string.IsNullOrWhiteSpace(summary) ? transcriptBadgeLabel : summary,
string.IsNullOrWhiteSpace(summary) ? transcriptBadgeLabel : summary,
$"status:{evt.Type}",
false,
false);
}
private static string ResolveThinkingTitle(string summary, string toolName)
{
if (summary.Contains("검증", StringComparison.OrdinalIgnoreCase))
return "결과 검증 중...";
if (summary.Contains("diff", StringComparison.OrdinalIgnoreCase))
return "변경 내용 확인 중...";
if (summary.Contains("retry", StringComparison.OrdinalIgnoreCase) || summary.Contains("재시도", StringComparison.OrdinalIgnoreCase))
return "재시도 준비 중...";
if (summary.Contains("fallback", StringComparison.OrdinalIgnoreCase) || summary.Contains("자동 생성", StringComparison.OrdinalIgnoreCase))
return "대체 경로 준비 중...";
if (toolName.Contains("document"))
return "문서 흐름 정리 중...";
return string.IsNullOrWhiteSpace(summary) ? "분석 중..." : summary;
}
private static string ResolveActivityGroup(string toolName, string summary)
{
if (toolName is "file_read" or "document_read" or "glob" or "grep" or "folder_map" or "multi_read")
return "read";
if (toolName is "file_edit" or "file_write")
return "edit";
if (toolName.Contains("build") || toolName.Contains("test") || toolName == "process")
return "execute";
if (toolName.Contains("git") || toolName.Contains("diff"))
return "git";
if (toolName.Contains("document") || toolName.Contains("html_create") || toolName.Contains("docx_create") || toolName.Contains("markdown_create"))
return "document";
if (toolName.Contains("web") || toolName.Contains("http"))
return "web";
if (summary.Contains("권한", StringComparison.OrdinalIgnoreCase))
return "permission";
return "general";
}
private static string BuildActivityTitle(string toolName, string itemDisplayName, string summary)
{
if (toolName is "multi_read")
return "여러 파일 읽는 중";
if (toolName is "file_read" or "document_read")
return "파일 읽는 중";
if (toolName is "glob")
return "관련 파일 찾는 중";
if (toolName is "grep")
return "코드 검색 중";
if (toolName is "folder_map")
return "구조 확인 중";
if (toolName is "file_edit")
return "파일 수정 중";
if (toolName is "file_write")
return "파일 작성 중";
if (toolName.Contains("build") || toolName.Contains("test"))
return "빌드/테스트 실행 중";
if (toolName == "process" || toolName == "bash" || toolName == "powershell")
return "명령 실행 중";
if (toolName.Contains("git") || toolName.Contains("diff"))
return "Git 작업 중";
if (toolName.Contains("document") || toolName.Contains("html_create") || toolName.Contains("docx_create"))
return "문서 결과 만드는 중";
if (toolName.Contains("web") || toolName.Contains("http"))
return "웹 정보 확인 중";
if (!string.IsNullOrWhiteSpace(itemDisplayName))
return $"{itemDisplayName} 실행 중";
return string.IsNullOrWhiteSpace(summary) ? "도구 실행 중" : summary;
}
private static string BuildActivityDescription(string group, string summary)
{
if (!string.IsNullOrWhiteSpace(summary))
return summary;
return group switch
{
"read" => "질문과 관련된 파일과 내용만 추려서 확인하고 있습니다.",
"edit" => "필요한 변경만 적용하고 결과를 다시 확인하고 있습니다.",
"execute" => "실행 결과와 로그를 확인해 다음 단계를 판단하고 있습니다.",
"git" => "변경 범위와 저장소 상태를 확인하고 있습니다.",
"document" => "문서 산출물을 준비하고 있습니다.",
"web" => "필요한 외부 정보를 확인하고 있습니다.",
_ => "다음 단계를 진행하기 위한 작업을 실행하고 있습니다."
};
}
private static string GetToolCategoryLabel(string? rawName) private static string GetToolCategoryLabel(string? rawName)
{ {
if (string.IsNullOrWhiteSpace(rawName)) if (string.IsNullOrWhiteSpace(rawName))
@@ -119,13 +399,13 @@ internal static class AgentTranscriptDisplayCatalog
return rawName.Trim().ToLowerInvariant() switch return rawName.Trim().ToLowerInvariant() switch
{ {
"file_read" or "file_write" or "file_edit" or "glob" or "grep" or "folder_map" or "file_watch" or "file_info" or "file_manage" "file_read" or "file_write" or "file_edit" or "glob" or "grep" or "folder_map" or "file_watch" or "file_info" or "file_manage" or "multi_read"
=> "파일", => "파일",
"build_run" or "test_loop" or "dev_env_detect" "build_run" or "test_loop" or "dev_env_detect"
=> "빌드", => "빌드",
"git_tool" or "diff_tool" or "diff_preview" "git_tool" or "diff_tool" or "diff_preview"
=> "Git", => "Git",
"document_reader" or "document_planner" or "document_assembler" or "document_review" or "format_convert" or "template_render" "document_read" or "document_reader" or "document_plan" or "document_planner" or "document_assemble" or "document_assembler" or "document_review" or "format_convert" or "template_render" or "html_create" or "docx_create"
=> "문서", => "문서",
"user_ask" "user_ask"
=> "질문", => "질문",

View File

@@ -1,3 +1,5 @@
using System;
namespace AxCopilot.Services.Agent; namespace AxCopilot.Services.Agent;
internal sealed record PermissionRequestPresentation( internal sealed record PermissionRequestPresentation(
@@ -20,104 +22,104 @@ internal static class PermissionRequestPresentationCatalog
if (tool.Contains("bash")) if (tool.Contains("bash"))
return Build("bash", pending, "\uE756", return Build("bash", pending, "\uE756",
"Bash 실행 권한 요청", "Bash 실행 확인", "Bash 실행 권한 요청", "Bash 실행 확인",
" 명령을 실행하기 전에 확인이 필요합니다.", "Bash 명령을 실행하기 전에 사용자 확인이 필요합니다.",
"Bash 실행이 승인되어 계속 진행합니다.", "Bash 실행을 확인해 주시면 같은 흐름으로 이어집니다.",
"명령과 작업 위치를 확인하세요.", "명령과 작업 위치를 먼저 확인해 주세요.",
"#FEF2F2", "#DC2626", "high", false); "#FEF2F2", "#DC2626", "high", false);
if (tool.Contains("powershell")) if (tool.Contains("powershell"))
return Build("powershell", pending, "\uE756", return Build("powershell", pending, "\uE756",
"PowerShell 권한 요청", "PowerShell 실행 확인", "PowerShell 권한 요청", "PowerShell 실행 확인",
"PowerShell 명령을 실행하기 전에 확인이 필요합니다.", "PowerShell 명령을 실행하기 전에 사용자 확인이 필요합니다.",
"PowerShell 실행 승인되어 계속 진행합니다.", "PowerShell 실행 승인하면 같은 작업을 이어서 진행합니다.",
"스크립트와 실행 범위를 확인세요.", "스크립트와 실행 범위를 확인해 주세요.",
"#FEF2F2", "#DC2626", "high", false); "#FEF2F2", "#DC2626", "high", false);
if (tool.Contains("process") || tool.Contains("build") || tool.Contains("test")) if (tool.Contains("process") || tool.Contains("build") || tool.Contains("test"))
return Build("command", pending, "\uE756", return Build("command", pending, "\uE756",
"명령 실행 권한 요청", "명령 실행 확인", "명령 실행 권한 요청", "명령 실행 확인",
"명령 또는 빌드 작업 실행 전에 확인이 필요합니다.", "명령이나 빌드 작업 실행하기 전에 확인이 필요합니다.",
"명령 실행 승인되어 계속 진행합니다.", "명령 실행 승인하면 현재 흐름을 이어서 진행합니다.",
"실행 명령과 영향 범위를 확인하세요.", "실행 명령과 영향 범위를 먼저 확인해 주세요.",
"#FEF2F2", "#DC2626", "high", false); "#FEF2F2", "#DC2626", "high", false);
if (tool.Contains("web") || tool.Contains("fetch") || tool.Contains("http")) if (tool.Contains("web") || tool.Contains("fetch") || tool.Contains("http"))
return Build("web_fetch", pending, "\uE774", return Build("web_fetch", pending, "\uE774",
"웹 요청 권한 요청", "웹 요청 확인", "웹 요청 권한 요청", "웹 요청 확인",
"외부 요청을 보내기 전에 확인이 필요합니다.", "외부 요청을 보내기 전에 확인이 필요합니다.",
"웹 요청이 승인되어 계속 진행합니다.", "요청 대상을 확인해 주시면 이어서 처리합니다.",
"조회 URL과 전송 범위를 확인세요.", "조회 URL과 전송 범위를 확인해 주세요.",
"#FFF7ED", "#C2410C", "medium", false); "#FFF7ED", "#C2410C", "medium", false);
if (tool.Contains("mcp")) if (tool.Contains("mcp"))
return Build("mcp", pending, "\uE943", return Build("mcp", pending, "\uE943",
"MCP 도구 권한 요청", "MCP 도구 확인", "MCP 도구 권한 요청", "MCP 도구 확인",
"연결된 MCP 도구 사용 전에 확인이 필요합니다.", "연결된 MCP 도구 사용하기 전에 확인이 필요합니다.",
"MCP 도구 사용 승인되어 계속 진행합니다.", "MCP 도구 사용 승인하면 흐름을 이어서 진행합니다.",
"서버와 호출 도를 확인세요.", "서버와 호출 도를 확인해 주세요.",
"#F5F3FF", "#7C3AED", "medium", false); "#F5F3FF", "#7C3AED", "medium", false);
if (tool.Contains("skill")) if (tool.Contains("skill"))
return Build("skill", pending, "\uE8A5", return Build("skill", pending, "\uE8A5",
"스킬 실행 권한 요청", "스킬 실행 확인", "스킬 실행 권한 요청", "스킬 실행 확인",
"연결된 스킬을 실행하기 전에 확인이 필요합니다.", "연결된 스킬을 실행하기 전에 확인이 필요합니다.",
"스킬 실행 승인되어 계속 진행합니다.", "스킬 실행 승인하면 같은 작업 흐름을 이어갑니다.",
"용 도구와 실행 컨텍스트를 확인세요.", "용 도구와 실행 컨텍스트를 확인해 주세요.",
"#F5F3FF", "#7C3AED", "medium", false); "#F5F3FF", "#7C3AED", "medium", false);
if (tool.Contains("ask")) if (tool.Contains("ask"))
return Build("question", pending, "\uE897", return Build("question", pending, "\uE897",
"의견 요청 확인", "의견 요청 완료", "질문 요청 확인", "질문 요청 완료",
"사용자에게 선택이나 답변을 요청합니다.", "사용자에게 추가 질문이나 선택을 요청합니다.",
"사용자 답변을 받 다음 단계로 진행합니다.", "응답을 받으면 다음 단계로 자동으로 이어집니다.",
"질문 의도와 선택지를 확인세요.", "질문 의도와 선택지를 확인해 주세요.",
"#EFF6FF", "#2563EB", "low", false); "#EFF6FF", "#2563EB", "low", false);
if (tool.Contains("file_edit") || tool.Contains("edit")) if (tool.Contains("file_edit") || tool.Contains("edit"))
return Build("file_edit", pending, "\uE70F", return Build("file_edit", pending, "\uE70F",
"파일 수정 권한 요청", "파일 수정 확인", "파일 수정 권한 요청", "파일 수정 확인",
"파일을 변경하기 전에 확인이 필요합니다.", "파일을 변경하기 전에 사용자 확인이 필요합니다.",
"파일 수정이 승인되어 계속 진행합니다.", "변경 내용을 확인하면 같은 흐름으로 이어집니다.",
"변경 diff와 대상 파일을 확인하세요.", "변경 diff와 대상 파일을 먼저 확인해 주세요.",
"#FFF7ED", "#C2410C", "high", true); "#FFF7ED", "#C2410C", "high", true);
if (tool.Contains("file_write") || tool.Contains("write")) if (tool.Contains("file_write") || tool.Contains("write"))
return Build("file_write", pending, "\uE70F", return Build("file_write", pending, "\uE70F",
"파일 쓰기 권한 요청", "파일 쓰기 확인", "파일 쓰기 권한 요청", "파일 쓰기 확인",
"새 파일 작성 또는 덮어쓰기 전에 확인이 필요합니다.", "새 파일 생성이나 덮어쓰기 전에 사용자 확인이 필요합니다.",
"파일 쓰기 승인되어 계속 진행합니다.", "파일 쓰기 승인하면 같은 흐름을 이어갑니다.",
"성 위치와 내용 미리보기를 확인세요.", "성 위치와 내용 미리보기를 확인해 주세요.",
"#FFF7ED", "#C2410C", "high", true); "#FFF7ED", "#C2410C", "high", true);
if (tool.Contains("git")) if (tool.Contains("git"))
return Build("git", pending, "\uE8A7", return Build("git", pending, "\uE8A7",
"Git 작업 권한 요청", "Git 작업 확인", "Git 작업 권한 요청", "Git 작업 확인",
"브랜치나 커밋 상태를 바꾸기 전에 확인이 필요합니다.", "브랜치나 작업트리 상태를 바꾸기 전에 확인이 필요합니다.",
"Git 작업 승인되어 계속 진행합니다.", "Git 작업 승인하면 같은 흐름을 이어갑니다.",
"브랜치와 변경 범위를 확인세요.", "브랜치와 변경 범위를 확인해 주세요.",
"#EFF6FF", "#2563EB", "medium", false); "#EFF6FF", "#2563EB", "medium", false);
if (tool.Contains("document") || tool.Contains("template") || tool.Contains("format")) if (tool.Contains("document") || tool.Contains("template") || tool.Contains("format"))
return Build("document", pending, "\uE8A5", return Build("document", pending, "\uE8A5",
"문서 작업 권한 요청", "문서 작업 확인", "문서 작업 권한 요청", "문서 작업 확인",
"문서 생성 또는 변환 작업 전에 확인이 필요합니다.", "문서 생성이나 변환 작업 전에 확인이 필요합니다.",
"문서 작업 승인되어 계속 진행합니다.", "문서 작업 승인하면 같은 흐름을 이어갑니다.",
"출력 형식과 저장 위치를 확인세요.", "출력 형식과 대상 위치를 확인해 주세요.",
"#FFF7ED", "#C2410C", "medium", false); "#FFF7ED", "#C2410C", "medium", false);
if (tool.Contains("file") || tool.Contains("glob") || tool.Contains("grep") || tool.Contains("folder")) if (tool.Contains("file") || tool.Contains("glob") || tool.Contains("grep") || tool.Contains("folder"))
return Build("filesystem", pending, "\uE8A5", return Build("filesystem", pending, "\uE8A5",
"파일 접근 권한 요청", "파일 접근 확인", "파일 접근 권한 요청", "파일 접근 확인",
"폴더나 파일 내용을 읽기 전에 확인이 필요합니다.", "파일이나 폴더를 읽기 전에 확인이 필요합니다.",
"파일 접근 승인되어 계속 진행합니다.", "파일 접근 승인하면 같은 흐름으로 이어집니다.",
"읽기 범위와 접근 경로를 확인세요.", "읽기 범위와 경로를 확인해 주세요.",
"#FFF7ED", "#C2410C", "medium", false); "#FFF7ED", "#C2410C", "medium", false);
return Build("generic", pending, "\uE897", return Build("generic", pending, "\uE897",
"권한 요청", "권한 확인", "권한 요청", "권한 확인",
"계속 진행하기 전에 사용자 확인이 필요합니다.", "계속 진행하기 전에 사용자 확인이 필요합니다.",
"요청 승인되어 계속 진행합니다.", "요청 승인하면 같은 흐름으로 이어집니다.",
"실행 의도와 대상 범위를 확인세요.", "실행 의도와 대상 범위를 확인해 주세요.",
"#FFF7ED", "#C2410C", "medium", false); "#FFF7ED", "#C2410C", "medium", false);
} }

View File

@@ -1,3 +1,5 @@
using System;
namespace AxCopilot.Services.Agent; namespace AxCopilot.Services.Agent;
internal sealed record ToolResultPresentation( internal sealed record ToolResultPresentation(
@@ -29,7 +31,7 @@ internal static class ToolResultPresentationCatalog
"\uE711", "\uE711",
$"{baseLabel} 취소", $"{baseLabel} 취소",
"요청이 중단되어 결과가 취소되었습니다.", "요청이 중단되어 결과가 취소되었습니다.",
"필요하면 같은 요청을 다시 실행하세요.", "필요하면 같은 요청을 다시 실행할 수 있습니다.",
"#F8FAFC", "#F8FAFC",
"#475569", "#475569",
"cancel", "cancel",
@@ -45,7 +47,7 @@ internal static class ToolResultPresentationCatalog
"\uE783", "\uE783",
$"{baseLabel} 거부", $"{baseLabel} 거부",
"권한이 거부되어 작업이 중단되었습니다.", "권한이 거부되어 작업이 중단되었습니다.",
"권한 모드를 바꾸거나 다시 인하면 이어서 진행할 수 있습니다.", "권한 모드를 바꾸거나 다시 인하면 이어서 진행할 수 있습니다.",
"#FEF2F2", "#FEF2F2",
"#DC2626", "#DC2626",
"reject", "reject",
@@ -58,8 +60,8 @@ internal static class ToolResultPresentationCatalog
return new ToolResultPresentation( return new ToolResultPresentation(
kind, kind,
"\uE8D7", "\uE8D7",
$"{baseLabel} 승인 대기", $"{baseLabel} 승인 필요",
"다음 단계로 진행하려면 사용자 승인이 필요합니다.", "다음 단계로 진행하려면 사용자 승인이나 확인이 필요합니다.",
"승인 후 같은 작업 흐름이 이어집니다.", "승인 후 같은 작업 흐름이 이어집니다.",
"#FFF7ED", "#FFF7ED",
"#C2410C", "#C2410C",
@@ -74,8 +76,8 @@ internal static class ToolResultPresentationCatalog
kind, kind,
"\uE7BA", "\uE7BA",
$"{baseLabel} 부분 완료", $"{baseLabel} 부분 완료",
"일부 단계만 완료되어 후속 확인이나 재실행이 필요할 수 있습니다.", "일부 단계만 완료되어 후속 확인이나 재시도가 필요할 수 있습니다.",
"남은 단계나 누락된 결과를 확인세요.", "후속 단계와 파일 결과를 확인해 주세요.",
"#FFFBEA", "#FFFBEA",
"#A16207", "#A16207",
"partial", "partial",
@@ -114,7 +116,7 @@ internal static class ToolResultPresentationCatalog
return "file_edit"; return "file_edit";
if (tool.Contains("file_write")) if (tool.Contains("file_write"))
return "file_write"; return "file_write";
if (tool.Contains("file_read") || tool.Contains("glob") || tool.Contains("grep")) if (tool.Contains("file_read") || tool.Contains("glob") || tool.Contains("grep") || tool.Contains("folder_map") || tool.Contains("multi_read"))
return "filesystem"; return "filesystem";
if (tool.Contains("file")) if (tool.Contains("file"))
return "file"; return "file";
@@ -141,111 +143,93 @@ internal static class ToolResultPresentationCatalog
return "generic"; return "generic";
} }
private static string BuildSuccessLabel(string kind, string baseLabel) private static string BuildSuccessLabel(string kind, string baseLabel) => kind switch
{ {
return kind switch "file_edit" => "파일 수정 완료",
{ "file_write" => "파일 쓰기 완료",
"file_edit" => "파일 수정 완료", "filesystem" => "파일 탐색 완료",
"file_write" => "파일 쓰기 완료", "file" => "파일 작업 완료",
"filesystem" => "파일 탐색 완료", "build_test" => "빌드/테스트 완료",
"file" => "파일 작업 완료", "git" => "Git 작업 완료",
"build_test" => "빌드/테스트 완료", "document" => "문서 작업 완료",
"git" => "Git 작업 완료", "skill" => "스킬 실행 완료",
"document" => "문서 작업 완료", "mcp" => "MCP 도구 완료",
"skill" => "스킬 실행 완료", "question" => "질문 요청 완료",
"mcp" => "MCP 도구 완료", "web" => "웹 요청 완료",
"question" => "의견 요청 완료", "command" => "명령 실행 완료",
"web" => "웹 요청 완료", _ => baseLabel,
"command" => "명령 실행 완료", };
_ => baseLabel,
};
}
private static string BuildFailureLabel(string kind, string baseLabel) private static string BuildFailureLabel(string kind, string baseLabel) => kind switch
{ {
return kind switch "file_edit" => "파일 수정 실패",
{ "file_write" => "파일 쓰기 실패",
"file_edit" => "파일 수정 실패", "filesystem" => "파일 탐색 실패",
"file_write" => "파일 쓰기 실패", "file" => "파일 작업 실패",
"filesystem" => "파일 탐색 실패", "build_test" => "빌드/테스트 실패",
"file" => "파일 작업 실패", "git" => "Git 작업 실패",
"build_test" => "빌드/테스트 실패", "document" => "문서 작업 실패",
"git" => "Git 작업 실패", "skill" => "스킬 실행 실패",
"document" => "문서 작업 실패", "mcp" => "MCP 도구 실패",
"skill" => "스킬 실행 실패", "question" => "질문 요청 실패",
"mcp" => "MCP 도구 실패", "web" => "웹 요청 실패",
"question" => "의견 요청 실패", "command" => "명령 실행 실패",
"web" => "웹 요청 실패", _ => $"{baseLabel} 실패",
"command" => "명령 실행 실패", };
_ => $"{baseLabel} 실패",
};
}
private static string BuildSuccessDescription(string kind) private static string BuildSuccessDescription(string kind) => kind switch
{ {
return kind switch "file_edit" => "파일 수정 결과가 정상적으로 반영되었습니다.",
{ "file_write" => "새 파일 생성이나 쓰기 작업이 완료되었습니다.",
"file_edit" => "파일 수정 결과가 저장되었습니다.", "filesystem" => "질문과 관련된 파일과 구조 정보를 찾았습니다.",
"file_write" => "파일 작성 결과가 저장되었습니다.", "file" => "파일 관련 작업이 정상적으로 끝났습니다.",
"filesystem" => "파일과 폴더 정보를 성공적으로 읽었습니다.", "build_test" => "빌드나 테스트 단계가 정상적으로 끝났습니다.",
"file" => "파일 관련 작업이 정상적으로 끝났습니다.", "git" => "저장소 상태 확인 또는 변경 작업이 완료되었습니다.",
"build_test" => "빌드 또는 테스트 단계가 성공적으로 끝났습니다.", "document" => "문서 산출물 생성 또는 조립이 완료되었습니다.",
"git" => "Git 관련 작업이 정상적으로 끝났습니다.", "skill" => "선택한 스킬이 정상적으로 실행되었습니다.",
"document" => "문서 생성 또는 변환 작업이 완료되었습니다.", "mcp" => "등록된 MCP 도구 호출이 완료되었습니다.",
"skill" => "선택한 스킬이 정상적으로 실행되었습니다.", "question" => "사용자 응답을 받아 다음 단계로 이어갈 수 있습니다.",
"mcp" => "등록된 MCP 도구 호출이 성공적으로 끝났습니다.", "web" => "필요한 웹 정보 조회가 완료되었습니다.",
"question" => "사용자 응답을 받아 다음 단계로 넘어갈 수 있습니다.", "command" => "명령 실행이 완료되어 결과를 확인할 수 있습니다.",
"web" => "요청이 정상적으로 끝났습니다.", _ => "요청한 작업이 정상적으로 완료되었습니다.",
"command" => "명령 실행이 정상적으로 끝났습니다.", };
_ => "요청한 작업이 정상적으로 완료되었습니다.",
};
}
private static string BuildFailureDescription(string kind) private static string BuildFailureDescription(string kind) => kind switch
{ {
return kind switch "file_edit" => "파일 변경 과정에서 문제가 발생했습니다.",
{ "file_write" => "파일 생성 또는 쓰기 과정에서 문제가 발생했습니다.",
"file_edit" => "파일 변경 과정에서 문제가 발생했습니다.", "filesystem" => "파일이나 폴더를 확인하는 과정에서 문제가 발생했습니다.",
"file_write" => "파일 작성 또는 저장 과정에서 문제가 발생했습니다.", "file" => "파일 처리 중 문제가 발생했습니다.",
"filesystem" => "파일/폴더 접근 중 문제가 발생했습니다.", "build_test" => "빌드 또는 테스트 단계에서 실패가 발생했습니다.",
"file" => "파일 처리문제가 발생했습니다.", "git" => "Git 작업오류가 발생했습니다.",
"build_test" => "빌드 또는 테스트 단계에서 실패가 발생했습니다.", "document" => "문서 생성 또는 조합 과정이 실패했습니다.",
"git" => "Git 관련 작업이 실패했습니다.", "skill" => "스킬 실행 중 문제가 발생했습니다.",
"document" => "문서 생성 또는 변환 작업이 실패했습니다.", "mcp" => "MCP 도구 호출 중 문제가 발생했습니다.",
"skill" => "스킬 실행 중 문제가 발생했습니다.", "question" => "사용자 질문/응답 처리 단계에서 문제가 발생했습니다.",
"mcp" => "MCP 도구 호출 중 문제가 발생했습니다.", "web" => "웹 요청 처리에 실패했습니다.",
"question" => "사용자 의견 요청 과정에서 문제가 발생했습니다.", "command" => "명령 실행 중 오류가 발생했습니다.",
"web" => "웹 요청 처리에 실패했습니다.", _ => "작업 처리 중 오류가 발생했습니다.",
"command" => "명령 실행 중 오류가 발생했습니다.", };
_ => "작업 처리 중 오류가 발생했습니다.",
};
}
private static string BuildSuccessFollowUp(string kind) private static string BuildSuccessFollowUp(string kind) => kind switch
{ {
return kind switch "file_edit" or "file_write" => "변경 내용은 preview나 diff에서 다시 확인할 수 있습니다.",
{ "build_test" => "출력 로그와 후속 수정 필요 여부를 확인해 주세요.",
"file_edit" or "file_write" => "변경 내용을 preview나 diff에서 다시 확인할 수 있습니다.", "git" => "브랜치와 변경 수치를 이어서 확인해 주세요.",
"build_test" => "출력 로그와 후속 수정 필요 여부를 확인세요.", "document" => "생성된 산출물 경로를 열어 결과를 확인해 주세요.",
"git" => "브랜치 상태나 변경 요약을 이어서 확인하세요.", "skill" => "같은 스킬을 다른 입력으로 이어서 실행할 수 있습니다.",
"document" => "생성된 산출물 경로를 열어 결과를 확인하세요.", _ => "필요하면 다음 요청으로 이어서 작업할 수 있습니다.",
"skill" => "같은 스킬을 다른 입력으로 이어서 실행할 수 있습니다.", };
_ => "필요하면 후속 요청을 이어서 실행할 수 있습니다.",
};
}
private static string BuildFailureFollowUp(string kind) private static string BuildFailureFollowUp(string kind) => kind switch
{ {
return kind switch "file_edit" or "file_write" => "대상 파일 경로와 권한, diff 결과를 다시 확인해 주세요.",
{ "build_test" => "실패 로그와 컴파일/테스트 오류 메시지를 먼저 확인해 주세요.",
"file_edit" or "file_write" => "대상 파일 경로와 권한, diff를 다시 확인세요.", "git" => "브랜치 상태와 충돌 여부를 다시 확인해 주세요.",
"build_test" => "실패 로그와 컴파일 오류 메시지를 먼저 확인하세요.", "document" => "입력 데이터, 출력 형식, 대상 위치를 다시 확인해 주세요.",
"git" => "현재 브랜치, 잠금 상태, 충돌 여부를 확인하세요.", "skill" => "사용 도구와 스킬 요구사항을 다시 확인해 주세요.",
"document" => "입력 데이터와 출력 형식, 저장 위치를 다시 확인세요.", "web" => "연결 상태와 요청 URL을 다시 확인해 주세요.",
"skill" => "허용 도구와 런타임 요구사항을 다시 확인세요.", "mcp" => "MCP 서버 연결 상태와 도구 등록 상태를 다시 확인해 주세요.",
"web" => "연결 상태와 요청 대상 URL을 다시 확인하세요.", _ => "같은 요청을 다시 시도하기 전에 원인 메시지를 먼저 확인해 주세요.",
"mcp" => "MCP 서버 연결 상태와 도구 등록 상태를 다시 확인하세요.", };
_ => "같은 요청을 재시도하기 전에 원인 메시지를 먼저 확인하세요.",
};
}
} }

View File

@@ -11,6 +11,12 @@ namespace AxCopilot.Views;
public partial class ChatWindow public partial class ChatWindow
{ {
private string? _lastGroupedProcessFeedKey;
private int _lastGroupedProcessFeedIndex = -1;
private int _processFeedAppendCount;
private int _processFeedMergeCount;
private readonly Dictionary<TranscriptRowKind, int> _transcriptRowKindCounts = new();
private static Color ResolveLiveProgressAccentColor(Brush accentBrush) private static Color ResolveLiveProgressAccentColor(Brush accentBrush)
{ {
return accentBrush is SolidColorBrush solid return accentBrush is SolidColorBrush solid
@@ -164,8 +170,24 @@ public partial class ChatWindow
}; };
} }
private void AddProcessFeedMessage(AgentEvent evt, string transcriptBadgeLabel, string itemDisplayName, string? eventSummaryText) private void ResetProcessFeedGrouping()
{ {
_lastGroupedProcessFeedKey = null;
_lastGroupedProcessFeedIndex = -1;
}
private void TrackTranscriptRowKind(TranscriptRowKind kind)
{
if (_transcriptRowKindCounts.TryGetValue(kind, out var count))
_transcriptRowKindCounts[kind] = count + 1;
else
_transcriptRowKindCounts[kind] = 1;
}
private void AddProcessFeedMessage(AgentEvent evt, AgentTranscriptRowPresentation rowPresentation, string? eventSummaryText)
{
TrackTranscriptRowKind(rowPresentation.Kind);
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray;
var hintBg = TryFindResource("HintBackground") as Brush var hintBg = TryFindResource("HintBackground") as Brush
@@ -174,9 +196,9 @@ public partial class ChatWindow
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
var processMeta = BuildReadableProgressMetaText(evt); var processMeta = BuildReadableProgressMetaText(evt);
var summary = BuildReadableProcessFeedSummary(evt, transcriptBadgeLabel, itemDisplayName).Trim(); var summary = rowPresentation.Title.Trim();
if (string.IsNullOrWhiteSpace(summary)) if (string.IsNullOrWhiteSpace(summary))
summary = transcriptBadgeLabel; summary = rowPresentation.BadgeLabel;
var msgMaxWidth = GetMessageMaxWidth(); var msgMaxWidth = GetMessageMaxWidth();
var stack = new StackPanel var stack = new StackPanel
@@ -203,7 +225,7 @@ public partial class ChatWindow
ApplyLiveWaitingPulseToMarker(pulseMarker); ApplyLiveWaitingPulseToMarker(pulseMarker);
stack.Children.Add(summaryRow); stack.Children.Add(summaryRow);
var body = (eventSummaryText ?? string.Empty).Trim(); var body = (string.IsNullOrWhiteSpace(eventSummaryText) ? rowPresentation.Description : eventSummaryText ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(body) if (!string.IsNullOrWhiteSpace(body)
&& !string.Equals(body, summary, StringComparison.OrdinalIgnoreCase)) && !string.Equals(body, summary, StringComparison.OrdinalIgnoreCase))
{ {
@@ -241,7 +263,21 @@ public partial class ChatWindow
stack.Children.Add(compactPathRow); stack.Children.Add(compactPathRow);
} }
AddTranscriptElement(stack); if (rowPresentation.CanGroup &&
!string.IsNullOrWhiteSpace(rowPresentation.GroupKey) &&
string.Equals(_lastGroupedProcessFeedKey, rowPresentation.GroupKey, StringComparison.Ordinal) &&
_lastGroupedProcessFeedIndex >= 0)
{
ReplaceTranscriptElement(_lastGroupedProcessFeedIndex, stack);
_processFeedMergeCount++;
}
else
{
AddTranscriptElement(stack);
_lastGroupedProcessFeedIndex = Math.Max(0, GetTranscriptElementCount() - 1);
_lastGroupedProcessFeedKey = rowPresentation.CanGroup ? rowPresentation.GroupKey : null;
_processFeedAppendCount++;
}
} }
private static void ApplyLiveWaitingPulse(Border summaryRow) private static void ApplyLiveWaitingPulse(Border summaryRow)
@@ -1151,6 +1187,9 @@ public partial class ChatWindow
var toolResultPresentation = evt.Type == AgentEventType.ToolResult var toolResultPresentation = evt.Type == AgentEventType.ToolResult
? ToolResultPresentationCatalog.Resolve(evt, transcriptBadgeLabel) ? ToolResultPresentationCatalog.Resolve(evt, transcriptBadgeLabel)
: null; : null;
var rowPresentation = AgentTranscriptDisplayCatalog.ResolveRowPresentation(evt, itemDisplayName: evt.Type == AgentEventType.SkillCall
? GetAgentItemDisplayName(evt.ToolName, slashPrefix: true)
: GetAgentItemDisplayName(evt.ToolName), transcriptBadgeLabel);
var (icon, label, bgHex, fgHex) = isTotalStats var (icon, label, bgHex, fgHex) = isTotalStats
? ("\uE9D2", "전체 통계", "#F3EEFF", "#7C3AED") ? ("\uE9D2", "전체 통계", "#F3EEFF", "#7C3AED")
@@ -1179,11 +1218,39 @@ public partial class ChatWindow
{ {
eventSummaryText = evt.Type switch eventSummaryText = evt.Type switch
{ {
AgentEventType.PermissionRequest or AgentEventType.PermissionGranted => permissionPresentation?.Description ?? "", AgentEventType.PermissionRequest or AgentEventType.PermissionGranted => permissionPresentation?.Description ?? rowPresentation.Description,
AgentEventType.ToolResult => toolResultPresentation?.Description ?? "", AgentEventType.ToolResult => toolResultPresentation?.Description ?? rowPresentation.Description,
_ => "" _ => rowPresentation.Description
}; };
} }
else if (string.IsNullOrWhiteSpace(rowPresentation.Description) == false &&
string.Equals(eventSummaryText, evt.Summary, StringComparison.OrdinalIgnoreCase))
{
eventSummaryText = rowPresentation.Description;
}
if (IsProcessFeedEvent(evt))
{
AddProcessFeedMessage(evt, rowPresentation, eventSummaryText);
return;
}
ResetProcessFeedGrouping();
TrackTranscriptRowKind(rowPresentation.Kind);
if (rowPresentation.Kind == TranscriptRowKind.ToolResult && toolResultPresentation != null)
{
eventSummaryText = rowPresentation.Description;
}
else if (rowPresentation.Kind == TranscriptRowKind.Permission && permissionPresentation != null)
{
eventSummaryText = rowPresentation.Description;
}
if (string.IsNullOrWhiteSpace(label) && !string.IsNullOrWhiteSpace(rowPresentation.BadgeLabel))
label = rowPresentation.BadgeLabel;
if (string.IsNullOrWhiteSpace(eventSummaryText))
eventSummaryText = rowPresentation.Description;
// HTML/대용량 파일 내용이 이벤트 요약에 포함된 경우 인라인 표시 대신 1줄 요약으로 축소 // HTML/대용량 파일 내용이 이벤트 요약에 포함된 경우 인라인 표시 대신 1줄 요약으로 축소
if (!string.IsNullOrWhiteSpace(eventSummaryText) && evt.Type == AgentEventType.ToolResult if (!string.IsNullOrWhiteSpace(eventSummaryText) && evt.Type == AgentEventType.ToolResult
@@ -1212,12 +1279,6 @@ public partial class ChatWindow
if (evt.Type == AgentEventType.StepStart && evt.StepTotal > 0) if (evt.Type == AgentEventType.StepStart && evt.StepTotal > 0)
UpdateProgressBar(evt); UpdateProgressBar(evt);
if (IsProcessFeedEvent(evt))
{
AddProcessFeedMessage(evt, transcriptBadgeLabel, itemDisplayName, eventSummaryText);
return;
}
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray;
var hintBg = TryFindResource("HintBackground") as Brush ?? BrushFromHex("#F8FAFC"); var hintBg = TryFindResource("HintBackground") as Brush ?? BrushFromHex("#F8FAFC");

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -47,9 +47,9 @@ public partial class ChatWindow
return preset.Description.Trim(); return preset.Description.Trim();
if (string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase)) if (string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase))
return "선택 작업 유형에 맞춰 문서·데이터·파일 작업 흐름으로 이어집니다."; return "선택 작업 유형에 맞춰 문서, 데이터, 파일 작업 흐름으로 이어집니다.";
return "선택 대화 주제에 맞춰 응답 방향과 초안 흐름을 정리합니다."; return "선택 대화 주제에 맞춰 응답 방향과 초안 흐름을 정리합니다.";
} }
private void UpdateFolderBar() private void UpdateFolderBar()
@@ -74,7 +74,7 @@ public partial class ChatWindow
} }
else else
{ {
FolderPathLabel.Text = "폴더를 선택하세요"; FolderPathLabel.Text = "폴더를 선택하세요.";
FolderPathLabel.ToolTip = null; FolderPathLabel.ToolTip = null;
} }
@@ -129,7 +129,7 @@ public partial class ChatWindow
memory.Load(workFolder); memory.Load(workFolder);
var docs = memory.InstructionDocuments; var docs = memory.InstructionDocuments;
var learned = memory.All.Count; var learned = memory.All.Count;
var includePolicy = _settings.Settings.Llm.AllowExternalMemoryIncludes ? "외부 include 허용" : "외부 include 차단"; var includePolicy = _settings.Settings.Llm.AllowExternalMemoryIncludes ? "?몃? include ?덉슜" : "?몃? include 李⑤떒";
var auditEnabled = _settings.Settings.Llm.EnableAuditLog; var auditEnabled = _settings.Settings.Llm.EnableAuditLog;
var recentIncludeEntries = AuditLogService.LoadRecent("MemoryInclude", maxCount: 5, daysBack: 3); var recentIncludeEntries = AuditLogService.LoadRecent("MemoryInclude", maxCount: 5, daysBack: 3);
@@ -143,7 +143,7 @@ public partial class ChatWindow
var panel = new StackPanel { Margin = new Thickness(2) }; var panel = new StackPanel { Margin = new Thickness(2) };
panel.Children.Add(new TextBlock panel.Children.Add(new TextBlock
{ {
Text = "메모리 상태", Text = "硫붾え由??곹깭",
FontSize = 13, FontSize = 13,
FontWeight = FontWeights.SemiBold, FontWeight = FontWeights.SemiBold,
Foreground = primaryText, Foreground = primaryText,
@@ -151,7 +151,7 @@ public partial class ChatWindow
}); });
panel.Children.Add(new TextBlock panel.Children.Add(new TextBlock
{ {
Text = $"계층형 규칙 {docs.Count}개 · 학습 메모리 {learned}개 · {includePolicy}", Text = $"怨꾩링??洹쒖튃 {docs.Count}媛?쨌 ?숈뒿 硫붾え由?{learned}媛?쨌 {includePolicy}",
FontSize = 11.5, FontSize = 11.5,
Foreground = secondaryText, Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap, TextWrapping = TextWrapping.Wrap,
@@ -163,7 +163,7 @@ public partial class ChatWindow
panel.Children.Add(CreateSurfacePopupSeparator()); panel.Children.Add(CreateSurfacePopupSeparator());
panel.Children.Add(new TextBlock panel.Children.Add(new TextBlock
{ {
Text = "적용 중 규칙", Text = "?곸슜 以?洹쒖튃",
FontSize = 12, FontSize = 12,
FontWeight = FontWeights.SemiBold, FontWeight = FontWeights.SemiBold,
Foreground = primaryText, Foreground = primaryText,
@@ -177,7 +177,7 @@ public partial class ChatWindow
{ {
panel.Children.Add(new TextBlock panel.Children.Add(new TextBlock
{ {
Text = $"{docs.Count - 6}개 규칙", Text = $"??{docs.Count - 6}媛?洹쒖튃",
FontSize = 11, FontSize = 11,
Foreground = secondaryText, Foreground = secondaryText,
Margin = new Thickness(8, 2, 8, 4), Margin = new Thickness(8, 2, 8, 4),
@@ -188,7 +188,7 @@ public partial class ChatWindow
panel.Children.Add(CreateSurfacePopupSeparator()); panel.Children.Add(CreateSurfacePopupSeparator());
panel.Children.Add(new TextBlock panel.Children.Add(new TextBlock
{ {
Text = "최근 include 감사", Text = "理쒓렐 include 媛먯궗",
FontSize = 12, FontSize = 12,
FontWeight = FontWeights.SemiBold, FontWeight = FontWeights.SemiBold,
Foreground = primaryText, Foreground = primaryText,
@@ -199,7 +199,7 @@ public partial class ChatWindow
{ {
panel.Children.Add(new TextBlock panel.Children.Add(new TextBlock
{ {
Text = "감사 로그가 꺼져 있어 include 이력은 기록되지 않습니다.", Text = "媛먯궗 濡쒓렇媛€ 爰쇱졇 ?덉뼱 include ?대젰?€ 湲곕줉?섏? ?딆뒿?덈떎.",
FontSize = 11, FontSize = 11,
Foreground = secondaryText, Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap, TextWrapping = TextWrapping.Wrap,
@@ -210,7 +210,7 @@ public partial class ChatWindow
{ {
panel.Children.Add(new TextBlock panel.Children.Add(new TextBlock
{ {
Text = "최근 3일간 include 감사 기록이 없습니다.", Text = "理쒓렐 3?쇨컙 include 媛먯궗 湲곕줉???놁뒿?덈떎.",
FontSize = 11, FontSize = 11,
Foreground = secondaryText, Foreground = secondaryText,
Margin = new Thickness(8, 0, 8, 6), Margin = new Thickness(8, 0, 8, 6),
@@ -245,7 +245,7 @@ public partial class ChatWindow
return path; return path;
var directory = Path.GetDirectoryName(path); var directory = Path.GetDirectoryName(path);
return string.IsNullOrWhiteSpace(directory) ? fileName : $"{fileName} · {directory}"; return string.IsNullOrWhiteSpace(directory) ? fileName : $"{fileName} {directory}";
} }
catch catch
{ {
@@ -266,14 +266,14 @@ public partial class ChatWindow
var stack = new StackPanel { Margin = new Thickness(8, 2, 8, 4) }; var stack = new StackPanel { Margin = new Thickness(8, 2, 8, 4) };
stack.Children.Add(new TextBlock stack.Children.Add(new TextBlock
{ {
Text = $"[{doc.Label}] 우선순위 {doc.Priority}", Text = $"[{doc.Label}] ?곗꽑?쒖쐞 {doc.Priority}",
FontSize = 11.5, FontSize = 11.5,
FontWeight = FontWeights.SemiBold, FontWeight = FontWeights.SemiBold,
Foreground = primaryText, Foreground = primaryText,
}); });
stack.Children.Add(new TextBlock stack.Children.Add(new TextBlock
{ {
Text = meta.Count == 0 ? doc.Layer : $"{doc.Layer} · {string.Join(" · ", meta)}", Text = meta.Count == 0 ? doc.Layer : $"{doc.Layer} {string.Join(" ", meta)}",
FontSize = 10.5, FontSize = 10.5,
Foreground = secondaryText, Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap, TextWrapping = TextWrapping.Wrap,
@@ -297,13 +297,13 @@ public partial class ChatWindow
private Border BuildMemoryPopupAuditRow(AuditEntry entry, Brush primaryText, Brush secondaryText, Brush okBrush, Brush warnBrush, Brush dangerBrush) private Border BuildMemoryPopupAuditRow(AuditEntry entry, Brush primaryText, Brush secondaryText, Brush okBrush, Brush warnBrush, Brush dangerBrush)
{ {
var statusBrush = entry.Success ? okBrush : dangerBrush; var statusBrush = entry.Success ? okBrush : dangerBrush;
var statusText = entry.Success ? "허용" : "차단"; var statusText = entry.Success ? "?덉슜" : "李⑤떒";
var resultBrush = entry.Success ? secondaryText : warnBrush; var resultBrush = entry.Success ? secondaryText : warnBrush;
var stack = new StackPanel { Margin = new Thickness(8, 2, 8, 4) }; var stack = new StackPanel { Margin = new Thickness(8, 2, 8, 4) };
stack.Children.Add(new TextBlock stack.Children.Add(new TextBlock
{ {
Text = $"{statusText} · {entry.Timestamp:HH:mm:ss}", Text = $"{statusText} {entry.Timestamp:HH:mm:ss}",
FontSize = 11.5, FontSize = 11.5,
FontWeight = FontWeights.SemiBold, FontWeight = FontWeights.SemiBold,
Foreground = statusBrush, Foreground = statusBrush,
@@ -359,8 +359,9 @@ public partial class ChatWindow
!string.IsNullOrWhiteSpace(m.Content) && !string.IsNullOrWhiteSpace(m.Content) &&
(string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase) || (string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase) ||
string.Equals(m.Role, "assistant", StringComparison.OrdinalIgnoreCase))) == true; string.Equals(m.Role, "assistant", StringComparison.OrdinalIgnoreCase))) == true;
var hasVisibleExecution = conversation?.ExecutionEvents?.Count > 0;
if (string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase) || hasVisibleMessages || _isStreaming) if (string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase) || hasVisibleMessages || hasVisibleExecution || _isStreaming)
{ {
SelectedPresetGuide.Visibility = Visibility.Collapsed; SelectedPresetGuide.Visibility = Visibility.Collapsed;
SelectedPresetGuideTitle.Text = ""; SelectedPresetGuideTitle.Text = "";
@@ -394,3 +395,4 @@ public partial class ChatWindow
SelectedPresetGuide.Visibility = Visibility.Visible; SelectedPresetGuide.Visibility = Visibility.Visible;
} }
} }

View File

@@ -117,6 +117,11 @@ public partial class ChatWindow
{ {
_transcriptElements.Clear(); _transcriptElements.Clear();
_transcriptElementMap.Clear(); _transcriptElementMap.Clear();
_lastGroupedProcessFeedKey = null;
_lastGroupedProcessFeedIndex = -1;
_processFeedAppendCount = 0;
_processFeedMergeCount = 0;
_transcriptRowKindCounts.Clear();
} }
private void RemoveTranscriptElement(UIElement element) private void RemoveTranscriptElement(UIElement element)

View File

@@ -1,4 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Windows.Threading; using System.Windows.Threading;
using AxCopilot.Models; using AxCopilot.Models;
using AxCopilot.Services; using AxCopilot.Services;
@@ -95,6 +96,11 @@ public partial class ChatWindow
renderedItems = renderPlan.NewKeys.Count, renderedItems = renderPlan.NewKeys.Count,
hiddenCount = renderPlan.HiddenCount, hiddenCount = renderPlan.HiddenCount,
transcriptElements = GetTranscriptElementCount(), transcriptElements = GetTranscriptElementCount(),
processFeedAppends = _processFeedAppendCount,
processFeedMerges = _processFeedMergeCount,
rowKindCounts = _transcriptRowKindCounts.ToDictionary(
pair => pair.Key.ToString(),
pair => pair.Value),
}); });
} }