diff --git a/README.md b/README.md index 2647c1b..1cc681d 100644 --- a/README.md +++ b/README.md @@ -1172,3 +1172,6 @@ MIT License - [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를 더 작업 바 중심으로 정리`와 `회귀 프롬프트 세트의 개발 루틴 고정`이다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index a85dc96..faf0927 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4915,3 +4915,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - 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. diff --git a/docs/claw-code-parity-plan.md b/docs/claw-code-parity-plan.md index 84f44ea..dc40047 100644 --- a/docs/claw-code-parity-plan.md +++ b/docs/claw-code-parity-plan.md @@ -12,6 +12,8 @@ - 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. ## Preserved History (Summary) - Core loop guards and post-tool verification gates are already partially implemented. diff --git a/src/AxCopilot/Views/ChatWindow.PlanApprovalPresentation.cs b/src/AxCopilot/Views/ChatWindow.PlanApprovalPresentation.cs new file mode 100644 index 0000000..efa3179 --- /dev/null +++ b/src/AxCopilot/Views/ChatWindow.PlanApprovalPresentation.cs @@ -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, Task> CreatePlanDecisionCallback() + { + return async (planSummary, options) => + { + var tcs = new TaskCompletionSource(); + 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; + } + } +} diff --git a/src/AxCopilot/Views/ChatWindow.InlineInteractions.cs b/src/AxCopilot/Views/ChatWindow.UserAskPresentation.cs similarity index 61% rename from src/AxCopilot/Views/ChatWindow.InlineInteractions.cs rename to src/AxCopilot/Views/ChatWindow.UserAskPresentation.cs index a868298..521c194 100644 --- a/src/AxCopilot/Views/ChatWindow.InlineInteractions.cs +++ b/src/AxCopilot/Views/ChatWindow.UserAskPresentation.cs @@ -4,7 +4,6 @@ using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; -using AxCopilot.Services.Agent; namespace AxCopilot.Views; @@ -250,175 +249,4 @@ public partial class ChatWindow inputBox.Focus(); inputBox.CaretIndex = inputBox.Text.Length; } - - private Func, Task> CreatePlanDecisionCallback() - { - return async (planSummary, options) => - { - var tcs = new TaskCompletionSource(); - 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; - } - } }