From 52475b662803b82f0a4c7b146c998c11ffa7f22d Mon Sep 17 00:00:00 2001 From: lacvet Date: Sun, 5 Apr 2026 12:51:54 +0900 Subject: [PATCH] =?UTF-8?q?AX=20Agent=20=EC=8B=A4=ED=96=89=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20UI=20=EA=B0=B1=EC=8B=A0=EC=9D=84=20?= =?UTF-8?q?=EB=B0=B0=EC=B9=98=ED=98=95=EC=9C=BC=EB=A1=9C=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95=ED=95=B4=20Cowork=C2=B7Code=20=ED=9D=94=EB=93=A4?= =?UTF-8?q?=EB=A6=BC=EC=9D=84=20=EC=A4=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- README.md | 3 + docs/DEVELOPMENT.md | 3 + src/AxCopilot/Views/ChatWindow.xaml.cs | 94 +++++++++++++++----------- 3 files changed, 62 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index c369b12..1623656 100644 --- a/README.md +++ b/README.md @@ -749,6 +749,8 @@ ow + toggle 시각 언어로 통일했습니다. - 같은 파일의 대기열 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 실행 중 빠른 이벤트 연속 구간에서 상태바와 진행률, 파일 미리보기 쪽이 따로따로 즉시 흔들리던 체감을 더 줄이는 방향으로 정리했습니다. - 검증: `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) @@ -760,6 +762,7 @@ ow + toggle 시각 언어로 통일했습니다. - 업데이트: 2026-04-05 13:03 (KST) - 업데이트: 2026-04-05 13:12 (KST) - 업데이트: 2026-04-05 13:20 (KST) +- 업데이트: 2026-04-05 13:29 (KST) --- diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index ffe56d2..94d24a2 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4519,3 +4519,6 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - 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 구조에서 더 강화하는 단계입니다. diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index 05d95db..6c58091 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -86,6 +86,7 @@ public partial class ChatWindow : Window private readonly DispatcherTimer _executionHistoryRenderTimer; private readonly DispatcherTimer _taskSummaryRefreshTimer; private readonly DispatcherTimer _conversationPersistTimer; + private readonly DispatcherTimer _agentUiEventTimer; private CancellationTokenSource? _gitStatusRefreshCts; private int _displayedLength; // 현재 화면에 표시된 글자 수 private ResourceDictionary? _agentThemeDictionary; @@ -122,6 +123,7 @@ public partial class ChatWindow : Window private bool _pendingExecutionHistoryAutoScroll; private readonly Dictionary _pendingConversationPersists = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet _expandedDraftQueueTabs = new(StringComparer.OrdinalIgnoreCase); + private AgentEvent? _pendingAgentUiEvent; private void ApplyQuickActionVisual(Button button, bool active, string activeBg, string activeFg) { if (button?.Content is not string text) @@ -260,6 +262,12 @@ public partial class ChatWindow : Window _conversationPersistTimer.Stop(); FlushPendingConversationPersists(); }; + _agentUiEventTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(90) }; + _agentUiEventTimer.Tick += (_, _) => + { + _agentUiEventTimer.Stop(); + FlushPendingAgentUiEvent(); + }; KeyDown += ChatWindow_KeyDown; UpdateConversationFailureFilterUi(); @@ -8543,6 +8551,7 @@ public partial class ChatWindow : Window private void ResetStreamingUiState() { FlushPendingConversationPersists(); + FlushPendingAgentUiEvent(); _cursorTimer.Stop(); _elapsedTimer.Stop(); _typingTimer.Stop(); @@ -8550,6 +8559,7 @@ public partial class ChatWindow : Window _pendingExecutionHistoryAutoScroll = false; _taskSummaryRefreshTimer.Stop(); _conversationPersistTimer.Stop(); + _agentUiEventTimer.Stop(); HideStickyProgress(); StopRainbowGlow(); _activeStreamText = null; @@ -8628,6 +8638,51 @@ public partial class ChatWindow : Window _conversationPersistTimer.Start(); } + private void ScheduleAgentUiEvent(AgentEvent evt) + { + _pendingAgentUiEvent = evt; + _agentUiEventTimer.Stop(); + _agentUiEventTimer.Start(); + } + + private void FlushPendingAgentUiEvent() + { + var evt = _pendingAgentUiEvent; + _pendingAgentUiEvent = null; + if (evt == null) + return; + + UpdateStatusBar(evt); + UpdateAgentProgressBar(evt); + if (evt.StepCurrent > 0 && evt.StepTotal > 0) + UpdatePlanViewerStep(evt); + if (evt.Type == AgentEventType.Complete) + CompletePlanViewer(); + + if (evt.Success && !string.IsNullOrEmpty(evt.FilePath)) + RefreshFileTreeIfVisible(); + + if (evt.Type == AgentEventType.ToolResult && evt.ToolName == "suggest_actions" && evt.Success) + RenderSuggestActionChips(evt.Summary); + + if (evt.Success && !string.IsNullOrEmpty(evt.FilePath) && + (evt.Type == AgentEventType.ToolResult || evt.Type == AgentEventType.Complete) && + WriteToolNames.Contains(evt.ToolName)) + { + var autoPreview = _settings.Settings.Llm.AutoPreview; + if (autoPreview == "auto") + { + if (PreviewWindow.IsOpen) + PreviewWindow.RefreshIfOpen(evt.FilePath); + else + TryShowPreview(evt.FilePath); + + if (!PreviewWindow.IsOpen) + TryShowPreview(evt.FilePath); + } + } + } + private void PersistConversationSnapshot(string rememberTab, ChatConversation conversation, string failureLabel) { if (conversation == null || string.IsNullOrWhiteSpace(conversation.Id)) @@ -9063,8 +9118,6 @@ public partial class ChatWindow : Window && string.Equals(eventTab, _activeTab, StringComparison.OrdinalIgnoreCase)) ScheduleExecutionHistoryRender(autoScroll: true); - // 하단 상태바 업데이트 - UpdateStatusBar(evt); _appState.ApplyAgentEvent(evt); if (evt.Type == AgentEventType.Complete) AppendConversationAgentRun(evt, "completed", string.IsNullOrWhiteSpace(evt.Summary) ? "작업 완료" : evt.Summary, eventTab); @@ -9079,42 +9132,7 @@ public partial class ChatWindow : Window UpdateStatusTokens(_agentCumulativeInputTokens, _agentCumulativeOutputTokens); } - // 스티키 진행률 바 업데이트 - UpdateAgentProgressBar(evt); - - // 계획 뷰어 단계 갱신 - if (evt.StepCurrent > 0 && evt.StepTotal > 0) - UpdatePlanViewerStep(evt); - if (evt.Type == AgentEventType.Complete) - CompletePlanViewer(); - - // 파일 탐색기 자동 새로고침 - if (evt.Success && !string.IsNullOrEmpty(evt.FilePath)) - RefreshFileTreeIfVisible(); - - // suggest_actions 도구 결과 → 후속 작업 칩 표시 - if (evt.Type == AgentEventType.ToolResult && evt.ToolName == "suggest_actions" && evt.Success) - RenderSuggestActionChips(evt.Summary); - - // 파일 생성/수정 결과가 있으면 미리보기 자동 표시 또는 갱신 - if (evt.Success && !string.IsNullOrEmpty(evt.FilePath) && - (evt.Type == AgentEventType.ToolResult || evt.Type == AgentEventType.Complete) && - WriteToolNames.Contains(evt.ToolName)) - { - var autoPreview = _settings.Settings.Llm.AutoPreview; - if (autoPreview == "auto") - { - // 별도 창 미리보기: 이미 열린 파일이면 새로고침, 아니면 새 탭 추가 - if (PreviewWindow.IsOpen) - PreviewWindow.RefreshIfOpen(evt.FilePath); - else - TryShowPreview(evt.FilePath); - - // 새 파일이면 항상 표시 - if (!PreviewWindow.IsOpen) - TryShowPreview(evt.FilePath); - } - } + ScheduleAgentUiEvent(evt); ScheduleTaskSummaryRefresh(); }