의견 요청과 계획 승인 렌더를 분리해 메시지 타입 구조를 정리한다
Some checks failed
Release Gate / gate (push) Has been cancelled
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)
This commit is contained in:
@@ -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를 더 작업 바 중심으로 정리`와 `회귀 프롬프트 세트의 개발 루틴 고정`이다.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
183
src/AxCopilot/Views/ChatWindow.PlanApprovalPresentation.cs
Normal file
183
src/AxCopilot/Views/ChatWindow.PlanApprovalPresentation.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private Func<string, List<string>, Task<string?>> CreatePlanDecisionCallback()
|
||||
{
|
||||
return async (planSummary, options) =>
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string?>();
|
||||
var steps = TaskDecomposer.ExtractSteps(planSummary);
|
||||
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_pendingPlanSummary = planSummary;
|
||||
_pendingPlanSteps = steps.ToList();
|
||||
EnsurePlanViewerWindow();
|
||||
_planViewerWindow?.LoadPlan(planSummary, steps, tcs);
|
||||
ShowPlanButton(true);
|
||||
AddDecisionButtons(tcs, options);
|
||||
});
|
||||
|
||||
var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromMinutes(5)));
|
||||
if (completed != tcs.Task)
|
||||
{
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_planViewerWindow?.Hide();
|
||||
ResetPendingPlanPresentation();
|
||||
});
|
||||
return "취소";
|
||||
}
|
||||
|
||||
var result = await tcs.Task;
|
||||
var agentDecision = result;
|
||||
if (result == null)
|
||||
{
|
||||
agentDecision = _planViewerWindow?.BuildApprovedDecisionPayload(AgentLoopService.ApprovedPlanDecisionPrefix);
|
||||
}
|
||||
else if (!string.Equals(result, "취소", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(result, "확인", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.IsNullOrWhiteSpace(result))
|
||||
{
|
||||
agentDecision = $"수정 요청: {result.Trim()}";
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_planViewerWindow?.SwitchToExecutionMode();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_planViewerWindow?.Hide();
|
||||
ResetPendingPlanPresentation();
|
||||
});
|
||||
}
|
||||
|
||||
return agentDecision;
|
||||
};
|
||||
}
|
||||
|
||||
private void EnsurePlanViewerWindow()
|
||||
{
|
||||
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
|
||||
return;
|
||||
|
||||
_planViewerWindow = new PlanViewerWindow(this);
|
||||
_planViewerWindow.Closing += (_, e) =>
|
||||
{
|
||||
e.Cancel = true;
|
||||
_planViewerWindow.Hide();
|
||||
};
|
||||
}
|
||||
|
||||
private void ShowPlanButton(bool show)
|
||||
{
|
||||
if (!show)
|
||||
{
|
||||
for (int i = MoodIconPanel.Children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (MoodIconPanel.Children[i] is Border b && b.Tag?.ToString() == "PlanBtn")
|
||||
{
|
||||
if (i > 0 && MoodIconPanel.Children[i - 1] is Border sep && sep.Tag?.ToString() == "PlanSep")
|
||||
MoodIconPanel.Children.RemoveAt(i - 1);
|
||||
if (i < MoodIconPanel.Children.Count)
|
||||
MoodIconPanel.Children.RemoveAt(Math.Min(i, MoodIconPanel.Children.Count - 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var child in MoodIconPanel.Children)
|
||||
{
|
||||
if (child is Border b && b.Tag?.ToString() == "PlanBtn")
|
||||
return;
|
||||
}
|
||||
|
||||
var separator = new Border
|
||||
{
|
||||
Width = 1,
|
||||
Height = 18,
|
||||
Background = TryFindResource("SeparatorColor") as Brush ?? Brushes.Gray,
|
||||
Margin = new Thickness(4, 0, 4, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Tag = "PlanSep",
|
||||
};
|
||||
MoodIconPanel.Children.Add(separator);
|
||||
|
||||
var planBtn = CreateFolderBarButton("\uE9D2", "계획", "실행 계획 보기", "#10B981");
|
||||
planBtn.Tag = "PlanBtn";
|
||||
planBtn.MouseLeftButtonUp += (_, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
if (string.IsNullOrWhiteSpace(_pendingPlanSummary) && _pendingPlanSteps.Count == 0)
|
||||
return;
|
||||
|
||||
EnsurePlanViewerWindow();
|
||||
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_planViewerWindow.PlanText)
|
||||
|| _planViewerWindow.PlanText != (_pendingPlanSummary ?? string.Empty)
|
||||
|| !_planViewerWindow.Steps.SequenceEqual(_pendingPlanSteps))
|
||||
{
|
||||
_planViewerWindow.LoadPlanPreview(_pendingPlanSummary ?? "", _pendingPlanSteps);
|
||||
}
|
||||
_planViewerWindow.Show();
|
||||
_planViewerWindow.Activate();
|
||||
}
|
||||
};
|
||||
MoodIconPanel.Children.Add(planBtn);
|
||||
}
|
||||
|
||||
private void UpdatePlanViewerStep(AgentEvent evt)
|
||||
{
|
||||
if (_planViewerWindow == null || !IsWindowAlive(_planViewerWindow))
|
||||
return;
|
||||
|
||||
if (evt.StepCurrent > 0)
|
||||
_planViewerWindow.UpdateCurrentStep(evt.StepCurrent - 1);
|
||||
}
|
||||
|
||||
private void CompletePlanViewer()
|
||||
{
|
||||
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
|
||||
_planViewerWindow.MarkComplete();
|
||||
ResetPendingPlanPresentation();
|
||||
}
|
||||
|
||||
private void ResetPendingPlanPresentation()
|
||||
{
|
||||
_pendingPlanSummary = null;
|
||||
_pendingPlanSteps.Clear();
|
||||
ShowPlanButton(false);
|
||||
}
|
||||
|
||||
private static bool IsWindowAlive(Window? w)
|
||||
{
|
||||
if (w == null)
|
||||
return false;
|
||||
try
|
||||
{
|
||||
var _ = w.IsVisible;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user