Some checks failed
Release Gate / gate (push) Has been cancelled
- Cowork와 Code 입력창 워터마크, 프리셋 안내, 메모리 상태 팝업의 깨진 한글 문자열 복구 - 메모리 적용 근거와 상태 문구를 읽기 쉬운 한국어로 재정리 - Release 빌드 경고/오류 0 재검증
441 lines
16 KiB
C#
441 lines
16 KiB
C#
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;
|
|
|
|
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
|
|
? "코드 수정, 원인 분석, 빌드·테스트를 요청하세요. 작업 폴더 코드를 참고하고, 상단 저장소 배너로 브랜치와 변경 상태를 함께 봅니다."
|
|
: "작업 폴더를 선택한 뒤 코드 수정, 원인 분석, 빌드·테스트를 요청하세요.",
|
|
_ => "질문, 요약, 초안 작성, 아이디어 정리를 요청하세요.",
|
|
};
|
|
}
|
|
|
|
private void RefreshInputWatermarkText()
|
|
{
|
|
if (InputWatermark == null)
|
|
return;
|
|
|
|
InputWatermark.Text = string.IsNullOrWhiteSpace(_promptCardPlaceholder)
|
|
? BuildDefaultInputWatermark()
|
|
: _promptCardPlaceholder;
|
|
}
|
|
|
|
private string BuildSelectedPresetGuideDescription(TopicPreset preset)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(preset.Description))
|
|
return preset.Description.Trim();
|
|
|
|
if (string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase))
|
|
return "선택된 작업 유형에 맞는 문서·데이터·파일 작업 흐름으로 이어집니다.";
|
|
|
|
return "선택된 대화 주제에 맞는 응답 방향과 초안 흐름으로 이어집니다.";
|
|
}
|
|
|
|
private void UpdateFolderBar()
|
|
{
|
|
if (FolderBar == null)
|
|
return;
|
|
|
|
if (_activeTab == "Chat")
|
|
{
|
|
FolderBar.Visibility = Visibility.Collapsed;
|
|
UpdateGitBranchUi(null, "", "", "", "", Visibility.Collapsed);
|
|
RefreshContextUsageVisual();
|
|
return;
|
|
}
|
|
|
|
FolderBar.Visibility = Visibility.Visible;
|
|
var folder = GetCurrentWorkFolder();
|
|
if (!string.IsNullOrEmpty(folder))
|
|
{
|
|
FolderPathLabel.Text = folder;
|
|
FolderPathLabel.ToolTip = folder;
|
|
}
|
|
else
|
|
{
|
|
FolderPathLabel.Text = "폴더를 선택하세요";
|
|
FolderPathLabel.ToolTip = null;
|
|
}
|
|
|
|
LoadConversationSettings();
|
|
LoadCompactionMetricsFromConversation();
|
|
UpdatePermissionUI();
|
|
UpdateDataUsageUI();
|
|
UpdateMemoryStatusUi();
|
|
RefreshContextUsageVisual();
|
|
ScheduleGitBranchRefresh();
|
|
UpdateGitBranchUi(_currentGitBranchName, GitBranchFilesText?.Text ?? "", GitBranchAddedText?.Text ?? "", GitBranchDeletedText?.Text ?? "", _currentGitTooltip ?? "", BtnGitBranch?.Visibility ?? Visibility.Collapsed);
|
|
}
|
|
|
|
private void UpdateDataUsageUI()
|
|
{
|
|
_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.LoadRecent("MemoryInclude", maxCount: 5, daysBack: 3);
|
|
|
|
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 = "최근 3일 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 string? BuildMemoryContextEvidenceText()
|
|
{
|
|
if (_activeTab == "Chat")
|
|
return null;
|
|
|
|
var app = System.Windows.Application.Current as App;
|
|
var memory = app?.MemoryService;
|
|
if (memory == null || !_settings.Settings.Llm.EnableAgentMemory)
|
|
return null;
|
|
|
|
memory.Load(GetCurrentWorkFolder());
|
|
var docs = memory.InstructionDocuments;
|
|
var learned = memory.All.Count;
|
|
if (docs.Count == 0 && learned == 0)
|
|
return null;
|
|
|
|
var labels = docs.Take(2).Select(x => x.Label).ToList();
|
|
var labelText = labels.Count == 0 ? "" : $" · {string.Join(", ", labels)}";
|
|
return $"메모리 규칙 {docs.Count}개 · 학습 {learned}개 적용 중{labelText}";
|
|
}
|
|
|
|
private void UpdateSelectedPresetGuide(ChatConversation? conversation = null)
|
|
{
|
|
if (SelectedPresetGuide == null || SelectedPresetGuideTitle == null || SelectedPresetGuideDesc == null)
|
|
return;
|
|
|
|
if (string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
SelectedPresetGuide.Visibility = Visibility.Collapsed;
|
|
SelectedPresetGuideTitle.Text = "";
|
|
SelectedPresetGuideDesc.Text = "";
|
|
return;
|
|
}
|
|
|
|
conversation ??= _currentConversation;
|
|
var category = conversation?.Category?.Trim();
|
|
if (string.IsNullOrWhiteSpace(category))
|
|
{
|
|
SelectedPresetGuide.Visibility = Visibility.Collapsed;
|
|
SelectedPresetGuideTitle.Text = "";
|
|
SelectedPresetGuideDesc.Text = "";
|
|
return;
|
|
}
|
|
|
|
var preset = PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets)
|
|
.FirstOrDefault(p => string.Equals(p.Category?.Trim(), category, StringComparison.OrdinalIgnoreCase));
|
|
if (preset == null)
|
|
{
|
|
SelectedPresetGuide.Visibility = Visibility.Collapsed;
|
|
SelectedPresetGuideTitle.Text = "";
|
|
SelectedPresetGuideDesc.Text = "";
|
|
return;
|
|
}
|
|
|
|
SelectedPresetGuideTitle.Text = string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase)
|
|
? $"선택된 작업 유형 · {preset.Label}"
|
|
: $"선택된 대화 주제 · {preset.Label}";
|
|
SelectedPresetGuideDesc.Text = BuildSelectedPresetGuideDescription(preset);
|
|
SelectedPresetGuide.Visibility = Visibility.Visible;
|
|
}
|
|
}
|