Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe843fb314 | ||
|
|
594bb6ffe6 |
@@ -1417,3 +1417,9 @@ MIT License
|
|||||||
- 업데이트: 2026-04-07 01:00 (KST)
|
- 업데이트: 2026-04-07 01:00 (KST)
|
||||||
- AX Agent 설정의 에이전트 메모리 섹션에 `적용 중 메모리 계층` 요약을 추가했습니다. 현재 컨텍스트에 반영된 계층형 규칙 수와 학습 메모리 수를 한 줄로 보고, 아래에서 활성 규칙 파일의 우선순위·설명·태그를 바로 확인할 수 있습니다.
|
- AX Agent 설정의 에이전트 메모리 섹션에 `적용 중 메모리 계층` 요약을 추가했습니다. 현재 컨텍스트에 반영된 계층형 규칙 수와 학습 메모리 수를 한 줄로 보고, 아래에서 활성 규칙 파일의 우선순위·설명·태그를 바로 확인할 수 있습니다.
|
||||||
- 메모리 파일을 편집하거나 학습 메모리를 초기화하면 이 요약이 즉시 다시 계산되도록 연결했고, `새로고침` 버튼도 추가해 현재 작업 폴더 기준 메모리 적용 상태를 바로 다시 확인할 수 있게 했습니다.
|
- 메모리 파일을 편집하거나 학습 메모리를 초기화하면 이 요약이 즉시 다시 계산되도록 연결했고, `새로고침` 버튼도 추가해 현재 작업 폴더 기준 메모리 적용 상태를 바로 다시 확인할 수 있게 했습니다.
|
||||||
|
업데이트: 2026-04-07 01:15 (KST)
|
||||||
|
|
||||||
|
- AX Agent 메모리 구조를 추가 강화했습니다. `@include` 확장 시도는 이제 감사 로그에 `MemoryInclude` 항목으로 남고, Cowork/Code 하단 폴더 바에 현재 적용 중인 계층형 메모리/학습 메모리 상태가 요약 표시됩니다.
|
||||||
|
- 업데이트: 2026-04-07 01:26 (KST)
|
||||||
|
- Cowork/Code 하단 메모리 칩을 눌렀을 때 `적용 중 규칙`과 `최근 include 감사`를 바로 확인할 수 있는 상세 팝업을 추가했습니다. 이제 메모리 계층이 실제로 어떻게 적용되고 있는지 채팅 하단에서 바로 추적할 수 있습니다.
|
||||||
|
- 설정의 메모리 개요에도 `최근 include 감사` 요약을 추가해, 메모리 규칙 상태와 include 시도 결과를 같은 화면에서 함께 점검할 수 있게 했습니다.
|
||||||
|
|||||||
@@ -5226,3 +5226,20 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
|
|||||||
- [SettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml.cs)
|
- [SettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml.cs)
|
||||||
- `RefreshMemoryOverview()`를 추가해 현재 작업 폴더 기준 메모리 계층 적용 상태를 계산해 UI에 표시하도록 했다.
|
- `RefreshMemoryOverview()`를 추가해 현재 작업 폴더 기준 메모리 계층 적용 상태를 계산해 UI에 표시하도록 했다.
|
||||||
- 설정 창 로드 시, 메모리 파일 저장 후, 학습 메모리 초기화 후 모두 이 요약을 자동으로 갱신해 설정 UI와 실제 적용 상태가 어긋나지 않도록 정리했다.
|
- 설정 창 로드 시, 메모리 파일 저장 후, 학습 메모리 초기화 후 모두 이 요약을 자동으로 갱신해 설정 UI와 실제 적용 상태가 어긋나지 않도록 정리했다.
|
||||||
|
업데이트: 2026-04-07 01:15 (KST)
|
||||||
|
|
||||||
|
- 메모리 구조 후속 고도화:
|
||||||
|
- `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건을 읽어 `허용/차단 · 시간 · 결과` 형식으로 요약해 표시하도록 확장했다.
|
||||||
|
|||||||
@@ -570,10 +570,11 @@ public class AgentMemoryService
|
|||||||
|
|
||||||
if (!inCodeBlock && trimmed.StartsWith("@", StringComparison.Ordinal) && trimmed.Length > 1)
|
if (!inCodeBlock && trimmed.StartsWith("@", StringComparison.Ordinal) && trimmed.Length > 1)
|
||||||
{
|
{
|
||||||
var includePath = ResolveIncludePath(fullPath, trimmed, projectRoot, IsExternalMemoryIncludeAllowed());
|
var resolution = ResolveIncludePath(fullPath, trimmed, projectRoot, IsExternalMemoryIncludeAllowed());
|
||||||
if (!string.IsNullOrWhiteSpace(includePath))
|
LogIncludeAudit(fullPath, trimmed, resolution);
|
||||||
|
if (!string.IsNullOrWhiteSpace(resolution.ResolvedPath))
|
||||||
{
|
{
|
||||||
var included = ExpandInstructionIncludes(includePath, projectRoot, visited, depth + 1);
|
var included = ExpandInstructionIncludes(resolution.ResolvedPath, projectRoot, visited, depth + 1);
|
||||||
if (!string.IsNullOrWhiteSpace(included))
|
if (!string.IsNullOrWhiteSpace(included))
|
||||||
{
|
{
|
||||||
sb.AppendLine(included.TrimEnd());
|
sb.AppendLine(included.TrimEnd());
|
||||||
@@ -595,18 +596,18 @@ public class AgentMemoryService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? ResolveIncludePath(string currentFile, string includeDirective, string? projectRoot, bool allowExternal)
|
private static MemoryIncludeResolution ResolveIncludePath(string currentFile, string includeDirective, string? projectRoot, bool allowExternal)
|
||||||
{
|
{
|
||||||
var target = includeDirective[1..].Trim();
|
var target = includeDirective[1..].Trim();
|
||||||
if (string.IsNullOrWhiteSpace(target))
|
if (string.IsNullOrWhiteSpace(target))
|
||||||
return null;
|
return MemoryIncludeResolution.CreateFailure("", "빈 include 지시문");
|
||||||
|
|
||||||
var hashIndex = target.IndexOf('#');
|
var hashIndex = target.IndexOf('#');
|
||||||
if (hashIndex >= 0)
|
if (hashIndex >= 0)
|
||||||
target = target[..hashIndex];
|
target = target[..hashIndex];
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(target))
|
if (string.IsNullOrWhiteSpace(target))
|
||||||
return null;
|
return MemoryIncludeResolution.CreateFailure("", "주석만 포함된 include 지시문");
|
||||||
|
|
||||||
string resolved;
|
string resolved;
|
||||||
var externalCandidate = false;
|
var externalCandidate = false;
|
||||||
@@ -634,18 +635,20 @@ public class AgentMemoryService
|
|||||||
if (!string.IsNullOrWhiteSpace(ext) &&
|
if (!string.IsNullOrWhiteSpace(ext) &&
|
||||||
!new[] { ".md", ".txt", ".cs", ".json", ".yml", ".yaml", ".xml", ".props", ".targets" }
|
!new[] { ".md", ".txt", ".cs", ".json", ".yml", ".yaml", ".xml", ".props", ".targets" }
|
||||||
.Contains(ext, StringComparer.OrdinalIgnoreCase))
|
.Contains(ext, StringComparer.OrdinalIgnoreCase))
|
||||||
return null;
|
return MemoryIncludeResolution.CreateFailure(resolved, $"지원하지 않는 확장자 {ext}");
|
||||||
|
|
||||||
if (!allowExternal)
|
if (!allowExternal)
|
||||||
{
|
{
|
||||||
if (externalCandidate)
|
if (externalCandidate)
|
||||||
return null;
|
return MemoryIncludeResolution.CreateFailure(resolved, "외부 include가 비활성화되어 있습니다");
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(projectRoot) && !IsSubPathOf(projectRoot, resolved))
|
if (!string.IsNullOrWhiteSpace(projectRoot) && !IsSubPathOf(projectRoot, resolved))
|
||||||
return null;
|
return MemoryIncludeResolution.CreateFailure(resolved, "프로젝트 바깥 경로는 허용되지 않습니다");
|
||||||
}
|
}
|
||||||
|
|
||||||
return File.Exists(resolved) ? resolved : null;
|
return File.Exists(resolved)
|
||||||
|
? MemoryIncludeResolution.CreateSuccess(resolved)
|
||||||
|
: MemoryIncludeResolution.CreateFailure(resolved, "포함할 파일을 찾지 못했습니다");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsSubPathOf(string baseDirectory, string candidatePath)
|
private static bool IsSubPathOf(string baseDirectory, string candidatePath)
|
||||||
@@ -680,6 +683,34 @@ public class AgentMemoryService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void LogIncludeAudit(string sourcePath, string directive, MemoryIncludeResolution resolution)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var app = System.Windows.Application.Current as App;
|
||||||
|
if (app?.SettingsService?.Settings.Llm.EnableAuditLog != true)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AuditLogService.Log(new AuditEntry
|
||||||
|
{
|
||||||
|
ConversationId = "",
|
||||||
|
Tab = "Memory",
|
||||||
|
Action = "MemoryInclude",
|
||||||
|
ToolName = "memory",
|
||||||
|
Parameters = $"{sourcePath} <= {directive}",
|
||||||
|
Result = resolution.Success
|
||||||
|
? $"허용: {resolution.ResolvedPath}"
|
||||||
|
: $"차단: {resolution.Reason}",
|
||||||
|
FilePath = resolution.ResolvedPath,
|
||||||
|
Success = resolution.Success,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 감사 로그 실패는 메모리 로드에 영향 주지 않음
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string? ReadInstructionFile(string scope, string? workFolder)
|
public string? ReadInstructionFile(string scope, string? workFolder)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
@@ -913,3 +944,24 @@ public class MemoryInstructionDocument
|
|||||||
[JsonPropertyName("priority")]
|
[JsonPropertyName("priority")]
|
||||||
public int Priority { get; set; }
|
public int Priority { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal sealed class MemoryIncludeResolution
|
||||||
|
{
|
||||||
|
public string? ResolvedPath { get; init; }
|
||||||
|
public string Reason { get; init; } = "";
|
||||||
|
public bool Success { get; init; }
|
||||||
|
|
||||||
|
public static MemoryIncludeResolution CreateSuccess(string path) => new()
|
||||||
|
{
|
||||||
|
ResolvedPath = path,
|
||||||
|
Success = true,
|
||||||
|
Reason = "허용"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static MemoryIncludeResolution CreateFailure(string? path, string reason) => new()
|
||||||
|
{
|
||||||
|
ResolvedPath = string.IsNullOrWhiteSpace(path) ? null : path,
|
||||||
|
Success = false,
|
||||||
|
Reason = reason
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Media;
|
||||||
using AxCopilot.Models;
|
using AxCopilot.Models;
|
||||||
using AxCopilot.Services;
|
using AxCopilot.Services;
|
||||||
|
|
||||||
@@ -10,18 +14,20 @@ namespace AxCopilot.Views;
|
|||||||
|
|
||||||
public partial class ChatWindow
|
public partial class ChatWindow
|
||||||
{
|
{
|
||||||
|
private Popup? _memoryStatusPopup;
|
||||||
|
|
||||||
private string BuildDefaultInputWatermark()
|
private string BuildDefaultInputWatermark()
|
||||||
{
|
{
|
||||||
var hasFolder = !string.IsNullOrWhiteSpace(GetCurrentWorkFolder());
|
var hasFolder = !string.IsNullOrWhiteSpace(GetCurrentWorkFolder());
|
||||||
return _activeTab switch
|
return _activeTab switch
|
||||||
{
|
{
|
||||||
"Cowork" => hasFolder
|
"Cowork" => hasFolder
|
||||||
? "문서 작성, 데이터 분석, 파일 작업을 요청하세요. 필요하면 작업 폴더 파일도 함께 참고합니다."
|
? "臾몄꽌 ?묒꽦, ?곗씠??遺꾩꽍, ?뚯씪 ?묒뾽???붿껌?섏꽭?? ?꾩슂?섎㈃ ?묒뾽 ?대뜑 ?뚯씪???④퍡 李멸퀬?⑸땲??"
|
||||||
: "문서 작성, 데이터 분석, 파일 작업을 요청하세요. 작업 폴더를 선택하면 관련 파일도 함께 참고합니다.",
|
: "臾몄꽌 ?묒꽦, ?곗씠??遺꾩꽍, ?뚯씪 ?묒뾽???붿껌?섏꽭?? ?묒뾽 ?대뜑瑜??좏깮?섎㈃ 愿???뚯씪???④퍡 李멸퀬?⑸땲??",
|
||||||
"Code" => hasFolder
|
"Code" => hasFolder
|
||||||
? "코드 수정, 원인 분석, 빌드·테스트를 요청하세요. 작업 폴더 코드를 참고하고, 상단 저장소 배너로 브랜치와 변경 상태를 함께 봅니다."
|
? "肄붾뱶 ?섏젙, ?먯씤 遺꾩꽍, 鍮뚮뱶쨌?뚯뒪?몃? ?붿껌?섏꽭?? ?묒뾽 ?대뜑 肄붾뱶瑜?李멸퀬?섍퀬, ?곷떒 ??μ냼 諛곕꼫濡?釉뚮옖移섏? 蹂寃??곹깭瑜??④퍡 遊낅땲??"
|
||||||
: "작업 폴더를 선택한 뒤 코드 수정, 원인 분석, 빌드·테스트를 요청하세요.",
|
: "?묒뾽 ?대뜑瑜??좏깮????肄붾뱶 ?섏젙, ?먯씤 遺꾩꽍, 鍮뚮뱶쨌?뚯뒪?몃? ?붿껌?섏꽭??",
|
||||||
_ => "질문, 요약, 초안 작성, 아이디어 정리를 요청하세요.",
|
_ => "吏덈Ц, ?붿빟, 珥덉븞 ?묒꽦, ?꾩씠?붿뼱 ?뺣━瑜??붿껌?섏꽭??",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,9 +47,9 @@ public partial class ChatWindow
|
|||||||
return preset.Description.Trim();
|
return preset.Description.Trim();
|
||||||
|
|
||||||
if (string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase))
|
||||||
return "선택한 작업 유형에 맞는 문서·데이터·파일 작업 흐름으로 이어집니다.";
|
return "?좏깮???묒뾽 ?좏삎??留욌뒗 臾몄꽌쨌?곗씠?걔룻뙆???묒뾽 ?먮쫫?쇰줈 ?댁뼱吏묐땲??";
|
||||||
|
|
||||||
return "선택한 대화 주제에 맞는 응답 방향과 초안 흐름으로 이어집니다.";
|
return "?좏깮?????二쇱젣??留욌뒗 ?묐떟 諛⑺뼢怨?珥덉븞 ?먮쫫?쇰줈 ?댁뼱吏묐땲??";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateFolderBar()
|
private void UpdateFolderBar()
|
||||||
@@ -68,7 +74,7 @@ public partial class ChatWindow
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
FolderPathLabel.Text = "폴더를 선택하세요";
|
FolderPathLabel.Text = "?대뜑瑜??좏깮?섏꽭??";
|
||||||
FolderPathLabel.ToolTip = null;
|
FolderPathLabel.ToolTip = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +82,7 @@ public partial class ChatWindow
|
|||||||
LoadCompactionMetricsFromConversation();
|
LoadCompactionMetricsFromConversation();
|
||||||
UpdatePermissionUI();
|
UpdatePermissionUI();
|
||||||
UpdateDataUsageUI();
|
UpdateDataUsageUI();
|
||||||
|
UpdateMemoryStatusUi();
|
||||||
RefreshContextUsageVisual();
|
RefreshContextUsageVisual();
|
||||||
ScheduleGitBranchRefresh();
|
ScheduleGitBranchRefresh();
|
||||||
UpdateGitBranchUi(_currentGitBranchName, GitBranchFilesText?.Text ?? "", GitBranchAddedText?.Text ?? "", GitBranchDeletedText?.Text ?? "", _currentGitTooltip ?? "", BtnGitBranch?.Visibility ?? Visibility.Collapsed);
|
UpdateGitBranchUi(_currentGitBranchName, GitBranchFilesText?.Text ?? "", GitBranchAddedText?.Text ?? "", GitBranchDeletedText?.Text ?? "", _currentGitTooltip ?? "", BtnGitBranch?.Visibility ?? Visibility.Collapsed);
|
||||||
@@ -86,6 +93,294 @@ public partial class ChatWindow
|
|||||||
_folderDataUsage = GetAutomaticFolderDataUsage();
|
_folderDataUsage = GetAutomaticFolderDataUsage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateMemoryStatusUi()
|
||||||
|
{
|
||||||
|
if (BtnMemoryStatus == null || MemoryStatusLabel == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_activeTab == "Chat")
|
||||||
|
{
|
||||||
|
BtnMemoryStatus.Visibility = Visibility.Collapsed;
|
||||||
|
MemoryStatusSeparator.Visibility = Visibility.Collapsed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var app = System.Windows.Application.Current as App;
|
||||||
|
var memory = app?.MemoryService;
|
||||||
|
if (memory == null || !_settings.Settings.Llm.EnableAgentMemory)
|
||||||
|
{
|
||||||
|
MemoryStatusLabel.Text = "메모리 꺼짐";
|
||||||
|
BtnMemoryStatus.ToolTip = "에이전트 메모리가 비활성화되어 있습니다.";
|
||||||
|
BtnMemoryStatus.Visibility = Visibility.Visible;
|
||||||
|
MemoryStatusSeparator.Visibility = Visibility.Visible;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var workFolder = GetCurrentWorkFolder();
|
||||||
|
memory.Load(workFolder);
|
||||||
|
var docs = memory.InstructionDocuments;
|
||||||
|
var learned = memory.All.Count;
|
||||||
|
|
||||||
|
MemoryStatusLabel.Text = docs.Count > 0 || learned > 0
|
||||||
|
? $"메모리 {docs.Count} · 학습 {learned}"
|
||||||
|
: "메모리 없음";
|
||||||
|
|
||||||
|
var lines = docs
|
||||||
|
.Take(4)
|
||||||
|
.Select(doc =>
|
||||||
|
{
|
||||||
|
var priority = doc.Priority > 0 ? $"우선순위 {doc.Priority}" : "우선순위 미정";
|
||||||
|
var description = string.IsNullOrWhiteSpace(doc.Description) ? "" : $" · {doc.Description}";
|
||||||
|
return $"[{doc.Label}] {priority}{description}";
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (docs.Count > lines.Count)
|
||||||
|
lines.Add($"외 {docs.Count - lines.Count}개 규칙");
|
||||||
|
|
||||||
|
var includePolicy = _settings.Settings.Llm.AllowExternalMemoryIncludes
|
||||||
|
? "외부 include 허용"
|
||||||
|
: "외부 include 차단";
|
||||||
|
BtnMemoryStatus.ToolTip = lines.Count == 0
|
||||||
|
? $"계층형 규칙이 없습니다.\n학습 메모리 {learned}개\n{includePolicy}"
|
||||||
|
: $"계층형 규칙 {docs.Count}개 · 학습 메모리 {learned}개\n{string.Join("\n", lines)}\n{includePolicy}";
|
||||||
|
BtnMemoryStatus.Visibility = Visibility.Visible;
|
||||||
|
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)
|
private void UpdateSelectedPresetGuide(ChatConversation? conversation = null)
|
||||||
{
|
{
|
||||||
if (SelectedPresetGuide == null || SelectedPresetGuideTitle == null || SelectedPresetGuideDesc == null)
|
if (SelectedPresetGuide == null || SelectedPresetGuideTitle == null || SelectedPresetGuideDesc == null)
|
||||||
@@ -120,8 +415,8 @@ public partial class ChatWindow
|
|||||||
}
|
}
|
||||||
|
|
||||||
SelectedPresetGuideTitle.Text = string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase)
|
SelectedPresetGuideTitle.Text = string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase)
|
||||||
? $"선택된 작업 유형 · {preset.Label}"
|
? $"?좏깮???묒뾽 ?좏삎 쨌 {preset.Label}"
|
||||||
: $"선택된 대화 주제 · {preset.Label}";
|
: $"?좏깮?????二쇱젣 쨌 {preset.Label}";
|
||||||
SelectedPresetGuideDesc.Text = BuildSelectedPresetGuideDescription(preset);
|
SelectedPresetGuideDesc.Text = BuildSelectedPresetGuideDescription(preset);
|
||||||
SelectedPresetGuide.Visibility = Visibility.Visible;
|
SelectedPresetGuide.Visibility = Visibility.Visible;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2379,9 +2379,11 @@
|
|||||||
<ColumnDefinition Width="Auto"/> <!-- 4: 구분선 -->
|
<ColumnDefinition Width="Auto"/> <!-- 4: 구분선 -->
|
||||||
<ColumnDefinition Width="Auto"/> <!-- 5: 데이터 활용 -->
|
<ColumnDefinition Width="Auto"/> <!-- 5: 데이터 활용 -->
|
||||||
<ColumnDefinition Width="Auto"/> <!-- 6: 구분선 -->
|
<ColumnDefinition Width="Auto"/> <!-- 6: 구분선 -->
|
||||||
<ColumnDefinition Width="Auto"/> <!-- 7: 권한 -->
|
<ColumnDefinition Width="Auto"/> <!-- 7: 메모리 -->
|
||||||
<ColumnDefinition Width="Auto"/> <!-- 8: 구분선 -->
|
<ColumnDefinition Width="Auto"/> <!-- 8: 구분선 -->
|
||||||
<ColumnDefinition Width="Auto"/> <!-- 9: git -->
|
<ColumnDefinition Width="Auto"/> <!-- 9: 권한 -->
|
||||||
|
<ColumnDefinition Width="Auto"/> <!-- 10: 구분선 -->
|
||||||
|
<ColumnDefinition Width="Auto"/> <!-- 11: git -->
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- 폴더 아이콘 -->
|
<!-- 폴더 아이콘 -->
|
||||||
@@ -2412,8 +2414,28 @@
|
|||||||
|
|
||||||
<!-- 폴더 데이터 활용은 코드/코워크 자동 정책으로만 동작 -->
|
<!-- 폴더 데이터 활용은 코드/코워크 자동 정책으로만 동작 -->
|
||||||
|
|
||||||
|
<Border x:Name="MemoryStatusSeparator" Grid.Column="8" Width="1" Height="18" Margin="4,0"
|
||||||
|
Visibility="Collapsed"
|
||||||
|
Background="{DynamicResource SeparatorColor}"/>
|
||||||
|
|
||||||
|
<Button x:Name="BtnMemoryStatus" Grid.Column="7" Style="{StaticResource FooterChipBtn}"
|
||||||
|
Padding="10,5"
|
||||||
|
Visibility="Collapsed"
|
||||||
|
BorderThickness="0"
|
||||||
|
Click="BtnMemoryStatus_Click"
|
||||||
|
ToolTip="현재 적용된 메모리 상태">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||||
|
Foreground="{DynamicResource SecondaryText}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
|
<TextBlock x:Name="MemoryStatusLabel" Text="메모리 없음" FontSize="12"
|
||||||
|
Foreground="{DynamicResource SecondaryText}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<!-- 권한 메뉴 -->
|
<!-- 권한 메뉴 -->
|
||||||
<Button x:Name="BtnPermission" Grid.Column="7" Style="{StaticResource FooterChipBtn}"
|
<Button x:Name="BtnPermission" Grid.Column="9" Style="{StaticResource FooterChipBtn}"
|
||||||
Padding="10,5" Click="BtnPermission_Click" ToolTip="파일 접근 권한"
|
Padding="10,5" Click="BtnPermission_Click" ToolTip="파일 접근 권한"
|
||||||
BorderThickness="0">
|
BorderThickness="0">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
@@ -2426,12 +2448,12 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Border x:Name="GitBranchSeparator" Grid.Column="8" Width="1" Height="18" Margin="4,0"
|
<Border x:Name="GitBranchSeparator" Grid.Column="10" Width="1" Height="18" Margin="4,0"
|
||||||
Visibility="Collapsed"
|
Visibility="Collapsed"
|
||||||
Background="{DynamicResource SeparatorColor}"/>
|
Background="{DynamicResource SeparatorColor}"/>
|
||||||
|
|
||||||
<Button x:Name="BtnGitBranch"
|
<Button x:Name="BtnGitBranch"
|
||||||
Grid.Column="9"
|
Grid.Column="11"
|
||||||
Style="{StaticResource FooterChipBtn}"
|
Style="{StaticResource FooterChipBtn}"
|
||||||
Padding="10,5"
|
Padding="10,5"
|
||||||
Margin="2,0,0,0"
|
Margin="2,0,0,0"
|
||||||
|
|||||||
@@ -4930,6 +4930,11 @@
|
|||||||
FontSize="12"
|
FontSize="12"
|
||||||
Foreground="{DynamicResource SecondaryText}"
|
Foreground="{DynamicResource SecondaryText}"
|
||||||
TextWrapping="Wrap"/>
|
TextWrapping="Wrap"/>
|
||||||
|
<TextBlock x:Name="TxtMemoryOverviewAudit"
|
||||||
|
Margin="0,8,0,0"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{DynamicResource SecondaryText}"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||||
<Button Style="{StaticResource MemoryScopeButton}" Content="새로고침" Click="BtnRefreshMemoryOverview_Click"/>
|
<Button Style="{StaticResource MemoryScopeButton}" Content="새로고침" Click="BtnRefreshMemoryOverview_Click"/>
|
||||||
@@ -4940,7 +4945,7 @@
|
|||||||
<Grid>
|
<Grid>
|
||||||
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||||
<TextBlock Style="{StaticResource RowLabel}" Text="외부 메모리 include 허용"/>
|
<TextBlock Style="{StaticResource RowLabel}" Text="외부 메모리 include 허용"/>
|
||||||
<TextBlock Style="{StaticResource RowHint}" Text="AXMEMORY.md의 @include가 프로젝트 바깥 파일을 읽는 것을 허용합니다. 기본은 안전하게 꺼져 있습니다."/>
|
<TextBlock Style="{StaticResource RowHint}" Text="AXMEMORY.md의 @include가 프로젝트 바깥 파일을 읽는 것을 허용합니다. 기본은 안전하게 꺼져 있으며, include 시도는 감사 로그에 기록됩니다."/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
|
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||||
IsChecked="{Binding AllowExternalMemoryIncludes, Mode=TwoWay}"/>
|
IsChecked="{Binding AllowExternalMemoryIncludes, Mode=TwoWay}"/>
|
||||||
|
|||||||
@@ -3197,7 +3197,7 @@ public partial class SettingsWindow : Window
|
|||||||
|
|
||||||
private void RefreshMemoryOverview()
|
private void RefreshMemoryOverview()
|
||||||
{
|
{
|
||||||
if (TxtMemoryOverviewSummary == null || TxtMemoryOverviewScopes == null)
|
if (TxtMemoryOverviewSummary == null || TxtMemoryOverviewScopes == null || TxtMemoryOverviewAudit == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var app = System.Windows.Application.Current as App;
|
var app = System.Windows.Application.Current as App;
|
||||||
@@ -3206,6 +3206,7 @@ public partial class SettingsWindow : Window
|
|||||||
{
|
{
|
||||||
TxtMemoryOverviewSummary.Text = "메모리 서비스를 사용할 수 없습니다.";
|
TxtMemoryOverviewSummary.Text = "메모리 서비스를 사용할 수 없습니다.";
|
||||||
TxtMemoryOverviewScopes.Text = "";
|
TxtMemoryOverviewScopes.Text = "";
|
||||||
|
TxtMemoryOverviewAudit.Text = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3219,24 +3220,40 @@ public partial class SettingsWindow : Window
|
|||||||
if (docs.Count == 0)
|
if (docs.Count == 0)
|
||||||
{
|
{
|
||||||
TxtMemoryOverviewScopes.Text = "현재 활성화된 계층형 메모리 파일이 없습니다.";
|
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
|
var includeEntries = AuditLogService.LoadToday()
|
||||||
.Take(6)
|
.Where(x => string.Equals(x.Action, "MemoryInclude", StringComparison.OrdinalIgnoreCase))
|
||||||
.Select(doc =>
|
.OrderByDescending(x => x.Timestamp)
|
||||||
|
.Take(3)
|
||||||
|
.Select(x =>
|
||||||
{
|
{
|
||||||
var priority = doc.Priority > 0 ? $"우선순위 {doc.Priority}" : "우선순위 미정";
|
var status = x.Success ? "허용" : "차단";
|
||||||
var description = string.IsNullOrWhiteSpace(doc.Description) ? "" : $" · {doc.Description}";
|
return $"{status} · {x.Timestamp:HH:mm:ss} · {x.Result}";
|
||||||
var tags = doc.Tags.Count > 0 ? $" · tags: {string.Join(", ", doc.Tags)}" : "";
|
|
||||||
return $"[{doc.Label}] {priority}{description}{tags}";
|
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (docs.Count > lines.Count)
|
TxtMemoryOverviewAudit.Text = includeEntries.Count == 0
|
||||||
lines.Add($"외 {docs.Count - lines.Count}개 규칙");
|
? "최근 include 감사 기록이 없습니다."
|
||||||
|
: "최근 include 감사\n" + string.Join("\n", includeEntries);
|
||||||
TxtMemoryOverviewScopes.Text = string.Join("\n", lines);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 에이전트 훅 관리 ─────────────────────────────────────────────────
|
// ─── 에이전트 훅 관리 ─────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user