From 35fbfc933d80ec68f2669a2ffa231ecca11f2cc3 Mon Sep 17 00:00:00 2001 From: lacvet Date: Sun, 5 Apr 2026 13:58:16 +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=20=EC=84=B8=EC=85=98=20=EB=B3=80=EC=9D=B4=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=97=94=EC=A7=84=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- README.md | 4 + docs/DEVELOPMENT.md | 2 + .../Services/Agent/AxAgentExecutionEngine.cs | 121 ++++++++++++++++++ src/AxCopilot/Views/ChatWindow.xaml.cs | 104 +++------------ 4 files changed, 147 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 31f5a4d..14880ce 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저 개발 참고: Claw Code 동등성 작업 추적 문서 `docs/claw-code-parity-plan.md` +- 업데이트: 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 문서를 갱신했습니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 5b4584b..3048c44 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -2,6 +2,8 @@ - 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. diff --git a/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs b/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs index 261de6d..2704660 100644 --- a/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs +++ b/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs @@ -16,6 +16,9 @@ public sealed class AxAgentExecutionEngine ExecutionMode Mode, IReadOnlyList PromptStack, List Messages); + public sealed record SessionMutationResult( + ChatConversation CurrentConversation, + ChatConversation UpdatedConversation); public IReadOnlyList BuildPromptStack( string? conversationSystem, @@ -159,6 +162,42 @@ public sealed class AxAgentExecutionEngine return normalized; } + 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, @@ -208,4 +247,86 @@ public sealed class AxAgentExecutionEngine FileName = source.FileName, }; } + + private static SessionMutationResult ApplyConversationMutation( + ChatSessionStateService session, + ChatStorageService storage, + ChatConversation? activeConversation, + string activeTab, + string targetTab, + Func 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"; + } } diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index 113a414..2206b16 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -9166,48 +9166,17 @@ public partial class ChatWindow : Window if (session == null) return; - var normalizedTarget = NormalizeTabName(targetTab); - var normalizedActive = NormalizeTabName(_activeTab); - ChatConversation updatedConversation; - if (string.Equals(normalizedTarget, normalizedActive, StringComparison.OrdinalIgnoreCase)) - { - _currentConversation = updatedConversation = session.AppendAgentRun(normalizedTarget, evt, status, summary, null); - ScheduleConversationPersist(updatedConversation); - return; - } - - var activeSnapshot = _currentConversation; - var previousSessionConversation = session.CurrentConversation; - updatedConversation = session.AppendAgentRun(normalizedTarget, evt, status, summary, null); - ScheduleConversationPersist(updatedConversation); - - if (activeSnapshot != null && string.Equals(NormalizeTabName(activeSnapshot.Tab), normalizedActive, StringComparison.OrdinalIgnoreCase)) - { - session.CurrentConversation = activeSnapshot; - _currentConversation = activeSnapshot; - } - else if (previousSessionConversation != null - && string.Equals(NormalizeTabName(previousSessionConversation.Tab), normalizedActive, StringComparison.OrdinalIgnoreCase)) - { - session.CurrentConversation = previousSessionConversation; - _currentConversation = previousSessionConversation; - } - else - { - var activeId = session.GetConversationId(normalizedActive); - var activeConv = string.IsNullOrWhiteSpace(activeId) ? null : _storage.Load(activeId); - if (activeConv != null) - { - session.CurrentConversation = activeConv; - _currentConversation = activeConv; - } - else - { - var fallback = session.LoadOrCreateConversation(normalizedActive, _storage, _settings); - session.CurrentConversation = fallback; - _currentConversation = fallback; - } - } + var result = _chatEngine.AppendAgentRun( + session, + _storage, + _currentConversation, + _activeTab, + targetTab, + evt, + status, + summary); + _currentConversation = result.CurrentConversation; + ScheduleConversationPersist(result.UpdatedConversation); } } @@ -9219,48 +9188,15 @@ public partial class ChatWindow : Window if (session == null) return; - var normalizedTarget = NormalizeTabName(targetTab); - var normalizedActive = NormalizeTabName(_activeTab); - ChatConversation updatedConversation; - if (string.Equals(normalizedTarget, normalizedActive, StringComparison.OrdinalIgnoreCase)) - { - _currentConversation = updatedConversation = session.AppendExecutionEvent(normalizedTarget, evt, null); - ScheduleConversationPersist(updatedConversation); - return; - } - - var activeSnapshot = _currentConversation; - var previousSessionConversation = session.CurrentConversation; - updatedConversation = session.AppendExecutionEvent(normalizedTarget, evt, null); - ScheduleConversationPersist(updatedConversation); - - if (activeSnapshot != null && string.Equals(NormalizeTabName(activeSnapshot.Tab), normalizedActive, StringComparison.OrdinalIgnoreCase)) - { - session.CurrentConversation = activeSnapshot; - _currentConversation = activeSnapshot; - } - else if (previousSessionConversation != null - && string.Equals(NormalizeTabName(previousSessionConversation.Tab), normalizedActive, StringComparison.OrdinalIgnoreCase)) - { - session.CurrentConversation = previousSessionConversation; - _currentConversation = previousSessionConversation; - } - else - { - var activeId = session.GetConversationId(normalizedActive); - var activeConv = string.IsNullOrWhiteSpace(activeId) ? null : _storage.Load(activeId); - if (activeConv != null) - { - session.CurrentConversation = activeConv; - _currentConversation = activeConv; - } - else - { - var fallback = session.LoadOrCreateConversation(normalizedActive, _storage, _settings); - session.CurrentConversation = fallback; - _currentConversation = fallback; - } - } + var result = _chatEngine.AppendExecutionEvent( + session, + _storage, + _currentConversation, + _activeTab, + targetTab, + evt); + _currentConversation = result.CurrentConversation; + ScheduleConversationPersist(result.UpdatedConversation); } }