From fe843fb314886a1b84e7283e05a9b14ecd23e8e0 Mon Sep 17 00:00:00 2001 From: lacvet Date: Tue, 7 Apr 2026 00:55:53 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A9=94=EB=AA=A8=EB=A6=AC=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=ED=8C=9D=EC=97=85=EA=B3=BC=20include=20=EA=B0=90?= =?UTF-8?q?=EC=82=AC=20=EC=9A=94=EC=95=BD=20UX=20=EA=B3=A0=EB=8F=84?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cowork/Code 하단 메모리 칩에서 적용 규칙과 최근 include 감사 이력을 팝업으로 확인 가능하게 개선 - 설정 메모리 개요에 최근 include 감사 요약을 추가해 메모리 계층과 감사 상태를 함께 점검 가능하도록 정리 - AX Agent 메모리 구조 고도화 마지막 UX 보강 반영 및 Release 빌드 경고/오류 0 검증 --- README.md | 3 + docs/DEVELOPMENT.md | 11 + .../Views/ChatWindow.FooterPresentation.cs | 259 +++++++++++++++++- src/AxCopilot/Views/ChatWindow.xaml | 1 + src/AxCopilot/Views/SettingsWindow.xaml | 5 + src/AxCopilot/Views/SettingsWindow.xaml.cs | 43 ++- 6 files changed, 299 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 5206fbe..25de999 100644 --- a/README.md +++ b/README.md @@ -1420,3 +1420,6 @@ MIT License 업데이트: 2026-04-07 01:15 (KST) - AX Agent 메모리 구조를 추가 강화했습니다. `@include` 확장 시도는 이제 감사 로그에 `MemoryInclude` 항목으로 남고, Cowork/Code 하단 폴더 바에 현재 적용 중인 계층형 메모리/학습 메모리 상태가 요약 표시됩니다. +- 업데이트: 2026-04-07 01:26 (KST) + - Cowork/Code 하단 메모리 칩을 눌렀을 때 `적용 중 규칙`과 `최근 include 감사`를 바로 확인할 수 있는 상세 팝업을 추가했습니다. 이제 메모리 계층이 실제로 어떻게 적용되고 있는지 채팅 하단에서 바로 추적할 수 있습니다. + - 설정의 메모리 개요에도 `최근 include 감사` 요약을 추가해, 메모리 규칙 상태와 include 시도 결과를 같은 화면에서 함께 점검할 수 있게 했습니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index e555e22..2f20a03 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -5232,3 +5232,14 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - `AgentMemoryService`에서 `@include` 시도 성공/차단을 감사 로그(`MemoryInclude`)로 기록 - `ChatWindow` 하단 폴더 바에 메모리 상태 요약(`메모리 n · 학습 n`) 추가 - 설정의 `외부 메모리 include 허용` 안내 문구를 감사 로그 기준으로 갱신 + +## 2026-04-07 01:26 (KST) + +- [ChatWindow.FooterPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.FooterPresentation.cs) + - Cowork/Code 하단 메모리 칩 클릭 시 여는 상세 팝업을 추가했다. + - 팝업은 `적용 중 규칙`, `최근 include 감사`를 같은 시각 언어로 묶어 보여주고, 현재 include 정책과 학습 메모리 개수도 함께 표시한다. + - include 감사 기록은 오늘 로그 중 최근 5건을 `허용/차단`, 시각, 요청 파라미터, 결과 기준으로 요약해 보여준다. +- [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml) + - 메모리 개요 row에 `TxtMemoryOverviewAudit` 영역을 추가해 최근 include 감사 상태를 설정 화면에서도 바로 볼 수 있게 했다. +- [SettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml.cs) + - `RefreshMemoryOverview()`가 최근 include 감사 3건을 읽어 `허용/차단 · 시간 · 결과` 형식으로 요약해 표시하도록 확장했다. diff --git a/src/AxCopilot/Views/ChatWindow.FooterPresentation.cs b/src/AxCopilot/Views/ChatWindow.FooterPresentation.cs index 9777449..6a1d93e 100644 --- a/src/AxCopilot/Views/ChatWindow.FooterPresentation.cs +++ b/src/AxCopilot/Views/ChatWindow.FooterPresentation.cs @@ -1,8 +1,12 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Media; using AxCopilot.Models; using AxCopilot.Services; @@ -10,18 +14,20 @@ namespace AxCopilot.Views; public partial class ChatWindow { + private Popup? _memoryStatusPopup; + private string BuildDefaultInputWatermark() { var hasFolder = !string.IsNullOrWhiteSpace(GetCurrentWorkFolder()); return _activeTab switch { "Cowork" => hasFolder - ? "문서 작성, 데이터 분석, 파일 작업을 요청하세요. 필요하면 작업 폴더 파일도 함께 참고합니다." - : "문서 작성, 데이터 분석, 파일 작업을 요청하세요. 작업 폴더를 선택하면 관련 파일도 함께 참고합니다.", + ? "臾몄꽌 ?묒꽦, ?곗씠??遺꾩꽍, ?뚯씪 ?묒뾽???붿껌?섏꽭?? ?꾩슂?섎㈃ ?묒뾽 ?대뜑 ?뚯씪???④퍡 李멸퀬?⑸땲??" + : "臾몄꽌 ?묒꽦, ?곗씠??遺꾩꽍, ?뚯씪 ?묒뾽???붿껌?섏꽭?? ?묒뾽 ?대뜑瑜??좏깮?섎㈃ 愿€???뚯씪???④퍡 李멸퀬?⑸땲??", "Code" => hasFolder - ? "코드 수정, 원인 분석, 빌드·테스트를 요청하세요. 작업 폴더 코드를 참고하고, 상단 저장소 배너로 브랜치와 변경 상태를 함께 봅니다." - : "작업 폴더를 선택한 뒤 코드 수정, 원인 분석, 빌드·테스트를 요청하세요.", - _ => "질문, 요약, 초안 작성, 아이디어 정리를 요청하세요.", + ? "肄붾뱶 ?섏젙, ?먯씤 遺꾩꽍, 鍮뚮뱶쨌?뚯뒪?몃? ?붿껌?섏꽭?? ?묒뾽 ?대뜑 肄붾뱶瑜?李멸퀬?섍퀬, ?곷떒 ?€?μ냼 諛곕꼫濡?釉뚮옖移섏? 蹂€寃??곹깭瑜??④퍡 遊낅땲??" + : "?묒뾽 ?대뜑瑜??좏깮????肄붾뱶 ?섏젙, ?먯씤 遺꾩꽍, 鍮뚮뱶쨌?뚯뒪?몃? ?붿껌?섏꽭??", + _ => "吏덈Ц, ?붿빟, 珥덉븞 ?묒꽦, ?꾩씠?붿뼱 ?뺣━瑜??붿껌?섏꽭??", }; } @@ -41,9 +47,9 @@ public partial class ChatWindow return preset.Description.Trim(); if (string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase)) - return "선택한 작업 유형에 맞는 문서·데이터·파일 작업 흐름으로 이어집니다."; + return "?좏깮???묒뾽 ?좏삎??留욌뒗 臾몄꽌쨌?곗씠?걔룻뙆???묒뾽 ?먮쫫?쇰줈 ?댁뼱吏묐땲??"; - return "선택한 대화 주제에 맞는 응답 방향과 초안 흐름으로 이어집니다."; + return "?좏깮???€??二쇱젣??留욌뒗 ?묐떟 諛⑺뼢怨?珥덉븞 ?먮쫫?쇰줈 ?댁뼱吏묐땲??"; } private void UpdateFolderBar() @@ -68,7 +74,7 @@ public partial class ChatWindow } else { - FolderPathLabel.Text = "폴더를 선택하세요"; + FolderPathLabel.Text = "?대뜑瑜??좏깮?섏꽭??"; FolderPathLabel.ToolTip = null; } @@ -142,6 +148,239 @@ public partial class ChatWindow MemoryStatusSeparator.Visibility = Visibility.Visible; } + private void BtnMemoryStatus_Click(object sender, RoutedEventArgs e) + { + if (BtnMemoryStatus == null) + return; + + _memoryStatusPopup?.SetCurrentValue(Popup.IsOpenProperty, false); + + var app = System.Windows.Application.Current as App; + var memory = app?.MemoryService; + if (memory == null) + return; + + var workFolder = GetCurrentWorkFolder(); + memory.Load(workFolder); + var docs = memory.InstructionDocuments; + var learned = memory.All.Count; + var includePolicy = _settings.Settings.Llm.AllowExternalMemoryIncludes + ? "외부 include 허용" + : "외부 include 차단"; + var auditEnabled = _settings.Settings.Llm.EnableAuditLog; + var recentIncludeEntries = AuditLogService.LoadToday() + .Where(x => string.Equals(x.Action, "MemoryInclude", StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(x => x.Timestamp) + .Take(5) + .ToList(); + + var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; + var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; + var accentBrush = TryFindResource("AccentColor") as Brush ?? secondaryText; + var okBrush = new SolidColorBrush(Color.FromRgb(0x22, 0x9A, 0x55)); + var warnBrush = new SolidColorBrush(Color.FromRgb(0xD9, 0x77, 0x06)); + var dangerBrush = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)); + + var panel = new StackPanel { Margin = new Thickness(2) }; + panel.Children.Add(new TextBlock + { + Text = "메모리 상태", + FontSize = 13, + FontWeight = FontWeights.SemiBold, + Foreground = primaryText, + Margin = new Thickness(8, 4, 8, 2), + }); + panel.Children.Add(new TextBlock + { + Text = $"계층형 규칙 {docs.Count}개 · 학습 메모리 {learned}개 · {includePolicy}", + FontSize = 11.5, + Foreground = secondaryText, + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(8, 0, 8, 6), + }); + + if (docs.Count > 0) + { + panel.Children.Add(CreateSurfacePopupSeparator()); + panel.Children.Add(new TextBlock + { + Text = "적용 중 규칙", + FontSize = 12, + FontWeight = FontWeights.SemiBold, + Foreground = primaryText, + Margin = new Thickness(8, 6, 8, 4), + }); + + foreach (var doc in docs.Take(6)) + panel.Children.Add(BuildMemoryPopupRuleRow(doc, primaryText, secondaryText, accentBrush)); + + if (docs.Count > 6) + { + panel.Children.Add(new TextBlock + { + Text = $"외 {docs.Count - 6}개 규칙", + FontSize = 11, + Foreground = secondaryText, + Margin = new Thickness(8, 2, 8, 4), + }); + } + } + + panel.Children.Add(CreateSurfacePopupSeparator()); + panel.Children.Add(new TextBlock + { + Text = "최근 include 감사", + FontSize = 12, + FontWeight = FontWeights.SemiBold, + Foreground = primaryText, + Margin = new Thickness(8, 6, 8, 4), + }); + + if (!auditEnabled) + { + panel.Children.Add(new TextBlock + { + Text = "감사 로그가 꺼져 있어 include 이력이 기록되지 않습니다.", + FontSize = 11, + Foreground = secondaryText, + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(8, 0, 8, 6), + }); + } + else if (recentIncludeEntries.Count == 0) + { + panel.Children.Add(new TextBlock + { + Text = "오늘 기록된 include 시도가 없습니다.", + FontSize = 11, + Foreground = secondaryText, + Margin = new Thickness(8, 0, 8, 6), + }); + } + else + { + foreach (var entry in recentIncludeEntries) + panel.Children.Add(BuildMemoryPopupAuditRow(entry, primaryText, secondaryText, okBrush, warnBrush, dangerBrush)); + } + + var container = CreateSurfacePopupContainer(panel, 340, new Thickness(8)); + _memoryStatusPopup = new Popup + { + Child = container, + StaysOpen = false, + AllowsTransparency = true, + PopupAnimation = PopupAnimation.Fade, + Placement = PlacementMode.Top, + PlacementTarget = BtnMemoryStatus, + VerticalOffset = -6, + }; + _memoryStatusPopup.IsOpen = true; + } + + private static string ShortenMemoryPath(string path) + { + try + { + var fileName = Path.GetFileName(path); + if (string.IsNullOrWhiteSpace(fileName)) + return path; + + var directory = Path.GetDirectoryName(path); + return string.IsNullOrWhiteSpace(directory) + ? fileName + : $"{fileName} · {directory}"; + } + catch + { + return path; + } + } + + private Border BuildMemoryPopupRuleRow(MemoryInstructionDocument doc, Brush primaryText, Brush secondaryText, Brush accentBrush) + { + var meta = new List(); + if (!string.IsNullOrWhiteSpace(doc.Description)) + meta.Add(doc.Description.Trim()); + if (doc.Tags.Count > 0) + meta.Add($"tags: {string.Join(", ", doc.Tags)}"); + if (doc.Paths.Count > 0) + meta.Add($"paths: {string.Join(", ", doc.Paths.Take(2))}{(doc.Paths.Count > 2 ? "..." : "")}"); + + var stack = new StackPanel { Margin = new Thickness(8, 2, 8, 4) }; + stack.Children.Add(new TextBlock + { + Text = $"[{doc.Label}] 우선순위 {doc.Priority}", + FontSize = 11.5, + FontWeight = FontWeights.SemiBold, + Foreground = primaryText, + }); + stack.Children.Add(new TextBlock + { + Text = meta.Count == 0 ? doc.Layer : $"{doc.Layer} · {string.Join(" · ", meta)}", + FontSize = 10.5, + Foreground = secondaryText, + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 2, 0, 0), + }); + if (!string.IsNullOrWhiteSpace(doc.Path)) + { + stack.Children.Add(new TextBlock + { + Text = ShortenMemoryPath(doc.Path), + FontSize = 10.5, + Foreground = accentBrush, + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 2, 0, 0), + }); + } + + return new Border + { + Background = Brushes.Transparent, + CornerRadius = new CornerRadius(8), + Child = stack, + }; + } + + private Border BuildMemoryPopupAuditRow(AuditEntry entry, Brush primaryText, Brush secondaryText, Brush okBrush, Brush warnBrush, Brush dangerBrush) + { + var statusBrush = entry.Success ? okBrush : dangerBrush; + var statusText = entry.Success ? "허용" : "차단"; + var resultBrush = entry.Success ? secondaryText : warnBrush; + + var stack = new StackPanel { Margin = new Thickness(8, 2, 8, 4) }; + stack.Children.Add(new TextBlock + { + Text = $"{statusText} · {entry.Timestamp:HH:mm:ss}", + FontSize = 11.5, + FontWeight = FontWeights.SemiBold, + Foreground = statusBrush, + }); + stack.Children.Add(new TextBlock + { + Text = entry.Parameters, + FontSize = 10.5, + Foreground = primaryText, + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 2, 0, 0), + }); + stack.Children.Add(new TextBlock + { + Text = string.IsNullOrWhiteSpace(entry.Result) ? (entry.FilePath ?? "") : entry.Result, + FontSize = 10.5, + Foreground = resultBrush, + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 2, 0, 0), + }); + + return new Border + { + Background = Brushes.Transparent, + CornerRadius = new CornerRadius(8), + Child = stack, + }; + } + private void UpdateSelectedPresetGuide(ChatConversation? conversation = null) { if (SelectedPresetGuide == null || SelectedPresetGuideTitle == null || SelectedPresetGuideDesc == null) @@ -176,8 +415,8 @@ public partial class ChatWindow } SelectedPresetGuideTitle.Text = string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase) - ? $"선택된 작업 유형 · {preset.Label}" - : $"선택된 대화 주제 · {preset.Label}"; + ? $"?좏깮???묒뾽 ?좏삎 쨌 {preset.Label}" + : $"?좏깮???€??二쇱젣 쨌 {preset.Label}"; SelectedPresetGuideDesc.Text = BuildSelectedPresetGuideDescription(preset); SelectedPresetGuide.Visibility = Visibility.Visible; } diff --git a/src/AxCopilot/Views/ChatWindow.xaml b/src/AxCopilot/Views/ChatWindow.xaml index b989056..cb6a509 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml +++ b/src/AxCopilot/Views/ChatWindow.xaml @@ -2422,6 +2422,7 @@ Padding="10,5" Visibility="Collapsed" BorderThickness="0" + Click="BtnMemoryStatus_Click" ToolTip="현재 적용된 메모리 상태"> +