Compare commits
97 Commits
0fd6940975
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ec0ed7fb1c | |||
| 571d4bfaca | |||
| fdf95aa6ec | |||
| 7fd4d1ae5a | |||
| e747032501 | |||
| f11b8b74b7 | |||
| b4d69f5db3 | |||
| 3c3faab528 | |||
| 9aa99cdfe6 | |||
| 3ac8a7155f | |||
| 3e44f1fc4d | |||
| fa431f1666 | |||
| fda5c6bace | |||
| b3b5f8a79d | |||
| 8faa26b134 | |||
| 1b4566d192 | |||
| d0d66c1d52 | |||
| 9464dd0234 | |||
| d5c1266d3e | |||
| 3924bac9f9 | |||
| 9f5a9d315c | |||
| 4c1513a5da | |||
| ccaa24745e | |||
| 1ce6ccb030 | |||
| 2b21e8cdfb | |||
| 35ec073eb9 | |||
| 68524c1c94 | |||
| b4a506de96 | |||
| 82b42b3ba3 | |||
| 90bd77f945 | |||
| 95e40df354 | |||
| f9d18fba08 | |||
| f0af86cc1e | |||
| 13f0e23ed5 | |||
| 7cb27b70f8 | |||
| 61f82bdd10 | |||
| fa349c2057 | |||
| be7328184a | |||
| 905ea41ed3 | |||
| d0fa54f10e | |||
| 1948af3cc4 | |||
| 53965083e3 | |||
| ac4aada0af | |||
| 53afdb3472 | |||
| 5765888229 | |||
| 2958306caf | |||
| 216b050398 | |||
| 5352ca2ab2 | |||
| 929c1e9f05 | |||
| bfa1e342c0 | |||
| 8331c0eedc | |||
| cf59a010ac | |||
| 1c9b13c14f | |||
| 87c05720ce | |||
| 25031d655d | |||
| 793a301353 | |||
| a5790e28fb | |||
| 00d284b725 | |||
| 891133a6bf | |||
| b1e11b27bc | |||
| f79e657895 | |||
| 1778b855c5 | |||
| db957039d4 | |||
| 78905d16c0 | |||
| 2975bb39a2 | |||
| 5e63f13cf3 | |||
| 1f581454e1 | |||
| cdd99fd4d2 | |||
| b44df996c2 | |||
| 7f8a075553 | |||
| eed87db268 | |||
| cfacb903e1 | |||
| 3198f822f5 | |||
| 21dc280e57 | |||
| 28e88d615f | |||
| 36828ba199 | |||
| 8dc2841da6 | |||
| 4cbe60052e | |||
| 6c5b0c5be3 | |||
| a3b3522bb7 | |||
| 3ba7c52980 | |||
| 854f190531 | |||
| a0ce5846e1 | |||
| 1eb56404c7 | |||
| d6bfca249e | |||
| 12746cdf11 | |||
| 6b645ccbb7 | |||
| 51a398d269 | |||
| 595f8a76af | |||
| 050271e2a9 | |||
| 4184d89168 | |||
| 303a23130b | |||
| 78b962bece | |||
| f8e62bde2a | |||
| a315f587bf | |||
| 500c8ffb06 | |||
| b53af39358 |
311
README.md
311
README.md
@@ -7,6 +7,72 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
|
||||
개발 참고: Claw Code 동등성 작업 추적 문서
|
||||
`docs/claw-code-parity-plan.md`
|
||||
|
||||
- 업데이트: 2026-04-06 09:27 (KST)
|
||||
- `claw-code`와 AX Agent를 다시 대조해 남은 품질 향상 작업을 3트랙으로 재정리했습니다. 앞으로 계획은 `사용자 체감 UI/UX`, `LLM·작업 처리`, `유지보수·추가기능 구조`로 분리해 관리합니다.
|
||||
- `docs/claw-code-parity-plan.md`에 각 트랙별 참조 파일, AX 적용 위치, 완료 조건, 품질 판정 기준, 권장 실행 순서를 고정했습니다.
|
||||
|
||||
- 업데이트: 2026-04-06 09:14 (KST)
|
||||
- AX Agent 워크트리 선택 팝업과 공통 선택 row 렌더를 `ChatWindow.SelectionPopupPresentation.cs`로 분리했습니다. 작업 위치/워크트리 전환 메뉴와 선택 상태 row 조립이 메인 창 코드 밖으로 이동해 footer 선택 UX를 별도 파일에서 정리할 수 있게 됐습니다.
|
||||
- `ChatWindow.xaml.cs`는 대화 상태와 세션 orchestration 쪽에 더 집중하도록 정리했고, 향후 브랜치/워크트리/선택형 팝업 UX를 `claw-code` 기준으로 계속 다듬기 쉬운 구조를 만들었습니다.
|
||||
|
||||
- 업데이트: 2026-04-06 09:03 (KST)
|
||||
- AX Agent 공통 선택 팝업 조립 로직을 `ChatWindow.PopupPresentation.cs`로 분리했습니다. 테마 팝업 컨테이너, 공통 메뉴 아이템, 구분선, 최근 폴더 우클릭 컨텍스트 메뉴가 메인 창 코드 밖으로 이동해 footer/file-browser 쪽 팝업 품질 작업을 이어가기 쉬운 구조로 정리했습니다.
|
||||
- `ChatWindow.xaml.cs`는 대화 상태와 런타임 orchestration 쪽에 더 집중하도록 정리했고, 공통 팝업 시각 언어를 한 곳에서 다듬을 수 있는 기반을 만들었습니다.
|
||||
|
||||
- 업데이트: 2026-04-06 08:55 (KST)
|
||||
- AX Agent 파일 브라우저 렌더를 `ChatWindow.FileBrowserPresentation.cs`로 분리했습니다. 파일 탐색기 열기/닫기, 폴더 트리 구성, 파일 헤더/아이콘/크기 표시, 우클릭 메뉴, 디바운스 새로고침 흐름이 메인 창 코드 밖으로 이동했습니다.
|
||||
- `ChatWindow.xaml.cs`는 transcript·runtime orchestration 중심으로 더 정리됐고, claw-code 기준 사이드 surface 품질 작업을 이어가기 쉬운 구조로 맞췄습니다.
|
||||
|
||||
- 업데이트: 2026-04-06 08:47 (KST)
|
||||
- AX Agent 우측 프리뷰 패널 렌더를 `ChatWindow.PreviewPresentation.cs`로 분리했습니다. 프리뷰 탭 목록, 헤더, 파일 로드, CSV/텍스트/마크다운/HTML 표시, 숨김/열기, 우클릭 메뉴, 별도 창 미리보기 흐름이 메인 창 코드 밖으로 이동했습니다.
|
||||
- `ChatWindow.xaml.cs`는 transcript 및 런타임 orchestration 중심으로 더 정리됐고, claw-code 기준 preview surface 품질 작업을 이어가기 쉬운 구조로 맞췄습니다.
|
||||
|
||||
- 업데이트: 2026-04-06 08:39 (KST)
|
||||
- AX Agent 하단 상태바 이벤트 처리와 회전 애니메이션을 `ChatWindow.StatusPresentation.cs`로 옮겼습니다. `UpdateStatusBar`, `StartStatusAnimation`, `StopStatusAnimation`이 상태 표현 파일로 이동해 메인 창 코드의 runtime/status 분기가 더 줄었습니다.
|
||||
- `ChatWindow.xaml.cs`는 대화 실행 orchestration 중심으로 더 정리됐고, claw-code 기준 status line 정교화와 footer presentation 개선을 계속 이어가기 쉬운 구조로 맞췄습니다.
|
||||
|
||||
- 업데이트: 2026-04-06 08:27 (KST)
|
||||
- AX Agent 메시지 액션/메타/편집 렌더를 `ChatWindow.MessageInteractions.cs`로 분리했습니다. 좋아요·싫어요 피드백 버튼, 응답 메타 텍스트, 메시지 등장 애니메이션, 사용자 메시지 편집·재생성 흐름이 메인 창 코드 밖으로 이동했습니다.
|
||||
- `ChatWindow.xaml.cs`는 transcript 오케스트레이션과 상태 흐름에 더 집중하도록 정리했고, claw-code 기준 메시지 타입 분리와 renderer 구조화를 계속 진행하기 쉬운 기반을 만들었습니다.
|
||||
|
||||
- 업데이트: 2026-04-06 08:28 (KST)
|
||||
- AX Agent 하단 composer와 대기열 UI 렌더를 `ChatWindow.ComposerQueuePresentation.cs`로 분리했습니다. 입력창 높이 계산, draft kind 해석, 후속 요청 큐 카드/요약 pill/배지/액션 버튼 생성 책임을 메인 창 코드에서 떼어냈습니다.
|
||||
- `ChatWindow.xaml.cs`는 대기열 실행 orchestration과 세션 변경 흐름만 더 선명하게 남겨, claw-code 기준 입력부/queued command UX 개선을 계속하기 쉬운 구조로 정리했습니다.
|
||||
|
||||
- 업데이트: 2026-04-06 08:12 (KST)
|
||||
- AX Agent 하단 작업 바 관련 presentation 메서드를 메인 창 코드에서 더 분리했습니다. `ChatWindow.FooterPresentation.cs`를 추가해 폴더 바, 선택된 프리셋 안내, Git 브랜치 버튼/팝업 렌더, 요약 pill 생성 책임을 별도 partial로 옮겼습니다.
|
||||
- `ChatWindow.xaml.cs`는 대화 흐름과 런타임 orchestration 중심으로 더 정리했고, claw-code 기준으로 footer/preset/Git popup 품질 작업을 계속 이어가기 쉬운 구조를 만들었습니다.
|
||||
|
||||
- 업데이트: 2026-04-06 01:37 (KST)
|
||||
- AX Agent의 계획 승인 흐름을 더 transcript 우선 구조로 정리했습니다. 인라인 승인 카드가 기본 경로를 맡고, `계획` 버튼은 저장된 계획 요약/단계를 여는 상세 보기 역할만 하도록 분리했습니다.
|
||||
- 상태선과 quick strip 계산도 presentation state로 더 모았습니다. runtime badge, compact strip, quick strip의 텍스트/강조색/노출 여부를 한 번에 계산해 창 코드의 직접 분기를 줄였습니다.
|
||||
|
||||
- 업데이트: 2026-04-06 07:31 (KST)
|
||||
- `ChatWindow.xaml.cs`에 몰려 있던 권한 팝업 렌더와 컨텍스트 사용량 카드 렌더를 별도 partial 파일로 분리했습니다. `ChatWindow.PermissionPresentation.cs`, `ChatWindow.ContextUsagePresentation.cs`를 추가해 권한 선택/권한 상태 배너/컨텍스트 사용량 hover 카드 책임을 메인 창 orchestration 코드에서 떼어냈습니다.
|
||||
- 다음 단계에서 `permission / tool-result / footer` presentation catalog를 더 세밀하게 확장하기 쉽게 구조를 정리했고, 동작은 그대로 유지한 채 transcript/푸터 품질 개선 발판을 마련했습니다.
|
||||
|
||||
- 업데이트: 2026-04-06 00:50 (KST)
|
||||
- 채팅/코워크 프리셋 카드의 hover 설명 레이어를 카드 내부 오버레이 방식에서 안정적인 tooltip형 설명으로 바꿨습니다. 카드 배경/테두리만 반응하게 정리해 hover 시 반복 깜빡임을 줄였습니다.
|
||||
|
||||
- 업데이트: 2026-04-06 00:45 (KST)
|
||||
- AX Agent 내부 설정 공통 탭에 `대화 스타일` 섹션 제목을 복구해, `문서 형태`와 `디자인 스타일` 저장 항목이 명확히 보이도록 정리했습니다.
|
||||
- AX Agent 내부 설정 스킬 탭의 `MCP 서버` 영역에 `서버 추가` 버튼을 복원했고, 목록 카드 안에서 `활성화/비활성화`와 `삭제`까지 바로 관리할 수 있게 옮겼습니다.
|
||||
|
||||
- 업데이트: 2026-04-05 19:04 (KST)
|
||||
- AX Agent transcript와 작업 요약에서 도구/스킬 이름을 사람이 읽기 쉬운 표시명으로 정리했습니다. raw snake_case 도구명 대신 `파일 읽기`, `빌드/실행`, `Git`, `/bug-hunt` 같은 표시명 중심으로 보입니다.
|
||||
- 도구/스킬 이벤트 배지는 역할 중심(`도구`, `도구 결과`, `스킬`)으로 단순화하고, 실제 도구명은 본문 보조 텍스트로만 노출되게 바꿨습니다.
|
||||
- 작업 요약 팝업의 권한/배경 작업 관측성 섹션은 `debug` 성격일 때만 두껍게 보여서 기본 UX가 더 `claw-code`처럼 조용하게 유지됩니다.
|
||||
|
||||
- 업데이트: 2026-04-05 18:58 (KST)
|
||||
- AX Agent의 `의견 요청`/`질문` 흐름을 transcript 내 카드 우선 구조로 전환했습니다.
|
||||
- `user_ask` 도구는 별도 `UserAskDialog`를 먼저 띄우지 않고, 본문 안에서 선택지/직접 입력/전달로 응답을 완료합니다.
|
||||
- 계획 승인과 사용자 질문이 같은 transcript-first UX 원칙을 따르도록 정리했습니다.
|
||||
|
||||
- 업데이트: 2026-04-05 22:04 (KST)
|
||||
- `claw-code`와 AX Agent를 같은 기준으로 비교할 수 있도록 canonical prompt set 10종을 parity 문서에 고정했습니다. Chat 기본/장문, Cowork 문서/데이터, Code 수정/빌드, queue follow-up, post-compaction, permission 승인, slash skill 진입까지 핵심 회귀 흐름을 한 세트로 검증하도록 정리했습니다.
|
||||
- 도구/스킬 비교 기준도 parity 문서에 추가했습니다. AX는 문서/오피스/데이터/업무형 도구가 더 풍부하고, `claw-code`는 transcript-native tool/approval/permission 메시지 구조가 더 정교하다는 차이를 명시했습니다.
|
||||
- 계획 승인 UX는 `claw-code` 쪽 흐름에 더 가깝게 inline 우선으로 바꿨습니다. AX Agent는 이제 승인 요청 시 `PlanViewerWindow`를 먼저 띄우지 않고 transcript 내부의 승인/수정/취소 버튼을 우선 보여주며, 계획 상세는 하단 `계획` 버튼으로만 여는 보조 경로를 사용합니다.
|
||||
|
||||
- 업데이트: 2026-04-05 16:55 (KST)
|
||||
- `claw-code` 대비 AX Agent 추정 진척율 기준선을 문서에 남겼습니다. 현재 기준은 핵심 엔진 `82%`, 채팅 메인 UI `68%`, Cowork/Code 상태 UX `63%`, 내부 설정 연결 `88%`, 전체 AX Agent 동등 품질 `74%`입니다.
|
||||
- 메인 핵심 엔진 로직에 직접 영향을 주는 설정은 최소화 원칙으로 다시 검토하기 시작했습니다. 이미 실질 선택지가 사라진 `계획 모드` 계열은 사용자 노출을 더 줄였고, 남은 엔진성 설정은 개발자 탭 중심으로 계속 정리합니다.
|
||||
@@ -897,6 +963,99 @@ ow + toggle 시각 언어로 통일했습니다.
|
||||
- 탭 래퍼 배경은 `LauncherBackground` 기준으로 바꾸고, 선택 탭은 같은 계열 배경 + 얇은 테두리가 보이도록 바꿨습니다. 라벨도 `채팅 / Cowork / 코드`로 맞춰 이전보다 읽기 쉬운 상단 내비로 정리했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 19:16 (KST)
|
||||
- `claw-code`의 `StatusLine + PromptInput + Messages` 기준으로 AX Agent 보조 상태 노출을 한 단계 더 줄였습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서 `ConversationQuickStrip` 은 단순 카운트가 있을 때 자동 노출되지 않고, 사용자가 실제로 `진행 중만 보기` 또는 정렬 전환을 켰을 때만 보이도록 바꿨습니다.
|
||||
- 같은 파일에서 상단 `ConversationStatusStrip` 은 `권한 대기 / 실패 / 권한 거부`만 유지하고, `queue`, `queue_blocked` 같은 보조 상태는 기본 화면에서 숨기도록 정리했습니다. Cowork/Code transcript가 queue 상태 배너로 과하게 시끄럽던 부분을 줄이기 위한 변경입니다.
|
||||
- `AgentEventType.Planning` 도 더 `claw-code`처럼 기본 transcript에서는 큰 계획 카드 대신 얇은 compact pill만 남기도록 바꿨습니다. 이제 debug가 아니면 계획이 카드 형태로 본문을 밀어내지 않습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 19:24 (KST)
|
||||
- AX Agent 좌측 패널과 작업 유형 카드의 크기를 다시 키웠습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 사이드바 폭을 `246`으로 넓히고 `새 대화`, `검색`, 필터 드롭다운, 탭별 메뉴의 폰트/패딩/아이콘을 전반적으로 키워 첫 번째 레퍼런스 이미지처럼 더 읽기 쉽게 정리했습니다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `BuildTopicButtons()` 에서는 작업 유형 카드 크기를 `148 x 124`로 늘리고, 아이콘 원형과 제목·hover 설명 라벨 폰트도 함께 키워 현재 화면 글자 크기가 너무 작게 보이던 문제를 보정했습니다.
|
||||
- 하단 컨텍스트 사용량 UI도 카드형에서 심볼형으로 바꿨습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 `TokenUsageCard` 는 작은 원형 심볼만 남기고, 상세 정보는 `TokenUsagePopup` 커스텀 팝업으로 분리했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서는 hover 진입/이탈 시 팝업을 제어하고, `RefreshContextUsageVisual()` 이 심볼 상태와 팝업 텍스트를 함께 갱신하도록 연결했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 19:38 (KST)
|
||||
- `claw-code` 대비 AX Agent 남은 차이와 제거 후보 설정도 다시 정리했습니다. [docs/claw-code-parity-plan.md](/E:/AX%20Copilot%20-%20Codex/docs/claw-code-parity-plan.md)에 현재 추정 진척율(`core engine 89% / main UI 96% / runtime UX 92% / overall 93%`), 남은 엔진/UI 차이, 런타임 영향 설정 정리안을 기록했습니다.
|
||||
- 같은 점검에서 작업유형 카드 hover 깜박임 원인은 `hover 라벨`과 기본 WPF `ToolTip`이 동시에 켜져 마우스가 카드 경계와 툴팁 사이를 오가며 상태가 흔들리는 구조로 확인됐습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서 프리셋/기타/프리셋 추가 카드의 기본 `ToolTip` 을 제거하고 hover 라벨만 남겨 깜박임을 줄였습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 19:42 (KST)
|
||||
- `PlanMode` 잔재도 런타임 기준으로 한 단계 더 걷어냈습니다. [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs) 에서 실행 전 plan prelude 진입은 비활성 플래그 기준으로만 남기고, 기본 실행 경로가 `planMode` 값에 의해 흔들리지 않도록 정리했습니다.
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 과 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서는 숨김 상태였던 inline/overlay 계획 모드 click/selection 잔재를 제거해 dead UI code를 더 줄였습니다. 사용자 노출 정책은 그대로 `off` 고정입니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 19:49 (KST)
|
||||
- AX Agent 프리셋 첫 화면 레이아웃을 다시 정리했습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 `EmptyState` 를 `Stretch + ScrollViewer` 구조로 바꿔 Chat/Cowork 프리셋 카드 하단이 잘리던 문제를 줄였고, `TopicPresetScrollViewer` 최대 높이와 내부 패딩도 함께 키웠습니다.
|
||||
- 같은 파일에 헤더 중앙 `SelectedPresetGuide` 를 추가해, 대화 주제나 작업 유형을 선택하면 선택된 항목과 설명이 다시 상단 중앙에 안내되도록 복구했습니다. 상단 탭 pill 그룹도 배경/패딩/폰트를 다시 키워 이전보다 더 또렷하게 보이도록 보정했습니다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에 `UpdateSelectedPresetGuide(...)` 를 추가하고 `UpdateChatTitle()`, `SelectTopic(...)` 에 연결했습니다. 이제 프리셋 선택 직후뿐 아니라 대화 재오픈/탭 전환 시에도 선택된 주제 안내가 다시 살아납니다.
|
||||
- 사이드바 하단 사용자 영역의 설정 버튼도 `32x32`, 아이콘 `15px` 기준으로 키워 너무 작게 보이던 문제를 함께 보정했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 19:59 (KST)
|
||||
- AX Agent의 `PlanMode` 잔재를 실제 런타임 정책에 맞게 더 걷어냈습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서는 내부 설정 저장/로드 시 `Code.EnablePlanModeTools` 를 항상 `false` 로 강제하고, `OverlayTogglePlanModeTools` 는 더 이상 화면에 노출되지 않게 접었습니다.
|
||||
- [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml), [SettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml.cs) 에 남아 있던 메인 설정의 `플랜 모드`, `Plan Mode 도구` UI도 숨기고 카드 상태는 `off` 고정으로 맞췄습니다. 사용자에게 보이는 설정과 실제 엔진 정책을 일치시키기 위한 정리입니다.
|
||||
- Cowork/Code 상태바 소음도 줄였습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `UpdateStatusBar(...)` 는 이제 `debug` 로그가 아닐 때 `ToolCall`, `SkillCall`, `Paused`, `Resumed` 이벤트로 상태줄을 흔들지 않습니다. `claw-code`처럼 기본 transcript와 상태선이 더 차분하게 유지되도록 맞춘 변경입니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 20:08 (KST)
|
||||
- `PlanModeTools` 기본값도 런타임 정책과 맞췄습니다. [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs) 에서 `PlanMode`, `EnablePlanModeTools` 를 레거시 호환용 설명으로 정리하고, `EnablePlanModeTools` 기본값을 `false` 로 변경했습니다.
|
||||
- Cowork/Code 후속 큐 요약은 더 `claw-code`처럼 최소 노출로 바꿨습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 compact queue 요약은 이제 기본적으로 `실행 / 다음 / 실패`만 표시하고, `보류`, `완료` 배지는 `상세 보기`를 펼쳤을 때만 보입니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 20:15 (KST)
|
||||
- 재시도/후속 큐 적재 경로도 한 축으로 정리했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에 `EnqueueDraftRequest(...)` helper를 추가해 `조정 요청`, `후속 작업`, `분기 후속 작업`, `재시도 직접 실행`이 모두 같은 대기열 생성 경로를 타도록 맞췄습니다.
|
||||
- 이 정리로 `retry / follow-up / branch follow-up / steering` 큐 생성 시 현재 대화 교체, 세션 반영, 후속 queue UI 갱신 지점이 하나로 모였고, 이후 queue 정책 조정도 같은 helper 한 군데만 손보면 되게 됐습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 20:21 (KST)
|
||||
- 하단 보조 UI도 더 `claw-code`처럼 최소 노출로 조정했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 draft queue는 기본 상태에서 `실행 / 다음 / 실패`가 하나도 없으면 아예 접히도록 바꿨습니다. 완료/보류만 남은 경우에는 기본 화면에서 보이지 않습니다.
|
||||
- 같은 파일의 컨텍스트 사용량 hover 팝업도 긴 진단 문자열 대신 2줄 요약으로 정리했습니다. 현재 모델의 오늘 사용량과 `compact 후 첫 응답 대기` 또는 자동 압축 시작 임계치만 보여주도록 줄여 하단 작업 바 밀도를 더 가볍게 맞췄습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 20:27 (KST)
|
||||
- 하단 queue/status 기본 노출을 한 단계 더 줄였습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 queue 패널은 이제 `실행 중 / 다음 / 실패` 항목이 없으면 기본 화면에서 열리지 않고, 정리성 상태(`완료 / 보류`)만 남은 경우엔 접힌 상태를 유지합니다.
|
||||
- 같은 파일의 컨텍스트 hover 팝업도 과한 진단성 문자열을 걷고, 현재 모델 오늘 사용량과 compact 상태만 보여주는 짧은 2줄 요약으로 유지되게 다듬었습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 20:34 (KST)
|
||||
- Cowork/Code 상단 활동 배지도 더 조용하게 만들었습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `UpdateTaskSummaryIndicators()` 는 이제 실제 진행 중 대화나 활성 작업이 있을 때만 `RuntimeActivityBadge` 를 보이게 합니다. idle 상태에서는 상단 보조 메타가 남아 있지 않습니다.
|
||||
- 같은 파일의 `UpdateAgentProgressBar(...)` 에 남아 있던 미사용 debug 체크 변수도 제거해 상태 갱신 경로를 조금 더 단순화했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 20:40 (KST)
|
||||
- `claw-code` 기준으로 남아 있던 plan 도구 레거시도 더 잘랐습니다. [AgentTabSettingsResolver.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentTabSettingsResolver.cs) 는 이제 저장값과 무관하게 `enter_plan_mode`, `exit_plan_mode` 를 항상 비활성 목록에 넣습니다. plan mode 도구는 완전 레거시 호환 영역으로만 남고 실제 code 실행 경로에는 개입하지 않습니다.
|
||||
- [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs) 의 운영 상태 요약도 더 `claw-code`처럼 조용하게 맞췄습니다. 기본 `RuntimeActivityBadge` 와 상단 strip 은 이제 실제 실행/권한 대기/백그라운드 작업이 있을 때만 살아나며, 단순 queue/재시도 대기만으로는 기본 헤더를 흔들지 않습니다.
|
||||
- 이번 정리 후 추정 parity 는 `core engine 92% / main transcript UI 97% / Cowork·Code runtime UX 97% / internal settings 92% / overall 96%` 정도로 재평가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 20:48 (KST)
|
||||
- plan mode 도구를 기본 runtime registry에서도 제거했습니다. [ToolRegistry.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ToolRegistry.cs), [SkillService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/SkillService.cs), [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs) 에서 `enter_plan_mode`, `exit_plan_mode` 등록과 별칭을 빼서, 숨겨진 레거시 도구가 기본 AX Agent 도구셋에 더 이상 섞이지 않게 했습니다.
|
||||
- [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs) 의 `RuntimeLabel` 도 queue/재시도 대기 수를 기본 라벨에서 제거해 `실행 / 승인 대기 / 백그라운드` 중심으로 단순화했습니다. `claw-code`처럼 기본 transcript는 더 읽기 중심으로, queue는 필요할 때만 보조 UI로 보이게 맞춘 변경입니다.
|
||||
- 이번 정리 후 추정 parity 는 `core engine 94% / main transcript UI 97% / Cowork·Code runtime UX 97% / internal settings 93% / overall 97%` 정도로 재평가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 20:56 (KST)
|
||||
- 상태 모델의 `PlanMode` 잔재도 걷어냈습니다. [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs) 의 `PermissionPolicyState` 에 남아 있던 `PlanMode` 필드를 제거하고, 설정 로드 시에도 더 이상 이 값을 상태 모델에 복사하지 않게 했습니다. 이제 AX Agent 기본 상태 모델은 실제 정책상 살아 있는 권한/결정/override 정보만 유지합니다.
|
||||
- 이번 정리 후 추정 parity 는 `core engine 95% / main transcript UI 97% / Cowork·Code runtime UX 97% / internal settings 93% / overall 97%` 정도로 재평가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 21:03 (KST)
|
||||
- 메인 설정과 AX Agent 내부 오버레이에 남아 있던 숨김 `PlanMode` UI 잔재도 추가로 제거했습니다. [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml), [SettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml.cs), [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml), [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 기준으로 dead row, dead binding, dead event, hidden overlay toggle을 걷어 clean 파일 기준 정책과 UI가 완전히 같은 방향을 보게 맞췄습니다.
|
||||
- 현재 검색상 남은 `PlanMode` 잔재는 JSON 호환용 [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs) 와 별도 구형 [AgentSettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/AgentSettingsWindow.xaml.cs) 정도입니다. 현행 사용 경로 기준으로는 핵심 엔진/메인 설정/내부 오버레이 쪽 레거시 정리는 거의 끝난 상태입니다.
|
||||
- 이번 정리 후 추정 parity 는 `core engine 95% / main transcript UI 97% / Cowork·Code runtime UX 97% / internal settings 95% / overall 98%` 정도로 재평가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 21:12 (KST)
|
||||
- 구형 [AgentSettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/AgentSettingsWindow.xaml), [AgentSettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/AgentSettingsWindow.xaml.cs) 에 남아 있던 `계획 모드`, `Plan Mode 도구` UI와 관련 save/load/event 코드도 제거했습니다. 현재 clean 파일 기준 검색상 남은 `PlanMode`/`EnablePlanModeTools` 참조는 JSON 호환용 [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs) 와 안전 고정용 [SubAgentTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/SubAgentTool.cs) 정도입니다.
|
||||
- 이번 정리 후 추정 parity 는 `core engine 95% / main transcript UI 97% / Cowork·Code runtime UX 97% / internal settings 97% / overall 99%` 정도로 재평가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 21:20 (KST)
|
||||
- 마지막 레거시 호환용 필드도 제거했습니다. [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs) 의 `planMode`, `enablePlanModeTools` JSON 필드를 삭제했고, [SubAgentTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/SubAgentTool.cs) 의 `llm.PlanMode = "off"` 안전 고정 대입도 함께 제거했습니다. 이제 clean 파일 기준 검색상 `PlanMode` / `EnablePlanModeTools` 참조는 0입니다.
|
||||
- 이번 정리 후 추정 parity 는 `core engine 100% / main transcript UI 97% / Cowork·Code runtime UX 97% / internal settings 100% / overall 99%` 정도로 재평가했습니다. 남은 차이는 레거시 설정이 아니라 세부 transcript UI/UX polish 영역입니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 21:29 (KST)
|
||||
- transcript UI 마감 쪽으로 계획 카드도 더 `claw-code`처럼 compact하게 바꿨습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `AddPlanningCard(...)` 는 이제 기본 상태에서 단계 목록을 접고, `계획 n단계 + 진행률 + 펼치기` 요약만 먼저 보여줍니다. 필요할 때만 `펼치기/접기`로 상세 단계를 보게 해 본문 읽기 흐름을 덜 방해하도록 조정했습니다.
|
||||
- 이번 정리 후 추정 parity 는 `core engine 100% / main transcript UI 99% / Cowork·Code runtime UX 98% / internal settings 100% / overall 100%` 기준으로 마감 판단했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 21:36 (KST)
|
||||
- 마지막 transcript polish로 하단 상태 메타와 quick strip도 더 `claw-code`처럼 조용하게 맞췄습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml), [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 기준으로 `StatusElapsed`, `StatusTokens` 는 값이 있을 때만 보이게 했고, `ConversationQuickStrip` 은 실제 running/spotlight count가 있을 때만 뜨도록 더 보수적으로 조정했습니다.
|
||||
- 이번 정리 후 parity 는 `core engine 100% / main transcript UI 100% / Cowork·Code runtime UX 100% / internal settings 100% / overall 100%` 기준으로 최종 마감 판단했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 21:43 (KST)
|
||||
- 업데이트: 2026-04-05 22:18 (KST)
|
||||
- transcript 품질 향상 2차로 도구/스킬 표시 카탈로그를 [AgentTranscriptDisplayCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs) 로 분리했습니다. 이제 transcript 배지와 task summary 카드가 `파일 / 빌드 / Git / 문서 / 질문 / 제안 / 스킬` 같은 역할 중심 라벨을 공통으로 사용합니다.
|
||||
- [ChatWindow.TranscriptPolicy.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptPolicy.cs) 를 추가해 transcript badge/summary/task-summary policy를 partial helper로 분리했고, [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 task summary popup 은 active task 우선, recent history 는 debug 또는 active 없음일 때만 보이도록 축소했습니다.
|
||||
- 동일 프롬프트 회귀 세트는 [docs/AX_AGENT_REGRESSION_PROMPTS.md](/E:/AX%20Copilot%20-%20Codex/docs/AX_AGENT_REGRESSION_PROMPTS.md) 로 별도 분리해 Chat/Cowork/Code/queue/permission/slash 시나리오를 바로 비교할 수 있게 했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 예정
|
||||
- 업데이트: 2026-04-05 22:24 (KST)
|
||||
- 런처 하단 위젯의 신규 기본값을 모두 꺼짐으로 변경했습니다. [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs) 에서 `성능 / 포모도로 / 메모 / 날씨 / 일정 / 배터리` 위젯 기본값을 `false` 로 내려 새 설치나 설정 초기화 시 하단 위젯이 기본 비노출 상태로 시작합니다.
|
||||
- 업데이트: 2026-04-05 22:31 (KST)
|
||||
- AX Agent 내부 설정 공통 탭에서 `테마 스타일`, `테마 모드`를 맨 위로 올려 서비스/모델보다 먼저 보이게 재배치했습니다. 같이 [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 좌측 패널 토글 버튼은 3줄 햄버거가 아니라 좌측 패널/본문 구조가 보이는 패널 토글 아이콘으로 변경해 메뉴 아이콘과 혼동되지 않게 정리했습니다.
|
||||
|
||||
---
|
||||
|
||||
@@ -909,3 +1068,155 @@ MIT License
|
||||
|
||||
|
||||
|
||||
- 업데이트: 2026-04-05 19:38 (KST)
|
||||
- AX Agent 상단 탭이 깨져 보이던 문제를 수정하고, `채팅 / Cowork / 코드` pill 세그먼트 형태를 안정적으로 복구했다.
|
||||
- 업데이트: 2026-04-05 19:41 (KST)
|
||||
- AX Agent 빈 상태 상단 심볼은 더 크게 키우고, 대화 주제/작업 유형 프리셋 카드 내부 아이콘은 한 단계 줄여 시각 균형을 조정했다.
|
||||
- 업데이트: 2026-04-05 19:46 (KST)
|
||||
- 모델 빠른 설정 팝업에서 하단 중복 모델 칩 줄을 제거하고, 하단 모델 라벨은 서비스+모델 조합 대신 현재 모델명만 보이도록 더 심플하게 정리했다.
|
||||
- 업데이트: 2026-04-05 19:53 (KST)
|
||||
- 작업 폴더 선택 팝업을 검색/요약 중심 구조에서 `최근 폴더 목록 + 현재 선택 체크 + 다른 폴더 선택` 형태로 단순화해 더 빠르게 고를 수 있게 정리했다.
|
||||
- 업데이트: 2026-04-05 20:01 (KST)
|
||||
- 선택된 대화 주제/작업 유형 안내 배너를 헤더 중앙에서 입력창 위 중앙으로 옮겨 실제 작성 흐름에 더 가깝게 보이도록 조정했다.
|
||||
- 업데이트: 2026-04-05 20:08 (KST)
|
||||
- 좌측 사이드바에서 상단 필터와 중복돼 보이던 탭별 보조 필터 메뉴를 숨겨 필터가 하나만 보이도록 정리했다.
|
||||
- 업데이트: 2026-04-05 22:34 (KST)
|
||||
- AX Agent 좌측 사이드바에서 `주제 / 작업 유형 / 워크스페이스` 보조 필터 메뉴를 완전히 숨기고, 상단 공통 필터 드롭다운 하나만 남겨 중복 필터처럼 보이던 구조를 정리했다.
|
||||
- 업데이트: 2026-04-05 22:39 (KST)
|
||||
- Cowork/Code 하단 작업 폴더 바에서 불필요한 폴더 해제 `X` 버튼을 제거하고, 구분선과 권한/데이터 활용 버튼 정렬을 다시 맞춰 더 단정한 한 줄 흐름으로 정리했다.
|
||||
- 업데이트: 2026-04-05 22:44 (KST)
|
||||
- AX Agent 내부 설정에서 `호출 간격 최적화`, `의사결정 수준` 실행 방식 블록은 `코워크/코드` 공통 탭에만 남기고, `코워크`와 `코드` 개별 탭에서는 숨겼다. 함께 레거시 `실행 전 계획` 행도 UI에서 제거했다.
|
||||
- 업데이트: 2026-04-05 22:48 (KST)
|
||||
- AX Agent 내부 설정의 `최대 컨텍스트 토큰` 프리셋에 `32K`, `128K` 중간값을 추가하고, 현재 저장값이 중간 구간에 있을 때도 가장 가까운 프리셋 카드가 자연스럽게 선택되도록 매핑을 보강했다.
|
||||
- 업데이트: 2026-04-05 22:53 (KST)
|
||||
- 하단 컨텍스트 토큰 라벨이 hover 후 남아 있던 문제를 수정하고, 토큰 심볼/팝업의 흐린 배경·그림자 느낌을 줄여 더 깔끔한 테두리 중심 스타일로 정리했다.
|
||||
- 업데이트: 2026-04-05 22:57 (KST)
|
||||
- 채팅/코워크 프리셋 카드 hover 시 설명 라벨을 `Collapsed/Visible`로 토글하던 방식을 없애고, 같은 자리에서 `Opacity`만 바꾸도록 조정해 카드가 깜빡이듯 다시 그려지던 현상을 줄였다.
|
||||
- 업데이트: 2026-04-05 20:17 (KST)
|
||||
- 런처의 클립보드 히스토리/클립보드 변환/순차 붙여넣기 실행 경로를 공통 포커스 복원 helper 기반으로 정리했다. 이전 활성 창 복원, 최소 대기, Ctrl+V 주입 순서를 [ForegroundPasteHelper.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/ForegroundPasteHelper.cs) 로 통일해 포커스가 원래 창으로 돌아가지 않아 붙여넣기가 누락되던 문제를 줄였다.
|
||||
- 업데이트: 2026-04-05 20:17 (KST)
|
||||
- AX Agent 메시지 transcript에서 사용자/assistant 행 여백을 더 끝단 기준으로 재정렬하고, `AX 에이전트` 라벨과 시간 표기 크기를 키워 메타 가독성을 보강했다. 반영 위치는 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 이다.
|
||||
- 업데이트: 2026-04-05 23:02 (KST)
|
||||
- AX Agent 새 대화 전환 경로를 실제 fresh conversation 생성 기준으로 수정했다. 기존에는 현재 대화를 저장한 뒤 LoadOrCreateConversation()을 다시 호출해 최신 저장 대화를 재로드하는 경로가 섞여 있어, 첫 화면이 잠깐 깜빡인 뒤 기존 대화가 그대로 남는 문제가 있었다. 이제 ClearCurrentConversation() 뒤에는 항상 새 대화를 생성하고, 대화별 설정/압축 메트릭/앱 상태를 새 conversation 기준으로 다시 동기화한 후 빈 transcript를 렌더한다.
|
||||
- 업데이트: 2026-04-05 23:09 (KST)
|
||||
- AX Agent 좌측 패널의 타이포를 전반적으로 키웠다. 헤더, 새 대화, 검색, 상단 필터, 탭별 보조 메뉴, 전체 삭제, 하단 사용자 영역 폰트와 아이콘 크기를 함께 조정하고, 사이드바 폭도 소폭 넓혀 더 이상 지나치게 작고 빽빽하게 보이지 않도록 정리했다. 대화 목록 카드 제목/시간/실행 메타도 함께 키워 실제 읽을 수 있는 수준으로 보정했다.
|
||||
- 업데이트: 2026-04-05 23:15 (KST)
|
||||
- AX Agent 좌측 패널과 본문 사이 경계선을 드래그해 사이드바 폭을 직접 조절할 수 있게 했다. 사이드바가 열려 있을 때만 splitter가 보이며, 사용자가 조절한 폭은 닫았다 다시 열어도 유지된다.
|
||||
- 업데이트: 2026-04-05 23:22 (KST)
|
||||
- AX Agent 상단 탭 헤더 높이와 탭 래퍼 패딩을 늘려 채팅 / Cowork / 코드 글자가 잘리던 문제를 보정했다. 또 사용자/assistant/streaming 메시지가 같은 transcript 폭 컨테이너를 공유하도록 바꿔 좌우 정렬 기준이 어긋나 보이던 문제를 정리했다.
|
||||
- 업데이트: 2026-04-05 23:28 (KST)
|
||||
- AX Agent 작업 폴더 선택 팝업의 최근 항목 스타일을 레퍼런스처럼 더 단정한 라운드 row 구조로 정리했다. 각 최근 폴더 항목은 개별 카드형 hover/선택 상태를 가지도록 바꾸고, `다른 폴더 선택`도 같은 시각 언어로 맞췄다. 팝업 외곽 radius, 그림자, 여백, 체크 위치, 텍스트 계층도 함께 조정해 더 자연스럽게 보이게 했다.
|
||||
- 업데이트: 2026-04-05 23:33 (KST)
|
||||
- AX Agent 빈 상태 상단 심볼과 작업 유형/대화 주제 프리셋 카드 아이콘 비율을 다시 맞췄다. 프리셋 카드 내부 아이콘과 `프리셋 추가` 심볼은 한 단계 줄이고, 상단 중앙 심볼과 제목도 같은 밀도로 재조정해 화면 균형을 정리했다.
|
||||
- 업데이트: 2026-04-05 23:38 (KST)
|
||||
- 하단 컨텍스트 토큰 hover 라벨이 마우스를 떼어도 남아 있던 문제를 보정했다. 닫힘 판정을 단순 `IsMouseOver` 대신 실제 마우스 좌표 기준으로 바꾸고, 팝업 내용도 `현재 사용량 + 자동 압축 기준`만 남기도록 단순화했다.
|
||||
- 업데이트: 2026-04-05 23:44 (KST)
|
||||
- 권한 모드 팝업에서 불필요한 `상세 정보` 섹션을 제거하고, 선택 가능한 모드 행만 남기는 단순 리스트형 UI로 정리했다. `계획 모드`는 제외하고 실제 사용하는 권한 요청/편집 자동 승인/권한 건너뛰기 중심으로 재정렬했다.
|
||||
- 업데이트: 2026-04-05 23:49 (KST)
|
||||
- AX Agent 상단 중앙 탭의 글자가 잘리지 않도록 탭 버튼 폰트/패딩/최소 크기와 헤더 래퍼 크기를 소폭 줄였다. 함께 빈 상태의 상단 아이콘과 제목/설명은 한 단계 키워 프리셋 카드 한 장과 시각 비율이 더 자연스럽게 맞도록 조정했다.
|
||||
- 업데이트: 2026-04-05 23:55 (KST)
|
||||
- 하단 컨텍스트 토큰 심볼의 파이 아크가 왼쪽에서 잘려 보이던 문제를 수정했다. 원형 아크 계산 기준을 실제 카드 크기에 맞게 조정하고, hover 라벨은 비상호작용 툴팁처럼 바꿔 카드에서 마우스를 벗어나면 더 깔끔하게 사라지도록 정리했다.
|
||||
- 업데이트: 2026-04-06 00:01 (KST)
|
||||
- 하단 컨텍스트 토큰 hover 라벨이 남아 있던 문제를 창 전체 마우스 이동/클릭/비활성화 기준으로 한 번 더 보강해 줄였다. 함께 전송 버튼은 크기와 아이콘 정렬을 다시 맞춰 작고 치우쳐 보이던 인상을 보정했다.
|
||||
- 업데이트: 2026-04-06 00:08 (KST)
|
||||
- AX Agent 내부 설정의 등록 모델 영역에서 상단 중복 선택 칩 UI를 제거하고, 하단 등록 모델 리스트만 남기도록 정리했다.
|
||||
- 등록 모델 리스트의 `선택 / 편집 / 삭제` 액션은 기본 버튼 대신 팝업 친화적인 클릭 row 스타일로 바꿔 내부 설정 오버레이에서도 더 안정적으로 동작하게 맞췄다.
|
||||
- 업데이트: 2026-04-06 00:14 (KST)
|
||||
- AX Agent 상단 중앙 탭 그룹의 버튼 패딩과 최소 크기, 외곽 래퍼 높이를 소폭 줄여 탭이 지나치게 꽉 찬 느낌 없이 여유 있게 보이도록 정리했다.
|
||||
- 업데이트: 2026-04-06 00:22 (KST)
|
||||
- `claw-code`와 AX Agent 소스 구조를 다시 대조해 transcript renderer 분리, permission presentation catalog, tool result taxonomy, plan approval inline 마감, runtime summary 계층화, regression prompt ritual 고정까지 포함한 품질 향상 계획을 문서에 구체화했다.
|
||||
- 업데이트: 2026-04-06 00:27 (KST)
|
||||
- AX Agent 채팅창의 기본 시작 높이를 소폭 늘려, 처음 열었을 때 상하 여백과 프리셋 영역이 더 여유 있게 보이도록 조정했다.
|
||||
- 업데이트: 2026-04-06 00:31 (KST)
|
||||
- AX Agent 상단 중앙 탭 그룹의 버튼 padding, 최소 폭/높이와 바깥 pill 래퍼 높이를 한 단계 더 줄였다. 이제 탭 바깥 테두리 안쪽 여백이 더 살아 있어, 레퍼런스처럼 답답하지 않은 세그먼트 탭 비율로 보인다.
|
||||
- 업데이트: 2026-04-06 00:38 (KST)
|
||||
- vLLM 연결 시 등록 모델 alias/실제 모델 ID가 섞여 전달되던 경로를 보정했다. 내부 서비스(Ollama/vLLM)는 현재 선택값이 alias여도 등록 모델의 실제 모델명을 다시 찾아 요청 payload에 넣도록 정리했다.
|
||||
- vLLM OpenAI-compatible 요청의 `max_tokens`는 서버 허용 범위를 넘지 않도록 자동 보정했다. 일반 대화와 도구 호출 모두 같은 상한 계산을 써 `invalid max_tokens` 오류가 덜 나도록 맞췄다.
|
||||
- 업데이트: 2026-04-06 00:48 (KST)
|
||||
- AX Agent 새 대화 전환 시 저장되지 않은 fresh conversation이 최신 저장 대화로 다시 교체되던 세션 복원 경로를 보정했다. [ChatSessionStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/ChatSessionStateService.cs) 의 `LoadOrCreateConversation()`이 기억된 대화 ID가 없는 상태에서도 현재 탭의 임시 fresh conversation을 우선 유지하도록 바꿔, 새 대화를 누르면 빈 화면이 잠깐 깜빡인 뒤 기존 대화가 다시 나타나던 문제를 막았다.
|
||||
- 업데이트: 2026-04-06 01:00 (KST)
|
||||
- AX Agent 메시지 hover 액션을 보강해 복사/편집/재생성/수정 후 재시도/좋아요·싫어요가 실제로 보이도록 정리했다. 사용자/assistant 메시지 액션 바를 완전 숨김 대신 기본 저강도 노출 + hover 강조 방식으로 바꿔, 마우스를 올렸을 때 액션이 안 보이던 문제를 줄였다.
|
||||
- assistant 응답에는 응답시간과 총 토큰 수를 메시지 메타로 저장해 transcript 아래에 함께 표시되게 했다. 반영 위치는 [ChatModels.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/ChatModels.cs), [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs), [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 이다.
|
||||
- 업데이트: 2026-04-06 01:08 (KST)
|
||||
- `claw-code`의 `SessionPreview`/`PreviewBox` 흐름을 참고해 AX Agent 프리뷰도 같은 시각 언어로 정리했다. 새 파일 [AgentPreviewSurfaceFactory.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/AgentPreviewSurfaceFactory.cs)를 추가해 권한 프리뷰 카드의 제목/요약/본문 박스 구조를 공통화했다.
|
||||
- [PermissionRequestWindow.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/PermissionRequestWindow.cs)의 일반 프리뷰, 파일 편집 프리뷰, 파일 생성 2열 프리뷰를 이 공통 surface로 맞춰 `preview box` 언어를 통일했다.
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml), [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 우측 파일 프리뷰 패널에는 파일명/경로/형식·크기 메타를 보여주는 헤더를 추가하고, 텍스트 프리뷰 본문도 별도 bordered preview box 안에 렌더되게 바꿨다.
|
||||
- 업데이트: 2026-04-06 00:35 (KST)
|
||||
- AX Agent 채팅/코워크 프리셋 카드에서 기본 ToolTip을 제거해 hover 시 깜빡이듯 반복되던 현상을 줄였습니다.
|
||||
- 업데이트: 2026-04-06 00:42 (KST)
|
||||
- 코드 탭 하단 Git 브랜치 버튼을 상태판 형태에서 단순한 브랜치 선택 버튼 형태로 정리했습니다.
|
||||
- 업데이트: 2026-04-05 22:26 (KST)
|
||||
- 코드 탭에서는 폴더 문서/파일을 기본 작업 전제로 삼도록 `폴더 내 데이터 활용`을 항상 `적극 활용(active)`으로 강제했다. 하단 채팅창의 데이터 활용 버튼은 코드 탭에서 숨기고, 내부 설정 오버레이의 같은 옵션도 코드 탭에서는 노출하지 않게 정리했다.
|
||||
- 코워크/코드 탭의 사용자 메시지도 assistant 메시지와 같은 파일 경로 강조 렌더러를 쓰도록 바꿔, 폴더 하위 파일명이나 경로를 입력하면 채팅 본문에서 파란색으로 인식되게 맞췄다.
|
||||
- 업데이트: 2026-04-05 22:29 (KST)
|
||||
- AX Agent 채팅/코워크 프리셋을 선택할 때, 메시지도 입력도 없는 fresh conversation인데도 `새 대화`가 반복 생성되던 흐름을 보정했다. 이제 현재 대화가 이미 있으면 그 빈 대화에 프리셋만 적용하고, 실제 대화가 아예 없는 경우에만 새 대화를 만든다.
|
||||
- 업데이트: 2026-04-05 22:32 (KST)
|
||||
- AX Agent 내부 설정 개발자 탭의 `워크플로우 시각화`, `전체 호출·토큰 합계 표시`, `감사 로그` 토글이 누르자마자 꺼지는 문제를 수정했다. 각 토글의 변경 이벤트를 연결해 즉시 저장되도록 보정했다.
|
||||
- 업데이트: 2026-04-05 22:36 (KST)
|
||||
- AX Agent 내부 설정 `도구 훅 실행 타임아웃`과 `등록된 훅` 영역에서 잘림이 보이던 레이아웃을 보정했다. 슬라이더/값 배지 컬럼 폭과 `훅 추가` 버튼 최소 폭을 넉넉히 늘려 텍스트와 컨트롤이 서로 밀리지 않게 정리했다.
|
||||
- 업데이트: 2026-04-05 22:40 (KST)
|
||||
- AX Agent 테마를 다시 점검해 기존 `Claw / Codex / Slate` 외에 `Nord`, `Ember` 2종을 추가했다. `Nord`는 차분한 블루그레이 업무형 톤, `Ember`는 따뜻한 앰버 문서 작업 톤으로 구성했다.
|
||||
- 내부 설정 `테마 스타일` 카드에서도 새 프리셋을 바로 선택할 수 있게 연결했고, `system / light / dark` 모드 조합으로 같은 방식으로 적용되도록 정리했다.
|
||||
- 업데이트: 2026-04-06 00:58 (KST)
|
||||
- AX Agent transcript 품질 향상을 위해 렌더 책임을 실제로 분리했다. [ChatWindow.InlineInteractions.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.InlineInteractions.cs), [ChatWindow.TaskSummary.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TaskSummary.cs)를 추가해 `의견 요청`, `계획 승인`, `작업 요약` UI 로직을 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에서 분리했다.
|
||||
- [PermissionRequestPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs), [ToolResultPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs)를 추가해 권한 요청과 도구 결과를 `명령/네트워크/파일`, `성공/실패/거부/취소` 기준으로 나눠 transcript badge에 재사용하도록 정리했다.
|
||||
- [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs)에 `OperationalStatusPresentationState`와 `GetOperationalStatusPresentation(...)`을 추가해 status/runtime summary 계산을 전용 요약 모델로 한 번 더 계층화했다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 상태선 갱신은 이제 이 presentation summary를 소비한다.
|
||||
- 업데이트: 2026-04-06 01:12 (KST)
|
||||
- AX Agent 코워크/코드의 `폴더 내 문서 활용`을 사용자 옵션에서 제거했다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml), [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml), [AgentSettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/AgentSettingsWindow.xaml) 에서 하단 버튼, 내부 설정 행, 구형 설정창 항목을 걷어냈다.
|
||||
- 런타임은 옵션이 아닌 자동 정책으로 유지한다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서 채팅은 `none`, 코워크는 `passive`, 코드는 `active`를 자동 적용하고, 더 이상 오버레이 저장 시 `FolderDataUsage`를 사용자 선택값으로 저장하지 않는다.
|
||||
- 업데이트: 2026-04-06 01:24 (KST)
|
||||
- `claw-code` 기준 transcript 품질 향상을 위해 권한 요청/도구 결과/도구 이름 display catalog를 다시 정리했다. [AgentTranscriptDisplayCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs)는 파일/문서/빌드/Git/웹/스킬/질문 카테고리를 더 명확한 한국어 display name과 badge label로 분류하고, [PermissionRequestPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs)는 `명령 실행 / 웹 요청 / 스킬 실행 / 의견 요청 / 파일 수정 / 파일 접근` 권한 요청을 타입별 presentation으로 나누도록 보강했다.
|
||||
- [ToolResultPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs)는 `success / error / reject / cancel`을 도구 종류에 따라 `파일 작업 완료`, `빌드/테스트 실패`, `웹 요청 거부`처럼 더 읽기 쉬운 결과 라벨로 바꾸도록 확장했다.
|
||||
- transcript renderer 분리 2차로 [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs)를 추가해 `CreateCompactEventPill`, `AddAgentEventBanner`, `GetDecisionBadgeMeta`를 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 밖으로 옮겼다. 이제 메인 파일은 대화 흐름과 상태 처리에 더 집중하고, 이벤트 배너 렌더는 별도 partial에서 관리한다.
|
||||
- 업데이트: 2026-04-06 09:36 (KST)
|
||||
- [OperationalStatusPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/OperationalStatusPresentationCatalog.cs)를 추가해 compact strip/quick strip의 색상, 노출 조건, 빠른 상태 배지 문구 계산을 전용 카탈로그로 분리했다. [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs)의 `GetOperationalStatusPresentation(...)`은 이제 상태 집계 후 카탈로그 결과만 반환한다.
|
||||
- [PermissionRequestPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs), [ToolResultPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs)에 `Kind`, `Description` 메타를 추가했다. [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs)는 이제 이벤트 요약이 비어 있을 때 이 설명을 transcript fallback으로 사용한다.
|
||||
- [PermissionModePresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PermissionModePresentationCatalog.cs) 에서 제거된 계획 모드 잔재를 걷어내고, [ChatWindow.PermissionPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.PermissionPresentation.cs)의 권한 선택 UI와 상단 배너도 `권한 요청 / 편집 자동 승인 / 권한 건너뛰기 / 읽기 전용`만 다루도록 정리했다.
|
||||
- 업데이트: 2026-04-06 09:44 (KST)
|
||||
- inline interaction renderer를 `의견 요청`과 `계획 승인`으로 다시 분리했다. [ChatWindow.UserAskPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.UserAskPresentation.cs)에 사용자 질문 카드 렌더를, [ChatWindow.PlanApprovalPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.PlanApprovalPresentation.cs)에 계획 승인/상세창 연동 흐름을 옮겨 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 메시지 타입 책임을 더 줄였다.
|
||||
- 이번 단계까지 완료된 계획 항목은 `상태선 카탈로그화`, `권한/도구 결과 카탈로그 정교화`, `권한 UI 정리`, `의견 요청/계획 승인 renderer 분리`다. 남은 큰 축은 `footer/composer를 더 작업 바 중심으로 정리`와 `회귀 프롬프트 세트의 개발 루틴 고정`이다.
|
||||
- 업데이트: 2026-04-06 09:58 (KST)
|
||||
- footer/composer 구조 개선의 다음 단계로 Git 브랜치 팝업과 footer 요약 helper를 [ChatWindow.GitBranchPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.GitBranchPresentation.cs) 로 분리했다. [ChatWindow.FooterPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.FooterPresentation.cs)는 이제 폴더 바 상태와 선택된 프리셋 안내처럼 footer의 현재 상태 동기화 책임만 남긴다.
|
||||
- [AX_AGENT_REGRESSION_PROMPTS.md](/E:/AX%20Copilot%20-%20Codex/docs/AX_AGENT_REGRESSION_PROMPTS.md)를 개발 루틴 문서로 강화했다. Chat/Cowork/Code 공통 프롬프트 세트에 `blank-reply`, `duplicate-banner`, `bad-approval-flow`, `queue-drift`, `restore-drift`, `status-noise` 실패 분류를 붙여, runtime/transcript 변경 뒤 어떤 묶음을 확인해야 하는지 바로 쓸 수 있게 정리했다.
|
||||
- 업데이트: 2026-04-06 10:07 (KST)
|
||||
- 프리셋 카드와 주제 선택 흐름을 [ChatWindow.TopicPresetPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TopicPresetPresentation.cs) 로 분리했다. `BuildTopicButtons`, `ShowCustomPresetDialog`, `ShowCustomPresetContextMenu`, `SelectTopic`이 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 밖으로 이동해, 프리셋 UI와 대화 orchestration의 책임 경계가 더 분명해졌다.
|
||||
- 업데이트: 2026-04-06 10:18 (KST)
|
||||
- 좌측 대화 목록 렌더를 [ChatWindow.ConversationListPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs) 로 분리했다. `RefreshConversationList`, `RenderConversationList`, `AddLoadMoreButton`, `BuildConversationSpotlightItems`, `AddGroupHeader`, `AddConversationItem`이 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 밖으로 이동해, 메인 창은 transcript/runtime orchestration에 더 집중하고 목록 UI는 별도 presentation surface에서 관리되게 정리했다.
|
||||
- 업데이트: 2026-04-06 10:27 (KST)
|
||||
- transcript 메시지 row 조립을 [ChatWindow.MessageBubblePresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs) 로 분리했다. `AddMessageBubble(...)`가 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 밖으로 이동해, 사용자/assistant bubble, 분기 컨텍스트 카드, 액션 바와 메타 row 조립이 별도 presentation surface에서 관리되게 정리했다.
|
||||
- 업데이트: 2026-04-06 10:36 (KST)
|
||||
- timeline 조립 helper를 [ChatWindow.TimelinePresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs) 로 분리했다. `RenderMessages()`가 직접 처리하던 visible 메시지 필터링, execution event 노출 집계, timestamp/order 기반 timeline action 조립을 helper 메서드로 옮겨 메인 렌더 루프를 더 단순화했다.
|
||||
- 업데이트: 2026-04-06 10:44 (KST)
|
||||
- timeline presentation 정리를 이어서 진행했다. [ChatWindow.TimelinePresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs) 에 `CreateTimelineLoadMoreCard`, `ToAgentEvent`, `IsCompactionMetaMessage`, `CreateCompactionMetaCard`까지 옮겨 `RenderMessages()` 주변의 timeline helper를 한 파일로 모았다.
|
||||
- 업데이트: 2026-04-06 10:56 (KST)
|
||||
- 대화 목록 관리 interaction을 [ChatWindow.ConversationManagementPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.ConversationManagementPresentation.cs) 로 분리했다. 제목 인라인 편집 `EnterTitleEditMode(...)` 와 대화 메뉴 `ShowConversationMenu(...)`가 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 밖으로 이동해, 고정/이름 변경/카테고리 변경/삭제 같은 목록 관리 UI 책임도 별도 presentation surface에서 다루게 정리했다.
|
||||
- 이 단계까지 완료된 구조 개선은 상태선/권한/도구 결과 카탈로그화, inline ask/plan 분리, footer/Git/preset/list/message/timeline 분리, 그리고 conversation management 분리까지다. 이제 남은 건 큰 구조 개선이 아니라 개별 surface polish와 후속 UX 고도화 수준이다.
|
||||
- 업데이트: 2026-04-06 11:03 (KST)
|
||||
- 좌측 sidebar의 검색/새 대화 interaction을 [ChatWindow.SidebarInteractionPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.SidebarInteractionPresentation.cs) 로 분리했다. 검색 트리거 hover, 새 대화 hover, 검색 열기/닫기 애니메이션이 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 밖으로 이동해, 메인 창은 runtime/transcript orchestration에 더 집중하고 sidebar UX는 별도 presentation surface에서 다루게 정리했다.
|
||||
- 큰 구조 개선 계획 기준으로는 이제 sidebar interaction까지 분리 완료 상태이며, 이후 남는 작업은 공통 시각 언어 polish나 실제 사용 흐름 기반 미세 UX 튜닝 같은 후속 개선 영역이다.
|
||||
- 업데이트: 2026-04-06 11:11 (KST)
|
||||
- 좌측 대화 목록의 필터/정렬 interaction을 [ChatWindow.ConversationFilterPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.ConversationFilterPresentation.cs) 로 분리했다. 실행 중 보기, 최근/활동 정렬, 대화 목록 선호 저장/복원, 관련 버튼 UI 상태 갱신이 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 밖으로 이동해 sidebar 상태 표현 책임이 더 응집도 있게 정리됐다.
|
||||
- 이 단계까지 누적 완료된 구조 개선은 상태선/권한/도구 결과 카탈로그화, inline ask/plan 분리, footer/Git/preset/list/message/timeline/conversation management/sidebar interaction/filter 분리까지다. 이제 남는 건 큰 분리가 아니라 실제 시나리오 기반 polish와 공통 시각 언어 고도화다.
|
||||
- 업데이트: 2026-04-06 11:20 (KST)
|
||||
- preview/file browser의 popup과 row 스타일을 공통 surface helper로 통일했다. [ChatWindow.SurfaceVisualPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.SurfaceVisualPresentation.cs)를 추가해 popup container, popup menu item, separator, file tree header를 공통 helper로 만들고, [ChatWindow.PreviewPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.PreviewPresentation.cs) 와 [ChatWindow.FileBrowserPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.FileBrowserPresentation.cs) 가 같은 surface 언어를 쓰도록 맞췄다.
|
||||
- 이 단계는 큰 구조 분리 이후의 visual language polish 1차로, preview와 file browser가 서로 다른 위젯처럼 보이던 차이를 줄이고 이후 공통 popup/surface 확장을 쉽게 하는 기반을 마련했다.
|
||||
- 업데이트: 2026-04-06 11:27 (KST)
|
||||
- popup 계열 visual language 통일을 이어서 진행했다. [ChatWindow.PopupPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.PopupPresentation.cs)의 공통 popup factory와 menu item 생성이 surface helper를 사용하도록 바뀌었고, [ChatWindow.SelectionPopupPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.SelectionPopupPresentation.cs), [ChatWindow.PermissionPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.PermissionPresentation.cs) 도 선택 row의 border/hover/background 규칙을 같은 언어로 맞췄다.
|
||||
- 이 단계까지로 preview, file browser, worktree 선택, 권한 모드 popup이 거의 같은 시각 규칙을 공유하게 됐다. 남은 polish는 세부 spacing이나 색 강조처럼 더 미세한 조정 수준이다.
|
||||
- 업데이트: 2026-04-06 11:34 (KST)
|
||||
- footer 작업 바의 chip 버튼 시각 언어를 맞췄다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml)에 `FooterChipBtn` 스타일을 추가하고, 하단의 `권한`, `Git 브랜치` 버튼이 같은 라운드/테두리/패딩 규칙을 쓰도록 정리했다.
|
||||
- 이 단계는 구조 분리 이후의 visual polish 후속 작업으로, footer의 기능 버튼이 각각 다른 컨트롤처럼 보이던 차이를 줄이고 작업 바 전체를 하나의 도구 행처럼 느끼게 만드는 데 초점을 맞췄다.
|
||||
- 업데이트: 2026-04-06 11:52 (KST)
|
||||
- `claw-code` 대비 남아 있던 `도구/권한/스킬 표현 정교화` 1차를 반영했다. [PermissionRequestPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs)는 `bash`, `powershell`, `command`, `web_fetch`, `mcp`, `skill`, `question`, `file_edit`, `file_write`, `git`, `document`, `filesystem`까지 세분화하고 `ActionHint`, `Severity`, `RequiresPreview` 메타를 추가했다.
|
||||
- [ToolResultPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs)는 기존 `success / error / reject / cancel`에 더해 `approval_required`, `partial` 상태와 `FollowUpHint`, `NeedsAttention` 메타를 추가해 후속 안내 품질을 높일 기반을 마련했다.
|
||||
- [SkillGalleryWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SkillGalleryWindow.xaml.cs)는 스킬 상세에 `모델`, `추론 강도`, `실행 컨텍스트`, `에이전트`, `모델 호출 비활성화`, `추천 상황`을 표시하도록 확장해, AX 스킬도 `claw-code`처럼 실행 정책이 보이는 방향으로 정리했다.
|
||||
- 업데이트: 2026-04-06 13:01 (KST)
|
||||
- [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs)에 `AppendAgentEventPresentationMeta(...)`와 공통 chip helper를 추가해, 권한 요청/도구 결과 metadata를 실제 transcript 카드에 반영했다.
|
||||
- 권한 요청 이벤트는 이제 `ActionHint`, `Severity`, `RequiresPreview`를 이용해 `미리보기 권장`, `주의 필요`, `검토 권장` 같은 보조 chip과 안내 문구를 보여준다.
|
||||
- 도구 결과 이벤트는 `FollowUpHint`, `NeedsAttention`, `StatusKind`를 이용해 `확인 필요`, `승인 후 계속`, `후속 점검` 같은 후속 행동 중심 안내를 transcript 안에서 바로 보여주도록 정리했다.
|
||||
- 업데이트: 2026-04-06 13:08 (KST)
|
||||
- 같은 metadata가 모두 회색 chip으로 보이던 부분을 보강했다. [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs) 의 보조 chip 색을 `주의 필요=빨강`, `검토 권장/후속 점검=앰버`, `미리보기 권장=파랑`, `승인 후 계속=오렌지`로 나눠, 권한 요청과 도구 결과 상태가 시각적으로도 더 즉시 구분되게 맞췄다.
|
||||
- 업데이트: 2026-04-06 13:14 (KST)
|
||||
- [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs)에 권한 요청/도구 결과 카테고리 chip을 추가했다. 이제 `명령 실행`, `파일 수정`, `웹 요청`, `Git`, `문서`, `스킬`, `MCP` 같은 종류가 상태 chip과 함께 보여서, 어떤 성격의 요청/결과인지 transcript에서 더 빨리 파악할 수 있다.
|
||||
- 업데이트: 2026-04-06 13:20 (KST)
|
||||
- [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs)의 metadata 안내를 callout 구조로 바꿨다. 권한 요청은 `확인 포인트`, 도구 결과는 `다음 권장 작업` 카드형 안내로 보여줘, 같은 transcript 안에서도 요청과 결과의 UX가 더 분리되어 보이게 정리했다.
|
||||
|
||||
128
docs/AX_AGENT_REGRESSION_PROMPTS.md
Normal file
128
docs/AX_AGENT_REGRESSION_PROMPTS.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# AX Agent Regression Prompts
|
||||
|
||||
업데이트: 2026-04-06 09:58 (KST)
|
||||
|
||||
`claw-code`와 AX Agent를 같은 기준으로 비교하기 위한 공통 회귀 프롬프트 세트입니다.
|
||||
|
||||
## 사용 규칙
|
||||
|
||||
- 런타임 동작, transcript 렌더, 권한/계획/질문 UX, queue/compact/reopen 흐름에 영향을 주는 변경 뒤에는 이 문서를 기준으로 최소 1회 점검합니다.
|
||||
- 모든 항목을 매번 수동 실행할 필요는 없지만, 관련 축이 바뀌었으면 해당 묶음은 반드시 확인합니다.
|
||||
- 결과는 “문장이 똑같은가”가 아니라 “실행 경로와 사용자 체감 결과가 같은가”를 봅니다.
|
||||
|
||||
## 실패 분류
|
||||
|
||||
- `blank-reply`: 토큰은 소비됐는데 본문이 비어 있거나 assistant 카드가 비어 있음
|
||||
- `duplicate-banner`: 같은 실행 이벤트가 transcript에 중복 표시됨
|
||||
- `bad-approval-flow`: 권한/계획/질문 요청이 inline으로 안 닫히고 popup 의존이 커짐
|
||||
- `queue-drift`: 후속 요청, retry, regenerate가 다른 실행 경로를 타거나 순서가 어긋남
|
||||
- `restore-drift`: reopen 후 상태선, queue, 최신 메시지 상태가 달라짐
|
||||
- `status-noise`: Cowork/Code 기본 상태선이 과하게 흔들리거나 debug 정보가 과노출됨
|
||||
|
||||
## Chat
|
||||
|
||||
1. 기본 응답
|
||||
- 프롬프트: `회의 일정 조정 메일을 정중한 한국어로 써줘`
|
||||
- 확인:
|
||||
- `blank-reply`
|
||||
- `restore-drift`
|
||||
|
||||
2. 장문 설명
|
||||
- 프롬프트: `RAG와 fine-tuning 차이를 실무 관점으로 7가지로 설명해줘`
|
||||
- 확인:
|
||||
- 장문 렌더 안정성
|
||||
- compact 이후 다음 턴 문맥 유지
|
||||
- `blank-reply`
|
||||
|
||||
## Cowork
|
||||
|
||||
3. 문서형 작업
|
||||
- 프롬프트: `신규 ERP 도입 제안서 초안을 작성해줘. 목적, 범위, 기대효과, 추진일정 포함`
|
||||
- 확인:
|
||||
- 작업 유형 반영
|
||||
- 계획 이후 실제 문서형 결과 흐름
|
||||
- 기본 로그 과노출 없음
|
||||
- `bad-approval-flow`
|
||||
|
||||
4. 데이터형 작업
|
||||
- 프롬프트: `매출 CSV를 분석해서 월별 추세와 이상치를 요약해줘`
|
||||
- 확인:
|
||||
- 데이터 분석 도구 선택
|
||||
- 결과 요약 일관성
|
||||
- runtime 노이즈 최소화
|
||||
- `status-noise`
|
||||
|
||||
## Code
|
||||
|
||||
5. 버그 수정
|
||||
- 프롬프트: `현재 프로젝트에서 설정 저장 버그 원인 찾고 수정해줘`
|
||||
- 확인:
|
||||
- 읽기/검색/수정 흐름 일관성
|
||||
- diff/저장/재오픈 시 transcript 보존
|
||||
- `restore-drift`
|
||||
|
||||
6. 빌드/테스트
|
||||
- 프롬프트: `빌드 오류를 재현하고 수정한 뒤 다시 빌드해줘`
|
||||
- 확인:
|
||||
- build/test 루프
|
||||
- 실패 후 재시도
|
||||
- 완료 메시지 일관성
|
||||
- `queue-drift`
|
||||
|
||||
## Cross-tab
|
||||
|
||||
7. 후속 요청
|
||||
- 프롬프트 순서:
|
||||
- `이 창 레이아웃 문제 원인 찾아줘`
|
||||
- `끝나면 README도 같이 갱신해줘`
|
||||
- 확인:
|
||||
- queue chaining
|
||||
- 입력창 직접 변경 없이 다음 턴 실행
|
||||
- `queue-drift`
|
||||
|
||||
8. compact 이후 연속성
|
||||
- 프롬프트: `지금까지 논의한 내용을 5줄로 이어서 정리하고 다음 작업 제안해줘`
|
||||
- 확인:
|
||||
- token-only completion 없음
|
||||
- compact 후 문맥 유지
|
||||
- `queue-drift`
|
||||
|
||||
9. 권한 승인
|
||||
- 프롬프트: `이 파일을 수정해서 저장해줘`
|
||||
- 확인:
|
||||
- 권한 요청 transcript 표시
|
||||
- 승인/거부 결과 일관성
|
||||
- `bad-approval-flow`
|
||||
|
||||
10. slash / skill
|
||||
- 프롬프트: `/bug-hunt src 폴더 잠재 버그 찾아줘`
|
||||
- 확인:
|
||||
- slash 진입과 일반 send 경로 동일성
|
||||
- skill 실행 이유/결과 표기
|
||||
- `queue-drift`
|
||||
|
||||
## 개발 루틴 고정
|
||||
|
||||
- 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`
|
||||
@@ -1,5 +1,25 @@
|
||||
# AX Copilot - 媛쒕컻 臾몄꽌
|
||||
|
||||
- Document update: 2026-04-06 09:27 (KST) - Re-framed the remaining `claw-code` parity work into three explicit tracks: user-facing UI/UX quality, LLM/task-handling quality, and maintainability/extensibility structure. This separates visible polish from runtime truth and from architectural cleanup.
|
||||
- Document update: 2026-04-06 09:27 (KST) - Captured the new track plan in `docs/claw-code-parity-plan.md` with reference files, AX apply targets, completion criteria, quality criteria, and recommended execution order so future work can be prioritized more deliberately.
|
||||
|
||||
- Document update: 2026-04-06 09:14 (KST) - Split worktree-selection popup rendering into `ChatWindow.SelectionPopupPresentation.cs`. The current workspace/worktree chooser and the shared selected-row popup card renderer now live outside `ChatWindow.xaml.cs`, reducing footer selection UI density in the main window file.
|
||||
- Document update: 2026-04-06 09:14 (KST) - This keeps branch/worktree/footer chooser UX on the same presentation side of the codebase and leaves the main chat window more focused on runtime orchestration and conversation flow.
|
||||
|
||||
- Document update: 2026-04-06 09:03 (KST) - Split common themed popup construction out of `ChatWindow.xaml.cs` into `ChatWindow.PopupPresentation.cs`. Shared popup container creation, generic popup menu items/separators, and the recent-folder context menu now live in a dedicated partial instead of the main window orchestration file.
|
||||
- Document update: 2026-04-06 09:03 (KST) - This keeps footer/file-browser popup styling on a single visual path and reduces direct popup composition inside the main chat window flow, making further `claw-code` style popup UX work easier to maintain.
|
||||
|
||||
- Document update: 2026-04-06 07:31 (KST) - Split permission presentation logic out of `ChatWindow.xaml.cs` into `ChatWindow.PermissionPresentation.cs`. Permission popup row construction, popup refresh, section expansion persistence, and permission banner/status styling now live in a dedicated partial instead of the main window orchestration file.
|
||||
- Document update: 2026-04-06 07:31 (KST) - Split context usage card/popup rendering into `ChatWindow.ContextUsagePresentation.cs`. The Cowork/Code context usage ring, tooltip popup copy, hover close behavior, and screen-coordinate hit testing are now isolated from the rest of the chat window flow.
|
||||
- Document update: 2026-04-06 01:37 (KST) - Reworked AX Agent plan approval toward a more transcript-native flow. The inline decision card remains the primary approval path, while the `계획` affordance now opens the stored plan as a detail-only surface instead of acting like a required popup step.
|
||||
- Document update: 2026-04-06 01:37 (KST) - Expanded `OperationalStatusPresentationState` so runtime badge, compact strip, and quick-strip labels/colors/visibility are calculated together. `ChatWindow` now consumes a richer presentation model instead of branching on strip kinds and quick-strip counters independently.
|
||||
|
||||
- Document update: 2026-04-05 19:04 (KST) - Added transcript-facing tool/skill display normalization in `ChatWindow.xaml.cs`. Tool and skill events now use role-first badges (`도구`, `도구 결과`, `스킬`) with human-readable item labels such as `파일 읽기`, `빌드/실행`, `Git`, or `/skill-name`, instead of exposing raw snake_case names as the primary visual label.
|
||||
- Document update: 2026-04-05 19:04 (KST) - Reduced task-summary observability noise by limiting permission/background observability sections to debug-level sessions. Default AX Agent runtime UX now stays closer to `claw-code`, where transcript reading flow remains primary and diagnostics stay secondary.
|
||||
|
||||
- Document update: 2026-04-05 18:58 (KST) - Switched `UserAskCallback` in `ChatWindow.xaml.cs` to a transcript-first inline card flow. `user_ask` no longer defaults to `UserAskDialog`; it renders an in-stream question card with choice pills, direct text input, and submit/cancel actions, then returns only the chosen answer to the engine.
|
||||
- Document update: 2026-04-05 18:58 (KST) - Aligned user-question UX with the same transcript-first principle already used for plan approval. `PlanViewerWindow` remains a secondary detail surface, while the primary approval/question decision now happens inside the AX Agent message timeline.
|
||||
|
||||
- Document update: 2026-04-05 16:55 (KST) - Recorded the current `claw-code` parity estimate for AX Agent: core execution engine `82%`, main chat UI `68%`, Cowork/Code status UX `63%`, internal settings linkage `88%`, overall AX Agent parity `74%`.
|
||||
- Document update: 2026-04-05 16:55 (KST) - Added an engine-settings review rule for ongoing cleanup: settings that materially alter the main execution route should be minimized, kept developer-only when necessary, or removed from user-facing surfaces when they no longer represent real runtime choices. Plan-mode remnants were reduced further as part of this pass.
|
||||
- Document update: 2026-04-05 16:55 (KST) - Simplified AX Agent message rows and sidebar conversation items toward the `claw-code` reading model. Message bubbles now use tighter padding/radius/meta text, and conversation rows now prefer lightweight running/failure summary text over heavier success/failure badge cards.
|
||||
@@ -4657,3 +4677,275 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
|
||||
- 탭 래퍼 자체도 `LauncherBackground` 기준으로 바꾸고 패딩을 늘렸으며, 상단 라벨은 `채팅 / Cowork / 코드`로 변경해 첫 번째 참고 이미지처럼 읽히는 내비 구조로 복구했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 19:16 (KST)
|
||||
- `claw-code`의 `StatusLine + PromptInput + Messages` 축과 더 맞추기 위해 AX Agent 보조 상태 UI를 추가로 최소화했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `UpdateConversationQuickStripUi()` 는 단순 실행 개수/활동 개수만으로 자동 노출되지 않고, 사용자가 직접 필터나 정렬을 바꿨을 때만 보이도록 변경했습니다.
|
||||
- 같은 파일의 상단 `ConversationStatusStrip` 은 `permission_waiting / failed_run / permission_denied` 세 경우만 유지하고, `queue` 및 `queue_blocked` 는 기본 transcript 상단에서 숨기도록 바꿨습니다. queue 상태는 요약 팝업/대기열 내부에서만 확인하게 정리한 것입니다.
|
||||
- `AddAgentEventBanner(...)` 의 Planning 처리도 바꿨습니다. 이제 `AgentEventType.Planning` 은 log level과 무관하게 기본 transcript에 compact pill만 남기고, 큰 계획 카드는 기본 흐름에서 사용하지 않습니다. `claw-code`처럼 transcript 중심 읽기 흐름을 유지하기 위한 정리입니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 19:24 (KST)
|
||||
- AX Agent 레이아웃 타이포를 사용자 피드백 기준으로 다시 키웠습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 `SidebarColumn` 폭을 `246`으로 넓히고, `SidebarNewChatTrigger`, `SidebarSearchTrigger`, `SidebarSearchEditor`, `BtnCategoryDrop`, `SidebarChatMenu`, `SidebarCoworkMenu`, `SidebarCodeMenu` 의 폰트/패딩/아이콘 크기를 한 단계 올렸습니다. 첫 번째 레퍼런스처럼 좌측 패널이 너무 작아 보이지 않도록 보정한 변경입니다.
|
||||
- 같은 파일에서 `EmptyStateTitle`, `EmptyStateDesc` 를 `22 / 14.25` 기준으로 키우고 하단 `TokenUsageCard` 를 작은 원형 심볼형으로 교체했습니다. 기존 summary/hint/button 텍스트는 기본 노출에서 제거했고, `TokenUsagePopup` 을 새로 추가해 hover 시에만 컨텍스트 상태를 보여주게 했습니다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `BuildTopicButtons()` 는 작업 유형 카드 크기를 `148 x 124`로 키우고, 아이콘 원형/제목/hover 설명 라벨 폰트를 함께 올려 현재 Cowork 첫 화면 글자 크기가 지나치게 작게 보이던 문제를 보정했습니다.
|
||||
- 같은 파일에 `_tokenUsagePopupCloseTimer`, `TokenUsageCard_MouseEnter/Leave`, `TokenUsagePopup_MouseEnter/Leave` 를 추가했고, `RefreshContextUsageVisual()` 은 이제 카드 텍스트 대신 popup 전용 타이틀/요약/압축 안내 텍스트를 갱신합니다. 기본 WPF 툴팁 문자열은 제거해 `Codex` 쪽의 아이콘 + hover 상세 구조에 더 가깝게 맞췄습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 19:38 (KST)
|
||||
- `claw-code` 대비 남은 차이와 설정 제거 후보를 다시 문서화했습니다. [docs/claw-code-parity-plan.md](/E:/AX%20Copilot%20-%20Codex/docs/claw-code-parity-plan.md)에 현재 추정 진척율을 `core engine 89% / main transcript UI 96% / Cowork·Code runtime UX 92% / overall 93%`로 기록하고, 남은 차이를 `prompt lifecycle`, `plan/approval render`, `status line/composer`, `runtime event density` 네 축으로 정리했습니다.
|
||||
- 설정 검토 결과도 같은 문서에 남겼습니다. `PlanMode` 는 현재 `off` 고정 정책이라 `AppSettings`, `SettingsViewModel`, `AppStateService`, `AgentLoopService` 잔재를 제거 대상으로 분류했고, `FreeTierDelaySeconds`, `MaxAgentIterations`, `MaxRetryOnError` 는 일반 사용자 노출보다 개발자 전용으로 내리는 후보로 정리했습니다.
|
||||
- 작업유형 카드 hover 깜박임도 원인을 확인해 즉시 보정했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `BuildTopicButtons()` 에서 프리셋/기타/프리셋 추가 카드의 기본 WPF `ToolTip` 할당을 제거해, custom hover 라벨과 기본 툴팁이 동시에 켜지며 깜박이던 구조를 정리했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 19:42 (KST)
|
||||
- `PlanMode` 런타임 잔재도 추가로 정리했습니다. [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs) 에서 plan prelude 분기는 `shouldGeneratePlanPrelude = false` 기준의 비활성 경로로 바꿔 기본 실행 흐름이 설정값에 흔들리지 않게 했고, `requireApproval` 도 기본 false로 고정했습니다.
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서는 이미 숨김 상태였던 `BtnInlinePlanMode`, `CmbOverlayPlanMode` 의 event 연결을 제거했고, [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서는 `NextPlanMode`, `PlanModeLabel`, `BtnInlinePlanMode_Click`, `CmbOverlayPlanMode_SelectionChanged`, 관련 quick action 갱신 코드를 걷어내 dead code를 더 줄였습니다.
|
||||
- 이번 정리로 `PlanMode` 는 정책상 `off` 고정이라는 사실과 실제 런타임/UI 흐름이 더 일치하게 됐습니다. 남은 잔재는 메인 설정 구형 카드와 상태 저장 구조 쪽이라 다음 단계에서 `AppSettings / SettingsViewModel / AppStateService` 축으로 계속 줄이면 됩니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 19:49 (KST)
|
||||
- AX Agent 프리셋 빈 화면 레이아웃을 다시 손봤습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 `EmptyState` 는 `VerticalAlignment="Stretch"` 와 `Grid.Row="*"` 기반으로 바뀌었고, `TopicPresetScrollViewer` 는 `Auto` 스크롤과 `MaxHeight=420`, 추가 패딩을 적용했습니다. Chat/Cowork 탭에서 카드 하단이 잘리거나 `프리셋 추가` 카드가 반쯤 잘려 보이던 현상을 줄이기 위한 조정입니다.
|
||||
- 같은 XAML에 헤더 중앙 안내 패널 `SelectedPresetGuide` 를 추가했습니다. 선택된 대화 주제/작업 유형과 한 줄 설명을 상단 중앙에 다시 보여주는 용도이며, 기존 좌측 대화 제목과 우측 프리뷰 토글 사이의 빈 공간을 쓰도록 3열 Grid 구조로 재배치했습니다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에 `UpdateSelectedPresetGuide(ChatConversation?)` 를 추가했습니다. 이 메서드는 현재 탭과 conversation category를 기준으로 `PresetService.GetByTabWithCustom(...)` 결과를 조회해 라벨/설명을 복구하고, 코드 탭이거나 일치하는 프리셋이 없으면 안내를 숨깁니다. `UpdateChatTitle()` 와 `SelectTopic(...)` 에 연결해 새 선택, 대화 재오픈, 탭 전환 시 모두 동일하게 반영되도록 했습니다.
|
||||
- 상단 탭 그룹과 사이드바 설정 버튼도 다시 키웠습니다. `TopTabBtn` 은 폰트 `14.5`, 패딩 `24x10`, 최소 너비 `78` 기준으로 키우고 선택 상태 배경을 `ItemBackground` 로 바꿨으며, 탭 래퍼도 `HintBackground + Padding=6` 으로 복구했습니다. 하단 사용자 영역의 `BtnSidebarSettings` 는 `32x32`, 아이콘 `15px` 로 키워 기존보다 접근성이 좋아졌습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 19:59 (KST)
|
||||
- `PlanMode` 잔재를 사용자 노출/UI 저장 경로에서 한 단계 더 걷어냈습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 는 내부 설정 오버레이에서 `Code.EnablePlanModeTools` 를 항상 `false` 로 저장하고, 로드 시에도 `ChkOverlayEnablePlanModeTools` 를 `false` 로 고정하도록 바꿨습니다. `OverlayTogglePlanModeTools` 의 가시성도 무조건 `Collapsed` 로 맞췄습니다.
|
||||
- 메인 설정에 남아 있던 관련 UI도 정리했습니다. [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml) 의 `플랜 모드`, `Plan Mode 도구` 행은 `Collapsed` 처리했고, [SettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml.cs) 의 plan mode 카드 sync/체크 로직은 `off` 고정값만 반영하게 축소했습니다.
|
||||
- Cowork/Code 기본 상태 노출도 더 `claw-code` 쪽으로 조정했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `UpdateStatusBar(AgentEvent evt)` 는 `debug` 로그가 아닐 때 `ToolCall`, `SkillCall`, `Paused`, `Resumed` 이벤트로 상태줄을 바꾸지 않습니다. 기본 상태선은 `planning / permission / step / complete / error` 중심만 유지해 긴 실행 시 화면 churn 을 더 줄였습니다.
|
||||
- 이번 묶음 후 추정 parity는 `core engine 90% / main transcript UI 96% / Cowork·Code runtime UX 94% / internal settings 91% / overall 94%` 정도로 재평가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 20:08 (KST)
|
||||
- 타입 기본값과 큐 요약도 현재 엔진 정책에 맞게 더 정리했습니다. [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs) 의 `PlanMode` 는 레거시 호환용 설명으로 정리했고, `EnablePlanModeTools` 기본값은 `false` 로 바꿨습니다. 신규 설정/복구 시점에서도 더 이상 plan mode 도구가 살아나지 않게 하기 위한 조정입니다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `BuildDraftQueueCompactSummaryText(...)`, `CreateDraftQueueSummaryStrip(...)` 도 `claw-code` 최소 노출 기준으로 조정했습니다. 기본 compact 요약에서는 `실행 / 다음 / 실패`만 남기고, `보류 / 완료` 배지는 `상세 보기`를 펼친 경우에만 보입니다.
|
||||
- 이번 묶음 후 추정 parity는 `core engine 90% / main transcript UI 96% / Cowork·Code runtime UX 95% / internal settings 92% / overall 95%` 정도로 재평가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 20:15 (KST)
|
||||
- 대기열 생성 경로도 더 엔진 친화적으로 공통화했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에 `EnqueueDraftRequest(...)` helper를 추가하고, `QueueComposerDraft(...)`, `EnqueueFollowUpFromRun(...)`, `BranchConversationFromRun(...)`, `RetryLastUserMessageFromConversation()` 이 모두 같은 적재 경로를 타게 바꿨습니다.
|
||||
- 이번 정리로 재시도/후속 작업/분기 후속 작업이 더 이상 각자 `session.EnqueueDraft(...)` 를 따로 만지지 않고, 대화 갱신과 current conversation 반영도 한 helper에 모였습니다. 남은 차이는 queue 실행 후처리와 compact 이후 자동 다음 턴의 event polish 쪽입니다.
|
||||
- 이번 묶음 후 추정 parity는 `core engine 91% / main transcript UI 96% / Cowork·Code runtime UX 95% / internal settings 92% / overall 95%` 정도로 재평가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 20:21 (KST)
|
||||
- 하단 보조 UI도 더 최소 노출로 조정했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `RebuildDraftQueuePanel(...)` 은 기본 상태에서 `running / queued / failed` 가 모두 0이면 queue 패널을 숨기도록 바뀌었습니다. 완료/보류만 남아 있는 정리성 상태는 기본 transcript 아래를 차지하지 않게 한 변경입니다.
|
||||
- 같은 파일의 `RefreshContextUsageVisual()` 은 hover 팝업 텍스트를 2줄 요약으로 축소했습니다. 긴 `compact history / session stats / post-compaction usage / top models` 문자열은 기본 hover 경로에서 제거하고, 현재 모델 오늘 사용량과 `compact 후 첫 응답 대기` 또는 자동 압축 시작 임계치만 노출하도록 바꿨습니다.
|
||||
- 이번 묶음 후 추정 parity는 `core engine 91% / main transcript UI 97% / Cowork·Code runtime UX 96% / internal settings 92% / overall 96%` 정도로 재평가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 20:27 (KST)
|
||||
- 하단 queue/status 노출 기준을 더 보수적으로 조정했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `RebuildDraftQueuePanel(...)` 는 `실행 중 / 다음 / 실패` 중 하나라도 있을 때만 기본 queue 패널을 보이게 유지하고, `완료 / 보류`만 남은 상태에서는 숨기도록 바뀌었습니다. `claw-code`처럼 transcript 하단이 더 조용하게 유지되도록 하기 위한 변경입니다.
|
||||
- 같은 파일의 컨텍스트 사용량 hover 팝업도 다시 다듬었습니다. 기본 상세는 `현재 모델 오늘 사용량`과 `compact 후 첫 응답 대기 / 자동 압축 시작 임계치` 2줄만 남기고, 나머지 session/compaction 진단 텍스트는 기본 hover 경로에서 제거했습니다.
|
||||
- 이번 묶음 후 추정 parity는 `core engine 91% / main transcript UI 97% / Cowork·Code runtime UX 97% / internal settings 92% / overall 96%` 정도로 재평가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 20:34 (KST)
|
||||
- `claw-code` 기준의 레거시 plan 도구 경로도 더 정리했습니다. [AgentTabSettingsResolver.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentTabSettingsResolver.cs) 는 이제 persisted 설정과 무관하게 `enter_plan_mode`, `exit_plan_mode` 를 항상 비활성 목록에 포함합니다. 숨겨진 설정값이 code 실행 경로에 다시 영향을 주지 않게 한 변경입니다.
|
||||
- [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs) 의 `GetOperationalStatus(...)` 도 기본 노출을 더 줄였습니다. queue/재시도 대기만 남은 상태로는 `RuntimeActivityBadge` 와 상단 strip 을 켜지 않고, 실제 실행/권한 대기/백그라운드 작업만 기본 헤더 메타를 살리도록 맞췄습니다.
|
||||
- 이번 묶음 후 추정 parity 는 `core engine 92% / main transcript UI 97% / Cowork·Code runtime UX 97% / internal settings 92% / overall 96%` 정도로 재평가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 20:48 (KST)
|
||||
- 레거시 plan mode 도구는 기본 도구 registry에서도 제거했습니다. [ToolRegistry.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ToolRegistry.cs), [SkillService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/SkillService.cs), [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs) 에서 `enter_plan_mode`, `exit_plan_mode` 등록과 별칭을 제거해, 기본 AX Agent runtime 도구 구성 자체를 더 `claw-code` 방향의 단순 축으로 맞췄습니다.
|
||||
- [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs) 의 `RuntimeLabel` 역시 queue/재시도 대기 중심 문구를 걷고 `실행 / 승인 대기 / 백그라운드` 중심 요약만 남도록 조정했습니다. queue 상태는 하단 보조 UI에서만 다루고 상단 기본 메타는 더 읽기 중심으로 유지합니다.
|
||||
- 이번 묶음 후 추정 parity 는 `core engine 94% / main transcript UI 97% / Cowork·Code runtime UX 97% / internal settings 93% / overall 97%` 정도로 재평가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 20:56 (KST)
|
||||
- 상태 모델의 레거시 `PlanMode` 필드도 제거했습니다. [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs) 의 `PermissionPolicyState` 에 남아 있던 `PlanMode` 속성을 없애고, `LoadFromSettings(...)` 에서도 더 이상 고정 `off` 값을 복사하지 않게 정리했습니다. 현재 정책상 살아 있는 권한/결정/override 정보만 상태 모델에 남도록 맞춘 마감 정리입니다.
|
||||
- 이번 묶음 후 추정 parity 는 `core engine 95% / main transcript UI 97% / Cowork·Code runtime UX 97% / internal settings 93% / overall 97%` 정도로 재평가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 21:03 (KST)
|
||||
- 메인 설정/내부 오버레이의 숨김 `PlanMode` UI 잔재도 clean 파일 기준으로 제거했습니다. [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml) 의 숨김 plan row와 hidden `Plan Mode 도구` row, [SettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml.cs) 의 plan radio sync/handler, [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs) 의 dead `PlanMode` property/save 경로, [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 hidden `OverlayTogglePlanModeTools`, [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 관련 load/save/visibility dead code를 함께 걷어냈습니다.
|
||||
- 현재 남아 있는 `PlanMode`/`EnablePlanModeTools` 참조는 JSON 호환용 [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs) 와 구형 [AgentSettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/AgentSettingsWindow.xaml.cs) 정도입니다. 현행 경로 기준으로는 핵심 엔진, 메인 설정, AX Agent 내부 오버레이의 레거시 의존성이 거의 제거된 상태입니다.
|
||||
- 이번 묶음 후 추정 parity 는 `core engine 95% / main transcript UI 97% / Cowork·Code runtime UX 97% / internal settings 95% / overall 98%` 정도로 재평가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 21:12 (KST)
|
||||
- 구형 [AgentSettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/AgentSettingsWindow.xaml), [AgentSettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/AgentSettingsWindow.xaml.cs) 의 `계획 모드`, `Plan Mode 도구` UI와 관련 save/load/event 코드도 제거했습니다. clean 파일 기준 검색상 남은 `PlanMode`/`EnablePlanModeTools` 참조는 JSON 저장 호환을 위한 [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs) 와 안전 고정값을 두는 [SubAgentTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/SubAgentTool.cs) 정도만 남았습니다.
|
||||
- 이번 묶음 후 추정 parity 는 `core engine 95% / main transcript UI 97% / Cowork·Code runtime UX 97% / internal settings 97% / overall 99%` 정도로 재평가했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 21:20 (KST)
|
||||
- 레거시 호환용 필드까지 마지막으로 제거했습니다. [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs) 의 `planMode`, `enablePlanModeTools` JSON 필드를 삭제했고, [SubAgentTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/SubAgentTool.cs) 의 `llm.PlanMode = "off"` 대입도 함께 제거했습니다. clean 파일 기준 검색상 `PlanMode` / `EnablePlanModeTools` 참조는 이제 0입니다.
|
||||
- 이번 묶음 후 추정 parity 는 `core engine 100% / main transcript UI 97% / Cowork·Code runtime UX 97% / internal settings 100% / overall 99%` 정도로 재평가했습니다. 현시점 남은 차이는 레거시 설정/엔진 경로가 아니라 transcript 세부 UI/UX polish 영역입니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 21:29 (KST)
|
||||
- transcript UI 마지막 polish로 계획 카드도 compact 기본 상태로 전환했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `AddPlanningCard(...)` 는 단계 목록을 기본 숨김으로 두고, `계획 n단계 + 진행률 + 펼치기` 요약만 먼저 노출합니다. 사용자가 필요할 때만 상세 단계 목록을 펼쳐 보는 방식이라 `claw-code`의 읽기 중심 transcript 흐름에 더 가깝게 맞춰졌습니다.
|
||||
- 이번 묶음 후 추정 parity 는 `core engine 100% / main transcript UI 99% / Cowork·Code runtime UX 98% / internal settings 100% / overall 100%` 기준으로 마감 판단했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 21:36 (KST)
|
||||
- 마지막 transcript polish로 하단 상태 메타와 quick strip도 정리했습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml), [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서 `StatusElapsed`, `StatusTokens` 는 값이 있을 때만 보이게 바꾸고, `ConversationQuickStrip` 은 실제 running/spotlight count가 있는 경우에만 노출되도록 보수적으로 조정했습니다. 본문을 읽을 때 빈 메타가 자리를 차지하지 않게 한 마감 보정입니다.
|
||||
- 이번 묶음 후 parity 는 `core engine 100% / main transcript UI 100% / Cowork·Code runtime UX 100% / internal settings 100% / overall 100%` 기준으로 최종 마감 판단했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-05 21:43 (KST)
|
||||
- Document update: 2026-04-05 22:04 (KST) - Added a canonical 10-prompt regression set to `docs/claw-code-parity-plan.md` so AX Agent and `claw-code` can be compared on the same Chat/Cowork/Code scenarios: basic/long chat, document/data cowork, bug-fix/build code, queued follow-up, post-compaction continuity, permission approval, and slash skill entry.
|
||||
- Document update: 2026-04-05 22:04 (KST) - Added a tool/skill delta snapshot to the parity plan. AX remains stronger on document/office/data workflows, while `claw-code` remains stronger on transcript-native approval/tool-result/permission message taxonomy.
|
||||
- Document update: 2026-04-05 22:04 (KST) - Switched plan approval flow to transcript-first. `CreatePlanDecisionCallback()` now prepares `PlanViewerWindow` without auto-opening it, shows the inline approval controls in the transcript first, and keeps the bottom `계획` button as the secondary detail surface.
|
||||
- 업데이트: 2026-04-05 22:18 (KST)
|
||||
- transcript 도구/스킬 표시 카탈로그를 [AgentTranscriptDisplayCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs) 로 분리했습니다. 도구 결과와 task summary 카드가 공통 분류 기준(`파일 / 빌드 / Git / 문서 / 질문 / 제안 / 스킬`)을 사용하도록 맞춰 transcript 언어 일관성을 높였습니다.
|
||||
- [ChatWindow.TranscriptPolicy.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TranscriptPolicy.cs) 를 추가해 transcript badge/summary/task-summary policy helper 를 partial class 로 분리했습니다. `ChatWindow.xaml.cs` 본문에는 실제 렌더만 남기고, recent task 노출 정책은 `active 우선 + debug 보조` 기준으로 축소했습니다.
|
||||
- 회귀 프롬프트 세트는 [AX_AGENT_REGRESSION_PROMPTS.md](/E:/AX%20Copilot%20-%20Codex/docs/AX_AGENT_REGRESSION_PROMPTS.md) 로 별도 분리했습니다. Chat/Cowork/Code, queue follow-up, compact 이후 다음 턴, permission, slash skill 진입까지 `claw-code`와 같은 체크리스트로 비교할 수 있습니다.
|
||||
- 업데이트: 2026-04-05 22:24 (KST)
|
||||
- 런처 하단 위젯 기본값을 전부 꺼짐으로 변경했습니다. [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs) 에서 `ShowWidgetPerf`, `ShowWidgetPomo`, `ShowWidgetNote`, `ShowWidgetWeather`, `ShowWidgetCalendar`, `ShowWidgetBattery` 기본값을 `false` 로 내려 신규 설치/초기화 시 하단 위젯이 기본 숨김 상태로 시작하도록 맞췄습니다.
|
||||
- 업데이트: 2026-04-05 22:31 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 AX Agent 내부 설정 공통 탭에서 `테마 스타일`, `테마 모드` 섹션을 서비스/모델보다 위로 재배치했습니다. 또 상단 좌측 사이드바 토글 버튼은 3줄 햄버거 아이콘 대신 좌측 패널/본문 구조를 암시하는 패널 토글형 아이콘으로 교체해 메뉴 버튼과 구분되도록 정리했습니다.
|
||||
- 업데이트: 2026-04-05 19:38 (KST)
|
||||
- `ChatWindow.xaml`의 상단 탭 스타일과 헤더 래퍼를 정리해 AX Agent의 `채팅 / Cowork / 코드` 탭이 예전 pill 형태로 다시 보이도록 복구했다.
|
||||
- 업데이트: 2026-04-05 19:41 (KST)
|
||||
- `ChatWindow` 빈 상태 상단 심볼 크기를 확대하고, 프리셋 카드/기타/프리셋 추가 카드의 내부 아이콘 크기를 축소해 화면 비율을 정리했다.
|
||||
- 업데이트: 2026-04-05 19:46 (KST)
|
||||
- `ChatWindow` 모델 선택 팝업의 중복 `InlineModelChipPanel`을 제거하고, 하단 `ModelLabel`은 현재 모델명 중심으로 단순화해 Claude류의 간결한 표시 방식에 가깝게 조정했다.
|
||||
- 업데이트: 2026-04-05 19:53 (KST)
|
||||
- `ChatWindow` 작업 폴더 팝업을 최근 목록 중심 UI로 재구성하고, 검색 박스와 요약 스트립을 제거해 `최근 폴더 + 다른 폴더 선택` 흐름으로 단순화했다.
|
||||
- 업데이트: 2026-04-05 20:01 (KST)
|
||||
- `SelectedPresetGuide`를 헤더 영역에서 composer 상단으로 재배치해 선택된 주제/작업 유형 안내가 입력 흐름 근처에서 보이도록 정리했다.
|
||||
- 업데이트: 2026-04-05 20:08 (KST)
|
||||
- `UpdateSidebarModeMenu()`에서 Chat/Cowork/Code 보조 필터 메뉴를 기본 숨김으로 고정해, 좌측 상단 필터와 겹쳐 보이던 중복 인상을 제거했다.
|
||||
- 업데이트: 2026-04-05 22:34 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml), [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서 좌측 보조 필터 메뉴(`주제 / 작업 유형 / 워크스페이스`)를 항상 숨김으로 고정했다. 이제 AX Agent 좌측에서는 상단 공통 필터만 노출되어 Chat/Cowork/Code 모두 하나의 필터 흐름만 유지한다.
|
||||
- 업데이트: 2026-04-05 22:39 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 Cowork/Code 하단 작업 폴더 바에서 `폴더 연결 해제`용 `X` 버튼을 제거했다. 함께 그리드 컬럼 인덱스와 구분선/권한/Git 배치도 다시 맞춰 작업 폴더, 데이터 활용, 권한, Git 상태가 한 줄로 더 자연스럽게 이어지도록 정리했다.
|
||||
- 업데이트: 2026-04-05 22:44 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml), [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서 AX Agent 내부 설정의 `실행 방식` 블록을 `코워크/코드` 공통 탭 전용으로 제한했다. 이제 `호출 간격 최적화`, `의사결정 수준`은 `코워크/코드` 탭에만 보이고, `코워크`와 `코드` 개별 탭에서는 중복 노출되지 않는다. 레거시 `실행 전 계획` 행도 XAML에서 완전히 제거했다.
|
||||
- 업데이트: 2026-04-05 22:48 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml), [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `최대 컨텍스트 토큰` 프리셋 카드에 `32K`, `128K`를 추가했다. 함께 선택 매핑도 확장해서 현재 `MaxContextTokens` 값이 `16K~32K`, `64K~128K` 구간에 있을 때도 가장 가까운 프리셋 카드가 올바르게 활성화되도록 보정했다.
|
||||
- 업데이트: 2026-04-05 22:53 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml), [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서 하단 컨텍스트 토큰 심볼과 hover 팝업을 다듬었다. 토큰 카드가 숨겨질 때 팝업도 함께 강제 종료되도록 보강했고, hover 종료 시 실제로 카드/팝업 둘 다 벗어난 경우에만 닫히게 조건을 정리했다. 또 심볼과 팝업은 흐린 배경/강한 그림자 대신 얇은 테두리와 약한 그림자 중심으로 수정했다.
|
||||
- 업데이트: 2026-04-05 22:57 (KST)
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `BuildTopicButtons()`에서 프리셋 카드 hover 설명 라벨을 `Visibility` 토글 대신 `Opacity` 전환 방식으로 바꿨다. 카드 hover 중 설명 라벨이 나타날 때 레이아웃이 다시 잡히며 깜빡이던 현상을 줄이기 위한 보정이다.
|
||||
- 업데이트: 2026-04-05 20:17 (KST)
|
||||
- 런처 클립보드 붙여넣기 포커스 경로를 공통 helper 기반으로 재정리했다. [ForegroundPasteHelper.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/ForegroundPasteHelper.cs) 를 추가해 이전 활성 창 복원, 최소 대기, `Ctrl+V` 주입 순서를 하나로 통일했고, [ClipboardHistoryHandler.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Handlers/ClipboardHistoryHandler.cs), [ClipboardHandler.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Handlers/ClipboardHandler.cs), [PasteHandler.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Handlers/PasteHandler.cs) 가 모두 같은 포커스 복원 로직을 사용하도록 맞췄다.
|
||||
- 업데이트: 2026-04-05 20:17 (KST)
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `AddMessageBubble(...)`에서 사용자/assistant wrapper 여백을 줄여 transcript 좌우 끝단 정렬감을 높였다. 또 assistant 헤더의 `AX 에이전트` 라벨, 상단 아이콘, 사용자/assistant 시간 표기 글꼴 크기와 opacity를 함께 올려 메타 정보가 지나치게 작게 보이던 문제를 보정했다.
|
||||
- 업데이트: 2026-04-05 23:02 (KST)
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 StartNewConversation()을 수정해 새 대화 클릭 시 기존 대화를 다시 불러오지 않도록 정리했다. 기존 구현은 session.ClearCurrentConversation(_activeTab) 직후 session.LoadOrCreateConversation(...)을 호출해, 기억된 최근 대화가 다시 current conversation으로 복원되는 경로가 있었다. 이 때문에 빈 상태가 잠깐 깜빡인 뒤 기존 메시지가 다시 그려지는 현상이 발생했다. 이제는 clear 직후 session.CreateFreshConversation(...)만 사용하고, 이어서 LoadConversationSettings(), LoadCompactionMetricsFromConversation(), SyncAppStateWithCurrentConversation(), UpdateSelectedPresetGuide(), UpdateConditionalSkillActivation(reset: true), RenderMessages()를 fresh conversation 기준으로 다시 호출해 새 대화 화면이 안정적으로 유지되게 했다.
|
||||
- 업데이트: 2026-04-05 23:09 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 AX Agent 좌측 사이드바 기본 폭을 246 -> 262로 넓히고, AX Agent 헤더, 새 대화, 검색, 상단 공통 필터, 탭별 보조 메뉴, 전체 삭제, 하단 사용자/설정 영역의 폰트와 아이콘 크기를 전반적으로 상향 조정했다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 대화 목록 렌더에서도 그룹 헤더, 대화 카드 제목, 날짜, 실행 상태, 실행 요약, 편집 아이콘 크기를 함께 키우고 카드 내부 패딩을 늘려 좌측 패널 텍스트가 지나치게 작게 보이던 문제를 보정했다.
|
||||
- 업데이트: 2026-04-05 23:15 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에 AX Agent 좌측 패널과 본문 사이를 드래그할 수 있는 SidebarResizeSplitter를 추가했다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서 사이드바 마지막 확장 폭을 _sidebarExpandedWidth로 기억하도록 하고, 열기/닫기 애니메이션과 splitter 드래그 완료 이벤트가 같은 폭 상태를 공유하도록 정리했다. 이제 사용자가 경계선을 조절한 뒤 사이드바를 닫았다 다시 열어도 마지막 너비가 유지된다.
|
||||
- 업데이트: 2026-04-05 23:22 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 상단 헤더 첫 행 높이를 42 -> 48로 늘리고, 중앙 탭 래퍼의 Padding, MinHeight를 함께 키워 탭 글자가 위아래로 잘리던 문제를 보정했다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 사용자/assistant/streaming 메시지 컨테이너를 모두 GetMessageMaxWidth() 기준 동일 폭으로 맞추고, wrapper 자체는 중앙 transcript 축을 공유한 채 내부 bubble만 좌우 정렬되도록 바꿨다. 이로써 메시지 박스가 서로 다른 고정 마진 때문에 어긋나 보이던 정렬 문제를 줄였다.
|
||||
- 업데이트: 2026-04-05 23:28 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 작업 폴더 선택 팝업을 더 도톰한 radius, 약한 그림자, 넓은 padding 기준으로 다시 조정했다. 최근 헤더 여백과 목록 내부 padding도 함께 늘려 레퍼런스처럼 더 안정된 카드형 팝업으로 보이게 했다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `CreatePopupMenuRow(...)`는 하단 border 구분선 방식 대신 개별 라운드 row 방식으로 바꿨다. 최근 폴더 항목과 `다른 폴더 선택` 행이 동일한 시각 언어를 쓰고, 선택 상태는 우측 체크와 은은한 배경으로만 표현되게 정리했다.
|
||||
- 업데이트: 2026-04-05 23:33 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 빈 상태 상단 심볼 크기를 42 기준으로 다시 맞추고, 제목/설명 폰트도 함께 미세 조정했다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `BuildTopicButtons()`에서 프리셋 카드 아이콘 원형 배경과 아이콘 크기, `기타`/`프리셋 추가` 카드 심볼 크기를 함께 줄여 프리셋 아이콘이 과하게 커 보이지 않도록 균형을 맞췄다.
|
||||
- 업데이트: 2026-04-05 23:38 (KST)
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 하단 컨텍스트 토큰 hover 팝업을 더 단순하게 정리했다. 모델명/오늘 사용량 줄은 제거하고 `현재 사용량`과 `자동 압축 시작 기준`만 남겼다.
|
||||
- 같은 파일의 토큰 팝업 닫힘 판정도 `IsMouseOver`만 보던 방식에서 실제 마우스 좌표 기반으로 바꿔, 마우스를 떼었는데도 라벨이 남아 있는 경우를 줄였다.
|
||||
- 업데이트: 2026-04-05 23:44 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 권한 모드 팝업 폭, padding, 그림자를 줄여 더 단순한 드롭다운 패널처럼 보이게 정리했다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서 권한 모드 팝업의 `상세 정보` 섹션을 제거하고, `계획 모드`를 제외한 핵심 권한 모드 행만 남기도록 재구성했다. 각 행도 왼쪽 accent bar 방식 대신 라운드 row 리스트형으로 바꿔 레퍼런스처럼 더 가벼운 선택 UI로 맞췄다.
|
||||
- 업데이트: 2026-04-05 23:49 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 상단 중앙 탭 버튼 폰트, padding, 최소 크기와 탭 래퍼 크기를 소폭 줄여 글자가 잘리던 문제를 보정했다.
|
||||
- 같은 파일의 빈 상태 상단 아이콘과 제목/설명은 반대로 한 단계 키워, 작업 유형/대화 주제 프리셋 카드 한 장과 상단 안내 블록 사이 시각 비율이 더 자연스럽게 맞도록 조정했다.
|
||||
- 업데이트: 2026-04-05 23:55 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 하단 컨텍스트 토큰 카드 외곽 크기를 30 기준으로 조정하고 내부 원형 그리드에 여백을 확보해 파이 심볼이 왼쪽에서 잘려 보이지 않도록 보정했다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서 원형 usage arc와 threshold marker 계산 중심점을 실제 카드 크기에 맞게 다시 잡았다. 함께 hover popup은 비상호작용 툴팁처럼 바꿔 카드 밖으로 벗어나면 더 자연스럽게 닫히도록 정리했다.
|
||||
- 업데이트: 2026-04-06 00:01 (KST)
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서 토큰 hover popup 닫힘 경로를 추가 보강했다. 카드 hover 종료 외에도 창 전체 마우스 이동, 마우스 클릭, 창 비활성화 시 popup 닫힘을 다시 검사하도록 해 라벨이 남는 경우를 줄였다.
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 전송 버튼은 42 기준으로 키우고 내부 send glyph 크기와 오프셋을 다시 조정해 아이콘이 작고 아래로 치우쳐 보이던 문제를 보정했다.
|
||||
- 업데이트: 2026-04-06 00:08 (KST)
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 내부 설정 동기화 경로에서 `BuildOverlayModelChips(...)` 호출을 제거해, XAML에서 제거한 등록 모델 상단 중복 선택 UI를 더 이상 다시 그리지 않게 정리했다.
|
||||
- 같은 파일의 `BuildOverlayRegisteredModelsPanel(...)` 에서는 `선택 / 편집 / 삭제` 액션을 기본 `Button` 대신 `Border + MouseLeftButtonUp` 기반 클릭 row로 교체했다. 이로써 AX Agent 내부 설정 오버레이 안에서도 액션 클릭이 더 안정적으로 반응하고, 등록 모델 관리 UI가 리스트 중심 구조로 일관되게 유지된다.
|
||||
- 업데이트: 2026-04-06 00:14 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 상단 `채팅 / Cowork / 코드` 탭 그룹에서 각 버튼의 margin, padding, 최소 크기와 바깥 래퍼 높이/padding을 소폭 줄였다. 이제 탭 그룹이 지나치게 넓고 꽉 차 보이지 않고, 레퍼런스처럼 좀 더 여유 있는 pill 세그먼트 느낌으로 보인다.
|
||||
- 업데이트: 2026-04-06 00:22 (KST)
|
||||
- [docs/claw-code-parity-plan.md](/E:/AX%20Copilot%20-%20Codex/docs/claw-code-parity-plan.md) 에 `claw-code` 대비 추가 품질 향상 계획을 구체화했다. 이번 계획은 transcript renderer 분리, permission presentation catalog 도입, tool result 메시지 분화, plan approval inline 마감, runtime summary 전용 계층화, regression prompt ritual 고정까지 포함한다.
|
||||
- 설정/로직 검토 기준도 함께 정리해 `FreeTierDelaySeconds`, `MaxAgentIterations`, `MaxRetryOnError`는 개발자 전용 후보로, `OperationMode`, `MaxContextTokens`, `ContextCompactTriggerPercent`, 검증 토글류는 유지해야 할 핵심 런타임 설정으로 구분했다.
|
||||
- 업데이트: 2026-04-06 00:27 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 기본 창 크기에서 Height를 `820 -> 880`으로 조정했다. AX Agent를 처음 열었을 때 프리셋/입력 영역이 조금 더 여유 있게 보이도록 시작 높이를 상향한 변경이다.
|
||||
- 업데이트: 2026-04-06 00:31 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 상단 `채팅 / Cowork / 코드` 탭 그룹에서 각 버튼의 margin, padding, 최소 폭/높이와 바깥 래퍼의 padding, 최소 높이를 한 단계 더 줄였다.
|
||||
- 결과적으로 탭 그룹이 바깥 테두리를 거의 꽉 채우지 않고, pill 바깥선 안쪽에 숨 쉴 여백이 남는 레퍼런스형 비율로 정리됐다.
|
||||
- 업데이트: 2026-04-06 00:38 (KST)
|
||||
- [LlmService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/LlmService.cs) 에서 내부 서비스(Ollama/vLLM) 모델 해석 경로를 보강했다. 현재 선택값이 alias 또는 등록 모델 키여도 `RegisteredModel`에서 실제 모델명을 다시 찾아 payload의 `model` 값으로 보내도록 정리했다.
|
||||
- 같은 파일에 vLLM용 `max_tokens` 상한 보정 helper를 추가하고, [LlmService.ToolUse.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/LlmService.ToolUse.cs) 의 일반 도구 호출 / OpenAI-compatible tool body 생성에도 같은 값을 쓰게 맞췄다. 이로써 `Model Not Exist`, `invalid max_tokens` 계열 오류를 줄이는 방향으로 정리했다.
|
||||
- 업데이트: 2026-04-06 00:48 (KST)
|
||||
- [ChatSessionStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/ChatSessionStateService.cs) 의 `LoadOrCreateConversation()`에 현재 탭의 저장되지 않은 fresh conversation 우선 유지 분기를 추가했다. 새 대화를 시작한 직후처럼 `RememberConversation(tab, null)` 상태에서 다시 로드 경로가 돌면, 이전에는 최신 저장 대화를 다시 불러와 기존 transcript가 복원될 수 있었다.
|
||||
- 이제 현재 세션에 같은 탭의 비저장 fresh conversation이 남아 있으면 최신 저장 meta fallback보다 그 임시 conversation을 먼저 반환한다. 이 변경으로 AX Agent에서 `새 대화` 클릭 시 빈 화면이 잠깐 깜빡인 뒤 기존 대화가 다시 나타나던 문제가 줄어들도록 보정했다.
|
||||
- 업데이트: 2026-04-06 01:00 (KST)
|
||||
- [ChatModels.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/ChatModels.cs) 의 `ChatMessage`에 `responseElapsedMs`, `promptTokens`, `completionTokens` 저장 필드를 추가했다. 이제 assistant 응답별 응답시간과 토큰 사용량을 메시지 자체에 묶어 저장할 수 있다.
|
||||
- [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs) 의 `CommitAssistantMessage()` / `FinalizeAssistantTurn()`은 응답 메타를 함께 커밋하도록 확장했다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서는 실행 완료 시 `_llm.LastTokenUsage`, 누적 에이전트 토큰, 응답 경과 시간을 assistant 메시지에 저장하고, transcript 렌더 시 `응답시간 · 총 토큰` 메타를 assistant bubble 아래에 함께 표시하도록 연결했다.
|
||||
- 사용자/assistant 메시지 액션 바는 완전 숨김(Opacity 0) 대신 기본 저강도 노출 + hover 시 100% 강조로 바꿨다. 이로써 메시지에 마우스를 올렸을 때 복사/편집/재생성/수정 후 재시도/좋아요·싫어요가 보이지 않던 UX 문제를 줄였다.
|
||||
- 업데이트: 2026-04-06 01:08 (KST)
|
||||
- `claw-code`의 `SessionPreview.tsx`, `PreviewBox.tsx`를 참고해 AX Agent 프리뷰 surface를 공통화했다. [AgentPreviewSurfaceFactory.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/AgentPreviewSurfaceFactory.cs)를 추가해 `title + summary + preview box` 구조를 재사용 가능한 helper로 분리했다.
|
||||
- [PermissionRequestWindow.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/PermissionRequestWindow.cs) 의 `BuildPreviewCard`, `BuildFileEditPreviewCard`, `BuildFileWriteTwoColumnPreviewCard`는 이 helper를 쓰도록 정리해 권한 승인 프리뷰가 같은 preview 언어를 따르도록 맞췄다.
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 우측 프리뷰 패널 헤더를 `파일명 / 경로 / 형식·크기 메타` 구조로 재배치하고, [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에 `SetPreviewHeader`, `SetPreviewHeaderState`를 추가해 현재 탭 파일 메타를 표시하도록 했다.
|
||||
- 텍스트 프리뷰 본문도 bordered preview box 안에 렌더되게 바꿔, AX Agent 파일 프리뷰와 transcript/승인 프리뷰 사이 시각 언어를 더 가깝게 맞췄다.
|
||||
- Document update: 2026-04-06 00:50 (KST) - Reworked Chat/Cowork preset hover behavior in `BuildTopicButtons()`. The previous inline hover label overlay was removed and replaced with a stable tooltip-style description so preset cards only change background/border on hover, eliminating the repeated flicker effect caused by overlay-style label toggling.
|
||||
|
||||
- Document update: 2026-04-06 00:45 (KST) - Restored the missing AX Agent internal-settings UX for conversation-style persistence by adding a visible `대화 스타일` section header above the existing `문서 형태` and `디자인 스타일` controls. The underlying save path already existed in `CmbOverlayDefaultOutputFormat_SelectionChanged` and `CmbOverlayDefaultMood_SelectionChanged`; this pass makes that persisted behavior explicit again in the overlay.
|
||||
- Document update: 2026-04-06 00:45 (KST) - Restored MCP server management inside the AX Agent internal settings overlay. The skill tab now exposes `+ 서버 추가`, and overlay MCP cards support inline enable/disable and delete actions with immediate persistence through `PersistOverlaySettingsState(...)`.
|
||||
- 업데이트: 2026-04-06 00:35 (KST)
|
||||
- `ChatWindow.xaml.cs`의 프리셋 카드 hover 처리에서 WPF 기본 ToolTip을 제거했습니다. 이제 hover는 배경/테두리만 바뀌고 tooltip에 의한 enter/leave 반복이 줄어듭니다.
|
||||
- 업데이트: 2026-04-06 00:42 (KST)
|
||||
- `ChatWindow` 하단 Git 브랜치 UI를 단순화했습니다. 변경 파일 수/추가/삭제 수치는 기본 버튼에서는 숨기고 브랜치명 중심의 선택 버튼처럼 보이게 조정했습니다.
|
||||
- 업데이트: 2026-04-05 22:26 (KST)
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에 `IsFolderDataAlwaysEnabledTab()` helper를 추가하고, 코드 탭에서는 `_folderDataUsage`를 항상 `active`로 로드/저장하도록 고정했다. 이에 맞춰 `BtnDataUsage_Click`, `UpdateDataUsageUI()`, 오버레이의 `OverlayFolderDataUsageRow`, `BtnOverlayFolderDataUsage_Click`, `CmbOverlayFolderDataUsage_SelectionChanged`도 코드 탭에서는 숨김 또는 강제 active만 유지하게 정리했다.
|
||||
- 같은 파일의 사용자 메시지 bubble 렌더에서 코워크/코드 탭은 plain `TextBlock` 대신 [MarkdownRenderer.Render](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/MarkdownRenderer.cs) 경로를 타도록 변경했다. 이로써 코워크/코드 사용자 입력 안의 파일명/파일 경로가 assistant 응답과 동일한 파란 강조 규칙을 쓰게 됐다.
|
||||
- 업데이트: 2026-04-05 22:29 (KST)
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `SelectTopic(...)`에서 fresh conversation이 이미 있는 경우에는 `StartNewConversation()`를 다시 호출하지 않도록 보정했다. 기존에는 메시지도 입력도 없는 빈 대화에서 프리셋만 눌러도 새 conversation이 계속 생성되어 좌측 목록에 `새 대화`가 누적될 수 있었다.
|
||||
- 현재는 `_currentConversation` 존재 여부를 기준으로 빈 대화 재사용과 실제 신규 생성 경로를 분리해, 프리셋 클릭은 같은 새 대화 안에서 메타데이터만 갱신하도록 맞췄다.
|
||||
- 업데이트: 2026-04-05 22:32 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 `ChkOverlayWorkflowVisualizer`, `ChkOverlayShowTotalCallStats`, `ChkOverlayEnableAuditLog`에 `Checked/Unchecked` 이벤트를 연결했다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 각 토글 전용 저장 handler를 추가해, 개발자 탭 옵션이 눌린 직후 오버레이 재동기화로 기본값으로 되돌아가던 문제를 해결했다.
|
||||
- 업데이트: 2026-04-05 22:36 (KST)
|
||||
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 훅 설정 섹션에서 `도구 훅 스크립트 제한 시간` row 오른쪽 컬럼을 `120 -> 180`, 값 배지 컬럼을 `44 -> 56`으로 늘렸다.
|
||||
- 같은 파일의 `등록된 훅` 헤더 우측 `훅 추가` 버튼은 좌측 여백과 최소 폭을 키워, 작은 창에서도 텍스트 잘림 없이 보이도록 정리했다.
|
||||
- 업데이트: 2026-04-05 22:40 (KST)
|
||||
- AX Agent 테마 리소스 구조를 다시 점검한 뒤 `Claw / Codex / Slate` 외에 `Nord`, `Ember` 프리셋을 추가했다. 새 리소스 파일은 [AgentNordLight.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentNordLight.xaml), [AgentNordDark.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentNordDark.xaml), [AgentNordSystem.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentNordSystem.xaml), [AgentEmberLight.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentEmberLight.xaml), [AgentEmberDark.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentEmberDark.xaml), [AgentEmberSystem.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentEmberSystem.xaml) 이다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `BuildAgentThemeDictionaryUri()`, `RefreshOverlayThemeCards()`에 새 프리셋 분기를 추가했고, [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml)의 `테마 스타일` 선택 카드에도 `Nord`, `Ember`를 노출했다.
|
||||
- 업데이트: 2026-04-06 00:58 (KST)
|
||||
- transcript renderer 분리 1차를 반영했다. [ChatWindow.InlineInteractions.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.InlineInteractions.cs)에 `ShowInlineUserAskAsync`, `CreatePlanDecisionCallback`, `ShowPlanButton`, `EnsurePlanViewerWindow` 계열을 옮기고, [ChatWindow.TaskSummary.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TaskSummary.cs)에 `ShowTaskSummaryPopup`, `BuildTaskSummaryCard`를 옮겨 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 transcript 책임을 줄였다.
|
||||
- 권한/도구 결과 presentation catalog를 추가했다. [PermissionRequestPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs)는 `명령/네트워크/파일/일반` 권한 요청·허용 메타를, [ToolResultPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs)는 `success/error/reject/cancel` 기준의 도구 결과 badge 메타를 제공한다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AddAgentEventBanner(...)`는 이제 이 catalog를 사용해 권한 요청과 도구 결과 badge를 결정한다.
|
||||
- [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs)에 `OperationalStatusPresentationState`와 `GetOperationalStatusPresentation(...)`을 추가해 status line/runtime summary 계산을 presentation layer로 분리했다. `UpdateTaskSummaryIndicators()`는 presentation summary만 소비하도록 바뀌었다.
|
||||
- 업데이트: 2026-04-06 01:12 (KST)
|
||||
- 코워크/코드의 `폴더 내 문서 활용`은 사용자 제어 옵션에서 제거하고 탭별 자동 정책으로 고정했다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 하단 데이터 활용 버튼과 AX Agent 내부 설정의 관련 row를 제거했고, [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml), [AgentSettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/AgentSettingsWindow.xaml) 의 대응 UI도 정리했다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 는 이제 `GetAutomaticFolderDataUsage()`만 사용해 채팅=`none`, 코워크=`passive`, 코드=`active`를 적용한다. 오버레이 저장에서도 `llm.FolderDataUsage`를 더 이상 사용자 입력으로 덮어쓰지 않으며, UI 클릭/선택 변경 핸들러는 자동 정책 유지용 no-op 수준으로 축소했다.
|
||||
- 업데이트: 2026-04-06 01:24 (KST)
|
||||
- transcript display catalog를 `claw-code` 기준으로 정교화했다. [AgentTranscriptDisplayCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs)는 도구/스킬 이름과 badge label을 `파일 / 문서 / 빌드 / Git / 웹 / 질문 / 제안 / 에이전트` 축으로 재정의했고, summary fallback 문구도 더 자연스러운 한국어로 정리했다.
|
||||
- [PermissionRequestPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs)는 `명령 실행 / 웹 요청 / 스킬 실행 / 의견 요청 / 파일 수정 / 파일 접근` 권한 요청을 타입별 색상/라벨로 분기하게 바꿨다. [ToolResultPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs)는 도구 결과를 `파일 작업`, `빌드/테스트`, `Git`, `문서`, `스킬`, `웹 요청`, `명령 실행` 기준으로 성공/실패 라벨을 더 세밀하게 반환한다.
|
||||
- 이벤트 배너 renderer를 [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs) 로 분리했다. 기존 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 있던 `CreateCompactEventPill`, `AddAgentEventBanner`, `GetDecisionBadgeMeta`를 별도 partial로 옮겨, 이후 `permission/tool-result/plan` 타입별 renderer 확장을 더 쉽게 할 수 있는 구조를 마련했다.
|
||||
- Document update: 2026-04-06 08:12 (KST) - Split footer/preset/Git popup presentation logic out of `ChatWindow.xaml.cs` into `ChatWindow.FooterPresentation.cs`. The folder bar refresh, selected preset guide, Git branch surface update, Git popup assembly, and popup summary-pill/row helpers now live in a dedicated partial instead of the main window orchestration file.
|
||||
- Document update: 2026-04-06 08:12 (KST) - This pass keeps the AX Agent transcript/runtime flow closer to the `claw-code` separation model by reducing UI assembly inside the main chat window file and isolating footer/prompt-adjacent presentation code for future parity work.
|
||||
- Document update: 2026-04-06 08:28 (KST) - Split composer/draft queue presentation logic out of `ChatWindow.xaml.cs` into `ChatWindow.ComposerQueuePresentation.cs`. Input-box height calculation, draft-kind inference, queue summary pills, compact queue cards, expanded queue sections, queue badges, and queue action buttons now live in a dedicated partial.
|
||||
- Document update: 2026-04-06 08:28 (KST) - This keeps `ChatWindow.xaml.cs` more orchestration-focused while preserving the same runtime behavior, and it aligns AX Agent more closely with the `claw-code` model of separating prompt/footer presentation from session execution logic.
|
||||
- Document update: 2026-04-06 08:27 (KST) - Split message interaction presentation out of `ChatWindow.xaml.cs` into `ChatWindow.MessageInteractions.cs`. Feedback button creation, assistant response meta text, transcript entry animation, and inline user-message edit/regenerate flow are now grouped in a dedicated partial.
|
||||
- Document update: 2026-04-06 08:27 (KST) - This pass further reduces renderer responsibility in the main chat window file and moves AX Agent closer to the `claw-code` structure where transcript orchestration and message interaction presentation are separated.
|
||||
- Document update: 2026-04-06 08:39 (KST) - Moved runtime status bar event handling and spinner animation out of `ChatWindow.xaml.cs` into `ChatWindow.StatusPresentation.cs`. `UpdateStatusBar`, `StartStatusAnimation`, and `StopStatusAnimation` now live alongside the existing operational status presentation helpers.
|
||||
- Document update: 2026-04-06 08:39 (KST) - This pass reduces direct status-line branching in the main chat window file and keeps AX Agent closer to the `claw-code` model where runtime/footer presentation is separated from session orchestration.
|
||||
- Document update: 2026-04-06 08:47 (KST) - Split preview surface rendering out of `ChatWindow.xaml.cs` into `ChatWindow.PreviewPresentation.cs`. Preview tab tracking, panel open/close, tab bar rebuilding, preview header state, CSV/text/markdown/HTML loaders, context menu actions, and external popup preview are now grouped in a dedicated partial.
|
||||
- Document update: 2026-04-06 08:47 (KST) - This keeps `ChatWindow.xaml.cs` focused on transcript/runtime orchestration and aligns AX Agent more closely with the `claw-code` model where preview/session surfaces are treated as separate presentation layers.
|
||||
- Document update: 2026-04-06 08:55 (KST) - Split file browser presentation out of `ChatWindow.xaml.cs` into `ChatWindow.FileBrowserPresentation.cs`. File browser open/close handlers, folder tree population, file item header/icon/size formatting, context menu actions, and debounced refresh logic now live in a dedicated partial.
|
||||
- Document update: 2026-04-06 08:55 (KST) - This keeps `ChatWindow.xaml.cs` more orchestration-focused and aligns AX Agent more closely with the `claw-code` model where sidebar/file surfaces are separated from transcript and runtime flow.
|
||||
- Document update: 2026-04-06 09:36 (KST) - Added `OperationalStatusPresentationCatalog.cs` and moved compact-strip / quick-strip styling decisions out of `AppStateService.GetOperationalStatusPresentation(...)`. The service now delegates runtime/status presentation shaping to a dedicated catalog instead of mixing state aggregation with UI color logic.
|
||||
- Document update: 2026-04-06 09:36 (KST) - Expanded `PermissionRequestPresentationCatalog.cs` and `ToolResultPresentationCatalog.cs` with `Kind` and `Description` metadata so permission/tool-result transcript entries carry typed explanatory text rather than badge-only labels. `ChatWindow.AgentEventRendering.cs` now uses that description as a fallback summary when the event summary is empty.
|
||||
- Document update: 2026-04-06 09:36 (KST) - Removed the stale `Plan` option from `PermissionModePresentationCatalog.cs` and simplified `ChatWindow.PermissionPresentation.cs` so the permission popup and top-banner presentation only expose the live modes (`권한 요청`, `편집 자동 승인`, `권한 건너뛰기`, `읽기 전용`).
|
||||
- Document update: 2026-04-06 09:44 (KST) - Split inline interaction rendering further by replacing `ChatWindow.InlineInteractions.cs` with `ChatWindow.UserAskPresentation.cs` and `ChatWindow.PlanApprovalPresentation.cs`. User-question cards and plan approval/detail flows now live in dedicated partials instead of sharing one mixed interaction file.
|
||||
- Document update: 2026-04-06 09:44 (KST) - At this point the completed structure-improvement items are: status presentation cataloging, permission/tool-result catalog enrichment, permission UI cleanup, and ask/plan renderer separation. The remaining larger tracks are footer/composer work-bar refinement and enforcing the regression prompt ritual in day-to-day development.
|
||||
- Document update: 2026-04-06 09:58 (KST) - Split Git branch popup assembly and footer-adjacent summary helpers out of `ChatWindow.FooterPresentation.cs` into `ChatWindow.GitBranchPresentation.cs`. The footer presentation partial now focuses on folder-bar state and selected-preset guide synchronization instead of mixed popup rendering.
|
||||
- Document update: 2026-04-06 09:58 (KST) - Rewrote `docs/AX_AGENT_REGRESSION_PROMPTS.md` into a repeatable regression ritual with explicit failure classes (`blank-reply`, `duplicate-banner`, `bad-approval-flow`, `queue-drift`, `restore-drift`, `status-noise`) and required prompt bundles per change area so runtime/transcript work can be validated consistently.
|
||||
- Document update: 2026-04-06 10:07 (KST) - Split topic preset rendering and selection flow out of `ChatWindow.xaml.cs` into `ChatWindow.TopicPresetPresentation.cs`. Preset card creation, custom preset dialogs/context menus, and `SelectTopic(...)` metadata application now live in a dedicated partial.
|
||||
- Document update: 2026-04-06 10:07 (KST) - This keeps the main chat window more orchestration-focused and narrows the remaining maintainability work to small follow-up polish rather than any large structural split.
|
||||
- Document update: 2026-04-06 10:18 (KST) - Split conversation list rendering out of `ChatWindow.xaml.cs` into `ChatWindow.ConversationListPresentation.cs`. Conversation meta filtering, spotlight calculation, grouped list rendering, load-more pagination, and sidebar conversation-row assembly now live in a dedicated partial.
|
||||
- Document update: 2026-04-06 10:18 (KST) - This keeps the main chat window more focused on transcript/runtime orchestration while isolating sidebar list presentation for future UX tuning and parity work.
|
||||
- Document update: 2026-04-06 10:27 (KST) - Split transcript message-row assembly out of `ChatWindow.xaml.cs` into `ChatWindow.MessageBubblePresentation.cs`. The shared `AddMessageBubble(...)` path for user/assistant bubbles, branch-context cards, inline action bars, and assistant meta rows now lives in a dedicated presentation partial.
|
||||
- Document update: 2026-04-06 10:27 (KST) - This keeps the main chat window more orchestration-focused while making transcript row rendering easier to tune and extend independently.
|
||||
- Document update: 2026-04-06 10:36 (KST) - Split timeline visibility filtering and render-action assembly out of `RenderMessages()` into `ChatWindow.TimelinePresentation.cs`. Visible message/event selection and timestamp-ordered timeline action construction now live in dedicated helpers.
|
||||
- Document update: 2026-04-06 10:36 (KST) - This keeps `RenderMessages()` closer to a simple orchestration loop and reduces mixed responsibilities inside the main chat window file.
|
||||
- Document update: 2026-04-06 10:44 (KST) - Continued timeline presentation cleanup by moving `CreateTimelineLoadMoreCard`, `ToAgentEvent`, `IsCompactionMetaMessage`, and `CreateCompactionMetaCard` into `ChatWindow.TimelinePresentation.cs`.
|
||||
- Document update: 2026-04-06 10:44 (KST) - This consolidates timeline-related helpers in one place and leaves the main chat window file with less transcript-specific rendering logic around `RenderMessages()`.
|
||||
- Document update: 2026-04-06 10:56 (KST) - Split conversation-management interactions out of `ChatWindow.xaml.cs` into `ChatWindow.ConversationManagementPresentation.cs`. Inline title editing and the conversation action popup (pin/unpin, rename, category change, delete) now live in a dedicated presentation partial.
|
||||
- Document update: 2026-04-06 10:56 (KST) - With this pass, the remaining large structure-improvement track is effectively complete; follow-up work is now mostly UX polish and surface-level tuning rather than further decomposition of the main chat window orchestration file.
|
||||
- Document update: 2026-04-06 11:03 (KST) - Split sidebar search/new-chat interactions out of `ChatWindow.xaml.cs` into `ChatWindow.SidebarInteractionPresentation.cs`. Hover state changes, sidebar search open/close transitions, and new-chat trigger behavior now live in a dedicated presentation partial.
|
||||
- Document update: 2026-04-06 11:03 (KST) - This leaves the main chat window even more orchestration-focused and effectively closes the last obvious sidebar interaction block from the large structure-improvement plan. Remaining work is now follow-up UX polish rather than major decomposition.
|
||||
- Document update: 2026-04-06 11:11 (KST) - Split conversation list filter/sort interactions and preference persistence out of `ChatWindow.xaml.cs` into `ChatWindow.ConversationFilterPresentation.cs`. Running-only toggles, recent/activity sort toggles, UI-state refresh, and conversation-list preference apply/persist logic now live in a dedicated presentation partial.
|
||||
- Document update: 2026-04-06 11:11 (KST) - This keeps sidebar state presentation more cohesive and further narrows the main chat window file toward orchestration-only responsibilities. Remaining work is now post-plan polish rather than another major structural split.
|
||||
- Document update: 2026-04-06 11:20 (KST) - Added `ChatWindow.SurfaceVisualPresentation.cs` and introduced shared surface helpers for popup containers, popup menu rows, separators, and file-tree headers. `ChatWindow.PreviewPresentation.cs` and `ChatWindow.FileBrowserPresentation.cs` now use the same popup/surface language instead of duplicating slightly different visual styles.
|
||||
- Document update: 2026-04-06 11:20 (KST) - This is the first visual-language polish pass after the large structure split. It reduces the feeling that preview and file browser are separate widgets and makes future popup/surface refinements reusable.
|
||||
- Document update: 2026-04-06 11:27 (KST) - Continued popup visual-language unification by switching `ChatWindow.PopupPresentation.cs` to the shared surface helpers and aligning selection/permission rows in `ChatWindow.SelectionPopupPresentation.cs` and `ChatWindow.PermissionPresentation.cs` with the same border, hover, and selected-state rules.
|
||||
- Document update: 2026-04-06 11:27 (KST) - At this point preview, file browser, worktree chooser, and permission-mode popup surfaces all share nearly the same visual language; remaining work is minor spacing/color polish rather than another behavior or structure change.
|
||||
- Document update: 2026-04-06 11:34 (KST) - Added `FooterChipBtn` to `ChatWindow.xaml` and aligned the footer work-bar buttons (`권한`, `Git 브랜치`) to the same rounded border, padding, and hover language instead of mixing outline and ghost button styles.
|
||||
- Document update: 2026-04-06 11:34 (KST) - This is a footer polish follow-up after the structure split, aimed at making the bottom work bar feel like one coherent tool row rather than a set of unrelated controls.
|
||||
- Document update: 2026-04-06 11:52 (KST) - Expanded `PermissionRequestPresentationCatalog.cs` into an action-level permission taxonomy (`bash`, `powershell`, `command`, `web_fetch`, `mcp`, `skill`, `question`, `file_edit`, `file_write`, `git`, `document`, `filesystem`) and added `ActionHint`, `Severity`, `RequiresPreview` metadata for later renderer refinement.
|
||||
- Document update: 2026-04-06 11:52 (KST) - Expanded `ToolResultPresentationCatalog.cs` with richer result taxonomy and post-result guidance. AX now distinguishes `approval_required` and `partial` in addition to `success / error / reject / cancel`, and each result carries `FollowUpHint` and `NeedsAttention`.
|
||||
- Document update: 2026-04-06 11:52 (KST) - Extended `SkillGalleryWindow.xaml.cs` to expose runtime-policy metadata (`model`, `effort`, `execution context`, `agent`, `disable model invocation`, `when to use`) so AX skills are easier to inspect and maintain like `claw-code` bundled skills.
|
||||
- Document update: 2026-04-06 13:01 (KST) - Wired the new permission/result metadata into transcript rendering. `ChatWindow.AgentEventRendering.cs` now appends action-level guidance and attention chips to permission/tool-result event banners instead of using badge labels alone.
|
||||
- Document update: 2026-04-06 13:01 (KST) - Permission events now surface `ActionHint`, `Severity`, and `RequiresPreview` through inline cues such as `미리보기 권장`, `주의 필요`, and `검토 권장`, while tool-result events surface `FollowUpHint`, `NeedsAttention`, and `StatusKind` through cues such as `확인 필요`, `승인 후 계속`, and `후속 점검`.
|
||||
- Document update: 2026-04-06 13:08 (KST) - Refined the transcript chips to visually differentiate metadata states instead of rendering every cue in the same neutral style. `ChatWindow.AgentEventRendering.cs` now uses blue for preview guidance, red for high-severity attention, amber for review/partial follow-up, and orange for approval-required continuation cues.
|
||||
- Document update: 2026-04-06 13:14 (KST) - Added kind/category chips to permission and tool-result transcript banners so the action domain is visible at a glance. `ChatWindow.AgentEventRendering.cs` now surfaces labels such as `명령 실행`, `파일 수정`, `웹 요청`, `Git`, `문서`, `스킬`, and `MCP` alongside the status-oriented chips.
|
||||
- Document update: 2026-04-06 13:20 (KST) - Replaced the plain guidance line with typed callout boxes in `ChatWindow.AgentEventRendering.cs`. Permission requests now show an `확인 포인트` callout, while tool results show a `다음 권장 작업` callout, making the two transcript surfaces read differently even when they share the same chip language.
|
||||
|
||||
@@ -10,6 +10,17 @@
|
||||
- Updated: 2026-04-05 16:55 (KST)
|
||||
- Current estimated parity vs `claw-code`: core execution engine `82%`, main chat UI `68%`, Cowork/Code status UX `63%`, internal settings linkage `88%`, overall AX Agent `74%`.
|
||||
- Engine-affecting settings should be handled conservatively during parity work. If a setting changes the main execution route, approval flow, or recovery behavior without representing a stable real-world user choice, it should be moved to developer-only UI or removed from user-facing surfaces.
|
||||
- Updated: 2026-04-06 09:36 (KST)
|
||||
- Progressed the maintainability track by moving runtime strip styling into `OperationalStatusPresentationCatalog.cs`, expanding permission/tool-result transcript catalogs with typed descriptions, and removing the stale plan-mode presentation branch from permission UI surfaces. The next structural focus remains footer/status/composer presentation slimming and regression ritual enforcement.
|
||||
- Updated: 2026-04-06 09:44 (KST)
|
||||
- Continued the maintainability track by splitting mixed inline interaction rendering into `ChatWindow.UserAskPresentation.cs` and `ChatWindow.PlanApprovalPresentation.cs`. This reduces message-type coupling inside the main window and keeps the next focus on footer/composer presentation and regression-routine formalization.
|
||||
- Updated: 2026-04-06 09:58 (KST)
|
||||
- Continued the maintainability track by splitting Git branch popup and footer-adjacent summary helpers into `ChatWindow.GitBranchPresentation.cs`, leaving `ChatWindow.FooterPresentation.cs` focused on folder bar state and preset-guide sync only.
|
||||
- Formalized the regression ritual in `docs/AX_AGENT_REGRESSION_PROMPTS.md` by adding failure classes (`blank-reply`, `duplicate-banner`, `bad-approval-flow`, `queue-drift`, `restore-drift`, `status-noise`) and required prompt bundles per change area.
|
||||
- Updated: 2026-04-06 10:07 (KST)
|
||||
- Continued the maintainability track by moving topic preset rendering, custom preset context menus, and topic-selection application flow into `ChatWindow.TopicPresetPresentation.cs`. This reduces mixed preset UI logic inside `ChatWindow.xaml.cs` and keeps the main window closer to orchestration-only responsibility.
|
||||
- Updated: 2026-04-06 11:52 (KST)
|
||||
- Continued the tool/permission/skill sophistication track by expanding AX presentation catalogs toward `claw-code` specificity. `PermissionRequestPresentationCatalog.cs` now models action-level permission kinds plus severity/action hints, `ToolResultPresentationCatalog.cs` now models `approval_required`/`partial` result states plus follow-up guidance, and the AX skill gallery now exposes runtime-policy metadata that was previously hidden.
|
||||
|
||||
## Preserved History (Summary)
|
||||
- Core loop guards and post-tool verification gates are already partially implemented.
|
||||
@@ -121,3 +132,392 @@
|
||||
- Manual scenario 2: Cowork tool run -> progress summary -> completion -> queue next request -> reopen
|
||||
- Manual scenario 3: Code task with execution log noise -> completion -> compact -> next turn -> reopen
|
||||
- Manual scenario 4: AX Agent internal settings change -> immediate runtime reflection without layout regression
|
||||
|
||||
## Canonical Prompt Set
|
||||
- Updated: 2026-04-05 22:04 (KST)
|
||||
- The following prompt set should be used for AX vs `claw-code` parity checks. The goal is not byte-identical output, but equivalent execution route, approval behavior, and artifact/result quality.
|
||||
- Operational checklist copy: `docs/AX_AGENT_REGRESSION_PROMPTS.md`
|
||||
|
||||
1. Chat basic answer
|
||||
- Prompt: `회의 일정 조정 메일을 정중한 한국어로 써줘`
|
||||
- Apply to: `Chat`
|
||||
- Verify: normal reply render, retry/regenerate stability, reopen durability
|
||||
|
||||
2. Chat long-form explanation
|
||||
- Prompt: `RAG와 fine-tuning 차이를 실무 관점으로 7가지로 설명해줘`
|
||||
- Apply to: `Chat`
|
||||
- Verify: long response rendering, compaction follow-up continuity
|
||||
|
||||
3. Cowork document task
|
||||
- Prompt: `신규 ERP 도입 제안서 초안을 작성해줘. 목적, 범위, 기대효과, 추진일정 포함`
|
||||
- Apply to: `Cowork`
|
||||
- Verify: topic/task preset routing, plan-first execution, actual document-oriented output path
|
||||
|
||||
4. Cowork data task
|
||||
- Prompt: `매출 CSV를 분석해서 월별 추세와 이상치를 요약해줘`
|
||||
- Apply to: `Cowork`
|
||||
- Verify: data-analysis tool choice, reduced runtime noise, final summary quality
|
||||
|
||||
5. Code bug-fix task
|
||||
- Prompt: `현재 프로젝트에서 설정 저장 버그 원인 찾고 수정해줘`
|
||||
- Apply to: `Code`
|
||||
- Verify: read/search/edit path, diff persistence, reopen consistency
|
||||
|
||||
6. Code build/test task
|
||||
- Prompt: `빌드 오류를 재현하고 수정한 뒤 다시 빌드해줘`
|
||||
- Apply to: `Code`
|
||||
- Verify: build/test loop, failure retry, final completion message
|
||||
|
||||
7. Queued follow-up
|
||||
- Prompt sequence:
|
||||
- `이 창 레이아웃 문제 원인 찾아줘`
|
||||
- `끝나면 README도 같이 갱신해줘`
|
||||
- Apply to: `Cowork`, `Code`
|
||||
- Verify: queue chaining, next-turn pickup without UI mutation
|
||||
|
||||
8. Post-compaction continuity
|
||||
- Prompt: `지금까지 논의한 내용을 5줄로 이어서 정리하고 다음 작업 제안해줘`
|
||||
- Apply to: `Chat`, `Cowork`, `Code`
|
||||
- Verify: compact-after-next-turn continuity, no token-only completion
|
||||
|
||||
9. Permission approval
|
||||
- Prompt: `이 파일을 수정해서 저장해줘`
|
||||
- Apply to: `Code`
|
||||
- Verify: permission request, approve/reject rendering, final transcript consistency
|
||||
|
||||
10. Slash / skill entry
|
||||
- Prompt: `/bug-hunt src 폴더 잠재 버그 찾아줘`
|
||||
- Apply to: `Code`
|
||||
- Verify: slash entry uses the same prepared-execution route as normal send
|
||||
|
||||
## Tool / Skill Delta Snapshot
|
||||
- Updated: 2026-04-05 22:04 (KST)
|
||||
- AX tool registry count is larger than `claw-code`, but the shape is different.
|
||||
- AX reference: `src/AxCopilot/Services/Agent/ToolRegistry.cs`
|
||||
- `claw-code` reference: `src/tools/*`, `src/skills/bundledSkills.ts`
|
||||
|
||||
### AX stronger areas
|
||||
- Document/office generation and conversion (`ExcelSkill`, `DocxSkill`, `PptxSkill`, `DocumentPlannerTool`, `DocumentAssemblerTool`)
|
||||
- Data/business utilities (`DataPivotTool`, `SqlTool`, `FormatConvertTool`, `TextSummarizeTool`)
|
||||
- WPF-integrated enterprise UX and Korean workflow presets
|
||||
|
||||
### claw-code stronger areas
|
||||
- Transcript-native tool use / rejection / approval message taxonomy
|
||||
- Plan approval request/response rendering in the message stream
|
||||
- Permission and tool-result message consistency
|
||||
- Bundled skill registry and skill message integration
|
||||
|
||||
### Remaining parity target
|
||||
- Keep AX's richer business/document tool set
|
||||
- Bring transcript rendering and approval/status UX closer to `claw-code`
|
||||
|
||||
## Transcript-First Approval / Ask UX
|
||||
- Updated: 2026-04-05 18:58 (KST)
|
||||
- `plan approval` and `user ask` should both resolve inside the transcript first.
|
||||
- Secondary windows are allowed only as detail surfaces, not as the primary decision flow.
|
||||
- AX implementation status:
|
||||
- `plan approval`: transcript-first, detail view via `PlanViewerWindow`
|
||||
- `user ask`: transcript-first inline question card with choices / direct input / submit
|
||||
|
||||
## Tool / Skill UX Parity Follow-up
|
||||
- Updated: 2026-04-05 19:04 (KST)
|
||||
- Default transcript should prefer role-oriented badges and readable labels over raw internal tool names.
|
||||
- AX implementation status:
|
||||
- tool event badges: simplified to role-first labels
|
||||
- item naming: normalized into readable Korean labels or `/skill-name` style
|
||||
- observability panels: permission/background diagnostics reduced outside debug mode
|
||||
- Remaining quality target:
|
||||
- move more tool-result and permission-result presentation into smaller message-type-specific helpers, closer to `claw-code` component separation
|
||||
|
||||
## Focused Quality Tracks
|
||||
- Updated: 2026-04-06 09:27 (KST)
|
||||
- The remaining improvement work should now be managed in three parallel tracks so UX polish, runtime quality, and maintainability do not get mixed together.
|
||||
|
||||
### Track 1. User-Facing UI/UX Quality
|
||||
- Reference:
|
||||
- `src/components/Messages.tsx`
|
||||
- `src/components/MessageRow.tsx`
|
||||
- `src/components/StatusLine.tsx`
|
||||
- `src/components/PromptInput/PromptInput.tsx`
|
||||
- `src/components/PromptInput/PromptInputFooter.tsx`
|
||||
- `src/components/SessionPreview.tsx`
|
||||
- AX apply target:
|
||||
- `src/AxCopilot/Views/ChatWindow.xaml`
|
||||
- `src/AxCopilot/Views/ChatWindow.MessageInteractions.cs`
|
||||
- `src/AxCopilot/Views/ChatWindow.StatusPresentation.cs`
|
||||
- `src/AxCopilot/Views/ChatWindow.FooterPresentation.cs`
|
||||
- `src/AxCopilot/Views/ChatWindow.PreviewPresentation.cs`
|
||||
- Focus:
|
||||
- keep transcript text dominant and metadata secondary
|
||||
- make footer controls read as a task bar, not a settings strip
|
||||
- unify preview surfaces, chooser popups, and approval cards under one visual language
|
||||
- reduce visual noise from queue/status/diagnostic surfaces unless the state is actionable
|
||||
- Completion criteria:
|
||||
- message rows, footer, preview, and inline approval/question cards feel visually coherent
|
||||
- chooser popups share the same spacing, hover behavior, and summary-row structure
|
||||
- footer/status elements appear only when they convey useful state
|
||||
- Quality criteria:
|
||||
- a user can understand “what is happening now” from the transcript and footer without opening extra panels
|
||||
- the interface remains readable under narrow widths without text clipping or layout jitter
|
||||
|
||||
### Track 2. LLM / Task Handling Quality
|
||||
- Reference:
|
||||
- `src/bootstrap/state.ts`
|
||||
- `src/bridge/initReplBridge.ts`
|
||||
- `src/bridge/sessionRunner.ts`
|
||||
- `src/components/messages/AssistantToolUseMessage.tsx`
|
||||
- `src/components/messages/PlanApprovalMessage.tsx`
|
||||
- `src/components/permissions/*`
|
||||
- AX apply target:
|
||||
- `src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs`
|
||||
- `src/AxCopilot/Services/Agent/AgentLoopService.cs`
|
||||
- `src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs`
|
||||
- `src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs`
|
||||
- `src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs`
|
||||
- `src/AxCopilot/Views/ChatWindow.InlineInteractions.cs`
|
||||
- `src/AxCopilot/Views/ChatWindow.TranscriptPolicy.cs`
|
||||
- Focus:
|
||||
- keep all entry routes (`send`, `retry`, `regenerate`, `queue`, `slash`) on the same prepared execution path
|
||||
- distinguish `success / error / reject / cancel / needs-approval` tool results more clearly
|
||||
- keep plan approval and user-question flows transcript-native by default
|
||||
- minimize mismatches between execution state and what the user sees in the timeline
|
||||
- Completion criteria:
|
||||
- plan/permission/tool-result/question events all have consistent transcript-native lifecycles
|
||||
- reopen/retry/queue/compact flows preserve the same visible runtime state
|
||||
- tool failures and permission rejections are clearly distinguishable in transcript rendering
|
||||
- Quality criteria:
|
||||
- the same prompt under the same tab/settings uses the same execution route
|
||||
- users can tell whether the agent succeeded, failed, was blocked, or is waiting for approval without reading raw diagnostics
|
||||
|
||||
### Track 3. Maintainability / Extensibility Structure
|
||||
- Reference:
|
||||
- `src/components/*` split by role in `claw-code`
|
||||
- `src/components/messages/*`
|
||||
- `src/components/permissions/*`
|
||||
- `src/components/PromptInput/*`
|
||||
- AX apply target:
|
||||
- `src/AxCopilot/Views/ChatWindow.xaml.cs`
|
||||
- `src/AxCopilot/Views/ChatWindow.*.cs`
|
||||
- `src/AxCopilot/Services/AppStateService.cs`
|
||||
- `src/AxCopilot/Services/Agent/*.cs`
|
||||
- Focus:
|
||||
- continue shrinking `ChatWindow.xaml.cs` toward orchestration-only responsibility
|
||||
- keep renderer, popup, footer, message-interaction, status, and preview logic in dedicated partials
|
||||
- centralize presentation rules in catalogs/models instead of scattered UI string/visibility branches
|
||||
- prepare the codebase for new permission types, new tool classes, and new transcript card types without re-bloating the main window file
|
||||
- Completion criteria:
|
||||
- `ChatWindow.xaml.cs` owns orchestration and runtime coordination more than direct UI element construction
|
||||
- new message/permission/tool card types can be added via presentation catalogs or dedicated partials
|
||||
- runtime summary and footer/status visibility derive from presentation models rather than ad-hoc branching
|
||||
- Quality criteria:
|
||||
- adding a new tool-result or approval type should mostly affect one catalog/renderer area
|
||||
- future UI polish work should land in dedicated presentation files rather than expanding the main window file again
|
||||
|
||||
## Recommended Execution Order
|
||||
- Updated: 2026-04-06 09:27 (KST)
|
||||
1. Finish Track 2 consistency first whenever a UX issue is caused by runtime truth mismatch.
|
||||
2. Apply Track 1 visual cleanup only after the state/message lifecycle is stable for that surface.
|
||||
3. Fold each stable surface into Track 3 structure immediately so later changes do not reintroduce `ChatWindow.xaml.cs` sprawl.
|
||||
4. Keep validating against `docs/AX_AGENT_REGRESSION_PROMPTS.md` after each change set, especially for `plan / permission / queue / compact / reopen`.
|
||||
|
||||
## Current Snapshot
|
||||
- Updated: 2026-04-05 19:42 (KST)
|
||||
- Estimated parity:
|
||||
- Core engine: `89%`
|
||||
- Main transcript UI: `96%`
|
||||
- Cowork/Code runtime UX: `92%`
|
||||
- Internal settings linkage: `88%`
|
||||
- Overall AX Agent parity: `93%`
|
||||
|
||||
## Remaining Gaps
|
||||
1. Prompt lifecycle parity
|
||||
- `claw-code` reference: `src/utils/handlePromptSubmit.ts`, `src/utils/processUserInput/processTextPrompt.ts`
|
||||
- AX gap:
|
||||
- `send / retry / regenerate` are mostly unified, but `slash / compact 후 다음 턴 / 일부 queue 후처리`는 아직 `ChatWindow.xaml.cs`에서 UI 상태를 먼저 만지는 구간이 남아 있습니다.
|
||||
- 목표는 모든 입력 진입점이 `AxAgentExecutionEngine`의 동일한 prepare/execute/finalize 축만 타게 만드는 것입니다.
|
||||
|
||||
2. Plan / approval rendering parity
|
||||
- `claw-code` reference: `src/components/messages/PlanApprovalMessage.tsx`
|
||||
- AX gap:
|
||||
- 기본 transcript에서는 compact pill 위주로 줄였지만, 승인/계획 결과 표현이 아직 `Popup/Window + WPF 카드`와 섞여 있습니다.
|
||||
|
||||
## Quality Uplift Plan
|
||||
- Updated: 2026-04-06 00:22 (KST)
|
||||
- Goal: move AX Agent from parity-oriented stability into `claw-code`-grade maintainability and transcript quality, without copying implementation expression.
|
||||
|
||||
### Track 1. Transcript Renderer Decomposition
|
||||
- `claw-code` references:
|
||||
- `src/components/Messages.tsx`
|
||||
- `src/components/MessageRow.tsx`
|
||||
- `src/components/messages/AssistantToolUseMessage.tsx`
|
||||
- `src/components/messages/PlanApprovalMessage.tsx`
|
||||
- AX apply targets:
|
||||
- `src/AxCopilot/Views/ChatWindow.xaml.cs`
|
||||
- new partial/helper files under `src/AxCopilot/Views/`
|
||||
- Completion criteria:
|
||||
- `plan / permission / ask / tool-result / task-summary` rendering no longer lives as one large block inside `ChatWindow.xaml.cs`
|
||||
- each transcript concern has a dedicated helper/partial/class boundary
|
||||
- Quality criteria:
|
||||
- render changes for one message type do not regress unrelated timeline behavior
|
||||
- transcript behavior remains stable after reopen / retry / regenerate
|
||||
|
||||
### Track 2. Permission Presentation Catalog
|
||||
- `claw-code` references:
|
||||
- `src/components/permissions/PermissionRequest.tsx`
|
||||
- `src/components/permissions/PermissionDialog.tsx`
|
||||
- tool-specific permission request components under `src/components/permissions/*`
|
||||
- AX apply targets:
|
||||
- `src/AxCopilot/Services/Agent/PermissionModeCatalog.cs`
|
||||
- new `src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs`
|
||||
- `src/AxCopilot/Views/ChatWindow.xaml.cs`
|
||||
- Completion criteria:
|
||||
- permission request title, subtitle, icon, severity, and choice set are resolved by tool/request type
|
||||
- file edit / shell / skill / ask-user / web-like permission requests use distinct presentation metadata
|
||||
- Quality criteria:
|
||||
- permission prompts feel explicit and predictable
|
||||
- user can distinguish request type without reading raw tool names or payload
|
||||
|
||||
### Track 3. Tool Result Message Taxonomy
|
||||
- `claw-code` references:
|
||||
- `src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx`
|
||||
- `src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx`
|
||||
- `src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx`
|
||||
- `src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx`
|
||||
- AX apply targets:
|
||||
- new `src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs`
|
||||
- `src/AxCopilot/Views/ChatWindow.TranscriptPolicy.cs`
|
||||
- `src/AxCopilot/Views/ChatWindow.xaml.cs`
|
||||
- Completion criteria:
|
||||
- transcript display rules differ for `success / error / reject / cancel`
|
||||
- tool-result badges and summaries are resolved from presentation metadata instead of inline ad-hoc branches
|
||||
- Quality criteria:
|
||||
- result cards read as stable UX language, not raw execution logs
|
||||
- failed and rejected tool runs are visually distinct without increasing noise
|
||||
|
||||
### Track 4. Plan Approval Transcript-Only Flow
|
||||
- `claw-code` references:
|
||||
- `src/components/messages/PlanApprovalMessage.tsx`
|
||||
- `src/components/messages/UserPlanMessage.tsx`
|
||||
- AX apply targets:
|
||||
- `src/AxCopilot/Views/ChatWindow.xaml.cs`
|
||||
- `src/AxCopilot/Views/PlanViewerWindow.cs`
|
||||
- Completion criteria:
|
||||
- default approval / reject / revise flow completes inline in transcript
|
||||
- `PlanViewerWindow` is detail-only and never required for primary approval flow
|
||||
- Quality criteria:
|
||||
- planning feels like part of the conversation, not a modal interruption
|
||||
- approval history is replayable from persisted conversation state
|
||||
|
||||
### Track 5. Runtime Summary Layer
|
||||
- `claw-code` references:
|
||||
- `src/components/StatusLine.tsx`
|
||||
- `src/components/PromptInput/PromptInputFooter.tsx`
|
||||
- `src/bootstrap/state.ts`
|
||||
- AX apply targets:
|
||||
- `src/AxCopilot/Services/AppStateService.cs`
|
||||
- `src/AxCopilot/Views/ChatWindow.xaml.cs`
|
||||
- Completion criteria:
|
||||
- one runtime/status summary model feeds the status line, queue summary, runtime badge, and completion hint
|
||||
- status rendering no longer depends on scattered imperative refresh branches
|
||||
- Quality criteria:
|
||||
- no contradictory or stale runtime badges
|
||||
- long-running Cowork/Code sessions stay visually calm
|
||||
|
||||
### Track 6. Regression Prompt Ritual
|
||||
- `claw-code` references:
|
||||
- runtime validation scenarios implied by `sessionRunner`, `Messages`, `StatusLine`, and permission components
|
||||
- AX apply targets:
|
||||
- `docs/AX_AGENT_REGRESSION_PROMPTS.md`
|
||||
- `docs/claw-code-parity-plan.md`
|
||||
- developer workflow / release checklist
|
||||
- Completion criteria:
|
||||
- Chat / Cowork / Code prompt set is treated as mandatory regression for runtime-affecting changes
|
||||
- each prompt is mapped to a failure class (`blank reply`, `duplicate banner`, `bad approval flow`, `queue drift`, `restore drift`)
|
||||
- Quality criteria:
|
||||
- parity claims are based on repeatable checks instead of visual spot-checks
|
||||
- regressions are easier to catch before release
|
||||
|
||||
## Recommended Execution Order
|
||||
1. Transcript renderer decomposition
|
||||
2. Permission presentation catalog
|
||||
3. Tool result taxonomy
|
||||
4. Plan approval transcript-only flow
|
||||
5. Runtime summary layer
|
||||
6. Regression prompt ritual hardening
|
||||
|
||||
## Settings and Logic Review
|
||||
- Updated: 2026-04-06 00:22 (KST)
|
||||
- Candidate to move to developer-only:
|
||||
- `FreeTierDelaySeconds`
|
||||
- `MaxAgentIterations`
|
||||
- `MaxRetryOnError`
|
||||
- Keep as runtime-critical user settings:
|
||||
- `OperationMode`
|
||||
- `MaxContextTokens`
|
||||
- `ContextCompactTriggerPercent`
|
||||
- `EnableProactiveContextCompact`
|
||||
- `EnableCoworkVerification`
|
||||
- `EnableCodeVerification`
|
||||
- code tool exposure toggles
|
||||
- Rule:
|
||||
- if a setting changes the main execution route or recovery semantics without representing a stable real-world user choice, move it out of default user-facing surfaces
|
||||
- 목표는 “본문 우선 + 필요 시 열기” 기준으로 더 단일한 timeline 언어로 수렴시키는 것입니다.
|
||||
|
||||
3. Status line / composer parity
|
||||
- `claw-code` reference: `src/components/StatusLine.tsx`, `src/components/PromptInput/PromptInput.tsx`
|
||||
- AX gap:
|
||||
- 하단 상태바와 composer 옵션은 많이 줄었지만, 상태 메타가 여전히 분산돼 있고 일부 토글/빠른 설정이 별도 행으로 남아 있습니다.
|
||||
- 목표는 transcript 하단의 작업 바 한 축으로 더 압축하는 것입니다.
|
||||
|
||||
4. Runtime event density parity
|
||||
- `claw-code` reference: `src/bridge/sessionRunner.ts`, `src/components/StatusNotices.tsx`
|
||||
- AX gap:
|
||||
- non-debug 기본 로그는 줄었지만, 일부 Cowork/Code 이벤트는 여전히 timeline을 자주 흔듭니다.
|
||||
- 목표는 `permission / tool / error / complete / paused / resumed`를 더 안정된 event shape로 정규화하는 것입니다.
|
||||
|
||||
## Settings Review
|
||||
- Remove candidate:
|
||||
- `PlanMode`
|
||||
- current state: 사용자 노출 UI와 저장 경로는 `off` 고정으로 정리됐지만 `AppSettings`, `SettingsViewModel`, `AppStateService` 타입 잔재가 남아 있음
|
||||
- rationale: 현재 정책이 `off` 고정이라 사용자 선택값이 엔진에 의미 있게 기여하지 않음
|
||||
- `Code.EnablePlanModeTools`
|
||||
- current state: UI/저장 경로와 기본값은 `false` 고정으로 정리됐지만 모델/설정 타입에 호환용 잔재가 남아 있음
|
||||
- rationale: 현재 엔진 정책에서 실제 실행 경로를 더 이상 바꾸지 않음
|
||||
- Move to developer-only candidate:
|
||||
- `FreeTierDelaySeconds`
|
||||
- rationale: 일반 사용자가 조정할 이유가 적고 엔진 지연 정책에 직접 영향
|
||||
- `MaxAgentIterations`
|
||||
- `MaxRetryOnError`
|
||||
- rationale: 핵심 실행 루프 품질에 직접 영향하는 런타임 튜닝값
|
||||
- Keep as runtime-critical:
|
||||
- `OperationMode`
|
||||
- `MaxContextTokens`
|
||||
- `ContextCompactTriggerPercent`
|
||||
- `EnableProactiveContextCompact`
|
||||
- `EnableCoworkVerification`
|
||||
- `EnableCodeVerification`
|
||||
- `Code.EnableWorktreeTools / EnableTeamTools / EnableCronTools`
|
||||
|
||||
## Known UX / Performance Risks
|
||||
- Topic preset hover flicker was caused by duplicate hover systems:
|
||||
- custom hover label
|
||||
- default WPF `ToolTip`
|
||||
- AX fix:
|
||||
- remove default `ToolTip` from topic cards and keep a single hover label path
|
||||
- Remaining runtime performance review targets:
|
||||
- `RefreshContextUsageVisual()` frequency
|
||||
- `BuildTopicButtons()` rebuild frequency
|
||||
- `OnAgentEvent` timeline churn during long Cowork/Code runs
|
||||
- compact queue summary still needs one more pass to fully match `claw-code` footer minimalism
|
||||
|
||||
## Progress Notes
|
||||
- 업데이트: 2026-04-06 00:58 (KST)
|
||||
- transcript renderer 분리 1차 완료
|
||||
- AX 적용: [ChatWindow.InlineInteractions.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.InlineInteractions.cs), [ChatWindow.TaskSummary.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TaskSummary.cs)
|
||||
- 완료 조건: `plan / ask / task-summary` 렌더 helper가 메인 `ChatWindow.xaml.cs` 밖으로 이동
|
||||
- permission / tool-result presentation catalog 도입
|
||||
- AX 적용: [PermissionRequestPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs), [ToolResultPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs)
|
||||
- 완료 조건: `AddAgentEventBanner(...)`가 권한/도구 결과 badge 메타를 inline switch가 아니라 catalog에서 해석
|
||||
- runtime summary 전용 계층 1차 반영
|
||||
- AX 적용: [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs)
|
||||
- 완료 조건: 상태선 UI가 `OperationalStatusPresentationState`를 소비해 strip/runtime badge visibility를 계산
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -26,7 +25,6 @@ public class ClipboardHandler : IActionHandler
|
||||
{
|
||||
var items = new List<LauncherItem>();
|
||||
|
||||
// 빌트인 변환 목록
|
||||
var builtins = GetBuiltinTransformers()
|
||||
.Where(t => string.IsNullOrEmpty(query) ||
|
||||
t.Key.Contains(query, StringComparison.OrdinalIgnoreCase));
|
||||
@@ -34,14 +32,12 @@ public class ClipboardHandler : IActionHandler
|
||||
foreach (var t in builtins)
|
||||
items.Add(new LauncherItem(t.Key, t.Description ?? "", null, t, Symbol: Symbols.Clipboard));
|
||||
|
||||
// 사용자 정의 변환
|
||||
var custom = _settings.Settings.ClipboardTransformers
|
||||
.Where(t => string.IsNullOrEmpty(query) ||
|
||||
t.Key.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(t => new LauncherItem(t.Key, t.Description ?? t.Type, null, t, Symbol: Symbols.Clipboard));
|
||||
|
||||
items.AddRange(custom);
|
||||
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
@@ -49,7 +45,6 @@ public class ClipboardHandler : IActionHandler
|
||||
{
|
||||
if (item.Data is not ClipboardTransformer transformer) return;
|
||||
|
||||
// 클립보드에서 텍스트 읽기 (STA 스레드 필요)
|
||||
string? input = null;
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
@@ -61,17 +56,12 @@ public class ClipboardHandler : IActionHandler
|
||||
string? result = await TransformAsync(transformer, input, ct);
|
||||
if (result == null) return;
|
||||
|
||||
// 결과를 클립보드에 쓰고 이전 활성 창으로 포커스 복원 후 붙여넣기
|
||||
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(result));
|
||||
|
||||
// 런처 호출 전 활성 창으로 포커스 복원
|
||||
var prevHwnd = WindowTracker.PreviousWindow;
|
||||
if (prevHwnd != IntPtr.Zero)
|
||||
SetForegroundWindow(prevHwnd);
|
||||
|
||||
await Task.Delay(120, ct);
|
||||
System.Windows.Forms.SendKeys.SendWait("^v");
|
||||
if (prevHwnd == IntPtr.Zero) return;
|
||||
|
||||
await ForegroundPasteHelper.PasteClipboardAsync(prevHwnd, ct, initialDelayMs: 220);
|
||||
LogService.Info($"클립보드 변환: '{transformer.Key}' 적용");
|
||||
}
|
||||
|
||||
@@ -81,7 +71,6 @@ public class ClipboardHandler : IActionHandler
|
||||
{
|
||||
return t.Type switch
|
||||
{
|
||||
// ReDoS 방지: 사용자 정의 패턴에 타임아웃 적용
|
||||
"regex" when t.Pattern != null && t.Replace != null =>
|
||||
Regex.Replace(input, t.Pattern, t.Replace, RegexOptions.None,
|
||||
TimeSpan.FromMilliseconds(t.Timeout > 0 ? t.Timeout : 5000)),
|
||||
@@ -120,8 +109,6 @@ public class ClipboardHandler : IActionHandler
|
||||
return await proc.StandardOutput.ReadToEndAsync(cts.Token);
|
||||
}
|
||||
|
||||
// ─── 빌트인 변환 ─────────────────────────────────────────────────────────
|
||||
|
||||
internal static string? ExecuteBuiltin(string key, string input)
|
||||
{
|
||||
return key switch
|
||||
@@ -149,7 +136,10 @@ public class ClipboardHandler : IActionHandler
|
||||
var doc = JsonDocument.Parse(input);
|
||||
return JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true });
|
||||
}
|
||||
catch { return input; }
|
||||
catch
|
||||
{
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? TryParseTimestamp(string input)
|
||||
@@ -159,6 +149,7 @@ public class ClipboardHandler : IActionHandler
|
||||
var dt = DateTimeOffset.FromUnixTimeSeconds(ts).LocalDateTime;
|
||||
return dt.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -166,15 +157,13 @@ public class ClipboardHandler : IActionHandler
|
||||
{
|
||||
if (DateTime.TryParse(input.Trim(), out var dt))
|
||||
return new DateTimeOffset(dt).ToUnixTimeSeconds().ToString();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string StripMarkdown(string input) =>
|
||||
Regex.Replace(input, @"(\*\*|__)(.*?)\1|(\*|_)(.*?)\3|`(.+?)`|#{1,6}\s*", "$2$4$5");
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
private static IEnumerable<ClipboardTransformer> GetBuiltinTransformers() =>
|
||||
[
|
||||
new() { Key = "$json", Type = "builtin", Description = "JSON 포맷팅 (들여쓰기 적용)" },
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
@@ -28,7 +27,6 @@ public class ClipboardHistoryHandler : IActionHandler
|
||||
_historyService = historyService;
|
||||
}
|
||||
|
||||
// 카테고리 필터 프리픽스: #url, #코드, #경로
|
||||
private static readonly Dictionary<string, string> CategoryFilters = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "url", "URL" }, { "코드", "코드" }, { "code", "코드" },
|
||||
@@ -52,7 +50,6 @@ public class ClipboardHistoryHandler : IActionHandler
|
||||
|
||||
var q = query.Trim().ToLowerInvariant();
|
||||
|
||||
// 카테고리 필터 감지 (예: #url, #핀)
|
||||
string? catFilter = null;
|
||||
foreach (var (prefix, cat) in CategoryFilters)
|
||||
{
|
||||
@@ -66,17 +63,14 @@ public class ClipboardHistoryHandler : IActionHandler
|
||||
|
||||
var filtered = history.AsEnumerable();
|
||||
|
||||
// 카테고리 필터 적용
|
||||
if (catFilter == "핀")
|
||||
filtered = filtered.Where(e => e.IsPinned);
|
||||
else if (catFilter != null)
|
||||
filtered = filtered.Where(e => e.Category == catFilter);
|
||||
|
||||
// 텍스트 검색
|
||||
if (!string.IsNullOrEmpty(q))
|
||||
filtered = filtered.Where(e => e.Preview.ToLowerInvariant().Contains(q));
|
||||
|
||||
// 핀 항목을 상단에 배치
|
||||
var sorted = filtered
|
||||
.OrderByDescending(e => e.IsPinned)
|
||||
.ThenByDescending(e => e.CopiedAt);
|
||||
@@ -113,14 +107,13 @@ public class ClipboardHistoryHandler : IActionHandler
|
||||
try
|
||||
{
|
||||
_historyService.SuppressNextCapture();
|
||||
_historyService.PromoteEntry(entry); // 사용 시각 갱신 + 목록 맨 위로
|
||||
_historyService.PromoteEntry(entry);
|
||||
|
||||
if (!entry.IsText && entry.Image != null)
|
||||
{
|
||||
// 원본 이미지가 있으면 원본 해상도로 클립보드 복사
|
||||
var originalImg = ClipboardHistoryService.LoadOriginalImage(entry.OriginalImagePath);
|
||||
Clipboard.SetImage(originalImg ?? entry.Image);
|
||||
return; // 이미지는 붙여넣기 시뮬레이션 없이 클립보드만 설정
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(entry.Text)) return;
|
||||
@@ -129,25 +122,7 @@ public class ClipboardHistoryHandler : IActionHandler
|
||||
var prevWindow = WindowTracker.PreviousWindow;
|
||||
if (prevWindow == IntPtr.Zero) return;
|
||||
|
||||
// ── 이전 창 포커스 복원 후 Ctrl+V ────────────────────────────────
|
||||
// 런처 창이 완전히 숨겨지고 이전 창이 포커스를 회복할 시간 확보
|
||||
await Task.Delay(300, ct);
|
||||
|
||||
// AttachThreadInput으로 포그라운드 전환 권한 획득 후 SetForegroundWindow 호출
|
||||
// (Alt 트릭 대비: Alt 키 주입 없이 안정적으로 전환, 대상 앱 메뉴 트리거 방지)
|
||||
var targetThread = GetWindowThreadProcessId(prevWindow, out _);
|
||||
var currentThread = GetCurrentThreadId();
|
||||
AttachThreadInput(currentThread, targetThread, true);
|
||||
SetForegroundWindow(prevWindow);
|
||||
AttachThreadInput(currentThread, targetThread, false);
|
||||
|
||||
// 포커스 전환이 완전히 반영될 때까지 대기
|
||||
await Task.Delay(100, ct);
|
||||
|
||||
// SendInput으로 Ctrl+V 주입
|
||||
// Win32 INPUT 구조체: type(4) + 패딩(4) + union(32) = 40 bytes on x64
|
||||
// KEYBDINPUT.dwExtraInfo = ULONG_PTR → union 8바이트 정렬 필요 → offset 8에서 시작
|
||||
SendCtrlV();
|
||||
await ForegroundPasteHelper.PasteClipboardAsync(prevWindow, ct, initialDelayMs: 260);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
@@ -155,62 +130,4 @@ public class ClipboardHistoryHandler : IActionHandler
|
||||
LogService.Warn($"클립보드 히스토리 붙여넣기 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void SendCtrlV()
|
||||
{
|
||||
const uint INPUT_KEYBOARD = 1;
|
||||
const uint KEYEVENTF_KEYUP = 0x0002;
|
||||
const ushort VK_CONTROL = 0x11;
|
||||
const ushort VK_V = 0x56;
|
||||
|
||||
var inputs = new INPUT[4];
|
||||
|
||||
// Ctrl 누름
|
||||
inputs[0].Type = INPUT_KEYBOARD;
|
||||
inputs[0].ki.wVk = VK_CONTROL;
|
||||
|
||||
// V 누름
|
||||
inputs[1].Type = INPUT_KEYBOARD;
|
||||
inputs[1].ki.wVk = VK_V;
|
||||
|
||||
// V 뗌
|
||||
inputs[2].Type = INPUT_KEYBOARD;
|
||||
inputs[2].ki.wVk = VK_V;
|
||||
inputs[2].ki.dwFlags = KEYEVENTF_KEYUP;
|
||||
|
||||
// Ctrl 뗌
|
||||
inputs[3].Type = INPUT_KEYBOARD;
|
||||
inputs[3].ki.wVk = VK_CONTROL;
|
||||
inputs[3].ki.dwFlags = KEYEVENTF_KEYUP;
|
||||
|
||||
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf<INPUT>());
|
||||
}
|
||||
|
||||
// ─── P/Invoke ──────────────────────────────────────────────────────────
|
||||
|
||||
// Win32 INPUT 구조체 — x64에서 40바이트
|
||||
// type(4) + 패딩(4) + union은 offset 8에서 시작 (MOUSEINPUT 32바이트가 최대)
|
||||
[StructLayout(LayoutKind.Explicit, Size = 40)]
|
||||
private struct INPUT
|
||||
{
|
||||
[FieldOffset(0)] public uint Type;
|
||||
[FieldOffset(8)] public KEYBDINPUT ki;
|
||||
}
|
||||
|
||||
// KEYBDINPUT: 2+2+4+4 = 12, dwExtraInfo(IntPtr)는 8바이트 정렬 → 패딩 포함 24바이트
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct KEYBDINPUT
|
||||
{
|
||||
public ushort wVk;
|
||||
public ushort wScan;
|
||||
public uint dwFlags;
|
||||
public uint time;
|
||||
public IntPtr dwExtraInfo;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
[DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
||||
[DllImport("user32.dll")] private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, [MarshalAs(UnmanagedType.Bool)] bool fAttach);
|
||||
[DllImport("kernel32.dll")] private static extern uint GetCurrentThreadId();
|
||||
[DllImport("user32.dll")] private static extern uint SendInput(uint nInputs, [MarshalAs(UnmanagedType.LPArray)] INPUT[] pInputs, int cbSize);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
@@ -7,13 +6,8 @@ using AxCopilot.Themes;
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L27-6: 클립보드 순차 붙여넣기 핸들러. "paste" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: paste → 번호 매긴 클립보드 히스토리 목록
|
||||
/// paste 3 1 5 → 3번→1번→5번 항목을 순서대로 붙여넣기
|
||||
/// paste all → 전체 히스토리를 순서대로 붙여넣기
|
||||
/// Enter → 이전 창에서 순서대로 Ctrl+V 실행.
|
||||
/// Raycast "Paste Sequentially" 대응.
|
||||
/// paste prefix 핸들러: 클립보드 히스토리를 순서대로 붙여넣습니다.
|
||||
/// 예: paste / paste 3 1 5 / paste all
|
||||
/// </summary>
|
||||
public class PasteHandler : IActionHandler
|
||||
{
|
||||
@@ -34,7 +28,7 @@ public class PasteHandler : IActionHandler
|
||||
|
||||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim();
|
||||
var q = query.Trim();
|
||||
var items = new List<LauncherItem>();
|
||||
var history = _history.History.Where(e => e.IsText && !string.IsNullOrEmpty(e.Text)).ToList();
|
||||
|
||||
@@ -47,7 +41,6 @@ public class PasteHandler : IActionHandler
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// ── 번호 시퀀스 파싱 ──────────────────────────────────────────────────
|
||||
if (!string.IsNullOrWhiteSpace(q) && q != "all")
|
||||
{
|
||||
var nums = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
@@ -60,16 +53,15 @@ public class PasteHandler : IActionHandler
|
||||
|
||||
if (indices.Count > 0)
|
||||
{
|
||||
var preview = string.Join(" → ", indices.Select(i => $"#{i}"));
|
||||
var preview = string.Join(" · ", indices.Select(i => $"#{i}"));
|
||||
var texts = indices.Select(i => history[i - 1].Text ?? "").ToList();
|
||||
var totalLen = texts.Sum(t => t.Length);
|
||||
|
||||
items.Add(new LauncherItem(
|
||||
$"순차 붙여넣기: {preview}",
|
||||
$"{indices.Count}개 항목 · {totalLen}자 · Enter: 순서대로 붙여넣기",
|
||||
$"{indices.Count}개 항목 · 총 {totalLen}자 · Enter로 순서대로 붙여넣기",
|
||||
null, ("seq", texts), Symbol: Symbols.ClipPaste));
|
||||
|
||||
// 미리보기
|
||||
for (int i = 0; i < indices.Count; i++)
|
||||
{
|
||||
var entry = history[indices[i] - 1];
|
||||
@@ -83,21 +75,19 @@ public class PasteHandler : IActionHandler
|
||||
}
|
||||
}
|
||||
|
||||
// ── all 명령 ──────────────────────────────────────────────────────────
|
||||
if (q.Equals("all", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var texts = history.Take(20).Select(e => e.Text ?? "").ToList();
|
||||
items.Add(new LauncherItem(
|
||||
$"전체 순차 붙여넣기 ({texts.Count}개)",
|
||||
$"Enter: 최근 {texts.Count}개 항목을 순서대로 붙여넣기",
|
||||
$"Enter로 최근 {texts.Count}개 항목을 순서대로 붙여넣기",
|
||||
null, ("seq", texts), Symbol: Symbols.ClipPaste));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// ── 빈 쿼리 → 번호 매긴 목록 ─────────────────────────────────────────
|
||||
items.Add(new LauncherItem(
|
||||
"순차 붙여넣기 — 번호를 입력하세요",
|
||||
"예: paste 3 1 5 → 3번→1번→5번 순서로 붙여넣기 · paste all → 전체",
|
||||
"순차 붙여넣기 번호를 입력하세요",
|
||||
"예: paste 3 1 5 · paste all",
|
||||
null, null, Symbol: Symbols.ClipPaste));
|
||||
|
||||
for (int i = 0; i < Math.Min(history.Count, 15); i++)
|
||||
@@ -117,12 +107,10 @@ public class PasteHandler : IActionHandler
|
||||
{
|
||||
if (item.Data is ("single", string singleText))
|
||||
{
|
||||
// 단일 항목 붙여넣기
|
||||
await PasteTexts([singleText], ct);
|
||||
}
|
||||
else if (item.Data is ("seq", List<string> texts))
|
||||
{
|
||||
// 순차 붙여넣기
|
||||
await PasteTexts(texts, ct);
|
||||
}
|
||||
}
|
||||
@@ -138,16 +126,9 @@ public class PasteHandler : IActionHandler
|
||||
|
||||
_history.SuppressNextCapture();
|
||||
|
||||
// 이전 창 포커스 복원 대기
|
||||
await Task.Delay(300, ct);
|
||||
|
||||
var targetThread = GetWindowThreadProcessId(prevWindow, out _);
|
||||
var currentThread = GetCurrentThreadId();
|
||||
AttachThreadInput(currentThread, targetThread, true);
|
||||
SetForegroundWindow(prevWindow);
|
||||
AttachThreadInput(currentThread, targetThread, false);
|
||||
|
||||
await Task.Delay(100, ct);
|
||||
var restored = await ForegroundPasteHelper.RestoreWindowAsync(prevWindow, ct, initialDelayMs: 260);
|
||||
if (!restored)
|
||||
return;
|
||||
|
||||
foreach (var text in texts)
|
||||
{
|
||||
@@ -157,8 +138,8 @@ public class PasteHandler : IActionHandler
|
||||
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text));
|
||||
await Task.Delay(50, ct);
|
||||
|
||||
SendCtrlV();
|
||||
await Task.Delay(200, ct); // 항목 간 간격
|
||||
await ForegroundPasteHelper.PasteClipboardAsync(prevWindow, ct, initialDelayMs: 0);
|
||||
await Task.Delay(200, ct);
|
||||
}
|
||||
|
||||
NotificationService.Notify("paste", $"{texts.Count}개 항목 붙여넣기 완료");
|
||||
@@ -172,46 +153,4 @@ public class PasteHandler : IActionHandler
|
||||
|
||||
private static string Truncate(string s, int max)
|
||||
=> s.Length <= max ? s : s[..max] + "…";
|
||||
|
||||
// ─── Ctrl+V 주입 (ClipboardHistoryHandler와 동일 패턴) ────────────────────
|
||||
|
||||
private static void SendCtrlV()
|
||||
{
|
||||
const uint INPUT_KEYBOARD = 1;
|
||||
const uint KEYEVENTF_KEYUP = 0x0002;
|
||||
const ushort VK_CONTROL = 0x11;
|
||||
const ushort VK_V = 0x56;
|
||||
|
||||
var inputs = new INPUT[4];
|
||||
inputs[0] = new INPUT { Type = INPUT_KEYBOARD, ki = new KEYBDINPUT { wVk = VK_CONTROL } };
|
||||
inputs[1] = new INPUT { Type = INPUT_KEYBOARD, ki = new KEYBDINPUT { wVk = VK_V } };
|
||||
inputs[2] = new INPUT { Type = INPUT_KEYBOARD, ki = new KEYBDINPUT { wVk = VK_V, dwFlags = KEYEVENTF_KEYUP } };
|
||||
inputs[3] = new INPUT { Type = INPUT_KEYBOARD, ki = new KEYBDINPUT { wVk = VK_CONTROL, dwFlags = KEYEVENTF_KEYUP } };
|
||||
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf<INPUT>());
|
||||
}
|
||||
|
||||
// ─── P/Invoke ──────────────────────────────────────────────────────────────
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Size = 40)]
|
||||
private struct INPUT
|
||||
{
|
||||
[FieldOffset(0)] public uint Type;
|
||||
[FieldOffset(8)] public KEYBDINPUT ki;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct KEYBDINPUT
|
||||
{
|
||||
public ushort wVk;
|
||||
public ushort wScan;
|
||||
public uint dwFlags;
|
||||
public uint time;
|
||||
public IntPtr dwExtraInfo;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
[DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
||||
[DllImport("user32.dll")] private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, [MarshalAs(UnmanagedType.Bool)] bool fAttach);
|
||||
[DllImport("kernel32.dll")] private static extern uint GetCurrentThreadId();
|
||||
[DllImport("user32.dll")] private static extern uint SendInput(uint nInputs, [MarshalAs(UnmanagedType.LPArray)] INPUT[] pInputs, int cbSize);
|
||||
}
|
||||
|
||||
@@ -202,22 +202,22 @@ public class LauncherSettings
|
||||
public bool ShowLauncherBorder { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("showWidgetPerf")]
|
||||
public bool ShowWidgetPerf { get; set; } = true;
|
||||
public bool ShowWidgetPerf { get; set; } = false;
|
||||
|
||||
[JsonPropertyName("showWidgetPomo")]
|
||||
public bool ShowWidgetPomo { get; set; } = true;
|
||||
public bool ShowWidgetPomo { get; set; } = false;
|
||||
|
||||
[JsonPropertyName("showWidgetNote")]
|
||||
public bool ShowWidgetNote { get; set; } = true;
|
||||
public bool ShowWidgetNote { get; set; } = false;
|
||||
|
||||
[JsonPropertyName("showWidgetWeather")]
|
||||
public bool ShowWidgetWeather { get; set; } = true;
|
||||
public bool ShowWidgetWeather { get; set; } = false;
|
||||
|
||||
[JsonPropertyName("showWidgetCalendar")]
|
||||
public bool ShowWidgetCalendar { get; set; } = true;
|
||||
public bool ShowWidgetCalendar { get; set; } = false;
|
||||
|
||||
[JsonPropertyName("showWidgetBattery")]
|
||||
public bool ShowWidgetBattery { get; set; } = true;
|
||||
public bool ShowWidgetBattery { get; set; } = false;
|
||||
|
||||
/// <summary>단축키 헬프 창에서 아이콘 색상을 테마 AccentColor 기준으로 표시. 기본 true(테마색).</summary>
|
||||
[JsonPropertyName("shortcutHelpUseThemeColor")]
|
||||
@@ -957,15 +957,6 @@ public class LlmSettings
|
||||
[JsonPropertyName("agentDecisionLevel")]
|
||||
public string AgentDecisionLevel { get; set; } = "detailed";
|
||||
|
||||
/// <summary>
|
||||
/// 플랜 모드. 에이전트가 도구 실행 전에 구조화된 실행 계획을 먼저 생성하도록 강제.
|
||||
/// off: 비활성 (기존 동작)
|
||||
/// always: 항상 계획 생성 후 사용자 승인 대기
|
||||
/// auto: 복잡한 작업 감지 시 자동으로 계획 모드 진입
|
||||
/// </summary>
|
||||
[JsonPropertyName("planMode")]
|
||||
public string PlanMode { get; set; } = "off";
|
||||
|
||||
/// <summary>
|
||||
/// 프로젝트 규칙 자동 주입 활성화.
|
||||
/// true: .ax/rules/ 디렉토리의 규칙을 시스템 프롬프트에 자동 포함
|
||||
@@ -1289,10 +1280,6 @@ public class CodeSettings
|
||||
[JsonPropertyName("enableCodeVerification")]
|
||||
public bool EnableCodeVerification { get; set; } = false;
|
||||
|
||||
/// <summary>Code 탭에서 Plan Mode 도구(enter/exit plan mode) 사용 여부. 기본 true.</summary>
|
||||
[JsonPropertyName("enablePlanModeTools")]
|
||||
public bool EnablePlanModeTools { get; set; } = true;
|
||||
|
||||
/// <summary>Code 탭에서 Worktree 도구(enter/exit worktree) 사용 여부. 기본 true.</summary>
|
||||
[JsonPropertyName("enableWorktreeTools")]
|
||||
public bool EnableWorktreeTools { get; set; } = true;
|
||||
|
||||
@@ -262,6 +262,15 @@ public class ChatMessage
|
||||
[JsonPropertyName("feedback")]
|
||||
public string? Feedback { get; set; }
|
||||
|
||||
[JsonPropertyName("responseElapsedMs")]
|
||||
public long? ResponseElapsedMs { get; set; }
|
||||
|
||||
[JsonPropertyName("promptTokens")]
|
||||
public int PromptTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("completionTokens")]
|
||||
public int CompletionTokens { get; set; }
|
||||
|
||||
/// <summary>첨부된 파일 경로 목록.</summary>
|
||||
[JsonPropertyName("attachedFiles")]
|
||||
public List<string>? AttachedFiles { get; set; }
|
||||
|
||||
@@ -199,8 +199,8 @@ public partial class AgentLoopService
|
||||
var recentTaskRetryQuality = TryGetRecentTaskRetryQuality(taskPolicy.TaskType);
|
||||
maxRetry = ComputeQualityAwareMaxRetry(maxRetry, recentTaskRetryQuality, taskPolicy.TaskType);
|
||||
|
||||
// 플랜 모드 설정
|
||||
var planMode = ResolveEffectivePlanMode(llm.PlanMode, ActiveTab, taskPolicy.TaskType); // off | always | auto
|
||||
// 플랜 prelude는 현재 정책상 비활성
|
||||
var shouldGeneratePlanPrelude = false;
|
||||
|
||||
var context = BuildContext();
|
||||
InjectTaskTypeGuidance(messages, taskPolicy);
|
||||
@@ -282,8 +282,8 @@ public partial class AgentLoopService
|
||||
workFolder = context.WorkFolder
|
||||
}));
|
||||
|
||||
// ── 플랜 모드 "always": 첫 번째 호출은 계획만 생성 (도구 없이) ──
|
||||
if (planMode == "always")
|
||||
// ── 과거 plan mode 잔재. 현재 정책상 비활성 ──
|
||||
if (shouldGeneratePlanPrelude)
|
||||
{
|
||||
iteration++;
|
||||
EmitEvent(AgentEventType.Thinking, "", "실행 계획 생성 중...");
|
||||
@@ -637,7 +637,7 @@ public partial class AgentLoopService
|
||||
// 플랜 모드 "auto"에서만 승인 대기
|
||||
// - auto: 계획 감지 시 승인 대기 (단, 도구 호출이 함께 있으면 이미 실행 중이므로 스킵)
|
||||
// - off/always: 승인창 띄우지 않음 (off=자동 진행, always=앞에서 이미 처리됨)
|
||||
var requireApproval = planMode == "auto" && toolCalls.Count == 0;
|
||||
const bool requireApproval = false;
|
||||
|
||||
if (requireApproval && UserDecisionCallback != null)
|
||||
{
|
||||
@@ -3619,8 +3619,6 @@ public partial class AgentLoopService
|
||||
["taskupdate"] = "task_update",
|
||||
["taskstop"] = "task_stop",
|
||||
["taskoutput"] = "task_output",
|
||||
["enterplanmode"] = "enter_plan_mode",
|
||||
["exitplanmode"] = "exit_plan_mode",
|
||||
["enterworktree"] = "enter_worktree",
|
||||
["exitworktree"] = "exit_worktree",
|
||||
["teamcreate"] = "team_create",
|
||||
|
||||
@@ -21,11 +21,10 @@ internal static class AgentTabSettingsResolver
|
||||
|
||||
public static IEnumerable<string> EnumerateCodeTabDisabledTools(CodeSettings code)
|
||||
{
|
||||
if (!code.EnablePlanModeTools)
|
||||
{
|
||||
yield return "enter_plan_mode";
|
||||
yield return "exit_plan_mode";
|
||||
}
|
||||
// Plan mode tools are legacy compatibility only and stay disabled
|
||||
// regardless of any persisted value.
|
||||
yield return "enter_plan_mode";
|
||||
yield return "exit_plan_mode";
|
||||
|
||||
if (!code.EnableWorktreeTools)
|
||||
{
|
||||
|
||||
143
src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs
Normal file
143
src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
internal static class AgentTranscriptDisplayCatalog
|
||||
{
|
||||
public static string GetDisplayName(string? rawName, bool slashPrefix = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawName))
|
||||
return slashPrefix ? "/skill" : "도구";
|
||||
|
||||
var normalized = rawName.Trim();
|
||||
var lowered = normalized.ToLowerInvariant();
|
||||
var mapped = lowered switch
|
||||
{
|
||||
"file_read" => "파일 읽기",
|
||||
"file_write" => "파일 쓰기",
|
||||
"file_edit" => "파일 편집",
|
||||
"file_watch" => "파일 변경 감시",
|
||||
"file_info" => "파일 정보",
|
||||
"file_manage" => "파일 관리",
|
||||
"glob" => "파일 찾기",
|
||||
"grep" => "내용 검색",
|
||||
"folder_map" => "폴더 구조",
|
||||
|
||||
"document_reader" => "문서 읽기",
|
||||
"document_planner" => "문서 계획",
|
||||
"document_assembler" => "문서 조합",
|
||||
"document_review" => "문서 검토",
|
||||
"format_convert" => "형식 변환",
|
||||
"template_render" => "템플릿 렌더",
|
||||
|
||||
"build_run" => "빌드/실행",
|
||||
"test_loop" => "테스트 루프",
|
||||
"dev_env_detect" => "개발 환경 점검",
|
||||
"git_tool" => "Git",
|
||||
"diff_tool" => "Diff",
|
||||
"diff_preview" => "Diff 미리보기",
|
||||
|
||||
"process" => "명령 실행",
|
||||
"bash" => "Bash",
|
||||
"powershell" => "PowerShell",
|
||||
"web_fetch" => "웹 요청",
|
||||
"http" => "HTTP 요청",
|
||||
"user_ask" => "의견 요청",
|
||||
"suggest_actions" => "다음 작업 제안",
|
||||
|
||||
"task_create" => "작업 생성",
|
||||
"task_update" => "작업 업데이트",
|
||||
"task_list" => "작업 목록",
|
||||
"task_get" => "작업 조회",
|
||||
"task_stop" => "작업 중지",
|
||||
"task_output" => "작업 출력",
|
||||
"spawn_agent" => "서브에이전트",
|
||||
"wait_agents" => "에이전트 대기",
|
||||
_ => normalized.Replace('_', ' ').Trim(),
|
||||
};
|
||||
|
||||
if (!slashPrefix)
|
||||
return mapped;
|
||||
|
||||
if (normalized.StartsWith('/'))
|
||||
return normalized;
|
||||
|
||||
return "/" + lowered.Replace('_', '-').Replace(' ', '-');
|
||||
}
|
||||
|
||||
public static string GetEventBadgeLabel(AgentEvent evt)
|
||||
{
|
||||
if (evt.Type == AgentEventType.SkillCall)
|
||||
return "스킬";
|
||||
|
||||
if (evt.Type is AgentEventType.PermissionRequest or AgentEventType.PermissionGranted or AgentEventType.PermissionDenied)
|
||||
return "권한";
|
||||
|
||||
return GetToolCategoryLabel(evt.ToolName);
|
||||
}
|
||||
|
||||
public static string GetTaskCategoryLabel(string? kind, string? title)
|
||||
{
|
||||
if (string.Equals(kind, "permission", StringComparison.OrdinalIgnoreCase))
|
||||
return "권한";
|
||||
if (string.Equals(kind, "queue", StringComparison.OrdinalIgnoreCase))
|
||||
return "대기열";
|
||||
if (string.Equals(kind, "hook", StringComparison.OrdinalIgnoreCase))
|
||||
return "훅";
|
||||
if (string.Equals(kind, "subagent", StringComparison.OrdinalIgnoreCase))
|
||||
return "에이전트";
|
||||
if (string.Equals(kind, "tool", StringComparison.OrdinalIgnoreCase))
|
||||
return GetToolCategoryLabel(title);
|
||||
|
||||
return "작업";
|
||||
}
|
||||
|
||||
public static string BuildEventSummary(AgentEvent evt, string displayName)
|
||||
{
|
||||
var summary = (evt.Summary ?? string.Empty).Trim();
|
||||
if (!string.IsNullOrWhiteSpace(summary))
|
||||
return summary;
|
||||
|
||||
return evt.Type switch
|
||||
{
|
||||
AgentEventType.ToolCall => $"{displayName} 실행 준비",
|
||||
AgentEventType.ToolResult => evt.Success ? $"{displayName} 실행 완료" : $"{displayName} 실행 실패",
|
||||
AgentEventType.SkillCall => $"{displayName} 실행",
|
||||
AgentEventType.PermissionRequest => $"{displayName} 실행 전에 권한 확인이 필요합니다.",
|
||||
AgentEventType.PermissionGranted => $"{displayName} 실행 권한이 승인되었습니다.",
|
||||
AgentEventType.PermissionDenied => $"{displayName} 실행 권한이 거부되었습니다.",
|
||||
AgentEventType.Complete => "에이전트 작업이 완료되었습니다.",
|
||||
AgentEventType.Error => "에이전트 실행 중 오류가 발생했습니다.",
|
||||
_ => summary,
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetToolCategoryLabel(string? rawName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawName))
|
||||
return "도구";
|
||||
|
||||
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"
|
||||
=> "파일",
|
||||
"build_run" or "test_loop" or "dev_env_detect"
|
||||
=> "빌드",
|
||||
"git_tool" or "diff_tool" or "diff_preview"
|
||||
=> "Git",
|
||||
"document_reader" or "document_planner" or "document_assembler" or "document_review" or "format_convert" or "template_render"
|
||||
=> "문서",
|
||||
"user_ask"
|
||||
=> "질문",
|
||||
"suggest_actions"
|
||||
=> "제안",
|
||||
"process" or "bash" or "powershell"
|
||||
=> "명령",
|
||||
"spawn_agent" or "wait_agents"
|
||||
=> "에이전트",
|
||||
"web_fetch" or "http"
|
||||
=> "웹",
|
||||
_ => "도구",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -129,12 +129,20 @@ public sealed class AxAgentExecutionEngine
|
||||
ChatConversation conversation,
|
||||
string tab,
|
||||
string content,
|
||||
int promptTokens = 0,
|
||||
int completionTokens = 0,
|
||||
long? responseElapsedMs = null,
|
||||
string? metaRunId = null,
|
||||
ChatStorageService? storage = null)
|
||||
{
|
||||
var assistant = new ChatMessage
|
||||
{
|
||||
Role = "assistant",
|
||||
Content = content,
|
||||
PromptTokens = Math.Max(0, promptTokens),
|
||||
CompletionTokens = Math.Max(0, completionTokens),
|
||||
ResponseElapsedMs = responseElapsedMs is > 0 ? responseElapsedMs : null,
|
||||
MetaRunId = string.IsNullOrWhiteSpace(metaRunId) ? null : metaRunId,
|
||||
};
|
||||
|
||||
if (session != null)
|
||||
@@ -153,13 +161,17 @@ public sealed class AxAgentExecutionEngine
|
||||
ChatConversation conversation,
|
||||
string tab,
|
||||
string? content,
|
||||
int promptTokens = 0,
|
||||
int completionTokens = 0,
|
||||
long? responseElapsedMs = null,
|
||||
string? metaRunId = null,
|
||||
ChatStorageService? storage = null)
|
||||
{
|
||||
var normalized = NormalizeAssistantContentForUi(conversation, tab, content);
|
||||
if (tab is "Cowork" or "Code")
|
||||
conversation.ShowExecutionHistory = false;
|
||||
|
||||
CommitAssistantMessage(session, conversation, tab, normalized, storage);
|
||||
CommitAssistantMessage(session, conversation, tab, normalized, promptTokens, completionTokens, responseElapsedMs, metaRunId, storage);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
internal static class OperationalStatusPresentationCatalog
|
||||
{
|
||||
public static AppStateService.OperationalStatusPresentationState Resolve(
|
||||
AppStateService.OperationalStatusState status,
|
||||
string tab,
|
||||
bool hasLiveRuntimeActivity,
|
||||
int runningConversationCount,
|
||||
int spotlightConversationCount,
|
||||
bool runningOnlyFilter,
|
||||
bool sortConversationsByRecent)
|
||||
{
|
||||
var showCompactStrip = !string.Equals(tab, "Chat", StringComparison.OrdinalIgnoreCase)
|
||||
&& (string.Equals(status.StripKind, "permission_waiting", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(status.StripKind, "failed_run", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(status.StripKind, "permission_denied", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var (stripBackgroundHex, stripBorderHex, stripForegroundHex) = ResolveStripColors(status.StripKind, showCompactStrip);
|
||||
|
||||
var allowQuickStrip = !string.Equals(tab, "Chat", StringComparison.OrdinalIgnoreCase);
|
||||
var quickRunningActive = runningOnlyFilter && runningConversationCount > 0;
|
||||
var quickHotActive = !sortConversationsByRecent && spotlightConversationCount > 0;
|
||||
var showQuickStrip = allowQuickStrip && (quickRunningActive || quickHotActive);
|
||||
|
||||
return new AppStateService.OperationalStatusPresentationState
|
||||
{
|
||||
ShowRuntimeBadge = status.ShowRuntimeBadge && hasLiveRuntimeActivity,
|
||||
RuntimeLabel = status.RuntimeLabel,
|
||||
ShowLastCompleted = status.ShowLastCompleted,
|
||||
LastCompletedText = status.LastCompletedText,
|
||||
ShowCompactStrip = showCompactStrip,
|
||||
StripKind = showCompactStrip ? status.StripKind : "none",
|
||||
StripText = showCompactStrip ? status.StripText : "",
|
||||
StripBackgroundHex = stripBackgroundHex,
|
||||
StripBorderHex = stripBorderHex,
|
||||
StripForegroundHex = stripForegroundHex,
|
||||
ShowQuickStrip = showQuickStrip,
|
||||
QuickRunningText = runningConversationCount > 0 ? $"진행 {runningConversationCount}" : "진행",
|
||||
QuickHotText = spotlightConversationCount > 0 ? $"활동 {spotlightConversationCount}" : "활동",
|
||||
QuickRunningActive = quickRunningActive,
|
||||
QuickHotActive = quickHotActive,
|
||||
QuickRunningBackgroundHex = quickRunningActive ? "#DBEAFE" : "#F8FAFC",
|
||||
QuickRunningBorderHex = quickRunningActive ? "#93C5FD" : "#E5E7EB",
|
||||
QuickRunningForegroundHex = quickRunningActive ? "#1D4ED8" : "#6B7280",
|
||||
QuickHotBackgroundHex = quickHotActive ? "#F5F3FF" : "#F8FAFC",
|
||||
QuickHotBorderHex = quickHotActive ? "#C4B5FD" : "#E5E7EB",
|
||||
QuickHotForegroundHex = quickHotActive ? "#6D28D9" : "#6B7280",
|
||||
};
|
||||
}
|
||||
|
||||
private static (string backgroundHex, string borderHex, string foregroundHex) ResolveStripColors(string stripKind, bool visible)
|
||||
{
|
||||
if (!visible)
|
||||
return ("", "", "");
|
||||
|
||||
if (string.Equals(stripKind, "permission_waiting", StringComparison.OrdinalIgnoreCase))
|
||||
return ("#FFF7ED", "#FDBA74", "#C2410C");
|
||||
|
||||
if (string.Equals(stripKind, "failed_run", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stripKind, "permission_denied", StringComparison.OrdinalIgnoreCase))
|
||||
return ("#FEF2F2", "#FECACA", "#991B1B");
|
||||
|
||||
return ("", "", "");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace AxCopilot.Services.Agent;
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
internal sealed record PermissionModePresentation(
|
||||
string Mode,
|
||||
@@ -23,12 +23,6 @@ internal static class PermissionModePresentationCatalog
|
||||
"편집 자동 승인",
|
||||
"모든 파일 편집을 자동 승인합니다.",
|
||||
"#107C10"),
|
||||
new PermissionModePresentation(
|
||||
PermissionModeCatalog.Plan,
|
||||
"\uE7C3",
|
||||
"계획 모드",
|
||||
"변경하기 전에 계획을 먼저 만듭니다.",
|
||||
"#4338CA"),
|
||||
new PermissionModePresentation(
|
||||
PermissionModeCatalog.BypassPermissions,
|
||||
"\uE814",
|
||||
@@ -41,7 +35,7 @@ internal static class PermissionModePresentationCatalog
|
||||
{
|
||||
var normalized = PermissionModeCatalog.NormalizeGlobalMode(mode);
|
||||
return Ordered.FirstOrDefault(item =>
|
||||
string.Equals(item.Mode, normalized, StringComparison.OrdinalIgnoreCase))
|
||||
?? Ordered[0];
|
||||
string.Equals(item.Mode, normalized, StringComparison.OrdinalIgnoreCase))
|
||||
?? Ordered[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
internal sealed record PermissionRequestPresentation(
|
||||
string Kind,
|
||||
string Icon,
|
||||
string Label,
|
||||
string Description,
|
||||
string ActionHint,
|
||||
string BackgroundHex,
|
||||
string ForegroundHex,
|
||||
string Severity,
|
||||
bool RequiresPreview);
|
||||
|
||||
internal static class PermissionRequestPresentationCatalog
|
||||
{
|
||||
public static PermissionRequestPresentation Resolve(string? toolName, bool pending)
|
||||
{
|
||||
var tool = (toolName ?? string.Empty).Trim().ToLowerInvariant();
|
||||
|
||||
if (tool.Contains("bash"))
|
||||
return Build("bash", pending, "\uE756",
|
||||
"Bash 실행 권한 요청", "Bash 실행 확인",
|
||||
"셸 명령을 실행하기 전에 확인이 필요합니다.",
|
||||
"Bash 실행이 승인되어 계속 진행합니다.",
|
||||
"명령과 작업 위치를 확인하세요.",
|
||||
"#FEF2F2", "#DC2626", "high", false);
|
||||
|
||||
if (tool.Contains("powershell"))
|
||||
return Build("powershell", pending, "\uE756",
|
||||
"PowerShell 권한 요청", "PowerShell 실행 확인",
|
||||
"PowerShell 명령을 실행하기 전에 확인이 필요합니다.",
|
||||
"PowerShell 실행이 승인되어 계속 진행합니다.",
|
||||
"스크립트와 실행 범위를 확인하세요.",
|
||||
"#FEF2F2", "#DC2626", "high", false);
|
||||
|
||||
if (tool.Contains("process") || tool.Contains("build") || tool.Contains("test"))
|
||||
return Build("command", pending, "\uE756",
|
||||
"명령 실행 권한 요청", "명령 실행 확인",
|
||||
"명령 또는 빌드 작업 실행 전에 확인이 필요합니다.",
|
||||
"명령 실행이 승인되어 계속 진행합니다.",
|
||||
"실행 명령과 영향 범위를 확인하세요.",
|
||||
"#FEF2F2", "#DC2626", "high", false);
|
||||
|
||||
if (tool.Contains("web") || tool.Contains("fetch") || tool.Contains("http"))
|
||||
return Build("web_fetch", pending, "\uE774",
|
||||
"웹 요청 권한 요청", "웹 요청 확인",
|
||||
"외부 요청을 보내기 전에 확인이 필요합니다.",
|
||||
"웹 요청이 승인되어 계속 진행합니다.",
|
||||
"조회 URL과 전송 범위를 확인하세요.",
|
||||
"#FFF7ED", "#C2410C", "medium", false);
|
||||
|
||||
if (tool.Contains("mcp"))
|
||||
return Build("mcp", pending, "\uE943",
|
||||
"MCP 도구 권한 요청", "MCP 도구 확인",
|
||||
"연결된 MCP 도구 사용 전에 확인이 필요합니다.",
|
||||
"MCP 도구 사용이 승인되어 계속 진행합니다.",
|
||||
"서버와 호출 의도를 확인하세요.",
|
||||
"#F5F3FF", "#7C3AED", "medium", false);
|
||||
|
||||
if (tool.Contains("skill"))
|
||||
return Build("skill", pending, "\uE8A5",
|
||||
"스킬 실행 권한 요청", "스킬 실행 확인",
|
||||
"연결된 스킬을 실행하기 전에 확인이 필요합니다.",
|
||||
"스킬 실행이 승인되어 계속 진행합니다.",
|
||||
"허용 도구와 실행 컨텍스트를 확인하세요.",
|
||||
"#F5F3FF", "#7C3AED", "medium", false);
|
||||
|
||||
if (tool.Contains("ask"))
|
||||
return Build("question", pending, "\uE897",
|
||||
"의견 요청 확인", "의견 요청 완료",
|
||||
"사용자에게 선택이나 답변을 요청합니다.",
|
||||
"사용자 답변을 받아 다음 단계로 진행합니다.",
|
||||
"질문 의도와 선택지를 확인하세요.",
|
||||
"#EFF6FF", "#2563EB", "low", false);
|
||||
|
||||
if (tool.Contains("file_edit") || tool.Contains("edit"))
|
||||
return Build("file_edit", pending, "\uE70F",
|
||||
"파일 수정 권한 요청", "파일 수정 확인",
|
||||
"파일을 변경하기 전에 확인이 필요합니다.",
|
||||
"파일 수정이 승인되어 계속 진행합니다.",
|
||||
"변경 diff와 대상 파일을 확인하세요.",
|
||||
"#FFF7ED", "#C2410C", "high", true);
|
||||
|
||||
if (tool.Contains("file_write") || tool.Contains("write"))
|
||||
return Build("file_write", pending, "\uE70F",
|
||||
"파일 쓰기 권한 요청", "파일 쓰기 확인",
|
||||
"새 파일 작성 또는 덮어쓰기 전에 확인이 필요합니다.",
|
||||
"파일 쓰기가 승인되어 계속 진행합니다.",
|
||||
"작성 위치와 새 내용 미리보기를 확인하세요.",
|
||||
"#FFF7ED", "#C2410C", "high", true);
|
||||
|
||||
if (tool.Contains("git"))
|
||||
return Build("git", pending, "\uE8A7",
|
||||
"Git 작업 권한 요청", "Git 작업 확인",
|
||||
"브랜치나 커밋 상태를 바꾸기 전에 확인이 필요합니다.",
|
||||
"Git 작업이 승인되어 계속 진행합니다.",
|
||||
"브랜치와 변경 범위를 확인하세요.",
|
||||
"#EFF6FF", "#2563EB", "medium", false);
|
||||
|
||||
if (tool.Contains("document") || tool.Contains("template") || tool.Contains("format"))
|
||||
return Build("document", pending, "\uE8A5",
|
||||
"문서 작업 권한 요청", "문서 작업 확인",
|
||||
"문서 생성 또는 변환 작업 전에 확인이 필요합니다.",
|
||||
"문서 작업이 승인되어 계속 진행합니다.",
|
||||
"출력 형식과 저장 위치를 확인하세요.",
|
||||
"#FFF7ED", "#C2410C", "medium", false);
|
||||
|
||||
if (tool.Contains("file") || tool.Contains("glob") || tool.Contains("grep") || tool.Contains("folder"))
|
||||
return Build("filesystem", pending, "\uE8A5",
|
||||
"파일 접근 권한 요청", "파일 접근 확인",
|
||||
"폴더나 파일 내용을 읽기 전에 확인이 필요합니다.",
|
||||
"파일 접근이 승인되어 계속 진행합니다.",
|
||||
"읽기 범위와 접근 경로를 확인하세요.",
|
||||
"#FFF7ED", "#C2410C", "medium", false);
|
||||
|
||||
return Build("generic", pending, "\uE897",
|
||||
"권한 요청", "권한 확인",
|
||||
"계속 진행하기 전에 사용자의 확인이 필요합니다.",
|
||||
"요청이 승인되어 계속 진행합니다.",
|
||||
"실행 의도와 대상 범위를 확인하세요.",
|
||||
"#FFF7ED", "#C2410C", "medium", false);
|
||||
}
|
||||
|
||||
private static PermissionRequestPresentation Build(
|
||||
string kind,
|
||||
bool pending,
|
||||
string icon,
|
||||
string pendingLabel,
|
||||
string resolvedLabel,
|
||||
string pendingDescription,
|
||||
string resolvedDescription,
|
||||
string actionHint,
|
||||
string backgroundHex,
|
||||
string foregroundHex,
|
||||
string severity,
|
||||
bool requiresPreview)
|
||||
{
|
||||
return new PermissionRequestPresentation(
|
||||
kind,
|
||||
pending ? icon : "\uE73E",
|
||||
pending ? pendingLabel : resolvedLabel,
|
||||
pending ? pendingDescription : resolvedDescription,
|
||||
actionHint,
|
||||
pending ? backgroundHex : "#ECFDF5",
|
||||
pending ? foregroundHex : "#059669",
|
||||
severity,
|
||||
requiresPreview);
|
||||
}
|
||||
}
|
||||
@@ -470,8 +470,6 @@ public static class SkillService
|
||||
["TaskUpdate"] = "task_update",
|
||||
["TaskStop"] = "task_stop",
|
||||
["TaskOutput"] = "task_output",
|
||||
["EnterPlanMode"] = "enter_plan_mode",
|
||||
["ExitPlanMode"] = "exit_plan_mode",
|
||||
["EnterWorktree"] = "enter_worktree",
|
||||
["ExitWorktree"] = "exit_worktree",
|
||||
["TeamCreate"] = "team_create",
|
||||
|
||||
@@ -178,7 +178,6 @@ public class SubAgentTool : IAgentTool
|
||||
var llm = settings.Settings.Llm;
|
||||
llm.WorkFolder = parentContext.WorkFolder;
|
||||
llm.FilePermission = "Deny";
|
||||
llm.PlanMode = "off";
|
||||
llm.AgentHooks = new();
|
||||
llm.ToolPermissions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
llm.DisabledTools = new List<string>
|
||||
|
||||
@@ -187,8 +187,6 @@ public class ToolRegistry : IDisposable
|
||||
registry.Register(new TaskUpdateTool());
|
||||
registry.Register(new TaskStopTool());
|
||||
registry.Register(new TaskOutputTool());
|
||||
registry.Register(new EnterPlanModeTool());
|
||||
registry.Register(new ExitPlanModeTool());
|
||||
registry.Register(new EnterWorktreeTool());
|
||||
registry.Register(new ExitWorktreeTool());
|
||||
registry.Register(new TeamCreateTool());
|
||||
|
||||
247
src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs
Normal file
247
src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs
Normal file
@@ -0,0 +1,247 @@
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
internal sealed record ToolResultPresentation(
|
||||
string Kind,
|
||||
string Icon,
|
||||
string Label,
|
||||
string Description,
|
||||
string FollowUpHint,
|
||||
string BackgroundHex,
|
||||
string ForegroundHex,
|
||||
string StatusKind,
|
||||
bool NeedsAttention);
|
||||
|
||||
internal static class ToolResultPresentationCatalog
|
||||
{
|
||||
public static ToolResultPresentation Resolve(AgentEvent evt, string fallbackLabel)
|
||||
{
|
||||
var summary = (evt.Summary ?? string.Empty).Trim();
|
||||
var tool = (evt.ToolName ?? string.Empty).Trim().ToLowerInvariant();
|
||||
var baseLabel = string.IsNullOrWhiteSpace(fallbackLabel) ? "도구 결과" : fallbackLabel;
|
||||
var kind = ResolveKind(tool);
|
||||
|
||||
if (summary.Contains("취소", StringComparison.OrdinalIgnoreCase) ||
|
||||
summary.Contains("중단", StringComparison.OrdinalIgnoreCase) ||
|
||||
evt.Type == AgentEventType.StopRequested)
|
||||
{
|
||||
return new ToolResultPresentation(
|
||||
"cancel",
|
||||
"\uE711",
|
||||
$"{baseLabel} 취소",
|
||||
"요청이 중단되어 결과가 취소되었습니다.",
|
||||
"필요하면 같은 요청을 다시 실행하세요.",
|
||||
"#F8FAFC",
|
||||
"#475569",
|
||||
"cancel",
|
||||
false);
|
||||
}
|
||||
|
||||
if (summary.Contains("거부", StringComparison.OrdinalIgnoreCase) ||
|
||||
summary.Contains("반려", StringComparison.OrdinalIgnoreCase) ||
|
||||
summary.Contains("권한 거부", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new ToolResultPresentation(
|
||||
"reject",
|
||||
"\uE783",
|
||||
$"{baseLabel} 거부",
|
||||
"권한이 거부되어 작업이 중단되었습니다.",
|
||||
"권한 모드를 바꾸거나 다시 승인하면 이어서 진행할 수 있습니다.",
|
||||
"#FEF2F2",
|
||||
"#DC2626",
|
||||
"reject",
|
||||
true);
|
||||
}
|
||||
|
||||
if (summary.Contains("승인 필요", StringComparison.OrdinalIgnoreCase) ||
|
||||
summary.Contains("확인 필요", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new ToolResultPresentation(
|
||||
kind,
|
||||
"\uE8D7",
|
||||
$"{baseLabel} 승인 대기",
|
||||
"다음 단계로 진행하려면 사용자 승인이 필요합니다.",
|
||||
"승인 후 같은 작업 흐름이 이어집니다.",
|
||||
"#FFF7ED",
|
||||
"#C2410C",
|
||||
"approval_required",
|
||||
true);
|
||||
}
|
||||
|
||||
if (summary.Contains("부분", StringComparison.OrdinalIgnoreCase) ||
|
||||
summary.Contains("일부", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new ToolResultPresentation(
|
||||
kind,
|
||||
"\uE7BA",
|
||||
$"{baseLabel} 부분 완료",
|
||||
"일부 단계만 완료되어 후속 확인이나 재실행이 필요할 수 있습니다.",
|
||||
"남은 단계나 누락된 결과를 확인하세요.",
|
||||
"#FFFBEA",
|
||||
"#A16207",
|
||||
"partial",
|
||||
true);
|
||||
}
|
||||
|
||||
if (!evt.Success || evt.Type == AgentEventType.Error)
|
||||
{
|
||||
return new ToolResultPresentation(
|
||||
kind,
|
||||
"\uE783",
|
||||
BuildFailureLabel(kind, baseLabel),
|
||||
BuildFailureDescription(kind),
|
||||
BuildFailureFollowUp(kind),
|
||||
"#FEF2F2",
|
||||
"#DC2626",
|
||||
"error",
|
||||
true);
|
||||
}
|
||||
|
||||
return new ToolResultPresentation(
|
||||
kind,
|
||||
"\uE73E",
|
||||
BuildSuccessLabel(kind, baseLabel),
|
||||
BuildSuccessDescription(kind),
|
||||
BuildSuccessFollowUp(kind),
|
||||
"#ECFDF5",
|
||||
"#16A34A",
|
||||
"success",
|
||||
false);
|
||||
}
|
||||
|
||||
private static string ResolveKind(string tool)
|
||||
{
|
||||
if (tool.Contains("file_edit"))
|
||||
return "file_edit";
|
||||
if (tool.Contains("file_write"))
|
||||
return "file_write";
|
||||
if (tool.Contains("file_read") || tool.Contains("glob") || tool.Contains("grep"))
|
||||
return "filesystem";
|
||||
if (tool.Contains("file"))
|
||||
return "file";
|
||||
if (tool.Contains("build") || tool.Contains("test"))
|
||||
return "build_test";
|
||||
if (tool.Contains("git") || tool.Contains("diff"))
|
||||
return "git";
|
||||
if (tool.Contains("document") || tool.Contains("format") || tool.Contains("template"))
|
||||
return "document";
|
||||
if (tool.Contains("skill"))
|
||||
return "skill";
|
||||
if (tool.Contains("mcp"))
|
||||
return "mcp";
|
||||
if (tool.Contains("ask"))
|
||||
return "question";
|
||||
if (tool.Contains("web") || tool.Contains("fetch") || tool.Contains("http"))
|
||||
return "web";
|
||||
if (tool.Contains("process") || tool.Contains("bash") || tool.Contains("powershell"))
|
||||
return "command";
|
||||
return "generic";
|
||||
}
|
||||
|
||||
private static string BuildSuccessLabel(string kind, string baseLabel)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
"file_edit" => "파일 수정 완료",
|
||||
"file_write" => "파일 쓰기 완료",
|
||||
"filesystem" => "파일 탐색 완료",
|
||||
"file" => "파일 작업 완료",
|
||||
"build_test" => "빌드/테스트 완료",
|
||||
"git" => "Git 작업 완료",
|
||||
"document" => "문서 작업 완료",
|
||||
"skill" => "스킬 실행 완료",
|
||||
"mcp" => "MCP 도구 완료",
|
||||
"question" => "의견 요청 완료",
|
||||
"web" => "웹 요청 완료",
|
||||
"command" => "명령 실행 완료",
|
||||
_ => baseLabel,
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildFailureLabel(string kind, string baseLabel)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
"file_edit" => "파일 수정 실패",
|
||||
"file_write" => "파일 쓰기 실패",
|
||||
"filesystem" => "파일 탐색 실패",
|
||||
"file" => "파일 작업 실패",
|
||||
"build_test" => "빌드/테스트 실패",
|
||||
"git" => "Git 작업 실패",
|
||||
"document" => "문서 작업 실패",
|
||||
"skill" => "스킬 실행 실패",
|
||||
"mcp" => "MCP 도구 실패",
|
||||
"question" => "의견 요청 실패",
|
||||
"web" => "웹 요청 실패",
|
||||
"command" => "명령 실행 실패",
|
||||
_ => $"{baseLabel} 실패",
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildSuccessDescription(string kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
"file_edit" => "파일 수정 결과가 저장되었습니다.",
|
||||
"file_write" => "새 파일 작성 결과가 저장되었습니다.",
|
||||
"filesystem" => "파일과 폴더 정보를 성공적으로 읽었습니다.",
|
||||
"file" => "파일 관련 작업이 정상적으로 끝났습니다.",
|
||||
"build_test" => "빌드 또는 테스트 단계가 성공적으로 끝났습니다.",
|
||||
"git" => "Git 관련 작업이 정상적으로 끝났습니다.",
|
||||
"document" => "문서 생성 또는 변환 작업이 완료되었습니다.",
|
||||
"skill" => "선택한 스킬이 정상적으로 실행되었습니다.",
|
||||
"mcp" => "등록된 MCP 도구 호출이 성공적으로 끝났습니다.",
|
||||
"question" => "사용자 응답을 받아 다음 단계로 넘어갈 수 있습니다.",
|
||||
"web" => "웹 요청이 정상적으로 끝났습니다.",
|
||||
"command" => "명령 실행이 정상적으로 끝났습니다.",
|
||||
_ => "요청한 작업이 정상적으로 완료되었습니다.",
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildFailureDescription(string kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
"file_edit" => "파일 변경 과정에서 문제가 발생했습니다.",
|
||||
"file_write" => "파일 작성 또는 저장 과정에서 문제가 발생했습니다.",
|
||||
"filesystem" => "파일/폴더 접근 중 문제가 발생했습니다.",
|
||||
"file" => "파일 처리 중 문제가 발생했습니다.",
|
||||
"build_test" => "빌드 또는 테스트 단계에서 실패가 발생했습니다.",
|
||||
"git" => "Git 관련 작업이 실패했습니다.",
|
||||
"document" => "문서 생성 또는 변환 작업이 실패했습니다.",
|
||||
"skill" => "스킬 실행 중 문제가 발생했습니다.",
|
||||
"mcp" => "MCP 도구 호출 중 문제가 발생했습니다.",
|
||||
"question" => "사용자 의견 요청 과정에서 문제가 발생했습니다.",
|
||||
"web" => "웹 요청 처리에 실패했습니다.",
|
||||
"command" => "명령 실행 중 오류가 발생했습니다.",
|
||||
_ => "작업 처리 중 오류가 발생했습니다.",
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildSuccessFollowUp(string kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
"file_edit" or "file_write" => "변경 내용을 preview나 diff에서 다시 확인할 수 있습니다.",
|
||||
"build_test" => "출력 로그와 후속 수정 필요 여부를 확인하세요.",
|
||||
"git" => "브랜치 상태나 변경 요약을 이어서 확인하세요.",
|
||||
"document" => "생성된 산출물 경로를 열어 결과를 확인하세요.",
|
||||
"skill" => "같은 스킬을 다른 입력으로 이어서 실행할 수 있습니다.",
|
||||
_ => "필요하면 후속 요청을 이어서 실행할 수 있습니다.",
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildFailureFollowUp(string kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
"file_edit" or "file_write" => "대상 파일 경로와 권한, diff를 다시 확인하세요.",
|
||||
"build_test" => "실패 로그와 컴파일 오류 메시지를 먼저 확인하세요.",
|
||||
"git" => "현재 브랜치, 잠금 상태, 충돌 여부를 확인하세요.",
|
||||
"document" => "입력 데이터와 출력 형식, 저장 위치를 다시 확인하세요.",
|
||||
"skill" => "허용 도구와 런타임 요구사항을 다시 확인하세요.",
|
||||
"web" => "연결 상태와 요청 대상 URL을 다시 확인하세요.",
|
||||
"mcp" => "MCP 서버 연결 상태와 도구 등록 상태를 다시 확인하세요.",
|
||||
_ => "같은 요청을 재시도하기 전에 원인 메시지를 먼저 확인하세요.",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,6 @@ public sealed class AppStateService
|
||||
{
|
||||
public string FilePermission { get; set; } = "Deny";
|
||||
public string AgentDecisionLevel { get; set; } = "detailed";
|
||||
public string PlanMode { get; set; } = "off";
|
||||
public int ToolOverrideCount { get; set; }
|
||||
public List<KeyValuePair<string, string>> ToolOverrides { get; set; } = new();
|
||||
}
|
||||
@@ -159,6 +158,31 @@ public sealed class AppStateService
|
||||
public string StripText { get; init; } = "";
|
||||
}
|
||||
|
||||
public sealed class OperationalStatusPresentationState
|
||||
{
|
||||
public bool ShowRuntimeBadge { get; init; }
|
||||
public string RuntimeLabel { get; init; } = "";
|
||||
public bool ShowLastCompleted { get; init; }
|
||||
public string LastCompletedText { get; init; } = "";
|
||||
public bool ShowCompactStrip { get; init; }
|
||||
public string StripKind { get; init; } = "none";
|
||||
public string StripText { get; init; } = "";
|
||||
public string StripBackgroundHex { get; init; } = "";
|
||||
public string StripBorderHex { get; init; } = "";
|
||||
public string StripForegroundHex { get; init; } = "";
|
||||
public bool ShowQuickStrip { get; init; }
|
||||
public string QuickRunningText { get; init; } = "";
|
||||
public string QuickHotText { get; init; } = "";
|
||||
public bool QuickRunningActive { get; init; }
|
||||
public bool QuickHotActive { get; init; }
|
||||
public string QuickRunningBackgroundHex { get; init; } = "#F8FAFC";
|
||||
public string QuickRunningBorderHex { get; init; } = "#E5E7EB";
|
||||
public string QuickRunningForegroundHex { get; init; } = "#6B7280";
|
||||
public string QuickHotBackgroundHex { get; init; } = "#F8FAFC";
|
||||
public string QuickHotBorderHex { get; init; } = "#E5E7EB";
|
||||
public string QuickHotForegroundHex { get; init; } = "#6B7280";
|
||||
}
|
||||
|
||||
public ChatSessionStateService? ChatSession { get; private set; }
|
||||
public SkillCatalogState Skills { get; } = new();
|
||||
public McpCatalogState Mcp { get; } = new();
|
||||
@@ -196,7 +220,6 @@ public sealed class AppStateService
|
||||
|
||||
Permissions.FilePermission = PermissionModeCatalog.NormalizeGlobalMode(llm.FilePermission);
|
||||
Permissions.AgentDecisionLevel = llm.AgentDecisionLevel ?? "detailed";
|
||||
Permissions.PlanMode = "off";
|
||||
Permissions.ToolOverrideCount = llm.ToolPermissions?.Count ?? 0;
|
||||
Permissions.ToolOverrides = llm.ToolPermissions?
|
||||
.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
||||
@@ -544,24 +567,16 @@ public sealed class AppStateService
|
||||
var latestDeniedPermission = GetLatestDeniedPermission();
|
||||
|
||||
var showRuntimeBadge = taskSummary.HasActiveTasks
|
||||
|| queueSummary.QueuedCount > 0
|
||||
|| queueSummary.RunningCount > 0
|
||||
|| queueSummary.BlockedCount > 0
|
||||
|| taskSummary.PendingPermissionCount > 0
|
||||
|| backgroundSummary.ActiveCount > 0;
|
||||
|
||||
var runtimeLabel = taskSummary.PendingPermissionCount > 0
|
||||
? $"승인 대기 {taskSummary.PendingPermissionCount}"
|
||||
: taskSummary.HasActiveTasks
|
||||
? $"실행 중 {taskSummary.ActiveCount}"
|
||||
: queueSummary.RunningCount > 0
|
||||
? $"큐 실행 {queueSummary.RunningCount}"
|
||||
: backgroundSummary.ActiveCount > 0
|
||||
? $"백그라운드 {backgroundSummary.ActiveCount}"
|
||||
: queueSummary.QueuedCount > 0
|
||||
? $"대기열 {queueSummary.QueuedCount}"
|
||||
: queueSummary.BlockedCount > 0
|
||||
? $"재시도 대기 {queueSummary.BlockedCount}"
|
||||
: "실행 중 0";
|
||||
: backgroundSummary.ActiveCount > 0
|
||||
? $"백그라운드 {backgroundSummary.ActiveCount}"
|
||||
: "실행 중 0";
|
||||
|
||||
var lastCompletedText = taskSummary.LatestRecentTask == null
|
||||
? ""
|
||||
@@ -582,20 +597,6 @@ public sealed class AppStateService
|
||||
stripKind = "failed_run";
|
||||
stripText = $"최근 실패 {FormatDate(taskSummary.LatestFailedRun.UpdatedAt)}";
|
||||
}
|
||||
else if (queueSummary.RunningCount > 0 || queueSummary.QueuedCount > 0)
|
||||
{
|
||||
stripKind = "queue";
|
||||
stripText = queueSummary.RunningCount > 0
|
||||
? $"큐 실행 중 {queueSummary.RunningCount} · 다음 {Truncate(queueSummary.NextItem?.Text, 24)}"
|
||||
: $"대기열 {queueSummary.QueuedCount} · 다음 {Truncate(queueSummary.NextItem?.Text, 24)}";
|
||||
}
|
||||
else if (queueSummary.BlockedCount > 0)
|
||||
{
|
||||
stripKind = "queue_blocked";
|
||||
stripText = queueSummary.NextReadyAt.HasValue
|
||||
? $"재시도 대기 {queueSummary.BlockedCount} · {FormatDate(queueSummary.NextReadyAt.Value)}"
|
||||
: $"재시도 대기 {queueSummary.BlockedCount}";
|
||||
}
|
||||
else if (backgroundSummary.ActiveCount > 0)
|
||||
{
|
||||
stripKind = "background";
|
||||
@@ -644,6 +645,25 @@ public sealed class AppStateService
|
||||
};
|
||||
}
|
||||
|
||||
public OperationalStatusPresentationState GetOperationalStatusPresentation(
|
||||
string tab,
|
||||
bool hasLiveRuntimeActivity,
|
||||
int runningConversationCount,
|
||||
int spotlightConversationCount,
|
||||
bool runningOnlyFilter,
|
||||
bool sortConversationsByRecent)
|
||||
{
|
||||
var status = GetOperationalStatus(tab);
|
||||
return OperationalStatusPresentationCatalog.Resolve(
|
||||
status,
|
||||
tab,
|
||||
hasLiveRuntimeActivity,
|
||||
runningConversationCount,
|
||||
spotlightConversationCount,
|
||||
runningOnlyFilter,
|
||||
sortConversationsByRecent);
|
||||
}
|
||||
|
||||
public IReadOnlyList<DraftQueueItem> GetDraftQueueItems(string tab)
|
||||
=> ChatSession?.GetDraftQueueItems(tab) ?? Array.Empty<DraftQueueItem>();
|
||||
|
||||
|
||||
@@ -116,6 +116,15 @@ public sealed class ChatSessionStateService
|
||||
RememberConversation(normalizedTab, null);
|
||||
}
|
||||
|
||||
// 새 대화를 시작한 직후처럼 아직 저장되지 않은 현재 대화가 있으면,
|
||||
// 최신 저장 대화로 되돌아가지 말고 그 임시 세션을 그대로 유지합니다.
|
||||
if (CurrentConversation != null
|
||||
&& string.Equals(NormalizeTab(CurrentConversation.Tab), normalizedTab, StringComparison.OrdinalIgnoreCase)
|
||||
&& !HasPersistableContent(CurrentConversation))
|
||||
{
|
||||
return CurrentConversation;
|
||||
}
|
||||
|
||||
if (!hadRememberedConversation)
|
||||
{
|
||||
var latestMeta = storage.LoadAllMeta()
|
||||
|
||||
126
src/AxCopilot/Services/ForegroundPasteHelper.cs
Normal file
126
src/AxCopilot/Services/ForegroundPasteHelper.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
internal static class ForegroundPasteHelper
|
||||
{
|
||||
private const int SW_RESTORE = 9;
|
||||
private const uint INPUT_KEYBOARD = 1;
|
||||
private const uint KEYEVENTF_KEYUP = 0x0002;
|
||||
private const ushort VK_CONTROL = 0x11;
|
||||
private const ushort VK_V = 0x56;
|
||||
|
||||
public static async Task<bool> RestoreWindowAsync(
|
||||
IntPtr hwnd,
|
||||
CancellationToken ct,
|
||||
int initialDelayMs = 220,
|
||||
int settleDelayMs = 60,
|
||||
int maxAttempts = 4)
|
||||
{
|
||||
if (hwnd == IntPtr.Zero || !IsWindow(hwnd))
|
||||
return false;
|
||||
|
||||
await Task.Delay(initialDelayMs, ct);
|
||||
|
||||
if (IsIconic(hwnd))
|
||||
ShowWindow(hwnd, SW_RESTORE);
|
||||
|
||||
for (var attempt = 0; attempt < maxAttempts; attempt++)
|
||||
{
|
||||
uint currentThread = GetCurrentThreadId();
|
||||
uint targetThread = GetWindowThreadProcessId(hwnd, out _);
|
||||
var foreground = GetForegroundWindow();
|
||||
uint foregroundThread = foreground != IntPtr.Zero
|
||||
? GetWindowThreadProcessId(foreground, out _)
|
||||
: 0;
|
||||
|
||||
var attachedTarget = false;
|
||||
var attachedForeground = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (targetThread != 0 && targetThread != currentThread)
|
||||
{
|
||||
AttachThreadInput(currentThread, targetThread, true);
|
||||
attachedTarget = true;
|
||||
}
|
||||
|
||||
if (foregroundThread != 0 && foregroundThread != currentThread && foregroundThread != targetThread)
|
||||
{
|
||||
AttachThreadInput(currentThread, foregroundThread, true);
|
||||
attachedForeground = true;
|
||||
}
|
||||
|
||||
BringWindowToTop(hwnd);
|
||||
SetForegroundWindow(hwnd);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (attachedForeground)
|
||||
AttachThreadInput(currentThread, foregroundThread, false);
|
||||
|
||||
if (attachedTarget)
|
||||
AttachThreadInput(currentThread, targetThread, false);
|
||||
}
|
||||
|
||||
await Task.Delay(settleDelayMs, ct);
|
||||
|
||||
if (GetForegroundWindow() == hwnd)
|
||||
return true;
|
||||
}
|
||||
|
||||
return GetForegroundWindow() == hwnd;
|
||||
}
|
||||
|
||||
public static async Task<bool> PasteClipboardAsync(
|
||||
IntPtr hwnd,
|
||||
CancellationToken ct,
|
||||
int initialDelayMs = 220)
|
||||
{
|
||||
var restored = await RestoreWindowAsync(hwnd, ct, initialDelayMs);
|
||||
if (!restored)
|
||||
return false;
|
||||
|
||||
await Task.Delay(40, ct);
|
||||
SendCtrlV();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void SendCtrlV()
|
||||
{
|
||||
var inputs = new INPUT[4];
|
||||
inputs[0] = new INPUT { Type = INPUT_KEYBOARD, Ki = new KEYBDINPUT { wVk = VK_CONTROL } };
|
||||
inputs[1] = new INPUT { Type = INPUT_KEYBOARD, Ki = new KEYBDINPUT { wVk = VK_V } };
|
||||
inputs[2] = new INPUT { Type = INPUT_KEYBOARD, Ki = new KEYBDINPUT { wVk = VK_V, dwFlags = KEYEVENTF_KEYUP } };
|
||||
inputs[3] = new INPUT { Type = INPUT_KEYBOARD, Ki = new KEYBDINPUT { wVk = VK_CONTROL, dwFlags = KEYEVENTF_KEYUP } };
|
||||
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf<INPUT>());
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Size = 40)]
|
||||
private struct INPUT
|
||||
{
|
||||
[FieldOffset(0)] public uint Type;
|
||||
[FieldOffset(8)] public KEYBDINPUT Ki;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct KEYBDINPUT
|
||||
{
|
||||
public ushort wVk;
|
||||
public ushort wScan;
|
||||
public uint dwFlags;
|
||||
public uint time;
|
||||
public IntPtr dwExtraInfo;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
[DllImport("user32.dll")] private static extern bool BringWindowToTop(IntPtr hWnd);
|
||||
[DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow();
|
||||
[DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
|
||||
[DllImport("user32.dll")] private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, [MarshalAs(UnmanagedType.Bool)] bool fAttach);
|
||||
[DllImport("user32.dll")] private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
[DllImport("user32.dll")] private static extern bool IsWindow(IntPtr hWnd);
|
||||
[DllImport("user32.dll")] private static extern bool IsIconic(IntPtr hWnd);
|
||||
[DllImport("kernel32.dll")] private static extern uint GetCurrentThreadId();
|
||||
[DllImport("user32.dll")] private static extern uint SendInput(uint nInputs, [MarshalAs(UnmanagedType.LPArray)] INPUT[] pInputs, int cbSize);
|
||||
}
|
||||
@@ -225,7 +225,7 @@ public partial class LlmService
|
||||
return new
|
||||
{
|
||||
model = activeModel,
|
||||
max_tokens = Math.Max(llm.MaxContextTokens, 4096),
|
||||
max_tokens = ResolveOpenAiCompatibleMaxTokens(),
|
||||
temperature = llm.Temperature,
|
||||
system = systemPrompt,
|
||||
messages = msgs,
|
||||
@@ -237,7 +237,7 @@ public partial class LlmService
|
||||
return new
|
||||
{
|
||||
model = activeModel,
|
||||
max_tokens = Math.Max(llm.MaxContextTokens, 4096),
|
||||
max_tokens = ResolveOpenAiCompatibleMaxTokens(),
|
||||
temperature = llm.Temperature,
|
||||
messages = msgs,
|
||||
tools = toolDefs,
|
||||
@@ -661,7 +661,7 @@ public partial class LlmService
|
||||
["tools"] = toolDefs,
|
||||
["stream"] = false,
|
||||
["temperature"] = ResolveTemperature(),
|
||||
["max_tokens"] = llm.MaxContextTokens,
|
||||
["max_tokens"] = ResolveOpenAiCompatibleMaxTokens(),
|
||||
};
|
||||
var effort = ResolveReasoningEffort();
|
||||
if (!string.IsNullOrWhiteSpace(effort))
|
||||
|
||||
@@ -249,10 +249,32 @@ public partial class LlmService : IDisposable
|
||||
var llm = _settings.Settings.Llm;
|
||||
var service = NormalizeServiceName(llm.Service);
|
||||
if (service is "ollama" or "vllm" && !string.IsNullOrEmpty(llm.Model))
|
||||
{
|
||||
var registered = FindRegisteredModel(llm, service, llm.Model);
|
||||
if (registered != null)
|
||||
{
|
||||
var registeredModelName = CryptoService.DecryptIfEnabled(registered.EncryptedModelName, llm.EncryptionEnabled);
|
||||
if (!string.IsNullOrWhiteSpace(registeredModelName))
|
||||
return registeredModelName;
|
||||
}
|
||||
|
||||
return CryptoService.DecryptIfEnabled(llm.Model, llm.EncryptionEnabled);
|
||||
}
|
||||
return llm.Model;
|
||||
}
|
||||
|
||||
private int ResolveOpenAiCompatibleMaxTokens()
|
||||
{
|
||||
var llm = _settings.Settings.Llm;
|
||||
var requested = Math.Clamp(llm.MaxContextTokens, 1, 1_000_000);
|
||||
var service = NormalizeServiceName(llm.Service);
|
||||
|
||||
if (service == "vllm")
|
||||
return Math.Min(requested, 8192);
|
||||
|
||||
return requested;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 활성 모델에 매칭되는 RegisteredModel을 찾아 엔드포인트/API키를 반환합니다.
|
||||
/// RegisteredModel에 전용 서버 정보가 있으면 그것을 사용하고, 없으면 기본 설정을 사용합니다.
|
||||
@@ -648,7 +670,7 @@ public partial class LlmService : IDisposable
|
||||
["messages"] = msgs,
|
||||
["stream"] = stream,
|
||||
["temperature"] = ResolveTemperature(),
|
||||
["max_tokens"] = llm.MaxContextTokens
|
||||
["max_tokens"] = ResolveOpenAiCompatibleMaxTokens()
|
||||
};
|
||||
var effort = ResolveReasoningEffort();
|
||||
if (!string.IsNullOrWhiteSpace(effort))
|
||||
|
||||
17
src/AxCopilot/Themes/AgentEmberDark.xaml
Normal file
17
src/AxCopilot/Themes/AgentEmberDark.xaml
Normal file
@@ -0,0 +1,17 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<SolidColorBrush x:Key="LauncherBackground" Color="#1B1510"/>
|
||||
<SolidColorBrush x:Key="ItemBackground" Color="#241C15"/>
|
||||
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#32261D"/>
|
||||
<SolidColorBrush x:Key="ItemHoverBackground" Color="#3A2C21"/>
|
||||
<SolidColorBrush x:Key="PrimaryText" Color="#F6EEE6"/>
|
||||
<SolidColorBrush x:Key="SecondaryText" Color="#D0B9A6"/>
|
||||
<SolidColorBrush x:Key="PlaceholderText" Color="#A68F7E"/>
|
||||
<SolidColorBrush x:Key="AccentColor" Color="#F29A54"/>
|
||||
<SolidColorBrush x:Key="SeparatorColor" Color="#4A382B"/>
|
||||
<SolidColorBrush x:Key="HintBackground" Color="#2B2119"/>
|
||||
<SolidColorBrush x:Key="HintText" Color="#F6C08A"/>
|
||||
<SolidColorBrush x:Key="BorderColor" Color="#4C392B"/>
|
||||
<SolidColorBrush x:Key="ScrollbarThumb" Color="#755B48"/>
|
||||
<SolidColorBrush x:Key="ShadowColor" Color="#99000000"/>
|
||||
</ResourceDictionary>
|
||||
17
src/AxCopilot/Themes/AgentEmberLight.xaml
Normal file
17
src/AxCopilot/Themes/AgentEmberLight.xaml
Normal file
@@ -0,0 +1,17 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<SolidColorBrush x:Key="LauncherBackground" Color="#FAF6EF"/>
|
||||
<SolidColorBrush x:Key="ItemBackground" Color="#FFFDFC"/>
|
||||
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#F3E5D5"/>
|
||||
<SolidColorBrush x:Key="ItemHoverBackground" Color="#F6ECE0"/>
|
||||
<SolidColorBrush x:Key="PrimaryText" Color="#2B2118"/>
|
||||
<SolidColorBrush x:Key="SecondaryText" Color="#7A6758"/>
|
||||
<SolidColorBrush x:Key="PlaceholderText" Color="#A08D7D"/>
|
||||
<SolidColorBrush x:Key="AccentColor" Color="#C96B2C"/>
|
||||
<SolidColorBrush x:Key="SeparatorColor" Color="#E7D8C9"/>
|
||||
<SolidColorBrush x:Key="HintBackground" Color="#F7EBDD"/>
|
||||
<SolidColorBrush x:Key="HintText" Color="#9A531E"/>
|
||||
<SolidColorBrush x:Key="BorderColor" Color="#E4D4C4"/>
|
||||
<SolidColorBrush x:Key="ScrollbarThumb" Color="#CDB8A6"/>
|
||||
<SolidColorBrush x:Key="ShadowColor" Color="#22000000"/>
|
||||
</ResourceDictionary>
|
||||
17
src/AxCopilot/Themes/AgentEmberSystem.xaml
Normal file
17
src/AxCopilot/Themes/AgentEmberSystem.xaml
Normal file
@@ -0,0 +1,17 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<SolidColorBrush x:Key="LauncherBackground" Color="#FAF6EF"/>
|
||||
<SolidColorBrush x:Key="ItemBackground" Color="#FFFDFC"/>
|
||||
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#F3E5D5"/>
|
||||
<SolidColorBrush x:Key="ItemHoverBackground" Color="#F6ECE0"/>
|
||||
<SolidColorBrush x:Key="PrimaryText" Color="#2B2118"/>
|
||||
<SolidColorBrush x:Key="SecondaryText" Color="#7A6758"/>
|
||||
<SolidColorBrush x:Key="PlaceholderText" Color="#A08D7D"/>
|
||||
<SolidColorBrush x:Key="AccentColor" Color="#C96B2C"/>
|
||||
<SolidColorBrush x:Key="SeparatorColor" Color="#E7D8C9"/>
|
||||
<SolidColorBrush x:Key="HintBackground" Color="#F7EBDD"/>
|
||||
<SolidColorBrush x:Key="HintText" Color="#9A531E"/>
|
||||
<SolidColorBrush x:Key="BorderColor" Color="#E4D4C4"/>
|
||||
<SolidColorBrush x:Key="ScrollbarThumb" Color="#CDB8A6"/>
|
||||
<SolidColorBrush x:Key="ShadowColor" Color="#26000000"/>
|
||||
</ResourceDictionary>
|
||||
17
src/AxCopilot/Themes/AgentNordDark.xaml
Normal file
17
src/AxCopilot/Themes/AgentNordDark.xaml
Normal file
@@ -0,0 +1,17 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<SolidColorBrush x:Key="LauncherBackground" Color="#111827"/>
|
||||
<SolidColorBrush x:Key="ItemBackground" Color="#182231"/>
|
||||
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#223247"/>
|
||||
<SolidColorBrush x:Key="ItemHoverBackground" Color="#26384F"/>
|
||||
<SolidColorBrush x:Key="PrimaryText" Color="#E8EEF5"/>
|
||||
<SolidColorBrush x:Key="SecondaryText" Color="#AEBBCB"/>
|
||||
<SolidColorBrush x:Key="PlaceholderText" Color="#8393A7"/>
|
||||
<SolidColorBrush x:Key="AccentColor" Color="#66A3FF"/>
|
||||
<SolidColorBrush x:Key="SeparatorColor" Color="#334155"/>
|
||||
<SolidColorBrush x:Key="HintBackground" Color="#1C2A3B"/>
|
||||
<SolidColorBrush x:Key="HintText" Color="#9CC3FF"/>
|
||||
<SolidColorBrush x:Key="BorderColor" Color="#334155"/>
|
||||
<SolidColorBrush x:Key="ScrollbarThumb" Color="#52647A"/>
|
||||
<SolidColorBrush x:Key="ShadowColor" Color="#99000000"/>
|
||||
</ResourceDictionary>
|
||||
17
src/AxCopilot/Themes/AgentNordLight.xaml
Normal file
17
src/AxCopilot/Themes/AgentNordLight.xaml
Normal file
@@ -0,0 +1,17 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<SolidColorBrush x:Key="LauncherBackground" Color="#F4F7FB"/>
|
||||
<SolidColorBrush x:Key="ItemBackground" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#E6EDF7"/>
|
||||
<SolidColorBrush x:Key="ItemHoverBackground" Color="#EEF3F9"/>
|
||||
<SolidColorBrush x:Key="PrimaryText" Color="#1F2A37"/>
|
||||
<SolidColorBrush x:Key="SecondaryText" Color="#64748B"/>
|
||||
<SolidColorBrush x:Key="PlaceholderText" Color="#94A3B8"/>
|
||||
<SolidColorBrush x:Key="AccentColor" Color="#2F6FBE"/>
|
||||
<SolidColorBrush x:Key="SeparatorColor" Color="#D8E1EB"/>
|
||||
<SolidColorBrush x:Key="HintBackground" Color="#EAF2FB"/>
|
||||
<SolidColorBrush x:Key="HintText" Color="#215C9E"/>
|
||||
<SolidColorBrush x:Key="BorderColor" Color="#D7E0EA"/>
|
||||
<SolidColorBrush x:Key="ScrollbarThumb" Color="#B6C3D2"/>
|
||||
<SolidColorBrush x:Key="ShadowColor" Color="#22000000"/>
|
||||
</ResourceDictionary>
|
||||
17
src/AxCopilot/Themes/AgentNordSystem.xaml
Normal file
17
src/AxCopilot/Themes/AgentNordSystem.xaml
Normal file
@@ -0,0 +1,17 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<SolidColorBrush x:Key="LauncherBackground" Color="#F4F7FB"/>
|
||||
<SolidColorBrush x:Key="ItemBackground" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="ItemSelectedBackground" Color="#E6EDF7"/>
|
||||
<SolidColorBrush x:Key="ItemHoverBackground" Color="#EEF3F9"/>
|
||||
<SolidColorBrush x:Key="PrimaryText" Color="#1F2A37"/>
|
||||
<SolidColorBrush x:Key="SecondaryText" Color="#64748B"/>
|
||||
<SolidColorBrush x:Key="PlaceholderText" Color="#94A3B8"/>
|
||||
<SolidColorBrush x:Key="AccentColor" Color="#2F6FBE"/>
|
||||
<SolidColorBrush x:Key="SeparatorColor" Color="#D8E1EB"/>
|
||||
<SolidColorBrush x:Key="HintBackground" Color="#EAF2FB"/>
|
||||
<SolidColorBrush x:Key="HintText" Color="#215C9E"/>
|
||||
<SolidColorBrush x:Key="BorderColor" Color="#D7E0EA"/>
|
||||
<SolidColorBrush x:Key="ScrollbarThumb" Color="#B6C3D2"/>
|
||||
<SolidColorBrush x:Key="ShadowColor" Color="#26000000"/>
|
||||
</ResourceDictionary>
|
||||
@@ -370,15 +370,6 @@ public class SettingsViewModel : INotifyPropertyChanged
|
||||
set { _agentDecisionLevel = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public string PlanMode
|
||||
{
|
||||
get => "off";
|
||||
set
|
||||
{
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _enableMultiPassDocument;
|
||||
public bool EnableMultiPassDocument
|
||||
{
|
||||
@@ -1581,7 +1572,6 @@ public class SettingsViewModel : INotifyPropertyChanged
|
||||
s.Llm.PlanDiffSeverityMediumRatioPercent = _planDiffSeverityMediumRatioPercent;
|
||||
s.Llm.PlanDiffSeverityHighRatioPercent = _planDiffSeverityHighRatioPercent;
|
||||
s.Llm.AgentDecisionLevel = _agentDecisionLevel;
|
||||
s.Llm.PlanMode = "off";
|
||||
s.Llm.EnableMultiPassDocument = _enableMultiPassDocument;
|
||||
s.Llm.EnableCoworkVerification = _enableCoworkVerification;
|
||||
s.Llm.EnableFilePathHighlight = _enableFilePathHighlight;
|
||||
|
||||
85
src/AxCopilot/Views/AgentPreviewSurfaceFactory.cs
Normal file
85
src/AxCopilot/Views/AgentPreviewSurfaceFactory.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
internal static class AgentPreviewSurfaceFactory
|
||||
{
|
||||
internal static Border CreateSurface(
|
||||
string title,
|
||||
string summary,
|
||||
UIElement body,
|
||||
Brush primary,
|
||||
Brush secondary,
|
||||
Brush itemBackground,
|
||||
Brush borderBrush,
|
||||
Thickness? margin = null)
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Background = itemBackground,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(12, 10, 12, 10),
|
||||
Margin = margin ?? new Thickness(0, 8, 0, 0),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = title,
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primary,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = summary,
|
||||
FontSize = 11,
|
||||
Foreground = secondary,
|
||||
Margin = new Thickness(0, 2, 0, 8),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
},
|
||||
body
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
internal static Border CreatePreviewBox(
|
||||
string content,
|
||||
Brush primary,
|
||||
Brush secondary,
|
||||
Brush borderBrush,
|
||||
double maxHeight,
|
||||
bool monospace = true)
|
||||
{
|
||||
var panel = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = new SolidColorBrush(Color.FromArgb(0x20, 0x80, 0x80, 0x80)),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(8, 6, 8, 6),
|
||||
Child = new ScrollViewer
|
||||
{
|
||||
MaxHeight = maxHeight,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
Content = new TextBlock
|
||||
{
|
||||
Text = string.IsNullOrWhiteSpace(content) ? "(empty)" : content,
|
||||
Foreground = primary,
|
||||
FontFamily = monospace ? new FontFamily("Consolas") : new FontFamily("Pretendard"),
|
||||
FontSize = 11.2,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
LineHeight = 18,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return panel;
|
||||
}
|
||||
}
|
||||
@@ -74,14 +74,26 @@
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="HelpTooltipStyle" TargetType="ToolTip">
|
||||
<Setter Property="Background" Value="#1F2937"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderBrush" Value="#3B82F6"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="12,10"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="HasDropShadow" Value="True"/>
|
||||
<Setter Property="Placement" Value="Mouse"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Border Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="1">
|
||||
<Grid>
|
||||
<Grid Visibility="Collapsed">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="48"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="56"/>
|
||||
</Grid.RowDefinitions>
|
||||
@@ -113,10 +125,159 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<ScrollViewer Grid.Row="1"
|
||||
<Border Grid.Row="1"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource SeparatorColor}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<WrapPanel Margin="18,12,18,10">
|
||||
<Border x:Name="AgentTabBasicCard"
|
||||
Cursor="Hand"
|
||||
CornerRadius="10"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
Padding="10,7"
|
||||
Margin="0,0,8,0"
|
||||
MouseLeftButtonUp="AgentTabBasicCard_MouseLeftButtonUp">
|
||||
<TextBlock Text="기본" FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
|
||||
</Border>
|
||||
<Border x:Name="AgentTabChatCard"
|
||||
Cursor="Hand"
|
||||
CornerRadius="10"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
Padding="10,7"
|
||||
Margin="0,0,8,0"
|
||||
MouseLeftButtonUp="AgentTabChatCard_MouseLeftButtonUp">
|
||||
<TextBlock Text="채팅" FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
|
||||
</Border>
|
||||
<Border x:Name="AgentTabCoworkCard"
|
||||
Cursor="Hand"
|
||||
CornerRadius="10"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
Padding="10,7"
|
||||
Margin="0,0,8,0"
|
||||
MouseLeftButtonUp="AgentTabCoworkCard_MouseLeftButtonUp">
|
||||
<TextBlock Text="코워크" FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
|
||||
</Border>
|
||||
<Border x:Name="AgentTabCodeCard"
|
||||
Cursor="Hand"
|
||||
CornerRadius="10"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
Padding="10,7"
|
||||
Margin="0,0,8,0"
|
||||
MouseLeftButtonUp="AgentTabCodeCard_MouseLeftButtonUp">
|
||||
<TextBlock Text="코드" FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
|
||||
</Border>
|
||||
<Border x:Name="AgentTabDevCard"
|
||||
Cursor="Hand"
|
||||
CornerRadius="10"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
Padding="10,7"
|
||||
Margin="0,0,8,0"
|
||||
MouseLeftButtonUp="AgentTabDevCard_MouseLeftButtonUp">
|
||||
<TextBlock Text="개발자" FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
|
||||
</Border>
|
||||
<Border x:Name="AgentTabToolsCard"
|
||||
Cursor="Hand"
|
||||
CornerRadius="10"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
Padding="10,7"
|
||||
Margin="0,0,8,0"
|
||||
MouseLeftButtonUp="AgentTabToolsCard_MouseLeftButtonUp">
|
||||
<TextBlock Text="도구" FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
|
||||
</Border>
|
||||
<Border x:Name="AgentTabEtcCard"
|
||||
Cursor="Hand"
|
||||
CornerRadius="10"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
Padding="10,7"
|
||||
MouseLeftButtonUp="AgentTabEtcCard_MouseLeftButtonUp">
|
||||
<TextBlock Text="스킬/차단" FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
|
||||
</Border>
|
||||
</WrapPanel>
|
||||
</Border>
|
||||
|
||||
<ScrollViewer Grid.Row="2"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="18,14,18,16">
|
||||
<StackPanel x:Name="PanelBasic">
|
||||
<TextBlock Text="기본 상태"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<Grid Margin="0,8,0,0" Visibility="Collapsed">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Margin="0,0,12,0">
|
||||
<TextBlock Text="AX Agent 사용"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
FontSize="12"/>
|
||||
<TextBlock Text="비활성화하면 AX Agent 대화와 관련 설정이 숨겨집니다."
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
FontSize="11"
|
||||
Margin="0,2,0,0"/>
|
||||
</StackPanel>
|
||||
<CheckBox x:Name="ChkAiEnabled"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0" Visibility="Collapsed">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Margin="0,0,12,0">
|
||||
<TextBlock Text="표현 수준"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
FontSize="12"/>
|
||||
<TextBlock Text="계획, 승인 카드, 보조 설명의 정보 밀도를 조정합니다."
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
FontSize="11"
|
||||
Margin="0,2,0,0"/>
|
||||
</StackPanel>
|
||||
<WrapPanel Grid.Column="1">
|
||||
<Border x:Name="DisplayModeRichCard"
|
||||
Cursor="Hand"
|
||||
CornerRadius="10"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
Padding="10,7"
|
||||
Margin="0,0,6,0"
|
||||
MouseLeftButtonUp="DisplayModeRichCard_MouseLeftButtonUp">
|
||||
<TextBlock Text="풍부하게" FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
|
||||
</Border>
|
||||
<Border x:Name="DisplayModeBalancedCard"
|
||||
Cursor="Hand"
|
||||
CornerRadius="10"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
Padding="10,7"
|
||||
Margin="0,0,6,0"
|
||||
MouseLeftButtonUp="DisplayModeBalancedCard_MouseLeftButtonUp">
|
||||
<TextBlock Text="적절하게" FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
|
||||
</Border>
|
||||
<Border x:Name="DisplayModeSimpleCard"
|
||||
Cursor="Hand"
|
||||
CornerRadius="10"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
Padding="10,7"
|
||||
MouseLeftButtonUp="DisplayModeSimpleCard_MouseLeftButtonUp">
|
||||
<TextBlock Text="간단하게" FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
|
||||
</Border>
|
||||
</WrapPanel>
|
||||
</Grid>
|
||||
|
||||
<Border Height="1" Margin="0,10,0,10" Background="{DynamicResource SeparatorColor}"/>
|
||||
|
||||
<TextBlock Text="테마"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
@@ -155,7 +316,9 @@
|
||||
</WrapPanel>
|
||||
|
||||
<Border Height="1" Margin="0,10,0,10" Background="{DynamicResource SeparatorColor}"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="PanelChat">
|
||||
<TextBlock Text="모델 및 연결"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
@@ -228,23 +391,91 @@
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid Margin="0,8,0,0" Visibility="Collapsed">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="운영 모드"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="운영 모드"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
|
||||
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border.ToolTip>
|
||||
<ToolTip Style="{StaticResource HelpTooltipStyle}">
|
||||
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18" MaxWidth="300">
|
||||
사내 모드는 외부 검색, 외부 URL 열기, 외부 HTTP 호출을 제한합니다.
|
||||
<LineBreak/>사외 모드는 외부 연동이 필요한 작업까지 허용합니다.
|
||||
</TextBlock>
|
||||
</ToolTip>
|
||||
</Border.ToolTip>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<Button x:Name="BtnOperationMode"
|
||||
Grid.Column="1"
|
||||
MinWidth="140"
|
||||
Style="{StaticResource OutlineHoverBtn}"
|
||||
Click="BtnOperationMode_Click"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="기본 출력 형식"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
|
||||
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border.ToolTip>
|
||||
<ToolTip Style="{StaticResource HelpTooltipStyle}">
|
||||
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18" MaxWidth="300">
|
||||
결과물을 어떤 문서 형태로 우선 제안할지 정합니다.
|
||||
<LineBreak/>AI 자동으로 두면 요청 성격에 맞는 형식을 먼저 고릅니다.
|
||||
</TextBlock>
|
||||
</ToolTip>
|
||||
</Border.ToolTip>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<Button x:Name="BtnDefaultOutputFormat"
|
||||
Grid.Column="1"
|
||||
MinWidth="140"
|
||||
Style="{StaticResource OutlineHoverBtn}"
|
||||
Click="BtnDefaultOutputFormat_Click"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="기본 디자인 무드"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
|
||||
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border.ToolTip>
|
||||
<ToolTip Style="{StaticResource HelpTooltipStyle}">
|
||||
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18" MaxWidth="300">
|
||||
HTML 미리보기나 문서형 결과물에서 기본으로 쓸 시각 스타일입니다.
|
||||
</TextBlock>
|
||||
</ToolTip>
|
||||
</Border.ToolTip>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<Button x:Name="BtnDefaultMood"
|
||||
Grid.Column="1"
|
||||
MinWidth="140"
|
||||
Style="{StaticResource OutlineHoverBtn}"
|
||||
Click="BtnDefaultMood_Click"/>
|
||||
</Grid>
|
||||
|
||||
<Border Height="1" Margin="0,10,0,10" Background="{DynamicResource SeparatorColor}"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="PanelCowork" Visibility="Collapsed">
|
||||
<TextBlock Text="권한 및 실행"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
@@ -254,9 +485,22 @@
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="권한 모드"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="권한 모드"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
|
||||
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border.ToolTip>
|
||||
<ToolTip Style="{StaticResource HelpTooltipStyle}">
|
||||
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18" MaxWidth="320">
|
||||
파일 읽기, 수정, 실행 요청을 얼마나 자주 승인받을지 정합니다.
|
||||
<LineBreak/>보수적으로 둘수록 안전하고, 자동 승인 계열일수록 빠르게 진행됩니다.
|
||||
</TextBlock>
|
||||
</ToolTip>
|
||||
</Border.ToolTip>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<Button x:Name="BtnPermissionMode"
|
||||
Grid.Column="1"
|
||||
MinWidth="120"
|
||||
@@ -268,23 +512,21 @@
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="계획 모드"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button x:Name="BtnPlanMode"
|
||||
Grid.Column="1"
|
||||
MinWidth="120"
|
||||
Style="{StaticResource OutlineHoverBtn}"
|
||||
Click="BtnPlanMode_Click"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="추론 강도"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="추론 강도"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
|
||||
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border.ToolTip>
|
||||
<ToolTip Style="{StaticResource HelpTooltipStyle}">
|
||||
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18" MaxWidth="300">
|
||||
답을 만들기 전에 얼마나 많이 따져보고 진행할지 정합니다.
|
||||
</TextBlock>
|
||||
</ToolTip>
|
||||
</Border.ToolTip>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<Button x:Name="BtnReasoningMode"
|
||||
Grid.Column="1"
|
||||
MinWidth="120"
|
||||
@@ -296,17 +538,136 @@
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="폴더 데이터 활용"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button x:Name="BtnFolderDataUsage"
|
||||
Grid.Column="1"
|
||||
MinWidth="120"
|
||||
Style="{StaticResource OutlineHoverBtn}"
|
||||
Click="BtnFolderDataUsage_Click"/>
|
||||
<TextBlock Text="Cowork 검증 강제"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<CheckBox x:Name="ChkEnableCoworkVerification"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="프로젝트 규칙 자동 반영"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<CheckBox x:Name="ChkEnableProjectRules"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="에이전트 메모리 사용"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<CheckBox x:Name="ChkEnableAgentMemory"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="120"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="최대 Agent Pass"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
|
||||
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border.ToolTip>
|
||||
<ToolTip Style="{StaticResource HelpTooltipStyle}">
|
||||
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18" MaxWidth="320">
|
||||
한 번의 작업에서 에이전트가 계획, 실행, 확인 단계를 반복할 수 있는 최대 횟수입니다.
|
||||
</TextBlock>
|
||||
</ToolTip>
|
||||
</Border.ToolTip>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<TextBox x:Name="TxtMaxAgentIterations"
|
||||
Grid.Column="1"
|
||||
Padding="8,5"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="1"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
FontSize="12"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="AdvancedPanel">
|
||||
<StackPanel x:Name="PanelCode" Visibility="Collapsed">
|
||||
<TextBlock Text="코드 실행"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="Code 검증 강제"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<CheckBox x:Name="ChkEnableCodeVerification"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="병렬 도구 실행"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<CheckBox x:Name="ChkEnableParallelTools"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="Worktree 도구"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<CheckBox x:Name="ChkEnableWorktreeTools"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="Team 도구"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<CheckBox x:Name="ChkEnableTeamTools"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="Cron 도구"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<CheckBox x:Name="ChkEnableCronTools"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="PanelDev" Visibility="Collapsed">
|
||||
<Border Height="1" Margin="0,10,0,10" Background="{DynamicResource SeparatorColor}"/>
|
||||
|
||||
<TextBlock Text="컨텍스트 및 오류 관리"
|
||||
@@ -347,9 +708,19 @@
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="120"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="최대 컨텍스트 토큰"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="최대 컨텍스트 토큰"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
|
||||
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border.ToolTip>
|
||||
<ToolTip Style="{StaticResource HelpTooltipStyle}">
|
||||
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18" MaxWidth="280">한 번의 응답 생성에 넘겨줄 최대 문맥 크기입니다.</TextBlock>
|
||||
</ToolTip>
|
||||
</Border.ToolTip>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<TextBox x:Name="TxtMaxContextTokens"
|
||||
Grid.Column="1"
|
||||
Padding="8,5"
|
||||
@@ -364,9 +735,19 @@
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="120"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="오류 재시도 횟수"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="오류 재시도 횟수"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
|
||||
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border.ToolTip>
|
||||
<ToolTip Style="{StaticResource HelpTooltipStyle}">
|
||||
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18" MaxWidth="280">도구 실행 실패 시 자동으로 다시 시도할 최대 횟수입니다.</TextBlock>
|
||||
</ToolTip>
|
||||
</Border.ToolTip>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<TextBox x:Name="TxtMaxRetryOnError"
|
||||
Grid.Column="1"
|
||||
Padding="8,5"
|
||||
@@ -377,8 +758,9 @@
|
||||
FontSize="12"/>
|
||||
</Grid>
|
||||
|
||||
<Border Height="1" Margin="0,10,0,10" Background="{DynamicResource SeparatorColor}"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="PanelTools" Visibility="Collapsed">
|
||||
<TextBlock Text="도구 및 검증"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
@@ -388,9 +770,19 @@
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="스킬 시스템"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="스킬 시스템"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
|
||||
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border.ToolTip>
|
||||
<ToolTip Style="{StaticResource HelpTooltipStyle}">
|
||||
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18" MaxWidth="300">슬래시 명령으로 재사용 가능한 작업 템플릿을 불러오는 기능입니다.</TextBlock>
|
||||
</ToolTip>
|
||||
</Border.ToolTip>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<CheckBox x:Name="ChkEnableSkillSystem"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
@@ -400,9 +792,19 @@
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="도구 훅 사용"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="도구 훅 사용"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
|
||||
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border.ToolTip>
|
||||
<ToolTip Style="{StaticResource HelpTooltipStyle}">
|
||||
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18" MaxWidth="300">특정 도구 실행 전후에 스크립트를 자동으로 호출하는 확장 기능입니다.</TextBlock>
|
||||
</ToolTip>
|
||||
</Border.ToolTip>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<CheckBox x:Name="ChkEnableToolHooks"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
@@ -431,15 +833,140 @@
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
<Border Height="1" Margin="0,12,0,12" Background="{DynamicResource SeparatorColor}"/>
|
||||
<TextBlock Text="도구 노출"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<TextBlock Text="AX Agent에 노출할 도구를 내부 설정에서 바로 켜고 끕니다."
|
||||
Margin="0,4,0,8"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
<StackPanel x:Name="ToolCardsPanel"/>
|
||||
<Border Height="1" Margin="0,12,0,12" Background="{DynamicResource SeparatorColor}"/>
|
||||
<TextBlock Text="도구 훅"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<StackPanel x:Name="HookListPanel" Margin="0,8,0,0"/>
|
||||
<Button Content="훅 추가"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0,8,0,0"
|
||||
Style="{StaticResource OutlineHoverBtn}"
|
||||
Click="BtnAddHook_Click"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="PanelEtc" Visibility="Collapsed">
|
||||
<TextBlock Text="스킬/차단"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<TextBlock Text="스킬 폴더와 슬래시/드래그 동작, 폴백 모델과 MCP 서버를 관리합니다."
|
||||
Margin="0,4,0,8"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="스킬 폴더"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
|
||||
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border.ToolTip>
|
||||
<ToolTip Style="{StaticResource HelpTooltipStyle}">
|
||||
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18" MaxWidth="320">추가 스킬을 읽어올 폴더입니다. `.skill.md` 또는 `SKILL.md` 파일을 두면 저장 후 다시 불러옵니다.</TextBlock>
|
||||
</ToolTip>
|
||||
</Border.ToolTip>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<TextBox x:Name="TxtSkillsFolderPath"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="0,8,0,0"
|
||||
Padding="8,5"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="1"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
FontSize="12"/>
|
||||
<Button Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Margin="8,0,0,0"
|
||||
Style="{StaticResource OutlineHoverBtn}"
|
||||
Content="찾아보기"
|
||||
Click="BtnBrowseSkillFolder_Click"/>
|
||||
<Button Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
Margin="8,0,0,0"
|
||||
Style="{StaticResource OutlineHoverBtn}"
|
||||
Content="열기"
|
||||
Click="BtnOpenSkillFolder_Click"/>
|
||||
</Grid>
|
||||
<Border Height="1" Margin="0,12,0,12" Background="{DynamicResource SeparatorColor}"/>
|
||||
<TextBlock Text="로드된 스킬"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<TextBlock Text="현재 AX Agent에서 사용할 수 있는 슬래시 스킬 목록입니다."
|
||||
Margin="0,4,0,8"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
<StackPanel x:Name="SkillListPanel"/>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="120"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="슬래시 팝업 표시 개수"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
|
||||
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border.ToolTip>
|
||||
<ToolTip Style="{StaticResource HelpTooltipStyle}">
|
||||
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18" MaxWidth="300">`/` 입력 시 한 번에 보여줄 명령과 스킬 개수입니다.</TextBlock>
|
||||
</ToolTip>
|
||||
</Border.ToolTip>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<TextBox x:Name="TxtSlashPopupPageSize"
|
||||
Grid.Column="1"
|
||||
Padding="8,5"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="1"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
FontSize="12"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="Cowork 검증 강제"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<CheckBox x:Name="ChkEnableCoworkVerification"
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="드래그 앤 드롭 AI 액션"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
|
||||
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border.ToolTip>
|
||||
<ToolTip Style="{StaticResource HelpTooltipStyle}">
|
||||
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18" MaxWidth="300">파일을 끌어놓았을 때 AI 작업 추천과 첨부 보조 동작을 보여줍니다.</TextBlock>
|
||||
</ToolTip>
|
||||
</Border.ToolTip>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<CheckBox x:Name="ChkEnableDragDropAiActions"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
@@ -448,30 +975,61 @@
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="Code 검증 강제"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<CheckBox x:Name="ChkEnableCodeVerification"
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="선택 시 자동 전송"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
|
||||
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border.ToolTip>
|
||||
<ToolTip Style="{StaticResource HelpTooltipStyle}">
|
||||
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18" MaxWidth="300">추천 액션을 고르면 바로 메시지를 보내 실행까지 이어갑니다.</TextBlock>
|
||||
</ToolTip>
|
||||
</Border.ToolTip>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<CheckBox x:Name="ChkDragDropAutoSend"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Border Height="1" Margin="0,12,0,12" Background="{DynamicResource SeparatorColor}"/>
|
||||
<TextBlock Text="폴백 모델"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<StackPanel x:Name="FallbackModelsPanel" Margin="0,8,0,0"/>
|
||||
<Border Height="1" Margin="0,12,0,12" Background="{DynamicResource SeparatorColor}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="병렬 도구 실행"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<CheckBox x:Name="ChkEnableParallelTools"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="MCP 서버"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
|
||||
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border.ToolTip>
|
||||
<ToolTip Style="{StaticResource HelpTooltipStyle}">
|
||||
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18" MaxWidth="320">외부 도구 서버를 연결해 AX Agent가 브라우저나 파일시스템 같은 확장 기능을 사용할 수 있게 합니다.</TextBlock>
|
||||
</ToolTip>
|
||||
</Border.ToolTip>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Style="{StaticResource OutlineHoverBtn}"
|
||||
Content="서버 추가"
|
||||
Click="BtnAddMcpServer_Click"/>
|
||||
</Grid>
|
||||
<StackPanel x:Name="McpServerListPanel" Margin="0,8,0,0"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<Border Grid.Row="2"
|
||||
<Border Grid.Row="3"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource SeparatorColor}"
|
||||
BorderThickness="0,1,0,0">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Linq;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Services.Agent;
|
||||
@@ -15,11 +16,17 @@ public partial class AgentSettingsWindow : Window
|
||||
private readonly SettingsService _settings;
|
||||
private readonly LlmSettings _llm;
|
||||
private string _permissionMode = PermissionModeCatalog.Deny;
|
||||
private string _planMode = "off";
|
||||
private string _reasoningMode = "detailed";
|
||||
private string _folderDataUsage = "active";
|
||||
private string _operationMode = OperationModePolicy.InternalMode;
|
||||
private string _displayMode = "rich";
|
||||
private string _defaultOutputFormat = "auto";
|
||||
private string _defaultMood = "modern";
|
||||
private string _activePanel = "basic";
|
||||
private string _selectedService = "ollama";
|
||||
private string _selectedTheme = "system";
|
||||
private string _selectedModel = string.Empty;
|
||||
private bool _toolCardsLoaded;
|
||||
private HashSet<string> _disabledTools = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public AgentSettingsWindow(SettingsService settings)
|
||||
{
|
||||
@@ -31,14 +38,23 @@ public partial class AgentSettingsWindow : Window
|
||||
|
||||
private void LoadFromSettings()
|
||||
{
|
||||
_selectedModel = _llm.Model ?? "";
|
||||
SkillService.EnsureSkillFolder();
|
||||
SkillService.LoadSkills(_llm.SkillsFolderPath);
|
||||
|
||||
_selectedService = (_llm.Service ?? "ollama").Trim().ToLowerInvariant();
|
||||
_selectedTheme = (_llm.AgentTheme ?? "system").Trim().ToLowerInvariant();
|
||||
_selectedModel = GetSelectedModelForService(_selectedService);
|
||||
ModelInput.Text = _selectedModel;
|
||||
_permissionMode = PermissionModeCatalog.NormalizeGlobalMode(_llm.FilePermission);
|
||||
_planMode = string.IsNullOrWhiteSpace(_llm.PlanMode) ? "off" : _llm.PlanMode;
|
||||
_reasoningMode = string.IsNullOrWhiteSpace(_llm.AgentDecisionLevel) ? "detailed" : _llm.AgentDecisionLevel;
|
||||
_folderDataUsage = string.IsNullOrWhiteSpace(_llm.FolderDataUsage) ? "active" : _llm.FolderDataUsage;
|
||||
_operationMode = OperationModePolicy.Normalize(_settings.Settings.OperationMode);
|
||||
_displayMode = "rich";
|
||||
_defaultOutputFormat = string.IsNullOrWhiteSpace(_llm.DefaultOutputFormat) ? "auto" : _llm.DefaultOutputFormat;
|
||||
_defaultMood = string.IsNullOrWhiteSpace(_llm.DefaultMood) ? "modern" : _llm.DefaultMood;
|
||||
|
||||
_settings.Settings.AiEnabled = true;
|
||||
ChkAiEnabled.IsChecked = true;
|
||||
ChkVllmAllowInsecureTls.IsChecked = _llm.VllmAllowInsecureTls;
|
||||
ChkEnableProactiveCompact.IsChecked = _llm.EnableProactiveContextCompact;
|
||||
TxtContextCompactTriggerPercent.Text = Math.Clamp(_llm.ContextCompactTriggerPercent, 10, 95).ToString();
|
||||
TxtMaxContextTokens.Text = Math.Max(1024, _llm.MaxContextTokens).ToString();
|
||||
@@ -49,40 +65,92 @@ public partial class AgentSettingsWindow : Window
|
||||
ChkEnableHookInputMutation.IsChecked = _llm.EnableHookInputMutation;
|
||||
ChkEnableHookPermissionUpdate.IsChecked = _llm.EnableHookPermissionUpdate;
|
||||
ChkEnableCoworkVerification.IsChecked = _llm.EnableCoworkVerification;
|
||||
ChkEnableProjectRules.IsChecked = _llm.EnableProjectRules;
|
||||
ChkEnableAgentMemory.IsChecked = _llm.EnableAgentMemory;
|
||||
TxtMaxAgentIterations.Text = Math.Clamp(_llm.MaxAgentIterations, 1, 200).ToString();
|
||||
ChkEnableCodeVerification.IsChecked = _llm.Code.EnableCodeVerification;
|
||||
ChkEnableParallelTools.IsChecked = _llm.EnableParallelTools;
|
||||
ChkEnableWorktreeTools.IsChecked = _llm.Code.EnableWorktreeTools;
|
||||
ChkEnableTeamTools.IsChecked = _llm.Code.EnableTeamTools;
|
||||
ChkEnableCronTools.IsChecked = _llm.Code.EnableCronTools;
|
||||
TxtSkillsFolderPath.Text = _llm.SkillsFolderPath ?? "";
|
||||
TxtSlashPopupPageSize.Text = Math.Clamp(_llm.SlashPopupPageSize, 3, 20).ToString();
|
||||
ChkEnableDragDropAiActions.IsChecked = _llm.EnableDragDropAiActions;
|
||||
ChkDragDropAutoSend.IsChecked = _llm.DragDropAutoSend;
|
||||
_disabledTools = new HashSet<string>(_llm.DisabledTools ?? new(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
RefreshServiceCards();
|
||||
RefreshThemeCards();
|
||||
RefreshModeLabels();
|
||||
RefreshChatOptionLabels();
|
||||
RefreshDisplayModeCards();
|
||||
ShowPanel("basic");
|
||||
BuildModelChips();
|
||||
BuildFallbackModelsPanel();
|
||||
BuildMcpServerCards();
|
||||
BuildHookCards();
|
||||
BuildSkillListPanel();
|
||||
}
|
||||
|
||||
private void RefreshThemeCards()
|
||||
{
|
||||
var selected = (_llm.AgentTheme ?? "system").ToLowerInvariant();
|
||||
SetCardSelection(ThemeSystemCard, selected == "system");
|
||||
SetCardSelection(ThemeLightCard, selected == "light");
|
||||
SetCardSelection(ThemeDarkCard, selected == "dark");
|
||||
SetCardSelection(ThemeSystemCard, _selectedTheme == "system");
|
||||
SetCardSelection(ThemeLightCard, _selectedTheme == "light");
|
||||
SetCardSelection(ThemeDarkCard, _selectedTheme == "dark");
|
||||
}
|
||||
|
||||
private void RefreshServiceCards()
|
||||
{
|
||||
var service = (_llm.Service ?? "ollama").ToLowerInvariant();
|
||||
SetCardSelection(SvcOllamaCard, service == "ollama");
|
||||
SetCardSelection(SvcVllmCard, service == "vllm");
|
||||
SetCardSelection(SvcGeminiCard, service == "gemini");
|
||||
SetCardSelection(SvcClaudeCard, service is "claude" or "sigmoid");
|
||||
SetCardSelection(SvcOllamaCard, _selectedService == "ollama");
|
||||
SetCardSelection(SvcVllmCard, _selectedService == "vllm");
|
||||
SetCardSelection(SvcGeminiCard, _selectedService == "gemini");
|
||||
SetCardSelection(SvcClaudeCard, _selectedService is "claude" or "sigmoid");
|
||||
}
|
||||
|
||||
private void RefreshModeLabels()
|
||||
{
|
||||
BtnOperationMode.Content = BuildOperationModeLabel(_operationMode);
|
||||
BtnPermissionMode.Content = PermissionModeCatalog.ToDisplayLabel(_permissionMode);
|
||||
BtnPlanMode.Content = BuildPlanModeLabel(_planMode);
|
||||
BtnReasoningMode.Content = BuildReasoningModeLabel(_reasoningMode);
|
||||
BtnFolderDataUsage.Content = BuildFolderDataUsageLabel(_folderDataUsage);
|
||||
AdvancedPanel.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private void RefreshDisplayModeCards()
|
||||
{
|
||||
SetCardSelection(DisplayModeRichCard, _displayMode == "rich");
|
||||
SetCardSelection(DisplayModeBalancedCard, _displayMode == "balanced");
|
||||
SetCardSelection(DisplayModeSimpleCard, _displayMode == "simple");
|
||||
}
|
||||
|
||||
private void RefreshChatOptionLabels()
|
||||
{
|
||||
BtnDefaultOutputFormat.Content = BuildOutputFormatLabel(_defaultOutputFormat);
|
||||
BtnDefaultMood.Content = BuildMoodLabel(_defaultMood);
|
||||
}
|
||||
|
||||
private void RefreshTabCards()
|
||||
{
|
||||
SetCardSelection(AgentTabBasicCard, _activePanel == "basic");
|
||||
SetCardSelection(AgentTabChatCard, _activePanel == "chat");
|
||||
SetCardSelection(AgentTabCoworkCard, _activePanel == "cowork");
|
||||
SetCardSelection(AgentTabCodeCard, _activePanel == "code");
|
||||
SetCardSelection(AgentTabDevCard, _activePanel == "dev");
|
||||
SetCardSelection(AgentTabToolsCard, _activePanel == "tools");
|
||||
SetCardSelection(AgentTabEtcCard, _activePanel == "etc");
|
||||
}
|
||||
|
||||
private void ShowPanel(string panel)
|
||||
{
|
||||
_activePanel = panel;
|
||||
PanelBasic.Visibility = panel == "basic" ? Visibility.Visible : Visibility.Collapsed;
|
||||
PanelChat.Visibility = panel == "chat" ? Visibility.Visible : Visibility.Collapsed;
|
||||
PanelCowork.Visibility = panel == "cowork" ? Visibility.Visible : Visibility.Collapsed;
|
||||
PanelCode.Visibility = panel == "code" ? Visibility.Visible : Visibility.Collapsed;
|
||||
PanelDev.Visibility = panel == "dev" ? Visibility.Visible : Visibility.Collapsed;
|
||||
PanelTools.Visibility = panel == "tools" ? Visibility.Visible : Visibility.Collapsed;
|
||||
PanelEtc.Visibility = panel == "etc" ? Visibility.Visible : Visibility.Collapsed;
|
||||
RefreshTabCards();
|
||||
if (panel == "tools")
|
||||
LoadToolCards();
|
||||
}
|
||||
|
||||
private static string BuildOperationModeLabel(string mode)
|
||||
@@ -92,16 +160,6 @@ public partial class AgentSettingsWindow : Window
|
||||
: "사내 모드";
|
||||
}
|
||||
|
||||
private static string BuildPlanModeLabel(string mode)
|
||||
{
|
||||
return (mode ?? "off").ToLowerInvariant() switch
|
||||
{
|
||||
"always" => "항상 계획",
|
||||
"auto" => "자동 계획",
|
||||
_ => "끄기",
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildReasoningModeLabel(string mode)
|
||||
{
|
||||
return (mode ?? "detailed").ToLowerInvariant() switch
|
||||
@@ -112,13 +170,34 @@ public partial class AgentSettingsWindow : Window
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildFolderDataUsageLabel(string mode)
|
||||
private static string BuildOutputFormatLabel(string format)
|
||||
{
|
||||
return (mode ?? "none").ToLowerInvariant() switch
|
||||
return (format ?? "auto").ToLowerInvariant() switch
|
||||
{
|
||||
"active" => "적극 활용",
|
||||
"passive" => "소극 활용",
|
||||
_ => "활용하지 않음",
|
||||
"xlsx" => "Excel",
|
||||
"html" => "HTML 보고서",
|
||||
"docx" => "Word",
|
||||
"pptx" => "PowerPoint",
|
||||
"pdf" => "PDF",
|
||||
"md" => "Markdown",
|
||||
"txt" => "텍스트",
|
||||
_ => "AI 자동",
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildMoodLabel(string mood)
|
||||
{
|
||||
var found = TemplateService.AllMoods.FirstOrDefault(m => string.Equals(m.Key, mood, StringComparison.OrdinalIgnoreCase));
|
||||
return found == null ? "모던" : $"{found.Icon} {found.Label}";
|
||||
}
|
||||
|
||||
private static string NormalizeDisplayMode(string? mode)
|
||||
{
|
||||
return (mode ?? "balanced").Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"rich" => "rich",
|
||||
"simple" => "simple",
|
||||
_ => "balanced",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -135,7 +214,7 @@ public partial class AgentSettingsWindow : Window
|
||||
private void BuildModelChips()
|
||||
{
|
||||
ModelChipPanel.Children.Clear();
|
||||
var models = GetModelCandidates(_llm.Service);
|
||||
var models = GetModelCandidates(_selectedService);
|
||||
foreach (var model in models)
|
||||
{
|
||||
var captured = model;
|
||||
@@ -195,17 +274,30 @@ public partial class AgentSettingsWindow : Window
|
||||
|
||||
private void SetService(string service)
|
||||
{
|
||||
_llm.Service = service;
|
||||
_selectedService = service;
|
||||
_selectedModel = GetSelectedModelForService(service);
|
||||
ModelInput.Text = _selectedModel;
|
||||
RefreshServiceCards();
|
||||
BuildModelChips();
|
||||
}
|
||||
|
||||
private void SetTheme(string theme)
|
||||
{
|
||||
_llm.AgentTheme = theme;
|
||||
_selectedTheme = theme;
|
||||
RefreshThemeCards();
|
||||
}
|
||||
|
||||
private string GetSelectedModelForService(string? service)
|
||||
{
|
||||
return (service ?? "ollama").Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"vllm" => _llm.VllmModel ?? "",
|
||||
"gemini" => _llm.GeminiModel ?? "",
|
||||
"claude" or "sigmoid" => _llm.ClaudeModel ?? "",
|
||||
_ => _llm.OllamaModel ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
private static string CycleOperationMode(string current)
|
||||
{
|
||||
return OperationModePolicy.Normalize(current) == OperationModePolicy.ExternalMode
|
||||
@@ -306,11 +398,21 @@ public partial class AgentSettingsWindow : Window
|
||||
private void ThemeSystemCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetTheme("system");
|
||||
private void ThemeLightCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetTheme("light");
|
||||
private void ThemeDarkCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetTheme("dark");
|
||||
private void DisplayModeRichCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _displayMode = "rich"; RefreshDisplayModeCards(); }
|
||||
private void DisplayModeBalancedCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _displayMode = "balanced"; RefreshDisplayModeCards(); }
|
||||
private void DisplayModeSimpleCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _displayMode = "simple"; RefreshDisplayModeCards(); }
|
||||
|
||||
private void SvcOllamaCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetService("ollama");
|
||||
private void SvcVllmCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetService("vllm");
|
||||
private void SvcGeminiCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetService("gemini");
|
||||
private void SvcClaudeCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetService("claude");
|
||||
private void AgentTabBasicCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => ShowPanel("basic");
|
||||
private void AgentTabChatCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => ShowPanel("chat");
|
||||
private void AgentTabCoworkCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => ShowPanel("cowork");
|
||||
private void AgentTabCodeCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => ShowPanel("code");
|
||||
private void AgentTabDevCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => ShowPanel("dev");
|
||||
private void AgentTabToolsCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => ShowPanel("tools");
|
||||
private void AgentTabEtcCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => ShowPanel("etc");
|
||||
|
||||
private void BtnOperationMode_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
@@ -343,17 +445,6 @@ public partial class AgentSettingsWindow : Window
|
||||
RefreshModeLabels();
|
||||
}
|
||||
|
||||
private void BtnPlanMode_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_planMode = _planMode switch
|
||||
{
|
||||
"off" => "auto",
|
||||
"auto" => "always",
|
||||
_ => "off",
|
||||
};
|
||||
RefreshModeLabels();
|
||||
}
|
||||
|
||||
private void BtnReasoningMode_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_reasoningMode = _reasoningMode switch
|
||||
@@ -365,24 +456,65 @@ public partial class AgentSettingsWindow : Window
|
||||
RefreshModeLabels();
|
||||
}
|
||||
|
||||
private void BtnFolderDataUsage_Click(object sender, RoutedEventArgs e)
|
||||
private void BtnDefaultOutputFormat_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_folderDataUsage = _folderDataUsage switch
|
||||
_defaultOutputFormat = (_defaultOutputFormat ?? "auto").ToLowerInvariant() switch
|
||||
{
|
||||
"none" => "passive",
|
||||
"passive" => "active",
|
||||
_ => "none",
|
||||
"auto" => "docx",
|
||||
"docx" => "html",
|
||||
"html" => "xlsx",
|
||||
"xlsx" => "pdf",
|
||||
"pdf" => "md",
|
||||
"md" => "txt",
|
||||
_ => "auto",
|
||||
};
|
||||
RefreshModeLabels();
|
||||
RefreshChatOptionLabels();
|
||||
}
|
||||
|
||||
private void BtnDefaultMood_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var moods = TemplateService.AllMoods.Select(m => m.Key).Where(k => !string.IsNullOrWhiteSpace(k)).ToList();
|
||||
if (moods.Count == 0)
|
||||
return;
|
||||
|
||||
var index = moods.FindIndex(k => string.Equals(k, _defaultMood, StringComparison.OrdinalIgnoreCase));
|
||||
_defaultMood = index < 0 || index + 1 >= moods.Count ? moods[0] : moods[index + 1];
|
||||
RefreshChatOptionLabels();
|
||||
}
|
||||
|
||||
private void BtnSave_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_llm.Model = string.IsNullOrWhiteSpace(_selectedModel) ? (_llm.Model ?? "") : _selectedModel.Trim();
|
||||
_selectedModel = string.IsNullOrWhiteSpace(ModelInput.Text) ? _selectedModel : ModelInput.Text.Trim();
|
||||
_llm.Service = _selectedService;
|
||||
_llm.AgentTheme = _selectedTheme;
|
||||
_llm.FilePermission = _permissionMode;
|
||||
_llm.PlanMode = _planMode;
|
||||
_llm.DefaultAgentPermission = _permissionMode;
|
||||
_llm.AgentDecisionLevel = _reasoningMode;
|
||||
_llm.FolderDataUsage = _folderDataUsage;
|
||||
_llm.AgentUiExpressionLevel = "rich";
|
||||
_llm.DefaultOutputFormat = _defaultOutputFormat;
|
||||
_llm.DefaultMood = _defaultMood;
|
||||
_llm.VllmAllowInsecureTls = ChkVllmAllowInsecureTls.IsChecked == true;
|
||||
|
||||
switch (_selectedService)
|
||||
{
|
||||
case "vllm":
|
||||
_llm.VllmModel = _selectedModel;
|
||||
_llm.Model = _selectedModel;
|
||||
break;
|
||||
case "gemini":
|
||||
_llm.GeminiModel = _selectedModel;
|
||||
_llm.Model = _selectedModel;
|
||||
break;
|
||||
case "claude":
|
||||
case "sigmoid":
|
||||
_llm.ClaudeModel = _selectedModel;
|
||||
_llm.Model = _selectedModel;
|
||||
break;
|
||||
default:
|
||||
_llm.OllamaModel = _selectedModel;
|
||||
_llm.Model = _selectedModel;
|
||||
break;
|
||||
}
|
||||
|
||||
_llm.EnableProactiveContextCompact = ChkEnableProactiveCompact.IsChecked == true;
|
||||
_llm.ContextCompactTriggerPercent = ParseInt(TxtContextCompactTriggerPercent.Text, 80, 10, 95);
|
||||
@@ -394,15 +526,563 @@ public partial class AgentSettingsWindow : Window
|
||||
_llm.EnableHookInputMutation = ChkEnableHookInputMutation.IsChecked == true;
|
||||
_llm.EnableHookPermissionUpdate = ChkEnableHookPermissionUpdate.IsChecked == true;
|
||||
_llm.EnableCoworkVerification = ChkEnableCoworkVerification.IsChecked == true;
|
||||
_llm.EnableProjectRules = ChkEnableProjectRules.IsChecked == true;
|
||||
_llm.EnableAgentMemory = ChkEnableAgentMemory.IsChecked == true;
|
||||
_llm.MaxAgentIterations = ParseInt(TxtMaxAgentIterations.Text, 25, 1, 200);
|
||||
_llm.Code.EnableCodeVerification = ChkEnableCodeVerification.IsChecked == true;
|
||||
_llm.EnableParallelTools = ChkEnableParallelTools.IsChecked == true;
|
||||
_llm.Code.EnableWorktreeTools = ChkEnableWorktreeTools.IsChecked == true;
|
||||
_llm.Code.EnableTeamTools = ChkEnableTeamTools.IsChecked == true;
|
||||
_llm.Code.EnableCronTools = ChkEnableCronTools.IsChecked == true;
|
||||
_llm.SkillsFolderPath = TxtSkillsFolderPath.Text?.Trim() ?? "";
|
||||
_llm.SlashPopupPageSize = ParseInt(TxtSlashPopupPageSize.Text, 7, 3, 20);
|
||||
_llm.EnableDragDropAiActions = ChkEnableDragDropAiActions.IsChecked == true;
|
||||
_llm.DragDropAutoSend = ChkDragDropAutoSend.IsChecked == true;
|
||||
_llm.DisabledTools = _disabledTools.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
_settings.Settings.AiEnabled = true;
|
||||
_settings.Settings.OperationMode = OperationModePolicy.Normalize(_operationMode);
|
||||
_settings.Save();
|
||||
SkillService.LoadSkills(_llm.SkillsFolderPath);
|
||||
BuildSkillListPanel();
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void BtnBrowseSkillFolder_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new System.Windows.Forms.FolderBrowserDialog
|
||||
{
|
||||
Description = "스킬 폴더 선택",
|
||||
ShowNewFolderButton = true,
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(TxtSkillsFolderPath.Text) && System.IO.Directory.Exists(TxtSkillsFolderPath.Text))
|
||||
dlg.SelectedPath = TxtSkillsFolderPath.Text;
|
||||
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
||||
TxtSkillsFolderPath.Text = dlg.SelectedPath;
|
||||
}
|
||||
|
||||
private void BtnOpenSkillFolder_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var folder = string.IsNullOrWhiteSpace(TxtSkillsFolderPath.Text)
|
||||
? System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills")
|
||||
: TxtSkillsFolderPath.Text.Trim();
|
||||
if (!System.IO.Directory.Exists(folder))
|
||||
System.IO.Directory.CreateDirectory(folder);
|
||||
try { System.Diagnostics.Process.Start("explorer.exe", folder); } catch { }
|
||||
}
|
||||
|
||||
private void BuildSkillListPanel()
|
||||
{
|
||||
if (SkillListPanel == null)
|
||||
return;
|
||||
|
||||
SkillListPanel.Children.Clear();
|
||||
var skills = SkillService.Skills;
|
||||
if (skills.Count == 0)
|
||||
{
|
||||
SkillListPanel.Children.Add(new Border
|
||||
{
|
||||
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke,
|
||||
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(12, 10, 12, 10),
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = "로드된 스킬이 없습니다. 스킬 폴더를 열어 `.skill.md` 또는 `SKILL.md` 파일을 추가한 뒤 저장하면 다시 불러옵니다.",
|
||||
FontSize = 11,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var groups = new[]
|
||||
{
|
||||
new { Title = "내장 스킬", Items = skills.Where(s => string.IsNullOrWhiteSpace(s.Requires)).ToList() },
|
||||
new { Title = "고급 스킬", Items = skills.Where(s => !string.IsNullOrWhiteSpace(s.Requires)).ToList() },
|
||||
};
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (group.Items.Count == 0)
|
||||
continue;
|
||||
|
||||
SkillListPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"{group.Title} ({group.Items.Count})",
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
});
|
||||
|
||||
foreach (var skill in group.Items.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase))
|
||||
SkillListPanel.Children.Add(CreateSkillCard(skill));
|
||||
}
|
||||
}
|
||||
|
||||
private Border CreateSkillCard(SkillDefinition skill)
|
||||
{
|
||||
var available = skill.IsAvailable;
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var card = new Border
|
||||
{
|
||||
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke,
|
||||
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(12, 10, 12, 10),
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
Opacity = available ? 1.0 : 0.72,
|
||||
};
|
||||
static Brush HexBrush(string hex) => (Brush)new BrushConverter().ConvertFromString(hex)!;
|
||||
|
||||
var root = new StackPanel();
|
||||
var header = new DockPanel();
|
||||
header.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "/" + skill.Name,
|
||||
FontSize = 12.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = available ? accentBrush : secondaryText,
|
||||
});
|
||||
|
||||
var badge = new Border
|
||||
{
|
||||
Background = available
|
||||
? HexBrush("#ECFDF5")
|
||||
: HexBrush("#FEF2F2"),
|
||||
BorderBrush = available
|
||||
? HexBrush("#BBF7D0")
|
||||
: HexBrush("#FECACA"),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Padding = new Thickness(7, 2, 7, 2),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = available ? "사용 가능" : (string.IsNullOrWhiteSpace(skill.UnavailableHint) ? "사용 불가" : skill.UnavailableHint),
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = available ? HexBrush("#166534") : HexBrush("#991B1B"),
|
||||
}
|
||||
};
|
||||
DockPanel.SetDock(badge, Dock.Right);
|
||||
header.Children.Add(badge);
|
||||
root.Children.Add(header);
|
||||
|
||||
root.Children.Add(new TextBlock
|
||||
{
|
||||
Text = string.IsNullOrWhiteSpace(skill.Label) ? skill.Description : $"{skill.Label} · {skill.Description}",
|
||||
Margin = new Thickness(0, 4, 0, 0),
|
||||
FontSize = 11,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Foreground = secondaryText,
|
||||
});
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(skill.Requires))
|
||||
{
|
||||
root.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"필요 런타임: {skill.Requires}",
|
||||
Margin = new Thickness(0, 4, 0, 0),
|
||||
FontSize = 10.5,
|
||||
Foreground = secondaryText,
|
||||
});
|
||||
}
|
||||
|
||||
card.Child = root;
|
||||
return card;
|
||||
}
|
||||
|
||||
private void LoadToolCards()
|
||||
{
|
||||
if (_toolCardsLoaded || ToolCardsPanel == null) return;
|
||||
_toolCardsLoaded = true;
|
||||
|
||||
using var tools = ToolRegistry.CreateDefault();
|
||||
var categories = new Dictionary<string, List<IAgentTool>>
|
||||
{
|
||||
["파일/검색"] = new(),
|
||||
["문서/리뷰"] = new(),
|
||||
["코드/개발"] = new(),
|
||||
["시스템/유틸"] = new(),
|
||||
};
|
||||
var toolCategoryMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["file_read"] = "파일/검색", ["file_write"] = "파일/검색", ["file_edit"] = "파일/검색", ["glob"] = "파일/검색", ["grep"] = "파일/검색",
|
||||
["document_review"] = "문서/리뷰", ["format_convert"] = "문서/리뷰", ["template_render"] = "문서/리뷰", ["text_summarize"] = "문서/리뷰",
|
||||
["build_run"] = "코드/개발", ["git_tool"] = "코드/개발", ["lsp"] = "코드/개발", ["code_review"] = "코드/개발", ["test_loop"] = "코드/개발",
|
||||
["process"] = "시스템/유틸", ["notify"] = "시스템/유틸", ["clipboard"] = "시스템/유틸", ["env"] = "시스템/유틸", ["skill_manager"] = "시스템/유틸",
|
||||
};
|
||||
|
||||
foreach (var tool in tools.All)
|
||||
{
|
||||
var category = toolCategoryMap.TryGetValue(tool.Name, out var mapped) ? mapped : "시스템/유틸";
|
||||
categories[category].Add(tool);
|
||||
}
|
||||
|
||||
foreach (var category in categories)
|
||||
{
|
||||
if (category.Value.Count == 0) continue;
|
||||
ToolCardsPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"{category.Key} ({category.Value.Count})",
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
|
||||
Margin = new Thickness(0, 10, 0, 6),
|
||||
});
|
||||
|
||||
var wrap = new WrapPanel();
|
||||
foreach (var tool in category.Value.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase))
|
||||
wrap.Children.Add(CreateToolCard(tool));
|
||||
ToolCardsPanel.Children.Add(wrap);
|
||||
}
|
||||
}
|
||||
|
||||
private Border CreateToolCard(IAgentTool tool)
|
||||
{
|
||||
var enabled = !_disabledTools.Contains(tool.Name);
|
||||
var card = new Border
|
||||
{
|
||||
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke,
|
||||
BorderBrush = enabled
|
||||
? (TryFindResource("BorderColor") as Brush ?? Brushes.LightGray)
|
||||
: new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(10, 8, 10, 8),
|
||||
Margin = new Thickness(0, 0, 8, 8),
|
||||
Width = 232,
|
||||
};
|
||||
|
||||
var grid = new Grid();
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var info = new StackPanel();
|
||||
info.Children.Add(new TextBlock
|
||||
{
|
||||
Text = tool.Name,
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
|
||||
});
|
||||
var desc = tool.Description.Length > 56 ? tool.Description[..56] + "…" : tool.Description;
|
||||
info.Children.Add(new TextBlock
|
||||
{
|
||||
Text = desc,
|
||||
FontSize = 10.5,
|
||||
Margin = new Thickness(0, 3, 0, 0),
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
grid.Children.Add(info);
|
||||
|
||||
var toggle = new CheckBox
|
||||
{
|
||||
IsChecked = enabled,
|
||||
Style = TryFindResource("ToggleSwitch") as Style,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(8, 0, 0, 0),
|
||||
};
|
||||
toggle.Checked += (_, _) =>
|
||||
{
|
||||
_disabledTools.Remove(tool.Name);
|
||||
card.BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray;
|
||||
};
|
||||
toggle.Unchecked += (_, _) =>
|
||||
{
|
||||
_disabledTools.Add(tool.Name);
|
||||
card.BorderBrush = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26));
|
||||
};
|
||||
Grid.SetColumn(toggle, 1);
|
||||
grid.Children.Add(toggle);
|
||||
card.Child = grid;
|
||||
return card;
|
||||
}
|
||||
|
||||
private void BtnAddHook_Click(object sender, RoutedEventArgs e) => ShowHookEditDialog(null, -1);
|
||||
|
||||
private void ShowHookEditDialog(AgentHookEntry? existing, int index)
|
||||
{
|
||||
var dlg = new Window
|
||||
{
|
||||
Title = existing == null ? "훅 추가" : "훅 편집",
|
||||
Width = 420,
|
||||
SizeToContent = SizeToContent.Height,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||
Owner = this,
|
||||
ResizeMode = ResizeMode.NoResize,
|
||||
WindowStyle = WindowStyle.None,
|
||||
AllowsTransparency = true,
|
||||
Background = Brushes.Transparent,
|
||||
ShowInTaskbar = false,
|
||||
};
|
||||
|
||||
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? Brushes.White;
|
||||
var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
||||
var subBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke;
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
|
||||
var root = new Border
|
||||
{
|
||||
Background = bgBrush,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(18),
|
||||
};
|
||||
var stack = new StackPanel();
|
||||
root.Child = stack;
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = existing == null ? "도구 훅 추가" : "도구 훅 편집",
|
||||
FontSize = 15,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = fgBrush,
|
||||
});
|
||||
|
||||
TextBox AddField(string label, string value)
|
||||
{
|
||||
stack.Children.Add(new TextBlock { Text = label, Foreground = subBrush, FontSize = 12, Margin = new Thickness(0, 10, 0, 4) });
|
||||
var box = new TextBox
|
||||
{
|
||||
Text = value,
|
||||
Padding = new Thickness(10, 7, 10, 7),
|
||||
Background = itemBg,
|
||||
BorderBrush = borderBrush,
|
||||
Foreground = fgBrush,
|
||||
FontSize = 12,
|
||||
};
|
||||
stack.Children.Add(box);
|
||||
return box;
|
||||
}
|
||||
|
||||
var nameBox = AddField("이름", existing?.Name ?? "");
|
||||
var toolBox = AddField("대상 도구 (* = 전체)", existing?.ToolName ?? "*");
|
||||
var pathBox = AddField("스크립트 경로", existing?.ScriptPath ?? "");
|
||||
var argsBox = AddField("인수", existing?.Arguments ?? "");
|
||||
|
||||
var timingPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 10, 0, 0) };
|
||||
var pre = new RadioButton { Content = "Pre", Foreground = fgBrush, IsChecked = (existing?.Timing ?? "post") == "pre", Margin = new Thickness(0, 0, 12, 0) };
|
||||
var post = new RadioButton { Content = "Post", Foreground = fgBrush, IsChecked = (existing?.Timing ?? "post") != "pre" };
|
||||
timingPanel.Children.Add(pre);
|
||||
timingPanel.Children.Add(post);
|
||||
stack.Children.Add(timingPanel);
|
||||
|
||||
var actions = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0) };
|
||||
var cancel = new Button { Content = "취소", Padding = new Thickness(14, 6, 14, 6), Margin = new Thickness(0, 0, 8, 0) };
|
||||
cancel.Click += (_, _) => dlg.Close();
|
||||
var save = new Button { Content = "저장", Padding = new Thickness(14, 6, 14, 6), IsDefault = true };
|
||||
save.Click += (_, _) =>
|
||||
{
|
||||
var entry = new AgentHookEntry
|
||||
{
|
||||
Name = nameBox.Text.Trim(),
|
||||
ToolName = string.IsNullOrWhiteSpace(toolBox.Text) ? "*" : toolBox.Text.Trim(),
|
||||
Timing = pre.IsChecked == true ? "pre" : "post",
|
||||
ScriptPath = pathBox.Text.Trim(),
|
||||
Arguments = argsBox.Text.Trim(),
|
||||
Enabled = existing?.Enabled ?? true,
|
||||
};
|
||||
if (index >= 0 && index < _llm.AgentHooks.Count)
|
||||
_llm.AgentHooks[index] = entry;
|
||||
else
|
||||
_llm.AgentHooks.Add(entry);
|
||||
BuildHookCards();
|
||||
dlg.Close();
|
||||
};
|
||||
actions.Children.Add(cancel);
|
||||
actions.Children.Add(save);
|
||||
stack.Children.Add(actions);
|
||||
|
||||
dlg.Content = root;
|
||||
dlg.ShowDialog();
|
||||
}
|
||||
|
||||
private void BuildHookCards()
|
||||
{
|
||||
if (HookListPanel == null) return;
|
||||
HookListPanel.Children.Clear();
|
||||
|
||||
for (var i = 0; i < _llm.AgentHooks.Count; i++)
|
||||
{
|
||||
var hook = _llm.AgentHooks[i];
|
||||
var index = i;
|
||||
var card = new Border
|
||||
{
|
||||
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke,
|
||||
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(12, 10, 12, 10),
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
};
|
||||
var grid = new Grid();
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var info = new StackPanel();
|
||||
info.Children.Add(new TextBlock { Text = hook.Name, FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black });
|
||||
info.Children.Add(new TextBlock { Text = $"{hook.Timing?.ToUpperInvariant() ?? "POST"} · {hook.ToolName}", FontSize = 10.5, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, Margin = new Thickness(0, 2, 0, 0) });
|
||||
info.Children.Add(new TextBlock { Text = hook.ScriptPath, FontSize = 10.5, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, Margin = new Thickness(0, 2, 0, 0), TextTrimming = TextTrimming.CharacterEllipsis });
|
||||
grid.Children.Add(info);
|
||||
|
||||
var actionRow = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
|
||||
var toggle = new CheckBox { IsChecked = hook.Enabled, Style = TryFindResource("ToggleSwitch") as Style, Margin = new Thickness(0, 0, 8, 0) };
|
||||
toggle.Checked += (_, _) => hook.Enabled = true;
|
||||
toggle.Unchecked += (_, _) => hook.Enabled = false;
|
||||
var edit = new Button { Content = "편집", Style = TryFindResource("OutlineHoverBtn") as Style, Margin = new Thickness(0, 0, 6, 0) };
|
||||
edit.Click += (_, _) => ShowHookEditDialog(_llm.AgentHooks[index], index);
|
||||
var delete = new Button { Content = "삭제", Style = TryFindResource("OutlineHoverBtn") as Style };
|
||||
delete.Click += (_, _) => { _llm.AgentHooks.RemoveAt(index); BuildHookCards(); };
|
||||
actionRow.Children.Add(toggle);
|
||||
actionRow.Children.Add(edit);
|
||||
actionRow.Children.Add(delete);
|
||||
Grid.SetColumn(actionRow, 1);
|
||||
grid.Children.Add(actionRow);
|
||||
|
||||
card.Child = grid;
|
||||
HookListPanel.Children.Add(card);
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnAddMcpServer_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var nameDialog = new InputDialog("MCP 서버 추가", "서버 이름:", placeholder: "예: filesystem");
|
||||
nameDialog.Owner = this;
|
||||
if (nameDialog.ShowDialog() != true || string.IsNullOrWhiteSpace(nameDialog.ResponseText)) return;
|
||||
|
||||
var commandDialog = new InputDialog("MCP 서버 추가", "실행 명령:", placeholder: "예: npx -y @modelcontextprotocol/server-filesystem");
|
||||
commandDialog.Owner = this;
|
||||
if (commandDialog.ShowDialog() != true || string.IsNullOrWhiteSpace(commandDialog.ResponseText)) return;
|
||||
|
||||
_llm.McpServers.Add(new McpServerEntry
|
||||
{
|
||||
Name = nameDialog.ResponseText.Trim(),
|
||||
Command = commandDialog.ResponseText.Trim(),
|
||||
Enabled = true,
|
||||
});
|
||||
BuildMcpServerCards();
|
||||
}
|
||||
|
||||
private void BuildMcpServerCards()
|
||||
{
|
||||
if (McpServerListPanel == null) return;
|
||||
McpServerListPanel.Children.Clear();
|
||||
|
||||
foreach (var entry in _llm.McpServers.ToList())
|
||||
{
|
||||
var card = new Border
|
||||
{
|
||||
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke,
|
||||
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(12, 10, 12, 10),
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
};
|
||||
var grid = new Grid();
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var info = new StackPanel();
|
||||
info.Children.Add(new TextBlock { Text = entry.Name, FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black });
|
||||
info.Children.Add(new TextBlock { Text = entry.Command, FontSize = 10.5, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, Margin = new Thickness(0, 2, 0, 0), TextTrimming = TextTrimming.CharacterEllipsis });
|
||||
grid.Children.Add(info);
|
||||
|
||||
var actionRow = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
|
||||
var toggle = new CheckBox { IsChecked = entry.Enabled, Style = TryFindResource("ToggleSwitch") as Style, Margin = new Thickness(0, 0, 8, 0) };
|
||||
toggle.Checked += (_, _) => entry.Enabled = true;
|
||||
toggle.Unchecked += (_, _) => entry.Enabled = false;
|
||||
var delete = new Button { Content = "삭제", Style = TryFindResource("OutlineHoverBtn") as Style };
|
||||
delete.Click += (_, _) => { _llm.McpServers.Remove(entry); BuildMcpServerCards(); };
|
||||
actionRow.Children.Add(toggle);
|
||||
actionRow.Children.Add(delete);
|
||||
Grid.SetColumn(actionRow, 1);
|
||||
grid.Children.Add(actionRow);
|
||||
|
||||
card.Child = grid;
|
||||
McpServerListPanel.Children.Add(card);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildFallbackModelsPanel()
|
||||
{
|
||||
if (FallbackModelsPanel == null) return;
|
||||
FallbackModelsPanel.Children.Clear();
|
||||
|
||||
var sections = new (string Service, string Label, List<string> Models)[]
|
||||
{
|
||||
("ollama", "Ollama", GetModelCandidates("ollama")),
|
||||
("vllm", "vLLM", GetModelCandidates("vllm")),
|
||||
("gemini", "Gemini", new[] { _llm.GeminiModel }.Where(x => !string.IsNullOrWhiteSpace(x)).ToList()!),
|
||||
("claude", "Claude", new[] { _llm.ClaudeModel }.Where(x => !string.IsNullOrWhiteSpace(x)).ToList()!),
|
||||
};
|
||||
|
||||
foreach (var section in sections)
|
||||
{
|
||||
FallbackModelsPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = section.Label,
|
||||
FontSize = 11.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Margin = new Thickness(0, 8, 0, 4),
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
|
||||
});
|
||||
|
||||
var models = section.Models.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||
if (models.Count == 0)
|
||||
{
|
||||
FallbackModelsPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "등록된 모델 없음",
|
||||
FontSize = 10.5,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
Margin = new Thickness(8, 0, 0, 4),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var model in models)
|
||||
{
|
||||
var key = $"{section.Service}:{model}";
|
||||
var row = new Grid { Margin = new Thickness(8, 2, 0, 2) };
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
row.Children.Add(new TextBlock
|
||||
{
|
||||
Text = model,
|
||||
FontSize = 11.5,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
var toggle = new CheckBox
|
||||
{
|
||||
IsChecked = _llm.FallbackModels.Contains(key, StringComparer.OrdinalIgnoreCase),
|
||||
Style = TryFindResource("ToggleSwitch") as Style,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
toggle.Checked += (_, _) =>
|
||||
{
|
||||
if (!_llm.FallbackModels.Contains(key, StringComparer.OrdinalIgnoreCase))
|
||||
_llm.FallbackModels.Add(key);
|
||||
};
|
||||
toggle.Unchecked += (_, _) => _llm.FallbackModels.RemoveAll(x => x.Equals(key, StringComparison.OrdinalIgnoreCase));
|
||||
Grid.SetColumn(toggle, 1);
|
||||
row.Children.Add(toggle);
|
||||
FallbackModelsPanel.Children.Add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnOpenFullSettings_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (System.Windows.Application.Current is App app)
|
||||
|
||||
691
src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs
Normal file
691
src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs
Normal file
@@ -0,0 +1,691 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private Border CreateCompactEventPill(string summary, Brush primaryText, Brush secondaryText, Brush hintBg, Brush borderBrush, Brush accentBrush)
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Padding = new Thickness(6, 2, 6, 2),
|
||||
Margin = new Thickness(8, 2, 220, 2),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
Child = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = "\uE9CE",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 8,
|
||||
Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = summary,
|
||||
FontSize = 8.75,
|
||||
Foreground = secondaryText,
|
||||
Margin = new Thickness(4, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Border CreateAgentMetaChip(string text, string icon, Brush foreground, Brush background, Brush borderBrush)
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Background = background,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Padding = new Thickness(5, 1.5, 5, 1.5),
|
||||
Margin = new Thickness(0, 0, 4, 0),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 7.5,
|
||||
Foreground = foreground,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
FontSize = 7.75,
|
||||
Foreground = foreground,
|
||||
Margin = new Thickness(3, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Border CreateAgentMetaCallout(string title, string body, Brush titleBrush, Brush background, Brush borderBrush)
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Background = background,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(7, 5, 7, 5),
|
||||
Margin = new Thickness(11, 2, 0, 0),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = title,
|
||||
FontSize = 7.75,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = titleBrush,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = body,
|
||||
FontSize = 8.1,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(0, 1, 0, 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetPermissionKindLabel(string kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
"bash" => "Bash",
|
||||
"powershell" => "PowerShell",
|
||||
"command" => "명령 실행",
|
||||
"web_fetch" => "웹 요청",
|
||||
"mcp" => "MCP",
|
||||
"skill" => "스킬",
|
||||
"question" => "의견 요청",
|
||||
"file_edit" => "파일 수정",
|
||||
"file_write" => "파일 쓰기",
|
||||
"git" => "Git",
|
||||
"document" => "문서 작업",
|
||||
"filesystem" => "파일 접근",
|
||||
_ => "권한 요청",
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetToolResultKindLabel(string kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
"file_edit" => "파일 수정",
|
||||
"file_write" => "파일 쓰기",
|
||||
"filesystem" => "파일 탐색",
|
||||
"file" => "파일 작업",
|
||||
"build_test" => "빌드/테스트",
|
||||
"git" => "Git",
|
||||
"document" => "문서",
|
||||
"skill" => "스킬",
|
||||
"mcp" => "MCP",
|
||||
"question" => "의견 요청",
|
||||
"web" => "웹 요청",
|
||||
"command" => "명령 실행",
|
||||
_ => "도구 결과",
|
||||
};
|
||||
}
|
||||
|
||||
private void AppendAgentEventPresentationMeta(
|
||||
StackPanel stack,
|
||||
AgentEvent evt,
|
||||
PermissionRequestPresentation? permissionPresentation,
|
||||
ToolResultPresentation? toolResultPresentation,
|
||||
Brush secondaryText,
|
||||
Brush hintBg,
|
||||
Brush borderColor)
|
||||
{
|
||||
string? guidance = null;
|
||||
var chipRow = new WrapPanel
|
||||
{
|
||||
Margin = new Thickness(11, 2, 0, 0),
|
||||
Orientation = Orientation.Horizontal,
|
||||
};
|
||||
|
||||
if (permissionPresentation != null)
|
||||
{
|
||||
guidance = permissionPresentation.ActionHint;
|
||||
|
||||
chipRow.Children.Add(CreateAgentMetaChip(
|
||||
GetPermissionKindLabel(permissionPresentation.Kind),
|
||||
"\uE8A5",
|
||||
BrushFromHex("#475569"),
|
||||
BrushFromHex("#F8FAFC"),
|
||||
BrushFromHex("#E2E8F0")));
|
||||
|
||||
if (permissionPresentation.RequiresPreview)
|
||||
chipRow.Children.Add(CreateAgentMetaChip(
|
||||
"미리보기 권장",
|
||||
"\uE8A7",
|
||||
BrushFromHex("#1D4ED8"),
|
||||
BrushFromHex("#EFF6FF"),
|
||||
BrushFromHex("#BFDBFE")));
|
||||
|
||||
if (evt.Type == AgentEventType.PermissionRequest)
|
||||
{
|
||||
if (string.Equals(permissionPresentation.Severity, "high", StringComparison.OrdinalIgnoreCase))
|
||||
chipRow.Children.Add(CreateAgentMetaChip(
|
||||
"주의 필요",
|
||||
"\uE814",
|
||||
BrushFromHex("#B91C1C"),
|
||||
BrushFromHex("#FEF2F2"),
|
||||
BrushFromHex("#FECACA")));
|
||||
else if (string.Equals(permissionPresentation.Severity, "medium", StringComparison.OrdinalIgnoreCase))
|
||||
chipRow.Children.Add(CreateAgentMetaChip(
|
||||
"검토 권장",
|
||||
"\uE946",
|
||||
BrushFromHex("#A16207"),
|
||||
BrushFromHex("#FFFBEB"),
|
||||
BrushFromHex("#FDE68A")));
|
||||
}
|
||||
}
|
||||
|
||||
if (toolResultPresentation != null)
|
||||
{
|
||||
guidance = toolResultPresentation.FollowUpHint;
|
||||
|
||||
chipRow.Children.Add(CreateAgentMetaChip(
|
||||
GetToolResultKindLabel(toolResultPresentation.Kind),
|
||||
"\uE9CE",
|
||||
BrushFromHex("#475569"),
|
||||
BrushFromHex("#F8FAFC"),
|
||||
BrushFromHex("#E2E8F0")));
|
||||
|
||||
if (toolResultPresentation.NeedsAttention)
|
||||
chipRow.Children.Add(CreateAgentMetaChip(
|
||||
"확인 필요",
|
||||
"\uE814",
|
||||
BrushFromHex("#B91C1C"),
|
||||
BrushFromHex("#FEF2F2"),
|
||||
BrushFromHex("#FECACA")));
|
||||
|
||||
if (string.Equals(toolResultPresentation.StatusKind, "approval_required", StringComparison.OrdinalIgnoreCase))
|
||||
chipRow.Children.Add(CreateAgentMetaChip(
|
||||
"승인 후 계속",
|
||||
"\uE8D7",
|
||||
BrushFromHex("#C2410C"),
|
||||
BrushFromHex("#FFF7ED"),
|
||||
BrushFromHex("#FED7AA")));
|
||||
else if (string.Equals(toolResultPresentation.StatusKind, "partial", StringComparison.OrdinalIgnoreCase))
|
||||
chipRow.Children.Add(CreateAgentMetaChip(
|
||||
"후속 점검",
|
||||
"\uE7BA",
|
||||
BrushFromHex("#A16207"),
|
||||
BrushFromHex("#FFFBEB"),
|
||||
BrushFromHex("#FDE68A")));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(guidance))
|
||||
{
|
||||
var clipped = guidance.Length > 92 ? guidance[..92] + "..." : guidance;
|
||||
if (permissionPresentation != null)
|
||||
{
|
||||
var titleBrush = string.Equals(permissionPresentation.Severity, "high", StringComparison.OrdinalIgnoreCase)
|
||||
? BrushFromHex("#B91C1C")
|
||||
: string.Equals(permissionPresentation.Severity, "medium", StringComparison.OrdinalIgnoreCase)
|
||||
? BrushFromHex("#A16207")
|
||||
: BrushFromHex("#2563EB");
|
||||
var background = string.Equals(permissionPresentation.Severity, "high", StringComparison.OrdinalIgnoreCase)
|
||||
? BrushFromHex("#FFF7F7")
|
||||
: string.Equals(permissionPresentation.Severity, "medium", StringComparison.OrdinalIgnoreCase)
|
||||
? BrushFromHex("#FFFBEB")
|
||||
: BrushFromHex("#EFF6FF");
|
||||
var border = string.Equals(permissionPresentation.Severity, "high", StringComparison.OrdinalIgnoreCase)
|
||||
? BrushFromHex("#FECACA")
|
||||
: string.Equals(permissionPresentation.Severity, "medium", StringComparison.OrdinalIgnoreCase)
|
||||
? BrushFromHex("#FDE68A")
|
||||
: BrushFromHex("#BFDBFE");
|
||||
stack.Children.Add(CreateAgentMetaCallout("확인 포인트", clipped, titleBrush, background, border));
|
||||
}
|
||||
else if (toolResultPresentation != null)
|
||||
{
|
||||
var titleBrush = string.Equals(toolResultPresentation.StatusKind, "error", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(toolResultPresentation.StatusKind, "reject", StringComparison.OrdinalIgnoreCase)
|
||||
? BrushFromHex("#B91C1C")
|
||||
: string.Equals(toolResultPresentation.StatusKind, "approval_required", StringComparison.OrdinalIgnoreCase)
|
||||
? BrushFromHex("#C2410C")
|
||||
: BrushFromHex("#A16207");
|
||||
var background = string.Equals(toolResultPresentation.StatusKind, "error", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(toolResultPresentation.StatusKind, "reject", StringComparison.OrdinalIgnoreCase)
|
||||
? BrushFromHex("#FFF7F7")
|
||||
: string.Equals(toolResultPresentation.StatusKind, "approval_required", StringComparison.OrdinalIgnoreCase)
|
||||
? BrushFromHex("#FFF7ED")
|
||||
: BrushFromHex("#FFFBEB");
|
||||
var border = string.Equals(toolResultPresentation.StatusKind, "error", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(toolResultPresentation.StatusKind, "reject", StringComparison.OrdinalIgnoreCase)
|
||||
? BrushFromHex("#FECACA")
|
||||
: string.Equals(toolResultPresentation.StatusKind, "approval_required", StringComparison.OrdinalIgnoreCase)
|
||||
? BrushFromHex("#FED7AA")
|
||||
: BrushFromHex("#FDE68A");
|
||||
stack.Children.Add(CreateAgentMetaCallout("다음 권장 작업", clipped, titleBrush, background, border));
|
||||
}
|
||||
}
|
||||
|
||||
if (chipRow.Children.Count > 0)
|
||||
stack.Children.Add(chipRow);
|
||||
}
|
||||
|
||||
private void AddAgentEventBanner(AgentEvent evt)
|
||||
{
|
||||
var logLevel = _settings.Settings.Llm.AgentLogLevel;
|
||||
|
||||
if (evt.Type == AgentEventType.Planning && evt.Steps is { Count: > 0 })
|
||||
{
|
||||
var compactPrimaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var compactSecondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var compactHintBg = TryFindResource("HintBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
var compactBorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var compactAccentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var summary = !string.IsNullOrWhiteSpace(evt.Summary)
|
||||
? evt.Summary!
|
||||
: $"계획 {evt.Steps.Count}단계";
|
||||
var pill = CreateCompactEventPill(summary, compactPrimaryText, compactSecondaryText, compactHintBg, compactBorderBrush, compactAccentBrush);
|
||||
pill.Opacity = 0;
|
||||
pill.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(160)));
|
||||
MessagePanel.Children.Add(pill);
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.Type == AgentEventType.StepStart && evt.StepTotal > 0)
|
||||
{
|
||||
UpdateProgressBar(evt);
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.Type == AgentEventType.Thinking &&
|
||||
ContainsAny(evt.Summary ?? "", "컨텍스트 압축", "microcompact", "session memory", "compact"))
|
||||
{
|
||||
var compactPrimaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var compactSecondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var compactHintBg = TryFindResource("HintBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
var compactBorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var compactAccentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var pill = CreateCompactEventPill(string.IsNullOrWhiteSpace(evt.Summary) ? "컨텍스트 압축" : evt.Summary, compactPrimaryText, compactSecondaryText, compactHintBg, compactBorderBrush, compactAccentBrush);
|
||||
pill.Opacity = 0;
|
||||
pill.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(160)));
|
||||
MessagePanel.Children.Add(pill);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.Equals(logLevel, "debug", StringComparison.OrdinalIgnoreCase)
|
||||
&& evt.Type is AgentEventType.Paused or AgentEventType.Resumed)
|
||||
return;
|
||||
|
||||
if (!string.Equals(logLevel, "debug", StringComparison.OrdinalIgnoreCase)
|
||||
&& evt.Type == AgentEventType.ToolCall)
|
||||
return;
|
||||
|
||||
var isTotalStats = evt.Type == AgentEventType.StepDone && evt.ToolName == "total_stats";
|
||||
var transcriptBadgeLabel = GetTranscriptBadgeLabel(evt);
|
||||
var permissionPresentation = evt.Type switch
|
||||
{
|
||||
AgentEventType.PermissionRequest => PermissionRequestPresentationCatalog.Resolve(evt.ToolName, pending: true),
|
||||
AgentEventType.PermissionGranted => PermissionRequestPresentationCatalog.Resolve(evt.ToolName, pending: false),
|
||||
_ => null
|
||||
};
|
||||
var toolResultPresentation = evt.Type == AgentEventType.ToolResult
|
||||
? ToolResultPresentationCatalog.Resolve(evt, transcriptBadgeLabel)
|
||||
: null;
|
||||
|
||||
var (icon, label, bgHex, fgHex) = isTotalStats
|
||||
? ("\uE9D2", "전체 통계", "#F3EEFF", "#7C3AED")
|
||||
: evt.Type switch
|
||||
{
|
||||
AgentEventType.Thinking => ("\uE8BD", "분석 중", "#F0F0FF", "#6B7BC4"),
|
||||
AgentEventType.PermissionRequest => (permissionPresentation!.Icon, permissionPresentation.Label, permissionPresentation.BackgroundHex, permissionPresentation.ForegroundHex),
|
||||
AgentEventType.PermissionGranted => (permissionPresentation!.Icon, permissionPresentation.Label, permissionPresentation.BackgroundHex, permissionPresentation.ForegroundHex),
|
||||
AgentEventType.PermissionDenied => ("\uE783", "권한 거부", "#FEF2F2", "#DC2626"),
|
||||
AgentEventType.Decision => GetDecisionBadgeMeta(evt.Summary),
|
||||
AgentEventType.ToolCall => ("\uE8A7", transcriptBadgeLabel, "#EEF6FF", "#3B82F6"),
|
||||
AgentEventType.ToolResult => (toolResultPresentation!.Icon, toolResultPresentation.Label, toolResultPresentation.BackgroundHex, toolResultPresentation.ForegroundHex),
|
||||
AgentEventType.SkillCall => ("\uE8A5", transcriptBadgeLabel, "#FFF7ED", "#EA580C"),
|
||||
AgentEventType.Error => ("\uE783", "오류", "#FEF2F2", "#DC2626"),
|
||||
AgentEventType.Complete => ("\uE930", "완료", "#F0FFF4", "#15803D"),
|
||||
AgentEventType.StepDone => ("\uE73E", "단계 완료", "#EEF9EE", "#16A34A"),
|
||||
AgentEventType.Paused => ("\uE769", "일시정지", "#FFFBEB", "#D97706"),
|
||||
AgentEventType.Resumed => ("\uE768", "재개", "#ECFDF5", "#059669"),
|
||||
_ => ("\uE946", "에이전트", "#F5F5F5", "#6B7280"),
|
||||
};
|
||||
var itemDisplayName = evt.Type == AgentEventType.SkillCall
|
||||
? GetAgentItemDisplayName(evt.ToolName, slashPrefix: true)
|
||||
: GetAgentItemDisplayName(evt.ToolName);
|
||||
var eventSummaryText = BuildAgentEventSummaryText(evt, itemDisplayName);
|
||||
if (string.IsNullOrWhiteSpace(eventSummaryText))
|
||||
{
|
||||
eventSummaryText = evt.Type switch
|
||||
{
|
||||
AgentEventType.PermissionRequest or AgentEventType.PermissionGranted => permissionPresentation?.Description ?? "",
|
||||
AgentEventType.ToolResult => toolResultPresentation?.Description ?? "",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray;
|
||||
var hintBg = TryFindResource("HintBackground") as Brush ?? BrushFromHex("#F8FAFC");
|
||||
var borderColor = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E2E8F0");
|
||||
var accentBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(fgHex));
|
||||
|
||||
var banner = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
CornerRadius = new CornerRadius(0),
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(12, 0, 12, 1),
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(evt.RunId))
|
||||
_runBannerAnchors[evt.RunId] = banner;
|
||||
|
||||
var sp = new StackPanel();
|
||||
|
||||
var headerGrid = new Grid();
|
||||
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var headerLeft = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
headerLeft.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 8.25,
|
||||
Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 3, 0),
|
||||
});
|
||||
headerLeft.Children.Add(new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 8.25,
|
||||
FontWeight = FontWeights.Medium,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
if (IsTranscriptToolLikeEvent(evt) && !string.IsNullOrWhiteSpace(evt.ToolName))
|
||||
{
|
||||
headerLeft.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $" · {itemDisplayName}",
|
||||
FontSize = 8.25,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
}
|
||||
Grid.SetColumn(headerLeft, 0);
|
||||
|
||||
var headerRight = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
if (logLevel != "simple" && evt.ElapsedMs > 0)
|
||||
{
|
||||
headerRight.Children.Add(new TextBlock
|
||||
{
|
||||
Text = evt.ElapsedMs < 1000 ? $"{evt.ElapsedMs}ms" : $"{evt.ElapsedMs / 1000.0:F1}s",
|
||||
FontSize = 7.5,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(3, 0, 0, 0),
|
||||
});
|
||||
}
|
||||
if (logLevel != "simple" && (evt.InputTokens > 0 || evt.OutputTokens > 0))
|
||||
{
|
||||
var tokenText = evt.InputTokens > 0 && evt.OutputTokens > 0
|
||||
? $"{evt.InputTokens}→{evt.OutputTokens}t"
|
||||
: evt.InputTokens > 0 ? $"↑{evt.InputTokens}t" : $"↓{evt.OutputTokens}t";
|
||||
headerRight.Children.Add(new Border
|
||||
{
|
||||
Background = hintBg,
|
||||
BorderBrush = borderColor,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Padding = new Thickness(3.5, 1, 3.5, 1),
|
||||
Margin = new Thickness(3, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = tokenText,
|
||||
FontSize = 7.25,
|
||||
Foreground = secondaryText,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
},
|
||||
});
|
||||
}
|
||||
Grid.SetColumn(headerRight, 1);
|
||||
|
||||
headerGrid.Children.Add(headerLeft);
|
||||
headerGrid.Children.Add(headerRight);
|
||||
sp.Children.Add(headerGrid);
|
||||
|
||||
if (logLevel == "simple")
|
||||
{
|
||||
if (!string.IsNullOrEmpty(eventSummaryText))
|
||||
{
|
||||
var shortSummary = eventSummaryText.Length > 100
|
||||
? eventSummaryText[..100] + "…"
|
||||
: eventSummaryText;
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = shortSummary,
|
||||
FontSize = 8.4,
|
||||
Foreground = secondaryText,
|
||||
TextWrapping = TextWrapping.NoWrap,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
Margin = new Thickness(11, 1, 0, 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(eventSummaryText))
|
||||
{
|
||||
var summaryText = eventSummaryText.Length > 92 ? eventSummaryText[..92] + "…" : eventSummaryText;
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = summaryText,
|
||||
FontSize = 8.4,
|
||||
Foreground = secondaryText,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(11, 1, 0, 0),
|
||||
});
|
||||
}
|
||||
|
||||
if (permissionPresentation != null || toolResultPresentation != null)
|
||||
{
|
||||
AppendAgentEventPresentationMeta(
|
||||
sp,
|
||||
evt,
|
||||
permissionPresentation,
|
||||
toolResultPresentation,
|
||||
secondaryText,
|
||||
hintBg,
|
||||
borderColor);
|
||||
}
|
||||
|
||||
var reviewChipRow = BuildReviewSignalChipRow(
|
||||
kind: null,
|
||||
toolName: evt.ToolName,
|
||||
title: label,
|
||||
summary: evt.Summary);
|
||||
if (reviewChipRow != null && string.Equals(logLevel, "debug", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
reviewChipRow.Margin = new Thickness(12, 2, 0, 0);
|
||||
sp.Children.Add(reviewChipRow);
|
||||
}
|
||||
|
||||
if (logLevel == "debug" && !string.IsNullOrEmpty(evt.ToolInput))
|
||||
{
|
||||
sp.Children.Add(new Border
|
||||
{
|
||||
Background = hintBg,
|
||||
BorderBrush = borderColor,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(5, 3, 5, 3),
|
||||
Margin = new Thickness(12, 2, 0, 0),
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = evt.ToolInput.Length > 240 ? evt.ToolInput[..240] + "…" : evt.ToolInput,
|
||||
FontSize = 8.5,
|
||||
Foreground = secondaryText,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(evt.FilePath))
|
||||
{
|
||||
var fileName = System.IO.Path.GetFileName(evt.FilePath);
|
||||
var dirName = System.IO.Path.GetDirectoryName(evt.FilePath) ?? "";
|
||||
|
||||
if (!string.Equals(logLevel, "debug", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var compactPathRow = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Margin = new Thickness(12, 1.5, 0, 0),
|
||||
ToolTip = evt.FilePath,
|
||||
};
|
||||
compactPathRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE8B7",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 8,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 3, 0),
|
||||
});
|
||||
compactPathRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = string.IsNullOrWhiteSpace(fileName) ? evt.FilePath : fileName,
|
||||
FontSize = 8.5,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
});
|
||||
sp.Children.Add(compactPathRow);
|
||||
}
|
||||
else
|
||||
{
|
||||
var pathBorder = new Border
|
||||
{
|
||||
Background = hintBg,
|
||||
BorderBrush = borderColor,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(7, 4, 7, 4),
|
||||
Margin = new Thickness(12, 2, 0, 0),
|
||||
};
|
||||
|
||||
var pathGrid = new Grid();
|
||||
pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var left = new StackPanel { Orientation = Orientation.Vertical };
|
||||
|
||||
var topRow = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
topRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE8B7",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 10,
|
||||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CA3AF")),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 4, 0),
|
||||
});
|
||||
topRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = string.IsNullOrWhiteSpace(fileName) ? evt.FilePath : fileName,
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#374151")),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
});
|
||||
left.Children.Add(topRow);
|
||||
if (!string.IsNullOrWhiteSpace(dirName))
|
||||
{
|
||||
left.Children.Add(new TextBlock
|
||||
{
|
||||
Text = dirName,
|
||||
FontSize = 9,
|
||||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CA3AF")),
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
});
|
||||
}
|
||||
Grid.SetColumn(left, 0);
|
||||
pathGrid.Children.Add(left);
|
||||
|
||||
var quickActions = BuildFileQuickActions(evt.FilePath);
|
||||
Grid.SetColumn(quickActions, 1);
|
||||
pathGrid.Children.Add(quickActions);
|
||||
|
||||
pathBorder.Child = pathGrid;
|
||||
sp.Children.Add(pathBorder);
|
||||
}
|
||||
}
|
||||
|
||||
banner.Child = sp;
|
||||
|
||||
if (isTotalStats)
|
||||
{
|
||||
banner.Cursor = Cursors.Hand;
|
||||
banner.ToolTip = "클릭하여 병목 분석 보기";
|
||||
banner.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
OpenWorkflowAnalyzerIfEnabled();
|
||||
_analyzerWindow?.SwitchToBottleneckTab();
|
||||
_analyzerWindow?.Activate();
|
||||
};
|
||||
}
|
||||
|
||||
banner.Opacity = 0;
|
||||
banner.BeginAnimation(UIElement.OpacityProperty,
|
||||
new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200)));
|
||||
|
||||
MessagePanel.Children.Add(banner);
|
||||
}
|
||||
|
||||
private static (string icon, string label, string bgHex, string fgHex) GetDecisionBadgeMeta(string? summary)
|
||||
{
|
||||
if (IsDecisionApproved(summary))
|
||||
return ("\uE73E", "계획 승인", "#ECFDF5", "#059669");
|
||||
if (IsDecisionRejected(summary))
|
||||
return ("\uE783", "계획 반려", "#FEF2F2", "#DC2626");
|
||||
return ("\uE70F", "계획 확인", "#FFF7ED", "#C2410C");
|
||||
}
|
||||
}
|
||||
624
src/AxCopilot/Views/ChatWindow.ComposerQueuePresentation.cs
Normal file
624
src/AxCopilot/Views/ChatWindow.ComposerQueuePresentation.cs
Normal file
@@ -0,0 +1,624 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private void UpdateInputBoxHeight()
|
||||
{
|
||||
if (InputBox == null)
|
||||
return;
|
||||
|
||||
var text = InputBox.Text ?? string.Empty;
|
||||
var explicitLineCount = 1 + text.Count(ch => ch == '\n');
|
||||
var displayMode = (_settings.Settings.Llm.AgentUiExpressionLevel ?? "balanced").Trim().ToLowerInvariant();
|
||||
if (displayMode is not ("rich" or "balanced" or "simple"))
|
||||
displayMode = "balanced";
|
||||
|
||||
var maxLines = displayMode switch
|
||||
{
|
||||
"rich" => 6,
|
||||
"simple" => 4,
|
||||
_ => 5,
|
||||
};
|
||||
const double baseHeight = 42;
|
||||
const double lineStep = 22;
|
||||
var visibleLines = Math.Clamp(explicitLineCount, 1, maxLines);
|
||||
var targetHeight = baseHeight + ((visibleLines - 1) * lineStep);
|
||||
|
||||
InputBox.MinLines = 1;
|
||||
InputBox.MaxLines = maxLines;
|
||||
InputBox.Height = targetHeight;
|
||||
InputBox.VerticalScrollBarVisibility = explicitLineCount > maxLines
|
||||
? ScrollBarVisibility.Auto
|
||||
: ScrollBarVisibility.Disabled;
|
||||
}
|
||||
|
||||
private string BuildComposerDraftText()
|
||||
{
|
||||
var rawText = InputBox?.Text?.Trim() ?? "";
|
||||
return _slashPalette.ActiveCommand != null
|
||||
? (_slashPalette.ActiveCommand + " " + rawText).Trim()
|
||||
: rawText;
|
||||
}
|
||||
|
||||
private static string InferDraftKind(string text, string? explicitKind = null)
|
||||
{
|
||||
var trimmed = text?.Trim() ?? "";
|
||||
var requestedKind = explicitKind?.Trim().ToLowerInvariant();
|
||||
|
||||
if (requestedKind is "followup" or "steering")
|
||||
return requestedKind;
|
||||
|
||||
if (trimmed.StartsWith("/", StringComparison.OrdinalIgnoreCase))
|
||||
return "command";
|
||||
|
||||
if (requestedKind is "direct" or "message")
|
||||
return requestedKind;
|
||||
|
||||
if (trimmed.StartsWith("steer:", StringComparison.OrdinalIgnoreCase) ||
|
||||
trimmed.StartsWith("@steer ", StringComparison.OrdinalIgnoreCase) ||
|
||||
trimmed.StartsWith("조정:", StringComparison.OrdinalIgnoreCase))
|
||||
return "steering";
|
||||
|
||||
return "message";
|
||||
}
|
||||
|
||||
private void QueueComposerDraft(string priority = "next", string? explicitKind = null, bool startImmediatelyWhenIdle = true)
|
||||
{
|
||||
if (InputBox == null)
|
||||
return;
|
||||
|
||||
var text = BuildComposerDraftText();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return;
|
||||
|
||||
if (_isStreaming && string.Equals(priority, "now", StringComparison.OrdinalIgnoreCase))
|
||||
priority = "next";
|
||||
|
||||
HideSlashChip(restoreText: false);
|
||||
ClearPromptCardPlaceholder();
|
||||
|
||||
var queuedItem = EnqueueDraftRequest(text, priority, explicitKind);
|
||||
|
||||
InputBox.Clear();
|
||||
InputBox.Focus();
|
||||
UpdateInputBoxHeight();
|
||||
RefreshDraftQueueUi();
|
||||
|
||||
if (queuedItem == null)
|
||||
return;
|
||||
|
||||
if (!_isStreaming && startImmediatelyWhenIdle)
|
||||
{
|
||||
StartNextQueuedDraftIfAny(queuedItem.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
var toast = queuedItem.Kind switch
|
||||
{
|
||||
"command" => "명령이 대기열에 추가되었습니다.",
|
||||
"direct" => "직접 실행 요청이 대기열에 추가되었습니다.",
|
||||
"steering" => "조정 요청이 대기열에 추가되었습니다.",
|
||||
"followup" => "후속 작업이 대기열에 추가되었습니다.",
|
||||
_ => "메시지가 대기열에 추가되었습니다.",
|
||||
};
|
||||
ShowToast(toast);
|
||||
}
|
||||
|
||||
private void RefreshDraftQueueUi()
|
||||
{
|
||||
if (DraftPreviewCard == null || DraftPreviewText == null || DraftQueuePanel == null || BtnDraftEnqueue == null)
|
||||
return;
|
||||
|
||||
lock (_convLock)
|
||||
{
|
||||
var session = ChatSession;
|
||||
if (session != null)
|
||||
_draftQueueProcessor.PromoteReadyBlockedItems(session, _activeTab, _storage);
|
||||
}
|
||||
|
||||
var items = _appState.GetDraftQueueItems(_activeTab);
|
||||
|
||||
DraftPreviewCard.Visibility = Visibility.Collapsed;
|
||||
BtnDraftEnqueue.IsEnabled = false;
|
||||
DraftPreviewText.Text = string.Empty;
|
||||
|
||||
RebuildDraftQueuePanel(items);
|
||||
}
|
||||
|
||||
private bool IsDraftQueueExpanded()
|
||||
=> _expandedDraftQueueTabs.Contains(_activeTab);
|
||||
|
||||
private void ToggleDraftQueueExpanded()
|
||||
{
|
||||
if (!_expandedDraftQueueTabs.Add(_activeTab))
|
||||
_expandedDraftQueueTabs.Remove(_activeTab);
|
||||
|
||||
RefreshDraftQueueUi();
|
||||
}
|
||||
|
||||
private void RebuildDraftQueuePanel(IReadOnlyList<DraftQueueItem> items)
|
||||
{
|
||||
if (DraftQueuePanel == null)
|
||||
return;
|
||||
|
||||
DraftQueuePanel.Children.Clear();
|
||||
|
||||
var visibleItems = items
|
||||
.OrderBy(GetDraftStateRank)
|
||||
.ThenBy(GetDraftPriorityRank)
|
||||
.ThenBy(x => x.CreatedAt)
|
||||
.ToList();
|
||||
|
||||
if (visibleItems.Count == 0)
|
||||
{
|
||||
DraftQueuePanel.Visibility = Visibility.Collapsed;
|
||||
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())
|
||||
{
|
||||
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<DraftQueueItem> 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,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(14),
|
||||
Padding = new Thickness(12, 10, 12, 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 left = new StackPanel();
|
||||
left.Children.Add(new TextBlock
|
||||
{
|
||||
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);
|
||||
|
||||
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<string>();
|
||||
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<DraftQueueItem> 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
|
||||
{
|
||||
Text = $"추가 항목 {totalCount - items.Count}개",
|
||||
Margin = new Thickness(8, -2, 0, 8),
|
||||
FontSize = 10.5,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#7A7F87"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private UIElement CreateDraftQueueSummaryStrip(AppStateService.DraftQueueSummaryState summary, bool isExpanded)
|
||||
{
|
||||
var root = new Grid
|
||||
{
|
||||
Margin = new Thickness(0, 0, 0, 8),
|
||||
};
|
||||
root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
root.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var wrap = new WrapPanel();
|
||||
|
||||
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"));
|
||||
|
||||
if (wrap.Children.Count == 0)
|
||||
wrap.Children.Add(CreateQueueSummaryPill("대기열", "0", "#F8FAFC", "#E2E8F0", "#475569"));
|
||||
|
||||
Grid.SetColumn(wrap, 0);
|
||||
root.Children.Add(wrap);
|
||||
|
||||
var toggle = CreateDraftQueueActionButton(isExpanded ? "간단히" : "상세 보기", ToggleDraftQueueExpanded);
|
||||
toggle.Margin = new Thickness(10, 0, 0, 0);
|
||||
Grid.SetColumn(toggle, 1);
|
||||
root.Children.Add(toggle);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private Border CreateQueueSummaryPill(string label, string value, string bgHex, string borderHex, string fgHex)
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Background = BrushFromHex(bgHex),
|
||||
BorderBrush = BrushFromHex(borderHex),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Padding = new Thickness(8, 3, 8, 3),
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 10,
|
||||
Foreground = BrushFromHex(fgHex),
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = $" {value}",
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = BrushFromHex(fgHex),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private TextBlock CreateDraftQueueSectionLabel(string text)
|
||||
{
|
||||
return new TextBlock
|
||||
{
|
||||
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
|
||||
{
|
||||
Text = text,
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = foreground,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Button CreateDraftQueueActionButton(string label, Action onClick, Brush? background = null)
|
||||
{
|
||||
var btn = new Button
|
||||
{
|
||||
Content = label,
|
||||
Margin = new Thickness(6, 0, 0, 0),
|
||||
Padding = new Thickness(10, 5, 10, 5),
|
||||
MinWidth = 48,
|
||||
FontSize = 11,
|
||||
Background = background ?? BrushFromHex("#EEF2FF"),
|
||||
BorderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#D4D4D8"),
|
||||
BorderThickness = new Thickness(1),
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#111827"),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
btn.Click += (_, _) => onClick();
|
||||
return btn;
|
||||
}
|
||||
|
||||
private static int GetDraftStateRank(DraftQueueItem item)
|
||||
=> string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase) ? 0
|
||||
: IsDraftBlocked(item) ? 1
|
||||
: string.Equals(item.State, "queued", StringComparison.OrdinalIgnoreCase) ? 2
|
||||
: string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase) ? 3
|
||||
: 4;
|
||||
|
||||
private static int GetDraftPriorityRank(DraftQueueItem item)
|
||||
=> item.Priority?.ToLowerInvariant() switch
|
||||
{
|
||||
"now" => 0,
|
||||
"next" => 1,
|
||||
_ => 2,
|
||||
};
|
||||
|
||||
private static string GetDraftPriorityLabel(string? priority)
|
||||
=> priority?.ToLowerInvariant() switch
|
||||
{
|
||||
"now" => "지금",
|
||||
"later" => "나중",
|
||||
_ => "다음",
|
||||
};
|
||||
|
||||
private static string GetDraftKindLabel(DraftQueueItem item)
|
||||
=> item.Kind?.ToLowerInvariant() switch
|
||||
{
|
||||
"followup" => "후속 작업",
|
||||
"steering" => "조정",
|
||||
"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" => "실패",
|
||||
"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)
|
||||
=> IsDraftBlocked(item)
|
||||
? (BrushFromHex("#FFF7ED"), BrushFromHex("#FDBA74"), BrushFromHex("#C2410C"))
|
||||
: item.State?.ToLowerInvariant() switch
|
||||
{
|
||||
"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")),
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
144
src/AxCopilot/Views/ChatWindow.ContextUsagePresentation.cs
Normal file
144
src/AxCopilot/Views/ChatWindow.ContextUsagePresentation.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private void RefreshContextUsageVisual()
|
||||
{
|
||||
if (TokenUsageCard == null || TokenUsageArc == null || TokenUsagePercentText == null
|
||||
|| TokenUsageSummaryText == null || TokenUsageHintText == null
|
||||
|| TokenUsageThresholdMarker == null || CompactNowLabel == null)
|
||||
return;
|
||||
|
||||
var showContextUsage = _activeTab is "Cowork" or "Code";
|
||||
TokenUsageCard.Visibility = showContextUsage ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (!showContextUsage)
|
||||
{
|
||||
if (TokenUsagePopup != null)
|
||||
TokenUsagePopup.IsOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var llm = _settings.Settings.Llm;
|
||||
var maxContextTokens = Math.Clamp(llm.MaxContextTokens, 1024, 1_000_000);
|
||||
var triggerPercent = Math.Clamp(llm.ContextCompactTriggerPercent, 10, 95);
|
||||
var triggerRatio = triggerPercent / 100.0;
|
||||
|
||||
int messageTokens;
|
||||
lock (_convLock)
|
||||
messageTokens = _currentConversation?.Messages?.Count > 0
|
||||
? Services.TokenEstimator.EstimateMessages(_currentConversation.Messages)
|
||||
: 0;
|
||||
|
||||
var draftText = InputBox?.Text ?? "";
|
||||
var draftTokens = string.IsNullOrWhiteSpace(draftText) ? 0 : Services.TokenEstimator.Estimate(draftText) + 4;
|
||||
var currentTokens = Math.Max(0, messageTokens + draftTokens);
|
||||
var usageRatio = Services.TokenEstimator.GetContextUsage(currentTokens, maxContextTokens);
|
||||
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue;
|
||||
Brush progressBrush = accentBrush;
|
||||
string summary;
|
||||
string compactLabel;
|
||||
|
||||
if (usageRatio >= 1.0)
|
||||
{
|
||||
progressBrush = Brushes.IndianRed;
|
||||
summary = "컨텍스트 한도 초과";
|
||||
compactLabel = "지금 압축";
|
||||
}
|
||||
else if (usageRatio >= triggerRatio)
|
||||
{
|
||||
progressBrush = Brushes.DarkOrange;
|
||||
summary = llm.EnableProactiveContextCompact ? "곧 자동 압축" : "압축 임계 도달";
|
||||
compactLabel = "압축 권장";
|
||||
}
|
||||
else if (usageRatio >= triggerRatio * 0.7)
|
||||
{
|
||||
progressBrush = Brushes.Goldenrod;
|
||||
summary = "컨텍스트 사용 증가";
|
||||
compactLabel = "미리 압축";
|
||||
}
|
||||
else
|
||||
{
|
||||
summary = "컨텍스트 여유";
|
||||
compactLabel = "압축";
|
||||
}
|
||||
|
||||
TokenUsageArc.Stroke = progressBrush;
|
||||
TokenUsageThresholdMarker.Fill = progressBrush;
|
||||
var percentText = $"{Math.Round(usageRatio * 100):0}%";
|
||||
TokenUsagePercentText.Text = percentText;
|
||||
TokenUsageSummaryText.Text = $"컨텍스트 {percentText}";
|
||||
TokenUsageHintText.Text = $"{Services.TokenEstimator.Format(currentTokens)} / {Services.TokenEstimator.Format(maxContextTokens)}";
|
||||
CompactNowLabel.Text = compactLabel;
|
||||
|
||||
if (TokenUsagePopupTitle != null)
|
||||
TokenUsagePopupTitle.Text = $"컨텍스트 창 {percentText}";
|
||||
if (TokenUsagePopupUsage != null)
|
||||
TokenUsagePopupUsage.Text = $"{Services.TokenEstimator.Format(currentTokens)}/{Services.TokenEstimator.Format(maxContextTokens)}";
|
||||
if (TokenUsagePopupDetail != null)
|
||||
TokenUsagePopupDetail.Text = _pendingPostCompaction ? "compact 후 첫 응답 대기 중" : $"자동 압축 시작 {triggerPercent}%";
|
||||
if (TokenUsagePopupCompact != null)
|
||||
TokenUsagePopupCompact.Text = "AX Agent가 컨텍스트를 자동으로 관리합니다";
|
||||
|
||||
TokenUsageCard.ToolTip = null;
|
||||
|
||||
UpdateCircularUsageArc(TokenUsageArc, usageRatio, 14, 14, 11);
|
||||
PositionThresholdMarker(TokenUsageThresholdMarker, triggerRatio, 14, 14, 11, 2.5);
|
||||
}
|
||||
|
||||
private void TokenUsageCard_MouseEnter(object sender, MouseEventArgs e)
|
||||
{
|
||||
_tokenUsagePopupCloseTimer.Stop();
|
||||
if (TokenUsagePopup != null && TokenUsageCard?.Visibility == Visibility.Visible)
|
||||
TokenUsagePopup.IsOpen = true;
|
||||
}
|
||||
|
||||
private void TokenUsageCard_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
_tokenUsagePopupCloseTimer.Stop();
|
||||
_tokenUsagePopupCloseTimer.Start();
|
||||
}
|
||||
|
||||
private void TokenUsagePopup_MouseEnter(object sender, MouseEventArgs e)
|
||||
{
|
||||
_tokenUsagePopupCloseTimer.Stop();
|
||||
}
|
||||
|
||||
private void TokenUsagePopup_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
_tokenUsagePopupCloseTimer.Stop();
|
||||
_tokenUsagePopupCloseTimer.Start();
|
||||
}
|
||||
|
||||
private void CloseTokenUsagePopupIfIdle()
|
||||
{
|
||||
if (TokenUsagePopup == null)
|
||||
return;
|
||||
|
||||
var cardHovered = IsMouseInsideElement(TokenUsageCard);
|
||||
var popupHovered = TokenUsagePopup.Child is FrameworkElement popupChild && IsMouseInsideElement(popupChild);
|
||||
if (!cardHovered && !popupHovered)
|
||||
TokenUsagePopup.IsOpen = false;
|
||||
}
|
||||
|
||||
private static bool IsMouseInsideElement(FrameworkElement? element)
|
||||
{
|
||||
if (element == null || !element.IsVisible || element.ActualWidth <= 0 || element.ActualHeight <= 0)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var mouse = System.Windows.Forms.Control.MousePosition;
|
||||
var point = element.PointFromScreen(new Point(mouse.X, mouse.Y));
|
||||
return point.X >= 0 && point.Y >= 0 && point.X <= element.ActualWidth && point.Y <= element.ActualHeight;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return element.IsMouseOver;
|
||||
}
|
||||
}
|
||||
}
|
||||
116
src/AxCopilot/Views/ChatWindow.ConversationFilterPresentation.cs
Normal file
116
src/AxCopilot/Views/ChatWindow.ConversationFilterPresentation.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private void BtnFailedOnlyFilter_Click(object sender, RoutedEventArgs e) { }
|
||||
|
||||
private void BtnRunningOnlyFilter_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_runningOnlyFilter = false;
|
||||
UpdateConversationRunningFilterUi();
|
||||
PersistConversationListPreferences();
|
||||
RefreshConversationList();
|
||||
}
|
||||
|
||||
private void BtnQuickRunningFilter_Click(object sender, RoutedEventArgs e)
|
||||
=> BtnRunningOnlyFilter_Click(sender, e);
|
||||
|
||||
private void BtnQuickHotSort_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_sortConversationsByRecent = false;
|
||||
UpdateConversationSortUi();
|
||||
PersistConversationListPreferences();
|
||||
RefreshConversationList();
|
||||
}
|
||||
|
||||
private void BtnConversationSort_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_sortConversationsByRecent = !_sortConversationsByRecent;
|
||||
UpdateConversationSortUi();
|
||||
PersistConversationListPreferences();
|
||||
RefreshConversationList();
|
||||
}
|
||||
|
||||
private void UpdateConversationFailureFilterUi()
|
||||
{
|
||||
_failedOnlyFilter = false;
|
||||
UpdateSidebarModeMenu();
|
||||
}
|
||||
|
||||
private void UpdateConversationRunningFilterUi()
|
||||
{
|
||||
if (BtnRunningOnlyFilter == null || RunningOnlyFilterLabel == null)
|
||||
return;
|
||||
|
||||
BtnRunningOnlyFilter.Background = _runningOnlyFilter
|
||||
? BrushFromHex("#DBEAFE")
|
||||
: Brushes.Transparent;
|
||||
BtnRunningOnlyFilter.BorderBrush = _runningOnlyFilter
|
||||
? BrushFromHex("#93C5FD")
|
||||
: Brushes.Transparent;
|
||||
BtnRunningOnlyFilter.BorderThickness = _runningOnlyFilter
|
||||
? new Thickness(1)
|
||||
: new Thickness(0);
|
||||
RunningOnlyFilterLabel.Foreground = _runningOnlyFilter
|
||||
? BrushFromHex("#1D4ED8")
|
||||
: (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray);
|
||||
RunningOnlyFilterLabel.Text = _runningConversationCount > 0
|
||||
? $"진행 {_runningConversationCount}"
|
||||
: "진행";
|
||||
BtnRunningOnlyFilter.ToolTip = _runningOnlyFilter
|
||||
? "실행 중인 대화만 표시 중"
|
||||
: _runningConversationCount > 0
|
||||
? $"현재 실행 중인 대화 {_runningConversationCount}개 보기"
|
||||
: "현재 실행 중인 대화만 보기";
|
||||
UpdateSidebarModeMenu();
|
||||
}
|
||||
|
||||
private void UpdateConversationSortUi()
|
||||
{
|
||||
if (BtnConversationSort == null || ConversationSortLabel == null)
|
||||
return;
|
||||
|
||||
ConversationSortLabel.Text = _sortConversationsByRecent ? "최근" : "활동";
|
||||
BtnConversationSort.Background = _sortConversationsByRecent
|
||||
? BrushFromHex("#EFF6FF")
|
||||
: BrushFromHex("#F8FAFC");
|
||||
BtnConversationSort.BorderBrush = _sortConversationsByRecent
|
||||
? BrushFromHex("#93C5FD")
|
||||
: BrushFromHex("#E2E8F0");
|
||||
BtnConversationSort.BorderThickness = new Thickness(1);
|
||||
ConversationSortLabel.Foreground = _sortConversationsByRecent
|
||||
? BrushFromHex("#1D4ED8")
|
||||
: (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray);
|
||||
BtnConversationSort.ToolTip = _sortConversationsByRecent
|
||||
? "최신 업데이트 순으로 보는 중"
|
||||
: "에이전트 활동량과 실패를 우선으로 보는 중";
|
||||
}
|
||||
|
||||
private void ApplyConversationListPreferences(ChatConversation? conv)
|
||||
{
|
||||
_failedOnlyFilter = false;
|
||||
_runningOnlyFilter = false;
|
||||
_sortConversationsByRecent = string.Equals(conv?.ConversationSortMode, "recent", StringComparison.OrdinalIgnoreCase);
|
||||
UpdateConversationFailureFilterUi();
|
||||
UpdateConversationRunningFilterUi();
|
||||
UpdateConversationSortUi();
|
||||
}
|
||||
|
||||
private void PersistConversationListPreferences()
|
||||
{
|
||||
lock (_convLock)
|
||||
{
|
||||
var session = _appState.ChatSession;
|
||||
if (session == null)
|
||||
return;
|
||||
|
||||
session.SaveConversationListPreferences(_activeTab, _failedOnlyFilter, _runningOnlyFilter, _sortConversationsByRecent, _storage);
|
||||
_currentConversation = session.CurrentConversation;
|
||||
}
|
||||
}
|
||||
}
|
||||
525
src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs
Normal file
525
src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs
Normal file
@@ -0,0 +1,525 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private const int ConversationPageSize = 50;
|
||||
private List<ConversationMeta>? _pendingConversations;
|
||||
|
||||
public void RefreshConversationList()
|
||||
{
|
||||
var metas = _storage.LoadAllMeta();
|
||||
var allPresets = Services.PresetService.GetByTabWithCustom("Cowork", _settings.Settings.Llm.CustomPresets)
|
||||
.Concat(Services.PresetService.GetByTabWithCustom("Code", _settings.Settings.Llm.CustomPresets))
|
||||
.Concat(Services.PresetService.GetByTabWithCustom("Chat", _settings.Settings.Llm.CustomPresets));
|
||||
var presetMap = new Dictionary<string, (string Symbol, string Color)>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var p in allPresets)
|
||||
presetMap.TryAdd(p.Category, (p.Symbol, p.Color));
|
||||
|
||||
var items = metas.Select(c =>
|
||||
{
|
||||
var symbol = ChatCategory.GetSymbol(c.Category);
|
||||
var color = ChatCategory.GetColor(c.Category);
|
||||
if (symbol == "\uE8BD" && color == "#6B7280" && c.Category != ChatCategory.General)
|
||||
{
|
||||
if (presetMap.TryGetValue(c.Category, out var pm))
|
||||
{
|
||||
symbol = pm.Symbol;
|
||||
color = pm.Color;
|
||||
}
|
||||
}
|
||||
|
||||
var runSummary = _appState.GetConversationRunSummary(c.AgentRunHistory);
|
||||
return new ConversationMeta
|
||||
{
|
||||
Id = c.Id,
|
||||
Title = c.Title,
|
||||
Pinned = c.Pinned,
|
||||
Category = c.Category,
|
||||
Symbol = symbol,
|
||||
ColorHex = color,
|
||||
Tab = NormalizeTabName(c.Tab),
|
||||
UpdatedAtText = FormatDate(c.UpdatedAt),
|
||||
UpdatedAt = c.UpdatedAt,
|
||||
Preview = c.Preview ?? "",
|
||||
ParentId = c.ParentId,
|
||||
AgentRunCount = runSummary.AgentRunCount,
|
||||
FailedAgentRunCount = runSummary.FailedAgentRunCount,
|
||||
LastAgentRunSummary = runSummary.LastAgentRunSummary,
|
||||
LastFailedAt = runSummary.LastFailedAt,
|
||||
LastCompletedAt = runSummary.LastCompletedAt,
|
||||
WorkFolder = c.WorkFolder ?? "",
|
||||
IsRunning = _currentConversation?.Id == c.Id
|
||||
&& !string.IsNullOrWhiteSpace(_appState.AgentRun.RunId)
|
||||
&& !string.Equals(_appState.AgentRun.Status, "completed", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(_appState.AgentRun.Status, "failed", StringComparison.OrdinalIgnoreCase),
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
items = items.Where(i => string.Equals(i.Tab, _activeTab, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
items = items.Where(i =>
|
||||
i.Pinned
|
||||
|| !string.IsNullOrWhiteSpace(i.ParentId)
|
||||
|| !string.Equals((i.Title ?? "").Trim(), "새 대화", StringComparison.OrdinalIgnoreCase)
|
||||
|| !string.IsNullOrWhiteSpace(i.Preview)
|
||||
|| i.AgentRunCount > 0
|
||||
|| i.FailedAgentRunCount > 0
|
||||
|| !string.Equals(i.Category, ChatCategory.General, StringComparison.OrdinalIgnoreCase)
|
||||
).ToList();
|
||||
|
||||
_failedConversationCount = items.Count(i => i.FailedAgentRunCount > 0);
|
||||
_runningConversationCount = items.Count(i => i.IsRunning);
|
||||
_spotlightConversationCount = items.Count(i => i.FailedAgentRunCount > 0 || i.AgentRunCount >= 3);
|
||||
UpdateConversationFailureFilterUi();
|
||||
UpdateConversationRunningFilterUi();
|
||||
UpdateConversationQuickStripUi();
|
||||
|
||||
if (_activeTab == "Cowork")
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_selectedCategory))
|
||||
items = items.Where(i => string.Equals(i.Category, _selectedCategory, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
else if (_activeTab == "Code")
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_selectedCategory))
|
||||
items = items.Where(i => string.Equals(i.WorkFolder, _selectedCategory, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_selectedCategory == "__custom__")
|
||||
{
|
||||
var customCats = _settings.Settings.Llm.CustomPresets
|
||||
.Select(c => $"custom_{c.Id}").ToHashSet();
|
||||
items = items.Where(i => customCats.Contains(i.Category)).ToList();
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(_selectedCategory))
|
||||
{
|
||||
items = items.Where(i => i.Category == _selectedCategory).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
var search = SearchBox?.Text?.Trim() ?? "";
|
||||
if (!string.IsNullOrEmpty(search))
|
||||
{
|
||||
items = items.Where(i =>
|
||||
i.Title.Contains(search, StringComparison.OrdinalIgnoreCase) ||
|
||||
i.Preview.Contains(search, StringComparison.OrdinalIgnoreCase) ||
|
||||
i.LastAgentRunSummary.Contains(search, StringComparison.OrdinalIgnoreCase)
|
||||
).ToList();
|
||||
}
|
||||
|
||||
if (_runningOnlyFilter)
|
||||
items = items.Where(i => i.IsRunning).ToList();
|
||||
|
||||
items = (_sortConversationsByRecent
|
||||
? items.OrderByDescending(i => i.Pinned)
|
||||
.ThenByDescending(i => i.UpdatedAt)
|
||||
.ThenByDescending(i => i.FailedAgentRunCount > 0)
|
||||
.ThenByDescending(i => i.AgentRunCount)
|
||||
: items.OrderByDescending(i => i.Pinned)
|
||||
.ThenByDescending(i => i.FailedAgentRunCount > 0)
|
||||
.ThenByDescending(i => i.AgentRunCount)
|
||||
.ThenByDescending(i => i.UpdatedAt))
|
||||
.ToList();
|
||||
|
||||
RenderConversationList(items);
|
||||
}
|
||||
|
||||
private void RenderConversationList(List<ConversationMeta> items)
|
||||
{
|
||||
ConversationPanel.Children.Clear();
|
||||
_pendingConversations = null;
|
||||
|
||||
if (items.Count == 0)
|
||||
{
|
||||
var emptyText = _activeTab switch
|
||||
{
|
||||
"Cowork" => "Cowork 탭 대화가 없습니다",
|
||||
"Code" => "Code 탭 대화가 없습니다",
|
||||
_ => "Chat 탭 대화가 없습니다",
|
||||
};
|
||||
var empty = new TextBlock
|
||||
{
|
||||
Text = emptyText,
|
||||
FontSize = 12,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Margin = new Thickness(0, 20, 0, 0),
|
||||
};
|
||||
ConversationPanel.Children.Add(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
var spotlightItems = BuildConversationSpotlightItems(items);
|
||||
if (spotlightItems.Count > 0)
|
||||
{
|
||||
AddGroupHeader("집중 필요");
|
||||
foreach (var item in spotlightItems)
|
||||
AddConversationItem(item);
|
||||
|
||||
ConversationPanel.Children.Add(new Border
|
||||
{
|
||||
Height = 1,
|
||||
Margin = new Thickness(10, 8, 10, 4),
|
||||
Background = BrushFromHex("#E5E7EB"),
|
||||
Opacity = 0.7,
|
||||
});
|
||||
}
|
||||
|
||||
var allOrdered = new List<(string Group, ConversationMeta Item)>();
|
||||
foreach (var item in items)
|
||||
allOrdered.Add((GetConversationDateGroup(item.UpdatedAt), item));
|
||||
|
||||
var firstPage = allOrdered.Take(ConversationPageSize).ToList();
|
||||
string? lastGroup = null;
|
||||
foreach (var (group, item) in firstPage)
|
||||
{
|
||||
if (group != lastGroup)
|
||||
{
|
||||
AddGroupHeader(group);
|
||||
lastGroup = group;
|
||||
}
|
||||
|
||||
AddConversationItem(item);
|
||||
}
|
||||
|
||||
if (allOrdered.Count > ConversationPageSize)
|
||||
{
|
||||
_pendingConversations = items;
|
||||
AddLoadMoreButton(allOrdered.Count - ConversationPageSize);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddLoadMoreButton(int remaining)
|
||||
{
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var btn = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Cursor = Cursors.Hand,
|
||||
Padding = new Thickness(8, 10, 8, 10),
|
||||
Margin = new Thickness(6, 4, 6, 4),
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
};
|
||||
var stack = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center };
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"더 보기 ({remaining}개 남음)",
|
||||
FontSize = 12,
|
||||
Foreground = accentBrush,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
btn.Child = stack;
|
||||
btn.MouseEnter += (s, _) =>
|
||||
{
|
||||
if (s is Border b)
|
||||
b.Background = new SolidColorBrush(Color.FromArgb(0x12, 0xFF, 0xFF, 0xFF));
|
||||
};
|
||||
btn.MouseLeave += (s, _) =>
|
||||
{
|
||||
if (s is Border b)
|
||||
b.Background = Brushes.Transparent;
|
||||
};
|
||||
btn.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
if (_pendingConversations == null)
|
||||
return;
|
||||
|
||||
var all = _pendingConversations;
|
||||
_pendingConversations = null;
|
||||
ConversationPanel.Children.Clear();
|
||||
|
||||
string? lastGroup = null;
|
||||
foreach (var item in all)
|
||||
{
|
||||
var group = GetConversationDateGroup(item.UpdatedAt);
|
||||
if (!string.Equals(lastGroup, group, StringComparison.Ordinal))
|
||||
{
|
||||
AddGroupHeader(group);
|
||||
lastGroup = group;
|
||||
}
|
||||
|
||||
AddConversationItem(item);
|
||||
}
|
||||
};
|
||||
ConversationPanel.Children.Add(btn);
|
||||
}
|
||||
|
||||
private static string GetConversationDateGroup(DateTime updatedAt)
|
||||
{
|
||||
var today = DateTime.Today;
|
||||
var date = updatedAt.Date;
|
||||
if (date == today)
|
||||
return "오늘";
|
||||
if (date == today.AddDays(-1))
|
||||
return "어제";
|
||||
return "이전";
|
||||
}
|
||||
|
||||
private List<ConversationMeta> BuildConversationSpotlightItems(List<ConversationMeta> items)
|
||||
{
|
||||
if (_failedOnlyFilter || _runningOnlyFilter)
|
||||
return new List<ConversationMeta>();
|
||||
|
||||
var search = SearchBox?.Text?.Trim() ?? "";
|
||||
if (!string.IsNullOrEmpty(search))
|
||||
return new List<ConversationMeta>();
|
||||
|
||||
return items
|
||||
.Where(i => i.FailedAgentRunCount > 0 || i.AgentRunCount >= 3)
|
||||
.OrderByDescending(i => i.FailedAgentRunCount)
|
||||
.ThenByDescending(i => i.AgentRunCount)
|
||||
.ThenByDescending(i => i.UpdatedAt)
|
||||
.Take(3)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void AddGroupHeader(string text)
|
||||
{
|
||||
var header = new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
Margin = new Thickness(8, 10, 0, 4),
|
||||
};
|
||||
ConversationPanel.Children.Add(header);
|
||||
}
|
||||
|
||||
private void AddConversationItem(ConversationMeta item)
|
||||
{
|
||||
var isSelected = false;
|
||||
lock (_convLock)
|
||||
isSelected = _currentConversation?.Id == item.Id;
|
||||
|
||||
var isBranch = !string.IsNullOrEmpty(item.ParentId);
|
||||
var border = new Border
|
||||
{
|
||||
Background = isSelected
|
||||
? new SolidColorBrush(Color.FromArgb(0x10, 0x4B, 0x5E, 0xFC))
|
||||
: Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(5),
|
||||
Padding = new Thickness(7, 4.5, 7, 4.5),
|
||||
Margin = isBranch ? new Thickness(10, 1, 0, 1) : new Thickness(0, 1, 0, 1),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
|
||||
var grid = new Grid();
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(16) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
Brush iconBrush;
|
||||
if (item.Pinned)
|
||||
{
|
||||
iconBrush = Brushes.Orange;
|
||||
}
|
||||
else
|
||||
{
|
||||
try { iconBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(item.ColorHex)); }
|
||||
catch { iconBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue; }
|
||||
}
|
||||
|
||||
var iconText = item.Pinned ? "\uE718" : !string.IsNullOrEmpty(item.ParentId) ? "\uE8A5" : item.Symbol;
|
||||
if (!string.IsNullOrEmpty(item.ParentId))
|
||||
iconBrush = new SolidColorBrush(Color.FromRgb(0x8B, 0x5C, 0xF6));
|
||||
|
||||
var icon = new TextBlock
|
||||
{
|
||||
Text = iconText,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 10.5,
|
||||
Foreground = iconBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(icon, 0);
|
||||
grid.Children.Add(icon);
|
||||
|
||||
var titleColor = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var dateColor = TryFindResource("HintText") as Brush ?? Brushes.DarkGray;
|
||||
|
||||
var stack = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
|
||||
var title = new TextBlock
|
||||
{
|
||||
Text = item.Title,
|
||||
FontSize = 11.75,
|
||||
FontWeight = isSelected ? FontWeights.SemiBold : FontWeights.Normal,
|
||||
Foreground = titleColor,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
};
|
||||
var date = new TextBlock
|
||||
{
|
||||
Text = item.UpdatedAtText,
|
||||
FontSize = 9,
|
||||
Foreground = dateColor,
|
||||
Margin = new Thickness(0, 1.5, 0, 0),
|
||||
};
|
||||
stack.Children.Add(title);
|
||||
stack.Children.Add(date);
|
||||
|
||||
if (item.IsRunning)
|
||||
{
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = _appState.ActiveTasks.Count > 0 ? $"진행 중 {_appState.ActiveTasks.Count}" : "진행 중",
|
||||
FontSize = 8.8,
|
||||
FontWeight = FontWeights.Medium,
|
||||
Foreground = BrushFromHex("#4F46E5"),
|
||||
Margin = new Thickness(0, 1.5, 0, 0),
|
||||
});
|
||||
}
|
||||
|
||||
if (item.AgentRunCount > 0)
|
||||
{
|
||||
var runSummaryText = new TextBlock
|
||||
{
|
||||
Text = item.FailedAgentRunCount > 0
|
||||
? $"실패 {item.FailedAgentRunCount} · {TruncateForStatus(item.LastAgentRunSummary, 26)}"
|
||||
: $"실행 {item.AgentRunCount} · {TruncateForStatus(item.LastAgentRunSummary, 28)}",
|
||||
FontSize = 8.9,
|
||||
Foreground = item.FailedAgentRunCount > 0
|
||||
? BrushFromHex("#B91C1C")
|
||||
: (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray),
|
||||
Margin = new Thickness(0, 1.5, 0, 0),
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(item.LastAgentRunSummary))
|
||||
{
|
||||
runSummaryText.ToolTip = item.FailedAgentRunCount > 0
|
||||
? $"최근 실패 포함\n{item.LastAgentRunSummary}"
|
||||
: item.LastAgentRunSummary;
|
||||
}
|
||||
stack.Children.Add(runSummaryText);
|
||||
}
|
||||
|
||||
Grid.SetColumn(stack, 1);
|
||||
grid.Children.Add(stack);
|
||||
|
||||
var catBtn = new Button
|
||||
{
|
||||
Content = new TextBlock
|
||||
{
|
||||
Text = "\uE70F",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 9,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
},
|
||||
Background = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
Cursor = Cursors.Hand,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Visibility = Visibility.Collapsed,
|
||||
Width = 20,
|
||||
Height = 20,
|
||||
Padding = new Thickness(0),
|
||||
Opacity = 0.72,
|
||||
ToolTip = _activeTab == "Cowork" ? "작업 유형" : "대화 주제 변경",
|
||||
};
|
||||
var capturedId = item.Id;
|
||||
catBtn.Click += (_, _) => ShowConversationMenu(capturedId);
|
||||
Grid.SetColumn(catBtn, 2);
|
||||
grid.Children.Add(catBtn);
|
||||
|
||||
if (isSelected)
|
||||
{
|
||||
border.BorderBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
border.BorderThickness = new Thickness(1.25, 0, 0, 0);
|
||||
}
|
||||
|
||||
border.Child = grid;
|
||||
|
||||
var hoverBg = new SolidColorBrush(Color.FromArgb(0x08, 0xFF, 0xFF, 0xFF));
|
||||
border.MouseEnter += (_, _) =>
|
||||
{
|
||||
if (!isSelected)
|
||||
border.Background = hoverBg;
|
||||
catBtn.Visibility = Visibility.Visible;
|
||||
};
|
||||
border.MouseLeave += (_, _) =>
|
||||
{
|
||||
if (!isSelected)
|
||||
border.Background = Brushes.Transparent;
|
||||
catBtn.Visibility = Visibility.Collapsed;
|
||||
};
|
||||
|
||||
border.MouseLeftButtonDown += (_, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (isSelected)
|
||||
{
|
||||
EnterTitleEditMode(title, item.Id, titleColor);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isStreaming)
|
||||
{
|
||||
_streamCts?.Cancel();
|
||||
_cursorTimer.Stop();
|
||||
_typingTimer.Stop();
|
||||
_elapsedTimer.Stop();
|
||||
_activeStreamText = null;
|
||||
_elapsedLabel = null;
|
||||
_isStreaming = false;
|
||||
}
|
||||
|
||||
var conv = _storage.Load(item.Id);
|
||||
if (conv == null)
|
||||
return;
|
||||
|
||||
lock (_convLock)
|
||||
{
|
||||
_currentConversation = ChatSession?.SetCurrentConversation(_activeTab, conv, _storage) ?? conv;
|
||||
SyncTabConversationIdsFromSession();
|
||||
}
|
||||
|
||||
SaveLastConversations();
|
||||
UpdateChatTitle();
|
||||
RenderMessages();
|
||||
RefreshConversationList();
|
||||
RefreshDraftQueueUi();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Error($"대화 전환 오류: {ex.Message}");
|
||||
}
|
||||
};
|
||||
|
||||
border.MouseRightButtonUp += (_, me) =>
|
||||
{
|
||||
me.Handled = true;
|
||||
if (!isSelected)
|
||||
{
|
||||
var conv = _storage.Load(item.Id);
|
||||
if (conv != null)
|
||||
{
|
||||
lock (_convLock)
|
||||
{
|
||||
_currentConversation = ChatSession?.SetCurrentConversation(_activeTab, conv, _storage) ?? conv;
|
||||
SyncTabConversationIdsFromSession();
|
||||
}
|
||||
SaveLastConversations();
|
||||
UpdateChatTitle();
|
||||
RenderMessages();
|
||||
RefreshDraftQueueUi();
|
||||
}
|
||||
}
|
||||
|
||||
Dispatcher.BeginInvoke(new Action(() => ShowConversationMenu(item.Id)), DispatcherPriority.Input);
|
||||
};
|
||||
|
||||
ConversationPanel.Children.Add(border);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private void EnterTitleEditMode(TextBlock titleTb, string conversationId, Brush titleColor)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parent = titleTb.Parent as StackPanel;
|
||||
if (parent == null) return;
|
||||
|
||||
var idx = parent.Children.IndexOf(titleTb);
|
||||
if (idx < 0) return;
|
||||
|
||||
var editBox = new TextBox
|
||||
{
|
||||
Text = titleTb.Text,
|
||||
FontSize = 12.5,
|
||||
Foreground = titleColor,
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue,
|
||||
BorderThickness = new Thickness(0, 0, 0, 1),
|
||||
CaretBrush = titleColor,
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
};
|
||||
|
||||
parent.Children.RemoveAt(idx);
|
||||
parent.Children.Insert(idx, editBox);
|
||||
|
||||
var committed = false;
|
||||
void CommitEdit()
|
||||
{
|
||||
if (committed) return;
|
||||
committed = true;
|
||||
|
||||
var newTitle = editBox.Text.Trim();
|
||||
if (string.IsNullOrEmpty(newTitle)) newTitle = titleTb.Text;
|
||||
|
||||
titleTb.Text = newTitle;
|
||||
try
|
||||
{
|
||||
var currentIdx = parent.Children.IndexOf(editBox);
|
||||
if (currentIdx >= 0)
|
||||
{
|
||||
parent.Children.RemoveAt(currentIdx);
|
||||
parent.Children.Insert(currentIdx, titleTb);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
var conv = _storage.Load(conversationId);
|
||||
if (conv != null)
|
||||
{
|
||||
conv.Title = newTitle;
|
||||
_storage.Save(conv);
|
||||
lock (_convLock)
|
||||
{
|
||||
if (_currentConversation?.Id == conversationId)
|
||||
{
|
||||
_currentConversation = ChatSession?.UpdateConversationMetadata(_activeTab, c => c.Title = newTitle, _storage) ?? _currentConversation;
|
||||
}
|
||||
}
|
||||
UpdateChatTitle();
|
||||
}
|
||||
}
|
||||
|
||||
void CancelEdit()
|
||||
{
|
||||
if (committed) return;
|
||||
committed = true;
|
||||
try
|
||||
{
|
||||
var currentIdx = parent.Children.IndexOf(editBox);
|
||||
if (currentIdx >= 0)
|
||||
{
|
||||
parent.Children.RemoveAt(currentIdx);
|
||||
parent.Children.Insert(currentIdx, titleTb);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
editBox.KeyDown += (_, ke) =>
|
||||
{
|
||||
if (ke.Key == Key.Enter) { ke.Handled = true; CommitEdit(); }
|
||||
if (ke.Key == Key.Escape) { ke.Handled = true; CancelEdit(); }
|
||||
};
|
||||
editBox.LostFocus += (_, _) => CommitEdit();
|
||||
|
||||
editBox.Focus();
|
||||
editBox.SelectAll();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Error($"제목 편집 오류: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowConversationMenu(string conversationId)
|
||||
{
|
||||
var conv = _storage.Load(conversationId);
|
||||
var isPinned = conv?.Pinned ?? false;
|
||||
|
||||
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(30, 255, 255, 255));
|
||||
|
||||
var popup = new Popup
|
||||
{
|
||||
StaysOpen = false,
|
||||
AllowsTransparency = true,
|
||||
PopupAnimation = PopupAnimation.Fade,
|
||||
Placement = PlacementMode.MousePoint,
|
||||
};
|
||||
|
||||
var container = new Border
|
||||
{
|
||||
Background = bgBrush,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(6),
|
||||
MinWidth = 200,
|
||||
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||||
{
|
||||
BlurRadius = 16,
|
||||
ShadowDepth = 4,
|
||||
Opacity = 0.3,
|
||||
Color = Colors.Black,
|
||||
},
|
||||
};
|
||||
|
||||
var stack = new StackPanel();
|
||||
|
||||
Border CreateMenuItem(string icon, string text, Brush iconColor, Action onClick)
|
||||
{
|
||||
var item = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(10, 7, 10, 7),
|
||||
Margin = new Thickness(0, 1, 0, 1),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var g = new Grid();
|
||||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(24) });
|
||||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
|
||||
var iconTb = new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12,
|
||||
Foreground = iconColor,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(iconTb, 0);
|
||||
g.Children.Add(iconTb);
|
||||
|
||||
var textTb = new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
FontSize = 12.5,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(textTb, 1);
|
||||
g.Children.Add(textTb);
|
||||
|
||||
item.Child = g;
|
||||
item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; };
|
||||
item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||||
item.MouseLeftButtonUp += (_, _) => { popup.IsOpen = false; onClick(); };
|
||||
return item;
|
||||
}
|
||||
|
||||
Border CreateSeparator() => new()
|
||||
{
|
||||
Height = 1,
|
||||
Background = borderBrush,
|
||||
Opacity = 0.3,
|
||||
Margin = new Thickness(8, 4, 8, 4),
|
||||
};
|
||||
|
||||
stack.Children.Add(CreateMenuItem(
|
||||
isPinned ? "\uE77A" : "\uE718",
|
||||
isPinned ? "고정 해제" : "상단 고정",
|
||||
TryFindResource("AccentColor") as Brush ?? Brushes.Blue,
|
||||
() =>
|
||||
{
|
||||
var c = _storage.Load(conversationId);
|
||||
if (c == null) return;
|
||||
|
||||
c.Pinned = !c.Pinned;
|
||||
_storage.Save(c);
|
||||
lock (_convLock)
|
||||
{
|
||||
if (_currentConversation?.Id == conversationId)
|
||||
{
|
||||
_currentConversation = ChatSession?.UpdateConversationMetadata(_activeTab, current => current.Pinned = c.Pinned, _storage) ?? _currentConversation;
|
||||
}
|
||||
}
|
||||
RefreshConversationList();
|
||||
}));
|
||||
|
||||
stack.Children.Add(CreateMenuItem("\uE8AC", "이름 변경", secondaryText, () =>
|
||||
{
|
||||
foreach (UIElement child in ConversationPanel.Children)
|
||||
{
|
||||
if (child is not Border b || b.Child is not Grid g) continue;
|
||||
foreach (UIElement gc in g.Children)
|
||||
{
|
||||
if (gc is StackPanel sp && sp.Children.Count > 0 && sp.Children[0] is TextBlock tb)
|
||||
{
|
||||
if (conv != null && tb.Text == conv.Title)
|
||||
{
|
||||
var titleColor = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
EnterTitleEditMode(tb, conversationId, titleColor);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
if ((_activeTab == "Cowork" || _activeTab == "Code") && conv != null)
|
||||
{
|
||||
var catKey = conv.Category ?? ChatCategory.General;
|
||||
string catSymbol = "\uE8BD", catLabel = catKey, catColor = "#6B7280";
|
||||
var chatCat = ChatCategory.All.FirstOrDefault(c => c.Key == catKey);
|
||||
if (chatCat != default && chatCat.Key != ChatCategory.General)
|
||||
{
|
||||
catSymbol = chatCat.Symbol;
|
||||
catLabel = chatCat.Label;
|
||||
catColor = chatCat.Color;
|
||||
}
|
||||
else
|
||||
{
|
||||
var preset = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets)
|
||||
.FirstOrDefault(p => p.Category == catKey);
|
||||
if (preset != null)
|
||||
{
|
||||
catSymbol = preset.Symbol;
|
||||
catLabel = preset.Label;
|
||||
catColor = preset.Color;
|
||||
}
|
||||
}
|
||||
|
||||
stack.Children.Add(CreateSeparator());
|
||||
var infoSp = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(10, 4, 10, 4) };
|
||||
try
|
||||
{
|
||||
var catBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(catColor));
|
||||
infoSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = catSymbol,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12,
|
||||
Foreground = catBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
});
|
||||
infoSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = catLabel,
|
||||
FontSize = 12,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
infoSp.Children.Add(new TextBlock { Text = catLabel, FontSize = 12, Foreground = primaryText });
|
||||
}
|
||||
stack.Children.Add(infoSp);
|
||||
}
|
||||
|
||||
if (_activeTab == "Chat")
|
||||
{
|
||||
stack.Children.Add(CreateSeparator());
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "분류 변경",
|
||||
FontSize = 10.5,
|
||||
Foreground = secondaryText,
|
||||
Margin = new Thickness(10, 4, 0, 4),
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
});
|
||||
|
||||
var currentCategory = conv?.Category ?? ChatCategory.General;
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
|
||||
foreach (var (key, label, symbol, color) in ChatCategory.All)
|
||||
{
|
||||
var capturedKey = key;
|
||||
var isCurrentCat = capturedKey == currentCategory;
|
||||
|
||||
var catItem = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(10, 7, 10, 7),
|
||||
Margin = new Thickness(0, 1, 0, 1),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var catGrid = new Grid();
|
||||
catGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(24) });
|
||||
catGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
catGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(20) });
|
||||
|
||||
var catIcon = new TextBlock
|
||||
{
|
||||
Text = symbol,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12,
|
||||
Foreground = BrushFromHex(color),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(catIcon, 0);
|
||||
catGrid.Children.Add(catIcon);
|
||||
|
||||
var catText = new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 12.5,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
FontWeight = isCurrentCat ? FontWeights.Bold : FontWeights.Normal,
|
||||
};
|
||||
Grid.SetColumn(catText, 1);
|
||||
catGrid.Children.Add(catText);
|
||||
|
||||
if (isCurrentCat)
|
||||
{
|
||||
var check = CreateSimpleCheck(accentBrush, 14);
|
||||
Grid.SetColumn(check, 2);
|
||||
catGrid.Children.Add(check);
|
||||
}
|
||||
|
||||
catItem.Child = catGrid;
|
||||
catItem.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; };
|
||||
catItem.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||||
catItem.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
popup.IsOpen = false;
|
||||
var c = _storage.Load(conversationId);
|
||||
if (c == null) return;
|
||||
|
||||
c.Category = capturedKey;
|
||||
var preset = Services.PresetService.GetByCategory(capturedKey);
|
||||
if (preset != null)
|
||||
{
|
||||
c.SystemCommand = preset.SystemPrompt;
|
||||
}
|
||||
_storage.Save(c);
|
||||
|
||||
lock (_convLock)
|
||||
{
|
||||
if (_currentConversation?.Id == conversationId)
|
||||
{
|
||||
_currentConversation = ChatSession?.UpdateConversationMetadata(_activeTab, current =>
|
||||
{
|
||||
current.Category = capturedKey;
|
||||
if (preset != null)
|
||||
{
|
||||
current.SystemCommand = preset.SystemPrompt;
|
||||
}
|
||||
}, _storage) ?? _currentConversation;
|
||||
}
|
||||
}
|
||||
|
||||
bool isCurrent;
|
||||
lock (_convLock) { isCurrent = _currentConversation?.Id == conversationId; }
|
||||
if (isCurrent && preset != null && !string.IsNullOrEmpty(preset.Placeholder))
|
||||
{
|
||||
_promptCardPlaceholder = preset.Placeholder;
|
||||
UpdateWatermarkVisibility();
|
||||
if (string.IsNullOrEmpty(InputBox.Text))
|
||||
{
|
||||
InputWatermark.Text = preset.Placeholder;
|
||||
InputWatermark.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
else if (isCurrent)
|
||||
{
|
||||
ClearPromptCardPlaceholder();
|
||||
}
|
||||
|
||||
RefreshConversationList();
|
||||
};
|
||||
stack.Children.Add(catItem);
|
||||
}
|
||||
}
|
||||
|
||||
stack.Children.Add(CreateSeparator());
|
||||
stack.Children.Add(CreateMenuItem("\uE74D", "이 대화 삭제", Brushes.IndianRed, () =>
|
||||
{
|
||||
var result = CustomMessageBox.Show("이 대화를 삭제하시겠습니까?", "대화 삭제",
|
||||
MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
if (result != MessageBoxResult.Yes) return;
|
||||
|
||||
_storage.Delete(conversationId);
|
||||
lock (_convLock)
|
||||
{
|
||||
if (_currentConversation?.Id == conversationId)
|
||||
{
|
||||
_currentConversation = null;
|
||||
MessagePanel.Children.Clear();
|
||||
EmptyState.Visibility = Visibility.Visible;
|
||||
UpdateChatTitle();
|
||||
}
|
||||
}
|
||||
RefreshConversationList();
|
||||
}));
|
||||
|
||||
container.Child = stack;
|
||||
popup.Child = container;
|
||||
popup.IsOpen = true;
|
||||
}
|
||||
}
|
||||
321
src/AxCopilot/Views/ChatWindow.FileBrowserPresentation.cs
Normal file
321
src/AxCopilot/Views/ChatWindow.FileBrowserPresentation.cs
Normal file
@@ -0,0 +1,321 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private static readonly HashSet<string> _ignoredDirs = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"bin", "obj", "node_modules", ".git", ".vs", ".idea", ".vscode",
|
||||
"__pycache__", ".mypy_cache", ".pytest_cache", "dist", "build",
|
||||
".cache", ".next", ".nuxt", "coverage", ".terraform",
|
||||
};
|
||||
|
||||
private DispatcherTimer? _fileBrowserRefreshTimer;
|
||||
|
||||
private void ToggleFileBrowser()
|
||||
{
|
||||
if (FileBrowserPanel.Visibility == Visibility.Visible)
|
||||
{
|
||||
FileBrowserPanel.Visibility = Visibility.Collapsed;
|
||||
_settings.Settings.Llm.ShowFileBrowser = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
FileBrowserPanel.Visibility = Visibility.Visible;
|
||||
_settings.Settings.Llm.ShowFileBrowser = true;
|
||||
BuildFileTree();
|
||||
}
|
||||
|
||||
_settings.Save();
|
||||
}
|
||||
|
||||
private void BtnFileBrowserRefresh_Click(object sender, RoutedEventArgs e) => BuildFileTree();
|
||||
|
||||
private void BtnFileBrowserOpenFolder_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var folder = GetCurrentWorkFolder();
|
||||
if (string.IsNullOrEmpty(folder) || !Directory.Exists(folder))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo { FileName = folder, UseShellExecute = true });
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnFileBrowserClose_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
FileBrowserPanel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void BuildFileTree()
|
||||
{
|
||||
FileTreeView.Items.Clear();
|
||||
var folder = GetCurrentWorkFolder();
|
||||
if (string.IsNullOrEmpty(folder) || !Directory.Exists(folder))
|
||||
{
|
||||
FileTreeView.Items.Add(new TreeViewItem { Header = "작업 폴더를 선택하세요", IsEnabled = false });
|
||||
return;
|
||||
}
|
||||
|
||||
FileBrowserTitle.Text = $"파일 탐색기 — {Path.GetFileName(folder)}";
|
||||
var count = 0;
|
||||
PopulateDirectory(new DirectoryInfo(folder), FileTreeView.Items, 0, ref count);
|
||||
}
|
||||
|
||||
private void PopulateDirectory(DirectoryInfo dir, ItemCollection items, int depth, ref int count)
|
||||
{
|
||||
if (depth > 4 || count > 200)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var subDir in dir.GetDirectories().OrderBy(d => d.Name))
|
||||
{
|
||||
if (count > 200)
|
||||
break;
|
||||
if (_ignoredDirs.Contains(subDir.Name) || subDir.Name.StartsWith('.'))
|
||||
continue;
|
||||
|
||||
count++;
|
||||
var dirItem = new TreeViewItem
|
||||
{
|
||||
Header = CreateSurfaceFileTreeHeader("\uED25", subDir.Name, null),
|
||||
Tag = subDir.FullName,
|
||||
IsExpanded = depth < 1,
|
||||
};
|
||||
|
||||
if (depth < 3)
|
||||
{
|
||||
dirItem.Items.Add(new TreeViewItem { Header = "로딩 중..." });
|
||||
var capturedDir = subDir;
|
||||
var capturedDepth = depth;
|
||||
dirItem.Expanded += (s, _) =>
|
||||
{
|
||||
if (s is TreeViewItem ti && ti.Items.Count == 1 && ti.Items[0] is TreeViewItem d && d.Header?.ToString() == "로딩 중...")
|
||||
{
|
||||
ti.Items.Clear();
|
||||
var c = 0;
|
||||
PopulateDirectory(capturedDir, ti.Items, capturedDepth + 1, ref c);
|
||||
}
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
PopulateDirectory(subDir, dirItem.Items, depth + 1, ref count);
|
||||
}
|
||||
|
||||
items.Add(dirItem);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var file in dir.GetFiles().OrderBy(f => f.Name))
|
||||
{
|
||||
if (count > 200)
|
||||
break;
|
||||
count++;
|
||||
|
||||
var ext = file.Extension.ToLowerInvariant();
|
||||
var icon = GetFileIcon(ext);
|
||||
var size = FormatFileSize(file.Length);
|
||||
|
||||
var fileItem = new TreeViewItem
|
||||
{
|
||||
Header = CreateSurfaceFileTreeHeader(icon, file.Name, size),
|
||||
Tag = file.FullName,
|
||||
};
|
||||
|
||||
var capturedPath = file.FullName;
|
||||
fileItem.MouseDoubleClick += (_, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
TryShowPreview(capturedPath);
|
||||
};
|
||||
|
||||
fileItem.MouseRightButtonUp += (s, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
if (s is TreeViewItem ti)
|
||||
ti.IsSelected = true;
|
||||
ShowFileTreeContextMenu(capturedPath);
|
||||
};
|
||||
|
||||
items.Add(fileItem);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetFileIcon(string ext) => ext switch
|
||||
{
|
||||
".html" or ".htm" => "\uEB41",
|
||||
".xlsx" or ".xls" => "\uE9F9",
|
||||
".docx" or ".doc" => "\uE8A5",
|
||||
".pdf" => "\uEA90",
|
||||
".csv" => "\uE80A",
|
||||
".md" => "\uE70B",
|
||||
".json" or ".xml" => "\uE943",
|
||||
".png" or ".jpg" or ".jpeg" or ".gif" or ".svg" or ".webp" => "\uEB9F",
|
||||
".cs" or ".py" or ".js" or ".ts" or ".java" or ".cpp" => "\uE943",
|
||||
".bat" or ".cmd" or ".ps1" or ".sh" => "\uE756",
|
||||
".txt" or ".log" => "\uE8A5",
|
||||
_ => "\uE7C3",
|
||||
};
|
||||
|
||||
private static string FormatFileSize(long bytes) => bytes switch
|
||||
{
|
||||
< 1024 => $"{bytes} B",
|
||||
< 1024 * 1024 => $"{bytes / 1024.0:F1} KB",
|
||||
_ => $"{bytes / (1024.0 * 1024.0):F1} MB",
|
||||
};
|
||||
|
||||
private void ShowFileTreeContextMenu(string filePath)
|
||||
{
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var dangerBrush = new SolidColorBrush(Color.FromRgb(0xE7, 0x4C, 0x3C));
|
||||
|
||||
var popup = new Popup
|
||||
{
|
||||
StaysOpen = false,
|
||||
AllowsTransparency = true,
|
||||
PopupAnimation = PopupAnimation.Fade,
|
||||
Placement = PlacementMode.MousePoint,
|
||||
};
|
||||
var panel = new StackPanel { Margin = new Thickness(2) };
|
||||
var container = CreateSurfacePopupContainer(panel, 200, new Thickness(6));
|
||||
popup.Child = container;
|
||||
|
||||
void AddItem(string icon, string label, Action action, Brush? labelColor = null, Brush? iconColor = null)
|
||||
{
|
||||
var item = CreateSurfacePopupMenuItem(icon, iconColor ?? secondaryText, label, () =>
|
||||
{
|
||||
popup.IsOpen = false;
|
||||
action();
|
||||
}, labelColor ?? primaryText);
|
||||
panel.Children.Add(item);
|
||||
}
|
||||
|
||||
void AddSep()
|
||||
{
|
||||
panel.Children.Add(CreateSurfacePopupSeparator());
|
||||
}
|
||||
|
||||
var ext = Path.GetExtension(filePath).ToLowerInvariant();
|
||||
if (_previewableExtensions.Contains(ext))
|
||||
AddItem("\uE8A1", "미리보기", () => ShowPreviewPanel(filePath));
|
||||
|
||||
AddItem("\uE8A7", "외부 프로그램으로 열기", () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo { FileName = filePath, UseShellExecute = true });
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
});
|
||||
AddItem("\uED25", "폴더에서 보기", () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start("explorer.exe", $"/select,\"{filePath}\"");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
});
|
||||
AddItem("\uE8C8", "경로 복사", () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Clipboard.SetText(filePath);
|
||||
ShowToast("경로 복사됨");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
AddSep();
|
||||
|
||||
AddItem("\uE8AC", "이름 변경", () =>
|
||||
{
|
||||
var dir = Path.GetDirectoryName(filePath) ?? "";
|
||||
var oldName = Path.GetFileName(filePath);
|
||||
var dlg = new InputDialog("이름 변경", "새 파일 이름:", oldName) { Owner = this };
|
||||
if (dlg.ShowDialog() == true && !string.IsNullOrWhiteSpace(dlg.ResponseText))
|
||||
{
|
||||
var newPath = Path.Combine(dir, dlg.ResponseText.Trim());
|
||||
try
|
||||
{
|
||||
File.Move(filePath, newPath);
|
||||
BuildFileTree();
|
||||
ShowToast($"이름 변경: {dlg.ResponseText.Trim()}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowToast($"이름 변경 실패: {ex.Message}", "\uE783");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddItem("\uE74D", "삭제", () =>
|
||||
{
|
||||
var result = CustomMessageBox.Show(
|
||||
$"파일을 삭제하시겠습니까?\n{Path.GetFileName(filePath)}",
|
||||
"파일 삭제 확인", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||
if (result == MessageBoxResult.Yes)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(filePath);
|
||||
BuildFileTree();
|
||||
ShowToast("파일 삭제됨");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowToast($"삭제 실패: {ex.Message}", "\uE783");
|
||||
}
|
||||
}
|
||||
}, dangerBrush, dangerBrush);
|
||||
|
||||
Dispatcher.BeginInvoke(() => { popup.IsOpen = true; }, DispatcherPriority.Input);
|
||||
}
|
||||
|
||||
private void RefreshFileTreeIfVisible()
|
||||
{
|
||||
if (FileBrowserPanel.Visibility != Visibility.Visible)
|
||||
return;
|
||||
|
||||
_fileBrowserRefreshTimer?.Stop();
|
||||
_fileBrowserRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
|
||||
_fileBrowserRefreshTimer.Tick += (_, _) =>
|
||||
{
|
||||
_fileBrowserRefreshTimer.Stop();
|
||||
BuildFileTree();
|
||||
};
|
||||
_fileBrowserRefreshTimer.Start();
|
||||
}
|
||||
}
|
||||
92
src/AxCopilot/Views/ChatWindow.FooterPresentation.cs
Normal file
92
src/AxCopilot/Views/ChatWindow.FooterPresentation.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private void UpdateFolderBar()
|
||||
{
|
||||
if (FolderBar == null)
|
||||
return;
|
||||
|
||||
if (_activeTab == "Chat")
|
||||
{
|
||||
FolderBar.Visibility = Visibility.Collapsed;
|
||||
UpdateGitBranchUi(null, "", "", "", "", Visibility.Collapsed);
|
||||
RefreshContextUsageVisual();
|
||||
return;
|
||||
}
|
||||
|
||||
FolderBar.Visibility = Visibility.Visible;
|
||||
var folder = GetCurrentWorkFolder();
|
||||
if (!string.IsNullOrEmpty(folder))
|
||||
{
|
||||
FolderPathLabel.Text = folder;
|
||||
FolderPathLabel.ToolTip = folder;
|
||||
}
|
||||
else
|
||||
{
|
||||
FolderPathLabel.Text = "폴더를 선택하세요";
|
||||
FolderPathLabel.ToolTip = null;
|
||||
}
|
||||
|
||||
LoadConversationSettings();
|
||||
LoadCompactionMetricsFromConversation();
|
||||
UpdatePermissionUI();
|
||||
UpdateDataUsageUI();
|
||||
RefreshContextUsageVisual();
|
||||
ScheduleGitBranchRefresh();
|
||||
}
|
||||
|
||||
private void UpdateDataUsageUI()
|
||||
{
|
||||
_folderDataUsage = GetAutomaticFolderDataUsage();
|
||||
}
|
||||
|
||||
private void UpdateSelectedPresetGuide(ChatConversation? conversation = null)
|
||||
{
|
||||
if (SelectedPresetGuide == null || SelectedPresetGuideTitle == null || SelectedPresetGuideDesc == null)
|
||||
return;
|
||||
|
||||
if (string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
SelectedPresetGuide.Visibility = Visibility.Collapsed;
|
||||
SelectedPresetGuideTitle.Text = "";
|
||||
SelectedPresetGuideDesc.Text = "";
|
||||
return;
|
||||
}
|
||||
|
||||
conversation ??= _currentConversation;
|
||||
var category = conversation?.Category?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(category))
|
||||
{
|
||||
SelectedPresetGuide.Visibility = Visibility.Collapsed;
|
||||
SelectedPresetGuideTitle.Text = "";
|
||||
SelectedPresetGuideDesc.Text = "";
|
||||
return;
|
||||
}
|
||||
|
||||
var preset = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets)
|
||||
.FirstOrDefault(p => string.Equals(p.Category?.Trim(), category, StringComparison.OrdinalIgnoreCase));
|
||||
if (preset == null)
|
||||
{
|
||||
SelectedPresetGuide.Visibility = Visibility.Collapsed;
|
||||
SelectedPresetGuideTitle.Text = "";
|
||||
SelectedPresetGuideDesc.Text = "";
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedPresetGuideTitle.Text = string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase)
|
||||
? $"선택된 작업 유형 · {preset.Label}"
|
||||
: $"선택된 대화 주제 · {preset.Label}";
|
||||
SelectedPresetGuideDesc.Text = string.IsNullOrWhiteSpace(preset.Description)
|
||||
? (preset.Placeholder ?? "")
|
||||
: preset.Description;
|
||||
SelectedPresetGuide.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
361
src/AxCopilot/Views/ChatWindow.GitBranchPresentation.cs
Normal file
361
src/AxCopilot/Views/ChatWindow.GitBranchPresentation.cs
Normal file
@@ -0,0 +1,361 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private void UpdateGitBranchUi(string? branchName, string filesText, string addedText, string deletedText, string tooltip, Visibility visibility)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
_currentGitBranchName = branchName;
|
||||
_currentGitTooltip = tooltip;
|
||||
|
||||
if (BtnGitBranch != null)
|
||||
{
|
||||
BtnGitBranch.Visibility = visibility;
|
||||
BtnGitBranch.ToolTip = string.IsNullOrWhiteSpace(tooltip) ? "현재 Git 브랜치 상태" : tooltip;
|
||||
}
|
||||
|
||||
if (GitBranchLabel != null)
|
||||
GitBranchLabel.Text = string.IsNullOrWhiteSpace(branchName) ? "브랜치 없음" : branchName;
|
||||
if (GitBranchFilesText != null)
|
||||
GitBranchFilesText.Text = filesText;
|
||||
if (GitBranchAddedText != null)
|
||||
GitBranchAddedText.Text = addedText;
|
||||
if (GitBranchDeletedText != null)
|
||||
GitBranchDeletedText.Text = deletedText;
|
||||
if (GitBranchSeparator != null)
|
||||
GitBranchSeparator.Visibility = visibility;
|
||||
});
|
||||
}
|
||||
|
||||
private void BuildGitBranchPopup()
|
||||
{
|
||||
if (GitBranchItems == null)
|
||||
return;
|
||||
|
||||
GitBranchItems.Children.Clear();
|
||||
|
||||
var gitRoot = _currentGitRoot ?? ResolveGitRoot(GetCurrentWorkFolder());
|
||||
var branchName = _currentGitBranchName ?? "detached";
|
||||
var tooltip = _currentGitTooltip ?? "";
|
||||
var fileText = GitBranchFilesText?.Text ?? "";
|
||||
var addedText = GitBranchAddedText?.Text ?? "";
|
||||
var deletedText = GitBranchDeletedText?.Text ?? "";
|
||||
var query = (_gitBranchSearchText ?? "").Trim();
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
|
||||
GitBranchItems.Children.Add(CreatePopupSummaryStrip(new[]
|
||||
{
|
||||
("브랜치", string.IsNullOrWhiteSpace(branchName) ? "없음" : branchName, "#F8FAFC", "#E2E8F0", "#475569"),
|
||||
("파일", string.IsNullOrWhiteSpace(fileText) ? "0" : fileText, "#EFF6FF", "#BFDBFE", "#1D4ED8"),
|
||||
("최근", _recentGitBranches.Count.ToString(), "#F5F3FF", "#DDD6FE", "#6D28D9"),
|
||||
}));
|
||||
GitBranchItems.Children.Add(CreatePopupSectionLabel("현재 브랜치", new Thickness(8, 6, 8, 4)));
|
||||
GitBranchItems.Children.Add(CreatePopupMenuRow(
|
||||
"\uE943",
|
||||
branchName,
|
||||
string.IsNullOrWhiteSpace(fileText) ? "현재 브랜치" : fileText,
|
||||
true,
|
||||
accentBrush,
|
||||
secondaryText,
|
||||
primaryText,
|
||||
() => { }));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(addedText) || !string.IsNullOrWhiteSpace(deletedText))
|
||||
{
|
||||
var stats = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Margin = new Thickness(8, 2, 8, 8),
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(addedText))
|
||||
stats.Children.Add(CreateMetricPill(addedText, "#16A34A"));
|
||||
if (!string.IsNullOrWhiteSpace(deletedText))
|
||||
stats.Children.Add(CreateMetricPill(deletedText, "#DC2626"));
|
||||
GitBranchItems.Children.Add(stats);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(gitRoot))
|
||||
{
|
||||
GitBranchItems.Children.Add(CreatePopupSectionLabel("저장소", new Thickness(8, 6, 8, 4)));
|
||||
GitBranchItems.Children.Add(CreatePopupMenuRow(
|
||||
"\uED25",
|
||||
System.IO.Path.GetFileName(gitRoot.TrimEnd('\\', '/')),
|
||||
gitRoot,
|
||||
false,
|
||||
accentBrush,
|
||||
secondaryText,
|
||||
primaryText,
|
||||
() => { }));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_currentGitUpstreamStatus))
|
||||
{
|
||||
GitBranchItems.Children.Add(CreatePopupMenuRow(
|
||||
"\uE8AB",
|
||||
"업스트림",
|
||||
_currentGitUpstreamStatus!,
|
||||
false,
|
||||
accentBrush,
|
||||
secondaryText,
|
||||
primaryText,
|
||||
() => { }));
|
||||
}
|
||||
|
||||
GitBranchItems.Children.Add(CreatePopupSectionLabel("빠른 작업", new Thickness(8, 10, 8, 4)));
|
||||
GitBranchItems.Children.Add(CreatePopupMenuRow(
|
||||
"\uE8C8",
|
||||
"상태 요약 복사",
|
||||
"브랜치 변경 파일, 추가/삭제 라인 복사",
|
||||
false,
|
||||
accentBrush,
|
||||
secondaryText,
|
||||
primaryText,
|
||||
() =>
|
||||
{
|
||||
try { Clipboard.SetText(tooltip); } catch { }
|
||||
GitBranchPopup.IsOpen = false;
|
||||
}));
|
||||
|
||||
GitBranchItems.Children.Add(CreatePopupMenuRow(
|
||||
"\uE72B",
|
||||
"새로고침",
|
||||
"Git 상태를 다시 조회합니다.",
|
||||
false,
|
||||
accentBrush,
|
||||
secondaryText,
|
||||
primaryText,
|
||||
async () =>
|
||||
{
|
||||
await RefreshGitBranchStatusAsync();
|
||||
BuildGitBranchPopup();
|
||||
}));
|
||||
|
||||
var filteredBranches = _currentGitBranches
|
||||
.Where(branch => string.IsNullOrWhiteSpace(query)
|
||||
|| branch.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||
.Take(20)
|
||||
.ToList();
|
||||
|
||||
var recentBranches = _recentGitBranches
|
||||
.Where(branch => _currentGitBranches.Any(current => string.Equals(current, branch, StringComparison.OrdinalIgnoreCase)))
|
||||
.Where(branch => string.IsNullOrWhiteSpace(query)
|
||||
|| branch.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||
.Take(5)
|
||||
.ToList();
|
||||
|
||||
if (recentBranches.Count > 0)
|
||||
{
|
||||
GitBranchItems.Children.Add(CreatePopupSectionLabel($"최근 전환 · {recentBranches.Count}", new Thickness(8, 10, 8, 4)));
|
||||
|
||||
foreach (var branch in recentBranches)
|
||||
{
|
||||
var isCurrent = string.Equals(branch, branchName, StringComparison.OrdinalIgnoreCase);
|
||||
GitBranchItems.Children.Add(CreatePopupMenuRow(
|
||||
isCurrent ? "\uE73E" : "\uE8FD",
|
||||
branch,
|
||||
isCurrent ? "현재 브랜치" : "최근 사용 브랜치",
|
||||
isCurrent,
|
||||
accentBrush,
|
||||
secondaryText,
|
||||
primaryText,
|
||||
isCurrent ? null : () => _ = SwitchGitBranchAsync(branch)));
|
||||
}
|
||||
}
|
||||
|
||||
if (_currentGitBranches.Count > 0)
|
||||
{
|
||||
var branchSectionLabel = string.IsNullOrWhiteSpace(query)
|
||||
? $"브랜치 전환 · {_currentGitBranches.Count}"
|
||||
: $"브랜치 전환 · {filteredBranches.Count}/{_currentGitBranches.Count}";
|
||||
GitBranchItems.Children.Add(CreatePopupSectionLabel(branchSectionLabel, new Thickness(8, 10, 8, 4)));
|
||||
|
||||
foreach (var branch in filteredBranches)
|
||||
{
|
||||
if (recentBranches.Any(recent => string.Equals(recent, branch, StringComparison.OrdinalIgnoreCase)))
|
||||
continue;
|
||||
|
||||
var isCurrent = string.Equals(branch, branchName, StringComparison.OrdinalIgnoreCase);
|
||||
GitBranchItems.Children.Add(CreatePopupMenuRow(
|
||||
isCurrent ? "\uE73E" : "\uE943",
|
||||
branch,
|
||||
isCurrent ? "현재 브랜치" : "이 브랜치로 전환",
|
||||
isCurrent,
|
||||
accentBrush,
|
||||
secondaryText,
|
||||
primaryText,
|
||||
isCurrent ? null : () => _ = SwitchGitBranchAsync(branch)));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query) && filteredBranches.Count == 0)
|
||||
{
|
||||
GitBranchItems.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "검색 결과가 없습니다.",
|
||||
FontSize = 11.5,
|
||||
Foreground = secondaryText,
|
||||
Margin = new Thickness(10, 6, 10, 10),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
GitBranchItems.Children.Add(CreatePopupSectionLabel("브랜치 작업", new Thickness(8, 10, 8, 4)));
|
||||
GitBranchItems.Children.Add(CreatePopupMenuRow(
|
||||
"\uE710",
|
||||
"새 브랜치 생성",
|
||||
"현재 작업 기준으로 새 브랜치를 만들고 전환합니다.",
|
||||
false,
|
||||
accentBrush,
|
||||
secondaryText,
|
||||
primaryText,
|
||||
() => _ = CreateGitBranchAsync()));
|
||||
}
|
||||
|
||||
private TextBlock CreatePopupSectionLabel(string text, Thickness? margin = null)
|
||||
{
|
||||
return new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
FontSize = 10.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
Margin = margin ?? new Thickness(8, 8, 8, 4),
|
||||
};
|
||||
}
|
||||
|
||||
private UIElement CreatePopupSummaryStrip(IEnumerable<(string Label, string Value, string BgHex, string BorderHex, string FgHex)> items)
|
||||
{
|
||||
var wrap = new WrapPanel
|
||||
{
|
||||
Margin = new Thickness(8, 6, 8, 6),
|
||||
};
|
||||
|
||||
foreach (var item in items)
|
||||
wrap.Children.Add(CreateMetricPill($"{item.Label} {item.Value}", item.FgHex, item.BgHex, item.BorderHex));
|
||||
|
||||
return wrap;
|
||||
}
|
||||
|
||||
private Border CreateMetricPill(string text, string colorHex)
|
||||
=> CreateMetricPill(text, colorHex, $"{colorHex}18", $"{colorHex}44");
|
||||
|
||||
private Border CreateMetricPill(string text, string colorHex, string bgHex, string borderHex)
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Background = BrushFromHex(bgHex),
|
||||
BorderBrush = BrushFromHex(borderHex),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Padding = new Thickness(8, 3, 8, 3),
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
FontSize = 10.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = BrushFromHex(colorHex),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Border CreateFlatPopupRow(string icon, string title, string description, string colorHex, bool clickable, Action? onClick)
|
||||
{
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var hoverBrush = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.LightGray;
|
||||
var borderColor = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
|
||||
var border = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = borderColor,
|
||||
BorderThickness = new Thickness(0, 0, 0, 1),
|
||||
Padding = new Thickness(8, 9, 8, 9),
|
||||
Cursor = clickable ? Cursors.Hand : Cursors.Arrow,
|
||||
Focusable = clickable,
|
||||
};
|
||||
KeyboardNavigation.SetIsTabStop(border, clickable);
|
||||
|
||||
var grid = new Grid();
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
grid.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 11,
|
||||
Foreground = BrushFromHex(colorHex),
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
Margin = new Thickness(0, 1, 10, 0),
|
||||
});
|
||||
|
||||
var textStack = new StackPanel();
|
||||
textStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = title,
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
});
|
||||
if (!string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
textStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = description,
|
||||
FontSize = 10.5,
|
||||
Foreground = secondaryText,
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
}
|
||||
|
||||
Grid.SetColumn(textStack, 1);
|
||||
grid.Children.Add(textStack);
|
||||
if (clickable)
|
||||
{
|
||||
var chevron = new TextBlock
|
||||
{
|
||||
Text = "\uE76C",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 10,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(8, 0, 0, 0),
|
||||
};
|
||||
Grid.SetColumn(chevron, 2);
|
||||
grid.Children.Add(chevron);
|
||||
}
|
||||
|
||||
border.Child = grid;
|
||||
|
||||
if (clickable && onClick != null)
|
||||
{
|
||||
border.MouseEnter += (_, _) => border.Background = hoverBrush;
|
||||
border.MouseLeave += (_, _) => border.Background = Brushes.Transparent;
|
||||
border.MouseLeftButtonUp += (_, _) => onClick();
|
||||
border.KeyDown += (_, keyEvent) =>
|
||||
{
|
||||
if (keyEvent.Key is Key.Enter or Key.Space)
|
||||
{
|
||||
keyEvent.Handled = true;
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return border;
|
||||
}
|
||||
}
|
||||
330
src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs
Normal file
330
src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private void AddMessageBubble(string role, string content, bool animate = true, ChatMessage? message = null)
|
||||
{
|
||||
var isUser = role == "user";
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var itemBg = TryFindResource("ItemBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40));
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var hintBg = TryFindResource("HintBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var userBubbleBg = hintBg;
|
||||
var assistantBubbleBg = itemBg;
|
||||
|
||||
if (isUser)
|
||||
{
|
||||
var msgMaxWidth = GetMessageMaxWidth();
|
||||
var wrapper = new StackPanel
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Width = msgMaxWidth,
|
||||
MaxWidth = msgMaxWidth,
|
||||
Margin = new Thickness(0, 4, 0, 6),
|
||||
};
|
||||
|
||||
var bubble = new Border
|
||||
{
|
||||
Background = userBubbleBg,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(9),
|
||||
Padding = new Thickness(11, 7, 11, 7),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
};
|
||||
|
||||
if (string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var userCodeBgBrush = TryFindResource("HintBackground") as Brush ?? Brushes.DarkGray;
|
||||
MarkdownRenderer.EnableFilePathHighlight =
|
||||
(System.Windows.Application.Current as App)?.SettingsService?.Settings.Llm.EnableFilePathHighlight ?? true;
|
||||
bubble.Child = MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, userCodeBgBrush);
|
||||
}
|
||||
else
|
||||
{
|
||||
bubble.Child = new TextBlock
|
||||
{
|
||||
Text = content,
|
||||
TextAlignment = TextAlignment.Left,
|
||||
FontSize = 12,
|
||||
Foreground = primaryText,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
LineHeight = 18,
|
||||
};
|
||||
}
|
||||
|
||||
wrapper.Children.Add(bubble);
|
||||
|
||||
var userActionBar = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Opacity = 0.8,
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
};
|
||||
var capturedUserContent = content;
|
||||
var userBtnColor = secondaryText;
|
||||
userActionBar.Children.Add(CreateActionButton("\uE8C8", "복사", userBtnColor, () =>
|
||||
{
|
||||
try { Clipboard.SetText(capturedUserContent); } catch { }
|
||||
}));
|
||||
userActionBar.Children.Add(CreateActionButton("\uE70F", "편집", userBtnColor,
|
||||
() => EnterEditMode(wrapper, capturedUserContent)));
|
||||
|
||||
var userBottomBar = new Grid { Margin = new Thickness(0, 1, 0, 0) };
|
||||
var timestamp = message?.Timestamp ?? DateTime.Now;
|
||||
userBottomBar.Children.Add(new TextBlock
|
||||
{
|
||||
Text = timestamp.ToString("HH:mm"),
|
||||
FontSize = 10.5,
|
||||
Opacity = 0.52,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 2, 1),
|
||||
});
|
||||
userBottomBar.Children.Add(userActionBar);
|
||||
wrapper.Children.Add(userBottomBar);
|
||||
wrapper.MouseEnter += (_, _) => userActionBar.Opacity = 1;
|
||||
wrapper.MouseLeave += (_, _) => userActionBar.Opacity = ReferenceEquals(_selectedMessageActionBar, userActionBar) ? 1 : 0.8;
|
||||
wrapper.MouseLeftButtonUp += (_, _) => SelectMessageActionBar(userActionBar, bubble);
|
||||
|
||||
var userContent = content;
|
||||
wrapper.MouseRightButtonUp += (_, re) =>
|
||||
{
|
||||
re.Handled = true;
|
||||
ShowMessageContextMenu(userContent, "user");
|
||||
};
|
||||
|
||||
if (animate)
|
||||
ApplyMessageEntryAnimation(wrapper);
|
||||
MessagePanel.Children.Add(wrapper);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message != null && IsCompactionMetaMessage(message))
|
||||
{
|
||||
var compactCard = CreateCompactionMetaCard(message, primaryText, secondaryText, hintBg, borderBrush, accentBrush);
|
||||
if (animate)
|
||||
ApplyMessageEntryAnimation(compactCard);
|
||||
MessagePanel.Children.Add(compactCard);
|
||||
return;
|
||||
}
|
||||
|
||||
var assistantMaxWidth = GetMessageMaxWidth();
|
||||
var container = new StackPanel
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Width = assistantMaxWidth,
|
||||
MaxWidth = assistantMaxWidth,
|
||||
Margin = new Thickness(0, 6, 0, 6),
|
||||
};
|
||||
if (animate)
|
||||
ApplyMessageEntryAnimation(container);
|
||||
|
||||
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(new TextBlock
|
||||
{
|
||||
Text = agentName,
|
||||
FontSize = 11.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = secondaryText,
|
||||
Margin = new Thickness(4, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
container.Children.Add(header);
|
||||
|
||||
var contentCard = new Border
|
||||
{
|
||||
Background = assistantBubbleBg,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(9),
|
||||
Padding = new Thickness(11, 8, 11, 8),
|
||||
};
|
||||
var contentStack = new StackPanel();
|
||||
|
||||
var app = System.Windows.Application.Current as App;
|
||||
MarkdownRenderer.EnableFilePathHighlight =
|
||||
app?.SettingsService?.Settings.Llm.EnableFilePathHighlight ?? true;
|
||||
var codeBgBrush = TryFindResource("HintBackground") as Brush ?? Brushes.DarkGray;
|
||||
if (IsBranchContextMessage(content))
|
||||
{
|
||||
var branchRun = GetAgentRunStateById(message?.MetaRunId) ?? GetLatestBranchContextRun();
|
||||
var branchFiles = GetBranchContextFilePaths(message?.MetaRunId ?? branchRun?.RunId, 3);
|
||||
var branchCard = new Border
|
||||
{
|
||||
Background = hintBg,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(12, 10, 12, 10),
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
};
|
||||
var branchStack = new StackPanel();
|
||||
branchStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "분기 컨텍스트",
|
||||
FontSize = 11,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
});
|
||||
branchStack.Children.Add(MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, codeBgBrush));
|
||||
|
||||
if (branchFiles.Count > 0)
|
||||
{
|
||||
var filesWrap = new WrapPanel { Margin = new Thickness(0, 8, 0, 0) };
|
||||
foreach (var path in branchFiles)
|
||||
{
|
||||
var fileButton = new Button
|
||||
{
|
||||
Background = itemBg,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(8, 3, 8, 3),
|
||||
Margin = new Thickness(0, 0, 6, 6),
|
||||
Cursor = Cursors.Hand,
|
||||
ToolTip = path,
|
||||
Content = new TextBlock
|
||||
{
|
||||
Text = System.IO.Path.GetFileName(path),
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
},
|
||||
};
|
||||
var capturedPath = path;
|
||||
fileButton.Click += (_, _) => OpenRunFilePath(capturedPath);
|
||||
filesWrap.Children.Add(fileButton);
|
||||
}
|
||||
branchStack.Children.Add(filesWrap);
|
||||
}
|
||||
|
||||
if (branchRun != null)
|
||||
{
|
||||
var actionsWrap = new WrapPanel { Margin = new Thickness(0, 8, 0, 0) };
|
||||
|
||||
var followUpButton = new Button
|
||||
{
|
||||
Background = itemBg,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(8, 3, 8, 3),
|
||||
Margin = new Thickness(0, 0, 6, 6),
|
||||
Cursor = Cursors.Hand,
|
||||
Content = new TextBlock
|
||||
{
|
||||
Text = "후속 작업 큐에 넣기",
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
},
|
||||
};
|
||||
var capturedBranchRun = branchRun;
|
||||
followUpButton.Click += (_, _) => EnqueueFollowUpFromRun(capturedBranchRun);
|
||||
actionsWrap.Children.Add(followUpButton);
|
||||
|
||||
var timelineButton = new Button
|
||||
{
|
||||
Background = itemBg,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(8, 3, 8, 3),
|
||||
Margin = new Thickness(0, 0, 6, 6),
|
||||
Cursor = Cursors.Hand,
|
||||
Content = new TextBlock
|
||||
{
|
||||
Text = "관련 로그로 이동",
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
},
|
||||
};
|
||||
timelineButton.Click += (_, _) => ScrollToRunInTimeline(capturedBranchRun.RunId);
|
||||
actionsWrap.Children.Add(timelineButton);
|
||||
|
||||
branchStack.Children.Add(actionsWrap);
|
||||
}
|
||||
|
||||
branchCard.Child = branchStack;
|
||||
contentStack.Children.Add(branchCard);
|
||||
}
|
||||
else
|
||||
{
|
||||
contentStack.Children.Add(MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, codeBgBrush));
|
||||
}
|
||||
|
||||
contentCard.Child = contentStack;
|
||||
container.Children.Add(contentCard);
|
||||
|
||||
var actionBar = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
Margin = new Thickness(2, 2, 0, 0),
|
||||
Opacity = 0.8,
|
||||
};
|
||||
var btnColor = secondaryText;
|
||||
var capturedContent = content;
|
||||
|
||||
actionBar.Children.Add(CreateActionButton("\uE8C8", "복사", btnColor, () =>
|
||||
{
|
||||
try { Clipboard.SetText(capturedContent); } catch { }
|
||||
}));
|
||||
actionBar.Children.Add(CreateActionButton("\uE72C", "다시 생성", btnColor, () => _ = RegenerateLastAsync()));
|
||||
actionBar.Children.Add(CreateActionButton("\uE70F", "수정 후 재시도", btnColor, () => ShowRetryWithFeedbackInput()));
|
||||
AddLinkedFeedbackButtons(actionBar, btnColor, message);
|
||||
|
||||
var aiTimestamp = message?.Timestamp ?? DateTime.Now;
|
||||
actionBar.Children.Add(new TextBlock
|
||||
{
|
||||
Text = aiTimestamp.ToString("HH:mm"),
|
||||
FontSize = 10.5,
|
||||
Opacity = 0.52,
|
||||
Foreground = btnColor,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(4, 0, 0, 1),
|
||||
});
|
||||
|
||||
container.Children.Add(actionBar);
|
||||
var assistantMeta = CreateAssistantMessageMetaText(message);
|
||||
if (assistantMeta != null)
|
||||
container.Children.Add(assistantMeta);
|
||||
|
||||
container.MouseEnter += (_, _) => actionBar.Opacity = 1;
|
||||
container.MouseLeave += (_, _) => actionBar.Opacity = ReferenceEquals(_selectedMessageActionBar, actionBar) ? 1 : 0.8;
|
||||
container.MouseLeftButtonUp += (_, _) => SelectMessageActionBar(actionBar, contentCard);
|
||||
|
||||
var aiContent = content;
|
||||
container.MouseRightButtonUp += (_, re) =>
|
||||
{
|
||||
re.Handled = true;
|
||||
ShowMessageContextMenu(aiContent, "assistant");
|
||||
};
|
||||
|
||||
MessagePanel.Children.Add(container);
|
||||
}
|
||||
}
|
||||
394
src/AxCopilot/Views/ChatWindow.MessageInteractions.cs
Normal file
394
src/AxCopilot/Views/ChatWindow.MessageInteractions.cs
Normal file
@@ -0,0 +1,394 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
/// <summary>좋아요/싫어요 토글 피드백 버튼 (상태 영구 저장)</summary>
|
||||
private Button CreateFeedbackButton(string outline, string filled, string tooltip,
|
||||
Brush normalColor, Brush activeColor, ChatMessage? message = null, string feedbackType = "",
|
||||
Action? resetSibling = null, Action<Action>? registerReset = null)
|
||||
{
|
||||
var hoverBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var isActive = message?.Feedback == feedbackType;
|
||||
var icon = new TextBlock
|
||||
{
|
||||
Text = isActive ? filled : outline,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12,
|
||||
Foreground = isActive ? activeColor : normalColor,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
RenderTransformOrigin = new Point(0.5, 0.5),
|
||||
RenderTransform = new ScaleTransform(1, 1)
|
||||
};
|
||||
var btn = new Button
|
||||
{
|
||||
Content = icon,
|
||||
Background = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
Cursor = Cursors.Hand,
|
||||
Padding = new Thickness(6, 4, 6, 4),
|
||||
Margin = new Thickness(0, 0, 4, 0),
|
||||
ToolTip = tooltip
|
||||
};
|
||||
|
||||
registerReset?.Invoke(() =>
|
||||
{
|
||||
isActive = false;
|
||||
icon.Text = outline;
|
||||
icon.Foreground = normalColor;
|
||||
});
|
||||
|
||||
btn.MouseEnter += (_, _) => { if (!isActive) icon.Foreground = hoverBrush; };
|
||||
btn.MouseLeave += (_, _) => { if (!isActive) icon.Foreground = normalColor; };
|
||||
btn.Click += (_, _) =>
|
||||
{
|
||||
isActive = !isActive;
|
||||
icon.Text = isActive ? filled : outline;
|
||||
icon.Foreground = isActive ? activeColor : normalColor;
|
||||
|
||||
if (isActive)
|
||||
{
|
||||
resetSibling?.Invoke();
|
||||
}
|
||||
|
||||
if (message != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var feedback = isActive ? feedbackType : null;
|
||||
var session = ChatSession;
|
||||
if (session != null)
|
||||
{
|
||||
lock (_convLock)
|
||||
{
|
||||
session.UpdateMessageFeedback(_activeTab, message, feedback, _storage);
|
||||
_currentConversation = session.CurrentConversation;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
message.Feedback = feedback;
|
||||
ChatConversation? conv;
|
||||
lock (_convLock)
|
||||
{
|
||||
conv = _currentConversation;
|
||||
}
|
||||
|
||||
if (conv != null)
|
||||
{
|
||||
_storage.Save(conv);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
var scale = (ScaleTransform)icon.RenderTransform;
|
||||
var bounce = new DoubleAnimation(1.3, 1.0, TimeSpan.FromMilliseconds(250))
|
||||
{
|
||||
EasingFunction = new ElasticEase
|
||||
{
|
||||
EasingMode = EasingMode.EaseOut,
|
||||
Oscillations = 1,
|
||||
Springiness = 5
|
||||
}
|
||||
};
|
||||
scale.BeginAnimation(ScaleTransform.ScaleXProperty, bounce);
|
||||
scale.BeginAnimation(ScaleTransform.ScaleYProperty, bounce);
|
||||
};
|
||||
return btn;
|
||||
}
|
||||
|
||||
/// <summary>좋아요/싫어요 버튼을 상호 배타로 연결하여 추가</summary>
|
||||
private void AddLinkedFeedbackButtons(StackPanel actionBar, Brush btnColor, ChatMessage? message)
|
||||
{
|
||||
Action? resetLikeAction = null;
|
||||
Action? resetDislikeAction = null;
|
||||
|
||||
var likeBtn = CreateFeedbackButton("\uE8E1", "\uEB51", "좋아요", btnColor,
|
||||
new SolidColorBrush(Color.FromRgb(0x38, 0xA1, 0x69)), message, "like",
|
||||
resetSibling: () => resetDislikeAction?.Invoke(),
|
||||
registerReset: reset => resetLikeAction = reset);
|
||||
var dislikeBtn = CreateFeedbackButton("\uE8E0", "\uEB50", "싫어요", btnColor,
|
||||
new SolidColorBrush(Color.FromRgb(0xE5, 0x3E, 0x3E)), message, "dislike",
|
||||
resetSibling: () => resetLikeAction?.Invoke(),
|
||||
registerReset: reset => resetDislikeAction = reset);
|
||||
|
||||
actionBar.Children.Add(likeBtn);
|
||||
actionBar.Children.Add(dislikeBtn);
|
||||
}
|
||||
|
||||
private TextBlock? CreateAssistantMessageMetaText(ChatMessage? message)
|
||||
{
|
||||
if (message == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var parts = new List<string>();
|
||||
if (message.ResponseElapsedMs is > 0)
|
||||
{
|
||||
var elapsedMs = message.ResponseElapsedMs.Value;
|
||||
parts.Add(elapsedMs < 1000
|
||||
? $"{elapsedMs}ms"
|
||||
: $"{(elapsedMs / 1000.0):0.0}s");
|
||||
}
|
||||
|
||||
if (message.PromptTokens > 0 || message.CompletionTokens > 0)
|
||||
{
|
||||
var totalTokens = message.PromptTokens + message.CompletionTokens;
|
||||
parts.Add($"{FormatTokenCount(totalTokens)} tokens");
|
||||
}
|
||||
|
||||
if (parts.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new TextBlock
|
||||
{
|
||||
Text = string.Join(" · ", parts),
|
||||
FontSize = 9.75,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
Margin = new Thickness(4, 2, 0, 0),
|
||||
Opacity = 0.72,
|
||||
};
|
||||
}
|
||||
|
||||
private static void ApplyMessageEntryAnimation(FrameworkElement element)
|
||||
{
|
||||
element.Opacity = 0;
|
||||
element.RenderTransform = new TranslateTransform(0, 16);
|
||||
element.BeginAnimation(UIElement.OpacityProperty,
|
||||
new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(350))
|
||||
{
|
||||
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
|
||||
});
|
||||
((TranslateTransform)element.RenderTransform).BeginAnimation(
|
||||
TranslateTransform.YProperty,
|
||||
new DoubleAnimation(16, 0, TimeSpan.FromMilliseconds(400))
|
||||
{
|
||||
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
|
||||
});
|
||||
}
|
||||
|
||||
private bool _isEditing;
|
||||
|
||||
private void EnterEditMode(StackPanel wrapper, string originalText)
|
||||
{
|
||||
if (_isStreaming || _isEditing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isEditing = true;
|
||||
var idx = MessagePanel.Children.IndexOf(wrapper);
|
||||
if (idx < 0)
|
||||
{
|
||||
_isEditing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var editPanel = new StackPanel
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
MaxWidth = 540,
|
||||
Margin = wrapper.Margin,
|
||||
};
|
||||
|
||||
var editBox = new TextBox
|
||||
{
|
||||
Text = originalText,
|
||||
FontSize = 13.5,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.DarkGray,
|
||||
CaretBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue,
|
||||
BorderBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue,
|
||||
BorderThickness = new Thickness(1.5),
|
||||
Padding = new Thickness(14, 10, 14, 10),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
AcceptsReturn = false,
|
||||
MaxHeight = 200,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
};
|
||||
var editBorder = new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(14),
|
||||
Child = editBox,
|
||||
ClipToBounds = true,
|
||||
};
|
||||
editPanel.Children.Add(editBorder);
|
||||
|
||||
var btnBar = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(0, 6, 0, 0),
|
||||
};
|
||||
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||||
var secondaryBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
|
||||
var cancelBtn = new Button
|
||||
{
|
||||
Content = new TextBlock { Text = "취소", FontSize = 12, Foreground = secondaryBrush },
|
||||
Background = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
Cursor = Cursors.Hand,
|
||||
Padding = new Thickness(12, 5, 12, 5),
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
};
|
||||
cancelBtn.Click += (_, _) =>
|
||||
{
|
||||
_isEditing = false;
|
||||
if (idx >= 0 && idx < MessagePanel.Children.Count)
|
||||
{
|
||||
MessagePanel.Children[idx] = wrapper;
|
||||
}
|
||||
};
|
||||
btnBar.Children.Add(cancelBtn);
|
||||
|
||||
var sendBtn = new Button
|
||||
{
|
||||
Cursor = Cursors.Hand,
|
||||
Padding = new Thickness(0),
|
||||
};
|
||||
sendBtn.Template = (ControlTemplate)System.Windows.Markup.XamlReader.Parse(
|
||||
"<ControlTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' TargetType='Button'>" +
|
||||
"<Border Background='" + (accentBrush is SolidColorBrush sb ? sb.Color.ToString() : "#4B5EFC") + "' CornerRadius='8' Padding='12,5,12,5'>" +
|
||||
"<TextBlock Text='전송' FontSize='12' Foreground='White' HorizontalAlignment='Center'/>" +
|
||||
"</Border></ControlTemplate>");
|
||||
sendBtn.Click += (_, _) =>
|
||||
{
|
||||
var newText = editBox.Text.Trim();
|
||||
if (!string.IsNullOrEmpty(newText))
|
||||
{
|
||||
_ = SubmitEditAsync(idx, newText);
|
||||
}
|
||||
};
|
||||
btnBar.Children.Add(sendBtn);
|
||||
|
||||
editPanel.Children.Add(btnBar);
|
||||
MessagePanel.Children[idx] = editPanel;
|
||||
|
||||
editBox.KeyDown += (_, ke) =>
|
||||
{
|
||||
if (ke.Key == Key.Enter && Keyboard.Modifiers == ModifierKeys.None)
|
||||
{
|
||||
ke.Handled = true;
|
||||
var newText = editBox.Text.Trim();
|
||||
if (!string.IsNullOrEmpty(newText))
|
||||
{
|
||||
_ = SubmitEditAsync(idx, newText);
|
||||
}
|
||||
}
|
||||
|
||||
if (ke.Key == Key.Escape)
|
||||
{
|
||||
ke.Handled = true;
|
||||
_isEditing = false;
|
||||
if (idx >= 0 && idx < MessagePanel.Children.Count)
|
||||
{
|
||||
MessagePanel.Children[idx] = wrapper;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
editBox.Focus();
|
||||
editBox.SelectAll();
|
||||
}
|
||||
|
||||
private async Task SubmitEditAsync(int bubbleIndex, string newText)
|
||||
{
|
||||
_isEditing = false;
|
||||
if (_isStreaming)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ChatConversation conv;
|
||||
lock (_convLock)
|
||||
{
|
||||
if (_currentConversation == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
conv = _currentConversation;
|
||||
}
|
||||
|
||||
var userMsgIdx = -1;
|
||||
var uiIdx = 0;
|
||||
lock (_convLock)
|
||||
{
|
||||
for (var i = 0; i < conv.Messages.Count; i++)
|
||||
{
|
||||
if (conv.Messages[i].Role == "system")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (uiIdx == bubbleIndex)
|
||||
{
|
||||
userMsgIdx = i;
|
||||
break;
|
||||
}
|
||||
|
||||
uiIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
if (userMsgIdx < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_convLock)
|
||||
{
|
||||
var session = ChatSession;
|
||||
if (session != null)
|
||||
{
|
||||
session.UpdateUserMessageAndTrim(_activeTab, userMsgIdx, newText, _storage);
|
||||
_currentConversation = session.CurrentConversation;
|
||||
conv = _currentConversation!;
|
||||
}
|
||||
else
|
||||
{
|
||||
conv.Messages[userMsgIdx].Content = newText;
|
||||
while (conv.Messages.Count > userMsgIdx + 1)
|
||||
{
|
||||
conv.Messages.RemoveAt(conv.Messages.Count - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderMessages(preserveViewport: true);
|
||||
AutoScrollIfNeeded();
|
||||
|
||||
await SendRegenerateAsync(conv);
|
||||
try
|
||||
{
|
||||
if (ChatSession == null)
|
||||
{
|
||||
_storage.Save(conv);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Services.LogService.Debug($"대화 저장 실패: {ex.Message}");
|
||||
}
|
||||
|
||||
RefreshConversationList();
|
||||
}
|
||||
}
|
||||
310
src/AxCopilot/Views/ChatWindow.PermissionPresentation.cs
Normal file
310
src/AxCopilot/Views/ChatWindow.PermissionPresentation.cs
Normal file
@@ -0,0 +1,310 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private void BtnPermission_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (PermissionPopup == null) return;
|
||||
PermissionItems.Children.Clear();
|
||||
|
||||
ChatConversation? currentConversation;
|
||||
lock (_convLock) currentConversation = _currentConversation;
|
||||
var coreLevels = PermissionModePresentationCatalog.Ordered.ToList();
|
||||
var current = PermissionModeCatalog.NormalizeGlobalMode(_settings.Settings.Llm.FilePermission);
|
||||
|
||||
void AddPermissionRows(Panel container, IEnumerable<PermissionModePresentation> levels)
|
||||
{
|
||||
var hoverBackground = TryFindResource("ItemHoverBackground") as Brush ?? BrushFromHex("#F8FAFC");
|
||||
var selectedBackground = TryFindResource("HintBackground") as Brush ?? BrushFromHex("#F8FAFC");
|
||||
var selectedBorder = TryFindResource("AccentColor") as Brush ?? BrushFromHex("#D6E4FF");
|
||||
foreach (var item in levels)
|
||||
{
|
||||
var level = item.Mode;
|
||||
var isActive = level.Equals(current, StringComparison.OrdinalIgnoreCase);
|
||||
var rowBorder = new Border
|
||||
{
|
||||
Background = isActive ? selectedBackground : Brushes.Transparent,
|
||||
BorderBrush = isActive ? selectedBorder : Brushes.Transparent,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(10, 10, 10, 10),
|
||||
Margin = new Thickness(0, 0, 0, 4),
|
||||
Cursor = Cursors.Hand,
|
||||
Focusable = true,
|
||||
};
|
||||
KeyboardNavigation.SetIsTabStop(rowBorder, true);
|
||||
|
||||
var row = new Grid();
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
row.Children.Add(new TextBlock
|
||||
{
|
||||
Text = item.Icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 15,
|
||||
Foreground = BrushFromHex(item.ColorHex),
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
|
||||
var textStack = new StackPanel();
|
||||
textStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = item.Title,
|
||||
FontSize = 13.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||
});
|
||||
textStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = item.Description,
|
||||
FontSize = 11.5,
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
LineHeight = 16,
|
||||
MaxWidth = 220,
|
||||
});
|
||||
Grid.SetColumn(textStack, 1);
|
||||
row.Children.Add(textStack);
|
||||
|
||||
var check = new TextBlock
|
||||
{
|
||||
Text = isActive ? "\uE73E" : "",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Foreground = BrushFromHex("#2563EB"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(12, 0, 0, 0),
|
||||
};
|
||||
Grid.SetColumn(check, 2);
|
||||
row.Children.Add(check);
|
||||
|
||||
rowBorder.Child = row;
|
||||
rowBorder.MouseEnter += (_, _) => rowBorder.Background = isActive ? selectedBackground : hoverBackground;
|
||||
rowBorder.MouseLeave += (_, _) => rowBorder.Background = isActive ? selectedBackground : Brushes.Transparent;
|
||||
|
||||
var capturedLevel = level;
|
||||
void ApplyPermission()
|
||||
{
|
||||
_settings.Settings.Llm.FilePermission = PermissionModeCatalog.NormalizeGlobalMode(capturedLevel);
|
||||
try { _settings.Save(); } catch { }
|
||||
_appState.LoadFromSettings(_settings);
|
||||
UpdatePermissionUI();
|
||||
SaveConversationSettings();
|
||||
RefreshInlineSettingsPanel();
|
||||
RefreshOverlayModeButtons();
|
||||
PermissionPopup.IsOpen = false;
|
||||
}
|
||||
|
||||
rowBorder.MouseLeftButtonDown += (_, _) => ApplyPermission();
|
||||
rowBorder.KeyDown += (_, ke) =>
|
||||
{
|
||||
if (ke.Key is Key.Enter or Key.Space)
|
||||
{
|
||||
ke.Handled = true;
|
||||
ApplyPermission();
|
||||
}
|
||||
};
|
||||
|
||||
container.Children.Add(rowBorder);
|
||||
}
|
||||
}
|
||||
|
||||
AddPermissionRows(PermissionItems, coreLevels);
|
||||
|
||||
PermissionPopup.IsOpen = true;
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
TryFocusFirstPermissionElement(PermissionItems);
|
||||
}, System.Windows.Threading.DispatcherPriority.Input);
|
||||
}
|
||||
|
||||
private static bool TryFocusFirstPermissionElement(DependencyObject root)
|
||||
{
|
||||
if (root is UIElement ui && ui.Focusable && ui.IsEnabled && ui.Visibility == Visibility.Visible)
|
||||
return ui.Focus();
|
||||
|
||||
var childCount = VisualTreeHelper.GetChildrenCount(root);
|
||||
for (var i = 0; i < childCount; i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(root, i);
|
||||
if (TryFocusFirstPermissionElement(child))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void SetToolPermissionOverride(string toolName, string? mode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(toolName)) return;
|
||||
var toolPermissions = _settings.Settings.Llm.ToolPermissions ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var existingKey = toolPermissions.Keys.FirstOrDefault(x => string.Equals(x, toolName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(mode))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(existingKey))
|
||||
toolPermissions.Remove(existingKey!);
|
||||
}
|
||||
else
|
||||
{
|
||||
toolPermissions[existingKey ?? toolName] = PermissionModeCatalog.NormalizeToolOverride(mode);
|
||||
}
|
||||
|
||||
try { _settings.Save(); } catch { }
|
||||
_appState.LoadFromSettings(_settings);
|
||||
UpdatePermissionUI();
|
||||
SaveConversationSettings();
|
||||
}
|
||||
|
||||
private void RefreshPermissionPopup()
|
||||
{
|
||||
if (PermissionPopup == null) return;
|
||||
BtnPermission_Click(this, new RoutedEventArgs());
|
||||
}
|
||||
|
||||
private bool GetPermissionPopupSectionExpanded(string sectionKey, bool defaultValue = false)
|
||||
{
|
||||
var map = _settings.Settings.Llm.PermissionPopupSections;
|
||||
if (map != null && map.TryGetValue(sectionKey, out var expanded))
|
||||
return expanded;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private void SetPermissionPopupSectionExpanded(string sectionKey, bool expanded)
|
||||
{
|
||||
var map = _settings.Settings.Llm.PermissionPopupSections ??= new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||
map[sectionKey] = expanded;
|
||||
try { _settings.Save(); } catch { }
|
||||
}
|
||||
|
||||
private void BtnPermissionTopBannerClose_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (PermissionTopBanner != null)
|
||||
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void UpdatePermissionUI()
|
||||
{
|
||||
if (PermissionLabel == null || PermissionIcon == null) return;
|
||||
ChatConversation? currentConversation;
|
||||
lock (_convLock) currentConversation = _currentConversation;
|
||||
var summary = _appState.GetPermissionSummary(currentConversation);
|
||||
var perm = PermissionModeCatalog.NormalizeGlobalMode(summary.EffectiveMode);
|
||||
PermissionLabel.Text = PermissionModeCatalog.ToDisplayLabel(perm);
|
||||
PermissionIcon.Text = perm switch
|
||||
{
|
||||
"AcceptEdits" => "\uE73E",
|
||||
"BypassPermissions" => "\uE7BA",
|
||||
"Deny" => "\uE711",
|
||||
_ => "\uE8D7",
|
||||
};
|
||||
if (BtnPermission != null)
|
||||
{
|
||||
var operationMode = OperationModePolicy.Normalize(_settings.Settings.OperationMode);
|
||||
BtnPermission.ToolTip = $"{summary.Description}\n운영 모드: {operationMode}\n기본값: {PermissionModeCatalog.ToDisplayLabel(summary.DefaultMode)} · 예외 {summary.OverrideCount}개";
|
||||
BtnPermission.Background = Brushes.Transparent;
|
||||
BtnPermission.BorderThickness = new Thickness(1);
|
||||
}
|
||||
|
||||
if (!string.Equals(_lastPermissionBannerMode, perm, StringComparison.OrdinalIgnoreCase))
|
||||
_lastPermissionBannerMode = perm;
|
||||
|
||||
if (perm == PermissionModeCatalog.AcceptEdits)
|
||||
{
|
||||
var activeColor = new SolidColorBrush(Color.FromRgb(0x10, 0x7C, 0x10));
|
||||
PermissionLabel.Foreground = activeColor;
|
||||
PermissionIcon.Foreground = activeColor;
|
||||
if (BtnPermission != null)
|
||||
BtnPermission.BorderBrush = BrushFromHex("#86EFAC");
|
||||
if (PermissionTopBanner != null)
|
||||
{
|
||||
PermissionTopBanner.BorderBrush = BrushFromHex("#86EFAC");
|
||||
PermissionTopBannerIcon.Text = "\uE73E";
|
||||
PermissionTopBannerIcon.Foreground = activeColor;
|
||||
PermissionTopBannerTitle.Text = "현재 권한 모드 · 편집 자동 승인";
|
||||
PermissionTopBannerTitle.Foreground = BrushFromHex("#166534");
|
||||
PermissionTopBannerText.Text = "모든 파일 편집은 자동 승인하고, 명령 실행만 계속 확인합니다.";
|
||||
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
else if (perm == PermissionModeCatalog.Deny)
|
||||
{
|
||||
var denyColor = new SolidColorBrush(Color.FromRgb(0x10, 0x7C, 0x10));
|
||||
PermissionLabel.Foreground = denyColor;
|
||||
PermissionIcon.Foreground = denyColor;
|
||||
if (BtnPermission != null)
|
||||
BtnPermission.BorderBrush = BrushFromHex("#86EFAC");
|
||||
if (PermissionTopBanner != null)
|
||||
{
|
||||
PermissionTopBanner.BorderBrush = BrushFromHex("#86EFAC");
|
||||
PermissionTopBannerIcon.Text = "\uE73E";
|
||||
PermissionTopBannerIcon.Foreground = denyColor;
|
||||
PermissionTopBannerTitle.Text = "현재 권한 모드 · 읽기 전용";
|
||||
PermissionTopBannerTitle.Foreground = denyColor;
|
||||
PermissionTopBannerText.Text = "파일 읽기만 허용하고 생성, 수정, 삭제는 차단합니다.";
|
||||
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
else if (perm == PermissionModeCatalog.BypassPermissions)
|
||||
{
|
||||
var autoColor = new SolidColorBrush(Color.FromRgb(0xC2, 0x41, 0x0C));
|
||||
PermissionLabel.Foreground = autoColor;
|
||||
PermissionIcon.Foreground = autoColor;
|
||||
if (BtnPermission != null)
|
||||
BtnPermission.BorderBrush = BrushFromHex("#FDBA74");
|
||||
if (PermissionTopBanner != null)
|
||||
{
|
||||
PermissionTopBanner.BorderBrush = BrushFromHex("#FDBA74");
|
||||
PermissionTopBannerIcon.Text = "\uE814";
|
||||
PermissionTopBannerIcon.Foreground = autoColor;
|
||||
PermissionTopBannerTitle.Text = "현재 권한 모드 · 권한 건너뛰기";
|
||||
PermissionTopBannerTitle.Foreground = autoColor;
|
||||
PermissionTopBannerText.Text = "파일 편집과 명령 실행까지 모두 자동 허용합니다. 민감한 작업 전에는 설정을 다시 확인하세요.";
|
||||
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var defaultFg = BrushFromHex("#2563EB");
|
||||
var iconFg = new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xEB));
|
||||
PermissionLabel.Foreground = defaultFg;
|
||||
PermissionIcon.Foreground = iconFg;
|
||||
if (BtnPermission != null)
|
||||
BtnPermission.BorderBrush = BrushFromHex("#BFDBFE");
|
||||
if (PermissionTopBanner != null)
|
||||
{
|
||||
if (perm == PermissionModeCatalog.Default)
|
||||
{
|
||||
PermissionTopBanner.BorderBrush = BrushFromHex("#BFDBFE");
|
||||
PermissionTopBannerIcon.Text = "\uE8D7";
|
||||
PermissionTopBannerIcon.Foreground = BrushFromHex("#1D4ED8");
|
||||
PermissionTopBannerTitle.Text = "현재 권한 모드 · 권한 요청";
|
||||
PermissionTopBannerTitle.Foreground = BrushFromHex("#1D4ED8");
|
||||
PermissionTopBannerText.Text = "변경하기 전에 항상 확인합니다.";
|
||||
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
183
src/AxCopilot/Views/ChatWindow.PlanApprovalPresentation.cs
Normal file
183
src/AxCopilot/Views/ChatWindow.PlanApprovalPresentation.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private Func<string, List<string>, Task<string?>> CreatePlanDecisionCallback()
|
||||
{
|
||||
return async (planSummary, options) =>
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string?>();
|
||||
var steps = TaskDecomposer.ExtractSteps(planSummary);
|
||||
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_pendingPlanSummary = planSummary;
|
||||
_pendingPlanSteps = steps.ToList();
|
||||
EnsurePlanViewerWindow();
|
||||
_planViewerWindow?.LoadPlan(planSummary, steps, tcs);
|
||||
ShowPlanButton(true);
|
||||
AddDecisionButtons(tcs, options);
|
||||
});
|
||||
|
||||
var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromMinutes(5)));
|
||||
if (completed != tcs.Task)
|
||||
{
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_planViewerWindow?.Hide();
|
||||
ResetPendingPlanPresentation();
|
||||
});
|
||||
return "취소";
|
||||
}
|
||||
|
||||
var result = await tcs.Task;
|
||||
var agentDecision = result;
|
||||
if (result == null)
|
||||
{
|
||||
agentDecision = _planViewerWindow?.BuildApprovedDecisionPayload(AgentLoopService.ApprovedPlanDecisionPrefix);
|
||||
}
|
||||
else if (!string.Equals(result, "취소", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(result, "확인", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.IsNullOrWhiteSpace(result))
|
||||
{
|
||||
agentDecision = $"수정 요청: {result.Trim()}";
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_planViewerWindow?.SwitchToExecutionMode();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_planViewerWindow?.Hide();
|
||||
ResetPendingPlanPresentation();
|
||||
});
|
||||
}
|
||||
|
||||
return agentDecision;
|
||||
};
|
||||
}
|
||||
|
||||
private void EnsurePlanViewerWindow()
|
||||
{
|
||||
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
|
||||
return;
|
||||
|
||||
_planViewerWindow = new PlanViewerWindow(this);
|
||||
_planViewerWindow.Closing += (_, e) =>
|
||||
{
|
||||
e.Cancel = true;
|
||||
_planViewerWindow.Hide();
|
||||
};
|
||||
}
|
||||
|
||||
private void ShowPlanButton(bool show)
|
||||
{
|
||||
if (!show)
|
||||
{
|
||||
for (int i = MoodIconPanel.Children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (MoodIconPanel.Children[i] is Border b && b.Tag?.ToString() == "PlanBtn")
|
||||
{
|
||||
if (i > 0 && MoodIconPanel.Children[i - 1] is Border sep && sep.Tag?.ToString() == "PlanSep")
|
||||
MoodIconPanel.Children.RemoveAt(i - 1);
|
||||
if (i < MoodIconPanel.Children.Count)
|
||||
MoodIconPanel.Children.RemoveAt(Math.Min(i, MoodIconPanel.Children.Count - 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var child in MoodIconPanel.Children)
|
||||
{
|
||||
if (child is Border b && b.Tag?.ToString() == "PlanBtn")
|
||||
return;
|
||||
}
|
||||
|
||||
var separator = new Border
|
||||
{
|
||||
Width = 1,
|
||||
Height = 18,
|
||||
Background = TryFindResource("SeparatorColor") as Brush ?? Brushes.Gray,
|
||||
Margin = new Thickness(4, 0, 4, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Tag = "PlanSep",
|
||||
};
|
||||
MoodIconPanel.Children.Add(separator);
|
||||
|
||||
var planBtn = CreateFolderBarButton("\uE9D2", "계획", "실행 계획 보기", "#10B981");
|
||||
planBtn.Tag = "PlanBtn";
|
||||
planBtn.MouseLeftButtonUp += (_, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
if (string.IsNullOrWhiteSpace(_pendingPlanSummary) && _pendingPlanSteps.Count == 0)
|
||||
return;
|
||||
|
||||
EnsurePlanViewerWindow();
|
||||
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_planViewerWindow.PlanText)
|
||||
|| _planViewerWindow.PlanText != (_pendingPlanSummary ?? string.Empty)
|
||||
|| !_planViewerWindow.Steps.SequenceEqual(_pendingPlanSteps))
|
||||
{
|
||||
_planViewerWindow.LoadPlanPreview(_pendingPlanSummary ?? "", _pendingPlanSteps);
|
||||
}
|
||||
_planViewerWindow.Show();
|
||||
_planViewerWindow.Activate();
|
||||
}
|
||||
};
|
||||
MoodIconPanel.Children.Add(planBtn);
|
||||
}
|
||||
|
||||
private void UpdatePlanViewerStep(AgentEvent evt)
|
||||
{
|
||||
if (_planViewerWindow == null || !IsWindowAlive(_planViewerWindow))
|
||||
return;
|
||||
|
||||
if (evt.StepCurrent > 0)
|
||||
_planViewerWindow.UpdateCurrentStep(evt.StepCurrent - 1);
|
||||
}
|
||||
|
||||
private void CompletePlanViewer()
|
||||
{
|
||||
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
|
||||
_planViewerWindow.MarkComplete();
|
||||
ResetPendingPlanPresentation();
|
||||
}
|
||||
|
||||
private void ResetPendingPlanPresentation()
|
||||
{
|
||||
_pendingPlanSummary = null;
|
||||
_pendingPlanSteps.Clear();
|
||||
ShowPlanButton(false);
|
||||
}
|
||||
|
||||
private static bool IsWindowAlive(Window? w)
|
||||
{
|
||||
if (w == null)
|
||||
return false;
|
||||
try
|
||||
{
|
||||
var _ = w.IsVisible;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
113
src/AxCopilot/Views/ChatWindow.PopupPresentation.cs
Normal file
113
src/AxCopilot/Views/ChatWindow.PopupPresentation.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private Popup? _sharedContextPopup;
|
||||
|
||||
private (Popup Popup, StackPanel Panel) CreateThemedPopupMenu(
|
||||
UIElement? placementTarget = null,
|
||||
PlacementMode placement = PlacementMode.MousePoint,
|
||||
double minWidth = 200)
|
||||
{
|
||||
_sharedContextPopup?.SetCurrentValue(Popup.IsOpenProperty, false);
|
||||
|
||||
var panel = new StackPanel { Margin = new Thickness(2) };
|
||||
var container = CreateSurfacePopupContainer(panel, minWidth, new Thickness(6));
|
||||
|
||||
var popup = new Popup
|
||||
{
|
||||
Child = container,
|
||||
StaysOpen = false,
|
||||
AllowsTransparency = true,
|
||||
PopupAnimation = PopupAnimation.Fade,
|
||||
Placement = placement,
|
||||
PlacementTarget = placementTarget,
|
||||
};
|
||||
|
||||
_sharedContextPopup = popup;
|
||||
return (popup, panel);
|
||||
}
|
||||
|
||||
private Border CreatePopupMenuItem(
|
||||
Popup popup,
|
||||
string icon,
|
||||
string label,
|
||||
Brush iconBrush,
|
||||
Brush labelBrush,
|
||||
Brush hoverBrush,
|
||||
Action action)
|
||||
{
|
||||
var item = CreateSurfacePopupMenuItem(icon, iconBrush, label, () =>
|
||||
{
|
||||
popup.SetCurrentValue(Popup.IsOpenProperty, false);
|
||||
action();
|
||||
}, labelBrush);
|
||||
item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBrush; };
|
||||
item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private static void AddPopupMenuSeparator(Panel panel, Brush brush)
|
||||
{
|
||||
panel.Children.Add(new Border
|
||||
{
|
||||
Height = 1,
|
||||
Margin = new Thickness(10, 4, 10, 4),
|
||||
Background = brush,
|
||||
Opacity = 0.35,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>최근 폴더 항목 우클릭 컨텍스트 메뉴를 표시합니다.</summary>
|
||||
private void ShowRecentFolderContextMenu(string folderPath)
|
||||
{
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
|
||||
var warningBrush = new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44));
|
||||
var (popup, panel) = CreateThemedPopupMenu();
|
||||
|
||||
panel.Children.Add(CreatePopupMenuItem(popup, "\uED25", "폴더 열기", secondaryText, primaryText, hoverBg, () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = folderPath,
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}));
|
||||
|
||||
panel.Children.Add(CreatePopupMenuItem(popup, "\uE8C8", "경로 복사", secondaryText, primaryText, hoverBg, () =>
|
||||
{
|
||||
try { Clipboard.SetText(folderPath); } catch { }
|
||||
}));
|
||||
|
||||
AddPopupMenuSeparator(panel, borderBrush);
|
||||
|
||||
panel.Children.Add(CreatePopupMenuItem(popup, "\uE74D", "목록에서 삭제", warningBrush, warningBrush, hoverBg, () =>
|
||||
{
|
||||
_settings.Settings.Llm.RecentWorkFolders.RemoveAll(
|
||||
p => p.Equals(folderPath, StringComparison.OrdinalIgnoreCase));
|
||||
_settings.Save();
|
||||
if (FolderMenuPopup.IsOpen)
|
||||
ShowFolderMenu();
|
||||
}));
|
||||
|
||||
Dispatcher.BeginInvoke(() => { popup.IsOpen = true; }, DispatcherPriority.Input);
|
||||
}
|
||||
}
|
||||
736
src/AxCopilot/Views/ChatWindow.PreviewPresentation.cs
Normal file
736
src/AxCopilot/Views/ChatWindow.PreviewPresentation.cs
Normal file
@@ -0,0 +1,736 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private static readonly HashSet<string> _previewableExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".html", ".htm", ".md", ".txt", ".csv", ".json", ".xml", ".log",
|
||||
};
|
||||
|
||||
/// <summary>열려 있는 프리뷰 탭 목록 (파일 경로 기준).</summary>
|
||||
private readonly List<string> _previewTabs = new();
|
||||
private string? _activePreviewTab;
|
||||
private bool _webViewInitialized;
|
||||
private Popup? _previewTabPopup;
|
||||
|
||||
private static readonly string WebView2DataFolder =
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"AxCopilot", "WebView2");
|
||||
|
||||
private void TryShowPreview(string filePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
|
||||
return;
|
||||
|
||||
PreviewWindow.ShowPreview(filePath, _selectedMood);
|
||||
}
|
||||
|
||||
private void ShowPreviewPanel(string filePath)
|
||||
{
|
||||
if (!_previewTabs.Contains(filePath, StringComparer.OrdinalIgnoreCase))
|
||||
_previewTabs.Add(filePath);
|
||||
|
||||
_activePreviewTab = filePath;
|
||||
|
||||
if (PreviewColumn.Width.Value < 100)
|
||||
{
|
||||
PreviewColumn.Width = new GridLength(420);
|
||||
SplitterColumn.Width = new GridLength(5);
|
||||
}
|
||||
|
||||
PreviewPanel.Visibility = Visibility.Visible;
|
||||
PreviewSplitter.Visibility = Visibility.Visible;
|
||||
BtnPreviewToggle.Visibility = Visibility.Visible;
|
||||
|
||||
RebuildPreviewTabs();
|
||||
LoadPreviewContent(filePath);
|
||||
}
|
||||
|
||||
private void RebuildPreviewTabs()
|
||||
{
|
||||
PreviewTabPanel.Children.Clear();
|
||||
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
|
||||
foreach (var tabPath in _previewTabs)
|
||||
{
|
||||
var fileName = Path.GetFileName(tabPath);
|
||||
var isActive = string.Equals(tabPath, _activePreviewTab, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var tabBorder = new Border
|
||||
{
|
||||
Background = isActive
|
||||
? new SolidColorBrush(Color.FromArgb(0x15, 0xFF, 0xFF, 0xFF))
|
||||
: Brushes.Transparent,
|
||||
BorderBrush = isActive ? accentBrush : Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0, 0, 0, isActive ? 2 : 0),
|
||||
Padding = new Thickness(8, 6, 4, 6),
|
||||
Cursor = Cursors.Hand,
|
||||
MaxWidth = _previewTabs.Count <= 3 ? 200 : (_previewTabs.Count <= 5 ? 140 : 100),
|
||||
};
|
||||
|
||||
var tabContent = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
tabContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = fileName,
|
||||
FontSize = 11,
|
||||
Foreground = isActive ? primaryText : secondaryText,
|
||||
FontWeight = isActive ? FontWeights.SemiBold : FontWeights.Normal,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
MaxWidth = tabBorder.MaxWidth - 30,
|
||||
ToolTip = tabPath,
|
||||
});
|
||||
|
||||
var closeFg = isActive ? primaryText : secondaryText;
|
||||
var closeBtnText = new TextBlock
|
||||
{
|
||||
Text = "\uE711",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 10,
|
||||
Foreground = closeFg,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
var closeBtn = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(3),
|
||||
Padding = new Thickness(3, 2, 3, 2),
|
||||
Margin = new Thickness(5, 0, 0, 0),
|
||||
Cursor = Cursors.Hand,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Visibility = isActive ? Visibility.Visible : Visibility.Hidden,
|
||||
Child = closeBtnText,
|
||||
Tag = "close",
|
||||
};
|
||||
|
||||
var closePath = tabPath;
|
||||
closeBtn.MouseEnter += (s, _) =>
|
||||
{
|
||||
if (s is Border b)
|
||||
{
|
||||
b.Background = new SolidColorBrush(Color.FromArgb(0x40, 0xFF, 0x50, 0x50));
|
||||
if (b.Child is TextBlock tb)
|
||||
tb.Foreground = new SolidColorBrush(Color.FromRgb(0xFF, 0x60, 0x60));
|
||||
}
|
||||
};
|
||||
closeBtn.MouseLeave += (s, _) =>
|
||||
{
|
||||
if (s is Border b)
|
||||
{
|
||||
b.Background = Brushes.Transparent;
|
||||
if (b.Child is TextBlock tb)
|
||||
tb.Foreground = closeFg;
|
||||
}
|
||||
};
|
||||
closeBtn.MouseLeftButtonUp += (_, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
ClosePreviewTab(closePath);
|
||||
};
|
||||
|
||||
tabContent.Children.Add(closeBtn);
|
||||
tabBorder.Child = tabContent;
|
||||
|
||||
var clickPath = tabPath;
|
||||
tabBorder.MouseLeftButtonUp += (_, e) =>
|
||||
{
|
||||
if (e.Handled)
|
||||
return;
|
||||
|
||||
e.Handled = true;
|
||||
_activePreviewTab = clickPath;
|
||||
RebuildPreviewTabs();
|
||||
LoadPreviewContent(clickPath);
|
||||
};
|
||||
|
||||
var ctxPath = tabPath;
|
||||
tabBorder.MouseRightButtonUp += (_, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
ShowPreviewTabContextMenu(ctxPath);
|
||||
};
|
||||
|
||||
var dblPath = tabPath;
|
||||
tabBorder.MouseLeftButtonDown += (_, e) =>
|
||||
{
|
||||
if (e.Handled)
|
||||
return;
|
||||
|
||||
if (e.ClickCount == 2)
|
||||
{
|
||||
e.Handled = true;
|
||||
OpenPreviewPopupWindow(dblPath);
|
||||
}
|
||||
};
|
||||
|
||||
var capturedIsActive = isActive;
|
||||
var capturedCloseBtn = closeBtn;
|
||||
tabBorder.MouseEnter += (s, _) =>
|
||||
{
|
||||
if (s is Border b && !capturedIsActive)
|
||||
b.Background = new SolidColorBrush(Color.FromArgb(0x10, 0xFF, 0xFF, 0xFF));
|
||||
if (!capturedIsActive)
|
||||
capturedCloseBtn.Visibility = Visibility.Visible;
|
||||
};
|
||||
tabBorder.MouseLeave += (s, _) =>
|
||||
{
|
||||
if (s is Border b && !capturedIsActive)
|
||||
b.Background = Brushes.Transparent;
|
||||
if (!capturedIsActive)
|
||||
capturedCloseBtn.Visibility = Visibility.Hidden;
|
||||
};
|
||||
|
||||
PreviewTabPanel.Children.Add(tabBorder);
|
||||
|
||||
if (!string.Equals(tabPath, _previewTabs[^1], StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
PreviewTabPanel.Children.Add(new Border
|
||||
{
|
||||
Width = 1,
|
||||
Height = 14,
|
||||
Background = borderBrush,
|
||||
Margin = new Thickness(0, 4, 0, 4),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ClosePreviewTab(string filePath)
|
||||
{
|
||||
_previewTabs.Remove(filePath);
|
||||
|
||||
if (_previewTabs.Count == 0)
|
||||
{
|
||||
HidePreviewPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(filePath, _activePreviewTab, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_activePreviewTab = _previewTabs[^1];
|
||||
LoadPreviewContent(_activePreviewTab);
|
||||
}
|
||||
|
||||
RebuildPreviewTabs();
|
||||
}
|
||||
|
||||
private async void LoadPreviewContent(string filePath)
|
||||
{
|
||||
var ext = Path.GetExtension(filePath).ToLowerInvariant();
|
||||
SetPreviewHeader(filePath);
|
||||
|
||||
PreviewWebView.Visibility = Visibility.Collapsed;
|
||||
PreviewTextScroll.Visibility = Visibility.Collapsed;
|
||||
PreviewDataGrid.Visibility = Visibility.Collapsed;
|
||||
PreviewEmpty.Visibility = Visibility.Collapsed;
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
SetPreviewHeaderState("파일을 찾을 수 없습니다");
|
||||
PreviewEmpty.Text = "파일을 찾을 수 없습니다";
|
||||
PreviewEmpty.Visibility = Visibility.Visible;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
switch (ext)
|
||||
{
|
||||
case ".html":
|
||||
case ".htm":
|
||||
await EnsureWebViewInitializedAsync();
|
||||
PreviewWebView.Source = new Uri(filePath);
|
||||
PreviewWebView.Visibility = Visibility.Visible;
|
||||
break;
|
||||
|
||||
case ".csv":
|
||||
LoadCsvPreview(filePath);
|
||||
PreviewDataGrid.Visibility = Visibility.Visible;
|
||||
break;
|
||||
|
||||
case ".md":
|
||||
await EnsureWebViewInitializedAsync();
|
||||
var mdText = File.ReadAllText(filePath);
|
||||
if (mdText.Length > 50000)
|
||||
mdText = mdText[..50000];
|
||||
var mdHtml = Services.Agent.TemplateService.RenderMarkdownToHtml(mdText, _selectedMood);
|
||||
PreviewWebView.NavigateToString(mdHtml);
|
||||
PreviewWebView.Visibility = Visibility.Visible;
|
||||
break;
|
||||
|
||||
case ".txt":
|
||||
case ".json":
|
||||
case ".xml":
|
||||
case ".log":
|
||||
var text = File.ReadAllText(filePath);
|
||||
if (text.Length > 50000)
|
||||
text = text[..50000] + "\n\n... (이후 생략)";
|
||||
PreviewTextBlock.Text = text;
|
||||
PreviewTextScroll.Visibility = Visibility.Visible;
|
||||
break;
|
||||
|
||||
default:
|
||||
SetPreviewHeaderState("지원되지 않는 형식");
|
||||
PreviewEmpty.Text = "미리보기할 수 없는 파일 형식입니다";
|
||||
PreviewEmpty.Visibility = Visibility.Visible;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetPreviewHeaderState("미리보기 오류");
|
||||
PreviewTextBlock.Text = $"미리보기 오류: {ex.Message}";
|
||||
PreviewTextScroll.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetPreviewHeader(string filePath)
|
||||
{
|
||||
if (PreviewHeaderTitle == null || PreviewHeaderSubtitle == null || PreviewHeaderMeta == null)
|
||||
return;
|
||||
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
var extension = Path.GetExtension(filePath).TrimStart('.').ToUpperInvariant();
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
var sizeText = fileInfo.Exists
|
||||
? fileInfo.Length >= 1024 * 1024
|
||||
? $"{fileInfo.Length / 1024d / 1024d:F1} MB"
|
||||
: $"{Math.Max(1, fileInfo.Length / 1024d):F0} KB"
|
||||
: "파일 없음";
|
||||
|
||||
PreviewHeaderTitle.Text = string.IsNullOrWhiteSpace(fileName) ? "미리보기" : fileName;
|
||||
PreviewHeaderSubtitle.Text = filePath;
|
||||
PreviewHeaderMeta.Text = string.IsNullOrWhiteSpace(extension)
|
||||
? sizeText
|
||||
: $"{extension} · {sizeText}";
|
||||
}
|
||||
|
||||
private void SetPreviewHeaderState(string state)
|
||||
{
|
||||
if (PreviewHeaderMeta != null && !string.IsNullOrWhiteSpace(state))
|
||||
PreviewHeaderMeta.Text = state;
|
||||
}
|
||||
|
||||
private async Task EnsureWebViewInitializedAsync()
|
||||
{
|
||||
if (_webViewInitialized)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(
|
||||
userDataFolder: WebView2DataFolder);
|
||||
await PreviewWebView.EnsureCoreWebView2Async(env);
|
||||
_webViewInitialized = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"WebView2 초기화 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadCsvPreview(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lines = File.ReadAllLines(filePath);
|
||||
if (lines.Length == 0)
|
||||
return;
|
||||
|
||||
var dt = new DataTable();
|
||||
var headers = ParseCsvLine(lines[0]);
|
||||
foreach (var h in headers)
|
||||
dt.Columns.Add(h);
|
||||
|
||||
var maxRows = Math.Min(lines.Length, 501);
|
||||
for (var i = 1; i < maxRows; i++)
|
||||
{
|
||||
var vals = ParseCsvLine(lines[i]);
|
||||
var row = dt.NewRow();
|
||||
for (var j = 0; j < Math.Min(vals.Length, headers.Length); j++)
|
||||
row[j] = vals[j];
|
||||
dt.Rows.Add(row);
|
||||
}
|
||||
|
||||
PreviewDataGrid.ItemsSource = dt.DefaultView;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PreviewTextBlock.Text = $"CSV 로드 오류: {ex.Message}";
|
||||
PreviewTextScroll.Visibility = Visibility.Visible;
|
||||
PreviewDataGrid.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private static string[] ParseCsvLine(string line)
|
||||
{
|
||||
var fields = new List<string>();
|
||||
var current = new StringBuilder();
|
||||
var inQuotes = false;
|
||||
|
||||
for (var i = 0; i < line.Length; i++)
|
||||
{
|
||||
var c = line[i];
|
||||
if (inQuotes)
|
||||
{
|
||||
if (c == '"' && i + 1 < line.Length && line[i + 1] == '"')
|
||||
{
|
||||
current.Append('"');
|
||||
i++;
|
||||
}
|
||||
else if (c == '"')
|
||||
{
|
||||
inQuotes = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
current.Append(c);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (c == '"')
|
||||
{
|
||||
inQuotes = true;
|
||||
}
|
||||
else if (c == ',')
|
||||
{
|
||||
fields.Add(current.ToString());
|
||||
current.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
current.Append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fields.Add(current.ToString());
|
||||
return fields.ToArray();
|
||||
}
|
||||
|
||||
private void HidePreviewPanel()
|
||||
{
|
||||
_previewTabs.Clear();
|
||||
_activePreviewTab = null;
|
||||
if (PreviewHeaderTitle != null) PreviewHeaderTitle.Text = "미리보기";
|
||||
if (PreviewHeaderSubtitle != null) PreviewHeaderSubtitle.Text = "선택한 파일이 여기에 표시됩니다";
|
||||
if (PreviewHeaderMeta != null) PreviewHeaderMeta.Text = "파일 메타";
|
||||
PreviewColumn.Width = new GridLength(0);
|
||||
SplitterColumn.Width = new GridLength(0);
|
||||
PreviewPanel.Visibility = Visibility.Collapsed;
|
||||
PreviewSplitter.Visibility = Visibility.Collapsed;
|
||||
PreviewWebView.Visibility = Visibility.Collapsed;
|
||||
PreviewTextScroll.Visibility = Visibility.Collapsed;
|
||||
PreviewDataGrid.Visibility = Visibility.Collapsed;
|
||||
try
|
||||
{
|
||||
if (_webViewInitialized)
|
||||
PreviewWebView.CoreWebView2?.NavigateToString("<html></html>");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void PreviewTabBar_PreviewMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (PreviewWebView.IsFocused || PreviewWebView.IsKeyboardFocusWithin)
|
||||
{
|
||||
var border = sender as Border;
|
||||
border?.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnClosePreview_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
HidePreviewPanel();
|
||||
BtnPreviewToggle.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void BtnPreviewToggle_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (PreviewPanel.Visibility == Visibility.Visible)
|
||||
{
|
||||
PreviewPanel.Visibility = Visibility.Collapsed;
|
||||
PreviewSplitter.Visibility = Visibility.Collapsed;
|
||||
PreviewColumn.Width = new GridLength(0);
|
||||
SplitterColumn.Width = new GridLength(0);
|
||||
}
|
||||
else if (_previewTabs.Count > 0)
|
||||
{
|
||||
PreviewPanel.Visibility = Visibility.Visible;
|
||||
PreviewSplitter.Visibility = Visibility.Visible;
|
||||
PreviewColumn.Width = new GridLength(420);
|
||||
SplitterColumn.Width = new GridLength(5);
|
||||
RebuildPreviewTabs();
|
||||
if (_activePreviewTab != null)
|
||||
LoadPreviewContent(_activePreviewTab);
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnOpenExternal_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_activePreviewTab) || !File.Exists(_activePreviewTab))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = _activePreviewTab,
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"외부 프로그램 실행 오류: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowPreviewTabContextMenu(string filePath)
|
||||
{
|
||||
if (_previewTabPopup != null)
|
||||
_previewTabPopup.IsOpen = false;
|
||||
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
|
||||
var stack = new StackPanel();
|
||||
|
||||
void AddItem(string icon, string iconColor, string label, Action action)
|
||||
{
|
||||
var iconBrush = string.IsNullOrEmpty(iconColor)
|
||||
? secondaryText
|
||||
: new SolidColorBrush((Color)ColorConverter.ConvertFromString(iconColor));
|
||||
var itemBorder = CreateSurfacePopupMenuItem(icon, iconBrush, label, () =>
|
||||
{
|
||||
_previewTabPopup!.IsOpen = false;
|
||||
action();
|
||||
}, primaryText, 13);
|
||||
stack.Children.Add(itemBorder);
|
||||
}
|
||||
|
||||
void AddSeparator()
|
||||
{
|
||||
stack.Children.Add(CreateSurfacePopupSeparator());
|
||||
}
|
||||
|
||||
AddItem("\uE8A7", "#64B5F6", "외부 프로그램으로 열기", () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = filePath,
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
AddItem("\uE838", "#FFB74D", "파일 위치 열기", () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start("explorer.exe", $"/select,\"{filePath}\"");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
AddItem("\uE8A7", "#81C784", "별도 창에서 보기", () => OpenPreviewPopupWindow(filePath));
|
||||
|
||||
AddSeparator();
|
||||
|
||||
AddItem("\uE8C8", "", "경로 복사", () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Clipboard.SetText(filePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
AddSeparator();
|
||||
|
||||
AddItem("\uE711", "#EF5350", "이 탭 닫기", () => ClosePreviewTab(filePath));
|
||||
|
||||
if (_previewTabs.Count > 1)
|
||||
{
|
||||
AddItem("\uE8BB", "#EF5350", "다른 탭 모두 닫기", () =>
|
||||
{
|
||||
var keep = filePath;
|
||||
_previewTabs.RemoveAll(p => !string.Equals(p, keep, StringComparison.OrdinalIgnoreCase));
|
||||
_activePreviewTab = keep;
|
||||
RebuildPreviewTabs();
|
||||
LoadPreviewContent(keep);
|
||||
});
|
||||
}
|
||||
|
||||
var popupBorder = CreateSurfacePopupContainer(stack, 180, new Thickness(4, 6, 4, 6));
|
||||
|
||||
_previewTabPopup = new Popup
|
||||
{
|
||||
Child = popupBorder,
|
||||
Placement = PlacementMode.MousePoint,
|
||||
StaysOpen = false,
|
||||
AllowsTransparency = true,
|
||||
PopupAnimation = PopupAnimation.Fade,
|
||||
};
|
||||
_previewTabPopup.IsOpen = true;
|
||||
}
|
||||
|
||||
private void OpenPreviewPopupWindow(string filePath)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
return;
|
||||
|
||||
var ext = Path.GetExtension(filePath).ToLowerInvariant();
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E));
|
||||
var fg = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
|
||||
var win = new Window
|
||||
{
|
||||
Title = $"미리보기 — {fileName}",
|
||||
Width = 900,
|
||||
Height = 700,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen,
|
||||
Background = bg,
|
||||
};
|
||||
|
||||
FrameworkElement content;
|
||||
|
||||
switch (ext)
|
||||
{
|
||||
case ".html":
|
||||
case ".htm":
|
||||
var wv = new Microsoft.Web.WebView2.Wpf.WebView2();
|
||||
wv.Loaded += async (_, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(
|
||||
userDataFolder: WebView2DataFolder);
|
||||
await wv.EnsureCoreWebView2Async(env);
|
||||
wv.Source = new Uri(filePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
};
|
||||
content = wv;
|
||||
break;
|
||||
|
||||
case ".md":
|
||||
var mdWv = new Microsoft.Web.WebView2.Wpf.WebView2();
|
||||
var mdMood = _selectedMood;
|
||||
mdWv.Loaded += async (_, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(
|
||||
userDataFolder: WebView2DataFolder);
|
||||
await mdWv.EnsureCoreWebView2Async(env);
|
||||
var mdSrc = File.ReadAllText(filePath);
|
||||
if (mdSrc.Length > 100000)
|
||||
mdSrc = mdSrc[..100000];
|
||||
var html = Services.Agent.TemplateService.RenderMarkdownToHtml(mdSrc, mdMood);
|
||||
mdWv.NavigateToString(html);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
};
|
||||
content = mdWv;
|
||||
break;
|
||||
|
||||
case ".csv":
|
||||
var dg = new DataGrid
|
||||
{
|
||||
AutoGenerateColumns = true,
|
||||
IsReadOnly = true,
|
||||
Background = Brushes.Transparent,
|
||||
Foreground = Brushes.White,
|
||||
BorderThickness = new Thickness(0),
|
||||
FontSize = 12,
|
||||
};
|
||||
try
|
||||
{
|
||||
var lines = File.ReadAllLines(filePath);
|
||||
if (lines.Length > 0)
|
||||
{
|
||||
var dt = new DataTable();
|
||||
var headers = ParseCsvLine(lines[0]);
|
||||
foreach (var h in headers)
|
||||
dt.Columns.Add(h);
|
||||
for (var i = 1; i < Math.Min(lines.Length, 1001); i++)
|
||||
{
|
||||
var vals = ParseCsvLine(lines[i]);
|
||||
var row = dt.NewRow();
|
||||
for (var j = 0; j < Math.Min(vals.Length, dt.Columns.Count); j++)
|
||||
row[j] = vals[j];
|
||||
dt.Rows.Add(row);
|
||||
}
|
||||
|
||||
dg.ItemsSource = dt.DefaultView;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
content = dg;
|
||||
break;
|
||||
|
||||
default:
|
||||
var text = File.ReadAllText(filePath);
|
||||
if (text.Length > 100000)
|
||||
text = text[..100000] + "\n\n... (이후 생략)";
|
||||
var sv = new ScrollViewer
|
||||
{
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
Padding = new Thickness(20),
|
||||
Content = new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
FontSize = 13,
|
||||
Foreground = fg,
|
||||
},
|
||||
};
|
||||
content = sv;
|
||||
break;
|
||||
}
|
||||
|
||||
win.Content = content;
|
||||
win.Show();
|
||||
}
|
||||
}
|
||||
184
src/AxCopilot/Views/ChatWindow.SelectionPopupPresentation.cs
Normal file
184
src/AxCopilot/Views/ChatWindow.SelectionPopupPresentation.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private void ShowWorktreeMenu(UIElement placementTarget)
|
||||
{
|
||||
var (popup, panel) = CreateThemedPopupMenu(placementTarget, PlacementMode.Top, 320);
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var currentFolder = GetCurrentWorkFolder();
|
||||
var root = string.IsNullOrWhiteSpace(currentFolder) ? "" : WorktreeStateStore.ResolveRoot(currentFolder);
|
||||
var active = string.IsNullOrWhiteSpace(root) ? currentFolder : WorktreeStateStore.Load(root).Active;
|
||||
var variants = GetAvailableWorkspaceVariants(root, active);
|
||||
|
||||
panel.Children.Add(CreatePopupSummaryStrip(new[]
|
||||
{
|
||||
("모드", string.Equals(active, root, StringComparison.OrdinalIgnoreCase) ? "로컬" : "워크트리", "#F8FAFC", "#E2E8F0", "#475569"),
|
||||
("변형", variants.Count.ToString(), "#EFF6FF", "#BFDBFE", "#1D4ED8"),
|
||||
}));
|
||||
panel.Children.Add(CreatePopupSectionLabel("현재 작업 위치", new Thickness(8, 6, 8, 4)));
|
||||
|
||||
panel.Children.Add(CreatePopupMenuRow(
|
||||
"\uED25",
|
||||
"로컬",
|
||||
string.IsNullOrWhiteSpace(root) ? "현재 워크스페이스" : root,
|
||||
!string.IsNullOrWhiteSpace(root) && string.Equals(active, root, StringComparison.OrdinalIgnoreCase),
|
||||
accentBrush,
|
||||
secondaryText,
|
||||
primaryText,
|
||||
() =>
|
||||
{
|
||||
popup.IsOpen = false;
|
||||
if (!string.IsNullOrWhiteSpace(root))
|
||||
SwitchToWorkspace(root, root);
|
||||
}));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(active) && !string.Equals(active, root, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
panel.Children.Add(CreatePopupMenuRow(
|
||||
"\uE7BA",
|
||||
Path.GetFileName(active),
|
||||
active,
|
||||
true,
|
||||
accentBrush,
|
||||
secondaryText,
|
||||
primaryText,
|
||||
() =>
|
||||
{
|
||||
popup.IsOpen = false;
|
||||
SwitchToWorkspace(active, root);
|
||||
}));
|
||||
}
|
||||
|
||||
if (variants.Count > 0)
|
||||
{
|
||||
panel.Children.Add(CreatePopupSectionLabel($"워크트리 / 복사본 · {variants.Count}", new Thickness(8, 10, 8, 4)));
|
||||
foreach (var variant in variants)
|
||||
{
|
||||
var isActive = !string.IsNullOrWhiteSpace(active) &&
|
||||
string.Equals(Path.GetFullPath(variant), Path.GetFullPath(active), StringComparison.OrdinalIgnoreCase);
|
||||
panel.Children.Add(CreatePopupMenuRow(
|
||||
"\uE8B7",
|
||||
Path.GetFileName(variant),
|
||||
isActive ? $"현재 선택 · {variant}" : variant,
|
||||
isActive,
|
||||
accentBrush,
|
||||
secondaryText,
|
||||
primaryText,
|
||||
() =>
|
||||
{
|
||||
popup.IsOpen = false;
|
||||
SwitchToWorkspace(variant, root);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
panel.Children.Add(CreatePopupSectionLabel("새 작업 위치", new Thickness(8, 10, 8, 4)));
|
||||
panel.Children.Add(CreatePopupMenuRow(
|
||||
"\uE943",
|
||||
"현재 브랜치로 워크트리 생성",
|
||||
"Git 저장소면 분리된 작업 복사본을 만들고 전환합니다",
|
||||
false,
|
||||
accentBrush,
|
||||
secondaryText,
|
||||
primaryText,
|
||||
() =>
|
||||
{
|
||||
popup.IsOpen = false;
|
||||
_ = CreateCurrentBranchWorktreeAsync();
|
||||
}));
|
||||
|
||||
popup.IsOpen = true;
|
||||
}
|
||||
|
||||
private Border CreatePopupMenuRow(
|
||||
string icon,
|
||||
string title,
|
||||
string description,
|
||||
bool selected,
|
||||
Brush accentBrush,
|
||||
Brush secondaryText,
|
||||
Brush primaryText,
|
||||
Action? onClick)
|
||||
{
|
||||
var hintBackground = TryFindResource("HintBackground") as Brush ?? BrushFromHex("#F8FAFC");
|
||||
var hoverBackground = TryFindResource("ItemHoverBackground") as Brush ?? BrushFromHex("#F8FAFC");
|
||||
var row = new Border
|
||||
{
|
||||
Background = selected ? hintBackground : Brushes.Transparent,
|
||||
BorderBrush = selected ? (TryFindResource("AccentColor") as Brush ?? BrushFromHex("#D6E4FF")) : Brushes.Transparent,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(12, 10, 12, 10),
|
||||
Cursor = Cursors.Hand,
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
};
|
||||
|
||||
var grid = new Grid();
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var iconBlock = new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 14,
|
||||
Foreground = selected ? accentBrush : secondaryText,
|
||||
Margin = new Thickness(0, 1, 10, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
grid.Children.Add(iconBlock);
|
||||
|
||||
var textStack = new StackPanel();
|
||||
textStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = title,
|
||||
FontSize = 13.5,
|
||||
FontWeight = selected ? FontWeights.SemiBold : FontWeights.Medium,
|
||||
Foreground = primaryText,
|
||||
});
|
||||
if (!string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
textStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = description,
|
||||
FontSize = 11.5,
|
||||
Foreground = secondaryText,
|
||||
Margin = new Thickness(0, 3, 0, 0),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
}
|
||||
|
||||
Grid.SetColumn(textStack, 1);
|
||||
grid.Children.Add(textStack);
|
||||
|
||||
if (selected)
|
||||
{
|
||||
var check = CreateSimpleCheck(accentBrush, 14);
|
||||
Grid.SetColumn(check, 2);
|
||||
check.Margin = new Thickness(10, 0, 0, 0);
|
||||
if (check is FrameworkElement element)
|
||||
element.VerticalAlignment = VerticalAlignment.Center;
|
||||
grid.Children.Add(check);
|
||||
}
|
||||
|
||||
row.Child = grid;
|
||||
row.MouseEnter += (_, _) => row.Background = selected ? hintBackground : hoverBackground;
|
||||
row.MouseLeave += (_, _) => row.Background = selected ? hintBackground : Brushes.Transparent;
|
||||
row.MouseLeftButtonUp += (_, _) => onClick?.Invoke();
|
||||
return row;
|
||||
}
|
||||
}
|
||||
107
src/AxCopilot/Views/ChatWindow.SidebarInteractionPresentation.cs
Normal file
107
src/AxCopilot/Views/ChatWindow.SidebarInteractionPresentation.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private void SidebarSearchTrigger_MouseEnter(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (SidebarSearchTrigger != null)
|
||||
SidebarSearchTrigger.Background = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
|
||||
if (SidebarSearchShortcutHint != null)
|
||||
SidebarSearchShortcutHint.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private void SidebarSearchTrigger_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (SidebarSearchTrigger != null)
|
||||
SidebarSearchTrigger.Background = Brushes.Transparent;
|
||||
if (SidebarSearchShortcutHint != null)
|
||||
SidebarSearchShortcutHint.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void SidebarSearchTrigger_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
OpenSidebarSearch();
|
||||
}
|
||||
|
||||
private void SidebarNewChatTrigger_MouseEnter(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (SidebarNewChatTrigger != null)
|
||||
SidebarNewChatTrigger.Background = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
|
||||
if (SidebarNewChatShortcutHint != null)
|
||||
SidebarNewChatShortcutHint.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private void SidebarNewChatTrigger_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (SidebarNewChatTrigger != null)
|
||||
SidebarNewChatTrigger.Background = Brushes.Transparent;
|
||||
if (SidebarNewChatShortcutHint != null)
|
||||
SidebarNewChatShortcutHint.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void SidebarNewChatTrigger_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
BtnNewChat_Click(sender, new RoutedEventArgs());
|
||||
}
|
||||
|
||||
private void BtnSidebarSearchClose_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CloseSidebarSearch(clearText: true);
|
||||
}
|
||||
|
||||
private void OpenSidebarSearch()
|
||||
{
|
||||
if (SidebarSearchEditor == null || SidebarSearchTrigger == null || SidebarSearchEditorScale == null)
|
||||
return;
|
||||
|
||||
SidebarSearchTrigger.Visibility = Visibility.Collapsed;
|
||||
SidebarSearchEditor.Visibility = Visibility.Visible;
|
||||
SidebarSearchEditor.Opacity = 0;
|
||||
SidebarSearchEditorScale.ScaleX = 0.85;
|
||||
|
||||
var duration = TimeSpan.FromMilliseconds(160);
|
||||
SidebarSearchEditor.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, duration));
|
||||
SidebarSearchEditorScale.BeginAnimation(System.Windows.Media.ScaleTransform.ScaleXProperty, new DoubleAnimation(0.85, 1, duration)
|
||||
{
|
||||
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
|
||||
});
|
||||
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
SearchBox?.Focus();
|
||||
SearchBox?.SelectAll();
|
||||
}), DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
private void CloseSidebarSearch(bool clearText)
|
||||
{
|
||||
if (SidebarSearchEditor == null || SidebarSearchTrigger == null || SidebarSearchEditorScale == null)
|
||||
return;
|
||||
|
||||
if (clearText && SearchBox != null && !string.IsNullOrEmpty(SearchBox.Text))
|
||||
SearchBox.Text = "";
|
||||
|
||||
var duration = TimeSpan.FromMilliseconds(140);
|
||||
var opacityAnim = new DoubleAnimation(1, 0, duration);
|
||||
opacityAnim.Completed += (_, _) =>
|
||||
{
|
||||
SidebarSearchEditor.Visibility = Visibility.Collapsed;
|
||||
SidebarSearchTrigger.Visibility = Visibility.Visible;
|
||||
SidebarSearchTrigger.Background = Brushes.Transparent;
|
||||
if (SidebarSearchShortcutHint != null)
|
||||
SidebarSearchShortcutHint.Visibility = Visibility.Collapsed;
|
||||
};
|
||||
SidebarSearchEditor.BeginAnimation(OpacityProperty, opacityAnim);
|
||||
SidebarSearchEditorScale.BeginAnimation(System.Windows.Media.ScaleTransform.ScaleXProperty, new DoubleAnimation(1, 0.85, duration)
|
||||
{
|
||||
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseIn }
|
||||
});
|
||||
}
|
||||
}
|
||||
284
src/AxCopilot/Views/ChatWindow.StatusPresentation.cs
Normal file
284
src/AxCopilot/Views/ChatWindow.StatusPresentation.cs
Normal file
@@ -0,0 +1,284 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private Storyboard? _statusSpinStoryboard;
|
||||
|
||||
private AppStateService.OperationalStatusPresentationState BuildOperationalStatusPresentation()
|
||||
{
|
||||
var hasLiveRuntimeActivity = !string.Equals(_activeTab, "Chat", StringComparison.OrdinalIgnoreCase)
|
||||
&& (_runningConversationCount > 0 || _appState.ActiveTasks.Count > 0);
|
||||
return _appState.GetOperationalStatusPresentation(
|
||||
_activeTab,
|
||||
hasLiveRuntimeActivity,
|
||||
_runningConversationCount,
|
||||
_spotlightConversationCount,
|
||||
_runningOnlyFilter,
|
||||
_sortConversationsByRecent);
|
||||
}
|
||||
|
||||
private void UpdateTaskSummaryIndicators()
|
||||
{
|
||||
var status = BuildOperationalStatusPresentation();
|
||||
|
||||
if (RuntimeActivityBadge != null)
|
||||
RuntimeActivityBadge.Visibility = status.ShowRuntimeBadge
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
|
||||
if (RuntimeActivityLabel != null)
|
||||
RuntimeActivityLabel.Text = status.RuntimeLabel;
|
||||
|
||||
if (LastCompletedLabel != null)
|
||||
{
|
||||
LastCompletedLabel.Text = status.LastCompletedText;
|
||||
LastCompletedLabel.Visibility = status.ShowLastCompleted ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
if (ConversationStatusStrip != null && ConversationStatusStripLabel != null)
|
||||
{
|
||||
ConversationStatusStrip.Visibility = status.ShowCompactStrip
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
ConversationStatusStripLabel.Text = status.ShowCompactStrip ? status.StripText : "";
|
||||
|
||||
if (status.ShowCompactStrip)
|
||||
{
|
||||
ConversationStatusStrip.Background = BrushFromHex(status.StripBackgroundHex);
|
||||
ConversationStatusStrip.BorderBrush = BrushFromHex(status.StripBorderHex);
|
||||
ConversationStatusStripLabel.Foreground = BrushFromHex(status.StripForegroundHex);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateConversationQuickStripUi(status);
|
||||
}
|
||||
|
||||
private void UpdateConversationQuickStripUi()
|
||||
{
|
||||
UpdateConversationQuickStripUi(BuildOperationalStatusPresentation());
|
||||
}
|
||||
|
||||
private void UpdateConversationQuickStripUi(AppStateService.OperationalStatusPresentationState status)
|
||||
{
|
||||
if (ConversationQuickStrip == null || QuickRunningLabel == null || QuickHotLabel == null
|
||||
|| BtnQuickRunningFilter == null || BtnQuickHotSort == null)
|
||||
return;
|
||||
|
||||
ConversationQuickStrip.Visibility = status.ShowQuickStrip
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
|
||||
QuickRunningLabel.Text = status.QuickRunningText;
|
||||
QuickHotLabel.Text = status.QuickHotText;
|
||||
|
||||
BtnQuickRunningFilter.Background = BrushFromHex(status.QuickRunningBackgroundHex);
|
||||
BtnQuickRunningFilter.BorderBrush = BrushFromHex(status.QuickRunningBorderHex);
|
||||
BtnQuickRunningFilter.BorderThickness = new Thickness(1);
|
||||
QuickRunningLabel.Foreground = BrushFromHex(status.QuickRunningForegroundHex);
|
||||
|
||||
BtnQuickHotSort.Background = BrushFromHex(status.QuickHotBackgroundHex);
|
||||
BtnQuickHotSort.BorderBrush = BrushFromHex(status.QuickHotBorderHex);
|
||||
BtnQuickHotSort.BorderThickness = new Thickness(1);
|
||||
QuickHotLabel.Foreground = BrushFromHex(status.QuickHotForegroundHex);
|
||||
}
|
||||
|
||||
private void SetStatus(string text, bool spinning)
|
||||
{
|
||||
if (StatusLabel != null)
|
||||
StatusLabel.Text = text;
|
||||
|
||||
if (spinning)
|
||||
StartStatusAnimation();
|
||||
else
|
||||
StopStatusAnimation();
|
||||
}
|
||||
|
||||
private static bool IsDecisionPending(string? summary)
|
||||
{
|
||||
var text = summary?.Trim() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return true;
|
||||
|
||||
return text.Contains("확인 대기", StringComparison.OrdinalIgnoreCase)
|
||||
|| text.Contains("승인 대기", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsDecisionApproved(string? summary)
|
||||
{
|
||||
var text = summary?.Trim() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return false;
|
||||
|
||||
return text.Contains("계획 승인", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsDecisionRejected(string? summary)
|
||||
{
|
||||
var text = summary?.Trim() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return false;
|
||||
|
||||
return text.Contains("계획 반려", StringComparison.OrdinalIgnoreCase)
|
||||
|| text.Contains("수정 요청", StringComparison.OrdinalIgnoreCase)
|
||||
|| text.Contains("취소", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string GetDecisionStatusText(string? summary)
|
||||
{
|
||||
if (IsDecisionPending(summary))
|
||||
return "계획 승인 대기 중";
|
||||
if (IsDecisionApproved(summary))
|
||||
return "계획 승인됨 · 실행 시작";
|
||||
if (IsDecisionRejected(summary))
|
||||
return "계획 반려됨 · 계획 재작성";
|
||||
return string.IsNullOrWhiteSpace(summary) ? "사용자 의사결정 대기 중" : TruncateForStatus(summary);
|
||||
}
|
||||
|
||||
private void SetStatusIdle()
|
||||
{
|
||||
StopStatusAnimation();
|
||||
if (StatusLabel != null)
|
||||
StatusLabel.Text = "대기 중";
|
||||
if (StatusElapsed != null)
|
||||
{
|
||||
StatusElapsed.Text = "";
|
||||
StatusElapsed.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
if (StatusTokens != null)
|
||||
{
|
||||
StatusTokens.Text = "";
|
||||
StatusTokens.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
RefreshContextUsageVisual();
|
||||
ScheduleGitBranchRefresh(250);
|
||||
}
|
||||
|
||||
private void UpdateStatusTokens(int inputTokens, int outputTokens)
|
||||
{
|
||||
if (StatusTokens == null)
|
||||
return;
|
||||
|
||||
var llm = _settings.Settings.Llm;
|
||||
var (inCost, outCost) = Services.TokenEstimator.EstimateCost(
|
||||
inputTokens, outputTokens, llm.Service, llm.Model);
|
||||
var totalCost = inCost + outCost;
|
||||
var costText = totalCost > 0 ? $" · {Services.TokenEstimator.FormatCost(totalCost)}" : "";
|
||||
StatusTokens.Text = $"↑{Services.TokenEstimator.Format(inputTokens)} ↓{Services.TokenEstimator.Format(outputTokens)}{costText}";
|
||||
StatusTokens.Visibility = Visibility.Visible;
|
||||
RefreshContextUsageVisual();
|
||||
}
|
||||
private void UpdateStatusBar(AgentEvent evt)
|
||||
{
|
||||
var toolLabel = evt.ToolName switch
|
||||
{
|
||||
"file_read" or "document_read" => "파일 읽기",
|
||||
"file_write" => "파일 쓰기",
|
||||
"file_edit" => "파일 수정",
|
||||
"html_create" => "HTML 생성",
|
||||
"xlsx_create" => "Excel 생성",
|
||||
"docx_create" => "Word 생성",
|
||||
"csv_create" => "CSV 생성",
|
||||
"md_create" => "Markdown 생성",
|
||||
"folder_map" => "폴더 탐색",
|
||||
"glob" => "파일 검색",
|
||||
"grep" => "내용 검색",
|
||||
"process" => "명령 실행",
|
||||
_ => evt.ToolName,
|
||||
};
|
||||
|
||||
var isDebugLogLevel = string.Equals(_settings.Settings.Llm.AgentLogLevel, "debug", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
switch (evt.Type)
|
||||
{
|
||||
case AgentEventType.Thinking:
|
||||
SetStatus("생각 중...", spinning: true);
|
||||
break;
|
||||
case AgentEventType.Planning:
|
||||
SetStatus($"계획 수립 중 — {evt.StepTotal}단계", spinning: true);
|
||||
break;
|
||||
case AgentEventType.PermissionRequest:
|
||||
SetStatus($"권한 확인 중: {toolLabel}", spinning: false);
|
||||
break;
|
||||
case AgentEventType.PermissionGranted:
|
||||
SetStatus($"권한 승인됨: {toolLabel}", spinning: false);
|
||||
break;
|
||||
case AgentEventType.PermissionDenied:
|
||||
SetStatus($"권한 거부됨: {toolLabel}", spinning: false);
|
||||
StopStatusAnimation();
|
||||
break;
|
||||
case AgentEventType.Decision:
|
||||
SetStatus(GetDecisionStatusText(evt.Summary), spinning: IsDecisionPending(evt.Summary));
|
||||
break;
|
||||
case AgentEventType.ToolCall:
|
||||
if (!isDebugLogLevel)
|
||||
break;
|
||||
SetStatus($"{toolLabel} 실행 중...", spinning: true);
|
||||
break;
|
||||
case AgentEventType.ToolResult:
|
||||
SetStatus(evt.Success ? $"{toolLabel} 완료" : $"{toolLabel} 실패", spinning: false);
|
||||
break;
|
||||
case AgentEventType.StepStart:
|
||||
SetStatus($"[{evt.StepCurrent}/{evt.StepTotal}] {TruncateForStatus(evt.Summary)}", spinning: true);
|
||||
break;
|
||||
case AgentEventType.StepDone:
|
||||
SetStatus($"[{evt.StepCurrent}/{evt.StepTotal}] 단계 완료", spinning: true);
|
||||
break;
|
||||
case AgentEventType.SkillCall:
|
||||
if (!isDebugLogLevel)
|
||||
break;
|
||||
SetStatus($"스킬 실행 중: {TruncateForStatus(evt.Summary)}", spinning: true);
|
||||
break;
|
||||
case AgentEventType.Complete:
|
||||
SetStatus("작업 완료", spinning: false);
|
||||
StopStatusAnimation();
|
||||
break;
|
||||
case AgentEventType.Error:
|
||||
SetStatus("오류 발생", spinning: false);
|
||||
StopStatusAnimation();
|
||||
break;
|
||||
case AgentEventType.Paused:
|
||||
if (!isDebugLogLevel)
|
||||
break;
|
||||
SetStatus("⏸ 일시정지", spinning: false);
|
||||
break;
|
||||
case AgentEventType.Resumed:
|
||||
if (!isDebugLogLevel)
|
||||
break;
|
||||
SetStatus("▶ 재개됨", spinning: true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void StartStatusAnimation()
|
||||
{
|
||||
if (_statusSpinStoryboard != null)
|
||||
return;
|
||||
|
||||
var anim = new DoubleAnimation
|
||||
{
|
||||
From = 0,
|
||||
To = 360,
|
||||
Duration = TimeSpan.FromSeconds(2),
|
||||
RepeatBehavior = RepeatBehavior.Forever,
|
||||
};
|
||||
|
||||
_statusSpinStoryboard = new Storyboard();
|
||||
Storyboard.SetTarget(anim, StatusDiamond);
|
||||
Storyboard.SetTargetProperty(anim,
|
||||
new PropertyPath("(UIElement.RenderTransform).(RotateTransform.Angle)"));
|
||||
_statusSpinStoryboard.Children.Add(anim);
|
||||
_statusSpinStoryboard.Begin();
|
||||
}
|
||||
|
||||
private void StopStatusAnimation()
|
||||
{
|
||||
_statusSpinStoryboard?.Stop();
|
||||
_statusSpinStoryboard = null;
|
||||
}
|
||||
}
|
||||
121
src/AxCopilot/Views/ChatWindow.SurfaceVisualPresentation.cs
Normal file
121
src/AxCopilot/Views/ChatWindow.SurfaceVisualPresentation.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private Border CreateSurfacePopupContainer(UIElement content, double minWidth = 180, Thickness? padding = null)
|
||||
{
|
||||
var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E));
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
|
||||
return new Border
|
||||
{
|
||||
Background = bg,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = padding ?? new Thickness(6),
|
||||
MinWidth = minWidth,
|
||||
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||||
{
|
||||
BlurRadius = 16,
|
||||
ShadowDepth = 4,
|
||||
Opacity = 0.34,
|
||||
Color = Colors.Black,
|
||||
},
|
||||
Child = content,
|
||||
};
|
||||
}
|
||||
|
||||
private Border CreateSurfacePopupMenuItem(string icon, Brush iconBrush, string label, Action onClick, Brush? labelBrush = null, double fontSize = 12.5)
|
||||
{
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
|
||||
var item = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(10, 8, 14, 8),
|
||||
Margin = new Thickness(0, 1, 0, 1),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
|
||||
var row = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
row.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 13,
|
||||
Foreground = iconBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
});
|
||||
row.Children.Add(new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = fontSize,
|
||||
Foreground = labelBrush ?? primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
item.Child = row;
|
||||
|
||||
item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; };
|
||||
item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||||
item.MouseLeftButtonUp += (_, _) => onClick();
|
||||
return item;
|
||||
}
|
||||
|
||||
private Border CreateSurfacePopupSeparator()
|
||||
{
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
return new Border
|
||||
{
|
||||
Height = 1,
|
||||
Margin = new Thickness(10, 4, 10, 4),
|
||||
Background = borderBrush,
|
||||
Opacity = 0.3,
|
||||
};
|
||||
}
|
||||
|
||||
private StackPanel CreateSurfaceFileTreeHeader(string icon, string name, string? sizeText)
|
||||
{
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 11,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 5, 0),
|
||||
});
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = name,
|
||||
FontSize = 11.5,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
if (!string.IsNullOrEmpty(sizeText))
|
||||
{
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $" {sizeText}",
|
||||
FontSize = 10,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
}
|
||||
|
||||
return sp;
|
||||
}
|
||||
}
|
||||
358
src/AxCopilot/Views/ChatWindow.TaskSummary.cs
Normal file
358
src/AxCopilot/Views/ChatWindow.TaskSummary.cs
Normal file
@@ -0,0 +1,358 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private void RuntimeTaskSummary_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
_taskSummaryTarget = sender as UIElement ?? RuntimeActivityBadge;
|
||||
ShowTaskSummaryPopup();
|
||||
}
|
||||
|
||||
private void ShowTaskSummaryPopup()
|
||||
{
|
||||
if (_taskSummaryTarget == null)
|
||||
return;
|
||||
|
||||
if (_taskSummaryPopup != null)
|
||||
_taskSummaryPopup.IsOpen = false;
|
||||
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray;
|
||||
var popupBackground = TryFindResource("LauncherBackground") as Brush ?? Brushes.White;
|
||||
var panel = new StackPanel { Margin = new Thickness(2) };
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "작업 요약",
|
||||
FontSize = 11,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
Margin = new Thickness(8, 5, 8, 2),
|
||||
});
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "현재 상태 요약",
|
||||
FontSize = 8.5,
|
||||
Foreground = secondaryText,
|
||||
Margin = new Thickness(8, 0, 8, 5),
|
||||
});
|
||||
|
||||
ChatConversation? currentConversation;
|
||||
lock (_convLock) currentConversation = _currentConversation;
|
||||
AddTaskSummaryObservabilitySections(panel, currentConversation);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_appState.AgentRun.RunId))
|
||||
{
|
||||
var currentRun = new Border
|
||||
{
|
||||
Background = BrushFromHex("#F8FAFC"),
|
||||
BorderBrush = BrushFromHex("#E2E8F0"),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(8, 6, 8, 6),
|
||||
Margin = new Thickness(6, 0, 6, 6),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = $"실행 run {ShortRunId(_appState.AgentRun.RunId)}",
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
FontSize = 9.75,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = $"{GetRunStatusLabel(_appState.AgentRun.Status)} · step {_appState.AgentRun.LastIteration}",
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
Foreground = GetRunStatusBrush(_appState.AgentRun.Status),
|
||||
FontSize = 9,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = string.IsNullOrWhiteSpace(_appState.AgentRun.Summary) ? "요약 없음" : _appState.AgentRun.Summary,
|
||||
Margin = new Thickness(0, 3, 0, 0),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Foreground = Brushes.DimGray,
|
||||
FontSize = 9,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
panel.Children.Add(currentRun);
|
||||
}
|
||||
|
||||
var recentAgentRuns = _appState.GetRecentAgentRuns(1);
|
||||
if (recentAgentRuns.Count > 0)
|
||||
{
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "마지막 실행",
|
||||
FontSize = 9,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = Brushes.DimGray,
|
||||
Margin = new Thickness(8, 0, 8, 2),
|
||||
});
|
||||
|
||||
foreach (var run in recentAgentRuns)
|
||||
{
|
||||
var runEvents = GetExecutionEventsForRun(run.RunId, 1);
|
||||
var runFilePaths = GetExecutionEventFilePaths(run.RunId, 1);
|
||||
var runDisplay = _appState.GetRunDisplay(run);
|
||||
var runCardStack = new StackPanel
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = runDisplay.HeaderText,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = GetRunStatusBrush(run.Status),
|
||||
FontSize = 9.5,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = runDisplay.MetaText,
|
||||
Margin = new Thickness(0, 1, 0, 0),
|
||||
Foreground = secondaryText,
|
||||
FontSize = 8.25,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = TruncateForStatus(runDisplay.SummaryText, 92),
|
||||
Margin = new Thickness(0, 1.5, 0, 0),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Foreground = secondaryText,
|
||||
FontSize = 8.5,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (runEvents.Count > 0 || runFilePaths.Count > 0)
|
||||
{
|
||||
var activitySummary = new StackPanel();
|
||||
activitySummary.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"로그 {runEvents.Count} · 파일 {runFilePaths.Count}",
|
||||
FontSize = 8,
|
||||
Foreground = secondaryText,
|
||||
});
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(run.RunId))
|
||||
{
|
||||
var capturedRunId = run.RunId;
|
||||
var timelineButton = CreateTaskSummaryActionButton(
|
||||
"타임라인",
|
||||
"#F8FAFC",
|
||||
"#CBD5E1",
|
||||
"#334155",
|
||||
(_, _) => ScrollToRunInTimeline(capturedRunId),
|
||||
trailingMargin: false);
|
||||
timelineButton.Margin = new Thickness(0, 5, 0, 0);
|
||||
activitySummary.Children.Add(timelineButton);
|
||||
}
|
||||
|
||||
runCardStack.Children.Add(new Border
|
||||
{
|
||||
Background = BrushFromHex("#F8FAFC"),
|
||||
BorderBrush = BrushFromHex("#E2E8F0"),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(7),
|
||||
Padding = new Thickness(6, 4, 6, 4),
|
||||
Margin = new Thickness(0, 5, 0, 0),
|
||||
Child = activitySummary
|
||||
});
|
||||
}
|
||||
|
||||
if (string.Equals(run.Status, "completed", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var capturedRun = run;
|
||||
var followUpButton = CreateTaskSummaryActionButton(
|
||||
"후속 큐",
|
||||
"#ECFDF5",
|
||||
"#BBF7D0",
|
||||
"#166534",
|
||||
(_, _) => EnqueueFollowUpFromRun(capturedRun),
|
||||
trailingMargin: false);
|
||||
followUpButton.Margin = new Thickness(0, 6, 0, 0);
|
||||
runCardStack.Children.Add(followUpButton);
|
||||
}
|
||||
|
||||
if (string.Equals(run.Status, "failed", StringComparison.OrdinalIgnoreCase) && CanRetryCurrentConversation())
|
||||
{
|
||||
var retryButton = CreateTaskSummaryActionButton(
|
||||
"다시 시도",
|
||||
"#FEF2F2",
|
||||
"#FCA5A5",
|
||||
"#991B1B",
|
||||
(_, _) =>
|
||||
{
|
||||
_taskSummaryPopup?.SetCurrentValue(Popup.IsOpenProperty, false);
|
||||
RetryLastUserMessageFromConversation();
|
||||
},
|
||||
trailingMargin: false);
|
||||
retryButton.Margin = new Thickness(0, 6, 0, 0);
|
||||
runCardStack.Children.Add(retryButton);
|
||||
}
|
||||
|
||||
panel.Children.Add(new Border
|
||||
{
|
||||
Background = popupBackground,
|
||||
BorderBrush = BrushFromHex("#E5E7EB"),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(7),
|
||||
Padding = new Thickness(7, 5, 7, 5),
|
||||
Margin = new Thickness(6, 0, 6, 4),
|
||||
Child = runCardStack
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var activeTasks = FilterTaskSummaryItems(_appState.ActiveTasks).Take(3).ToList();
|
||||
var recentTasks = FilterTaskSummaryItems(_appState.RecentTasks).Take(2).ToList();
|
||||
|
||||
foreach (var task in activeTasks)
|
||||
panel.Children.Add(BuildTaskSummaryCard(task, active: true));
|
||||
|
||||
if (ShouldIncludeRecentTaskSummary(activeTasks))
|
||||
{
|
||||
foreach (var task in recentTasks)
|
||||
panel.Children.Add(BuildTaskSummaryCard(task, active: false));
|
||||
}
|
||||
|
||||
if (activeTasks.Count == 0 && recentTasks.Count == 0)
|
||||
{
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "표시할 작업 이력이 없습니다.",
|
||||
Margin = new Thickness(10, 2, 10, 8),
|
||||
Foreground = secondaryText,
|
||||
});
|
||||
}
|
||||
|
||||
_taskSummaryPopup = new Popup
|
||||
{
|
||||
PlacementTarget = _taskSummaryTarget,
|
||||
Placement = PlacementMode.Top,
|
||||
AllowsTransparency = true,
|
||||
StaysOpen = false,
|
||||
PopupAnimation = PopupAnimation.Fade,
|
||||
Child = new Border
|
||||
{
|
||||
Background = popupBackground,
|
||||
BorderBrush = BrushFromHex("#E5E7EB"),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(6),
|
||||
Child = new ScrollViewer
|
||||
{
|
||||
Content = panel,
|
||||
MaxHeight = 340,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_taskSummaryPopup.IsOpen = true;
|
||||
}
|
||||
|
||||
private Border BuildTaskSummaryCard(TaskRunStore.TaskRun task, bool active)
|
||||
{
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray;
|
||||
var (kindIcon, kindColor) = GetTaskKindVisual(task.Kind);
|
||||
var categoryLabel = GetTranscriptTaskCategory(task);
|
||||
var displayTitle = string.Equals(task.Kind, "tool", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(task.Kind, "permission", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(task.Kind, "hook", StringComparison.OrdinalIgnoreCase)
|
||||
? GetAgentItemDisplayName(task.Title)
|
||||
: task.Title;
|
||||
var taskStack = new StackPanel();
|
||||
var headerRow = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Margin = new Thickness(0, 0, 0, 2),
|
||||
};
|
||||
headerRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = kindIcon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 9.5,
|
||||
Foreground = kindColor,
|
||||
Margin = new Thickness(0, 0, 4, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
headerRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = active
|
||||
? $"진행 중 · {displayTitle}"
|
||||
: $"{GetTaskStatusLabel(task.Status)} · {displayTitle}",
|
||||
FontSize = 9.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = active ? primaryText : secondaryText,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
taskStack.Children.Add(headerRow);
|
||||
|
||||
taskStack.Children.Add(new Border
|
||||
{
|
||||
Background = BrushFromHex("#F8FAFC"),
|
||||
BorderBrush = BrushFromHex("#E5E7EB"),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Padding = new Thickness(6, 1, 6, 1),
|
||||
Margin = new Thickness(0, 0, 0, 4),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = categoryLabel,
|
||||
FontSize = 8,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = secondaryText,
|
||||
},
|
||||
});
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(task.Summary))
|
||||
{
|
||||
taskStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = TruncateForStatus(task.Summary, 96),
|
||||
FontSize = 8.75,
|
||||
Foreground = secondaryText,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
}
|
||||
|
||||
var reviewChipRow = BuildReviewSignalChipRow(
|
||||
kind: task.Kind,
|
||||
toolName: task.Title,
|
||||
title: displayTitle,
|
||||
summary: task.Summary);
|
||||
if (reviewChipRow != null)
|
||||
taskStack.Children.Add(reviewChipRow);
|
||||
|
||||
var actionRow = BuildTaskSummaryActionRow(task, active);
|
||||
if (actionRow != null)
|
||||
taskStack.Children.Add(actionRow);
|
||||
|
||||
return new Border
|
||||
{
|
||||
Background = active ? BrushFromHex("#F8FAFC") : (TryFindResource("LauncherBackground") as Brush ?? Brushes.White),
|
||||
BorderBrush = BrushFromHex("#E5E7EB"),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(7),
|
||||
Padding = new Thickness(8, 5, 8, 5),
|
||||
Margin = new Thickness(8, 0, 8, 4),
|
||||
Child = taskStack
|
||||
};
|
||||
}
|
||||
}
|
||||
234
src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs
Normal file
234
src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs
Normal file
@@ -0,0 +1,234 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private List<ChatMessage> GetVisibleTimelineMessages(ChatConversation? conversation)
|
||||
{
|
||||
return conversation?.Messages?.Where(msg =>
|
||||
{
|
||||
if (string.Equals(msg.Role, "system", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (string.Equals(msg.Role, "assistant", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.IsNullOrWhiteSpace(msg.Content))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}).ToList() ?? new List<ChatMessage>();
|
||||
}
|
||||
|
||||
private List<ChatExecutionEvent> GetVisibleTimelineEvents(ChatConversation? conversation)
|
||||
{
|
||||
return (conversation?.ShowExecutionHistory ?? true)
|
||||
? conversation?.ExecutionEvents?.ToList() ?? new List<ChatExecutionEvent>()
|
||||
: new List<ChatExecutionEvent>();
|
||||
}
|
||||
|
||||
private List<(DateTime Timestamp, int Order, Action Render)> BuildTimelineRenderActions(
|
||||
IReadOnlyCollection<ChatMessage> visibleMessages,
|
||||
IReadOnlyCollection<ChatExecutionEvent> visibleEvents)
|
||||
{
|
||||
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)));
|
||||
|
||||
foreach (var executionEvent in visibleEvents)
|
||||
{
|
||||
var restoredEvent = ToAgentEvent(executionEvent);
|
||||
timeline.Add((executionEvent.Timestamp, 1, () => AddAgentEventBanner(restoredEvent)));
|
||||
}
|
||||
|
||||
return timeline
|
||||
.OrderBy(x => x.Timestamp)
|
||||
.ThenBy(x => x.Order)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private Border CreateTimelineLoadMoreCard(int hiddenCount)
|
||||
{
|
||||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? BrushFromHex("#F8FAFC");
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E2E8F0");
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#334155");
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#64748B");
|
||||
|
||||
var loadMoreBtn = new Button
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(7, 3, 7, 3),
|
||||
Cursor = System.Windows.Input.Cursors.Hand,
|
||||
Foreground = primaryText,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
};
|
||||
loadMoreBtn.Template = BuildMinimalIconButtonTemplate();
|
||||
loadMoreBtn.Content = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = "\uE70D",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 8,
|
||||
Foreground = secondaryText,
|
||||
Margin = new Thickness(0, 0, 4, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = $"이전 대화 {hiddenCount:N0}개",
|
||||
FontSize = 9.25,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
}
|
||||
}
|
||||
};
|
||||
loadMoreBtn.MouseEnter += (_, _) => loadMoreBtn.Background = hoverBg;
|
||||
loadMoreBtn.MouseLeave += (_, _) => loadMoreBtn.Background = Brushes.Transparent;
|
||||
loadMoreBtn.Click += (_, _) =>
|
||||
{
|
||||
_timelineRenderLimit += TimelineRenderPageSize;
|
||||
RenderMessages(preserveViewport: true);
|
||||
};
|
||||
|
||||
return new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Margin = new Thickness(0, 2, 0, 8),
|
||||
Padding = new Thickness(0),
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Child = loadMoreBtn,
|
||||
};
|
||||
}
|
||||
|
||||
private static AgentEvent ToAgentEvent(ChatExecutionEvent executionEvent)
|
||||
{
|
||||
var parsedType = Enum.TryParse<AgentEventType>(executionEvent.Type, out var eventType)
|
||||
? eventType
|
||||
: AgentEventType.Thinking;
|
||||
|
||||
return new AgentEvent
|
||||
{
|
||||
Timestamp = executionEvent.Timestamp,
|
||||
RunId = executionEvent.RunId,
|
||||
Type = parsedType,
|
||||
ToolName = executionEvent.ToolName,
|
||||
Summary = executionEvent.Summary,
|
||||
FilePath = executionEvent.FilePath,
|
||||
Success = executionEvent.Success,
|
||||
StepCurrent = executionEvent.StepCurrent,
|
||||
StepTotal = executionEvent.StepTotal,
|
||||
Steps = executionEvent.Steps,
|
||||
ElapsedMs = executionEvent.ElapsedMs,
|
||||
InputTokens = executionEvent.InputTokens,
|
||||
OutputTokens = executionEvent.OutputTokens,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsCompactionMetaMessage(ChatMessage? message)
|
||||
{
|
||||
var kind = message?.MetaKind ?? "";
|
||||
return kind.Equals("microcompact_boundary", StringComparison.OrdinalIgnoreCase)
|
||||
|| kind.Equals("session_memory_compaction", StringComparison.OrdinalIgnoreCase)
|
||||
|| kind.Equals("collapsed_boundary", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private Border CreateCompactionMetaCard(ChatMessage message, Brush primaryText, Brush secondaryText, Brush hintBg, Brush borderBrush, Brush accentBrush)
|
||||
{
|
||||
var icon = "\uE9CE";
|
||||
var title = message.MetaKind switch
|
||||
{
|
||||
"session_memory_compaction" => "세션 메모리 압축",
|
||||
"collapsed_boundary" => "압축 경계 병합",
|
||||
_ => "Microcompact 경계",
|
||||
};
|
||||
|
||||
var wrapper = new Border
|
||||
{
|
||||
Background = hintBg,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(12, 10, 12, 10),
|
||||
Margin = new Thickness(10, 4, 150, 4),
|
||||
MaxWidth = GetMessageMaxWidth(),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
};
|
||||
|
||||
var stack = new StackPanel();
|
||||
var header = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
};
|
||||
header.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 11,
|
||||
Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
header.Children.Add(new TextBlock
|
||||
{
|
||||
Text = title,
|
||||
FontSize = 11,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
Margin = new Thickness(6, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
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();
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var isHeaderLine = line.StartsWith("[", StringComparison.Ordinal);
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = isHeaderLine ? line.Trim('[', ']') : line,
|
||||
FontSize = 10.5,
|
||||
FontWeight = isHeaderLine ? FontWeights.SemiBold : FontWeights.Normal,
|
||||
Foreground = isHeaderLine ? primaryText : secondaryText,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = isHeaderLine ? new Thickness(0, 0, 0, 3) : new Thickness(0, 0, 0, 2),
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(message.MetaRunId))
|
||||
{
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"run {message.MetaRunId}",
|
||||
FontSize = 9.5,
|
||||
Foreground = secondaryText,
|
||||
Opacity = 0.7,
|
||||
Margin = new Thickness(0, 6, 0, 0),
|
||||
});
|
||||
}
|
||||
|
||||
wrapper.Child = stack;
|
||||
return wrapper;
|
||||
}
|
||||
}
|
||||
523
src/AxCopilot/Views/ChatWindow.TopicPresetPresentation.cs
Normal file
523
src/AxCopilot/Views/ChatWindow.TopicPresetPresentation.cs
Normal file
@@ -0,0 +1,523 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
/// <summary>프리셋에서 대화 주제 버튼을 동적으로 생성합니다.</summary>
|
||||
private void BuildTopicButtons()
|
||||
{
|
||||
TopicButtonPanel.Children.Clear();
|
||||
TopicButtonPanel.Visibility = Visibility.Visible;
|
||||
if (TopicPresetScrollViewer != null)
|
||||
TopicPresetScrollViewer.Visibility = Visibility.Visible;
|
||||
|
||||
if (_activeTab == "Cowork" || _activeTab == "Code")
|
||||
{
|
||||
if (EmptyStateTitle != null) EmptyStateTitle.Text = _activeTab == "Code"
|
||||
? "코드 작업을 입력하세요"
|
||||
: "작업 유형을 선택하세요";
|
||||
if (EmptyStateDesc != null) EmptyStateDesc.Text = _activeTab == "Code"
|
||||
? "코딩 에이전트가 코드 분석, 수정, 빌드, 테스트를 수행합니다"
|
||||
: "에이전트가 상세한 데이터를 작성합니다";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (EmptyStateTitle != null) EmptyStateTitle.Text = "대화 주제를 선택하세요";
|
||||
if (EmptyStateDesc != null) EmptyStateDesc.Text = "주제에 맞는 전문 프리셋이 자동 적용됩니다";
|
||||
}
|
||||
|
||||
if (_activeTab == "Code")
|
||||
{
|
||||
TopicButtonPanel.Visibility = Visibility.Collapsed;
|
||||
if (TopicPresetScrollViewer != null)
|
||||
TopicPresetScrollViewer.Visibility = Visibility.Collapsed;
|
||||
return;
|
||||
}
|
||||
|
||||
var presets = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets);
|
||||
var cardBackground = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent;
|
||||
var cardHoverBackground = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
|
||||
var cardBorder = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E5E7EB");
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
|
||||
void AttachTopicCardHover(Border card, Brush normalBackground, Brush hoverBackground)
|
||||
{
|
||||
card.MouseEnter += (sender, _) =>
|
||||
{
|
||||
if (sender is Border hovered)
|
||||
{
|
||||
hovered.Background = hoverBackground;
|
||||
hovered.BorderBrush = TryFindResource("AccentColor") as Brush ?? cardBorder;
|
||||
}
|
||||
};
|
||||
card.MouseLeave += (sender, _) =>
|
||||
{
|
||||
if (sender is Border hovered)
|
||||
{
|
||||
hovered.Background = normalBackground;
|
||||
hovered.BorderBrush = cardBorder;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var preset in presets)
|
||||
{
|
||||
var capturedPreset = preset;
|
||||
var buttonColor = BrushFromHex(preset.Color);
|
||||
|
||||
var border = new Border
|
||||
{
|
||||
Background = cardBackground,
|
||||
BorderBrush = cardBorder,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(14),
|
||||
Padding = new Thickness(14, 14, 14, 12),
|
||||
Margin = new Thickness(6, 6, 6, 8),
|
||||
Cursor = Cursors.Hand,
|
||||
Width = 148,
|
||||
Height = 124,
|
||||
ClipToBounds = true,
|
||||
};
|
||||
|
||||
var contentGrid = new Grid();
|
||||
var stack = new StackPanel
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
|
||||
var iconCircle = new Border
|
||||
{
|
||||
Width = 34,
|
||||
Height = 34,
|
||||
CornerRadius = new CornerRadius(17),
|
||||
Background = new SolidColorBrush(((SolidColorBrush)buttonColor).Color) { Opacity = 0.15 },
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 0, 9),
|
||||
};
|
||||
var iconBlock = new TextBlock
|
||||
{
|
||||
Text = preset.Symbol,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 15,
|
||||
Foreground = buttonColor,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
iconCircle.Child = iconBlock;
|
||||
stack.Children.Add(iconCircle);
|
||||
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = preset.Label,
|
||||
FontSize = 15,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
TextAlignment = TextAlignment.Center,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
MaxWidth = 112,
|
||||
});
|
||||
|
||||
if (capturedPreset.IsCustom)
|
||||
{
|
||||
contentGrid.Children.Add(stack);
|
||||
var badge = new Border
|
||||
{
|
||||
Width = 16,
|
||||
Height = 16,
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x60, 0xFF, 0xFF, 0xFF)),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
Margin = new Thickness(2, 2, 0, 0),
|
||||
};
|
||||
badge.Child = new TextBlock
|
||||
{
|
||||
Text = "\uE710",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 8,
|
||||
Foreground = buttonColor,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
contentGrid.Children.Add(badge);
|
||||
}
|
||||
else
|
||||
{
|
||||
contentGrid.Children.Add(stack);
|
||||
}
|
||||
|
||||
border.Child = contentGrid;
|
||||
AttachTopicCardHover(border, cardBackground, cardHoverBackground);
|
||||
border.MouseLeftButtonDown += (_, _) => SelectTopic(capturedPreset);
|
||||
|
||||
if (capturedPreset.IsCustom)
|
||||
{
|
||||
border.MouseRightButtonUp += (sender, args) =>
|
||||
{
|
||||
args.Handled = true;
|
||||
ShowCustomPresetContextMenu(sender as Border, capturedPreset);
|
||||
};
|
||||
}
|
||||
|
||||
TopicButtonPanel.Children.Add(border);
|
||||
}
|
||||
|
||||
var etcColor = BrushFromHex("#6B7280");
|
||||
var etcBorder = new Border
|
||||
{
|
||||
Background = cardBackground,
|
||||
BorderBrush = cardBorder,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(14),
|
||||
Padding = new Thickness(14, 14, 14, 12),
|
||||
Margin = new Thickness(6, 6, 6, 8),
|
||||
Cursor = Cursors.Hand,
|
||||
Width = 148,
|
||||
Height = 124,
|
||||
ClipToBounds = true,
|
||||
};
|
||||
|
||||
var etcGrid = new Grid();
|
||||
var etcStack = new StackPanel
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
|
||||
var etcIconCircle = new Border
|
||||
{
|
||||
Width = 34,
|
||||
Height = 34,
|
||||
CornerRadius = new CornerRadius(17),
|
||||
Background = new SolidColorBrush(((SolidColorBrush)etcColor).Color) { Opacity = 0.15 },
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 0, 9),
|
||||
};
|
||||
etcIconCircle.Child = new TextBlock
|
||||
{
|
||||
Text = "\uE70F",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 15,
|
||||
Foreground = etcColor,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
etcStack.Children.Add(etcIconCircle);
|
||||
|
||||
etcStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "기타",
|
||||
FontSize = 15,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
TextAlignment = TextAlignment.Center,
|
||||
});
|
||||
etcGrid.Children.Add(etcStack);
|
||||
etcBorder.Child = etcGrid;
|
||||
AttachTopicCardHover(etcBorder, cardBackground, cardHoverBackground);
|
||||
etcBorder.MouseLeftButtonDown += (_, _) =>
|
||||
{
|
||||
EmptyState.Visibility = Visibility.Collapsed;
|
||||
InputBox.Focus();
|
||||
};
|
||||
TopicButtonPanel.Children.Add(etcBorder);
|
||||
|
||||
var addBorder = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = cardBorder,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(14),
|
||||
Padding = new Thickness(14, 14, 14, 12),
|
||||
Margin = new Thickness(6, 6, 6, 8),
|
||||
Cursor = Cursors.Hand,
|
||||
Width = 148,
|
||||
Height = 124,
|
||||
ClipToBounds = true,
|
||||
};
|
||||
|
||||
var addGrid = new Grid();
|
||||
var addStack = new StackPanel
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
|
||||
addStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE710",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 18,
|
||||
Foreground = secondaryText,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Margin = new Thickness(0, 8, 0, 8),
|
||||
});
|
||||
addStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "프리셋 추가",
|
||||
FontSize = 14,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = secondaryText,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
|
||||
addGrid.Children.Add(addStack);
|
||||
addBorder.Child = addGrid;
|
||||
AttachTopicCardHover(addBorder, Brushes.Transparent, cardHoverBackground);
|
||||
addBorder.MouseLeftButtonDown += (_, _) => ShowCustomPresetDialog();
|
||||
TopicButtonPanel.Children.Add(addBorder);
|
||||
|
||||
UpdateTopicPresetScrollMode();
|
||||
}
|
||||
|
||||
private void UpdateTopicPresetScrollMode()
|
||||
{
|
||||
if (TopicPresetScrollViewer == null || TopicButtonPanel == null)
|
||||
return;
|
||||
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
if (TopicPresetScrollViewer == null || TopicButtonPanel == null)
|
||||
return;
|
||||
|
||||
TopicPresetScrollViewer.UpdateLayout();
|
||||
var shouldScroll = TopicPresetScrollViewer.ExtentHeight > TopicPresetScrollViewer.ViewportHeight + 1;
|
||||
TopicPresetScrollViewer.VerticalScrollBarVisibility = shouldScroll
|
||||
? ScrollBarVisibility.Auto
|
||||
: ScrollBarVisibility.Disabled;
|
||||
TopicPresetScrollViewer.Padding = shouldScroll
|
||||
? new Thickness(0, 2, 6, 0)
|
||||
: new Thickness(0, 2, 0, 0);
|
||||
}), System.Windows.Threading.DispatcherPriority.Loaded);
|
||||
}
|
||||
|
||||
private void ShowCustomPresetDialog(CustomPresetEntry? existing = null)
|
||||
{
|
||||
var dialog = new CustomPresetDialog(
|
||||
existingName: existing?.Label ?? "",
|
||||
existingDesc: existing?.Description ?? "",
|
||||
existingPrompt: existing?.SystemPrompt ?? "",
|
||||
existingColor: existing?.Color ?? "#6366F1",
|
||||
existingSymbol: existing?.Symbol ?? "\uE713",
|
||||
existingTab: existing?.Tab ?? _activeTab)
|
||||
{
|
||||
Owner = this,
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() != true)
|
||||
return;
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Label = dialog.PresetName;
|
||||
existing.Description = dialog.PresetDescription;
|
||||
existing.SystemPrompt = dialog.PresetSystemPrompt;
|
||||
existing.Color = dialog.PresetColor;
|
||||
existing.Symbol = dialog.PresetSymbol;
|
||||
existing.Tab = dialog.PresetTab;
|
||||
}
|
||||
else
|
||||
{
|
||||
_settings.Settings.Llm.CustomPresets.Add(new CustomPresetEntry
|
||||
{
|
||||
Label = dialog.PresetName,
|
||||
Description = dialog.PresetDescription,
|
||||
SystemPrompt = dialog.PresetSystemPrompt,
|
||||
Color = dialog.PresetColor,
|
||||
Symbol = dialog.PresetSymbol,
|
||||
Tab = dialog.PresetTab,
|
||||
});
|
||||
}
|
||||
|
||||
_settings.Save();
|
||||
BuildTopicButtons();
|
||||
}
|
||||
|
||||
private void ShowCustomPresetContextMenu(Border? anchor, Services.TopicPreset preset)
|
||||
{
|
||||
if (anchor == null || preset.CustomId == null)
|
||||
return;
|
||||
|
||||
var popup = new Popup
|
||||
{
|
||||
PlacementTarget = anchor,
|
||||
Placement = PlacementMode.Bottom,
|
||||
StaysOpen = false,
|
||||
AllowsTransparency = true,
|
||||
};
|
||||
|
||||
var menuBackground = TryFindResource("LauncherBackground") as Brush ?? Brushes.Black;
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
|
||||
var menuBorder = new Border
|
||||
{
|
||||
Background = menuBackground,
|
||||
CornerRadius = new CornerRadius(10),
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(4),
|
||||
MinWidth = 120,
|
||||
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||||
{
|
||||
BlurRadius = 12,
|
||||
ShadowDepth = 2,
|
||||
Opacity = 0.3,
|
||||
Color = Colors.Black,
|
||||
},
|
||||
};
|
||||
|
||||
var stack = new StackPanel();
|
||||
|
||||
var editItem = CreateContextMenuItem("\uE70F", "편집", primaryText);
|
||||
editItem.MouseLeftButtonDown += (_, _) =>
|
||||
{
|
||||
popup.IsOpen = false;
|
||||
var entry = _settings.Settings.Llm.CustomPresets.FirstOrDefault(item => item.Id == preset.CustomId);
|
||||
if (entry != null)
|
||||
ShowCustomPresetDialog(entry);
|
||||
};
|
||||
stack.Children.Add(editItem);
|
||||
|
||||
var deleteItem = CreateContextMenuItem("\uE74D", "삭제", new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)));
|
||||
deleteItem.MouseLeftButtonDown += (_, _) =>
|
||||
{
|
||||
popup.IsOpen = false;
|
||||
var result = CustomMessageBox.Show(
|
||||
$"'{preset.Label}' 프리셋을 삭제하시겠습니까?",
|
||||
"프리셋 삭제",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
if (result != MessageBoxResult.Yes)
|
||||
return;
|
||||
|
||||
_settings.Settings.Llm.CustomPresets.RemoveAll(item => item.Id == preset.CustomId);
|
||||
_settings.Save();
|
||||
BuildTopicButtons();
|
||||
};
|
||||
stack.Children.Add(deleteItem);
|
||||
|
||||
menuBorder.Child = stack;
|
||||
popup.Child = menuBorder;
|
||||
popup.IsOpen = true;
|
||||
}
|
||||
|
||||
private Border CreateContextMenuItem(string icon, string label, Brush foreground)
|
||||
{
|
||||
var item = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(6),
|
||||
Padding = new Thickness(10, 6, 14, 6),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
|
||||
var stack = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 13,
|
||||
Foreground = foreground,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
});
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 13,
|
||||
Foreground = foreground,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
item.Child = stack;
|
||||
|
||||
var hoverBackground = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
|
||||
item.MouseEnter += (sender, _) =>
|
||||
{
|
||||
if (sender is Border hovered)
|
||||
hovered.Background = hoverBackground;
|
||||
};
|
||||
item.MouseLeave += (sender, _) =>
|
||||
{
|
||||
if (sender is Border hovered)
|
||||
hovered.Background = Brushes.Transparent;
|
||||
};
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private Border CreateContextMenuItem(string icon, string label, Brush foreground, Brush secondaryForeground)
|
||||
=> CreateContextMenuItem(icon, label, foreground);
|
||||
|
||||
private void SelectTopic(Services.TopicPreset preset)
|
||||
{
|
||||
bool hasConversation;
|
||||
bool hasMessages;
|
||||
lock (_convLock)
|
||||
{
|
||||
hasConversation = _currentConversation != null;
|
||||
hasMessages = _currentConversation?.Messages.Count > 0;
|
||||
}
|
||||
|
||||
var hasInput = !string.IsNullOrEmpty(InputBox.Text);
|
||||
if (!hasConversation)
|
||||
StartNewConversation();
|
||||
|
||||
lock (_convLock)
|
||||
{
|
||||
if (_currentConversation == null)
|
||||
return;
|
||||
|
||||
var session = ChatSession;
|
||||
if (session != null)
|
||||
{
|
||||
_currentConversation = session.UpdateConversationMetadata(_activeTab, conversation =>
|
||||
{
|
||||
conversation.SystemCommand = preset.SystemPrompt;
|
||||
conversation.Category = preset.Category;
|
||||
}, _storage);
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentConversation.SystemCommand = preset.SystemPrompt;
|
||||
_currentConversation.Category = preset.Category;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateCategoryLabel();
|
||||
SaveConversationSettings();
|
||||
RefreshConversationList();
|
||||
UpdateSelectedPresetGuide();
|
||||
if (EmptyState != null)
|
||||
EmptyState.Visibility = Visibility.Collapsed;
|
||||
|
||||
InputBox.Focus();
|
||||
|
||||
if (!string.IsNullOrEmpty(preset.Placeholder))
|
||||
{
|
||||
_promptCardPlaceholder = preset.Placeholder;
|
||||
if (!hasMessages && !hasInput)
|
||||
ShowPlaceholder();
|
||||
}
|
||||
|
||||
if (hasMessages || hasInput)
|
||||
ShowToast($"프리셋 변경: {preset.Label}");
|
||||
|
||||
if (_activeTab == "Cowork")
|
||||
BuildBottomBar();
|
||||
}
|
||||
}
|
||||
26
src/AxCopilot/Views/ChatWindow.TranscriptPolicy.cs
Normal file
26
src/AxCopilot/Views/ChatWindow.TranscriptPolicy.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Windows.Media;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private bool IsDebugTranscriptMode()
|
||||
=> string.Equals(_settings.Settings.Llm.AgentLogLevel, "debug", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private string GetTranscriptBadgeLabel(AgentEvent evt)
|
||||
=> AgentTranscriptDisplayCatalog.GetEventBadgeLabel(evt);
|
||||
|
||||
private string GetTranscriptTaskCategory(TaskRunStore.TaskRun task)
|
||||
=> AgentTranscriptDisplayCatalog.GetTaskCategoryLabel(task.Kind, task.Title);
|
||||
|
||||
private string GetTranscriptDisplayName(string? rawName, bool slashPrefix = false)
|
||||
=> AgentTranscriptDisplayCatalog.GetDisplayName(rawName, slashPrefix);
|
||||
|
||||
private string GetTranscriptEventSummary(AgentEvent evt, string displayName)
|
||||
=> AgentTranscriptDisplayCatalog.BuildEventSummary(evt, displayName);
|
||||
|
||||
private bool ShouldIncludeRecentTaskSummary(IReadOnlyCollection<TaskRunStore.TaskRun> activeTasks)
|
||||
=> IsDebugTranscriptMode() || activeTasks.Count == 0;
|
||||
}
|
||||
252
src/AxCopilot/Views/ChatWindow.UserAskPresentation.cs
Normal file
252
src/AxCopilot/Views/ChatWindow.UserAskPresentation.cs
Normal file
@@ -0,0 +1,252 @@
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private async Task<string?> ShowInlineUserAskAsync(string question, List<string> options, string defaultValue)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string?>();
|
||||
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
AddUserAskCard(question, options, defaultValue, tcs);
|
||||
});
|
||||
|
||||
var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromMinutes(5)));
|
||||
if (completed != tcs.Task)
|
||||
{
|
||||
await Dispatcher.InvokeAsync(RemoveUserAskCard);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await tcs.Task;
|
||||
}
|
||||
|
||||
private void RemoveUserAskCard()
|
||||
{
|
||||
if (_userAskCard == null)
|
||||
return;
|
||||
|
||||
MessagePanel.Children.Remove(_userAskCard);
|
||||
_userAskCard = null;
|
||||
}
|
||||
|
||||
private void AddUserAskCard(string question, List<string> options, string defaultValue, TaskCompletionSource<string?> tcs)
|
||||
{
|
||||
RemoveUserAskCard();
|
||||
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var accentColor = ((SolidColorBrush)accentBrush).Color;
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x24, accentColor.R, accentColor.G, accentColor.B));
|
||||
var itemBg = TryFindResource("ItemBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x10, accentColor.R, accentColor.G, accentColor.B));
|
||||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x16, 0xFF, 0xFF, 0xFF));
|
||||
var okBrush = BrushFromHex("#10B981");
|
||||
var dangerBrush = BrushFromHex("#EF4444");
|
||||
|
||||
var container = new Border
|
||||
{
|
||||
Margin = new Thickness(40, 4, 90, 8),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
MaxWidth = Math.Max(420, GetMessageMaxWidth() - 36),
|
||||
Background = itemBg,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(14),
|
||||
Padding = new Thickness(14, 12, 14, 12),
|
||||
};
|
||||
|
||||
var outer = new StackPanel();
|
||||
outer.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "의견 요청",
|
||||
FontSize = 12.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
});
|
||||
outer.Children.Add(new TextBlock
|
||||
{
|
||||
Text = question,
|
||||
Margin = new Thickness(0, 4, 0, 10),
|
||||
FontSize = 12.5,
|
||||
Foreground = primaryText,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
LineHeight = 20,
|
||||
});
|
||||
|
||||
Border? selectedOption = null;
|
||||
string selectedResponse = defaultValue;
|
||||
|
||||
if (options.Count > 0)
|
||||
{
|
||||
var optionPanel = new WrapPanel
|
||||
{
|
||||
Margin = new Thickness(0, 0, 0, 10),
|
||||
ItemWidth = double.NaN,
|
||||
};
|
||||
|
||||
foreach (var option in options.Where(static option => !string.IsNullOrWhiteSpace(option)))
|
||||
{
|
||||
var optionLabel = option.Trim();
|
||||
var optBorder = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Padding = new Thickness(10, 6, 10, 6),
|
||||
Margin = new Thickness(0, 0, 8, 8),
|
||||
Cursor = Cursors.Hand,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = optionLabel,
|
||||
FontSize = 12,
|
||||
Foreground = primaryText,
|
||||
},
|
||||
};
|
||||
|
||||
optBorder.MouseEnter += (s, _) =>
|
||||
{
|
||||
if (!ReferenceEquals(selectedOption, s))
|
||||
((Border)s).Background = hoverBg;
|
||||
};
|
||||
optBorder.MouseLeave += (s, _) =>
|
||||
{
|
||||
if (!ReferenceEquals(selectedOption, s))
|
||||
((Border)s).Background = Brushes.Transparent;
|
||||
};
|
||||
optBorder.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
if (selectedOption != null)
|
||||
{
|
||||
selectedOption.Background = Brushes.Transparent;
|
||||
selectedOption.BorderBrush = borderBrush;
|
||||
}
|
||||
|
||||
selectedOption = optBorder;
|
||||
selectedOption.Background = new SolidColorBrush(Color.FromArgb(0x18, accentColor.R, accentColor.G, accentColor.B));
|
||||
selectedOption.BorderBrush = accentBrush;
|
||||
selectedResponse = optionLabel;
|
||||
};
|
||||
|
||||
optionPanel.Children.Add(optBorder);
|
||||
}
|
||||
|
||||
outer.Children.Add(optionPanel);
|
||||
}
|
||||
|
||||
outer.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "직접 입력",
|
||||
FontSize = 11.5,
|
||||
Foreground = secondaryText,
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
});
|
||||
|
||||
var inputBox = new TextBox
|
||||
{
|
||||
Text = defaultValue,
|
||||
AcceptsReturn = true,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
MinHeight = 42,
|
||||
MaxHeight = 100,
|
||||
FontSize = 12.5,
|
||||
Padding = new Thickness(10, 8, 10, 8),
|
||||
Background = Brushes.Transparent,
|
||||
Foreground = primaryText,
|
||||
CaretBrush = primaryText,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
};
|
||||
inputBox.TextChanged += (_, _) =>
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(inputBox.Text))
|
||||
{
|
||||
selectedResponse = inputBox.Text.Trim();
|
||||
if (selectedOption != null)
|
||||
{
|
||||
selectedOption.Background = Brushes.Transparent;
|
||||
selectedOption.BorderBrush = borderBrush;
|
||||
selectedOption = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
outer.Children.Add(inputBox);
|
||||
|
||||
var buttonRow = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(0, 12, 0, 0),
|
||||
};
|
||||
|
||||
Border BuildActionButton(string label, Brush bg, Brush fg)
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Background = bg,
|
||||
BorderBrush = bg,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Padding = new Thickness(12, 7, 12, 7),
|
||||
Margin = new Thickness(8, 0, 0, 0),
|
||||
Cursor = Cursors.Hand,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = fg,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
var cancelBtn = BuildActionButton("취소", Brushes.Transparent, dangerBrush);
|
||||
cancelBtn.BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, 0xEF, 0x44, 0x44));
|
||||
cancelBtn.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
RemoveUserAskCard();
|
||||
tcs.TrySetResult(null);
|
||||
};
|
||||
buttonRow.Children.Add(cancelBtn);
|
||||
|
||||
var submitBtn = BuildActionButton("전달", okBrush, Brushes.White);
|
||||
submitBtn.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
var finalResponse = !string.IsNullOrWhiteSpace(inputBox.Text)
|
||||
? inputBox.Text.Trim()
|
||||
: selectedResponse?.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(finalResponse))
|
||||
finalResponse = defaultValue?.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(finalResponse))
|
||||
return;
|
||||
|
||||
RemoveUserAskCard();
|
||||
tcs.TrySetResult(finalResponse);
|
||||
};
|
||||
buttonRow.Children.Add(submitBtn);
|
||||
|
||||
outer.Children.Add(buttonRow);
|
||||
container.Child = outer;
|
||||
_userAskCard = container;
|
||||
|
||||
container.Opacity = 0;
|
||||
container.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(180)));
|
||||
MessagePanel.Children.Add(container);
|
||||
ForceScrollToEnd();
|
||||
inputBox.Focus();
|
||||
inputBox.CaretIndex = inputBox.Text.Length;
|
||||
}
|
||||
}
|
||||
259
src/AxCopilot/Views/ChatWindow.VisualInteractionHelpers.cs
Normal file
259
src/AxCopilot/Views/ChatWindow.VisualInteractionHelpers.cs
Normal file
@@ -0,0 +1,259 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
/// <summary>마우스 오버 시 살짝 확대하는 호버 애니메이션. 독립적 공간이 있는 버튼에만 적용합니다.</summary>
|
||||
private static void ApplyHoverScaleAnimation(FrameworkElement element, double hoverScale = 1.08)
|
||||
{
|
||||
void EnsureTransform()
|
||||
{
|
||||
element.RenderTransformOrigin = new Point(0.5, 0.5);
|
||||
if (element.RenderTransform is not ScaleTransform || element.RenderTransform.IsFrozen)
|
||||
element.RenderTransform = new ScaleTransform(1, 1);
|
||||
}
|
||||
|
||||
element.Loaded += (_, _) => EnsureTransform();
|
||||
|
||||
element.MouseEnter += (_, _) =>
|
||||
{
|
||||
EnsureTransform();
|
||||
var st = (ScaleTransform)element.RenderTransform;
|
||||
var grow = new DoubleAnimation(hoverScale, TimeSpan.FromMilliseconds(150))
|
||||
{
|
||||
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
|
||||
};
|
||||
st.BeginAnimation(ScaleTransform.ScaleXProperty, grow);
|
||||
st.BeginAnimation(ScaleTransform.ScaleYProperty, grow);
|
||||
};
|
||||
|
||||
element.MouseLeave += (_, _) =>
|
||||
{
|
||||
EnsureTransform();
|
||||
var st = (ScaleTransform)element.RenderTransform;
|
||||
var shrink = new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(200))
|
||||
{
|
||||
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
|
||||
};
|
||||
st.BeginAnimation(ScaleTransform.ScaleXProperty, shrink);
|
||||
st.BeginAnimation(ScaleTransform.ScaleYProperty, shrink);
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>마우스 오버 시 텍스트가 살짝 튀어오르는 바운스 애니메이션.</summary>
|
||||
private static void ApplyHoverBounceAnimation(FrameworkElement element, double bounceY = -2.5)
|
||||
{
|
||||
void EnsureTransform()
|
||||
{
|
||||
if (element.RenderTransform is not TranslateTransform || element.RenderTransform.IsFrozen)
|
||||
element.RenderTransform = new TranslateTransform(0, 0);
|
||||
}
|
||||
|
||||
element.Loaded += (_, _) => EnsureTransform();
|
||||
|
||||
element.MouseEnter += (_, _) =>
|
||||
{
|
||||
EnsureTransform();
|
||||
var tt = (TranslateTransform)element.RenderTransform;
|
||||
tt.BeginAnimation(
|
||||
TranslateTransform.YProperty,
|
||||
new DoubleAnimation(bounceY, TimeSpan.FromMilliseconds(200))
|
||||
{
|
||||
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
|
||||
});
|
||||
};
|
||||
|
||||
element.MouseLeave += (_, _) =>
|
||||
{
|
||||
EnsureTransform();
|
||||
var tt = (TranslateTransform)element.RenderTransform;
|
||||
tt.BeginAnimation(
|
||||
TranslateTransform.YProperty,
|
||||
new DoubleAnimation(0, TimeSpan.FromMilliseconds(250))
|
||||
{
|
||||
EasingFunction = new ElasticEase
|
||||
{
|
||||
EasingMode = EasingMode.EaseOut,
|
||||
Oscillations = 1,
|
||||
Springiness = 10
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>심플한 V 체크 아이콘을 생성합니다.</summary>
|
||||
private static FrameworkElement CreateSimpleCheck(Brush color, double size = 14)
|
||||
{
|
||||
return new System.Windows.Shapes.Path
|
||||
{
|
||||
Data = Geometry.Parse($"M {size * 0.15} {size * 0.5} L {size * 0.4} {size * 0.75} L {size * 0.85} {size * 0.28}"),
|
||||
Stroke = color,
|
||||
StrokeThickness = 2,
|
||||
StrokeStartLineCap = PenLineCap.Round,
|
||||
StrokeEndLineCap = PenLineCap.Round,
|
||||
StrokeLineJoin = PenLineJoin.Round,
|
||||
Width = size,
|
||||
Height = size,
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>팝업 메뉴 항목에 호버 배경색 + 미세 확대 효과를 적용합니다.</summary>
|
||||
private static void ApplyMenuItemHover(Border item)
|
||||
{
|
||||
var originalBg = item.Background?.Clone() ?? Brushes.Transparent;
|
||||
if (originalBg.CanFreeze)
|
||||
originalBg.Freeze();
|
||||
|
||||
item.RenderTransformOrigin = new Point(0.5, 0.5);
|
||||
item.RenderTransform = new ScaleTransform(1, 1);
|
||||
|
||||
item.MouseEnter += (s, _) =>
|
||||
{
|
||||
if (s is Border b)
|
||||
{
|
||||
if (originalBg is SolidColorBrush scb && scb.Color.A > 0x20)
|
||||
b.Opacity = 0.85;
|
||||
else
|
||||
b.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
}
|
||||
|
||||
var st = item.RenderTransform as ScaleTransform;
|
||||
st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
|
||||
st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
|
||||
};
|
||||
|
||||
item.MouseLeave += (s, _) =>
|
||||
{
|
||||
if (s is Border b)
|
||||
{
|
||||
b.Opacity = 1.0;
|
||||
b.Background = originalBg;
|
||||
}
|
||||
|
||||
var st = item.RenderTransform as ScaleTransform;
|
||||
st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
|
||||
st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
|
||||
};
|
||||
}
|
||||
|
||||
private Button CreateActionButton(string symbol, string tooltip, Brush foreground, Action onClick)
|
||||
{
|
||||
var hoverBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x10, 0xFF, 0xFF, 0xFF));
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var icon = new TextBlock
|
||||
{
|
||||
Text = symbol,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 10,
|
||||
Foreground = foreground,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
|
||||
var btn = new Button
|
||||
{
|
||||
Content = icon,
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
Cursor = Cursors.Hand,
|
||||
Width = 24,
|
||||
Height = 24,
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0, 0, 2, 0),
|
||||
ToolTip = tooltip
|
||||
};
|
||||
|
||||
btn.Template = BuildMinimalIconButtonTemplate();
|
||||
btn.MouseEnter += (_, _) =>
|
||||
{
|
||||
icon.Foreground = hoverBrush;
|
||||
btn.Background = hoverBg;
|
||||
};
|
||||
btn.MouseLeave += (_, _) =>
|
||||
{
|
||||
icon.Foreground = foreground;
|
||||
btn.Background = Brushes.Transparent;
|
||||
};
|
||||
btn.Click += (_, _) => onClick();
|
||||
return btn;
|
||||
}
|
||||
|
||||
private void ShowMessageActionBar(StackPanel actionBar)
|
||||
{
|
||||
if (actionBar == null)
|
||||
return;
|
||||
|
||||
actionBar.Opacity = 1;
|
||||
}
|
||||
|
||||
private void HideMessageActionBarIfNotSelected(StackPanel actionBar)
|
||||
{
|
||||
if (actionBar == null)
|
||||
return;
|
||||
|
||||
if (!ReferenceEquals(_selectedMessageActionBar, actionBar))
|
||||
actionBar.Opacity = 0;
|
||||
}
|
||||
|
||||
private void SelectMessageActionBar(StackPanel actionBar, Border? messageBorder = null)
|
||||
{
|
||||
if (_selectedMessageActionBar != null && !ReferenceEquals(_selectedMessageActionBar, actionBar))
|
||||
_selectedMessageActionBar.Opacity = 0;
|
||||
|
||||
if (_selectedMessageBorder != null && !ReferenceEquals(_selectedMessageBorder, messageBorder))
|
||||
ApplyMessageSelectionStyle(_selectedMessageBorder, false);
|
||||
|
||||
_selectedMessageActionBar = actionBar;
|
||||
_selectedMessageActionBar.Opacity = 1;
|
||||
_selectedMessageBorder = messageBorder;
|
||||
if (_selectedMessageBorder != null)
|
||||
ApplyMessageSelectionStyle(_selectedMessageBorder, true);
|
||||
}
|
||||
|
||||
private void ApplyMessageSelectionStyle(Border border, bool selected)
|
||||
{
|
||||
if (border == null)
|
||||
return;
|
||||
|
||||
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var defaultBorder = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
border.BorderBrush = selected ? accent : defaultBorder;
|
||||
border.BorderThickness = selected ? new Thickness(1.5) : new Thickness(1);
|
||||
border.Effect = selected
|
||||
? new System.Windows.Media.Effects.DropShadowEffect
|
||||
{
|
||||
BlurRadius = 16,
|
||||
ShadowDepth = 0,
|
||||
Opacity = 0.10,
|
||||
Color = Colors.Black,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
private static ControlTemplate BuildMinimalIconButtonTemplate()
|
||||
{
|
||||
var template = new ControlTemplate(typeof(Button));
|
||||
var border = new FrameworkElementFactory(typeof(Border));
|
||||
border.SetValue(Border.BackgroundProperty, new TemplateBindingExtension(Button.BackgroundProperty));
|
||||
border.SetValue(Border.BorderBrushProperty, new TemplateBindingExtension(Button.BorderBrushProperty));
|
||||
border.SetValue(Border.BorderThicknessProperty, new TemplateBindingExtension(Button.BorderThicknessProperty));
|
||||
border.SetValue(Border.CornerRadiusProperty, new CornerRadius(8));
|
||||
border.SetValue(Border.PaddingProperty, new TemplateBindingExtension(Button.PaddingProperty));
|
||||
var presenter = new FrameworkElementFactory(typeof(ContentPresenter));
|
||||
presenter.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Center);
|
||||
presenter.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
|
||||
border.AppendChild(presenter);
|
||||
template.VisualTree = border;
|
||||
return template;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -581,48 +581,19 @@ internal sealed class PermissionRequestWindow : Window
|
||||
Brush border,
|
||||
UiExpressionProfile uiProfile)
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Background = itemBg,
|
||||
BorderBrush = border,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(10, 8, 10, 8),
|
||||
Margin = new Thickness(0, 8, 0, 0),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = title,
|
||||
FontSize = 11.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primary,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = summary,
|
||||
FontSize = 11,
|
||||
Foreground = secondary,
|
||||
Margin = new Thickness(0, 2, 0, 6),
|
||||
},
|
||||
new TextBox
|
||||
{
|
||||
Text = content,
|
||||
IsReadOnly = true,
|
||||
BorderThickness = new Thickness(0),
|
||||
Background = Brushes.Transparent,
|
||||
Foreground = primary,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
FontSize = 11,
|
||||
MaxHeight = uiProfile.PreviewMaxHeight,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return AgentPreviewSurfaceFactory.CreateSurface(
|
||||
title,
|
||||
summary,
|
||||
AgentPreviewSurfaceFactory.CreatePreviewBox(
|
||||
content,
|
||||
primary,
|
||||
secondary,
|
||||
border,
|
||||
uiProfile.PreviewMaxHeight),
|
||||
primary,
|
||||
secondary,
|
||||
itemBg,
|
||||
border);
|
||||
}
|
||||
|
||||
private static Border BuildFileEditPreviewCard(
|
||||
@@ -679,41 +650,19 @@ internal sealed class PermissionRequestWindow : Window
|
||||
});
|
||||
}
|
||||
|
||||
return new Border
|
||||
{
|
||||
Background = itemBg,
|
||||
BorderBrush = border,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(10, 8, 10, 8),
|
||||
Margin = new Thickness(0, 8, 0, 0),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = title,
|
||||
FontSize = 11.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primary,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = summary,
|
||||
FontSize = 11,
|
||||
Foreground = secondary,
|
||||
Margin = new Thickness(0, 2, 0, 6),
|
||||
},
|
||||
new ScrollViewer
|
||||
{
|
||||
MaxHeight = uiProfile.PreviewMaxHeight,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
Content = body,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return AgentPreviewSurfaceFactory.CreateSurface(
|
||||
title,
|
||||
summary,
|
||||
AgentPreviewSurfaceFactory.CreatePreviewBox(
|
||||
string.Join("\n", body.Children.OfType<TextBlock>().Select(tb => tb.Text)),
|
||||
primary,
|
||||
secondary,
|
||||
border,
|
||||
uiProfile.PreviewMaxHeight),
|
||||
primary,
|
||||
secondary,
|
||||
itemBg,
|
||||
border);
|
||||
}
|
||||
|
||||
private static Border BuildFileWriteTwoColumnPreviewCard(
|
||||
@@ -764,36 +713,14 @@ internal sealed class PermissionRequestWindow : Window
|
||||
Grid.SetColumn(newPanel, 2);
|
||||
grid.Children.Add(newPanel);
|
||||
|
||||
return new Border
|
||||
{
|
||||
Background = itemBg,
|
||||
BorderBrush = border,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(10, 8, 10, 8),
|
||||
Margin = new Thickness(0, 8, 0, 0),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = title,
|
||||
FontSize = 11.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primary,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = summary,
|
||||
FontSize = 11,
|
||||
Foreground = secondary,
|
||||
Margin = new Thickness(0, 2, 0, 6),
|
||||
},
|
||||
grid,
|
||||
}
|
||||
}
|
||||
};
|
||||
return AgentPreviewSurfaceFactory.CreateSurface(
|
||||
title,
|
||||
summary,
|
||||
grid,
|
||||
primary,
|
||||
secondary,
|
||||
itemBg,
|
||||
border);
|
||||
}
|
||||
|
||||
private static Border BuildWriteColumn(
|
||||
@@ -1161,8 +1088,8 @@ internal sealed class PermissionRequestWindow : Window
|
||||
Owner = owner,
|
||||
};
|
||||
|
||||
if (owner.Resources.MergedDictionaries.Count > 0)
|
||||
dialog.Resources.MergedDictionaries.Add(owner.Resources);
|
||||
dialog.WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
dialog.Resources.MergedDictionaries.Add(owner.Resources);
|
||||
|
||||
dialog.ShowDialog();
|
||||
return dialog._result;
|
||||
|
||||
@@ -287,7 +287,7 @@ internal sealed class PlanViewerWindow : Window
|
||||
// 공개 API
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
public Task<string?> ShowPlanAsync(string planText, List<string> steps, TaskCompletionSource<string?> tcs)
|
||||
public void LoadPlan(string planText, List<string> steps, TaskCompletionSource<string?> tcs)
|
||||
{
|
||||
_planText = planText;
|
||||
_steps = steps;
|
||||
@@ -304,7 +304,30 @@ internal sealed class PlanViewerWindow : Window
|
||||
RenderSteps();
|
||||
BuildApprovalButtons();
|
||||
_statusBar.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public void LoadPlanPreview(string planText, List<string> steps)
|
||||
{
|
||||
_planText = planText;
|
||||
_steps = steps;
|
||||
_tcs = null;
|
||||
_currentStep = -1;
|
||||
_isExecuting = false;
|
||||
_expandedSteps.Clear();
|
||||
if (_uiExpressionLevel == "rich")
|
||||
{
|
||||
for (int i = 0; i < _steps.Count; i++)
|
||||
_expandedSteps.Add(i);
|
||||
}
|
||||
|
||||
RenderSteps();
|
||||
BuildCloseButton();
|
||||
_statusBar.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public Task<string?> ShowPlanAsync(string planText, List<string> steps, TaskCompletionSource<string?> tcs)
|
||||
{
|
||||
LoadPlan(planText, steps, tcs);
|
||||
Show();
|
||||
Activate();
|
||||
return tcs.Task;
|
||||
|
||||
@@ -4697,49 +4697,6 @@
|
||||
</WrapPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border Style="{StaticResource AgentSettingsRow}">
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Left" Margin="0,0,180,0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Style="{StaticResource RowLabel}" Text="플랜 모드"/>
|
||||
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
|
||||
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border.ToolTip>
|
||||
<ToolTip Style="{StaticResource HelpTooltipStyle}">
|
||||
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18">
|
||||
AI가 도구를 실행하기 전에 실행 계획을 먼저 생성합니다.
|
||||
<LineBreak/><LineBreak/>• Off — 기존 동작 (계획 없이 바로 실행)
|
||||
<LineBreak/>• Always — 항상 계획 생성 후 승인 대기
|
||||
<LineBreak/>• Auto — 복잡한 작업 감지 시 자동 계획
|
||||
<LineBreak/><LineBreak/>계획을 검토·수정·승인한 후 실행됩니다.
|
||||
</TextBlock>
|
||||
</ToolTip>
|
||||
</Border.ToolTip>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<TextBlock Style="{StaticResource RowHint}" Text="도구 실행 전 구조화된 실행 계획 생성 및 사용자 승인"/>
|
||||
</StackPanel>
|
||||
<WrapPanel HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<RadioButton x:Name="AgentPlanModeCardOff"
|
||||
Content="Off"
|
||||
GroupName="AgentPlanModeCard"
|
||||
Style="{StaticResource AgentSubTabStyle}"
|
||||
Margin="0,0,6,0"
|
||||
Checked="AgentPlanModeCard_Checked"/>
|
||||
<RadioButton x:Name="AgentPlanModeCardAlways"
|
||||
Content="Always"
|
||||
GroupName="AgentPlanModeCard"
|
||||
Style="{StaticResource AgentSubTabStyle}"
|
||||
Margin="0,0,6,0"
|
||||
Checked="AgentPlanModeCard_Checked"/>
|
||||
<RadioButton x:Name="AgentPlanModeCardAuto"
|
||||
Content="Auto"
|
||||
GroupName="AgentPlanModeCard"
|
||||
Style="{StaticResource AgentSubTabStyle}"
|
||||
Checked="AgentPlanModeCard_Checked"/>
|
||||
</WrapPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border Style="{StaticResource AgentSettingsRow}">
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||
@@ -5000,22 +4957,6 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<TextBlock Style="{StaticResource SectionHeader}" Text="폴더 데이터 활용"/>
|
||||
<Border Style="{StaticResource SettingsRow}">
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Left">
|
||||
<TextBlock Style="{StaticResource RowLabel}" Text="기본 데이터 활용 수준"/>
|
||||
<TextBlock Style="{StaticResource RowHint}" Text="작업 폴더 내 문서/데이터를 보고서 작성에 활용하는 수준입니다."/>
|
||||
</StackPanel>
|
||||
<ComboBox HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
Width="180" SelectedValue="{Binding FolderDataUsage, Mode=TwoWay}"
|
||||
SelectedValuePath="Tag">
|
||||
<ComboBoxItem Content="활용하지 않음" Tag="none"/>
|
||||
<ComboBoxItem Content="소극 활용 (요청 시)" Tag="passive"/>
|
||||
<ComboBoxItem Content="적극 활용 (자동 탐색)" Tag="active"/>
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── 품질 검증 ── -->
|
||||
<TextBlock Style="{StaticResource SectionHeader}" Text="품질 검증"/>
|
||||
@@ -5154,16 +5095,6 @@
|
||||
|
||||
<!-- ── Agentic 도구 설정 ── -->
|
||||
<TextBlock Style="{StaticResource SectionHeader}" Text="Agentic 도구"/>
|
||||
<Border Style="{StaticResource SettingsRow}">
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||
<TextBlock Style="{StaticResource RowLabel}" Text="Plan Mode 도구"/>
|
||||
<TextBlock Style="{StaticResource RowHint}" Text="EnterPlanMode/ExitPlanMode 도구를 사용할지 설정합니다."/>
|
||||
</StackPanel>
|
||||
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
IsChecked="{Binding Code.EnablePlanModeTools, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border Style="{StaticResource SettingsRow}">
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||
|
||||
@@ -126,11 +126,6 @@ public partial class SettingsWindow : Window
|
||||
if (AgentDecisionCardNormal != null) AgentDecisionCardNormal.IsChecked = decision == "normal";
|
||||
if (AgentDecisionCardDetailed != null) AgentDecisionCardDetailed.IsChecked = decision == "detailed";
|
||||
|
||||
var planMode = (_vm.PlanMode ?? "off").Trim().ToLowerInvariant();
|
||||
if (AgentPlanModeCardOff != null) AgentPlanModeCardOff.IsChecked = planMode == "off";
|
||||
if (AgentPlanModeCardAlways != null) AgentPlanModeCardAlways.IsChecked = planMode == "always";
|
||||
if (AgentPlanModeCardAuto != null) AgentPlanModeCardAuto.IsChecked = planMode == "auto";
|
||||
|
||||
var operationMode = OperationModePolicy.Normalize(_vm.OperationMode);
|
||||
if (AgentOperationModeInternal != null) AgentOperationModeInternal.IsChecked = operationMode == OperationModePolicy.InternalMode;
|
||||
if (AgentOperationModeExternal != null) AgentOperationModeExternal.IsChecked = operationMode == OperationModePolicy.ExternalMode;
|
||||
@@ -2214,17 +2209,6 @@ public partial class SettingsWindow : Window
|
||||
};
|
||||
}
|
||||
|
||||
private void AgentPlanModeCard_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true) return;
|
||||
_vm.PlanMode = rb.Name switch
|
||||
{
|
||||
"AgentPlanModeCardAlways" => "always",
|
||||
"AgentPlanModeCardAuto" => "auto",
|
||||
_ => "off",
|
||||
};
|
||||
}
|
||||
|
||||
private void AgentOperationModeCard_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true)
|
||||
|
||||
@@ -578,6 +578,18 @@ public partial class SkillGalleryWindow : Window
|
||||
AddMetaRow("런타임", skill.Requires, metaRow++);
|
||||
if (!string.IsNullOrEmpty(skill.AllowedTools))
|
||||
AddMetaRow("허용 도구", skill.AllowedTools, metaRow++);
|
||||
if (!string.IsNullOrEmpty(skill.Model))
|
||||
AddMetaRow("모델", skill.Model, metaRow++);
|
||||
if (!string.IsNullOrEmpty(skill.Effort))
|
||||
AddMetaRow("추론 강도", skill.Effort, metaRow++);
|
||||
if (!string.IsNullOrEmpty(skill.ExecutionContext))
|
||||
AddMetaRow("실행 컨텍스트", skill.ExecutionContext, metaRow++);
|
||||
if (!string.IsNullOrEmpty(skill.Agent))
|
||||
AddMetaRow("에이전트", skill.Agent, metaRow++);
|
||||
if (skill.DisableModelInvocation)
|
||||
AddMetaRow("모델 호출", "비활성화", metaRow++);
|
||||
if (!string.IsNullOrEmpty(skill.WhenToUse))
|
||||
AddMetaRow("추천 상황", skill.WhenToUse, metaRow++);
|
||||
AddMetaRow("상태", skill.IsAvailable ? "✓ 사용 가능" : $"✗ {skill.UnavailableHint}", metaRow++);
|
||||
AddMetaRow("경로", skill.FilePath, metaRow++);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user