Compare commits

...

147 Commits

Author SHA1 Message Date
571d4bfaca 권한·도구 결과 카테고리 표시 강화
Some checks are pending
Release Gate / gate (push) Waiting to run
transcript 카드에 작업 종류 chip을 추가해 권한 요청과 도구 결과의 성격을 더 빠르게 구분할 수 있게 했습니다.

명령 실행, 파일 수정, 웹 요청, Git, 문서, 스킬, MCP 등의 카테고리를 상태 chip과 함께 표시하도록 정리했습니다.

README와 DEVELOPMENT 문서를 갱신했고 dotnet build 기준 경고 0 / 오류 0을 확인했습니다.
2026-04-06 13:26:20 +09:00
fdf95aa6ec 권한·도구 결과 카드 시각 구분 강화
권한 요청과 도구 결과 transcript chip을 상태별 색상으로 분리했습니다.

미리보기 권장, 주의 필요, 검토 권장, 승인 후 계속, 후속 점검을 색과 라벨로 더 즉시 구분되게 정리했습니다.

README와 DEVELOPMENT 문서를 갱신했고 dotnet build 기준 경고 0 / 오류 0을 확인했습니다.
2026-04-06 13:14:07 +09:00
7fd4d1ae5a 권한·도구 결과 transcript 안내 강화
Some checks failed
Release Gate / gate (push) Has been cancelled
권한 요청과 도구 결과 metadata를 실제 transcript 카드에 반영했습니다.

ActionHint, Severity, RequiresPreview, FollowUpHint, NeedsAttention, StatusKind를 이용해 보조 문구와 주의 chip을 표시하도록 정리했습니다.

README와 DEVELOPMENT 문서를 갱신했고 dotnet build 기준 경고 0 / 오류 0을 확인했습니다.
2026-04-06 13:03:18 +09:00
e747032501 도구·권한·스킬 표현 정교화 1차 반영
Some checks failed
Release Gate / gate (push) Has been cancelled
- 권한 요청 카탈로그를 bash/powershell/web_fetch/mcp/skill/file_edit/file_write/git/document/filesystem 수준으로 세분화했습니다.

- 도구 결과 카탈로그에 approval_required, partial, follow-up hint, attention 메타를 추가해 후속 renderer 고도화 기반을 마련했습니다.

- 스킬 갤러리에 모델, 추론 강도, 실행 컨텍스트, 에이전트, 모델 호출 비활성화, 추천 상황을 표시하도록 확장했습니다.

- README, DEVELOPMENT, parity plan, regression prompts 문서를 2026-04-06 11:52 (KST) 기준으로 갱신했습니다.

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-06 12:57:15 +09:00
f11b8b74b7 AX Agent footer 작업 바 chip 스타일 통일 및 후속 polish
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow에 FooterChipBtn 스타일을 추가해 하단 작업 바 버튼 언어를 공통화
- 권한과 Git 브랜치 버튼이 같은 라운드/테두리/패딩 규칙을 사용하도록 정리
- README와 DEVELOPMENT 문서에 2026-04-06 11:34 (KST) 기준 footer polish 반영
- dotnet build 검증 경고 0, 오류 0 확인
2026-04-06 12:18:10 +09:00
b4d69f5db3 AX Agent popup 시각 언어 통일 및 선택 surface 후속 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- 공통 popup factory가 surface visual helper를 사용하도록 정리
- worktree 선택과 권한 모드 row의 border/hover/selected 규칙을 같은 visual language로 통일
- README와 DEVELOPMENT 문서에 2026-04-06 11:27 (KST) 기준 popup polish 누적 범위 반영
- dotnet build 검증 경고 0, 오류 0 확인
2026-04-06 12:12:44 +09:00
3c3faab528 AX Agent surface visual language 공통화 및 preview/file browser 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow에 공통 popup container/menu item/separator/file tree header helper를 추가
- Preview와 FileBrowser presentation이 같은 surface 스타일을 사용하도록 정리
- README와 DEVELOPMENT 문서에 2026-04-06 11:20 (KST) 기준 visual polish 1차 반영
- dotnet build 검증 경고 0, 오류 0 확인
2026-04-06 12:09:18 +09:00
9aa99cdfe6 AX Agent 대화 목록 필터 프레젠테이션 분리 및 사이드바 구조 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow에서 대화 목록 필터/정렬 interaction과 선호 저장 로직을 ConversationFilterPresentation partial로 분리
- 실행 중 보기, 최근/활동 정렬, 관련 버튼 UI 갱신을 메인 창 orchestration 코드 밖으로 이동
- README와 DEVELOPMENT 문서에 2026-04-06 11:11 (KST) 기준 구조 개선 누적 완료 범위 반영
- dotnet build 검증 경고 0, 오류 0 확인
2026-04-06 11:37:44 +09:00
3ac8a7155f AX Agent 사이드바 상호작용 프레젠테이션 분리 및 구조 개선 후속 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow에서 검색/새 대화 사이드바 interaction을 SidebarInteractionPresentation partial로 분리
- 검색 열기/닫기 애니메이션과 hover 상태 변경을 메인 창 orchestration 코드 밖으로 이동
- README와 DEVELOPMENT 문서에 2026-04-06 11:03 (KST) 기준 구조 개선 완료 범위와 남은 후속 작업 수준 반영
- dotnet build 검증 경고 0, 오류 0 확인
2026-04-06 11:28:26 +09:00
3e44f1fc4d AX Agent 대화 목록 관리 프레젠테이션 분리 및 구조 개선 마감\n\n- ChatWindow에서 제목 인라인 편집과 대화 관리 팝업 렌더를 ConversationManagementPresentation partial로 분리\n- 고정, 이름 변경, 카테고리 변경, 삭제 등 대화 목록 관리 상호작용을 메인 창 orchestration 코드 밖으로 이동\n- README와 DEVELOPMENT 문서에 구조 개선 완료 범위와 남은 작업 수준을 2026-04-06 10:56 (KST) 기준으로 반영\n- dotnet build 검증 경고 0, 오류 0 확인
Some checks failed
Release Gate / gate (push) Has been cancelled
2026-04-06 10:58:13 +09:00
fa431f1666 AX Agent timeline 프레젠테이션 helper를 한 파일로 모아 렌더 책임을 더 줄인다
Some checks failed
Release Gate / gate (push) Has been cancelled
CreateTimelineLoadMoreCard, ToAgentEvent, IsCompactionMetaMessage, CreateCompactionMetaCard를 ChatWindow.TimelinePresentation.cs로 이동해 RenderMessages 주변의 timeline 관련 helper를 하나의 presentation surface로 정리했다.

README와 DEVELOPMENT 문서에 2026-04-06 10:44 (KST) 기준 이력을 반영했고, dotnet build 검증 결과 경고 0 / 오류 0을 확인했다.
2026-04-06 10:42:01 +09:00
fda5c6bace AX Agent timeline 조립 helper를 분리해 메시지 렌더 구조를 단순화한다
Some checks failed
Release Gate / gate (push) Has been cancelled
RenderMessages()가 직접 처리하던 visible 메시지/이벤트 필터링과 timestamp/order 기반 timeline action 조립을 ChatWindow.TimelinePresentation.cs로 이동해 메인 렌더 루프가 orchestration 중심으로 더 단순해지도록 정리했다.

README와 DEVELOPMENT 문서에 2026-04-06 10:36 (KST) 기준 이력을 반영했고, dotnet build 검증 결과 경고 0 / 오류 0을 확인했다.
2026-04-06 10:33:57 +09:00
b3b5f8a79d AX Agent transcript 메시지 버블 렌더를 분리해 메인 창 구조를 정리한다
Some checks failed
Release Gate / gate (push) Has been cancelled
사용자/assistant 메시지 버블, 분기 컨텍스트 카드, 액션 바, 응답 메타 조립을 ChatWindow.MessageBubblePresentation.cs로 이동해 ChatWindow.xaml.cs가 transcript orchestration과 런타임 흐름에 더 집중하도록 정리했다.

README와 DEVELOPMENT 문서에 2026-04-06 10:27 (KST) 기준 이력을 반영했고, dotnet build 검증 결과 경고 0 / 오류 0을 확인했다.
2026-04-06 10:29:54 +09:00
8faa26b134 AX Agent 대화 목록 렌더를 별도 프레젠테이션 파일로 분리하고 문서 이력을 갱신한다
Some checks failed
Release Gate / gate (push) Has been cancelled
좌측 대화 목록의 메타 필터링, spotlight 계산, 그룹 렌더, 더 보기 페이지네이션, 목록 카드 조립을 ChatWindow.ConversationListPresentation.cs로 이동해 ChatWindow.xaml.cs가 transcript/runtime orchestration에 더 집중하도록 정리했다.

README와 DEVELOPMENT 문서에 2026-04-06 10:18 (KST) 기준 변경 이력을 반영했고, dotnet build 검증 결과 경고 0 / 오류 0을 확인했다.
2026-04-06 10:23:25 +09:00
1b4566d192 AX Agent 프리셋 렌더와 주제 선택 흐름을 분리해 메인 창 책임을 줄인다
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow.TopicPresetPresentation을 추가해 프리셋 카드 생성, 커스텀 프리셋 메뉴, 주제 선택 적용 흐름을 별도 프레젠테이션 계층으로 이동한다

- ChatWindow.xaml.cs에서는 프리셋 UI 조립 코드를 제거하고 대화 orchestration 중심 구조를 더 강화한다

- claw-code parity plan과 개발 문서에 구조 개선 진행 상황을 반영해 큰 구조 개선 항목이 사실상 마감 단계임을 기록한다

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-06 10:15:22 +09:00
d0d66c1d52 AX Agent footer와 Git 브랜치 프레젠테이션 구조를 분리하고 회귀 점검 루틴을 고정한다
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow.FooterPresentation에서 Git 브랜치 팝업 렌더와 요약 helper를 분리해 폴더 바 상태/프리셋 안내 동기화 책임만 남긴다

- ChatWindow.GitBranchPresentation을 추가해 브랜치 팝업 조립, 요약 pill, 최근 브랜치/전환 액션 렌더를 별도 프레젠테이션 계층으로 옮긴다

- AX_AGENT_REGRESSION_PROMPTS 문서를 재작성해 실패 분류와 Chat/Cowork/Code별 필수 회귀 묶음을 개발 루틴으로 고정한다

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-06 10:06:25 +09:00
9464dd0234 의견 요청과 계획 승인 렌더를 분리해 메시지 타입 구조를 정리한다
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow.InlineInteractions를 UserAskPresentation과 PlanApprovalPresentation으로 분리해 사용자 질문 카드와 계획 승인 흐름의 책임을 나눔

- 메시지 타입 renderer 분리 계획의 다음 단계로 ChatWindow.xaml.cs와 mixed inline interaction partial의 결합도를 낮춤

- README, DEVELOPMENT, claw-code parity plan 문서를 2026-04-06 09:44 (KST) 기준으로 갱신함

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-06 09:53:07 +09:00
d5c1266d3e 상태선/권한 카탈로그 구조 정리와 계획 모드 표현 잔재 제거
Some checks failed
Release Gate / gate (push) Has been cancelled
- OperationalStatusPresentationCatalog를 추가해 compact strip과 quick strip의 색상/노출 계산을 AppStateService 밖으로 분리함

- PermissionRequestPresentationCatalog와 ToolResultPresentationCatalog에 Kind/Description 메타를 추가해 transcript fallback 설명을 타입 기반으로 정리함

- PermissionModePresentationCatalog와 ChatWindow.PermissionPresentation에서 제거된 계획 모드 표현 분기를 걷어 권한 UI를 실제 지원 모드만 다루도록 단순화함

- README, DEVELOPMENT, claw-code parity plan 문서를 2026-04-06 09:36 (KST) 기준으로 갱신함

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-06 09:44:53 +09:00
3924bac9f9 claw-code 대조 기반 AX Agent 품질 향상 3트랙 계획을 수립한다
- 사용자 체감 UI/UX, LLM·작업 처리, 유지보수·추가기능 구조의 3트랙으로 남은 품질 향상 과제를 재정리했다.

- docs/claw-code-parity-plan.md에 참조 파일, AX 적용 위치, 완료 조건, 품질 기준, 권장 실행 순서를 고정했다.

- README와 DEVELOPMENT 문서에 2026-04-06 09:27 (KST) 기준 계획 변경 이력을 반영했다.

- 이번 변경은 문서화 작업으로 코드 변경이 없어 별도 빌드는 생략했다.
2026-04-06 09:30:13 +09:00
9f5a9d315c AX Agent 선택 팝업 렌더를 분리해 footer 선택 UX 구조를 정리한다
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow.SelectionPopupPresentation.cs를 추가해 워크트리 선택 팝업과 공통 선택 row 렌더를 메인 창 코드 밖으로 이동했다.

- ChatWindow.xaml.cs에서 작업 위치 선택 팝업 조립 책임을 제거해 대화 상태와 세션 orchestration 중심 구조를 더 선명하게 만들었다.

- README와 DEVELOPMENT 문서에 2026-04-06 09:14 (KST) 기준 구조 분리 이력을 반영했다.

- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 검증 결과 경고 0 / 오류 0을 확인했다.
2026-04-06 09:19:10 +09:00
4c1513a5da AX Agent 공통 팝업과 시각 상호작용 렌더를 분리해 메인 창 구조를 정리한다
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow.VisualInteractionHelpers.cs를 추가해 메시지 액션 버튼, 선택 스타일, hover 애니메이션, 공통 체크 아이콘 생성을 메인 창 코드 밖으로 이동했다.

- ChatWindow.PopupPresentation.cs를 추가해 공통 테마 팝업 컨테이너, 메뉴 아이템, 구분선, 최근 폴더 컨텍스트 메뉴 구성을 한 곳으로 모았다.

- README와 DEVELOPMENT 문서에 2026-04-06 09:03 (KST) 기준 구조 분리 이력을 반영했다.

- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 검증 결과 경고 0 / 오류 0을 확인했다.
2026-04-06 09:02:41 +09:00
ccaa24745e AX Agent 파일 브라우저 렌더 구조 분리 및 문서 갱신
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow.FileBrowserPresentation.cs를 추가해 파일 탐색기 열기/닫기, 폴더 트리 구성, 파일 헤더·아이콘·크기 표시, 우클릭 메뉴, 디바운스 새로고침 흐름을 메인 창 코드에서 분리함

- ChatWindow.xaml.cs는 transcript·runtime orchestration 중심으로 더 정리해 claw-code 기준 sidebar/file surface 품질 개선을 이어가기 쉬운 구조로 개선함

- README.md와 docs/DEVELOPMENT.md에 2026-04-06 08:55 (KST) 기준 변경 이력을 반영함

- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 기준 경고 0 / 오류 0 확인
2026-04-06 08:56:18 +09:00
1ce6ccb030 AX Agent 프리뷰 패널 렌더 구조 분리 및 문서 갱신
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow.PreviewPresentation.cs를 추가해 프리뷰 탭 목록, 헤더, 파일 로드, CSV·텍스트·마크다운·HTML 표시, 컨텍스트 메뉴, 별도 창 미리보기 흐름을 메인 창 코드에서 분리함

- ChatWindow.xaml.cs는 transcript 및 런타임 orchestration 중심으로 더 정리해 claw-code 기준 preview surface 품질 개선을 이어가기 쉬운 구조로 개선함

- README.md와 docs/DEVELOPMENT.md에 2026-04-06 08:47 (KST) 기준 변경 이력을 반영함

- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 기준 경고 0 / 오류 0 확인
2026-04-06 08:48:32 +09:00
2b21e8cdfb AX Agent 상태선 렌더 구조 분리 및 문서 갱신
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow.StatusPresentation.cs로 UpdateStatusBar, StartStatusAnimation, StopStatusAnimation을 이동해 runtime 상태 이벤트와 상태선 표현 책임을 메인 창 코드에서 분리함

- ChatWindow.xaml.cs는 transcript 오케스트레이션 중심으로 더 정리했고, claw-code 기준 status/footer 품질 개선을 이어가기 쉬운 구조로 개선함

- README.md와 docs/DEVELOPMENT.md에 2026-04-06 08:39 (KST) 기준 변경 이력을 반영함

- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 기준 경고 0 / 오류 0 확인
2026-04-06 08:40:56 +09:00
35ec073eb9 AX Agent 메시지 상호작용 렌더 구조 분리 및 문서 갱신
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow.MessageInteractions.cs를 추가해 좋아요·싫어요 피드백 버튼, 응답 메타 텍스트, 메시지 등장 애니메이션, 사용자 메시지 편집·재생성 흐름을 메인 창 코드에서 분리함

- ChatWindow.xaml.cs는 transcript 오케스트레이션과 상태 흐름에 더 집중하도록 정리해 claw-code 기준 renderer 분리 작업을 이어가기 쉬운 구조로 개선함

- README.md와 docs/DEVELOPMENT.md에 2026-04-06 08:27 (KST) 기준 변경 이력을 반영함

- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 기준 경고 0 / 오류 0 확인
2026-04-06 08:31:48 +09:00
68524c1c94 AX Agent composer·대기열 렌더 구조 분리 및 문서 갱신
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow.ComposerQueuePresentation.cs를 추가해 입력창 높이 계산, draft kind 해석, 대기열 요약/카드/배지/액션 버튼 생성 책임을 메인 창 코드에서 분리함

- ChatWindow.xaml.cs는 대기열 실행 orchestration과 세션 변경 흐름 중심으로 정리해 claw-code 기준 footer/composer 품질 개선 기반을 강화함

- README.md와 docs/DEVELOPMENT.md에 2026-04-06 08:28 (KST) 기준 변경 이력을 반영함

- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 기준 경고 0 / 오류 0 확인
2026-04-06 08:24:25 +09:00
b4a506de96 AX Agent 하단 작업바 렌더 구조 분리 및 문서 갱신
- ChatWindow.FooterPresentation.cs를 추가해 폴더 바, 선택 프리셋 안내, Git 브랜치 팝업 렌더를 메인 창 코드에서 분리함

- ChatWindow.xaml.cs는 대화 흐름과 런타임 orchestration 중심으로 정리해 claw-code 기준 footer presentation 개선 기반을 마련함

- README.md와 docs/DEVELOPMENT.md에 2026-04-06 08:12 (KST) 기준 변경 이력을 반영함

- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 기준 경고 0 / 오류 0 확인
2026-04-06 08:14:01 +09:00
82b42b3ba3 AX Agent 권한·컨텍스트 카드 렌더 구조 분리 및 문서 갱신
권한 선택 팝업과 권한 상태 배너 렌더를 ChatWindow.PermissionPresentation partial로 분리했습니다.

컨텍스트 사용량 카드와 hover 팝업 렌더를 ChatWindow.ContextUsagePresentation partial로 분리해 메인 ChatWindow.xaml.cs의 책임을 줄였습니다.

README와 DEVELOPMENT 문서에 2026-04-06 07:31 (KST) 기준 변경 이력을 반영했고 Release 빌드 경고 0 오류 0을 확인했습니다.
2026-04-06 07:33:21 +09:00
90bd77f945 AX Agent 계획 승인 흐름과 상태선 표현 계층 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- 계획 승인 기본 흐름을 transcript inline 우선 구조로 더 정리하고, 계획 버튼은 저장된 계획을 여는 상세 보기 성격으로 분리했습니다.

- OperationalStatusPresentationState를 확장해 runtime badge, compact strip, quick strip의 문구·강조색·노출 여부를 한 번에 계산하도록 통합했습니다.

- ChatWindow 상태선/quick strip/status token 로직을 StatusPresentation partial로 분리해 메인 창 코드의 직접 분기와 렌더 책임을 줄였습니다.

- 문서 이력(README, DEVELOPMENT)을 2026-04-06 01:37 KST 기준으로 갱신했습니다.

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-06 07:17:16 +09:00
95e40df354 AX Agent transcript 권한·도구 결과 표현 정교화 및 이벤트 렌더 분리
Some checks failed
Release Gate / gate (push) Has been cancelled
- claw-code 기준으로 transcript display catalog를 파일/문서/빌드/Git/웹/질문/에이전트 축으로 재정의
- 권한 요청 presentation catalog를 명령 실행, 웹 요청, 스킬 실행, 의견 요청, 파일 수정, 파일 접근 타입으로 세분화
- tool result catalog를 성공/실패/거부/취소와 도구 종류에 따라 더 읽기 쉬운 한국어 결과 라벨로 정리
- CreateCompactEventPill, AddAgentEventBanner, GetDecisionBadgeMeta를 ChatWindow.AgentEventRendering.cs로 분리해 메인 창 코드 비중 축소
- README와 DEVELOPMENT 문서에 변경 목적과 검증 결과 반영
- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ (경고 0 / 오류 0)
2026-04-06 06:59:22 +09:00
f9d18fba08 AX Agent 폴더 데이터 활용 사용자 옵션 제거 및 자동 정책 고정
Some checks failed
Release Gate / gate (push) Has been cancelled
- 코워크/코드의 폴더 내 문서 활용을 사용자 설정에서 제거하고 탭별 자동 정책으로 고정
- ChatWindow 하단 데이터 활용 버튼과 AX Agent 내부 설정 row, 메인 설정/구형 설정창의 관련 UI 제거
- Chat/Cowork/Code 탭별 자동 데이터 활용 정책을 none/passive/active로 고정하고 저장 경로에서 사용자 선택값 반영 제거
- README와 DEVELOPMENT 문서에 변경 이력 및 적용 기준 반영
- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ (경고 0 / 오류 0)
2026-04-05 23:06:38 +09:00
f0af86cc1e AX Agent transcript 렌더 구조를 분리하고 권한/도구 결과 표시 체계를 정리한다
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow.xaml.cs에 몰려 있던 의견 요청, 계획 승인, 작업 요약 렌더를 partial 파일로 분리해 transcript 책임을 낮췄다.

- PermissionRequestPresentationCatalog와 ToolResultPresentationCatalog를 추가해 권한 요청 및 도구 결과 badge를 타입별로 해석하도록 정리했다.

- AppStateService에 OperationalStatusPresentationState를 추가하고 상태선 계산을 presentation 계층으로 한 번 더 분리했다.

- README.md, docs/DEVELOPMENT.md, docs/claw-code-parity-plan.md에 2026-04-06 00:58 (KST) 기준 변경 내용을 반영했다.

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 22:55:56 +09:00
13f0e23ed5 AX Agent 테마 프리셋 2종 추가
Some checks failed
Release Gate / gate (push) Has been cancelled
AX Agent 전용 테마를 재점검해 Nord와 Ember 프리셋을 새로 추가했습니다.

내부 설정의 테마 스타일 카드에서 새 프리셋을 바로 선택할 수 있게 연결했고 system, light, dark 모드 조합으로 동일하게 적용되도록 정리했으며 문서 이력과 빌드 검증도 함께 반영했습니다.
2026-04-05 22:41:15 +09:00
7cb27b70f8 내부 설정 훅 섹션 잘림 레이아웃 보정
Some checks failed
Release Gate / gate (push) Has been cancelled
AX Agent 내부 설정의 도구 훅 실행 타임아웃과 등록된 훅 영역에서 컨트롤이 잘리던 레이아웃을 조정했습니다.

슬라이더와 값 배지 컬럼 폭, 훅 추가 버튼 최소 폭과 여백을 넓혀 작은 폭에서도 텍스트와 컨트롤이 안정적으로 보이도록 정리했고 문서 이력과 빌드 검증도 함께 반영했습니다.
2026-04-05 22:37:11 +09:00
61f82bdd10 내부 설정 개발자 토글 저장 경로 복구
Some checks failed
Release Gate / gate (push) Has been cancelled
AX Agent 내부 설정 개발자 탭의 워크플로우 시각화, 전체 호출·토큰 합계 표시, 감사 로그 토글에 변경 이벤트를 연결했습니다.

오버레이 재동기화 시 기본값으로 되돌아가던 문제를 막고 즉시 저장되도록 보정했으며 문서 이력과 빌드 검증도 함께 반영했습니다.
2026-04-05 22:33:39 +09:00
fa349c2057 프리셋 선택 시 새 대화 중복 생성 방지
Some checks failed
Release Gate / gate (push) Has been cancelled
AX Agent 채팅과 코워크에서 메시지와 입력이 없는 fresh conversation에 프리셋만 선택해도 새 대화가 반복 생성되던 흐름을 수정했습니다.

기존 빈 대화가 있으면 해당 대화를 재사용하고 프리셋 메타데이터만 갱신하도록 정리했으며 문서 이력과 빌드 검증도 함께 반영했습니다.
2026-04-05 22:30:53 +09:00
be7328184a 코드 탭 폴더 데이터 활용 기본 허용 및 파일명 강조 적용
Some checks failed
Release Gate / gate (push) Has been cancelled
코드 탭에서는 폴더 내 데이터 활용을 항상 적극 활용으로 고정하고 하단 버튼과 내부 설정 옵션을 숨겨 사용 흐름을 단순화했습니다.

코워크와 코드 탭의 사용자 메시지도 파일 경로 강조 렌더러를 사용하도록 바꿔 폴더 하위 파일명 입력 시 파란색으로 표시되게 맞췄습니다.

README와 DEVELOPMENT 문서에 변경 이력을 반영했고 Release 빌드에서 경고 0 오류 0을 확인했습니다.
2026-04-05 22:28:29 +09:00
905ea41ed3 코드 탭 Git 브랜치 선택 UI 단순화
- 하단 Git 브랜치 버튼을 상태판형에서 브랜치 선택 버튼 형태로 정리\n- 브랜치 버튼 기본 노출에서 변경 파일/추가/삭제 수치를 숨기고 브랜치명 중심으로 단순화\n- README 및 DEVELOPMENT 문서 이력 갱신\n\n검증:\n- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\\n- 경고 0 / 오류 0
2026-04-05 22:21:58 +09:00
d0fa54f10e AX Agent 내부 설정 MCP 서버 관리와 대화 스타일 섹션 복구
Some checks failed
Release Gate / gate (push) Has been cancelled
- AX Agent 내부 설정 공통 탭에 대화 스타일 섹션 제목을 복구해 문서 형태/디자인 스타일 저장 항목을 명확히 노출

- 스킬 탭 MCP 서버 영역에 서버 추가 버튼을 복원하고 목록 카드에서 활성화 전환과 삭제를 바로 처리하도록 보강

- 오버레이 저장 경로를 그대로 사용해 내부 설정에서 변경 즉시 저장되도록 유지

- README와 DEVELOPMENT 문서에 2026-04-06 00:45 (KST) 기준 이력 반영 및 Release 빌드 경고 0 오류 0 확인
2026-04-05 22:09:29 +09:00
1948af3cc4 AX Agent 프리뷰 UI를 claw-code 스타일로 정리하고 프리뷰 surface를 공통화
Some checks failed
Release Gate / gate (push) Has been cancelled
- AX Agent 권한 승인 프리뷰에 공통 preview surface helper를 도입해 제목/요약/본문 box 구성을 일관되게 정리함
- 우측 파일 프리뷰 패널에 파일명, 경로, 형식·크기 메타 헤더를 추가하고 텍스트 프리뷰를 bordered preview box 안에 렌더하도록 개선함
- README와 DEVELOPMENT 문서에 2026-04-06 01:08 (KST) 기준 변경 이력 반영 완료
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0 확인
2026-04-05 22:03:16 +09:00
53965083e3 AX Agent 메시지 액션과 응답 메타 표시 개선\n\n- assistant 메시지에 응답시간과 토큰 수를 저장하는 메타 필드 추가\n- 응답 커밋 시 토큰 사용량과 경과 시간을 메시지 모델에 함께 기록하도록 엔진과 전송 경로 보강\n- 사용자/assistant 메시지 액션 바를 기본 저강도 노출과 hover 강조 방식으로 바꿔 복사 편집 재생성 좋아요 싫어요가 보이도록 정리\n- README와 DEVELOPMENT 문서에 메시지 액션 및 응답 메타 개선 이력 반영\n- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
Some checks failed
Release Gate / gate (push) Has been cancelled
2026-04-05 21:54:15 +09:00
ac4aada0af AX Agent 새 대화 세션 복원 오류 수정\n\n- 새 대화 생성 직후 저장되지 않은 fresh conversation을 우선 유지하도록 ChatSessionStateService LoadOrCreateConversation 경로를 보정\n- 기억된 대화 ID가 없는 상태에서 최신 저장 대화를 다시 불러와 기존 transcript가 복원되던 문제 차단\n- README와 DEVELOPMENT 문서에 새 대화 세션 복원 버그 수정 이력 추가\n- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
Some checks failed
Release Gate / gate (push) Has been cancelled
2026-04-05 21:48:22 +09:00
53afdb3472 vLLM 모델 해석 및 max_tokens 상한 보정
Some checks failed
Release Gate / gate (push) Has been cancelled
vLLM 연결 시 등록 모델 alias와 실제 모델 ID가 섞여 payload로 전달되던 경로를 보정해 RegisteredModel에서 실제 모델명을 우선 찾아 요청에 사용하도록 수정했다.

OpenAI-compatible 일반 대화와 도구 호출 모두 vLLM 서버 허용 범위를 넘지 않도록 max_tokens를 자동 보정하도록 통일했다.

검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0, 오류 0)
2026-04-05 21:40:43 +09:00
5765888229 AX Agent 상단 탭 여백 비율 재조정 및 문서 반영
Some checks failed
Release Gate / gate (push) Has been cancelled
상단 중앙 채팅/Cowork/코드 탭 그룹의 버튼 패딩, 최소 폭/높이와 바깥 pill 래퍼 높이/패딩을 한 단계 더 줄여 바깥 테두리 안쪽 여백이 살아 있도록 조정했다.

README와 DEVELOPMENT 문서에 2026-04-06 00:31 (KST) 기준 변경 이력을 추가했다.

검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0, 오류 0)
2026-04-05 21:35:35 +09:00
2958306caf AX Agent 기본 시작 높이 상향 조정
Some checks failed
Release Gate / gate (push) Has been cancelled
- AX Agent 채팅창 기본 Height를 820에서 880으로 상향
- 처음 열었을 때 상하 여백과 프리셋 영역이 더 여유 있게 보이도록 조정
- README 및 DEVELOPMENT 문서에 2026-04-06 00:27 (KST) 기준 이력 반영
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ 경고 0 오류 0 확인
2026-04-05 21:28:40 +09:00
216b050398 claw-code 대비 AX Agent 품질 향상 계획 구체화
Some checks failed
Release Gate / gate (push) Has been cancelled
- claw-code 소스 구조와 AX Agent 구조를 다시 대조해 추가 품질 향상 계획 수립
- transcript renderer 분리, permission presentation catalog, tool result taxonomy, plan approval inline 마감, runtime summary 계층화, regression prompt ritual 고정 계획 문서화
- 런타임 핵심 설정과 개발자 전용 이동 후보 설정을 구분해 정리
- README 및 DEVELOPMENT 문서에 2026-04-06 00:22 (KST) 기준 이력 반영
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ 경고 0 오류 0 확인
2026-04-05 21:26:25 +09:00
5352ca2ab2 AX Agent 상단 탭 여백 및 비율 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- 상단 중앙 탭 버튼의 패딩과 최소 크기를 소폭 줄여 과하게 꽉 찬 느낌 완화
- 탭 그룹 외곽 래퍼 높이와 패딩을 함께 줄여 레퍼런스에 가까운 여유 간격 확보
- README 및 DEVELOPMENT 문서에 2026-04-06 00:14 (KST) 기준 변경 이력 반영
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ 경고 0 오류 0 확인
2026-04-05 21:23:52 +09:00
929c1e9f05 AX Agent 등록 모델 리스트 UI 정리 및 액션 안정화
Some checks failed
Release Gate / gate (push) Has been cancelled
- 내부 설정 공통 탭에서 등록 모델 상단 중복 선택 칩 UI 제거
- 등록 모델 관리 영역을 리스트 중심 구조로 정리
- 선택 편집 삭제 액션을 팝업 친화적인 클릭 row 방식으로 변경
- 오버레이 동기화 경로에서 제거된 모델 칩 렌더 호출 삭제
- README 및 DEVELOPMENT 문서에 2026-04-06 00:08 (KST) 기준 이력 반영
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ 경고 0 오류 0 확인
2026-04-05 21:17:13 +09:00
bfa1e342c0 AX Agent 토큰 라벨 닫힘 보강 및 전송 버튼 정렬 개선
Some checks failed
Release Gate / gate (push) Has been cancelled
- 토큰 hover popup 닫힘 경로를 창 전체 마우스 이동, 클릭, 비활성화 기준까지 확장해 라벨이 남는 현상 완화
- 컨텍스트 라벨은 카드 밖으로 벗어났을 때 더 빠르게 닫히도록 idle 검사 경로 보강
- 전송 버튼 크기를 키우고 내부 send glyph 크기와 오프셋을 조정해 작고 치우쳐 보이던 인상 보정
- README 및 DEVELOPMENT 문서에 2026-04-06 00:01 (KST) 기준 변경 이력 반영

검증
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\
- 경고 0 / 오류 0 확인
2026-04-05 21:11:39 +09:00
8331c0eedc AX Agent 컨텍스트 토큰 심볼 잘림 및 hover 라벨 닫힘 개선
- 하단 컨텍스트 토큰 카드 외곽 크기와 내부 원형 그리드 여백을 조정해 파이 심볼 왼쪽 잘림 인상 보정
- usage arc 및 threshold marker 계산 중심점을 실제 심볼 크기에 맞게 재설정
- hover popup을 비상호작용 툴팁 형태로 바꿔 카드 밖으로 벗어나면 더 자연스럽게 닫히도록 정리
- README 및 DEVELOPMENT 문서에 2026-04-05 23:55 (KST) 기준 변경 이력 반영

검증
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\
- 경고 0 / 오류 0 확인
2026-04-05 21:04:08 +09:00
cf59a010ac AX Agent 상단 탭 잘림 보정 및 빈 상태 비율 조정
Some checks failed
Release Gate / gate (push) Has been cancelled
- 상단 중앙 탭의 글자 잘림을 줄이기 위해 탭 버튼 폰트, 패딩, 최소 크기와 탭 래퍼 크기를 소폭 축소
- 빈 상태 상단 아이콘과 제목, 설명 크기를 조정해 프리셋 카드 한 장과의 시각 비율을 더 자연스럽게 정리
- 상단 헤더와 중앙 안내 블록의 밀도를 분리해 탭은 더 단정하게, 빈 상태는 더 또렷하게 보이도록 조정
- README 및 DEVELOPMENT 문서에 2026-04-05 23:49 (KST) 기준 변경 이력 반영

검증
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\
- 경고 0 / 오류 0 확인
2026-04-05 21:01:28 +09:00
1c9b13c14f AX Agent 권한 모드 팝업 단순 리스트형으로 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- 권한 모드 팝업 폭과 padding, 그림자 강도를 줄여 레퍼런스처럼 가벼운 드롭다운 패널 스타일로 정리
- 상세 정보 섹션과 부가 요약 영역을 제거하고 실제 선택 가능한 권한 모드 행만 남기도록 단순화
- 계획 모드는 레거시로 제외하고 권한 요청, 편집 자동 승인, 권한 건너뛰기 중심으로 재배치
- 각 권한 모드 행은 왼쪽 accent bar 대신 라운드 row 리스트형으로 바꾸고 체크 위치와 텍스트 계층 정리
- README 및 DEVELOPMENT 문서에 2026-04-05 23:44 (KST) 기준 변경 이력 반영

검증
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\
- 경고 0 / 오류 0 확인
2026-04-05 20:54:54 +09:00
87c05720ce AX Agent 컨텍스트 토큰 hover 라벨 닫힘 및 정보 표시 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- 하단 컨텍스트 토큰 popup 닫힘 판정을 IsMouseOver 중심에서 실제 마우스 좌표 기준으로 보강
- 마우스를 떼었는데도 hover 라벨이 남아 있던 문제를 줄이도록 card/popup 경계 검사 로직 정리
- popup 내용에서 모델명 및 오늘 사용량 줄을 제거하고 현재 사용량과 자동 압축 기준만 남겨 정보 밀도 단순화
- README 및 DEVELOPMENT 문서에 2026-04-05 23:38 (KST) 기준 변경 이력 반영

검증
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\
- 경고 0 / 오류 0 확인
2026-04-05 20:50:51 +09:00
25031d655d AX Agent 프리셋 아이콘과 빈 상태 심볼 비율 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- 작업 유형/대화 주제 프리셋 카드 내부 아이콘 크기와 원형 배경 비율을 줄여 과하게 커 보이던 인상을 완화
- 기타 및 프리셋 추가 카드의 심볼 크기도 함께 조정해 프리셋 행 전체 시각 균형 통일
- 빈 상태 상단 중앙 심볼과 제목/설명 크기를 재조정해 프리셋 카드와 같은 밀도로 보이게 정리
- README 및 DEVELOPMENT 문서에 2026-04-05 23:33 (KST) 기준 변경 이력 반영

검증
- 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:21 +09:00
793a301353 AX Agent 작업 폴더 선택 팝업 스타일 정리 및 최근 항목 UI 개선
Some checks failed
Release Gate / gate (push) Has been cancelled
- 최근 작업 폴더 팝업 외곽 radius, 그림자, 내부 여백을 조정해 레퍼런스에 더 가까운 카드형 스타일로 보정
- 최근 폴더 항목을 하단 구분선 방식에서 개별 라운드 row 방식으로 변경하고 선택 체크와 텍스트 계층을 정돈
- 다른 폴더 선택 행도 동일한 row 스타일로 통일해 팝업 전체 시각 언어를 일관되게 정리
- README 및 DEVELOPMENT 문서에 2026-04-05 23:28 (KST) 기준 변경 이력 반영

검증
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\
- 경고 0 / 오류 0 확인
2026-04-05 20:45:30 +09:00
a5790e28fb AX Agent 상단 탭 잘림 및 메시지 transcript 정렬 보정
Some checks failed
Release Gate / gate (push) Has been cancelled
- 상단 헤더 첫 행 높이와 중앙 탭 래퍼 패딩/최소 높이를 늘려 채팅, Cowork, 코드 탭 글자 잘림 수정
- 사용자, assistant, streaming 메시지 컨테이너를 동일 transcript 폭 기준으로 통일
- wrapper는 같은 중심 축을 공유하고 내부 bubble만 좌우 정렬되도록 변경해 메시지 박스 정렬 불일치 완화
- README 및 DEVELOPMENT 문서에 2026-04-05 23:22 (KST) 기준 변경 이력 반영

검증:
- 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:51 +09:00
00d284b725 AX Agent 좌측 패널 드래그 리사이즈 지원 추가
Some checks failed
Release Gate / gate (push) Has been cancelled
- 좌측 사이드바와 본문 사이 경계선에 GridSplitter 추가
- 사이드바가 열려 있을 때만 드래그 가능하도록 splitter 표시 상태 연동
- 사용자가 조절한 폭을 저장해 사이드바를 닫았다 다시 열어도 마지막 너비 유지
- 열기/닫기 애니메이션이 현재 폭과 저장 폭을 함께 사용하도록 정리
- README 및 DEVELOPMENT 문서에 2026-04-05 23:15 (KST) 기준 변경 이력 반영

검증:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\
- 경고 0 / 오류 0 확인
2026-04-05 20:33:09 +09:00
891133a6bf AX Agent 좌측 패널 글자 크기 및 대화 목록 가독성 개선
Some checks failed
Release Gate / gate (push) Has been cancelled
- 좌측 사이드바 폭을 소폭 확장하고 헤더, 새 대화, 검색, 상단 필터, 보조 메뉴, 전체 삭제, 사용자 영역 타이포와 아이콘 크기 상향
- 대화 목록 그룹 헤더, 제목, 날짜, 실행 상태, 실행 요약, 편집 아이콘 크기와 카드 패딩 조정
- 좌측 패널 텍스트가 지나치게 작아 보이던 문제를 전체 시각 균형 기준으로 보정
- README 및 DEVELOPMENT 문서에 2026-04-05 23:09 (KST) 기준 변경 이력 반영

검증:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\
- 경고 0 / 오류 0 확인
2026-04-05 20:29:57 +09:00
b1e11b27bc AX Agent 새 대화 전환 버그 수정 및 상태 초기화 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- 새 대화 시작 시 LoadOrCreateConversation 재호출로 기존 대화가 다시 복원되던 경로 제거
- ClearCurrentConversation 이후 항상 fresh conversation 생성으로 current conversation 전환 고정
- 새 대화 전환 시 대화별 설정, 압축 메트릭, 앱 상태, 프리셋 안내, 조건부 스킬 상태를 새 세션 기준으로 재동기화
- 빈 transcript를 다시 렌더하도록 정리해 첫 화면 깜빡임 후 기존 메시지가 남는 현상 수정
- README 및 DEVELOPMENT 문서에 2026-04-05 23:02 (KST) 기준 변경 이력 반영

검증:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\
- 경고 0 / 오류 0 확인
2026-04-05 20:26:59 +09:00
f79e657895 AX Agent 메시지 메타 가독성과 끝단 정렬 보정
Some checks failed
Release Gate / gate (push) Has been cancelled
- 사용자와 assistant 메시지 행 여백을 줄여 transcript 좌우 끝단 정렬감을 높였습니다.
- AX 에이전트 라벨, 아이콘, 시간 표기 글꼴 크기와 대비를 키워 메타 정보 가독성을 보강했습니다.
- 관련 변경 이력을 README와 DEVELOPMENT 문서에 반영했습니다.

검증:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\
- 경고 0 / 오류 0
2026-04-05 20:22:30 +09:00
1778b855c5 런처 클립보드 붙여넣기 포커스 복원 경로 통일
Some checks failed
Release Gate / gate (push) Has been cancelled
- 클립보드 히스토리, 클립보드 변환, 순차 붙여넣기 실행 경로에 공통 포커스 복원 helper를 추가했습니다.
- 이전 활성 창 복원, 최소 대기, Ctrl+V 주입 순서를 하나로 맞춰 포커스 누락으로 내용이 원래 창에 들어가지 않던 문제를 완화했습니다.
- 관련 변경 이력을 README와 DEVELOPMENT 문서에 반영했습니다.

검증:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\
- 경고 0 / 오류 0
2026-04-05 20:19:44 +09:00
db957039d4 AX Agent 프리셋 카드 hover 깜빡임 완화
Some checks failed
Release Gate / gate (push) Has been cancelled
- 채팅과 코워크 프리셋 카드 설명 라벨을 Visibility 토글 대신 Opacity 전환으로 변경
- 카드 hover 중 설명 라벨 표시 때문에 레이아웃이 다시 잡히며 깜빡이던 현상 완화
- README와 DEVELOPMENT 문서에 2026-04-05 22:57 (KST) 기준 작업 이력 반영
- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ (경고 0 / 오류 0)
2026-04-05 20:11:10 +09:00
78905d16c0 AX Agent 토큰 사용 라벨 hover 종료와 스타일 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- 하단 컨텍스트 토큰 라벨이 hover 후 남아 있던 문제를 카드와 팝업의 실제 hover 상태 기준으로 닫히도록 수정
- 토큰 카드가 숨겨질 때 popup도 함께 닫히도록 보강
- 토큰 심볼과 popup 스타일을 얇은 테두리와 약한 그림자 중심으로 정리
- README와 DEVELOPMENT 문서에 2026-04-05 22:53 (KST) 기준 작업 이력 반영
- 검증: 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:15 +09:00
2975bb39a2 AX Agent 최대 컨텍스트 토큰 프리셋 확장
Some checks failed
Release Gate / gate (push) Has been cancelled
- AX Agent 내부 설정의 최대 컨텍스트 토큰 프리셋에 32K와 128K 중간값 추가
- 현재 MaxContextTokens 값이 중간 구간에 있어도 가장 가까운 프리셋 카드가 자연스럽게 활성화되도록 선택 매핑 확장
- README와 DEVELOPMENT 문서에 2026-04-05 22:48 (KST) 기준 작업 이력 반영
- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ (경고 0 / 오류 0)
2026-04-05 20:05:17 +09:00
5e63f13cf3 AX Agent 내부 설정 중복 실행 옵션 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- AX Agent 내부 설정의 실행 방식 블록을 코워크/코드 공통 탭에만 노출되도록 조정
- 코워크와 코드 개별 탭에서 중복 노출되던 호출 간격 최적화, 의사결정 수준 항목 제거
- 레거시 실행 전 계획 행을 UI에서 삭제하고 관련 문서 이력 갱신
- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ (경고 0 / 오류 0)
2026-04-05 20:02:45 +09:00
1f581454e1 AX Agent 작업 폴더 바 X 버튼 제거 및 정렬 보정
- Cowork/Code 하단 작업 폴더 바에서 불필요한 폴더 해제 X 버튼 제거
- 작업 폴더 바 그리드 컬럼 구조를 다시 맞춰 데이터 활용, 권한, Git 상태가 더 자연스럽게 이어지도록 정렬 보정
- README와 DEVELOPMENT 문서에 2026-04-05 22:39 (KST) 기준 작업 이력 반영
- 검증: 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:57 +09:00
cdd99fd4d2 AX Agent 좌측 중복 필터 메뉴 정리 및 문서 갱신
Some checks failed
Release Gate / gate (push) Has been cancelled
- AX Agent 사이드바에서 주제/작업 유형/워크스페이스 보조 필터를 숨기고 상단 공통 필터만 유지
- 탭 전환 시 UpdateSidebarModeMenu 경로에서도 중복 필터 메뉴가 다시 나타나지 않도록 고정
- README와 DEVELOPMENT 문서에 2026-04-05 22:34 (KST) 기준 작업 이력 반영
- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ (경고 0 / 오류 0)
2026-04-05 19:56:57 +09:00
b44df996c2 AX Agent 선택 주제 안내 위치 조정
Some checks failed
Release Gate / gate (push) Has been cancelled
선택된 대화 주제와 작업 유형 안내 배너를 헤더 중앙에서 입력창 위 중앙으로 옮겼다.

실제 작성 흐름 가까이에서 선택 상태를 확인할 수 있도록 composer 상단 구조에 맞춰 재배치했다.

README와 DEVELOPMENT 문서에 변경 이력과 시점을 반영했다.

검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 19:52:42 +09:00
7f8a075553 AX Agent 작업 폴더 팝업 단순화
Some checks failed
Release Gate / gate (push) Has been cancelled
작업 폴더 선택 팝업을 검색과 요약 스트립 중심 구조에서 최근 폴더 목록 중심 구조로 재구성했다.

현재 선택 체크가 보이는 최근 폴더 목록과 다른 폴더 선택 액션만 남겨 캡처와 비슷한 빠른 선택 흐름으로 정리했다.

README와 DEVELOPMENT 문서에 변경 이력과 시점을 반영했다.

검증: 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:53 +09:00
eed87db268 AX Agent 모델 선택 표시 단순화
Some checks failed
Release Gate / gate (push) Has been cancelled
모델 빠른 설정 팝업 하단의 중복 모델 칩 줄을 제거해 리스트 선택만 남기도록 정리했다.

하단 모델 표시 라벨은 서비스명과 모델명을 함께 나열하던 방식에서 현재 모델명 중심의 간결한 표기로 바꿨다.

README와 DEVELOPMENT 문서에 변경 이력과 시점을 반영했다.

검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 19:45:20 +09:00
cfacb903e1 AX Agent 프리셋 아이콘 비율 조정
Some checks failed
Release Gate / gate (push) Has been cancelled
대화 주제와 작업 유형 프리셋 카드 내부 아이콘이 과하게 크게 보이던 문제를 조정했다.

빈 상태 상단 심볼은 더 또렷하게 키우고, 프리셋 카드와 기타/프리셋 추가 카드의 아이콘은 한 단계 줄여 전체 균형을 맞췄다.

README와 DEVELOPMENT 문서에 변경 이력과 시점을 반영했다.

검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 19:43:03 +09:00
3198f822f5 AX Agent 상단 탭 복구 및 헤더 세그먼트 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
AX Agent 상단 탭이 비정상적인 타원 형태로 깨져 보이던 문제를 수정했다.

TopTabBtn 스타일을 다시 정리해 채팅/Cowork/코드 pill 세그먼트가 예전 형태로 안정적으로 렌더되도록 복구했다.

헤더 중앙 탭 래퍼의 배경, 코너, 패딩, 최소 높이를 조정해 탭 그룹이 자연스럽게 보이도록 맞췄다.

README와 DEVELOPMENT 문서에 변경 이력과 시점을 반영했다.

검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 19:40:11 +09:00
21dc280e57 AX Agent 공통 설정 상단 정리와 패널 토글 아이콘 개선
Some checks failed
Release Gate / gate (push) Has been cancelled
- AX Agent 내부 설정 공통 탭에서 테마 스타일과 테마 모드를 서비스/모델보다 위로 재배치함
- 좌측 사이드바 토글 버튼을 햄버거 아이콘 대신 패널 열기/닫기 의미가 드러나는 아이콘으로 교체함
- 메뉴 버튼과 혼동되지 않도록 상단 헤더 UX를 정리함
- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 19:29:23 +09:00
28e88d615f 런처 하단 위젯 기본값 비활성화
Some checks failed
Release Gate / gate (push) Has been cancelled
- 런처 하단 위젯 6종의 신규 기본값을 모두 꺼짐 상태로 변경함
- 성능, 포모도로, 메모, 날씨, 일정, 배터리 위젯이 새 설치와 초기화 시 기본 비노출로 시작하도록 조정함
- 기존 사용자 설정은 유지하고 AppSettings 기본값만 변경함
- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 19:25:15 +09:00
36828ba199 AX Agent transcript 품질 향상과 회귀 기준 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- 도구/스킬 transcript 표시 카탈로그를 분리해 파일, 빌드, Git, 문서, 질문, 제안, 스킬 분류를 공통 라벨로 통일함
- task summary popup을 active 우선, recent/debug 보조 기준으로 축소하고 transcript policy helper를 partial로 분리함
- AX Agent와 claw-code 비교용 회귀 프롬프트 세트를 별도 문서로 추가하고 관련 개발 문서를 즉시 갱신함
- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 19:20:05 +09:00
8dc2841da6 AX Agent 도구·스킬 transcript 표현을 claw-code 기준으로 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- 도구/스킬 이벤트 배지를 역할 중심 라벨로 단순화
- raw snake_case 대신 사람이 읽기 쉬운 표시명과 /skill 형식 적용
- 작업 요약 팝업의 권한/배경 작업 관측성 섹션을 debug 수준으로 제한
- parity 문서와 개발 이력 문서에 tool/skill UX 정리 기준 반영

검증 결과
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\
- 경고 0 / 오류 0
2026-04-05 19:05:49 +09:00
4cbe60052e AX Agent 질문/의견 요청 흐름을 transcript 우선으로 전환
Some checks failed
Release Gate / gate (push) Has been cancelled
- user_ask 콜백을 별도 팝업 대신 본문 inline 카드 경로로 변경
- 선택지 pill, 직접 입력, 전달/취소 버튼을 timeline 안에서 처리
- 계획 승인과 질문 요청이 같은 transcript-first UX 원칙을 따르도록 정리
- claw-code parity 문서와 개발 이력 문서에 질문/승인 UX 기준을 반영

검증 결과
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\
- 경고 0 / 오류 0
2026-04-05 18:59:32 +09:00
6c5b0c5be3 AX Agent 계획 승인 흐름을 transcript 우선으로 전환하고 claw-code 비교 기준을 문서화
- claw-code 대비 canonical prompt set 10종을 parity 문서에 추가해 Chat/Cowork/Code 회귀 검증 기준을 고정함
- AX와 claw-code의 도구/스킬 차이를 문서에 정리해 남은 parity 목표를 명확히 함
- PlanViewerWindow를 즉시 띄우지 않고 inline 승인/수정/취소 버튼을 transcript에 먼저 노출하도록 계획 승인 흐름을 변경함
- PlanViewerWindow는 하단 계획 버튼으로 여는 보조 상세 보기 역할로 축소함
- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ (경고 0 / 오류 0)
2026-04-05 18:53:28 +09:00
a3b3522bb7 AX Agent transcript 보조 메타를 마지막으로 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- StatusElapsed, StatusTokens를 값이 있을 때만 노출되게 바꿔 하단 상태선 빈 공간 제거

- ConversationQuickStrip을 실제 running/spotlight count가 있을 때만 보이게 조정

- README와 DEVELOPMENT 문서에 2026-04-05 21:43 (KST) 기준 최종 100% 마감 판단과 검증 결과 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 18:21:48 +09:00
3ba7c52980 AX Agent 계획 카드를 compact transcript 형태로 마감
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow planning card를 기본 접힘 상태의 요약형 카드로 전환

- 단계 목록은 펼치기/접기에서만 보이게 해 claw-code식 읽기 중심 transcript 흐름으로 정리

- README와 DEVELOPMENT 문서에 2026-04-05 21:36 (KST) 기준 100% 마감 판단과 검증 결과 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 18:19:11 +09:00
854f190531 AX Agent 레거시 plan 필드를 최종 제거
Some checks failed
Release Gate / gate (push) Has been cancelled
- AppSettings에서 planMode와 enablePlanModeTools JSON 필드를 삭제

- SubAgentTool의 llm.PlanMode 고정 대입 제거로 clean 코드 기준 PlanMode 참조를 0으로 정리

- README와 DEVELOPMENT 문서에 2026-04-05 21:29 (KST) 기준 변경 내역과 엔진 100% 판정 근거 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 18:16:41 +09:00
a0ce5846e1 AX Agent 구형 설정창의 PlanMode dead UI를 제거
Some checks failed
Release Gate / gate (push) Has been cancelled
- AgentSettingsWindow에서 계획 모드와 Plan Mode 도구 UI 및 save/load/event 코드 제거

- clean 파일 기준 검색상 남은 PlanMode 참조를 JSON 호환용 AppSettings와 SubAgentTool 안전 고정 경로로 축소

- README와 DEVELOPMENT 문서에 2026-04-05 21:20 (KST) 기준 변경 내역과 parity 99% 재평가 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 18:12:31 +09:00
1eb56404c7 AX Agent 설정/오버레이의 PlanMode dead UI를 제거
Some checks failed
Release Gate / gate (push) Has been cancelled
- SettingsWindow와 ChatWindow에서 숨김 PlanMode row, hidden toggle, dead handler와 binding 제거

- SettingsViewModel dead PlanMode property/save 경로 정리로 현재 정책과 설정 모델을 일치시킴

- README와 DEVELOPMENT 문서에 2026-04-05 21:12 (KST) 기준 변경 내역과 parity 98% 재평가 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 18:07:14 +09:00
d6bfca249e AX Agent 상태 모델의 PlanMode 잔재를 제거
Some checks failed
Release Gate / gate (push) Has been cancelled
- AppStateService PermissionPolicyState에서 레거시 PlanMode 필드를 제거

- 설정 로드 시 고정 off 값을 상태 모델에 복사하던 경로 정리

- README와 DEVELOPMENT 문서에 2026-04-05 21:03 (KST) 기준 변경 내역과 parity 재평가 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 18:01:10 +09:00
12746cdf11 AX Agent 레거시 plan 도구 registry와 상태 라벨을 추가 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- ToolRegistry, SkillService, AgentLoopService에서 enter_plan_mode/exit_plan_mode 등록과 별칭 제거

- AppStateService runtime label을 실행/권한 대기/백그라운드 중심으로 단순화해 queue 메타 기본 노출 축소

- README와 DEVELOPMENT 문서에 2026-04-05 20:56 (KST) 기준 변경 내역과 parity 재평가 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 17:58:33 +09:00
6b645ccbb7 AX Agent 레거시 plan 도구와 상태 배지 노출을 추가 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- code 탭에서 enter_plan_mode/exit_plan_mode 도구를 항상 비활성화해 레거시 설정값이 런타임을 흔들지 않도록 정리

- queue/재시도 대기만 남은 상태에서는 RuntimeActivityBadge와 상단 strip이 기본 노출되지 않게 조정

- idle Cowork/Code 헤더 소음을 줄이도록 ChatWindow 상태 표시 조건을 추가 보정

- README와 DEVELOPMENT 문서에 2026-04-05 20:48 (KST) 기준 변경 내역 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 17:55:01 +09:00
51a398d269 AX Agent 하단 상태/큐 기본 노출 추가 축소
Some checks failed
Release Gate / gate (push) Has been cancelled
- draft queue 패널은 실행중·다음·실패 항목이 없으면 기본 화면에서 숨기도록 조정

- 컨텍스트 사용량 hover 팝업을 현재 모델 사용량과 compact 상태 2줄 요약으로 축소

- README와 DEVELOPMENT 이력에 claw-code parity 기준 하단 UX 정리 내용을 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
2026-04-05 17:43:03 +09:00
595f8a76af AX Agent 하단 보조 UI 최소 노출 기준 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- draft queue 패널은 기본 상태에서 실행/다음/실패가 없으면 숨기도록 조정

- 컨텍스트 사용량 hover 팝업을 현재 모델 사용량과 compact 상태 2줄 요약으로 축소

- README와 DEVELOPMENT 이력에 claw-code parity 기준 상태를 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
2026-04-05 17:22:11 +09:00
050271e2a9 AX Agent 대기열 적재 경로 공통화 반영
Some checks failed
Release Gate / gate (push) Has been cancelled
- retry, follow-up, branch follow-up, steering 요청이 모두 EnqueueDraftRequest helper를 타도록 정리

- 현재 대화 갱신과 세션 반영 지점을 queue 생성 helper 한 군데로 모아 이후 정책 변경 시 일관성을 높임

- README와 DEVELOPMENT 이력에 claw-code parity 기준 진행 상황을 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
2026-04-05 17:18:56 +09:00
4184d89168 AX Agent 큐 요약 최소화와 플랜도구 기본값 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- PlanMode 및 EnablePlanModeTools를 레거시 호환용 설명과 false 기본값 기준으로 정리

- Cowork/Code compact 대기열 요약에서 실행/다음/실패만 기본 노출하고 보류/완료는 상세 보기에서만 보이도록 조정

- claw-code parity 계획 문서와 README, DEVELOPMENT 이력에 현재 상태와 남은 리뷰 항목을 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
2026-04-05 17:15:30 +09:00
303a23130b AX Agent 플랜모드 잔재 제거와 상태바 소음 축소 반영
Some checks failed
Release Gate / gate (push) Has been cancelled
- AX Agent 내부 설정 오버레이에서 Plan Mode 도구 저장/노출 경로를 false 고정으로 정리

- 메인 설정에 남아 있던 플랜 모드 및 Plan Mode 도구 UI를 숨기고 카드 상태를 off 고정으로 정리

- Cowork/Code 상태바가 debug가 아닐 때 ToolCall/SkillCall/Paused/Resumed 이벤트로 과하게 흔들리지 않도록 조정

- claw-code parity 계획 문서와 README, DEVELOPMENT 이력을 현재 정책과 진척율 기준으로 갱신

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
2026-04-05 17:11:30 +09:00
78b962bece AX Agent 프리셋 안내와 상단 탭 레이아웃 복구\n\n- Chat/Cowork 프리셋 빈 화면을 Stretch 기반으로 바꿔 카드 하단 잘림을 줄임\n- 선택한 대화 주제/작업 유형을 헤더 중앙 가이드로 다시 표시하도록 복구\n- 상단 탭 pill 그룹과 하단 사용자 영역 설정 버튼 크기를 키워 가독성 보정\n- README와 DEVELOPMENT 문서에 2026-04-05 19:59(KST) 기준 변경 이력 반영\n- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
Some checks failed
Release Gate / gate (push) Has been cancelled
2026-04-05 16:59:42 +09:00
f8e62bde2a AX Agent 계획 모드 잔재를 더 걷어내고 UI hover 안정화\n\n- AgentLoop 실행 경로에서 계획 모드 분기를 비활성 정책 기준으로 단순화해 기본 런타임이 설정값에 흔들리지 않게 정리\n- 내부 설정의 숨김 상태 계획 모드 버튼과 콤보 이벤트 연결 및 관련 dead code 제거\n- 작업유형 카드에서 hover 라벨과 ToolTip 충돌로 발생하던 깜박임을 해결\n- README와 DEVELOPMENT 문서에 parity 진척율, 남은 작업축, 설정 제거 후보를 반영\n\n검증 결과\n- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\\n- 경고 0 / 오류 0
Some checks failed
Release Gate / gate (push) Has been cancelled
2026-04-05 16:51:24 +09:00
a315f587bf AX Agent 남은 parity 작업과 설정 정리 기준을 문서화하고 카드 hover 깜박임 수정\n\n- claw-code 대비 AX Agent 핵심 엔진/UI 남은 차이와 현재 추정 진척율을 parity 계획 문서에 기록\n- PlanMode, FreeTierDelaySeconds, MaxAgentIterations, MaxRetryOnError 등 런타임 영향 설정의 제거/개발자 전용 후보를 정리\n- 작업유형 카드에서 custom hover 라벨과 기본 ToolTip이 충돌해 발생하던 깜박임을 제거\n- README와 DEVELOPMENT 문서에 변경 이유와 검증 결과를 즉시 반영\n\n검증 결과\n- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\\n- 경고 0 / 오류 0
Some checks failed
Release Gate / gate (push) Has been cancelled
2026-04-05 16:44:35 +09:00
500c8ffb06 AX Agent 좌측 패널과 컨텍스트 사용량 UI를 Codex 기준으로 재정렬\n\n- 좌측 사이드바 폭과 새 대화/검색/필터/탭 메뉴 타이포를 키워 레퍼런스와 더 비슷한 밀도로 조정\n- Cowork 작업 유형 카드 크기와 제목/hover 설명 라벨 폰트를 확대해 가독성 보정\n- 하단 컨텍스트 사용량 카드를 작은 원형 심볼로 축소하고 hover 전용 커스텀 팝업으로 상세 정보 분리\n- README와 DEVELOPMENT 문서에 변경 이력 및 검증 결과 즉시 반영\n\n검증 결과\n- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\\n- 경고 0 / 오류 0
Some checks failed
Release Gate / gate (push) Has been cancelled
2026-04-05 16:39:32 +09:00
b53af39358 AX Agent 상태 노출과 계획 표시를 claw-code 기준으로 정리
- 대화 상단 빠른 스트립을 자동 카운트 노출 대신 사용자 필터와 정렬 전환 시에만 보이도록 조정
- 상단 상태 스트립은 권한 대기와 실패 계열만 남기고 queue 관련 기본 배너를 제거
- planning 이벤트는 기본 transcript에서 큰 카드 대신 compact pill만 표시되도록 변경
- README와 DEVELOPMENT 문서에 claw-code 비교 기준 정리 이력 및 검증 결과 반영

검증 결과
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\
- 경고 0 / 오류 0
2026-04-05 16:32:40 +09:00
0fd6940975 AX Agent 상단 탭 스타일을 사용자 레퍼런스 형태로 복구
Some checks failed
Release Gate / gate (push) Has been cancelled
- 상단 탭 버튼의 폰트, 패딩, 외곽선을 다시 키워 도톰한 pill 세그먼트 형태로 조정
- 탭 래퍼 배경과 선택 탭 배경을 LauncherBackground 기준으로 정리하고 채팅/코드 라벨을 한글로 복구
- README와 DEVELOPMENT 문서에 상단 탭 UI 복구 이력 및 검증 결과 반영

검증 결과
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\
- 경고 0 / 오류 0
2026-04-05 16:28:44 +09:00
29652c3ad4 AX Agent 내부 설정 서비스 전환과 테마 노출 정리\n\n- 공통 탭의 서비스 상세 패널을 주소 입력과 API 키 패널로 분리하고 Gemini/Claude에서는 주소 입력을 숨기도록 조정\n- 서비스 변경 직후 현재 서비스, 현재 모델, 라벨이 즉시 갱신되도록 내부 설정 새로고침 경로 보강\n- 테마 스타일과 테마 모드를 서비스/모델 바로 아래로 이동해 공통 탭에서 바로 보이게 재배치\n- README와 DEVELOPMENT 문서에 내부 설정 공통 탭 수정 이력 및 검증 결과 반영\n\n검증 결과\n- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\\n- 경고 0 / 오류 0
Some checks failed
Release Gate / gate (push) Has been cancelled
2026-04-05 16:26:15 +09:00
c3e1422b02 AX Agent 작업 유형 카드 정리 및 hover 설명 라벨 적용
Some checks failed
Release Gate / gate (push) Has been cancelled
- 작업 유형 카드 하단 상시 설명을 제거하고 모든 카드 크기를 동일 규격으로 통일함
- 카드 hover 시 확대 애니메이션을 제거하고 하단 설명 라벨과 배경/테두리 반응만 남겨 안정적으로 정리함
- 기타/프리셋 추가 카드도 같은 hover 규칙과 설명 노출 방식으로 통일함
- EmptyState 제목/설명 폰트를 키워 현재 화면 전반의 글자 크기를 보정함
- README 및 DEVELOPMENT 문서에 2026-04-05 19:02 (KST) 기준 변경 이력을 반영함

검증:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\
- 경고 0 / 오류 0
2026-04-05 16:20:55 +09:00
a4d21ecc0b AX Agent 상단 탭 복구 및 하단 컴포저 겹침 수정
Some checks failed
Release Gate / gate (push) Has been cancelled
- 상단 Chat/Cowork/Code 탭의 폰트와 패딩을 키워 예전처럼 더 읽기 쉬운 pill 형태로 복구함
- 하단 컴포저 상단 줄에서 토큰 카드와 프리셋 버튼이 같은 Grid 컬럼을 공유하던 구조를 분리해 겹침을 제거함
- 모델 선택, 토큰 사용 카드, 프리셋 버튼의 패딩과 글자 크기를 다시 키워 하단 정보 가독성을 복구함
- README 및 DEVELOPMENT 문서에 2026-04-05 18:55 (KST) 기준 변경 이력을 반영함

검증:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\
- 경고 0 / 오류 0
2026-04-05 16:17:57 +09:00
51ff046e1a AX Agent 타임라인 기본 노출 축소 및 계획/작업 카드 경량화
Some checks failed
Release Gate / gate (push) Has been cancelled
- non-debug 기본 로그에서 ToolCall 중간 이벤트를 숨겨 Cowork/Code 타임라인을 결과 중심으로 재정리함
- 계획 카드와 작업 요약 카드, 액션 버튼의 패딩/폰트/폭을 낮춰 transcript 중심 흐름을 강화함
- 권한 작업 카드에서 계획 모드 버튼을 제거해 현재 엔진 정책과 UI를 일치시킴
- README 및 DEVELOPMENT 문서에 2026-04-05 18:49 (KST) 기준 변경 이력과 parity 진척율을 반영함

검증:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\
- 경고 0 / 오류 0
2026-04-05 16:15:31 +09:00
abd6928e4a AX Agent 상태 UI 최소 노출 기준 정리 및 작업 요약 팝업 경량화
Some checks failed
Release Gate / gate (push) Has been cancelled
- Cowork/Code 보조 상태 스트립과 실행 로그 토글 영역의 패딩/폰트/간격을 추가 축소해 transcript 중심 흐름을 강화함
- 작업 요약 팝업에서 필터 칩과 과한 대시보드형 액션을 제거하고 마지막 실행 1건 중심 요약으로 재구성함
- 훅/백그라운드/최근 작업 카드의 밀도를 낮추고 기본 팝업에서 권한/백그라운드 현재 상태만 남기도록 최소 노출 기준으로 정리함
- README 및 DEVELOPMENT 문서에 2026-04-05 18:40 (KST) 기준 변경 이력과 parity 진척율을 반영함

검증:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\
- 경고 0 / 오류 0
2026-04-05 16:10:51 +09:00
aa3de8a6fd AX Agent 메인 UI를 claw-code 축으로 전면 재배치
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow 메인 레이아웃을 기존 카드형 AX 틀에서 벗어나 사이드바·본문·컴포저 중심의 더 평평한 claw-code 스타일로 재정렬

- 창 기본 크기, 사이드바 폭, 상단 탭 헤더, 검색 바, 빈 상태, 컴포저 외곽선과 입력축을 다시 조정해 메시지 축과 입력축이 같은 중심선을 공유하도록 변경

- 메시지 버블, compact pill, 이전 대화 로드 카드, planning 카드의 패딩·코너·메타 밀도를 낮춰 transcript 우선 시각 언어로 통일

- README와 docs/DEVELOPMENT.md에 2026-04-05 18:30 (KST) 기준 작업 이력 및 claw-code 대비 진척율 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 16:03:26 +09:00
ed1b8497c6 AX Agent 재생성 흐름과 엔진 마감 문구를 상태 기반으로 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- regenerate/retry가 MessagePanel 직접 삭제 대신 conversation state 수정 후 RenderMessages 경로를 타도록 정리

- AxAgentExecutionEngine에 UI용 마감 helper를 추가해 취소/오류/빈 응답과 Cowork·Code 완료 문구를 정상 한국어 기준으로 정규화

- Paused/Resumed 실행 이벤트는 debug가 아닐 때 기본 타임라인에서 숨겨 Cowork/Code 노이즈를 축소

- README와 docs/DEVELOPMENT.md에 2026-04-05 18:20 (KST) 기준 작업 이력 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 15:55:28 +09:00
57be80af3c AX Agent 실행 마감 helper 통합과 계획 노출 최소화
Some checks failed
Release Gate / gate (push) Has been cancelled
- send와 regenerate가 같은 실행/예외/취소/최종 커밋/후처리 helper를 타도록 ExecutePreparedTurnAsync 추가

- 계획 이벤트는 기본적으로 얇은 요약 pill만 노출하고 debug에서만 큰 카드가 보이도록 조정

- README와 DEVELOPMENT 문서에 2026-04-05 18:08 (KST) 기준 이력 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 15:43:44 +09:00
e4fddca53c AX Agent 계획 카드와 타임라인 카드 밀도 추가 축소
Some checks failed
Release Gate / gate (push) Has been cancelled
- 계획 카드의 라운드, 패딩, 헤더, 진행률 텍스트, 단계 행 폰트를 더 줄여 claw-code 쪽 카드 밀도로 정리

- 컨텍스트 압축 pill과 이전 대화 로드 카드도 함께 축소해 본문 대비 존재감을 낮춤

- README와 DEVELOPMENT 문서에 2026-04-05 18:01 (KST) 기준 이력 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 15:38:25 +09:00
3ea497f10a AX Agent 사이드바와 대화 목록 밀도 대폭 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- 좌측 헤더, 액션, 검색 편집기, 필터, 탭별 메뉴, 삭제/사용자 영역을 전반적으로 축소해 claw-code 쪽 비율로 정리

- 사이드바 폭을 248로 줄이고 대화 목록 카드 패딩, 메타, 편집 버튼, 선택 액센트 바 두께를 함께 축소

- README와 DEVELOPMENT 문서에 2026-04-05 17:53 (KST) 기준 이력 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 15:34:35 +09:00
825f7d55f2 AX Agent 상단 헤더 밀도 추가 축소
Some checks failed
Release Gate / gate (push) Has been cancelled
- 상단 탭 버튼과 탭 그룹 래퍼의 폰트, 패딩, 코너를 더 줄여 claw-code 쪽 밀도로 정리

- 제목 서브 바, 대화 제목, 빠른 스트립, 프리뷰 토글 규격을 함께 낮춰 상단 보조 정보 존재감을 축소

- README와 DEVELOPMENT 문서에 2026-04-05 17:45 (KST) 기준 이력 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 15:31:25 +09:00
abd33eb5df AX Agent 실행 타임라인 배너 밀도 추가 축소
- 일반 실행 배너의 아이콘, 라벨, 경과 시간, 토큰 pill, 요약, 파일 경로 표시를 더 얇게 정리

- review 신호 칩은 debug 로그에서만 보이게 제한해 평소 Cowork/Code 본문 흐름을 단순화

- README와 DEVELOPMENT 문서에 2026-04-05 17:39 (KST) 기준 이력 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 15:28:33 +09:00
cd1db562b1 AX Agent 메시지 메타와 완료 카드 문구 추가 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- 메시지 버블 패딩, 코너, 폰트, 타임스탬프와 assistant 헤더 메타를 더 축소해 본문 중심 흐름 강화

- 작업 요약 팝업의 완료 카드 라벨과 run/step 메타를 더 짧고 가볍게 정리

- README와 DEVELOPMENT 문서에 2026-04-05 17:33 (KST) 기준 이력 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 15:01:05 +09:00
d575139a6f AX Agent 상태 스트립과 작업 요약 팝업 밀도 추가 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- Cowork/Code 보조 상태 스트립의 폰트, 패딩, 간격을 더 줄여 본문 우선 시각 흐름에 맞춤

- 작업 요약 팝업 헤더/필터/최근 실행/훅/백그라운드 카드와 액션 버튼 규격을 축소

- README와 DEVELOPMENT 문서에 2026-04-05 17:27 (KST) 기준 이력 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 14:52:21 +09:00
3b223dc7dc AX Agent 상단 헤더와 대화 목록 메타를 claw-code 쪽으로 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- 상단 탭 그룹과 사이드바 토글 버튼을 더 얇은 세그먼트형 UI로 조정

- 좌측 대화 목록의 실행 메타와 편집 아이콘을 더 약하게 줄여 제목 중심의 목록 밀도로 재정렬

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 14:46:31 +09:00
f3c0366ee6 AX Agent 메시지 액션과 실행 배너를 claw-code 쪽으로 축소
Some checks failed
Release Gate / gate (push) Has been cancelled
- 메시지 액션 바를 텍스트+아이콘 조합에서 작은 아이콘 버튼 중심으로 바꿔 본문 집중도를 높임

- Cowork/Code 실행 배너의 여백, 메타 텍스트, 토큰 배지, 파일 경로 표시를 더 얇게 줄여 보조 정보 밀도를 낮춤

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 14:43:59 +09:00
7cc2b5b9b5 AX Agent UI를 claw-code 방향으로 골격 재정렬
Some checks failed
Release Gate / gate (push) Has been cancelled
- 좌측 패널, 축소 아이콘 바, 상태 스트립, 메시지 축, 컴포저를 더 얇고 평평한 업무형 레이아웃으로 조정

- 메시지 최대 폭 880, 컴포저 최대 폭 820 기준으로 반응형 폭 계산을 다시 맞춰 창 크기 변화에도 중심선이 자연스럽게 유지되도록 수정

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 14:40:44 +09:00
d99b46e3e2 AX Agent claw-code 진척율 문서화 및 메시지 축 단순화
Some checks failed
Release Gate / gate (push) Has been cancelled
- claw-code 대비 AX Agent 진척율을 README, DEVELOPMENT, parity 계획 문서에 수치로 기록

- 핵심 엔진 영향 설정 최소화 원칙을 정리하고 계획 모드 잔재 노출을 추가로 축소

- ChatWindow 메시지 버블과 좌측 대화 목록 카드를 더 얇은 밀도로 조정해 claw-code식 읽기 축에 가깝게 정리

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 14:38:03 +09:00
22eebc13d9 AX Agent UI와 응답 마감 엔진을 claw-code 기준으로 1차 재구성
Some checks failed
Release Gate / gate (push) Has been cancelled
사이드바, 메시지 축, 빈 상태, 컴포저를 더 얇고 밀도 높은 구조로 재배치하고 보조 상태 스트립 노출을 줄였습니다.

반응형 폭 계산을 다시 조정해 창 크기 변화에서도 메시지와 컴포저가 같은 중심선으로 자연스럽게 줄어들게 했습니다.

응답 취소와 오류 마감은 AxAgentExecutionEngine의 FinalizeExecutionContent로 공통화해 전송과 재생성의 후처리 경로를 맞췄습니다.

검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 오류 0
2026-04-05 14:19:07 +09:00
f53f35bbed 계획 모드 설정 제거와 플랜 승인 UX 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
메인 설정과 AX Agent 내부 설정에서 계획 모드 UI를 숨기고 저장값을 항상 off로 고정했습니다.

AgentLoop 런타임도 계획 모드를 off로 고정해 코워크와 코드에서 자동 계획 승인 팝업이 반복 노출되지 않도록 정리했습니다.

PlanViewerWindow는 AX Agent 창 owner 리소스를 직접 받아 같은 테마 축을 따르도록 바꾸고 인라인 승인 버튼 중복 노출을 제거했습니다.

검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 오류 0
2026-04-05 14:12:22 +09:00
9fafcd0192 코워크 문서 계획/생성 흐름을 claw-code 기준으로 복구
- document_plan 결과에서 body 골격과 후속 생성 도구를 안정적으로 추출하도록 AgentLoop 분기를 수정

- 코워크 문서형 작업은 planMode=off여도 계획 선행(always) 경로를 타도록 보정

- 코워크 시스템 프롬프트를 강화해 계획만 제시하고 끝나지 않고 실제 문서 파일 생성까지 이어지게 조정

- README와 DEVELOPMENT 문서에 2026-04-05 16:02 (KST) 기준 변경 이력 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 14:05:18 +09:00
35fbfc933d AX Agent: 실행 이벤트 세션 변이 경로 엔진으로 통합
- ChatWindow에 중복돼 있던 실행 이벤트/Agent run 교차 탭 복원 로직을 AxAgentExecutionEngine helper로 이동함

- AppendExecutionEvent, AppendAgentRun이 공통 session mutation 경로를 사용하도록 정리해 이후 runtime state 공통화 기반을 마련함

- README와 DEVELOPMENT 문서에 2026-04-05 15:42 (KST) 기준 변경 이력 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
2026-04-05 13:58:16 +09:00
5c142e1235 문서: claw-code 기준 AX Agent 개선 계획 재정립
Some checks failed
Release Gate / gate (push) Has been cancelled
- claw-code 실제 런타임 참조 축(state/initReplBridge/sessionRunner/REPL/Messages/StatusLine)을 기준으로 AX Agent 개선 계획을 재수립함

- README, DEVELOPMENT, parity plan 문서에 참조 파일, AX 적용 위치, 완료 조건, 품질 기준, 검증 시나리오를 반영함

- 이번 변경은 계획 문서 정리만 포함하며 코드 수정이나 빌드 검증은 수행하지 않음
2026-04-05 13:53:53 +09:00
5fd69d32f5 AX Commander 하단 위젯 설정 분리와 서버 상태 제거
Some checks failed
Release Gate / gate (push) Has been cancelled
- 일반 설정의 AX Commander 섹션에 성능, 포모도로, 메모, 날씨, 일정, 배터리 위젯 표시 토글을 추가

- 런처 하단의 Ollama, API, MCP 서버 상태 위젯을 완전히 제거하고 남은 위젯만 설정값 기준으로 표시되도록 정리

- 배터리 위젯은 실제 배터리 가용 상태와 사용자 토글을 함께 반영하고 위젯이 모두 꺼지면 하단 바 전체를 숨기도록 조정

- README와 DEVELOPMENT 문서를 2026-04-05 15:16 (KST) 기준으로 갱신하고 dotnet build 검증에서 경고 0 오류 0 확인
2026-04-05 13:48:00 +09:00
d368ebf822 AX Agent 내부 설정 탭 분리와 테마 복구
Some checks failed
Release Gate / gate (push) Has been cancelled
- AX Agent 내부 설정에서 스킬/차단 탭을 도구, 스킬, 차단으로 분리하고 각 패널을 기능별로 재배치

- 공통 탭에 테마 스타일과 테마 모드를 실제 선택 카드 UI로 복구하고 기존 숨김 플레이스홀더를 제거

- 메인 설정 좌측의 AX Agent 이동 항목을 맨 아래로 재배치하고 README 및 DEVELOPMENT 문서 이력을 2026-04-05 15:06 (KST) 기준으로 갱신

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ / 경고 0 오류 0
2026-04-05 13:39:44 +09:00
d102e17d47 AX Agent 대화 목록 카드와 축소 아이콘 바 UI 밀도 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow 대화 목록 행 카드의 선택/호버 강조를 더 얇게 줄이고 제목, 메타, 배지, 편집 버튼 밀도를 claw-code 기준으로 정리

- 축소 아이콘 바의 행 높이, 버튼 규격, 아이콘 크기, 사용자 배지를 현재 사이드바와 같은 중립형 시각 언어로 통일

- README와 DEVELOPMENT 문서에 변경 이력과 2026-04-05 14:57 (KST) 기준 업데이트 시각 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ / 경고 0 오류 0
2026-04-05 13:35:16 +09:00
792dea2dc2 AX Agent 실행 로그 배너의 파일 경로와 디버그 노출 축소
Some checks failed
Release Gate / gate (push) Has been cancelled
- AddAgentEventBanner에서 debug용 ToolInput 카드 길이와 패딩을 줄이고 일반 로그에서는 파일 경로를 카드형이 아닌 파일명 한 줄로만 표시하도록 분기함

- 파일 빠른 액션과 상세 경로 카드는 debug에서만 크게 보이게 바꿔 Cowork/Code 본문 아래 실행 로그가 메시지 축을 덜 밀어내도록 조정함

- README와 DEVELOPMENT 문서에 2026-04-05 14:49 (KST) 기준 작업 이력을 반영하고 Release 빌드 경고 0 오류 0을 확인함
2026-04-05 13:30:42 +09:00
cec4b75999 AX Agent 좌측 패널과 하단 상태바 형태 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow 사이드바 폭을 줄이고 헤더 배지, 새 대화/검색/카테고리 메뉴 행의 패딩과 아이콘 크기를 낮춰 claw-code 방향의 얇은 내비게이션으로 조정함

- 아이콘 바 사용자 배지, 하단 계정 영역, 삭제 버튼, 상태바 높이와 상태 아이콘 형태를 함께 단순화해 전체 시각 언어를 더 중립적으로 통일함

- README와 DEVELOPMENT 문서에 2026-04-05 14:43 (KST) 기준 작업 이력을 반영하고 Release 빌드 경고 0 오류 0을 확인함
2026-04-05 13:27:58 +09:00
2d7ede357e AX Agent 작업 요약 팝업 카드 밀도 추가 축소
Some checks failed
Release Gate / gate (push) Has been cancelled
- 작업 요약 액션 버튼 규격을 더 작게 낮추고 권한/훅/백그라운드 카드 패딩과 마진을 줄여 상태 패널 세로 길이를 완화함

- 최근 권한, 훅, 백그라운드 작업 표시 개수를 줄여 Cowork/Code 상태 요약이 긴 대시보드처럼 커지지 않도록 조정함

- README와 DEVELOPMENT 문서에 2026-04-05 14:36 (KST) 기준 작업 이력을 반영하고 Release 빌드 경고 0 오류 0을 확인함
2026-04-05 13:24:51 +09:00
88a21ead92 AX Agent 상태 스트립과 작업 요약 UI 축소
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow의 ConversationStatusStrip, AgentProgressBar, RuntimeActivityBadge, 실행 로그 관련 상태 요소 패딩과 폰트 밀도를 낮춰 본문 우선 구조로 조정함

- 작업 요약 팝업의 제목/설명/최근 실행 카드 밀도를 줄이고 최근 실행 표시 수를 축소해 상태 패널이 보조 레이어로 남도록 정리함

- README와 DEVELOPMENT 문서에 2026-04-05 14:31 (KST) 기준 작업 이력을 반영하고 Release 빌드 경고 0 오류 0을 확인함
2026-04-05 13:22:19 +09:00
458fd8da96 AX Agent 컴포저 상단 구조 밀도 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow 입력 바의 InputBorder, DraftPreviewCard, DraftQueuePanel 패딩과 그림자를 줄여 claw-code 방향의 얇은 작업 바 인상으로 정리함

- 모델 선택, 토큰 사용 카드, 프리셋 버튼의 높이·패딩·폰트·아이콘 크기를 함께 낮춰 옵션 바가 메시지 축보다 튀지 않도록 조정함

- README와 DEVELOPMENT 문서에 2026-04-05 14:23 (KST) 기준 작업 이력을 반영하고 Release 빌드 경고 0 오류 0을 확인함
2026-04-05 13:18:18 +09:00
b24afba2d8 AX Agent 채팅창 반응형 폭 계산 적용
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow의 ComposerShell 고정폭을 제거하고 실제 본문 영역 폭 기준으로 MessagePanel, EmptyState, ComposerShell을 함께 재계산하도록 정리함

- 창 크기 변경 시 메시지 축과 입력창이 자연스럽게 함께 줄고 늘어나도록 Loaded와 SizeChanged에 반응형 레이아웃 갱신을 연결함

- README와 DEVELOPMENT 문서에 2026-04-05 14:16 (KST) 기준 작업 이력을 반영하고 Release 빌드 경고 0 오류 0을 확인함
2026-04-05 13:13:15 +09:00
382c78e32f AX Agent 메시지 축 UI 밀도 정리 및 로그 표시 단순화
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow 메시지 카드의 여백, 패딩, 라운드, 타임스탬프 크기를 줄여 claw-code 기준의 본문 중심 읽기 흐름으로 정리함

- assistant 헤더와 액션 바, 실행 로그 배너의 아이콘/텍스트/배지 밀도를 낮춰 Cowork/Code에서 로그가 메시지보다 튀지 않도록 조정함

- README와 DEVELOPMENT 문서에 2026-04-05 14:09 (KST) 기준 작업 이력을 반영하고 Release 빌드 경고 0 오류 0을 확인함
2026-04-05 13:10:14 +09:00
28869caa32 AX Agent 채팅 UI를 claw-code 방향으로 1차 단순화
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow 메시지 컬럼과 빈 상태 폭을 880 기준으로 정리하고 상단 진행률 바 패딩과 폭을 축소

- 빈 상태의 부유 애니메이션 아이콘을 제거하고 정적인 카드형 아이콘과 간결한 문구로 단순화

- 컴포저를 800px 축으로 넓히고 DraftPreview/Input 테두리의 라운드와 그림자 강도를 낮춰 메시지와 입력 축이 먼저 보이게 조정

- README와 DEVELOPMENT 문서에 2026-04-05 14:00 (KST) 기준 이력과 검증 결과를 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0, 오류 0)
2026-04-05 13:02:22 +09:00
bd8a1ef7bd AX Agent 구형 본문 재시도 카드를 제거해 메시지 축을 더 단순화
- ChatWindow의 AddRetryButton 경로를 제거하고 실패 시 토스트 안내 후 작업 요약 재시도 액션을 사용하도록 정리

- 본문 MessagePanel에 임시 실패 카드를 직접 삽입하던 구형 흐름을 걷어내고 claw-code 방향의 메시지/상태 중심 구조로 추가 정리

- README와 DEVELOPMENT 문서에 2026-04-05 13:52 (KST) 기준 이력과 검증 결과를 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0, 오류 0)
2026-04-05 12:59:22 +09:00
554b1fb83e AX Agent 실패 재시도도 입력창 UI에서 분리해 Cowork·Code 실행 축을 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
- RetryLastUserMessageFromConversation이 입력창 텍스트와 포커스를 직접 바꾸지 않고 유휴 시 즉시 재실행, 진행 중에는 직접 대기열 적재로 전환

- 재시도 흐름을 입력 UI보다 세션/대기열 중심으로 옮겨 Cowork·Code 후처리 일관성을 강화

- README와 DEVELOPMENT 문서에 2026-04-05 13:44 (KST) 기준 이력과 검증 결과를 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0, 오류 0)
2026-04-05 12:56:29 +09:00
c5dfab8081 AX Agent 대기열 후속 실행을 입력창 UI에서 분리해 Cowork·Code 엔진 축을 강화
Some checks failed
Release Gate / gate (push) Has been cancelled
- SendMessageAsync가 직접 텍스트 인자를 받을 수 있게 바꾸고 대기열 후속 실행이 InputBox 상태를 바꾸지 않도록 조정

- StartNextQueuedDraftIfAny에서 입력창 텍스트/포커스/높이 조작을 제거하고 SendMessageAsync(next.Text) 직접 실행 경로로 전환

- README와 DEVELOPMENT 문서에 2026-04-05 13:37 (KST) 기준 이력과 검증 결과를 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0, 오류 0)
2026-04-05 12:54:19 +09:00
52475b6628 AX Agent 실행 이벤트 UI 갱신을 배치형으로 조정해 Cowork·Code 흔들림을 줄임
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow에 agent UI event timer를 추가해 상태바, 진행률, 플랜 뷰어, 자동 프리뷰를 최근 이벤트 기준으로 묶어서 반영

- OnAgentEvent는 실행 상태를 먼저 conversation/app state에 반영하고 화면 갱신은 FlushPendingAgentUiEvent 경로로 분리

- README와 DEVELOPMENT 문서에 2026-04-05 13:29 (KST) 기준 이력과 검증 결과를 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0, 오류 0)
2026-04-05 12:51:54 +09:00
0b1bc5f32f AX Agent 완료 직후 저장 경로를 정리해 Cowork·Code 실행 기록 누락을 방지
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow에 PersistConversationSnapshot을 추가해 중간 저장, 최종 저장, 지연 저장 flush를 한 경로로 통합

- ResetStreamingUiState에서 대기 중인 실행 이벤트 저장을 먼저 flush 하도록 바꿔 마지막 이벤트 누락 가능성을 차단

- RunAgentLoopAsync 내부의 중복 저장/RememberConversation 호출을 제거하고 FinalizeConversationTurn 단일 완료 경로로 정리

- README와 DEVELOPMENT 문서에 2026-04-05 13:20 (KST) 기준 이력과 검증 결과를 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0, 오류 0)
2026-04-05 12:48:13 +09:00
f18f48789a AX Agent 실행 보조 UI를 축약형으로 안정화하고 메시지 축 흔들림을 줄임
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow의 suggest_actions 결과를 본문 MessagePanel 직접 삽입 대신 토스트 요약으로 전환

- DraftQueuePanel을 기본 축약형으로 바꾸고 상세 보기 토글을 추가해 컴포저 위 레이아웃 변동을 줄임

- README와 DEVELOPMENT 문서에 2026-04-05 13:12 (KST) 기준 이력과 검증 결과를 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0, 오류 0)
2026-04-05 12:45:01 +09:00
abc355c451 AX Agent 실행 이벤트 저장을 배치형으로 조정
Some checks failed
Release Gate / gate (push) Has been cancelled
- execution event와 agent run 기록이 들어올 때마다 즉시 저장하지 않고 conversationPersistTimer로 220ms 단위 지연 저장하도록 변경함

- Cowork/Code 연속 이벤트 구간의 디스크 I/O를 줄여 체감 끊김과 잦은 저장 부하를 낮추는 방향으로 정리함

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
2026-04-05 12:39:54 +09:00
d24596a8ea AX Agent 상태 스트립 갱신도 배치형으로 조정
Some checks failed
Release Gate / gate (push) Has been cancelled
- task summary 전용 타이머를 추가해 RuntimeActivityBadge와 ConversationStatusStrip 갱신을 120ms 단위로 묶음

- 실행 이벤트 본문 재렌더 배치와 함께 Cowork/Code 실행 중 UI 깜빡임을 줄이는 방향으로 정리함

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
2026-04-05 12:37:29 +09:00
a40cacea4d AX Agent 실행 이벤트 재렌더를 배치형으로 조정
Some checks failed
Release Gate / gate (push) Has been cancelled
- OnAgentEvent가 실행 이벤트마다 본문 전체를 즉시 다시 그리지 않도록 execution history 전용 배치 렌더 타이머를 추가함

- Cowork/Code 실행 중 RenderMessages 호출을 120ms 단위로 묶어 본문 번쩍임과 잦은 로그 재배치를 줄임

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
2026-04-05 12:34:53 +09:00
8921f5da0f AX Agent 실행 후처리 경로를 공통화
Some checks failed
Release Gate / gate (push) Has been cancelled
- ResetStreamingUiState, FinalizeConversationTurn, FinalizeQueuedDraft를 추가해 전송과 재생성의 완료 후 UI/상태 정리를 같은 경로로 통합함

- 대화 저장, 목록 갱신, 대기열 완료/실패 반영, 다음 작업 시작 흐름을 helper 기준으로 다시 묶음

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
2026-04-05 12:32:12 +09:00
31a8b979c7 AX Agent 실행 선택 분기를 엔진으로 이관
Some checks failed
Release Gate / gate (push) Has been cancelled
- AxAgentExecutionEngine에 ExecutePreparedAsync를 추가해 AgentLoop와 일반 LLM 호출 선택을 엔진이 담당하도록 정리함

- SendMessageAsync와 SendRegenerateAsync가 공통 실행 진입점을 사용하도록 바꿔 창 코드의 중복 분기를 줄임

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
2026-04-05 12:29:13 +09:00
660f4e5a32 AX Agent 최종 응답 커밋 흐름을 엔진으로 통합
Some checks failed
Release Gate / gate (push) Has been cancelled
- AxAgentExecutionEngine에 FinalizeAssistantTurn을 추가해 assistant 최종 내용 정규화, Cowork/Code 실행 로그 접힘, 메시지 커밋을 한 메서드로 통합함

- SendMessageAsync와 SendRegenerateAsync가 동일한 엔진 마무리 경로를 타도록 정리해 UI 쪽 중복 후처리를 줄임

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
2026-04-05 12:26:51 +09:00
67961f280f AX Agent 실행 준비 로직을 엔진으로 추가 이관
Some checks failed
Release Gate / gate (push) Has been cancelled
- AxAgentExecutionEngine에 PreparedExecution, PrepareExecution, NormalizeAssistantContent를 추가해 실행 준비와 최종 응답 보정 책임을 더 모음

- ChatWindow의 일반 전송과 재생성이 PrepareExecutionForConversation을 통해 같은 준비 경로를 쓰도록 정리함

- Cowork/Code 시스템 프롬프트 선택, 실행 모드 판정, 프롬프트 스택 조합, outbound message 준비의 중복 분기를 줄임

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
2026-04-05 12:23:09 +09:00
3ed454a98c AX Agent 채팅 UI 백업 및 claw-code 기준 엔진 재정렬
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow 현재 UI와 엔진 기준본을 etc/chat-ui-backup/2026-04-05-1215에 백업해 회귀 비교 지점을 확보함

- 메시지 컬럼과 컴포저 폭을 claw-code식 단일 축으로 다시 맞추고 입력 셸을 안정적인 하단 컬럼 구조로 정리함

- 입력창 높이를 실제 줄바꿈 수 기준으로 다시 계산해 전송 후 높이가 남는 버그를 줄임

- 메시지 편집/피드백 후 재생성 경로의 직접 UI 버블 주입을 제거하고 RenderMessages 중심으로 통합함

- SendRegenerateAsync가 Cowork/Code에서 ResolveExecutionMode와 RunAgentLoopAsync를 타도록 바꿔 재생성도 동일 엔진 축으로 정렬함

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
2026-04-05 12:19:20 +09:00
f82cfc4541 AX Agent 로컬 응답 경로를 모델 렌더 축으로 통합
Some checks failed
Release Gate / gate (push) Has been cancelled
claw-code 기준 채팅 엔진 정상화 작업을 이어서 진행했습니다.

수동 컨텍스트 압축 결과와 slash 로컬 응답 경로에서 직접 AddMessageBubble로 UI 버블을 꽂던 흐름을 제거하고 conversation/session에 먼저 커밋한 뒤 RenderMessages로만 다시 그리도록 정리했습니다.

이로써 일반 전송, 재생성, 로컬 응답이 서로 다른 렌더 경로를 타며 순서가 꼬이던 상태를 줄였고 Cowork/Code 전용 엔진 정리에 필요한 공통 렌더 축을 더 맞췄습니다.

검증은 dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 기준 경고 0개, 오류 0개입니다.
2026-04-05 12:10:31 +09:00
890c8ce76b AX Agent 채팅 엔진 최종 응답 커밋형으로 정상화
Some checks failed
Release Gate / gate (push) Has been cancelled
claw-code 기준으로 AX Agent 채팅 전송 흐름을 준비, 실행, 최종 assistant 커밋, 재렌더 순서로 다시 정리했습니다.

ChatWindow의 SendMessageAsync와 SendRegenerateAsync에서 임시 assistant 메시지와 임시 스트리밍 컨테이너를 먼저 만드는 경로를 제거하고, 실행이 끝난 뒤 최종 assistant 텍스트만 conversation/session에 커밋하도록 수정했습니다.

OnAgentEvent는 실행 로그 배너를 즉시 UI에 직접 꽂지 않고 conversation ExecutionEvents에 먼저 저장한 뒤 ShowExecutionHistory가 켜진 경우에만 RenderMessages 기반으로 다시 그리게 바꿔 Cowork/Code의 플래시 잔상과 중복 표시를 줄였습니다.

AxAgentExecutionEngine도 Chat 실행을 스트리밍 UI 의존이 없는 최종 응답 커밋형 모드로 정리했습니다. 검증은 dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 기준 경고 0개, 오류 0개입니다.
2026-04-05 12:07:47 +09:00
74 changed files with 45974 additions and 9649 deletions

480
README.md
View File

@@ -7,6 +7,106 @@ 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%`입니다.
- 메인 핵심 엔진 로직에 직접 영향을 주는 설정은 최소화 원칙으로 다시 검토하기 시작했습니다. 이미 실질 선택지가 사라진 `계획 모드` 계열은 사용자 노출을 더 줄였고, 남은 엔진성 설정은 개발자 탭 중심으로 계속 정리합니다.
- 메시지 행과 좌측 대화 목록도 `claw-code` 방향으로 다시 단순화했습니다. 사용자/assistant 버블의 패딩, 라운드, 메타 텍스트를 줄였고, 대화 목록의 실행 상태도 배지 카드보다 얇은 텍스트 요약 중심으로 바꿔 읽는 축이 먼저 보이도록 눌렀습니다.
- 검증 예정: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\`
- 업데이트: 2026-04-05 17:03 (KST)
- AX Agent UI를 `claw-code` 쪽 시각 언어에 더 가깝게 맞추기 위해 레이아웃 골격을 다시 조정했습니다. 좌측 패널 폭과 헤더/액션 행 높이, 축소 아이콘 바, 상태 스트립, 메시지 축, 컴포저 폭과 코너 반경을 전반적으로 더 얇고 평평하게 정리했습니다.
- 반응형 폭 계산도 새 골격에 맞춰 다시 조정했습니다. 메시지 축은 최대 `880`, 컴포저는 최대 `820` 기준으로 더 자연스럽게 줄어들도록 바꿔 창 크기가 변해도 `claw-code`처럼 중심선이 크게 흔들리지 않게 맞췄습니다.
- 검증 예정: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\`
- 업데이트: 2026-04-05 17:12 (KST)
- 메시지 내부 액션 바와 Cowork/Code 실행 배너를 더 `claw-code`처럼 보조 레이어로 낮췄습니다. 메시지 액션은 텍스트 버튼 대신 작은 아이콘 버튼 중심으로 바꾸고, 실행 배너는 여백·폰트·토큰 배지·파일 경로 표시를 한 단계 더 얇게 줄여 본문보다 덜 튀게 정리했습니다.
- 검증 예정: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\`
- 업데이트: 2026-04-05 17:19 (KST)
- 상단 헤더와 탭 그룹, 좌측 대화 목록 행 메타를 더 `claw-code` 쪽 밀도로 조정했습니다. 상단 탭은 더 얇은 세그먼트형으로 줄였고, 사이드바 토글 버튼도 크기와 선 두께를 낮췄습니다.
- 대화 목록은 실행 상태/요약 텍스트와 우측 편집 아이콘을 더 약하게 줄여 제목 중심으로 읽히게 정리했습니다.
- 검증 예정: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\`
- 업데이트: 2026-04-05 16:02 (KST)
- `document_plan` 후속 실행 분기를 `claw-code` 기준으로 다시 보강했습니다. 이제 문서 플래너 출력에서 body 골격과 즉시 실행 지시를 깨진 문자열 비교에 의존하지 않고 안정적으로 추출해, `html_create / document_assemble / docx_create / markdown_create` 후속 호출 유도가 실제로 이어집니다.
- 코워크 문서형 작업은 설정이 `planMode=off`여도 내부적으로 `always` 플랜 경로를 타도록 보정했습니다. 그래서 문서/보고서/제안서 요청은 먼저 계획을 세우고, 그 계획을 바탕으로 실제 문서 생성 단계까지 이어가도록 정리했습니다.
- 코워크 시스템 프롬프트도 강화해 문서 작업은 계획만 제시하고 끝내지 말고 실제 산출물 파일 경로까지 만들어야 완료로 판단하도록 바꿨습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 15:42 (KST)
- AX Agent 엔진 공통화 1차로, Cowork/Code 실행 이벤트와 Agent run 기록을 탭별 현재 대화에 누적한 뒤 원래 활성 탭 대화를 복원하는 로직을 `ChatWindow`에서 `AxAgentExecutionEngine` helper로 옮겼습니다.
- 이제 실행 이벤트/최근 run 기록 반영 시 창 코드가 직접 교차 탭 복원 경로를 중복 처리하지 않고, 엔진의 공통 세션 mutation 경로를 사용합니다.
- 업데이트: 2026-04-05 15:34 (KST)
- AX Agent 개선 계획 기준을 이전 AX 비교본이 아니라 실제 `claw-code` 런타임 축으로 다시 고정했습니다. 현재 참조 spine은 `bootstrap/state.ts -> bridge/initReplBridge.ts -> bridge/sessionRunner.ts -> screens/REPL.tsx -> components/Messages.tsx -> components/StatusLine.tsx` 입니다.
- 이에 맞춰 AX Agent 개선도 `상태 정규화 -> 실행 준비 공통화 -> AgentLoop 이벤트 정규화 -> 타임라인 렌더 일원화 -> 컴포저/상태바 단순화 -> 복구/재개 검증` 순서로 진행하도록 parity 문서를 갱신했습니다.
- 업데이트: 2026-04-05 11:22 (KST)
- AX Agent 채팅 복구 1차로 컴포저를 하단 고정 배치로 조정해 세로 공간을 꽉 채우며 커 보이던 문제를 줄였습니다.
- 전송 직후 사용자 버블을 직접 UI에 꽂지 않고 대화 모델 기반 `RenderMessages()` 재렌더를 먼저 타도록 정리해, 중복 렌더와 빈 버블 누적 가능성을 낮췄습니다.
@@ -726,6 +826,236 @@ ow + toggle 시각 언어로 통일했습니다.
- 런처 보조 기능/설정 연결을 `Agent Compare` 기준으로 다시 대조하면서, 입력을 비웠을 때 이전 선택 항목과 미리보기 패널이 남아 있던 상태를 정리했습니다. [LauncherViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/LauncherViewModel.cs) 에서 빈 입력 시 `SelectedItem`과 미리보기 바인딩이 같이 초기화되도록 맞춰, 빠른 실행 칩/검색 히스토리/미리보기 패널 전환이 더 이상 이전 검색 상태를 끌고 가지 않게 했습니다.
- 검증: `Agent Compare`의 런처 설정 항목(`ShowNumberBadges`, `CloseOnFocusLost`, `RememberPosition`, `EnableActionMode`, `EnableRandomPlaceholder`, `ShowLauncherBorder` 등)과 현재 [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml), [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [LauncherWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/LauncherWindow.xaml.cs), [LauncherViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/LauncherViewModel.cs) 연결을 재검토했고, 런처 테마 동일화 작업은 제외한 상태에서 보조 기능/설정 연결 위주로 1차 마무리했습니다.
- 업데이트: 2026-04-05 11:56 (KST)
- AX Agent 채팅 엔진 정상화 1차로, [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) 의 전송 흐름을 `준비 → 실행 → 최종 assistant 커밋 → 재렌더` 중심으로 다시 정리했습니다. Chat/Cowork/Code 공통으로 임시 assistant 카드와 임시 스트리밍 컨테이너를 먼저 만들지 않도록 바꿔, 토큰은 올라가는데 채팅 본문이 비거나 빈 버블이 남는 증상을 줄였습니다.
- 같은 수정에서 에이전트 실행 로그도 화면에 즉시 배너를 직접 꽂지 않고, 대화 모델의 `ExecutionEvents`에 먼저 쌓은 뒤 `RenderMessages()` 기준으로만 다시 그리게 바꿨습니다. 그래서 Cowork/Code에서 실행 로그 문구가 플래시처럼 잔상으로 남거나 중복 표시되던 흐름을 줄이는 쪽으로 정리했습니다.
- 재생성 경로도 동일하게 정리해서, 피드백 후 재생성 시 빈 assistant 메시지를 먼저 추가하지 않고 최종 응답만 커밋하도록 맞췄습니다.
- 이어서 `/slash` 로컬 응답과 수동 컨텍스트 압축 결과 경로도 conversation/session에 먼저 커밋한 뒤 `RenderMessages()`로만 다시 그리게 맞췄습니다. 이제 로컬 응답 경로도 직접 `AddMessageBubble(...)`를 꽂지 않아서, Chat/Cowork/Code에서 같은 종류의 중복 버블/순서 어긋남이 덜 발생하도록 정리했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 12:06 (KST)
- 업데이트: 2026-04-05 12:09 (KST)
- AX Agent 채팅 UI는 `claw-code` 기준으로 다시 정리하기 전에 현재 상태를 `etc/chat-ui-backup/2026-04-05-1215/`에 백업했습니다. 이 백업에는 [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), [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs) 기준본이 포함되어 있습니다.
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서는 메시지 컬럼과 빈 상태 폭을 `920px` 축으로 맞추고, 컴포저를 `760px` 기준으로 넓히면서 입력 셸을 하나의 안정적인 하단 컬럼으로 다시 정리했습니다. 같은 수정에서 컴포저 안의 `대화 내보내기` 버튼은 숨겨 `claw-code`처럼 입력과 전송에 더 집중된 구조로 단순화했습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 입력창 높이 계산은 이제 실제 줄바꿈 수만 기준으로 `Height`를 직접 다시 잡습니다. 전송 후에도 남아 있던 과도한 높이를 줄이고, `Shift+Enter`로 개행이 생길 때만 높이가 커지도록 더 강하게 고정했습니다.
- 같은 파일에서 `메시지 편집 후 재생성`, `피드백 후 재생성` 경로도 직접 `AddMessageBubble(...)`를 꽂지 않고 `RenderMessages()` 축으로 다시 돌리게 맞췄습니다. 재생성 경로 자체도 `Cowork/Code`에서는 일반 LLM 호출이 아니라 `ResolveExecutionMode(...)` + `RunAgentLoopAsync(...)`를 타도록 바꿔, 코워크/코드가 채팅 재생성 때 일반 Chat 경로로 잘못 떨어지던 문제를 줄였습니다.
- 이어서 [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs)에 `PrepareExecution(...)`, `NormalizeAssistantContent(...)`를 추가하고, [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 일반 전송과 재생성이 모두 같은 준비 함수를 타도록 정리했습니다. 이제 실행 모드 판정, 프롬프트 스택 구성, 전송 메시지 조립, 최종 assistant 내용 보정이 한 엔진 축에서 처리됩니다.
- 이 변경으로 `SendMessageAsync()``SendRegenerateAsync()`가 각자 따로 Cowork/Code 시스템 프롬프트와 실행 모드를 계산하던 중복 분기가 줄었고, 이후 Cowork/Code 엔진을 `claw-code` 기준으로 더 밀 때도 준비 로직은 엔진 한 곳만 고치면 되게 정리했습니다.
- 이어서 `FinalizeAssistantTurn(...)`를 엔진에 추가해, 최종 assistant 내용 정규화와 Cowork/Code 실행 로그 접힘 처리, assistant 메시지 커밋을 전송/재생성 공통으로 같은 메서드에서 처리하게 바꿨습니다. 이제 채팅 마무리 단계도 UI 코드가 아니라 엔진이 더 많이 책임집니다.
- 이번엔 [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs)에 `ExecutePreparedAsync(...)`를 추가해서, 준비된 실행이 `AgentLoop`를 탈지 일반 LLM 호출을 탈지 결정하는 분기까지 엔진이 맡도록 옮겼습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 일반 전송과 재생성은 이제 둘 다 `ExecutePreparedAsync(...)`만 호출합니다.
- 이어서 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 실행 후처리도 `ResetStreamingUiState()`, `FinalizeConversationTurn()`, `FinalizeQueuedDraft()`로 묶었습니다. 전송과 재생성이 같은 정리 경로를 공유하게 해서, 응답 완료 뒤 상태 복구와 대화 저장, 대기열 완료/실패 처리 흐름도 더 한 축으로 정리했습니다.
- 이번엔 `OnAgentEvent(...)`의 본문 재렌더를 배치형으로 바꿨습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 `DispatcherTimer` 기반 `ScheduleExecutionHistoryRender()`를 추가해서, Cowork/Code 실행 중 이벤트가 연속으로 들어와도 `RenderMessages()`가 매 이벤트마다 바로 돌지 않고 짧게 묶여 한 번씩만 반영됩니다.
- 같은 흐름으로 작업 요약 스트립도 배치형 갱신으로 바꿨습니다. `UpdateTaskSummaryIndicators()`를 즉시 호출하는 대신 `ScheduleTaskSummaryRefresh()`가 120ms 단위로 상태 반영을 묶어, 실행 중 상단 상태 스트립과 런타임 배지가 과하게 흔들리지 않도록 정리했습니다.
- 추가로 실행 이벤트/실행 기록 저장도 지연 저장으로 바꿨습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AppendConversationExecutionEvent()``AppendConversationAgentRun()`은 이제 이벤트마다 바로 `_storage.Save(...)`를 호출하지 않고, `ScheduleConversationPersist()`를 통해 220ms 단위로 묶어서 flush 합니다. Cowork/Code의 연속 이벤트 구간에서 저장 I/O가 덜 붙도록 만든 조정입니다.
- 이번엔 실행 완료 뒤 메시지 축을 흔들던 보조 UI를 더 줄였습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `RenderSuggestActionChips()`는 더 이상 본문 `MessagePanel`에 제안 칩을 직접 삽입하지 않고, 요약 토스트만 띄우도록 바꿨습니다. 이 변경으로 Cowork/Code 작업 중간에 제안 칩이 본문 폭과 스크롤 위치를 흔들던 경로를 끊었습니다.
- 같은 파일의 대기열 UI도 기본 축약형으로 바꿨습니다. `DraftQueuePanel`은 이제 기본적으로 요약 pill + 핵심 항목 1개만 보이고, 필요할 때만 `상세 보기`로 전체 섹션 카드(`실행 중/다음 작업/보류/완료/실패`)를 펼칩니다. 대기열 카드가 매번 크게 다시 그려지면서 컴포저 위 레이아웃을 밀던 현상을 줄이기 위한 정리입니다.
- 이어서 Cowork/Code 완료 직후 저장 축도 정리했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `ResetStreamingUiState()`는 이제 배치 저장 대기 중인 실행 이벤트/실행 기록을 먼저 `FlushPendingConversationPersists()`로 확정 저장한 뒤 타이머를 내립니다. 이걸로 실행 종료 직전 들어온 마지막 이벤트가 지연 저장 타이머만 멈춘 채 사라질 수 있는 경로를 막았습니다.
- 같은 수정에서 `PersistConversationSnapshot(...)`를 추가해 중간 저장, 최종 저장, 지연 저장 flush를 한 경로로 묶었고, `RunAgentLoopAsync(...)` 안의 중복 `_storage.Save(...)` / `RememberConversation(...)`는 제거했습니다. 이제 Cowork/Code 완료 시점 저장은 `FinalizeConversationTurn(...)` 쪽의 단일 완료 경로가 맡습니다.
- 이번엔 실행 이벤트가 들어올 때 창 코드가 즉시 많이 만지던 UI 갱신도 배치형으로 묶었습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 `_agentUiEventTimer`, `ScheduleAgentUiEvent(...)`, `FlushPendingAgentUiEvent()`를 추가해서, 상태바/스티키 진행률/플랜 뷰어/파일 탐색기 자동 새로고침/제안 토스트/자동 프리뷰 반영이 가장 최근 이벤트 기준으로 90ms 단위로만 화면에 반영되게 했습니다.
- `OnAgentEvent(...)`는 이제 실행 이벤트 자체를 대화 모델과 앱 상태에 먼저 반영하고, 화면 갱신은 배치된 UI 이벤트 flush가 담당합니다. 이 조정으로 Cowork/Code 실행 중 빠른 이벤트 연속 구간에서 상태바와 진행률, 파일 미리보기 쪽이 따로따로 즉시 흔들리던 체감을 더 줄이는 방향으로 정리했습니다.
- 대기열 다음 작업 시작도 입력창 UI에 의존하지 않게 바꿨습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `SendMessageAsync(...)`는 이제 선택적으로 직접 텍스트를 받을 수 있고, `StartNextQueuedDraftIfAny(...)`는 더 이상 `InputBox.Text`를 바꿔 포커스를 흔든 뒤 전송하지 않고 `SendMessageAsync(next.Text)`로 바로 실행합니다. 이걸로 Cowork/Code 자동 이어달리기가 입력창 상태를 덜 건드리게 됐습니다.
- 실패 후 재시도도 같은 방향으로 정리했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `RetryLastUserMessageFromConversation()`는 이제 입력창에 마지막 요청을 다시 밀어 넣지 않고, 유휴 상태면 `SendMessageAsync(lastUserMessage)`로 바로 다시 실행하고, 이미 작업 중이면 같은 요청을 곧바로 대기열에 적재합니다. 재시도 동작도 입력창 포커스와 높이를 흔들지 않게 만든 조정입니다.
- 이어서 구형 본문 재시도 카드도 제거했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AddRetryButton()` 경로를 걷어내고, 실패 시에는 본문에 임시 재시도 카드를 꽂지 않고 짧은 토스트로만 안내한 뒤 작업 요약/실패 이력 쪽 재시도 액션을 사용하도록 정리했습니다. 본문을 메시지와 상태 중심으로 유지하는 `claw-code` 방향에 더 가깝게 맞춘 것입니다.
- UI도 `claw-code` 기준으로 1차 정리를 넣었습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 메시지 컬럼 폭을 `880`으로 더 정리하고, 상단 진행률 바 패딩과 폭을 줄였으며, 빈 상태는 떠다니는 그라디언트 아이콘 대신 더 작고 정적인 카드형 아이콘으로 단순화했습니다. 컴포저도 `800px` 축으로 넓히면서 라운드와 그림자를 조금 눌러, 화면 장식보다는 메시지/입력 흐름이 먼저 보이게 다듬은 단계입니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 12:24 (KST)
- 업데이트: 2026-04-05 12:31 (KST)
- 업데이트: 2026-04-05 12:36 (KST)
- 업데이트: 2026-04-05 12:41 (KST)
- 업데이트: 2026-04-05 12:47 (KST)
- 업데이트: 2026-04-05 12:53 (KST)
- 업데이트: 2026-04-05 12:58 (KST)
- 업데이트: 2026-04-05 13:03 (KST)
- 업데이트: 2026-04-05 13:12 (KST)
- 업데이트: 2026-04-05 13:20 (KST)
- 업데이트: 2026-04-05 13:29 (KST)
- 업데이트: 2026-04-05 13:37 (KST)
- 업데이트: 2026-04-05 13:44 (KST)
- 업데이트: 2026-04-05 13:52 (KST)
- 업데이트: 2026-04-05 14:00 (KST)
- 메시지 행 UI도 `claw-code` 기준으로 한 단계 더 눌렀습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 `MessagePanel` 하단 여백을 더 줄여 본문 축이 컴포저와 가깝게 이어지도록 했고, [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서 사용자/assistant 메시지 카드의 좌우 마진, 코너 라운드, 패딩, 폰트 크기, 타임스탬프 크기를 전반적으로 낮췄습니다.
- assistant 헤더는 아이콘과 이름을 더 작고 옅게 줄였고, 액션 바 버튼도 패딩과 간격을 축소해 메시지 본문보다 덜 튀게 만들었습니다. 같은 방향으로 실행 로그 배너(`AddAgentEventBanner`)도 좌우 마진, 아이콘/라벨 크기, 토큰 배지와 요약 텍스트 밀도를 낮춰, Cowork/Code에서 로그가 메시지보다 먼저 보이던 느낌을 줄였습니다.
- 폭 계산도 `claw-code`처럼 반응형으로 다시 맞췄습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml)의 `ComposerShell` 고정폭을 걷어내고, [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `UpdateResponsiveChatLayout()`가 실제 본문 폭 기준으로 `MessagePanel`, `EmptyState`, `ComposerShell` 폭을 함께 다시 계산하도록 연결했습니다.
- 이제 창이 작아질 때 메시지 축과 입력창이 따로 놀지 않고 같은 축으로 같이 줄어들며, 창이 넓을 때는 적당한 상한을 유지한 채 자연스럽게 넓어집니다. 초기 로드와 `SizeChanged` 모두 같은 반응형 계산을 타도록 붙였습니다.
- 이어서 컴포저 상단 구조도 `claw-code` 방향으로 더 눌렀습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 `InputBorder`, `DraftPreviewCard`, `DraftQueuePanel` 간격과 그림자를 줄였고, `BtnModelSelector`, `TokenUsageCard`, `BtnTemplateSelector`의 높이, 패딩, 아이콘/폰트 크기를 함께 낮춰 입력축보다 옵션 카드가 먼저 튀지 않게 정리했습니다.
- 토큰 카드도 원형 게이지와 텍스트, `압축` 버튼을 전반적으로 소형화해 상단 바가 두꺼운 툴 패널처럼 보이던 인상을 줄였습니다. 결과적으로 입력부는 더 얇은 하단 작업 바처럼 보이고, 메시지 본문 축과 시각적 우선순위가 덜 충돌하게 됐습니다.
- Cowork/Code 상태 UI도 더 얇게 조정했습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 `ConversationStatusStrip`, `ConversationQuickStrip`, `AgentProgressBar`, `RuntimeActivityBadge`, `ExecutionLog`, `SubAgentIndicator`, `StatusElapsed`, `StatusTokens`의 패딩과 폰트, 간격을 전반적으로 줄여 상태 바가 본문 위를 과하게 차지하지 않게 정리했습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 작업 요약 팝업도 제목/설명/최근 실행 카드 밀도를 낮추고 최근 실행 목록을 2개만 보여 주도록 줄였습니다. 이제 상태 UI는 더 보조적인 레이어로 남고, 메시지 본문이 먼저 읽히는 쪽으로 가까워졌습니다.
- 이어서 작업 요약 내부 카드도 더 가볍게 줄였습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `CreateTaskSummaryActionButton(...)`을 더 작은 버튼 규격으로 낮추고, 권한/훅/백그라운드 카드의 패딩과 마진도 한 단계 축소했습니다.
- 최근 권한 이력은 2개, 최근 훅은 3개, 최근 백그라운드 작업은 2개까지만 보여 주도록 줄여, 작업 요약 팝업이 긴 상태 대시보드처럼 커지지 않게 정리했습니다.
- 같은 축으로 Cowork/Code 보조 상태 레이어를 한 번 더 눌렀습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 `ConversationStatusStrip`, `ConversationQuickStrip`, `AgentProgressBar`, `RuntimeActivityBadge`, `LastCompletedLabel`, `ExecutionLog`, `SubAgentIndicator`, `StatusElapsed`, `StatusTokens`는 패딩·폰트·간격을 추가로 줄여 상시 노출돼도 본문보다 덜 튀도록 정리했습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `ShowTaskSummaryPopup()`, `CreateTaskSummaryActionButton(...)`, `BuildHookSummaryCard(...)`, `BuildActiveBackgroundSummaryCard(...)`, `BuildRecentBackgroundJobCard(...)` 도 같은 시각 언어로 다시 줄였습니다. 팝업 헤더/필터/최근 실행 카드/백그라운드 카드/훅 카드의 라운드, 패딩, 마진, 텍스트 크기를 전반적으로 낮춰 작업 요약이 진단용 보조 패널에 더 가깝게 보이게 했습니다.
- 업데이트: 2026-04-05 17:27 (KST)
- 메시지 자체 메타와 완료 카드 문구도 더 `claw-code` 쪽으로 눌렀습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AddMessageBubble(...)` 에서 사용자/assistant 버블 패딩, 코너, 폰트, 타임스탬프, assistant 헤더 아이콘/이름 크기를 한 단계 더 낮춰 본문 텍스트가 더 먼저 읽히도록 조정했습니다.
- 작업 요약 팝업의 완료 카드도 `실행 run`, `최근 실패`, `최근 실행`, `로그`, `파일`, `후속 큐`, `다시 시도`, `타임라인`처럼 더 짧은 문구로 정리했고, run/step 메타와 요약 텍스트 폰트도 함께 낮춰 정보 밀도를 더 가볍게 맞췄습니다.
- 업데이트: 2026-04-05 17:33 (KST)
- Cowork/Code 실행 타임라인 배너도 더 `claw-code`처럼 얇게 줄였습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AddAgentEventBanner(...)` 에서 일반 실행 배너의 좌우 마진, 아이콘/라벨, 경과 시간, 토큰 pill, 요약 텍스트, 파일 경로 행을 한 단계 더 축소했고, 상세 review 칩은 `debug` 로그일 때만 보이게 제한했습니다.
- 이 조정으로 평소 Cowork/Code에서는 실행 이벤트가 더 짧은 한 줄 요약 중심으로 보이고, debug 정보는 필요할 때만 확장되도록 정리됐습니다.
- 업데이트: 2026-04-05 17:39 (KST)
- 상단 헤더도 더 `claw-code` 쪽 밀도로 줄였습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 상단 탭 버튼의 폰트/패딩/코너를 다시 낮추고, 탭 그룹 래퍼와 제목 서브 바의 높이와 패딩도 함께 줄였습니다.
- 같은 변경에서 대화 제목 폰트와 최대 폭, 빠른 스트립 버튼 규격, 프리뷰 토글 크기와 라벨도 더 작게 조정해 상단 보조 정보가 본문보다 덜 튀게 정리했습니다.
- 업데이트: 2026-04-05 17:45 (KST)
- 좌측 사이드바도 한 번에 더 `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) 에서는 실제 사이드바 폭을 `270 -> 248`로 줄이고, 대화 목록 카드의 패딩, 코너, 아이콘 열 폭, 제목/날짜/실행 메타 폰트, 편집 버튼 규격, 선택 액센트 바 두께도 함께 축소해 목록이 더 차분하게 보이도록 맞췄습니다.
- 업데이트: 2026-04-05 17:53 (KST)
- 큰 카드형 요소도 더 `claw-code` 쪽으로 눌렀습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AddPlanningCard(...)` 에서 계획 카드 라운드, 패딩, 헤더 아이콘/텍스트, 진행률 텍스트, 단계 행 폰트를 전반적으로 줄였고, 계획 헤더 문구도 더 짧게 정리했습니다.
- 같은 변경에서 `CreateCompactEventPill(...)`, `CreateTimelineLoadMoreCard(...)`도 함께 축소해 컨텍스트 압축 pill과 “이전 대화 더 보기” 카드가 본문보다 과하게 두껍게 보이지 않도록 맞췄습니다.
- 업데이트: 2026-04-05 18:01 (KST)
- 엔진 마감도 한 단계 더 진행했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 `ExecutePreparedTurnAsync(...)`를 추가해 `send``regenerate`가 같은 실행/예외/취소/최종 커밋/후처리 경로를 타도록 묶었습니다. 이제 전송과 재생성은 같은 prepared-execution 축에서 닫히고, 실패 토스트와 최종 assistant 커밋도 같은 helper가 담당합니다.
- 같은 변경에서 계획 이벤트는 기본적으로 큰 카드가 아니라 얇은 요약 pill로만 보이고, `debug` 로그일 때만 `AddPlanningCard(...)`가 펼쳐지도록 바꿨습니다. 문서형 Cowork/Code 작업에서도 기본 노출이 더 `claw-code`처럼 차분한 상태가 됐습니다.
- 업데이트: 2026-04-05 18:08 (KST)
- 좌측 패널과 하단 바도 `claw-code` 쪽 밀도로 다시 맞췄습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 사이드바 폭을 줄이고, 헤더 앱 배지를 강조색 채운 정사각형 대신 `HintBackground + BorderColor` 기반의 작은 배지형으로 바꿨습니다.
- `새 대화`, `검색`, `작업 유형/워크스페이스`, 하단 사용자 영역, 삭제 영역까지 패딩과 폰트, 아이콘 크기를 함께 낮췄고, 하단 상태바는 다이아몬드 아이콘을 작은 원형 점으로 바꿔 더 단순한 상태선처럼 보이게 정리했습니다.
- 실행 로그 배너도 본문 침범을 더 줄였습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AddAgentEventBanner(...)` 에서 debug 전용 `ToolInput` 카드 길이를 더 짧게 줄였고, `FilePath`는 일반 로그에서는 빠른 액션이 붙은 카드형 대신 파일명 한 줄만 약하게 표시하도록 바꿨습니다.
- 이제 파일 경로 카드와 빠른 액션은 `debug`일 때만 크게 보이고, 일반 Cowork/Code 로그에서는 파일명만 보조 정보처럼 붙습니다. 덕분에 실행 로그가 본문 아래에서 더 얇게 흐르도록 정리됐습니다.
- 대화 목록 행 카드와 축소 아이콘 바도 같은 시각 언어로 더 정리했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AddConversationItem(...)`에서 선택 강조 배경과 좌측 액센트 바 두께를 더 얇게 줄이고, 행 패딩/아이콘/폰트/배지 크기를 전반적으로 낮춰 `claw-code`처럼 목록 자체가 먼저 튀지 않도록 정리했습니다.
- `진행 중`, `성공`, `실패` 배지와 실행 요약 텍스트도 더 작고 중립적인 톤으로 줄였고, 호버 시 확대 애니메이션은 제거해 목록이 더 차분하게 반응하도록 맞췄습니다. 편집 버튼도 더 작은 규격과 낮은 opacity를 써서 필요할 때만 보조 액션으로 보이게 조정했습니다.
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml)의 축소 아이콘 바는 상하 행 높이, 버튼 패딩, 아이콘 크기, 사용자 배지 크기를 한 단계 더 줄여 현재 사이드바와 같은 밀도로 묶었습니다. 이제 축소 상태에서도 검색/필터/새 대화 아이콘이 더 균일한 간격으로 정리되고, 하단 사용자 배지도 과하게 튀지 않는 중립형으로 유지됩니다.
- AX Agent 내부 설정 탭도 다시 정리했습니다. [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) 에서 사라졌던 `테마 스타일`, `테마 모드``공통` 탭에 실제 선택 카드로 복구했고, 기존 `스킬/차단` 탭은 `도구 / 스킬 / 차단`으로 나눠 각 항목이 맞는 탭에서만 보이게 재배치했습니다.
- 이제 `도구` 탭에서는 훅과 도구/커넥터 목록을, `스킬` 탭에서는 스킬 폴더, 슬래시 설정, 드래그 앤 드롭, 로드된 스킬, 폴백 모델, MCP 서버를, `차단` 탭에서는 차단 경로/확장자만 관리합니다. 같이 [SettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml.cs) 에서 메인 설정의 `AX Agent` 바로가기 탭을 좌측 사이드바 맨 아래로 재배치했습니다.
- 런처 하단 바도 요소별로 제어할 수 있게 바꿨습니다. [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs), [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml) 에 `성능 / 포모도로 / 메모 / 날씨 / 일정 / 배터리` 하단 위젯 표시 토글을 추가해서 일반 설정에서 항목별로 바로 켜고 끌 수 있게 했습니다.
- [LauncherWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/LauncherWindow.xaml), [LauncherWindow.Widgets.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/LauncherWindow.Widgets.cs) 에서는 `Ollama / API / MCP` 서버 상태 위젯을 런처 하단 기능에서 완전히 제거했고, 남은 위젯들만 설정값에 따라 실제 표시되도록 연결했습니다. 배터리 위젯도 노트북 상태와 사용자 토글을 함께 반영해 보이게 정리했습니다.
- `claw-code` 기준으로 계획 UX도 다시 눌렀습니다. [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs) 에서 저장된 `PlanMode` 값과 무관하게 런타임 계획 모드를 `off`로 고정해, 코워크/코드에서 매번 계획 승인 팝업이 뜨지 않도록 바꿨습니다.
- [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml), [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서는 메인 설정과 AX Agent 내부 설정의 `계획 모드` 행을 숨겼고, [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs) 에서도 항상 `off`만 저장/반영되게 정리했습니다.
- 계획 확인 팝업은 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs), [PlanViewerWindow.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/PlanViewerWindow.cs) 기준으로 AX Agent 창을 owner로 받아 리소스를 그대로 합치게 바꿨고, 채팅 본문에 별도 인라인 승인 버튼을 다시 꽂지 않도록 정리했습니다.
- 업데이트: 2026-04-05 16:20 (KST)
- `claw-code` 기준 UI/엔진 재구성 1차도 반영했습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 AX Agent 메인 레이아웃의 사이드바 폭, 메시지 축, 빈 상태 카드, 컴포저 외곽선을 더 압축해 메시지 중심 구조로 다시 정리했고, 상단/보조 스트립과 토큰 카드 노출도 더 보수적으로 줄였습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서는 반응형 폭 계산을 다시 조정해 창이 좁아질 때 메시지 축과 컴포저가 같은 중심선을 따라 자연스럽게 줄어들게 했고, Chat 탭에서는 보조 상태 스트립을 거의 숨기고 Cowork/Code도 실패/승인 대기 같은 핵심 상태만 남기도록 정리했습니다.
- 엔진 쪽은 [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs) 에 `FinalizeExecutionContent(...)` 를 추가해 취소/오류/빈 응답 정규화를 UI 바깥으로 넘겼고, 전송/재생성 마감 흐름이 같은 helper를 타도록 맞췄습니다. 내부 설정 오버레이 연결은 유지했습니다.
- 업데이트: 2026-04-05 16:33 (KST)
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 15:16 (KST)
- AX Agent 엔진 마감 2차로 [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs)에 UI용 마감 helper `FinalizeExecutionContentForUi(...)`, `NormalizeAssistantContentForUi(...)`를 추가했습니다. 취소/오류/빈 응답, Cowork/Code 완료 문구를 깨진 문자열이 아닌 정상 한국어 기준으로 정규화하도록 분리했습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서는 `RegenerateLastAsync()``RetryWithFeedbackAsync(...)`가 더 이상 `MessagePanel.Children.RemoveAt(...)`로 마지막 assistant 버블을 직접 지우지 않고, 대화 상태를 먼저 수정한 뒤 `RenderMessages()`와 자동 스크롤로 다시 그리게 바꿨습니다. 재생성/피드백 재시도 흐름이 세션 상태 기준으로 더 일관되게 닫힙니다.
- 같은 변경에서 `Paused/Resumed` 실행 이벤트는 `debug`가 아닐 때 본문 타임라인에 기본 노출되지 않게 줄여 Cowork/Code 실행 중 시각적 노이즈를 더 낮췄습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 18:20 (KST)
- AX Agent 메인 UI도 `claw-code` 기준으로 한 번 더 크게 재배치했습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 창 기본 크기, 사이드바 폭, 상단 헤더, 탭 그룹, 본문 스크롤 축, 빈 상태, 컴포저 외곽선과 입력부를 평평한 transcript 중심 구조로 다시 정리했고, 장식성 그림자와 두꺼운 카드 느낌을 더 많이 걷어냈습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서는 메시지 버블, assistant 헤더, 실행 요약 pill, 이전 대화 로드 카드, 계획 카드의 라운드/패딩/메타 밀도를 전반적으로 줄이고, 반응형 폭 계산도 `message 960 / composer 900` 축으로 다시 맞춰 창이 줄어들 때 `claw-code`처럼 더 자연스럽게 따라가도록 조정했습니다.
- 현재 `claw-code` 대비 추정 진척율은 핵심 엔진 `88%`, 채팅 메인 UI `94%`, Cowork/Code 상태 UX `89%`, 내부 설정 연결 `88%`, 전체 AX Agent `92%` 정도입니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 18:30 (KST)
- Cowork/Code 보조 상태 레이어도 다시 최소 노출 기준으로 정리했습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 `ConversationStatusStrip`, `ConversationQuickStrip`, `AgentProgressBar`, `RuntimeActivityBadge`, `ExecutionLog`, `SubAgentIndicator`, `StatusElapsed`, `StatusTokens`의 패딩·폰트·간격을 한 단계 더 줄여 상단/하단 보조 정보가 transcript보다 먼저 튀지 않도록 조정했습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `ShowTaskSummaryPopup()`은 필터 칩과 과한 대시보드형 액션을 걷어내고, 최근 실행도 1건 중심의 요약 카드만 남기도록 줄였습니다. 활성/최근 작업 카드는 각각 `3/2`개만 노출하도록 낮췄고, 배경색도 팝업 테마와 같은 축을 쓰게 맞췄습니다.
- 같은 변경에서 `BuildHookSummaryCard(...)`, `BuildActiveBackgroundSummaryCard(...)`, `BuildRecentBackgroundJobCard(...)`, `AddTaskSummaryObservabilitySections(...)`를 더 보수적으로 정리해 훅/백그라운드/권한 이력이 기본 팝업을 점유하지 않게 했습니다. 백그라운드 작업은 현재 활성 상태만 짧게 요약하고, 세부 이동/필터 버튼은 대부분 제거해 `claw-code`처럼 “필요할 때만 보이는 진단 패널”에 가깝게 맞췄습니다.
- 현재 `claw-code` 대비 추정 진척율은 핵심 엔진 `88%`, 채팅 메인 UI `95%`, Cowork/Code 상태 UX `91%`, 내부 설정 연결 `88%`, 전체 AX Agent `93%` 정도입니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 18:40 (KST)
- 실행 타임라인과 계획 카드도 기본 노출을 더 줄였습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AddAgentEventBanner(...)``debug`가 아닐 때 `ToolCall` 중간 이벤트를 숨기고, 일반 요약 문구도 더 짧은 길이로 잘라 본문보다 덜 튀게 조정했습니다.
- `AddPlanningCard(...)`는 계획 카드 라운드, 패딩, 헤더 텍스트, 단계 폰트와 최대 폭을 더 줄여 transcript 안의 보조 계획 메모처럼 보이게 바꿨고, `BuildTaskSummaryCard(...)``CreateTaskSummaryActionButton(...)` 도 카드/버튼 크기를 한 단계 더 낮췄습니다.
- 같은 정리에서 권한 작업 카드 액션은 `계획 모드` 버튼을 제거해 현재 엔진 정책과 UI가 다시 어긋나지 않게 맞췄습니다.
- 현재 `claw-code` 대비 추정 진척율은 핵심 엔진 `89%`, 채팅 메인 UI `96%`, Cowork/Code 상태 UX `92%`, 내부 설정 연결 `88%`, 전체 AX Agent `94%` 정도입니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 18:49 (KST)
- 상단 탭과 하단 컴포저 일부는 사용자 피드백 기준으로 다시 복구했습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 상단 `Chat / Cowork / Code` 탭은 너무 얇아졌던 pill 스타일을 되돌려 폰트와 패딩을 키우고, 래퍼 패딩도 약간 넓혀 예전처럼 더 또렷하게 보이도록 조정했습니다.
- 같은 파일에서 하단 컴포저의 `토큰 사용 카드``프리셋` 버튼이 같은 컬럼을 같이 써서 겹치던 문제를 수정했습니다. 모델/토큰/프리셋을 각각 독립 컬럼으로 분리했고, 관련 버튼과 레이블의 폰트/패딩도 함께 키워 하단 정보가 눌려 보이지 않게 다시 정리했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 18:55 (KST)
- 작업 유형 카드 UX도 다시 다듬었습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `BuildTopicButtons()` 에서 카드 하단의 상시 설명 텍스트를 제거하고, 모든 카드 크기를 같은 규격으로 통일했습니다.
- 각 카드 설명은 이제 hover 시 카드 하단의 작은 라벨로만 보이게 바꿨고, 기존 확대 애니메이션은 제거해 배경/테두리만 반응하는 안정적인 hover로 정리했습니다. `기타`, `프리셋 추가` 카드도 같은 규칙으로 맞췄습니다.
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서는 빈 상태 제목/설명 폰트도 함께 키워 이 화면 전반의 글자 크기가 너무 작아 보이지 않게 보정했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 19:02 (KST)
- AX Agent 내부 설정 공통 탭의 서비스 전환 UX를 다시 바로잡았습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 서비스 상세 영역을 `주소 입력``API 키` 패널로 분리해 이름을 부여했고, `테마 스타일``테마 모드`도 서비스/모델 바로 아래에서 보이도록 공통 설정 상단으로 옮겼습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서는 `RefreshOverlayServiceFieldVisibility(...)` 를 추가해 `Gemini/Claude` 선택 시 주소 입력 패널을 접고 API 키만 전체 폭으로 보이게 만들었습니다. `Ollama/vLLM` 은 기존처럼 주소와 키를 함께 보여줍니다.
- 같은 파일의 `SetOverlayService(...)` 는 저장 직후 `RefreshOverlayVisualState(true)` 를 다시 호출하도록 바꿔, 현재 서비스/현재 모델/라벨이 즉시 갱신되게 했습니다. 이제 Gemini를 눌렀는데도 `현재 서비스=Ollama`, `Ollama 서버 주소`가 남아 있는 어긋남을 줄였습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 19:10 (KST)
- 상단 탭도 사용자 피드백 기준으로 다시 되돌렸습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 `TopTabBtn` 스타일에서 폰트 크기, 패딩, 외곽선, 선택 배경을 다시 키워 첫 번째 레퍼런스 이미지처럼 더 도톰한 pill 세그먼트 느낌으로 조정했습니다.
- 탭 래퍼 배경은 `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줄 햄버거가 아니라 좌측 패널/본문 구조가 보이는 패널 토글 아이콘으로 변경해 메뉴 아이콘과 혼동되지 않게 정리했습니다.
---
@@ -738,3 +1068,153 @@ 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에서 더 빨리 파악할 수 있다.

View 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`

View File

@@ -1,5 +1,39 @@
# 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.
- Document update: 2026-04-05 17:03 (KST) - Reworked the AX Agent UI skeleton further toward `claw-code`: sidebar width and row heights were tightened, the collapsed icon rail and sidebar header were flattened, status strips were reduced, and the composer shell/input chrome was made slimmer and less card-like.
- Document update: 2026-04-05 17:03 (KST) - Re-tuned responsive width calculation for the new shell so the message axis now scales up to `880` and the composer up to `820`, keeping both aligned on the same center line as the window narrows or expands.
- Document update: 2026-04-05 17:12 (KST) - Reduced message action chrome and event-banner density further toward `claw-code`. Message actions now use compact icon-only buttons, and Cowork/Code event banners use tighter margins, smaller metadata, and lighter file-path summaries so execution status stays subordinate to the main transcript.
- Document update: 2026-04-05 17:19 (KST) - Tightened the top header/tab group and sidebar conversation-row chrome further toward `claw-code`. The top tabs now use a slimmer segmented control, the sidebar toggle button is smaller/lighter, and conversation-row metadata plus the edit affordance were reduced so the title remains the dominant visual anchor.
- Document update: 2026-04-05 16:02 (KST) - Fixed the Cowork document execution handoff around `document_plan`. The loop no longer depends on broken localized marker strings to detect the scaffold/body block or the immediate-next-step hint; it now extracts body markers robustly and resolves the correct follow-up tool (`html_create`, `document_assemble`, `docx_create`, `markdown_create`) before re-prompting the model.
- Document update: 2026-04-05 16:02 (KST) - Added `ResolveEffectivePlanMode(...)` so Cowork document/content tasks automatically use the `always` plan path even when the persisted plan mode is `off`. This brings Cowork closer to the `claw-code` expectation of plan-first execution for document-heavy work.
- Document update: 2026-04-05 16:02 (KST) - Strengthened `BuildCoworkSystemPrompt()` so document/report/proposal/manual requests must produce an execution plan first and are not considered complete until a real output file path has been created or updated.
- Document update: 2026-04-05 15:34 (KST) - Rebased the AX Agent improvement plan on actual `claw-code` runtime files instead of prior AX snapshots. The active reference spine is `src/bootstrap/state.ts -> src/bridge/initReplBridge.ts -> src/bridge/sessionRunner.ts -> src/screens/REPL.tsx -> src/components/Messages.tsx -> src/components/StatusLine.tsx`.
- Document update: 2026-04-05 15:34 (KST) - Locked the AX implementation order to the same quality sequence used by that spine: runtime state canonicalization, prepared execution unification, loop event normalization, timeline render parity, composer/status strip simplification, and recovery/resume validation.
- Document update: 2026-04-05 15:42 (KST) - Moved the cross-tab conversation restoration path for execution events and agent-run history from `ChatWindow.xaml.cs` into `AxAgentExecutionEngine`. `AppendExecutionEvent` and `AppendAgentRun` now go through one engine-owned session mutation helper, which preserves the active tab conversation while updating the target tab timeline.
- Document update: 2026-04-05 15:42 (KST) - Verified the first runtime-state/common-engine step with `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` and confirmed warning 0 / error 0.
- Document update: 2026-04-05 07:11 (KST) - Simplified the AX Agent footer for Cowork/Code by removing the duplicated `MoodIconPanel` chip group from those tabs and leaving workspace context only in the main folder path row. Also removed the outline border from the data-usage button so the footer option strip reads flatter and less pill-heavy.
- Document update: 2026-04-05 07:08 (KST) - Improved AX Agent responsiveness in three hot paths: added an ordered meta cache in `ChatStorageService` so repeated conversation-list refreshes stop re-sorting the full meta set every time, short-circuited `SaveConversationSettings()` when permission/data-usage/mood/output-format values are unchanged, and debounced the sidebar conversation search refresh to avoid re-filtering on every keystroke.
- Document update: 2026-04-05 02:00 (KST) - Reworked the AX Agent in-chat gear overlay navigation itself to match the restored internal settings taxonomy: `basic / chat / cowork / code / dev / tools / skill-block`. The left nav labels now follow that scheme, and the overlay rows/toggles are regrouped per tab instead of the earlier `common / service / permission / advanced` split.
@@ -4475,3 +4509,442 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
- 업데이트: 2026-04-05 11:56 (KST)
- `Agent Compare` 기준으로 런처 보조 기능/설정 연결을 다시 대조하면서, 입력을 지웠을 때 이전 선택 항목과 미리보기 상태가 남아 빠른 실행 칩·검색 히스토리·미리보기 패널 전환이 어색해지던 흐름을 정리했습니다. [LauncherViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/LauncherViewModel.cs) 는 빈 입력과 런처 재오픈 시 `SelectedItem`을 함께 초기화해, 현재 쿼리와 무관한 미리보기/선택 상태가 잔류하지 않게 했습니다.
- 설정 연결 검증도 같이 진행했습니다. `Agent Compare`의 런처 설정 항목(`ShowNumberBadges`, `CloseOnFocusLost`, `RememberPosition`, `EnableActionMode`, `EnableRandomPlaceholder`, `EnableIconAnimation`, `EnableSelectionGlow`, `ShowLauncherBorder`, `EnableFavorites`, `EnableRecent`, `ShowPrefixBadge`)을 현재 [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml), [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [LauncherWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/LauncherWindow.xaml.cs), [LauncherViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/LauncherViewModel.cs), [CommandResolver.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Core/CommandResolver.cs), [SnippetExpander.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Core/SnippetExpander.cs), [WebSearchHandler.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Handlers/WebSearchHandler.cs), [App.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/App.xaml.cs), [ClipboardHistoryService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/ClipboardHistoryService.cs) 기준으로 다시 점검했고, 테마 동일화는 제외한 채 보조 기능 디테일과 설정 반영 경로 위주로 마감 기준을 맞췄습니다.
- 업데이트: 2026-04-05 12:06 (KST)
- AX Agent 채팅 엔진 정상화 1차에서 [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs)의 Chat 실행 모드를 `최종 응답 커밋형`으로 고정하고, [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `SendMessageAsync()` / `SendRegenerateAsync()`가 임시 assistant 메시지, 임시 스트리밍 컨테이너, 직접 UI 버블 삽입에 의존하지 않도록 다시 정리했습니다.
- 현재 흐름은 `사용자 메시지 저장 → 전송 메시지 준비 → LLM 또는 AgentLoop 실행 → 최종 assistant 텍스트 확정 → conversation/session에 커밋 → RenderMessages()` 순서입니다. 이로써 토큰은 집계되지만 본문이 비거나, assistant 빈 버블이 남거나, 재생성 시 메시지 모델과 화면 상태가 어긋나는 증상을 줄이는 쪽으로 맞췄습니다.
- `OnAgentEvent(...)`도 직접 `AddAgentEventBanner()`를 바로 꽂는 방식을 중단하고, `AppendConversationExecutionEvent()``ShowExecutionHistory`가 켜진 경우에만 `RenderMessages(preserveViewport: true)`를 다시 타게 바꿨습니다. 이 수정은 Cowork/Code에서 실행 로그가 플래시처럼 남거나 중복 잔상으로 보이던 문제를 줄이기 위한 것입니다.
- 이어서 수동 컨텍스트 압축 결과와 `/slash` 로컬 응답 경로도 conversation/session에 먼저 반영한 뒤 `RenderMessages()` 기준으로 다시 그리도록 맞췄습니다. 이 변경으로 Chat/Cowork/Code에서 “일반 전송은 모델 렌더, 로컬 응답은 직접 버블 주입”으로 두 경로가 섞여 있던 상태를 더 줄였고, 코워크/코드 엔진 정상화 작업을 계속 진행할 수 있는 공통 렌더 축을 확보했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 12:09 (KST)
- 업데이트: 2026-04-05 12:24 (KST)
- AX Agent 채팅 UI/엔진 재작업 전에 현재 기준본을 `etc/chat-ui-backup/2026-04-05-1215/`에 백업했습니다. 백업 범위는 `ChatWindow.xaml`, `ChatWindow.xaml.cs`, `Services/Agent/AxAgentExecutionEngine.cs` 3개 파일이며, `claw-code` 기준 UI/엔진 전환 중 회귀 시 즉시 비교할 수 있도록 남겼습니다.
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 메시지 컬럼과 빈 상태 영역 폭을 `920px` 기준으로 정리하고, 컴포저는 `760px` 고정 축으로 넓혔습니다. 입력 셸의 `InputBorder`는 라운드/패딩을 단순화하고, 컴포저 안의 `대화 내보내기` 버튼은 숨겨 `claw-code`처럼 메시지와 입력 축 중심의 레이아웃으로 더 가깝게 맞췄습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `UpdateInputBoxHeight()``TextBox.MinLines/MaxLines`만 믿지 않고 실제 줄바꿈 개수 기준으로 `Height`를 직접 다시 계산하도록 바꿨습니다. 이 수정은 Chat/Cowork/Code 공통으로 “전송 후 빈 상태인데 입력창 높이가 남아 있는” 버그를 더 강하게 억제하기 위한 것입니다.
- 메시지 편집 후 재생성과 수정 피드백 후 재생성도 직접 `AddMessageBubble(...)`를 삽입하지 않고, conversation 모델 갱신 후 `RenderMessages(preserveViewport: true)`로 다시 그리게 정리했습니다. 그래서 재생성 직전에 UI만 앞서 나가면서 모델과 화면이 어긋나는 경로를 더 줄였습니다.
- `SendRegenerateAsync(...)``claw-code` 기준의 단일 실행 축에 더 가깝게 맞췄습니다. 이제 Cowork/Code 재생성도 `ResolveExecutionMode(...)``BuildPromptStack(...)`를 거쳐 필요 시 `RunAgentLoopAsync(...)`를 사용하고, 최종 assistant 텍스트만 커밋합니다. 이전처럼 재생성만 일반 `_llm.SendAsync(...)`로 빠져 코워크/코드가 Chat 경로로 잘못 처리되던 분기를 제거했습니다.
- 업데이트: 2026-04-05 12:31 (KST)
- [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs)에 `PreparedExecution` record와 `PrepareExecution(...)` 메서드를 추가해, 실행 모드 판정(`ResolveExecutionMode`)과 프롬프트 스택 조합(`BuildPromptStack`), 최종 전송 메시지 준비(`PrepareTurn`)를 한 번에 묶도록 정리했습니다.
- 같은 엔진에 `NormalizeAssistantContent(...)`도 옮겨서, 최종 assistant 텍스트가 비었을 때 최근 실행 이벤트 요약을 어떻게 대체할지까지 UI가 아니라 엔진이 책임지게 바꿨습니다. 이건 `claw-code`처럼 UI보다 세션/실행 레이어가 메시지 결과를 더 많이 책임지게 만드는 방향의 일부입니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 는 새 `PrepareExecutionForConversation(...)`을 통해 일반 전송과 재생성 모두 같은 엔진 준비 경로를 사용합니다. 그래서 Cowork/Code 시스템 프롬프트 선택, 실행 모드 판정, 프롬프트 스택 구성, outbound message 조립이 각 메서드마다 중복 구현되지 않게 됐고, 다음 단계부터는 AgentLoop 완료 처리와 후속 큐 정리도 같은 방식으로 더 엔진 쪽으로 밀 수 있는 상태가 됐습니다.
- 업데이트: 2026-04-05 12:36 (KST)
- [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs)에 `FinalizeAssistantTurn(...)`를 추가해, 최종 assistant 텍스트 정규화와 `Cowork/Code``ShowExecutionHistory=false` 처리, assistant 메시지 커밋을 한 메서드에 묶었습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `SendMessageAsync()``SendRegenerateAsync()`는 더 이상 직접 `NormalizeAssistantContent(...)``CommitAssistantMessage(...)`를 따로 호출하지 않고, 둘 다 `FinalizeAssistantTurn(...)`으로 마무리합니다. 이로써 AX Agent 채팅 엔진의 “준비 → 실행 → 최종 커밋” 축이 한 단계 더 짧고 일관되게 정리됐습니다.
- 업데이트: 2026-04-05 12:41 (KST)
- `AgentLoop vs 일반 LLM 호출` 선택 분기까지 엔진으로 옮기기 위해 [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs)에 `ExecutePreparedAsync(...)`를 추가했습니다. 준비된 실행(`PreparedExecution`)과 두 실행 delegate를 넘기면, 엔진이 `UseAgentLoop` 판정에 따라 적절한 실행 경로를 선택합니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `SendMessageAsync()``SendRegenerateAsync()`는 이제 직접 `if (executionMode.UseAgentLoop)` 분기를 들고 있지 않고, 둘 다 `ExecutePreparedAsync(...)`를 통해 같은 실행 진입점을 사용합니다. 이 변경으로 AX Agent 채팅 엔진 공통화는 “준비 / 실행 선택 / 최종 커밋”까지 한 덩어리로 거의 맞춰지고 있습니다.
- 업데이트: 2026-04-05 12:47 (KST)
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 `ResetStreamingUiState()`, `FinalizeConversationTurn()`, `FinalizeQueuedDraft()`를 추가해, 전송과 재생성에 흩어져 있던 실행 후 UI/상태 정리 로직을 공통화했습니다.
- 이 변경으로 응답 완료 후의 `타이머 중지`, `버튼 상태 복원`, `스트림 필드 정리`, `대화 저장`, `대화 목록 갱신`, `대기열 완료/실패 반영`, `다음 큐 작업 시작` 경로가 전송과 재생성에서 같은 helper를 공유하게 됐습니다. 현재 AX Agent 채팅 엔진 공통화는 준비/실행 선택/최종 커밋/후처리까지 대부분 한 축으로 모이는 단계까지 왔습니다.
- 업데이트: 2026-04-05 12:53 (KST)
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 `_executionHistoryRenderTimer``ScheduleExecutionHistoryRender()`를 추가해, `OnAgentEvent(...)`가 실행 이벤트마다 즉시 `RenderMessages()`를 호출하지 않도록 바꿨습니다.
- 이제 `ShowExecutionHistory`가 켜진 Cowork/Code 대화에서도 실행 이벤트가 짧은 간격으로 몰릴 때는 120ms 단위로 재렌더를 묶어서 한 번만 반영합니다. 이건 실행 중 본문이 번쩍이거나 로그 잔상이 자주 갱신되는 체감을 줄이기 위한 배치 렌더 단계입니다.
- 업데이트: 2026-04-05 12:58 (KST)
- 같은 파일에 `_taskSummaryRefreshTimer``ScheduleTaskSummaryRefresh()`도 추가해, `UpdateTaskSummaryIndicators()`가 실행 이벤트나 서브에이전트 상태 변경마다 즉시 도는 대신 120ms 단위로 묶여 반영되게 했습니다.
- 이 조정은 `RuntimeActivityBadge`, `ConversationStatusStrip`, 완료 요약 라벨 같은 상태 스트립이 빠르게 깜빡이는 체감을 줄이기 위한 것이며, 본문 재렌더 배치와 함께 Cowork/Code 실행 중 전체 UI 안정성을 높이는 단계입니다.
- 업데이트: 2026-04-05 13:03 (KST)
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 `_conversationPersistTimer`, `ScheduleConversationPersist()`, `FlushPendingConversationPersists()`를 추가해, 실행 이벤트와 실행 기록이 들어올 때마다 곧바로 `_storage.Save(...)`를 치지 않도록 바꿨습니다.
- `AppendConversationExecutionEvent()``AppendConversationAgentRun()`는 이제 `ChatSessionStateService`의 append 메서드를 `storage=null`로 호출하고, 변경된 `ChatConversation`만 220ms 단위로 지연 저장합니다. 이 변경은 Cowork/Code 실행 중 빈번한 이벤트가 들어올 때 디스크 I/O 때문에 체감이 끊기는 문제를 줄이기 위한 배치 저장 단계입니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 13:12 (KST)
- 실행 완료 뒤 메시지 컬럼을 크게 흔들던 보조 UI를 더 줄였습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `RenderSuggestActionChips()`는 더 이상 본문 `MessagePanel`에 직접 제안 칩 컨테이너를 추가하지 않고, 제안 라벨 요약만 토스트로 표시합니다. Cowork/Code에서 `suggest_actions` 결과가 들어올 때 본문 레이아웃과 스크롤이 흔들리던 경로를 먼저 끊는 안정화 조치입니다.
- 같은 파일의 `DraftQueuePanel`도 기본 축약 표시로 바꿨습니다. 대기열은 처음부터 모든 섹션 카드를 다 렌더하지 않고, `CreateDraftQueueSummaryStrip(...)`의 요약 pill과 `CreateCompactDraftQueuePanel(...)`의 핵심 항목 한 장만 먼저 보여 줍니다. 사용자가 `상세 보기`를 누를 때만 `실행 중 / 다음 작업 / 보류 / 완료 / 실패` 섹션이 펼쳐집니다.
- 이번 조정으로 AX Agent 채팅 엔진 공통화 작업 중에도 보조 카드가 컴포저 위 레이아웃을 크게 밀어 올리거나, 실행 중간에 메시지 축을 흔드는 체감을 줄이는 방향으로 정리했습니다. 다음 단계는 이 축약형 UI 위에서 Cowork/Code 완료 카드와 큐 완료 후처리를 더 엔진 중심으로 맞추는 것입니다.
- 업데이트: 2026-04-05 13:20 (KST)
- Cowork/Code 완료 직후 저장 경로도 한 단계 더 정리했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 `PersistConversationSnapshot(...)`을 추가해 중간 저장, 최종 저장, 지연 저장 flush를 같은 helper가 담당하도록 만들었습니다.
- `ResetStreamingUiState()`는 이제 스트리밍 UI 타이머를 내리기 전에 `FlushPendingConversationPersists()`를 먼저 호출합니다. 이 변경은 실행 종료 직전 들어온 마지막 `ExecutionEvent` 또는 `AgentRun`이 지연 저장 큐에만 남아 있다가 타이머 중지와 함께 누락될 수 있는 경로를 막기 위한 것입니다.
- 같은 변경에서 `RunAgentLoopAsync(...)` 내부의 직접 `_storage.Save(...)` / `RememberConversation(...)`는 제거했습니다. Cowork/Code 완료 시점 저장은 이제 `FinalizeConversationTurn(...)`의 단일 완료 경로에서만 수행되어, AgentLoop 내부 저장과 완료 후 저장이 중복으로 겹치던 경로를 줄였습니다.
- 업데이트: 2026-04-05 13:29 (KST)
- 실행 이벤트가 들어올 때 즉시 많이 흔들리던 UI 갱신도 배치형으로 추가 정리했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 `_agentUiEventTimer`, `ScheduleAgentUiEvent(...)`, `FlushPendingAgentUiEvent()`를 넣어, `UpdateStatusBar(...)`, `UpdateAgentProgressBar(...)`, `UpdatePlanViewerStep(...)`, `CompletePlanViewer()`, `RefreshFileTreeIfVisible()`, `RenderSuggestActionChips(...)`, 자동 프리뷰 반영이 모두 최근 이벤트 기준 90ms 배치 갱신을 타게 했습니다.
- `OnAgentEvent(...)`는 이제 실행 이벤트를 먼저 대화 모델(`ExecutionEvents`, `AgentRuns`)과 앱 상태에 반영하고, UI는 배치 flush가 따라가도록 정리됐습니다. 이건 `claw-code` 기준의 “세션/상태 우선, 화면은 그 결과를 따라감” 원칙을 AX 구조에서 더 강화하는 단계입니다.
- 업데이트: 2026-04-05 13:37 (KST)
- 남아 있던 큰 UI 결합점인 “대기열 실행이 입력창을 거쳐 시작되는 구조”도 줄였습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `SendMessageAsync(...)`는 이제 직접 텍스트 인자를 받을 수 있고, `StartNextQueuedDraftIfAny(...)``InputBox.Text = next.Text` / `Focus()` / `UpdateInputBoxHeight()`를 거치지 않고 `SendMessageAsync(next.Text)`를 바로 호출합니다.
- 이 변경으로 대기열 기반 Cowork/Code 후속 작업은 입력창 상태와 포커스를 덜 흔들고, 실행 축 자체가 입력 UI보다 엔진/세션 흐름에 더 가까워졌습니다. 현재 남은 마감 작업은 완료 카드와 일부 에러 재시도 표시처럼 아직 창 코드에 남아 있는 소수의 직접 UI 후처리를 더 줄이는 단계입니다.
- 업데이트: 2026-04-05 13:44 (KST)
- `RetryLastUserMessageFromConversation()`도 입력창 의존을 제거했습니다. 마지막 사용자 요청을 재시도할 때 더 이상 `InputBox.Text`를 바꾸지 않고, 유휴 상태면 `SendMessageAsync(lastUserMessage)`로 바로 실행하며, 이미 작업 중이면 `ChatSession.EnqueueDraft(..., \"now\", ..., \"direct\")`로 곧바로 대기열에 올립니다.
- 이 조정으로 실패 후 재시도 역시 입력창 포커스/높이/슬래시 칩 상태와 분리되었고, Cowork/Code의 재시도 흐름이 세션/대기열 중심으로 더 정리됐습니다.
- 업데이트: 2026-04-05 13:52 (KST)
- 남아 있던 구형 본문 재시도 카드도 제거했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AddRetryButton()` 메서드와 그 호출 경로를 걷어내고, 예외 발생 시에는 토스트로만 짧게 안내한 뒤 작업 요약/실패 이력의 재시도 액션을 사용하게 했습니다.
- 이 변경으로 본문 `MessagePanel`에 실패 카드가 임시로 직접 삽입되는 구형 흐름이 사라졌고, 오류 복구도 메시지 축보다는 작업 요약 액션 축으로 수렴하게 됐습니다.
- 업데이트: 2026-04-05 14:00 (KST)
- UI도 `claw-code` 레퍼런스 방향으로 1차 단순화를 반영했습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 `MessagePanel` / `EmptyState`의 폭을 `880`으로 낮추고, 메시지 스크롤 여백을 줄여 메시지 축이 더 밀도 있게 보이도록 조정했습니다.
- 빈 상태는 부유 애니메이션이 있는 그라디언트 아이콘을 제거하고, 작은 정적 카드형 아이콘 + 짧은 문구로 바꿨습니다. 상단 `AgentProgressBar`는 패딩과 프로그레스 바 높이를 줄였고, 컴포저는 `800px` 축으로 넓히면서 `DraftPreviewCard`, `InputGlowBorder`, `InputBorder`의 라운드/그림자 강도를 낮춰 장식량을 줄였습니다.
- 메시지 행 자체도 `claw-code` 방향으로 더 눌렀습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml)의 `MessagePanel` 하단 여백을 줄였고, [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AddMessageBubble(...)`에서 사용자/assistant 카드의 좌우 마진, 코너 라운드, 패딩, 폰트 크기, 타임스탬프 크기를 전반적으로 축소했습니다.
- assistant 헤더는 아이콘/이름을 더 작고 옅게 조정해 메시지 본문이 먼저 읽히도록 바꿨고, 액션 바 버튼도 패딩과 간격을 줄여 hover 시에도 과하게 튀지 않게 맞췄습니다. `AddAgentEventBanner(...)` 역시 좌우 마진, 아이콘/라벨, 토큰 배지, 요약 텍스트 밀도를 함께 낮춰 실행 로그가 본문보다 먼저 보이던 시각적 압박을 줄였습니다.
- 폭 계산도 `claw-code`식 반응형 축으로 다시 맞췄습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml)의 `ComposerShell` 고정폭을 제거하고, [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 `UpdateResponsiveChatLayout()`를 추가해 `MessageScroll.ActualWidth` 기준으로 `MessagePanel`, `EmptyState`, `ComposerShell` 폭을 함께 다시 계산합니다.
- 이 변경으로 창이 줄어들 때 메시지 축과 컴포저가 함께 자연스럽게 줄고, 넓어질 때는 적절한 상한만 유지한 채 부드럽게 넓어집니다. 초기 `Loaded`와 창 `SizeChanged` 둘 다 같은 계산 경로를 타게 연결했습니다.
- 컴포저 상단 바도 1차 재구성했습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 `InputBorder`, `DraftPreviewCard`, `DraftQueuePanel`의 패딩/마진/그림자를 줄여 큰 카드 느낌을 낮췄고, `BtnModelSelector`, `TokenUsageCard`, `BtnTemplateSelector`의 높이, 패딩, 아이콘·폰트 크기를 함께 축소했습니다.
- 토큰 사용 카드의 원형 게이지 크기, 보조 텍스트 크기, `압축` 버튼도 같이 줄여 상단 옵션 바가 입력축보다 과하게 두꺼워 보이던 문제를 완화했습니다. 이 단계는 `claw-code`처럼 입력부를 “크게 장식된 카드”보다 “얇은 하단 작업 바”에 가깝게 만드는 1차 조정입니다.
- Cowork/Code 상태 UI도 더 얇게 정리했습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 `ConversationStatusStrip`, `ConversationQuickStrip`, `AgentProgressBar`, `RuntimeActivityBadge`, `ExecutionLog`, `SubAgentIndicator`, `StatusElapsed`, `StatusTokens`의 패딩, 폰트 크기, 아이콘 크기, 간격을 전반적으로 줄여 항상 보이는 상태 스트립이 본문보다 앞서 튀지 않게 조정했습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `ShowTaskSummaryPopup()`도 제목/설명/최근 실행 섹션 밀도를 낮추고 최근 실행 목록을 2개로 줄였습니다. 작업 요약 팝업이 상태 진단용 보조 패널 역할에 더 가깝게 남도록 한 단계 정리한 것입니다.
- 작업 요약 내부 카드도 추가로 축소했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `CreateTaskSummaryActionButton(...)` 버튼 규격을 더 작게 낮췄고, 권한/훅/백그라운드 카드의 패딩과 마진도 줄였습니다.
- 최근 권한 이력은 2개, 최근 훅은 3개, 최근 백그라운드 작업은 2개까지만 보여 주도록 줄여, 작업 요약 팝업이 상태 정보는 유지하되 세로 길이와 시각 압박이 커지지 않도록 정리했습니다.
- 좌측 패널과 하단 바도 `claw-code` 시각 언어 쪽으로 다시 묶었습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 사이드바 폭을 `256`으로 줄이고, 헤더 앱 아이콘을 작은 배지형으로 바꿨으며, `새 대화`, `검색`, 카테고리 드롭다운, 탭별 메뉴 행의 패딩/폰트/아이콘 크기를 함께 낮췄습니다.
- 아이콘 바의 사용자 원형 배지와 사이드바 하단 계정 영역도 강조색 단색 원 대신 `HintBackground + BorderColor` 기반의 중립 배지형으로 바꿨고, 설정 버튼 크기도 줄였습니다. 하단 상태바는 높이와 패딩을 더 낮추고, `StatusDiamond`를 작은 원형 점으로 바꿔 `claw-code`처럼 얇은 상태선에 더 가깝게 정리했습니다.
- 실행 로그 배너도 본문 침범을 더 줄였습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AddAgentEventBanner(...)` 에서 debug 전용 `ToolInput` 카드는 길이와 패딩을 더 줄였고, `FilePath`는 일반 로그에서는 액션이 붙은 카드형 대신 파일명 한 줄만 보여 주도록 분기했습니다.
- 이 변경으로 빠른 액션이 붙은 파일 경로 카드와 디버그 상세는 `debug` 로그에서만 크게 보이고, 일반 Cowork/Code 로그에서는 본문 아래에 더 얇은 보조 행으로만 남습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 14:49 (KST)
- 대화 목록 행 카드와 축소 아이콘 바도 `claw-code` 쪽 시각 언어에 더 맞췄습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AddConversationItem(...)`에서 선택 상태 배경과 액센트 바 두께를 더 줄이고, 목록 행 패딩/아이콘/폰트/배지 크기를 전반적으로 낮췄습니다.
- `진행 중`, `성공`, `실패` 배지는 더 작고 중립적인 톤으로 정리했고, 실행 요약 텍스트도 한 단계 작은 크기와 짧은 상하 여백을 써서 목록 메타가 제목보다 덜 튀게 조정했습니다. 호버 시 확대 애니메이션은 제거하고 얇은 배경 강조만 남겨, 목록 반응이 더 차분한 레이어로 보이게 바꿨습니다.
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml)의 축소 아이콘 바는 상하 row 높이와 버튼 규격, 아이콘 크기, 사용자 배지 크기를 함께 줄여 현재 사이드바 헤더/하단 계정 영역과 같은 밀도로 묶었습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 14:57 (KST)
- AX Agent 내부 설정 탭 구조도 다시 나눴습니다. [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)의 `SetOverlaySection(...)` 가시성 로직도 각 탭에 맞는 패널만 보이도록 재구성했습니다.
- `공통` 탭에는 숨겨져 있던 `테마 스타일`, `테마 모드`를 실제 선택 카드로 복구했고, `도구` 탭에는 훅/도구 커넥터 목록, `스킬` 탭에는 스킬 폴더/슬래시/로드된 스킬/폴백 모델/MCP 서버, `차단` 탭에는 차단 경로와 확장자만 남도록 분리했습니다.
- 메인 설정 쪽은 [SettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml.cs) 에서 `AgentShortcutTabItem``MainSettingsTab` 맨 끝으로 다시 추가해, AX Agent 이동 항목이 좌측 사이드바 맨 아래에 오도록 정리했습니다.
- 런처 하단 위젯도 일반 설정에서 요소별로 제어할 수 있게 확장했습니다. [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs), [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml)에 `성능 / 포모도로 / 메모 / 날씨 / 일정 / 배터리` 위젯 표시 토글을 추가했습니다.
- [LauncherWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/LauncherWindow.xaml) 에서는 하단 `Ollama / API / MCP` 서버 상태 위젯을 제거했고, [LauncherWindow.Widgets.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/LauncherWindow.Widgets.cs) 에서는 서버 상태 polling과 dot 갱신 경로를 제거한 뒤 위젯 표시/숨김을 설정값 기준으로 통합했습니다.
- 배터리 위젯은 사용자 토글과 실제 배터리 가용 상태를 함께 반영하도록 `UpdateWidgetVisibility()`에서 최종 가시성을 결정하게 바꿨고, 모든 위젯이 꺼져 있으면 하단 위젯 바 전체도 자동으로 숨깁니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 15:16 (KST)
- `claw-code` 기준 계획 UX 정리도 반영했습니다. [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs) 의 `ResolveEffectivePlanMode(...)` 는 이제 항상 `off`를 반환해, 저장된 예전 계획 모드 값이 남아 있어도 런타임에서는 자동 계획/자동 승인 팝업이 다시 살아나지 않게 했습니다.
- [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml) 과 [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 메인 설정과 AX Agent 내부 설정의 `계획 모드` UI를 `Collapsed`로 전환했고, [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs) 도 `off` 고정으로 바꿔 예전 persisted 값이 다시 UI나 상태바에 반영되지 않게 정리했습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `CreatePlanDecisionCallback()`은 더 이상 채팅 본문에 인라인 승인 버튼을 추가하지 않고, [PlanViewerWindow.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/PlanViewerWindow.cs) 를 AX Agent 창 owner와 merged resources 기준으로 생성해 플랜 팝업이 AX Agent 테마와 같은 리소스 축을 따르도록 바꿨습니다.
- 검증 예정: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\`
- 업데이트: 2026-04-05 16:20 (KST)
- `claw-code` 기준 UI/엔진 재구성 1차도 진행했습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 AX Agent 메인 레이아웃의 사이드바 폭을 줄이고, 메시지/빈 상태/컴포저의 기본 폭과 패딩, 라운드, 그림자 강도를 다시 조정해 메시지 중심 구조로 압축했습니다.
- 보조 상태 UI도 더 얇게 만들었습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 대화 상태 스트립은 Chat 탭에서 숨기고 Cowork/Code도 실패/승인 대기/차단 같은 핵심 상태만 보여 주도록 줄였으며, 빠른 스트립도 Chat에서는 열리지 않게 정리했습니다.
- 반응형 폭 계산은 같은 파일의 `UpdateResponsiveChatLayout()`에서 다시 조정해 메시지 축 상한을 `840`, 컴포저 축 상한을 `760` 기준으로 맞췄고, 좁은 창에서는 둘이 같은 중심선으로 함께 줄어들도록 재계산하게 했습니다.
- 엔진 쪽은 [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs)에 `FinalizeExecutionContent(...)` 를 추가해 취소/오류/빈 응답 정규화를 공통 helper로 모았습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 일반 전송과 재생성은 이제 같은 helper를 써서 응답 마감을 처리합니다.
- 검증 예정: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\`
- 업데이트: 2026-04-05 16:33 (KST)
- Cowork/Code 보조 상태 레이어도 한 단계 더 줄였습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml)의 `ConversationStatusStrip`, `ConversationQuickStrip`, `AgentProgressBar`, `RuntimeActivityBadge`, `LastCompletedLabel`, `ExecutionLog`, `SubAgentIndicator`, `StatusElapsed`, `StatusTokens`는 패딩·폰트·간격을 추가로 낮춰 `claw-code`처럼 상시 노출돼도 본문보다 덜 튀는 보조 레이어로 정리했습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `ShowTaskSummaryPopup()`, `CreateTaskSummaryActionButton(...)`, `BuildHookSummaryCard(...)`, `BuildActiveBackgroundSummaryCard(...)`, `BuildRecentBackgroundJobCard(...)`는 팝업 헤더, 필터 칩, 최근 실행 카드, 훅/백그라운드 카드의 라운드·패딩·마진·글자 크기를 다시 줄여 작업 요약이 상태 대시보드보다 진단용 팝업에 가깝게 보이도록 맞췄습니다.
- 검증 예정: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\`
- 업데이트: 2026-04-05 17:27 (KST)
- 메시지 행 메타와 완료 카드 라벨도 한 단계 더 줄였습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AddMessageBubble(...)` 에서 사용자/assistant 버블 패딩, 코너, 폰트, 타임스탬프, assistant 헤더 아이콘/이름 크기를 추가로 낮춰 본문 텍스트가 더 먼저 읽히도록 조정했습니다.
- 작업 요약 팝업의 완료 카드도 `실행 run`, `최근 실패`, `최근 실행`, `로그`, `파일`, `후속 큐`, `다시 시도`, `타임라인`처럼 더 짧은 라벨로 바꾸고, run/step 메타와 요약 텍스트 폰트도 함께 낮춰 전체 밀도를 더 가볍게 정리했습니다.
- 검증 예정: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\`
- 업데이트: 2026-04-05 17:33 (KST)
- Cowork/Code 실행 타임라인 배너도 더 얇게 줄였습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AddAgentEventBanner(...)` 에서 일반 실행 배너의 좌우 마진, 아이콘/라벨, 경과 시간, 토큰 pill, 요약 텍스트, 파일 경로 행을 한 단계 더 축소했고, review 신호 칩은 `debug` 로그에서만 보이게 제한했습니다.
- 이제 평소 Cowork/Code에서는 실행 이벤트가 짧은 한 줄 요약 중심으로 보이고, 상세 진단 정보는 debug 수준일 때만 더 붙습니다.
- 검증 예정: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\`
- 업데이트: 2026-04-05 17:39 (KST)
- 상단 헤더도 더 `claw-code` 밀도로 줄였습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 상단 탭 버튼 폰트/패딩/코너를 다시 낮추고, 탭 그룹 래퍼와 제목 서브 바 높이·패딩도 함께 줄였습니다.
- 대화 제목 폰트와 최대 폭, 빠른 스트립 버튼 규격, 프리뷰 토글 크기와 라벨도 더 작게 조정해 상단 보조 정보가 본문보다 덜 튀게 정리했습니다.
- 검증 예정: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\`
- 업데이트: 2026-04-05 17:45 (KST)
- 좌측 사이드바도 한 번에 더 `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) 에서는 실제 사이드바 폭을 `270 -> 248`로 줄이고, `AddConversationItem(...)`의 대화 목록 카드 패딩, 코너, 아이콘 열 폭, 제목/날짜/실행 메타 폰트, 편집 버튼 규격, 선택 액센트 바 두께도 함께 축소해 목록 전체가 더 차분한 레이어로 보이게 맞췄습니다.
- 검증 예정: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\`
- 업데이트: 2026-04-05 17:53 (KST)
- 큰 카드형 요소도 더 `claw-code` 쪽으로 눌렀습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AddPlanningCard(...)` 에서 계획 카드 라운드, 패딩, 헤더 아이콘/텍스트, 진행률 텍스트, 단계 행 폰트를 전반적으로 줄이고, 계획 헤더 문구도 더 짧게 정리했습니다.
- 같은 변경에서 `CreateCompactEventPill(...)`, `CreateTimelineLoadMoreCard(...)`도 함께 축소해 컨텍스트 압축 pill과 “이전 대화 더 보기” 카드가 본문보다 과하게 두껍게 보이지 않도록 맞췄습니다.
- 검증 예정: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\`
- 업데이트: 2026-04-05 18:01 (KST)
- 엔진 마감도 한 단계 더 진행했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 `ExecutePreparedTurnAsync(...)`를 추가해 `send``regenerate`가 같은 실행/예외/취소/최종 커밋/후처리 helper를 타도록 묶었습니다. 이로써 prepared-execution 이후 마감 경로도 하나로 수렴했습니다.
- 같은 변경에서 계획 이벤트는 기본적으로 큰 카드가 아니라 얇은 요약 pill로만 보이고, `debug` 로그일 때만 `AddPlanningCard(...)`가 펼쳐지도록 바꿨습니다. 문서형 Cowork/Code 작업에서도 기본 노출이 더 최소화됐습니다.
- 검증 예정: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\`
- 업데이트: 2026-04-05 18:08 (KST)
- AX Agent 엔진 마감 2차로 [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs)에 `FinalizeExecutionContentForUi(...)`, `NormalizeAssistantContentForUi(...)`를 추가해 취소/오류/빈 응답과 Cowork/Code 완료 문구를 UI용 한국어 기준으로 다시 정규화했습니다. 기존 깨진 문자열을 직접 건드리기보다 호출 축을 새 helper로 교체해 위험을 낮췄습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서는 `RegenerateLastAsync()``RetryWithFeedbackAsync(...)`가 마지막 assistant 응답을 더 이상 `MessagePanel.Children.RemoveAt(...)`로 직접 지우지 않고, 세션/대화 모델을 먼저 갱신한 뒤 `RenderMessages(preserveViewport: true)`로 다시 그리게 바꿨습니다. retry/regenerate가 화면 구조보다 conversation state를 우선하도록 정리한 단계입니다.
- 같은 변경에서 `AddAgentEventBanner(...)``Paused/Resumed``debug`가 아닐 때 기본 타임라인에 그리지 않게 바꿔 Cowork/Code 실행 중 흔한 상태 전환 배너 노출을 더 줄였습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 18:20 (KST)
- AX Agent 메인 UI도 `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) 에서는 사용자/assistant 버블, assistant 헤더, compact pill, 이전 대화 로드 카드, planning 카드의 패딩/메타/폰트와 여백을 다시 줄여 transcript 우선 구조로 맞췄습니다. `UpdateResponsiveChatLayout()``message 960 / composer 900` 축으로 재설정해 창 축소·확대 시 본문과 입력축이 더 같은 중심선을 공유하도록 바꿨습니다.
- 같은 변경에서 사이드바 열림 폭은 `220`, 최소 폭은 `168`로 다시 줄여 `claw-code`처럼 좌측 내비가 본문을 덜 먹도록 조정했습니다.
- 현재 `claw-code` 대비 추정 진척율은 핵심 엔진 `88%`, 채팅 메인 UI `94%`, Cowork/Code 상태 UX `89%`, 내부 설정 연결 `88%`, 전체 AX Agent `92%` 정도로 봅니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 18:30 (KST)
- Cowork/Code 보조 상태 레이어를 추가로 축소했습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml)의 `ConversationStatusStrip`, `ConversationQuickStrip`, `AgentProgressBar`, `RuntimeActivityBadge`, `ExecutionLog`, `SubAgentIndicator`, `StatusElapsed`, `StatusTokens`는 패딩·폰트·간격을 한 단계 더 낮춰 transcript 우선 구조를 해치지 않도록 정리했습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `ShowTaskSummaryPopup()``claw-code` 기준 최소 노출 방향으로 다시 구성했습니다. 필터 칩 행을 제거했고, 최근 실행은 1건 기준의 마지막 실행 카드만 남기며, 활성/최근 작업 카드도 각각 `3/2`개만 노출하게 줄였습니다.
- 같은 파일의 `BuildHookSummaryCard(...)`, `BuildActiveBackgroundSummaryCard(...)`, `BuildRecentBackgroundJobCard(...)`는 라운드, 패딩, 타이포 밀도를 함께 낮추고, 배경/훅 카드의 이동·필터 액션을 대부분 제거해 “상태 대시보드”보다 “짧은 진단 카드” 쪽으로 맞췄습니다. `AddTaskSummaryObservabilitySections(...)`는 기본 팝업에서 권한/백그라운드 현재 상태만 남기고 권한 이력/훅 이력/최근 백그라운드 이력을 기본 노출에서 제외했습니다.
- 현재 `claw-code` 대비 추정 진척율은 핵심 엔진 `88%`, 채팅 메인 UI `95%`, Cowork/Code 상태 UX `91%`, 내부 설정 연결 `88%`, 전체 AX Agent `93%` 정도로 봅니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 18:40 (KST)
- Cowork/Code 실행 타임라인도 더 `claw-code` 기준으로 눌렀습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AddAgentEventBanner(...)``debug`가 아닐 때 `ToolCall` 중간 이벤트를 기본 타임라인에서 숨기고, 일반 요약 문구 길이와 메타 폰트도 더 줄여 결과/오류 중심 흐름이 먼저 보이게 정리했습니다.
- `AddPlanningCard(...)`는 계획 카드 라운드, 패딩, 헤더 문구, 단계 폰트와 최대 폭을 더 낮춰 “계획 패널”보다 “짧은 계획 메모”에 가깝게 축소했습니다. transcript에 붙는 계획 표시가 이제 본문과 더 같은 축을 쓰도록 정리됐습니다.
- 같은 변경에서 `BuildTaskSummaryCard(...)``CreateTaskSummaryActionButton(...)` 도 한 단계 더 소형화했고, `BuildTaskSummaryActionRow(...)` 의 권한 카드 액션에서는 이미 제거한 `계획 모드` 버튼을 빼서 엔진 기본 정책과 UI가 다시 어긋나지 않게 맞췄습니다.
- 현재 `claw-code` 대비 추정 진척율은 핵심 엔진 `89%`, 채팅 메인 UI `96%`, Cowork/Code 상태 UX `92%`, 내부 설정 연결 `88%`, 전체 AX Agent `94%` 정도로 봅니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 18:49 (KST)
- 사용자 피드백 기준 UI 복구도 반영했습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml)의 상단 탭은 너무 얇아진 세그먼트형에서 한 단계 되돌려 `TopTabBtn` 폰트와 패딩, 탭 그룹 외곽 패딩을 키워 예전처럼 더 읽기 쉬운 pill 형태로 복구했습니다.
- 같은 파일의 하단 컴포저 상단 줄에서는 `TokenUsageCard``BtnTemplateSelector`가 같은 Grid 컬럼을 공유해 겹치던 구조를 수정했습니다. 컬럼을 4개로 나눠 `모델 선택 / 여백 / 토큰 카드 / 프리셋 버튼`을 각각 독립 배치했고, 카드/버튼의 폰트와 패딩도 다시 키워 하단 레이아웃이 눌려 보이지 않게 정리했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 18:55 (KST)
- 작업 유형 카드도 사용자 피드백 기준으로 다시 정리했습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `BuildTopicButtons()` 에서 프리셋/기타/프리셋 추가 카드 하단의 상시 설명 텍스트를 제거하고, 모든 카드를 `132 x 110` 규격으로 통일했습니다.
- 카드 설명은 hover 시 카드 하단에 뜨는 작은 라벨과 ToolTip으로만 노출되게 바꿨고, 기존 `ScaleTransform` 확대 애니메이션은 제거해 hover 시 배경과 테두리만 바뀌도록 정리했습니다. 이로써 마우스 오버 시 카드가 들썩이던 UX를 안정적으로 바꿨습니다.
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서는 `EmptyStateTitle`, `EmptyStateDesc` 폰트를 키워 현재 빈 상태 화면 전반의 글자 크기도 함께 보정했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 19:02 (KST)
- AX Agent 내부 설정 공통 탭의 서비스/테마 배치를 다시 정리했습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 서비스 상세 패널을 `OverlayEndpointFieldPanel`, `OverlayApiKeyFieldPanel`로 분리하고, `테마 스타일`, `테마 모드` 블록을 서비스/모델 상세 바로 아래로 이동해 공통 탭에서 바로 보이게 조정했습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에 `RefreshOverlayServiceFieldVisibility(...)` 를 추가했습니다. 이 메서드는 `Gemini/Claude` 선택 시 주소 입력 패널을 접고 API 키 패널을 전체 폭으로 확장하며, `Ollama/vLLM` 은 주소+키 2열 구성을 유지합니다. `RefreshOverlayVisualState(...)` 에도 이 가시성 동기화를 추가했습니다.
- 같은 파일의 `SetOverlayService(...)` 는 설정 저장 직후 `RefreshOverlayVisualState(loadDeferredInputs: true)` 를 강제 호출하도록 바꿨습니다. 내부 설정에서 서비스 카드를 눌렀을 때 `현재 서비스`, `현재 모델`, 주소/키 라벨이 즉시 바뀌지 않던 문제를 바로잡기 위한 변경입니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 19:10 (KST)
- 상단 탭 UI도 사용자 레퍼런스에 맞춰 되돌렸습니다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml)의 `TopTabBtn` 스타일은 폰트 크기 `13.5`, 패딩 `20x8`, 버튼 간 간격 `2`로 다시 키우고, 선택 상태에서 `LauncherBackground + BorderColor` 조합으로 도톰한 pill 탭이 보이게 조정했습니다.
- 탭 래퍼 자체도 `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.

View File

@@ -3,11 +3,108 @@
## Scope
- Align AX Copilot with claw-code quality for loop reliability, permission/hook behavior, and session durability.
## Update
- Updated: 2026-04-05 15:34 (KST)
- Rebased the AX Agent improvement plan on actual `claw-code` runtime files instead of earlier AX snapshots. The reference spine is now `src/bootstrap/state.ts -> src/bridge/initReplBridge.ts -> src/bridge/sessionRunner.ts -> src/screens/REPL.tsx -> src/components/Messages.tsx -> src/components/StatusLine.tsx`.
- AX Agent work should follow that same quality order: state first, execution second, render last. UI-only fixes that bypass state/execution should be treated as temporary.
- 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.
- Plan Mode, parallel tool execution, and unknown-tool recovery are in place.
- Session restore hardening is ongoing.
## Reference Map
| claw-code reference | AX apply target | completion criteria | quality criteria |
|---|---|---|---|
| `src/bootstrap/state.ts` | `src/AxCopilot/Views/ChatWindow.xaml.cs`, `src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs`, `src/AxCopilot/Services/ChatStorageService.cs` | one canonical runtime/session state for current turn, queue, retry, execution events, and persisted snapshot | reopen/retry/queue flows do not create duplicate or blank assistant messages |
| `src/bridge/initReplBridge.ts` | `src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs`, `src/AxCopilot/Services/LlmService.cs` | send/regenerate/retry/queued follow-up/slash all enter through one prepared-execution path | same input under same settings takes same execution route regardless of entry point |
| `src/bridge/sessionRunner.ts` | `src/AxCopilot/Services/Agent/AgentLoopService.cs`, `src/AxCopilot/Services/Agent/AgentLoopTransitions.cs`, `src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs` | tool start/result/error/progress normalized once inside loop layer | Cowork/Code no longer flash repeated status strings or overshare debug payloads |
| `src/bridge/bridgeMessaging.ts` | `src/AxCopilot/Views/ChatWindow.xaml.cs`, `src/AxCopilot/Services/Agent/AgentLoopService.cs` | inbound execution events separated from display-only events before UI render | execution event replay does not duplicate visible timeline banners |
| `src/screens/REPL.tsx` | `src/AxCopilot/Views/ChatWindow.xaml`, `src/AxCopilot/Views/ChatWindow.xaml.cs` | screen state transitions, queue flow, retry flow, and composer state use shared runtime helpers | window resize, queue chaining, and retry feel stable instead of UI-patched |
| `src/components/Messages.tsx` | `src/AxCopilot/Views/ChatWindow.xaml.cs` | timeline derives from normalized conversation/session state only | no token-only completions, blank cards, or direct injected duplicates |
| `src/components/StatusLine.tsx` | `src/AxCopilot/Views/ChatWindow.xaml`, `src/AxCopilot/Views/ChatWindow.xaml.cs` | status strip computed from debounced runtime state, not multiple imperative refresh calls | metadata stays lightweight and does not overpower message timeline |
## AX Agent Improvement Phases
### Phase A. Runtime State Canonicalization
- Reference: `src/bootstrap/state.ts`
- AX apply location: `src/AxCopilot/Views/ChatWindow.xaml.cs`, `src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs`, `src/AxCopilot/Services/ChatStorageService.cs`
- Completion criteria:
- `Chat`, `Cowork`, `Code` all update one shared runtime/session state model.
- queue, retry, post-compaction, and execution-event state can be restored after reopen.
- Quality criteria:
- reopening a conversation reproduces the same visible timeline without extra assistant cards.
- queue and execution badges remain in sync with the stored conversation.
### Phase B. Prepared Execution Unification
- Reference: `src/bridge/initReplBridge.ts`
- AX apply location: `src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs`, `src/AxCopilot/Services/LlmService.cs`
- Completion criteria:
- prompt stack assembly, execution mode choice, and final assistant commit are engine-owned.
- send/regenerate/retry/queued follow-up/slash flows all call the same preparation API.
- Quality criteria:
- behavior is deterministic per tab/settings combination.
- UI stops building different prompt stacks for the same conversation state.
### Phase C. AgentLoop Event Normalization
- Reference: `src/bridge/sessionRunner.ts`, `src/bridge/bridgeMessaging.ts`
- AX apply location: `src/AxCopilot/Services/Agent/AgentLoopService.cs`, `src/AxCopilot/Services/Agent/AgentLoopTransitions.cs`, `src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs`
- Completion criteria:
- loop events are normalized into bounded activity/event records before UI consumption.
- permission requests, failure states, retries, and completion states use a stable event shape.
- Quality criteria:
- Cowork/Code no longer flash rapidly during long-running tool sequences.
- file path/debug detail remains collapsed by default.
### Phase D. Timeline Render Parity
- Reference: `src/screens/REPL.tsx`, `src/components/Messages.tsx`
- AX apply location: `src/AxCopilot/Views/ChatWindow.xaml`, `src/AxCopilot/Views/ChatWindow.xaml.cs`
- Completion criteria:
- assistant/user messages, execution logs, compact boundaries, and queue summaries are rendered from one derived timeline model.
- direct imperative bubble injection is removed from normal send/regenerate/retry flows.
- Quality criteria:
- no blank assistant cards.
- no token-only completion without visible content.
- no duplicate event banners after re-render.
### Phase E. Composer and Status Strip Simplification
- Reference: `src/screens/REPL.tsx`, `src/components/StatusLine.tsx`
- AX apply location: `src/AxCopilot/Views/ChatWindow.xaml`, `src/AxCopilot/Views/ChatWindow.xaml.cs`
- Completion criteria:
- composer height grows only on explicit line breaks.
- status strip, queue summary, and runtime activity all use debounced runtime updates.
- Chat/Cowork/Code share one responsive width calculation policy.
- Quality criteria:
- resizing feels natural.
- composer does not keep growing after send.
- metadata remains subordinate to the message timeline.
### Phase F. Recovery, Resume, and Verification
- Reference: `src/bootstrap/state.ts`, `src/bridge/sessionRunner.ts`, `src/screens/REPL.tsx`
- AX apply location: `src/AxCopilot/Views/ChatWindow.xaml.cs`, `src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs`, `src/AxCopilot/Services/ChatStorageService.cs`
- Completion criteria:
- reopen after interruption keeps queue, runtime summary, and latest visible assistant state consistent.
- retry-last and regenerate do not depend on mutating `InputBox.Text`.
- all three tabs pass reopen/retry/manual compact/manual stop/manual resume scenarios.
- Quality criteria:
- stored conversation and rendered conversation stay identical after restore.
- final reopened state matches the last completed runtime state.
## Execution Tracks
1. Hook contract parity
- Structured hook output support (`updatedInput`, `updatedPermissions`, `additionalContext`).
@@ -28,3 +125,399 @@
- Internal parity scenarios pass target threshold.
- Resume/replay failures: zero.
- `dotnet build` warnings/errors: zero.
## Validation Matrix
- Build: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\`
- Manual scenario 1: Chat send -> answer visible -> retry -> regenerate -> reopen conversation
- 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를 계산

View File

@@ -0,0 +1,128 @@
using AxCopilot.Models;
using AxCopilot.Services;
namespace AxCopilot.Services.Agent;
/// <summary>
/// AX Agent execution-prep engine.
/// Inspired by the `claw-code` split between input preparation and session execution,
/// so the UI layer stops owning message assembly and final assistant commit logic.
/// </summary>
public sealed class AxAgentExecutionEngine
{
public sealed record PreparedTurn(List<ChatMessage> Messages);
public sealed record ExecutionMode(bool UseAgentLoop, bool UseStreamingTransport, string? TaskSystemPrompt);
public IReadOnlyList<string> BuildPromptStack(
string? conversationSystem,
string? slashSystem,
string? taskSystem = null)
{
var prompts = new List<string>();
if (!string.IsNullOrWhiteSpace(conversationSystem))
prompts.Add(conversationSystem.Trim());
if (!string.IsNullOrWhiteSpace(slashSystem))
prompts.Add(slashSystem.Trim());
if (!string.IsNullOrWhiteSpace(taskSystem))
prompts.Add(taskSystem.Trim());
return prompts;
}
public ExecutionMode ResolveExecutionMode(
string runTab,
bool streamingEnabled,
string resolvedService,
string? coworkSystemPrompt,
string? codeSystemPrompt)
{
if (string.Equals(runTab, "Cowork", StringComparison.OrdinalIgnoreCase))
return new ExecutionMode(true, false, coworkSystemPrompt);
if (string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase))
return new ExecutionMode(true, false, codeSystemPrompt);
return new ExecutionMode(false, false, null);
}
public PreparedTurn PrepareTurn(
ChatConversation conversation,
IEnumerable<string?> systemPrompts,
string? fileContext = null,
IReadOnlyList<ImageAttachment>? images = null)
{
var outbound = conversation.Messages
.Select(CloneMessage)
.ToList();
if (!string.IsNullOrWhiteSpace(fileContext))
{
var lastUserIndex = outbound.FindLastIndex(m => string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase));
if (lastUserIndex >= 0)
outbound[lastUserIndex].Content = (outbound[lastUserIndex].Content ?? string.Empty) + fileContext;
}
if (images is { Count: > 0 })
{
var lastUserIndex = outbound.FindLastIndex(m => string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase));
if (lastUserIndex >= 0)
outbound[lastUserIndex].Images = images.Select(CloneImage).ToList();
}
var promptList = systemPrompts
.Where(prompt => !string.IsNullOrWhiteSpace(prompt))
.Select(prompt => prompt!.Trim())
.ToList();
for (var i = promptList.Count - 1; i >= 0; i--)
outbound.Insert(0, new ChatMessage { Role = "system", Content = promptList[i] });
return new PreparedTurn(outbound);
}
public ChatMessage CommitAssistantMessage(
ChatSessionStateService? session,
ChatConversation conversation,
string tab,
string content,
ChatStorageService? storage = null)
{
var assistant = new ChatMessage
{
Role = "assistant",
Content = content,
};
if (session != null)
{
session.AppendMessage(tab, assistant, storage);
return assistant;
}
conversation.Messages.Add(assistant);
conversation.UpdatedAt = DateTime.Now;
return assistant;
}
private static ChatMessage CloneMessage(ChatMessage source)
{
return new ChatMessage
{
Role = source.Role,
Content = source.Content,
Timestamp = source.Timestamp,
MetaRunId = source.MetaRunId,
AttachedFiles = source.AttachedFiles?.ToList(),
Images = source.Images?.Select(CloneImage).ToList(),
};
}
private static ImageAttachment CloneImage(ImageAttachment source)
{
return new ImageAttachment
{
Base64 = source.Base64,
MimeType = source.MimeType,
FileName = source.FileName,
};
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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 포맷팅 (들여쓰기 적용)" },

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -201,6 +201,24 @@ public class LauncherSettings
[JsonPropertyName("showLauncherBorder")]
public bool ShowLauncherBorder { get; set; } = true;
[JsonPropertyName("showWidgetPerf")]
public bool ShowWidgetPerf { get; set; } = false;
[JsonPropertyName("showWidgetPomo")]
public bool ShowWidgetPomo { get; set; } = false;
[JsonPropertyName("showWidgetNote")]
public bool ShowWidgetNote { get; set; } = false;
[JsonPropertyName("showWidgetWeather")]
public bool ShowWidgetWeather { get; set; } = false;
[JsonPropertyName("showWidgetCalendar")]
public bool ShowWidgetCalendar { get; set; } = false;
[JsonPropertyName("showWidgetBattery")]
public bool ShowWidgetBattery { get; set; } = false;
/// <summary>단축키 헬프 창에서 아이콘 색상을 테마 AccentColor 기준으로 표시. 기본 true(테마색).</summary>
[JsonPropertyName("shortcutHelpUseThemeColor")]
public bool ShortcutHelpUseThemeColor { get; set; } = true;
@@ -939,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/ 디렉토리의 규칙을 시스템 프롬프트에 자동 포함
@@ -1271,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;

View File

@@ -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; }

View File

@@ -199,8 +199,8 @@ public partial class AgentLoopService
var recentTaskRetryQuality = TryGetRecentTaskRetryQuality(taskPolicy.TaskType);
maxRetry = ComputeQualityAwareMaxRetry(maxRetry, recentTaskRetryQuality, taskPolicy.TaskType);
// 플랜 모드 설정
var planMode = llm.PlanMode ?? "off"; // 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)
{
@@ -2096,6 +2096,11 @@ public partial class AgentLoopService
return string.Equals(activeTab, "Code", StringComparison.OrdinalIgnoreCase) ? "feature" : "general";
}
internal static string ResolveEffectivePlanMode(string? configuredPlanMode, string? activeTab, string? taskType)
{
return "off";
}
private static void InjectTaskTypeGuidance(List<ChatMessage> messages, TaskTypePolicy taskPolicy)
{
if (messages.Any(m => m.Role == "user" && m.Content.StartsWith("[System:TaskType]", StringComparison.OrdinalIgnoreCase)))
@@ -3614,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",

View File

@@ -1327,31 +1327,82 @@ public partial class AgentLoopService
return;
documentPlanCalled = true;
var po = result.Output;
var po = result.Output ?? string.Empty;
var pm = System.Text.RegularExpressions.Regex.Match(po, @"path:\s*""([^""]+)""");
if (pm.Success) documentPlanPath = pm.Groups[1].Value;
var tm = System.Text.RegularExpressions.Regex.Match(po, @"title:\s*""([^""]+)""");
if (tm.Success) documentPlanTitle = tm.Groups[1].Value;
var bs = po.IndexOf("--- body ?쒖옉 ---", StringComparison.Ordinal);
var be = po.IndexOf("--- body ??---", StringComparison.Ordinal);
if (bs >= 0 && be > bs)
documentPlanScaffold = po[(bs + "--- body ?쒖옉 ---".Length)..be].Trim();
documentPlanScaffold = ExtractDocumentPlanScaffold(po);
if (!result.Output.Contains("利됱떆 ?ㅽ뻾:", StringComparison.Ordinal))
if (!ContainsDocumentPlanFollowUpInstruction(po))
return;
var toolHint = result.Output.Contains("html_create", StringComparison.OrdinalIgnoreCase) ? "html_create" :
result.Output.Contains("document_assemble", StringComparison.OrdinalIgnoreCase) ? "document_assemble" :
result.Output.Contains("file_write", StringComparison.OrdinalIgnoreCase) ? "file_write" : "html_create";
var toolHint = ResolveDocumentPlanFollowUpTool(po);
messages.Add(new ChatMessage
{
Role = "user",
Content = $"document_plan???꾨즺?섏뿀?듬땲?? " +
$"??寃곌낵??body/sections??[?댁슜...] 遺€遺꾩쓣 ?ㅼ젣 ?곸꽭 ?댁슜?쇰줈 紐⑤몢 梨꾩썙??" +
$"{toolHint} ?꾧뎄瑜?吏€湲?利됱떆 ?몄텧?섏꽭?? " +
$"媛??뱀뀡留덈떎 諛섎뱶??異⑸텇???댁슜???묒꽦?섍퀬, ?ㅻ챸 ?놁씠 ?꾧뎄瑜?諛붾줈 ?몄텧?섏꽭??"
Content =
"document_plan이 완료되었습니다. " +
"방금 생성된 골격의 [내용...] 자리와 각 섹션 내용을 실제 상세 본문으로 모두 채운 뒤 " +
$"{toolHint} 도구를 지금 즉시 호출하세요. " +
"설명만 하지 말고 실제 문서 생성 도구 호출로 바로 이어가세요."
});
EmitEvent(AgentEventType.Thinking, "", $"臾몄꽌 媛쒖슂 ?꾩꽦 ??{toolHint} ?몄텧 以?..");
EmitEvent(AgentEventType.Thinking, "", $"문서 개요 완료 · {toolHint} 실행 유도");
}
private static string? ExtractDocumentPlanScaffold(string output)
{
if (string.IsNullOrWhiteSpace(output))
return null;
var markers = new (string Start, string End)[]
{
("--- body 시작 ---", "--- body 끝 ---"),
("--- body start ---", "--- body end ---"),
("<!-- body start marker -->", "<!-- body end marker -->"),
};
foreach (var (startMarker, endMarker) in markers)
{
var start = output.IndexOf(startMarker, StringComparison.OrdinalIgnoreCase);
if (start < 0)
continue;
var contentStart = start + startMarker.Length;
var end = output.IndexOf(endMarker, contentStart, StringComparison.OrdinalIgnoreCase);
if (end <= contentStart)
continue;
var scaffold = output[contentStart..end].Trim();
if (!string.IsNullOrWhiteSpace(scaffold))
return scaffold;
}
return null;
}
private static bool ContainsDocumentPlanFollowUpInstruction(string output)
{
if (string.IsNullOrWhiteSpace(output))
return false;
return output.Contains("즉시 실행", StringComparison.OrdinalIgnoreCase)
|| output.Contains("immediate next step", StringComparison.OrdinalIgnoreCase)
|| output.Contains("call html_create", StringComparison.OrdinalIgnoreCase)
|| output.Contains("call document_assemble", StringComparison.OrdinalIgnoreCase);
}
private static string ResolveDocumentPlanFollowUpTool(string output)
{
if (output.Contains("document_assemble", StringComparison.OrdinalIgnoreCase))
return "document_assemble";
if (output.Contains("docx_create", StringComparison.OrdinalIgnoreCase))
return "docx_create";
if (output.Contains("markdown_create", StringComparison.OrdinalIgnoreCase))
return "markdown_create";
if (output.Contains("file_write", StringComparison.OrdinalIgnoreCase))
return "file_write";
return "html_create";
}
private void ApplyCodeQualityFollowUpTransition(

View File

@@ -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)
{

View 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"
=> "웹",
_ => "도구",
};
}
}

View File

@@ -0,0 +1,402 @@
using AxCopilot.Models;
using AxCopilot.Services;
namespace AxCopilot.Services.Agent;
/// <summary>
/// AX Agent execution-prep engine.
/// Inspired by the `claw-code` split between input preparation and session execution,
/// so the UI layer stops owning message assembly and final assistant commit logic.
/// </summary>
public sealed class AxAgentExecutionEngine
{
public sealed record PreparedTurn(List<ChatMessage> Messages);
public sealed record ExecutionMode(bool UseAgentLoop, bool UseStreamingTransport, string? TaskSystemPrompt);
public sealed record PreparedExecution(
ExecutionMode Mode,
IReadOnlyList<string> PromptStack,
List<ChatMessage> Messages);
public sealed record FinalizedContent(string Content, bool Cancelled, string? FailureReason);
public sealed record SessionMutationResult(
ChatConversation CurrentConversation,
ChatConversation UpdatedConversation);
public IReadOnlyList<string> BuildPromptStack(
string? conversationSystem,
string? slashSystem,
string? taskSystem = null)
{
var prompts = new List<string>();
if (!string.IsNullOrWhiteSpace(conversationSystem))
prompts.Add(conversationSystem.Trim());
if (!string.IsNullOrWhiteSpace(slashSystem))
prompts.Add(slashSystem.Trim());
if (!string.IsNullOrWhiteSpace(taskSystem))
prompts.Add(taskSystem.Trim());
return prompts;
}
public ExecutionMode ResolveExecutionMode(
string runTab,
bool streamingEnabled,
string resolvedService,
string? coworkSystemPrompt,
string? codeSystemPrompt)
{
if (string.Equals(runTab, "Cowork", StringComparison.OrdinalIgnoreCase))
return new ExecutionMode(true, false, coworkSystemPrompt);
if (string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase))
return new ExecutionMode(true, false, codeSystemPrompt);
return new ExecutionMode(false, false, null);
}
public PreparedExecution PrepareExecution(
ChatConversation conversation,
string runTab,
bool streamingEnabled,
string resolvedService,
string? conversationSystem,
string? slashSystem,
string? coworkSystemPrompt,
string? codeSystemPrompt,
string? fileContext = null,
IReadOnlyList<ImageAttachment>? images = null)
{
var mode = ResolveExecutionMode(
runTab,
streamingEnabled,
resolvedService,
coworkSystemPrompt,
codeSystemPrompt);
var promptStack = BuildPromptStack(
conversationSystem,
slashSystem,
mode.TaskSystemPrompt);
var preparedTurn = PrepareTurn(conversation, promptStack, fileContext, images);
return new PreparedExecution(mode, promptStack, preparedTurn.Messages);
}
public Task<string> ExecutePreparedAsync(
PreparedExecution prepared,
Func<IReadOnlyList<ChatMessage>, CancellationToken, Task<string>> agentLoopRunner,
Func<IReadOnlyList<ChatMessage>, CancellationToken, Task<string>> llmRunner,
CancellationToken cancellationToken)
{
return prepared.Mode.UseAgentLoop
? agentLoopRunner(prepared.Messages, cancellationToken)
: llmRunner(prepared.Messages, cancellationToken);
}
public PreparedTurn PrepareTurn(
ChatConversation conversation,
IEnumerable<string?> systemPrompts,
string? fileContext = null,
IReadOnlyList<ImageAttachment>? images = null)
{
var outbound = conversation.Messages
.Select(CloneMessage)
.ToList();
if (!string.IsNullOrWhiteSpace(fileContext))
{
var lastUserIndex = outbound.FindLastIndex(m => string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase));
if (lastUserIndex >= 0)
outbound[lastUserIndex].Content = (outbound[lastUserIndex].Content ?? string.Empty) + fileContext;
}
if (images is { Count: > 0 })
{
var lastUserIndex = outbound.FindLastIndex(m => string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase));
if (lastUserIndex >= 0)
outbound[lastUserIndex].Images = images.Select(CloneImage).ToList();
}
var promptList = systemPrompts
.Where(prompt => !string.IsNullOrWhiteSpace(prompt))
.Select(prompt => prompt!.Trim())
.ToList();
for (var i = promptList.Count - 1; i >= 0; i--)
outbound.Insert(0, new ChatMessage { Role = "system", Content = promptList[i] });
return new PreparedTurn(outbound);
}
public ChatMessage CommitAssistantMessage(
ChatSessionStateService? session,
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)
{
session.AppendMessage(tab, assistant, storage);
return assistant;
}
conversation.Messages.Add(assistant);
conversation.UpdatedAt = DateTime.Now;
return assistant;
}
public string FinalizeAssistantTurn(
ChatSessionStateService? session,
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, promptTokens, completionTokens, responseElapsedMs, metaRunId, storage);
return normalized;
}
public FinalizedContent FinalizeExecutionContentForUi(string? currentContent, Exception? error = null, bool cancelled = false)
{
if (cancelled)
{
var content = string.IsNullOrWhiteSpace(currentContent) ? "(취소됨)" : currentContent!;
return new FinalizedContent(content, true, "사용자가 작업을 중단했습니다.");
}
if (error != null)
return new FinalizedContent($"오류: {error.Message}", false, error.Message);
return new FinalizedContent(currentContent ?? string.Empty, false, null);
}
public string NormalizeAssistantContentForUi(
ChatConversation conversation,
string runTab,
string? content)
{
if (!string.IsNullOrWhiteSpace(content))
return content;
var latestEventSummary = conversation.ExecutionEvents?
.Where(evt => !string.IsNullOrWhiteSpace(evt.Summary))
.OrderByDescending(evt => evt.Timestamp)
.Select(evt => evt.Summary.Trim())
.FirstOrDefault();
if (!string.IsNullOrWhiteSpace(latestEventSummary))
{
return runTab switch
{
"Cowork" => $"코워크 작업이 완료되었습니다.\n\n{latestEventSummary}",
"Code" => $"코드 작업이 완료되었습니다.\n\n{latestEventSummary}",
_ => latestEventSummary,
};
}
return "(빈 응답)";
}
public FinalizedContent FinalizeExecutionContent(string? currentContent, Exception? error = null, bool cancelled = false)
{
if (cancelled)
{
var content = string.IsNullOrWhiteSpace(currentContent) ? "(취소됨)" : currentContent!;
return new FinalizedContent(content, true, "사용자가 작업을 중단했습니다.");
}
if (error != null)
{
return new FinalizedContent($"⚠ 오류: {error.Message}", false, error.Message);
}
return new FinalizedContent(currentContent ?? string.Empty, false, null);
}
public SessionMutationResult AppendExecutionEvent(
ChatSessionStateService session,
ChatStorageService storage,
ChatConversation? activeConversation,
string activeTab,
string targetTab,
AgentEvent evt)
{
return ApplyConversationMutation(
session,
storage,
activeConversation,
activeTab,
targetTab,
normalizedTarget => session.AppendExecutionEvent(normalizedTarget, evt, null));
}
public SessionMutationResult AppendAgentRun(
ChatSessionStateService session,
ChatStorageService storage,
ChatConversation? activeConversation,
string activeTab,
string targetTab,
AgentEvent evt,
string status,
string summary)
{
return ApplyConversationMutation(
session,
storage,
activeConversation,
activeTab,
targetTab,
normalizedTarget => session.AppendAgentRun(normalizedTarget, evt, status, summary, null));
}
public string NormalizeAssistantContent(
ChatConversation conversation,
string runTab,
string? content)
{
if (!string.IsNullOrWhiteSpace(content))
return content;
var latestEventSummary = conversation.ExecutionEvents?
.Where(evt => !string.IsNullOrWhiteSpace(evt.Summary))
.OrderByDescending(evt => evt.Timestamp)
.Select(evt => evt.Summary.Trim())
.FirstOrDefault();
if (!string.IsNullOrWhiteSpace(latestEventSummary))
{
return runTab switch
{
"Cowork" => $"코워크 작업이 완료되었습니다.\n\n{latestEventSummary}",
"Code" => $"코드 작업이 완료되었습니다.\n\n{latestEventSummary}",
_ => latestEventSummary,
};
}
return "(빈 응답)";
}
private static ChatMessage CloneMessage(ChatMessage source)
{
return new ChatMessage
{
Role = source.Role,
Content = source.Content,
Timestamp = source.Timestamp,
MetaRunId = source.MetaRunId,
AttachedFiles = source.AttachedFiles?.ToList(),
Images = source.Images?.Select(CloneImage).ToList(),
};
}
private static ImageAttachment CloneImage(ImageAttachment source)
{
return new ImageAttachment
{
Base64 = source.Base64,
MimeType = source.MimeType,
FileName = source.FileName,
};
}
private static SessionMutationResult ApplyConversationMutation(
ChatSessionStateService session,
ChatStorageService storage,
ChatConversation? activeConversation,
string activeTab,
string targetTab,
Func<string, ChatConversation> mutate)
{
var normalizedTarget = NormalizeTabName(targetTab);
var normalizedActive = NormalizeTabName(activeTab);
ChatConversation updatedConversation;
if (string.Equals(normalizedTarget, normalizedActive, StringComparison.OrdinalIgnoreCase))
{
session.CurrentConversation = updatedConversation = mutate(normalizedTarget);
return new SessionMutationResult(updatedConversation, updatedConversation);
}
var activeSnapshot = activeConversation;
var previousSessionConversation = session.CurrentConversation;
updatedConversation = mutate(normalizedTarget);
if (activeSnapshot != null
&& string.Equals(NormalizeTabName(activeSnapshot.Tab), normalizedActive, StringComparison.OrdinalIgnoreCase))
{
session.CurrentConversation = activeSnapshot;
return new SessionMutationResult(activeSnapshot, updatedConversation);
}
if (previousSessionConversation != null
&& string.Equals(NormalizeTabName(previousSessionConversation.Tab), normalizedActive, StringComparison.OrdinalIgnoreCase))
{
session.CurrentConversation = previousSessionConversation;
return new SessionMutationResult(previousSessionConversation, updatedConversation);
}
var activeId = session.GetConversationId(normalizedActive);
var restoredConversation = string.IsNullOrWhiteSpace(activeId)
? null
: storage.Load(activeId);
if (restoredConversation != null)
{
session.CurrentConversation = restoredConversation;
return new SessionMutationResult(restoredConversation, updatedConversation);
}
var fallbackConversation = session.LoadOrCreateConversation(normalizedActive, storage, GetFallbackSettings());
session.CurrentConversation = fallbackConversation;
return new SessionMutationResult(fallbackConversation, updatedConversation);
}
private static SettingsService GetFallbackSettings()
{
return (System.Windows.Application.Current as App)?.SettingsService
?? new SettingsService();
}
private static string NormalizeTabName(string? tab)
{
var normalized = (tab ?? "").Trim();
if (string.IsNullOrEmpty(normalized))
return "Chat";
if (normalized.Contains("코워크", StringComparison.OrdinalIgnoreCase))
return "Cowork";
var canonical = new string(normalized
.Where(char.IsLetterOrDigit)
.ToArray())
.ToLowerInvariant();
if (canonical is "cowork" or "coworkcode" or "coworkcodetab")
return "Cowork";
if (normalized.Contains("코드", StringComparison.OrdinalIgnoreCase)
|| canonical is "code" or "codetab")
return "Code";
return "Chat";
}
}

View File

@@ -155,7 +155,9 @@ public class DocumentPlannerTool : IAgentTool
output.AppendLine("(주석의 '활용 가능 요소'는 참고용이며, 내용에 맞게 다른 요소를 써도 됩니다)");
output.AppendLine();
output.AppendLine("--- body 시작 ---");
output.AppendLine("<!-- body start marker -->");
output.Append(bodySb);
output.AppendLine("<!-- body end marker -->");
output.AppendLine("--- body 끝 ---");
output.AppendLine();
output.AppendLine("⚠ html_create를 지금 즉시 호출하세요. 모든 섹션에 충분한 실제 내용을 작성하세요.");

View File

@@ -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 ("", "", "");
}
}

View File

@@ -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];
}
}

View File

@@ -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);
}
}

View File

@@ -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",

View File

@@ -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>

View File

@@ -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());

View 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 서버 연결 상태와 도구 등록 상태를 다시 확인하세요.",
_ => "같은 요청을 재시도하기 전에 원인 메시지를 먼저 확인하세요.",
};
}
}

View File

@@ -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 = llm.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>();

View File

@@ -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()

View 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);
}

View File

@@ -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))

View File

@@ -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))

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -160,12 +160,19 @@ public class SettingsViewModel : INotifyPropertyChanged
private bool _enableRecent;
private bool _enableActionMode;
private bool _closeOnFocusLost;
private bool _rememberPosition;
private bool _showPrefixBadge;
private bool _enableIconAnimation;
private bool _enableRainbowGlow;
private bool _enableSelectionGlow;
private bool _enableRandomPlaceholder;
private bool _showLauncherBorder;
private bool _showWidgetPerf;
private bool _showWidgetPomo;
private bool _showWidgetNote;
private bool _showWidgetWeather;
private bool _showWidgetCalendar;
private bool _showWidgetBattery;
private bool _shortcutHelpUseThemeColor;
// LLM 공통 설정
@@ -363,13 +370,6 @@ public class SettingsViewModel : INotifyPropertyChanged
set { _agentDecisionLevel = value; OnPropertyChanged(); }
}
private string _planMode = "off";
public string PlanMode
{
get => _planMode;
set { _planMode = value; OnPropertyChanged(); }
}
private bool _enableMultiPassDocument;
public bool EnableMultiPassDocument
{
@@ -755,12 +755,54 @@ public class SettingsViewModel : INotifyPropertyChanged
set { _closeOnFocusLost = value; OnPropertyChanged(); }
}
public bool RememberPosition
{
get => _rememberPosition;
set { _rememberPosition = value; OnPropertyChanged(); }
}
public bool ShowPrefixBadge
{
get => _showPrefixBadge;
set { _showPrefixBadge = value; OnPropertyChanged(); }
}
public bool ShowWidgetPerf
{
get => _showWidgetPerf;
set { _showWidgetPerf = value; OnPropertyChanged(); }
}
public bool ShowWidgetPomo
{
get => _showWidgetPomo;
set { _showWidgetPomo = value; OnPropertyChanged(); }
}
public bool ShowWidgetNote
{
get => _showWidgetNote;
set { _showWidgetNote = value; OnPropertyChanged(); }
}
public bool ShowWidgetWeather
{
get => _showWidgetWeather;
set { _showWidgetWeather = value; OnPropertyChanged(); }
}
public bool ShowWidgetCalendar
{
get => _showWidgetCalendar;
set { _showWidgetCalendar = value; OnPropertyChanged(); }
}
public bool ShowWidgetBattery
{
get => _showWidgetBattery;
set { _showWidgetBattery = value; OnPropertyChanged(); }
}
public bool EnableIconAnimation
{
get => _enableIconAnimation;
@@ -1030,12 +1072,19 @@ public class SettingsViewModel : INotifyPropertyChanged
_enableRecent = s.Launcher.EnableRecent;
_enableActionMode = s.Launcher.EnableActionMode;
_closeOnFocusLost = s.Launcher.CloseOnFocusLost;
_rememberPosition = s.Launcher.RememberPosition;
_showPrefixBadge = s.Launcher.ShowPrefixBadge;
_enableIconAnimation = s.Launcher.EnableIconAnimation;
_enableRainbowGlow = s.Launcher.EnableRainbowGlow;
_enableSelectionGlow = s.Launcher.EnableSelectionGlow;
_enableRandomPlaceholder = s.Launcher.EnableRandomPlaceholder;
_showLauncherBorder = s.Launcher.ShowLauncherBorder;
_showWidgetPerf = s.Launcher.ShowWidgetPerf;
_showWidgetPomo = s.Launcher.ShowWidgetPomo;
_showWidgetNote = s.Launcher.ShowWidgetNote;
_showWidgetWeather = s.Launcher.ShowWidgetWeather;
_showWidgetCalendar = s.Launcher.ShowWidgetCalendar;
_showWidgetBattery = s.Launcher.ShowWidgetBattery;
_shortcutHelpUseThemeColor = s.Launcher.ShortcutHelpUseThemeColor;
_enableTextAction = s.Launcher.EnableTextAction;
// v1.7.1: 파일 대화상자 통합 기본값을 false로 변경 (브라우저 업로드 오작동 방지)
@@ -1082,7 +1131,6 @@ public class SettingsViewModel : INotifyPropertyChanged
_planDiffSeverityMediumRatioPercent = llm.PlanDiffSeverityMediumRatioPercent > 0 ? Math.Clamp(llm.PlanDiffSeverityMediumRatioPercent, 1, 100) : 25;
_planDiffSeverityHighRatioPercent = llm.PlanDiffSeverityHighRatioPercent > 0 ? Math.Clamp(llm.PlanDiffSeverityHighRatioPercent, 1, 100) : 60;
_agentDecisionLevel = llm.AgentDecisionLevel;
_planMode = string.IsNullOrEmpty(llm.PlanMode) ? "off" : llm.PlanMode;
_enableMultiPassDocument = llm.EnableMultiPassDocument;
_enableCoworkVerification = llm.EnableCoworkVerification;
_enableFilePathHighlight = llm.EnableFilePathHighlight;
@@ -1476,12 +1524,19 @@ public class SettingsViewModel : INotifyPropertyChanged
s.Launcher.EnableRecent = _enableRecent;
s.Launcher.EnableActionMode = _enableActionMode;
s.Launcher.CloseOnFocusLost = _closeOnFocusLost;
s.Launcher.RememberPosition = _rememberPosition;
s.Launcher.ShowPrefixBadge = _showPrefixBadge;
s.Launcher.EnableIconAnimation = _enableIconAnimation;
s.Launcher.EnableRainbowGlow = _enableRainbowGlow;
s.Launcher.EnableSelectionGlow = _enableSelectionGlow;
s.Launcher.EnableRandomPlaceholder = _enableRandomPlaceholder;
s.Launcher.ShowLauncherBorder = _showLauncherBorder;
s.Launcher.ShowWidgetPerf = _showWidgetPerf;
s.Launcher.ShowWidgetPomo = _showWidgetPomo;
s.Launcher.ShowWidgetNote = _showWidgetNote;
s.Launcher.ShowWidgetWeather = _showWidgetWeather;
s.Launcher.ShowWidgetCalendar = _showWidgetCalendar;
s.Launcher.ShowWidgetBattery = _showWidgetBattery;
s.Launcher.ShortcutHelpUseThemeColor = _shortcutHelpUseThemeColor;
s.Launcher.EnableTextAction = _enableTextAction;
s.Launcher.EnableFileDialogIntegration = _enableFileDialogIntegration;
@@ -1517,7 +1572,6 @@ public class SettingsViewModel : INotifyPropertyChanged
s.Llm.PlanDiffSeverityMediumRatioPercent = _planDiffSeverityMediumRatioPercent;
s.Llm.PlanDiffSeverityHighRatioPercent = _planDiffSeverityHighRatioPercent;
s.Llm.AgentDecisionLevel = _agentDecisionLevel;
s.Llm.PlanMode = _planMode;
s.Llm.EnableMultiPassDocument = _enableMultiPassDocument;
s.Llm.EnableCoworkVerification = _enableCoworkVerification;
s.Llm.EnableFilePathHighlight = _enableFilePathHighlight;

View 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;
}
}

View File

@@ -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">

View File

@@ -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)

View File

@@ -0,0 +1,624 @@
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 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;
stack.Children.Add(new TextBlock
{
Text = clipped,
FontSize = 8.15,
Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(11, 1, 0, 0),
});
}
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");
}
}

View 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;
}

View 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;
}
}
}

View 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;
}
}
}

View 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);
}
}

View File

@@ -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;
}
}

View 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();
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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();
}
}

View 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;
}
}
}
}
}

View 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;
}
}
}

View 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);
}
}

View 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();
}
}

View 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;
}
}

View 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 }
});
}
}

View 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;
}
}

View 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;
}
}

View 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
};
}
}

View 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;
}
}

View 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();
}
}

View 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;
}

View 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;
}
}

View 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

View File

@@ -8,24 +8,17 @@ namespace AxCopilot.Views;
public partial class LauncherWindow
{
private DispatcherTimer? _widgetTimer;
private static readonly SolidColorBrush DotOnline = new(Color.FromRgb(0x10, 0xB9, 0x81));
private static readonly SolidColorBrush DotOffline = new(Color.FromRgb(0x9E, 0x9E, 0x9E));
private int _widgetBatteryTick;
private int _widgetWeatherTick;
internal void StartWidgetUpdates()
{
var settings = CurrentApp?.SettingsService?.Settings;
PerformanceMonitorService.Instance.StartPolling();
ServerStatusService.Instance.Start(settings);
PomodoroService.Instance.StateChanged -= OnPomoStateChanged;
PomodoroService.Instance.StateChanged += OnPomoStateChanged;
ServerStatusService.Instance.StatusChanged -= OnServerStatusChanged;
ServerStatusService.Instance.StatusChanged += OnServerStatusChanged;
_vm.UpdateWidgets();
UpdateServerDots();
UpdateWidgetVisibility();
UpdateBatteryWidget();
_ = RefreshWeatherAsync();
@@ -38,7 +31,7 @@ public partial class LauncherWindow
_widgetTimer.Tick += (_, _) =>
{
_vm.UpdateWidgets();
UpdateServerDots();
UpdateWidgetVisibility();
if (_vm.Widget_PerfText.Length > 0 && _widgetBatteryTick++ % 30 == 0)
UpdateBatteryWidget();
if (_widgetWeatherTick++ % 120 == 0)
@@ -55,7 +48,6 @@ public partial class LauncherWindow
_widgetTimer?.Stop();
PerformanceMonitorService.Instance.StopPolling();
PomodoroService.Instance.StateChanged -= OnPomoStateChanged;
ServerStatusService.Instance.StatusChanged -= OnServerStatusChanged;
}
private void OnPomoStateChanged(object? sender, EventArgs e)
@@ -64,23 +56,10 @@ public partial class LauncherWindow
{
_vm.UpdateWidgets();
UpdatePomoWidgetStyle();
UpdateWidgetVisibility();
});
}
private void OnServerStatusChanged(object? sender, EventArgs e)
=> Dispatcher.InvokeAsync(UpdateServerDots);
private void UpdateServerDots()
{
var server = ServerStatusService.Instance;
if (OllamaStatusDot != null)
OllamaStatusDot.Fill = server.OllamaOnline ? DotOnline : DotOffline;
if (LlmStatusDot != null)
LlmStatusDot.Fill = server.LlmOnline ? DotOnline : DotOffline;
if (McpStatusDot != null)
McpStatusDot.Fill = server.McpOnline ? DotOnline : DotOffline;
}
private void UpdatePomoWidgetStyle()
{
if (WgtPomo == null)
@@ -92,6 +71,37 @@ public partial class LauncherWindow
: new SolidColorBrush(Color.FromArgb(0x0D, 0xF5, 0x9E, 0x0B));
}
private void UpdateWidgetVisibility()
{
var launcher = CurrentApp?.SettingsService?.Settings?.Launcher;
if (launcher == null)
return;
if (WgtPerf != null)
WgtPerf.Visibility = launcher.ShowWidgetPerf ? Visibility.Visible : Visibility.Collapsed;
if (WgtPomo != null)
WgtPomo.Visibility = launcher.ShowWidgetPomo ? Visibility.Visible : Visibility.Collapsed;
if (WgtNote != null)
WgtNote.Visibility = launcher.ShowWidgetNote ? Visibility.Visible : Visibility.Collapsed;
if (WgtWeather != null)
WgtWeather.Visibility = launcher.ShowWidgetWeather ? Visibility.Visible : Visibility.Collapsed;
if (WgtCal != null)
WgtCal.Visibility = launcher.ShowWidgetCalendar ? Visibility.Visible : Visibility.Collapsed;
if (WgtBattery != null)
WgtBattery.Visibility = launcher.ShowWidgetBattery && _vm.Widget_BatteryVisible ? Visibility.Visible : Visibility.Collapsed;
var hasAny =
launcher.ShowWidgetPerf ||
launcher.ShowWidgetPomo ||
launcher.ShowWidgetNote ||
launcher.ShowWidgetWeather ||
launcher.ShowWidgetCalendar ||
(launcher.ShowWidgetBattery && _vm.Widget_BatteryVisible);
if (WidgetBar != null)
WidgetBar.Visibility = hasAny ? Visibility.Visible : Visibility.Collapsed;
}
private void WgtPerf_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
_vm.InputText = "info ";
@@ -110,14 +120,6 @@ public partial class LauncherWindow
InputBox?.Focus();
}
private void WgtServer_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
var settings = CurrentApp?.SettingsService?.Settings;
ServerStatusService.Instance.Refresh(settings);
_vm.InputText = "port";
InputBox?.Focus();
}
private void WgtWeather_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
var internalMode = CurrentApp?.SettingsService?.Settings.InternalModeEnabled ?? true;
@@ -153,7 +155,7 @@ public partial class LauncherWindow
}
catch (Exception ex)
{
LogService.Warn($"달력 열기 실패: {ex.Message}");
LogService.Warn($"일정 열기 실패: {ex.Message}");
}
}
}
@@ -179,6 +181,7 @@ public partial class LauncherWindow
if (pct > 1.0f || pct < 0f)
{
_vm.Widget_BatteryVisible = false;
UpdateWidgetVisibility();
return;
}
@@ -192,11 +195,13 @@ public partial class LauncherWindow
: pctInt >= 50 ? "\uEBA3"
: pctInt >= 25 ? "\uEBA1"
: "\uEBA0";
UpdateWidgetVisibility();
}
catch (Exception ex)
{
LogService.Warn($"배터리 위젯 갱신 실패: {ex.Message}");
LogService.Warn($"배터리 상태 갱신 실패: {ex.Message}");
_vm.Widget_BatteryVisible = false;
UpdateWidgetVisibility();
}
}
@@ -204,6 +209,10 @@ public partial class LauncherWindow
{
var internalMode = CurrentApp?.SettingsService?.Settings.InternalModeEnabled ?? true;
await WeatherWidgetService.RefreshAsync(internalMode);
await Dispatcher.InvokeAsync(() => { _vm.Widget_WeatherText = WeatherWidgetService.CachedText; });
await Dispatcher.InvokeAsync(() =>
{
_vm.Widget_WeatherText = WeatherWidgetService.CachedText;
UpdateWidgetVisibility();
});
}
}

View File

@@ -793,14 +793,13 @@
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="6"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="6"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border x:Name="WgtPerf" Grid.Column="0"
CornerRadius="5" Padding="8,5"
Background="#0D60A5FA"
Cursor="Hand"
Visibility="{Binding ShowWidgetPerf, Converter={StaticResource BoolToVisibilityConverter}}"
MouseLeftButtonUp="WgtPerf_Click">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE950;"
@@ -818,6 +817,7 @@
CornerRadius="5" Padding="8,5"
Background="#0DF59E0B"
Cursor="Hand"
Visibility="{Binding ShowWidgetPomo, Converter={StaticResource BoolToVisibilityConverter}}"
MouseLeftButtonUp="WgtPomo_Click">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE916;"
@@ -836,6 +836,7 @@
CornerRadius="5" Padding="8,5"
Background="#0D8B5CF6"
Cursor="Hand"
Visibility="{Binding ShowWidgetNote, Converter={StaticResource BoolToVisibilityConverter}}"
MouseLeftButtonUp="WgtNote_Click">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE70B;"
@@ -849,39 +850,6 @@
</StackPanel>
</Border>
<Border x:Name="WgtServer" Grid.Column="6"
CornerRadius="5" Padding="8,5"
Background="#0D10B981"
Cursor="Hand"
MouseLeftButtonUp="WgtServer_Click">
<StackPanel Orientation="Horizontal" x:Name="WgtServerContent">
<Ellipse x:Name="OllamaStatusDot"
Width="6" Height="6"
Fill="#9E9E9E"
VerticalAlignment="Center" Margin="0,0,3,0"/>
<TextBlock Text="Ollama"
FontSize="10"
Foreground="{DynamicResource SecondaryText}"
VerticalAlignment="Center" Margin="0,0,8,0"/>
<Ellipse x:Name="LlmStatusDot"
Width="6" Height="6"
Fill="#9E9E9E"
VerticalAlignment="Center" Margin="0,0,3,0"/>
<TextBlock Text="API"
FontSize="10"
Foreground="{DynamicResource SecondaryText}"
VerticalAlignment="Center" Margin="0,0,8,0"/>
<Ellipse x:Name="McpStatusDot"
Width="6" Height="6"
Fill="#9E9E9E"
VerticalAlignment="Center" Margin="0,0,3,0"/>
<TextBlock x:Name="McpNameText"
Text="{Binding Widget_McpName}"
FontSize="10"
Foreground="{DynamicResource SecondaryText}"
VerticalAlignment="Center"/>
</StackPanel>
</Border>
</Grid>
<Grid Margin="0,4,0,0">
@@ -897,6 +865,7 @@
CornerRadius="5" Padding="8,5"
Background="#0D3B82F6"
Cursor="Hand"
Visibility="{Binding ShowWidgetWeather, Converter={StaticResource BoolToVisibilityConverter}}"
MouseLeftButtonUp="WgtWeather_Click">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE708;"
@@ -916,6 +885,7 @@
CornerRadius="5" Padding="8,5"
Background="#0DEC4899"
Cursor="Hand"
Visibility="{Binding ShowWidgetCalendar, Converter={StaticResource BoolToVisibilityConverter}}"
MouseLeftButtonUp="WgtCal_Click">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE8BF;"
@@ -934,7 +904,7 @@
Background="#0D10B981"
Cursor="Hand"
MouseLeftButtonUp="WgtBattery_Click"
Visibility="{Binding Widget_BatteryVisible, Converter={StaticResource BoolToVisibilityConverter}}">
Visibility="Collapsed">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Widget_BatteryIcon}"
FontFamily="Segoe MDL2 Assets" FontSize="10"

View File

@@ -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;

View File

@@ -49,28 +49,37 @@ internal sealed class PlanViewerWindow : Window
private bool _isExecuting;
private readonly string _uiExpressionLevel;
public PlanViewerWindow()
public PlanViewerWindow(Window? owner = null)
{
if (owner != null)
{
Owner = owner;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
Resources.MergedDictionaries.Add(owner.Resources);
}
_uiExpressionLevel = ResolveUiExpressionLevel();
Width = 640;
Height = 520;
MinWidth = 480;
MinHeight = 360;
WindowStartupLocation = WindowStartupLocation.CenterScreen;
WindowStartupLocation = owner == null
? WindowStartupLocation.CenterScreen
: WindowStartupLocation.CenterOwner;
WindowStyle = WindowStyle.None;
AllowsTransparency = true;
Background = Brushes.Transparent;
ResizeMode = ResizeMode.CanResize; // WndProc로 직접 처리
ShowInTaskbar = false;
var bgBrush = Application.Current.TryFindResource("LauncherBackground") as Brush
var bgBrush = TryFindResource("LauncherBackground") as Brush
?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
var primaryText = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var accentBrush = TryFindResource("AccentColor") as Brush
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
var borderBrush = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var root = new Border
{
@@ -151,7 +160,7 @@ internal sealed class PlanViewerWindow : Window
mainGrid.Children.Add(_statusBar);
// ── 툴바: 모두 열기 / 모두 닫기 ──
var hoverBgTb = Application.Current.TryFindResource("ItemHoverBackground") as Brush
var hoverBgTb = TryFindResource("ItemHoverBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
var toolBar = new StackPanel
{
@@ -278,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;
@@ -295,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;

View File

@@ -121,10 +121,13 @@
<Setter Property="HasDropShadow" Value="False"/>
<Setter Property="MaxWidth" Value="320"/>
<Setter Property="Placement" Value="Bottom"/>
<Setter Property="Background" Value="#1F2440"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="TextElement.Foreground" Value="White"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToolTip">
<Border Background="{DynamicResource LauncherBackground}" CornerRadius="10" Padding="14,10"
<Border Background="{TemplateBinding Background}" CornerRadius="10" Padding="14,10"
BorderBrush="{DynamicResource AccentColor}" BorderThickness="1">
<ContentPresenter/>
</Border>
@@ -546,6 +549,7 @@
<!-- 탭 컨트롤 (좌측 사이드바) -->
<!-- ══════════════════════════════════════════════════════════════════ -->
<TabControl x:Name="MainSettingsTab" Grid.Row="0" TabStripPlacement="Left"
SelectionChanged="MainSettingsTab_SelectionChanged"
Background="Transparent" BorderThickness="0">
<TabControl.Template>
<ControlTemplate TargetType="TabControl">
@@ -656,58 +660,6 @@
<ScrollViewer x:Name="GeneralMainPanel" Grid.Row="2" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel>
<TextBlock Text="AI 기능" Style="{StaticResource SectionHeader}" Visibility="Collapsed"/>
<Border Style="{StaticResource SettingsRow}" Visibility="Collapsed">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Width="36" Height="36" CornerRadius="9"
Background="#8B2FC9" Margin="0,0,14,0" VerticalAlignment="Center">
<TextBlock Text="&#xE8BD;" FontFamily="Segoe MDL2 Assets" FontSize="17"
Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="AX Agent (AI 기능) 활성화" Style="{StaticResource RowLabel}"/>
<TextBlock Text="비활성화하면 ! 명령어가 차단되고 AX Agent 설정 메뉴가 숨겨집니다"
Style="{StaticResource RowHint}"/>
</StackPanel>
<CheckBox x:Name="AiEnabledToggle"
Grid.Column="2" Style="{StaticResource ToggleSwitch}"
HorizontalAlignment="Right" VerticalAlignment="Center"
Checked="AiEnabled_Changed" Unchecked="AiEnabled_Changed"/>
</Grid>
</Border>
<Border Style="{StaticResource SettingsRow}" Visibility="Collapsed">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="200"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Width="36" Height="36" CornerRadius="9"
Background="#2563EB" Margin="0,0,14,0" VerticalAlignment="Center">
<TextBlock Text="&#xE72E;" FontFamily="Segoe MDL2 Assets" FontSize="17"
Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="운영 모드 (사내/사외)" Style="{StaticResource RowLabel}"/>
<TextBlock Text="사내모드는 외부 검색/외부 HTTP 호출/외부 URL 열기를 차단합니다."
Style="{StaticResource RowHint}"/>
</StackPanel>
<ComboBox x:Name="OperationModeCombo"
Grid.Column="2"
Width="200" Height="34"
VerticalAlignment="Center"
SelectionChanged="OperationModeCombo_SelectionChanged">
<ComboBoxItem Tag="internal" Content="사내모드 (internal)"/>
<ComboBoxItem Tag="external" Content="사외모드 (external)"/>
</ComboBox>
</Grid>
</Border>
<TextBlock Text="단축키" Style="{StaticResource SectionHeader}"/>
<Border Style="{StaticResource SettingsRow}">
<Grid>
@@ -1155,6 +1107,20 @@
</Grid>
</TabItem>
<TabItem x:Name="AgentShortcutTabItem" Header="AX Agent" Tag="&#xE8BD;" Style="{StaticResource SideNavItem}">
<Grid>
<StackPanel VerticalAlignment="Top">
<TextBlock Text="AX Agent 내부 설정 바로가기" Style="{StaticResource SectionHeader}" Margin="2,8,0,8"/>
<Border Style="{StaticResource SettingsRow}">
<StackPanel>
<TextBlock Text="AX Agent 채팅창을 열고 내부 설정 오버레이를 바로 표시합니다."
Style="{StaticResource RowHint}"/>
</StackPanel>
</Border>
</StackPanel>
</Grid>
</TabItem>
<!-- ════════════════════════════════════════════════════════════ -->
<!-- 2. 테마 탭 (테마 선택 + 색상 편집 합침) -->
<!-- ════════════════════════════════════════════════════════════ -->
@@ -3020,6 +2986,19 @@
</Grid>
</Border>
<Border Style="{StaticResource SettingsRow}">
<Grid>
<StackPanel HorizontalAlignment="Left">
<TextBlock Style="{StaticResource RowLabel}" Text="마지막 위치 기억"/>
<TextBlock Style="{StaticResource RowHint}"
Text="AX Commander를 닫은 위치를 기억하고 다음에 같은 위치에서 엽니다."/>
</StackPanel>
<CheckBox Style="{StaticResource ToggleSwitch}"
HorizontalAlignment="Right" VerticalAlignment="Center"
IsChecked="{Binding RememberPosition, Mode=TwoWay}"/>
</Grid>
</Border>
<!-- 액션 모드 -->
<Border Style="{StaticResource SettingsRow}">
<Grid>
@@ -3065,6 +3044,74 @@
</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="CPU, RAM, 저장 공간 요약을 하단 바에 표시합니다."/>
</StackPanel>
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
IsChecked="{Binding ShowWidgetPerf, Mode=TwoWay}"/>
</Grid>
</Border>
<Border Style="{StaticResource SettingsRow}">
<Grid>
<StackPanel HorizontalAlignment="Left">
<TextBlock Style="{StaticResource RowLabel}" Text="포모도로 위젯 표시"/>
<TextBlock Style="{StaticResource RowHint}" Text="집중 타이머와 남은 시간을 런처 하단에 표시합니다."/>
</StackPanel>
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
IsChecked="{Binding ShowWidgetPomo, Mode=TwoWay}"/>
</Grid>
</Border>
<Border Style="{StaticResource SettingsRow}">
<Grid>
<StackPanel HorizontalAlignment="Left">
<TextBlock Style="{StaticResource RowLabel}" Text="메모 위젯 표시"/>
<TextBlock Style="{StaticResource RowHint}" Text="최근 메모 상태를 하단 바에서 빠르게 확인합니다."/>
</StackPanel>
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
IsChecked="{Binding ShowWidgetNote, Mode=TwoWay}"/>
</Grid>
</Border>
<Border Style="{StaticResource SettingsRow}">
<Grid>
<StackPanel HorizontalAlignment="Left">
<TextBlock Style="{StaticResource RowLabel}" Text="날씨 위젯 표시"/>
<TextBlock Style="{StaticResource RowHint}" Text="현재 날씨와 온도를 런처 하단에 표시합니다."/>
</StackPanel>
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
IsChecked="{Binding ShowWidgetWeather, Mode=TwoWay}"/>
</Grid>
</Border>
<Border Style="{StaticResource SettingsRow}">
<Grid>
<StackPanel HorizontalAlignment="Left">
<TextBlock Style="{StaticResource RowLabel}" Text="일정 위젯 표시"/>
<TextBlock Style="{StaticResource RowHint}" Text="다가오는 일정 또는 날짜 요약을 하단 바에서 보여줍니다."/>
</StackPanel>
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
IsChecked="{Binding ShowWidgetCalendar, Mode=TwoWay}"/>
</Grid>
</Border>
<Border Style="{StaticResource SettingsRow}">
<Grid>
<StackPanel HorizontalAlignment="Left">
<TextBlock Style="{StaticResource RowLabel}" Text="배터리 위젯 표시"/>
<TextBlock Style="{StaticResource RowHint}" Text="노트북 배터리 잔량과 충전 상태를 하단 바에 표시합니다."/>
</StackPanel>
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
IsChecked="{Binding ShowWidgetBattery, Mode=TwoWay}"/>
</Grid>
</Border>
</StackPanel>
</ScrollViewer>
@@ -3365,7 +3412,7 @@
<StackPanel x:Name="AgentCommonOverviewSection">
<TextBlock Style="{StaticResource SectionHeader}" Text="기본 상태"/>
<Border Style="{StaticResource AgentSettingsRow}">
<Border Style="{StaticResource AgentSettingsRow}" Visibility="Collapsed">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
@@ -3384,7 +3431,7 @@
Unchecked="AiEnabled_Changed"/>
</Grid>
</Border>
<Border Style="{StaticResource AgentSettingsRow}">
<Border Style="{StaticResource AgentSettingsRow}" Visibility="Collapsed">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
@@ -3409,7 +3456,7 @@
</WrapPanel>
</Grid>
</Border>
<Border Style="{StaticResource AgentSettingsRow}">
<Border Style="{StaticResource AgentSettingsRow}" Visibility="Collapsed">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
@@ -4606,7 +4653,7 @@
</StackPanel>
</Grid>
</Border>
<Border Style="{StaticResource AgentSettingsRow}">
<Border Style="{StaticResource AgentSettingsRow}" Visibility="Collapsed">
<Grid>
<StackPanel HorizontalAlignment="Left" Margin="0,0,180,0">
<StackPanel Orientation="Horizontal">
@@ -4650,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">
@@ -4953,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="품질 검증"/>
@@ -5107,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">
@@ -5669,17 +5647,6 @@
<Button Content="불러오기" Style="{StaticResource SecondaryButton}"
Click="ImportSettings_Click"
FontSize="10" Padding="8,3"/>
<Button Style="{StaticResource SecondaryButton}"
Click="BtnAgentShortcut_Click"
Margin="8,0,0,0"
FontSize="10"
Padding="10,4">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE8BD;" FontFamily="Segoe MDL2 Assets"
FontSize="11" Margin="0,0,5,0" VerticalAlignment="Center"/>
<TextBlock Text="AX Agent 설정" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</StackPanel>
<!-- 저장 / 취소 -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">

View File

@@ -34,6 +34,14 @@ public partial class SettingsWindow : Window
_revertCallback = revertCallback;
DataContext = vm;
if (MainSettingsTab != null && AgentTabItem != null && MainSettingsTab.Items.Contains(AgentTabItem))
MainSettingsTab.Items.Remove(AgentTabItem);
if (MainSettingsTab != null && AgentShortcutTabItem != null && MainSettingsTab.Items.Contains(AgentShortcutTabItem))
{
MainSettingsTab.Items.Remove(AgentShortcutTabItem);
MainSettingsTab.Items.Add(AgentShortcutTabItem);
}
vm.ThemePreviewRequested += (_, _) => _previewCallback(vm.SelectedThemeKey);
vm.SaveCompleted += (_, _) =>
{
@@ -60,8 +68,11 @@ public partial class SettingsWindow : Window
BuildQuoteCategoryCheckboxes();
BuildDockBarSettings();
BuildTextActionCommandsPanel();
MoveBlockSectionToEtc();
BuildServiceModelPanels();
if (HasLegacyAgentTab())
{
MoveBlockSectionToEtc();
BuildServiceModelPanels();
}
// 스킬이 아직 로드되지 않았으면 백그라운드에서 로드 후 UI 구성
var app = System.Windows.Application.Current as App;
@@ -76,8 +87,11 @@ public partial class SettingsWindow : Window
});
}
BuildToolRegistryPanel();
LoadAdvancedSettings();
if (HasLegacyAgentTab())
{
BuildToolRegistryPanel();
LoadAdvancedSettings();
}
RefreshStorageInfo();
// 개발자 모드는 저장된 설정 유지 (끄면 하위 기능 모두 비활성)
UpdateDevModeContentVisibility();
@@ -85,10 +99,14 @@ public partial class SettingsWindow : Window
ApplyAiEnabledState(app?.SettingsService?.Settings.AiEnabled ?? false, init: true);
ApplyOperationModeState(app?.SettingsService?.Settings.OperationMode);
InitializeDisplayModeUi();
SyncAgentSelectionCards();
if (HasLegacyAgentTab())
SyncAgentSelectionCards();
};
}
private bool HasLegacyAgentTab()
=> MainSettingsTab != null && AgentTabItem != null && MainSettingsTab.Items.Contains(AgentTabItem);
private void SyncAgentSelectionCards()
{
var service = (_vm.LlmService ?? "").Trim().ToLowerInvariant();
@@ -108,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;
@@ -139,8 +152,9 @@ public partial class SettingsWindow : Window
private void InitializeDisplayModeUi()
{
var app = System.Windows.Application.Current as App;
var saved = app?.SettingsService?.Settings?.Llm?.AgentUiExpressionLevel;
SetDisplayMode(saved ?? "balanced", persist: false);
if (app?.SettingsService?.Settings?.Llm != null)
app.SettingsService.Settings.Llm.AgentUiExpressionLevel = "rich";
SetDisplayMode("rich", persist: false);
}
private void DisplayMode_Checked(object sender, RoutedEventArgs e)
@@ -159,14 +173,7 @@ public partial class SettingsWindow : Window
private void AgentDisplayMode_Checked(object sender, RoutedEventArgs e)
{
if (_isDisplayModeSyncing) return;
var rb = sender as RadioButton;
var next = rb?.Name switch
{
"AgentDisplayModeRich" => "rich",
"AgentDisplayModeSimple" => "simple",
_ => "balanced",
};
SetDisplayMode(next, persist: true);
SetDisplayMode("rich", persist: true);
}
private void SetDisplayMode(string mode, bool persist)
@@ -195,7 +202,8 @@ public partial class SettingsWindow : Window
}
ApplyMainTabVisibility(mode);
ApplyAgentSubTabVisibility(mode);
if (HasLegacyAgentTab())
ApplyAgentSubTabVisibility(mode);
if (!persist) return;
var app = System.Windows.Application.Current as App;
@@ -210,11 +218,11 @@ public partial class SettingsWindow : Window
var simpleKeep = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"일반", "테마", "기능", "AX Agent"
"일반", "테마", "기능"
};
var balancedKeep = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"일반", "테마", "클립보드", "캡처", "시스템", "기능", "AX Agent"
"일반", "테마", "클립보드", "캡처", "시스템", "기능"
};
foreach (var item in MainSettingsTab.Items.OfType<TabItem>())
@@ -229,12 +237,6 @@ public partial class SettingsWindow : Window
if (string.Equals(header, "알림", StringComparison.OrdinalIgnoreCase))
visible = false;
if (string.Equals(header, "AX Agent", StringComparison.OrdinalIgnoreCase)
&& !((System.Windows.Application.Current as App)?.SettingsService?.Settings.AiEnabled ?? false))
{
visible = false;
}
item.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
}
@@ -2207,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)
@@ -2583,31 +2574,12 @@ public partial class SettingsWindow : Window
/// <summary>AI 기능 토글 상태를 UI와 설정에 반영합니다.</summary>
private void ApplyAiEnabledState(bool enabled, bool init = false)
{
// 토글 스위치 체크 상태 동기화 (init 시에는 이벤트 억제)
if (AiEnabledToggle != null && AiEnabledToggle.IsChecked != enabled)
{
AiEnabledToggle.IsChecked = enabled;
}
if (AgentAiEnabledToggle != null && AgentAiEnabledToggle.IsChecked != enabled)
{
AgentAiEnabledToggle.IsChecked = enabled;
}
if (AgentTabItem != null)
AgentTabItem.Visibility = enabled ? Visibility.Visible : Visibility.Collapsed;
ApplyMainTabVisibility(NormalizeDisplayMode((System.Windows.Application.Current as App)?.SettingsService?.Settings?.Llm?.AgentUiExpressionLevel));
}
public void SelectAgentSettingsTab()
{
if (AgentTabItem == null || MainSettingsTab == null || AgentTabItem.Visibility != Visibility.Visible)
return;
MainSettingsTab.SelectedItem = AgentTabItem;
if (AgentTabCommon != null)
AgentTabCommon.IsChecked = true;
AgentSubTab_Checked(this, new RoutedEventArgs());
Activate();
OpenAgentSettingsShortcut(closeAfterOpen: false);
}
private void BtnAgentSettingsBack_Click(object sender, RoutedEventArgs e)
@@ -2625,13 +2597,32 @@ public partial class SettingsWindow : Window
private void BtnAgentShortcut_Click(object sender, RoutedEventArgs e)
{
SelectAgentSettingsTab();
OpenAgentSettingsShortcut(closeAfterOpen: true);
}
private void MainSettingsTab_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (MainSettingsTab?.SelectedItem is not TabItem selected)
return;
if (!ReferenceEquals(selected, AgentShortcutTabItem))
return;
OpenAgentSettingsShortcut(closeAfterOpen: true);
}
private void OpenAgentSettingsShortcut(bool closeAfterOpen)
{
var app = System.Windows.Application.Current as App;
app?.OpenAgentSettingsInChat();
if (closeAfterOpen)
Close();
}
private void ApplyOperationModeState(string? mode)
{
var normalized = OperationModePolicy.Normalize(mode);
SyncOperationModeCombo(OperationModeCombo, normalized);
if (AgentOperationModeInternal != null) AgentOperationModeInternal.IsChecked = normalized == OperationModePolicy.InternalMode;
if (AgentOperationModeExternal != null) AgentOperationModeExternal.IsChecked = normalized == OperationModePolicy.ExternalMode;
}
@@ -2833,7 +2824,6 @@ public partial class SettingsWindow : Window
else
{
// 취소/실패 ? 토글 원상복구
if (AiEnabledToggle != null) AiEnabledToggle.IsChecked = false;
if (AgentAiEnabledToggle != null) AgentAiEnabledToggle.IsChecked = false;
}
}

View File

@@ -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++);