메모리 상태 팝업과 include 감사 요약 UX 고도화
Some checks failed
Release Gate / gate (push) Has been cancelled

- Cowork/Code 하단 메모리 칩에서 적용 규칙과 최근 include 감사 이력을 팝업으로 확인 가능하게 개선
- 설정 메모리 개요에 최근 include 감사 요약을 추가해 메모리 계층과 감사 상태를 함께 점검 가능하도록 정리
- AX Agent 메모리 구조 고도화 마지막 UX 보강 반영 및 Release 빌드 경고/오류 0 검증
This commit is contained in:
2026-04-07 00:55:53 +09:00
parent 594bb6ffe6
commit fe843fb314
6 changed files with 299 additions and 23 deletions

View File

@@ -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 시도 결과를 같은 화면에서 함께 점검할 수 있게 했습니다.

View File

@@ -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건을 읽어 `허용/차단 · 시간 · 결과` 형식으로 요약해 표시하도록 확장했다.

View File

@@ -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<string>();
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;
}

View File

@@ -2422,6 +2422,7 @@
Padding="10,5"
Visibility="Collapsed"
BorderThickness="0"
Click="BtnMemoryStatus_Click"
ToolTip="현재 적용된 메모리 상태">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE943;" FontFamily="Segoe MDL2 Assets" FontSize="12"

View File

@@ -4930,6 +4930,11 @@
FontSize="12"
Foreground="{DynamicResource SecondaryText}"
TextWrapping="Wrap"/>
<TextBlock x:Name="TxtMemoryOverviewAudit"
Margin="0,8,0,0"
FontSize="12"
Foreground="{DynamicResource SecondaryText}"
TextWrapping="Wrap"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Style="{StaticResource MemoryScopeButton}" Content="새로고침" Click="BtnRefreshMemoryOverview_Click"/>

View File

@@ -3197,7 +3197,7 @@ public partial class SettingsWindow : Window
private void RefreshMemoryOverview()
{
if (TxtMemoryOverviewSummary == null || TxtMemoryOverviewScopes == null)
if (TxtMemoryOverviewSummary == null || TxtMemoryOverviewScopes == null || TxtMemoryOverviewAudit == null)
return;
var app = System.Windows.Application.Current as App;
@@ -3206,6 +3206,7 @@ public partial class SettingsWindow : Window
{
TxtMemoryOverviewSummary.Text = "메모리 서비스를 사용할 수 없습니다.";
TxtMemoryOverviewScopes.Text = "";
TxtMemoryOverviewAudit.Text = "";
return;
}
@@ -3219,24 +3220,40 @@ public partial class SettingsWindow : Window
if (docs.Count == 0)
{
TxtMemoryOverviewScopes.Text = "현재 활성화된 계층형 메모리 파일이 없습니다.";
return;
}
else
{
var lines = docs
.Take(6)
.Select(doc =>
{
var priority = doc.Priority > 0 ? $"우선순위 {doc.Priority}" : "우선순위 미정";
var description = string.IsNullOrWhiteSpace(doc.Description) ? "" : $" · {doc.Description}";
var tags = doc.Tags.Count > 0 ? $" · tags: {string.Join(", ", doc.Tags)}" : "";
return $"[{doc.Label}] {priority}{description}{tags}";
})
.ToList();
if (docs.Count > lines.Count)
lines.Add($"외 {docs.Count - lines.Count}개 규칙");
TxtMemoryOverviewScopes.Text = string.Join("\n", lines);
}
var lines = docs
.Take(6)
.Select(doc =>
var includeEntries = AuditLogService.LoadToday()
.Where(x => string.Equals(x.Action, "MemoryInclude", StringComparison.OrdinalIgnoreCase))
.OrderByDescending(x => x.Timestamp)
.Take(3)
.Select(x =>
{
var priority = doc.Priority > 0 ? $"우선순위 {doc.Priority}" : "우선순위 미정";
var description = string.IsNullOrWhiteSpace(doc.Description) ? "" : $" · {doc.Description}";
var tags = doc.Tags.Count > 0 ? $" · tags: {string.Join(", ", doc.Tags)}" : "";
return $"[{doc.Label}] {priority}{description}{tags}";
var status = x.Success ? "허용" : "차단";
return $"{status} · {x.Timestamp:HH:mm:ss} · {x.Result}";
})
.ToList();
if (docs.Count > lines.Count)
lines.Add($"외 {docs.Count - lines.Count}개 규칙");
TxtMemoryOverviewScopes.Text = string.Join("\n", lines);
TxtMemoryOverviewAudit.Text = includeEntries.Count == 0
? "최근 include 감사 기록이 없습니다."
: "최근 include 감사\n" + string.Join("\n", includeEntries);
}
// ─── 에이전트 훅 관리 ─────────────────────────────────────────────────