Files
AX-Copilot/src/AxCopilot/Views/ChatWindow.AgentSupport.cs
lacvet 84e5cd9485 [Phase 18] 멀티에이전트 인프라 완성 + 리플레이 탭 통합
Phase 17-D3 수정:
- SkillManagerTool.ListSkills(): UserInvocable=false 내부 스킬 필터링 추가
  (SkillServiceExtensions.IsUserInvocable() 활용)

Phase 18-A 연결 완성:
- ToolRegistry: BackgroundAgentService 프로퍼티 노출 (AgentCompleted 구독용)
- ToolRegistry: WorktreeManager 인스턴스 생성 → DelegateAgentTool에 주입 (18-A2 완성)
- ChatWindow.xaml.cs: _toolRegistry.BackgroundAgentService.AgentCompleted 구독
- ChatWindow.AgentSupport.cs: OnBackgroundAgentCompleted() — 완료/실패 트레이 알림

Phase 18-B: 리플레이/디버깅 UI 통합:
- WorkflowAnalyzerWindow.xaml: "리플레이" 탭 버튼 추가 ()
- WorkflowAnalyzerWindow.xaml: 리플레이 패널 — 세션 목록, 재생 컨트롤, 속도 슬라이더, 진행 바, 이벤트 스트림
- WorkflowAnalyzerWindow.xaml.cs: TabReplay_Click, UpdateTabVisuals 3탭 지원
- WorkflowAnalyzerWindow.xaml.cs: LoadReplaySessions, BtnReplayStart/Stop, BuildReplayEventRow
- ReplayTimelineViewModel 통합 — StartReplayAsync/StopReplay, ReplayEventReceived 이벤트
- AGENT_ROADMAP.md: 18-A2/A3/A4/B1  완료 표시 갱신

빌드: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 01:30:29 +09:00

502 lines
25 KiB
C#

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;
using AxCopilot.Models;
using AxCopilot.Services;
using AxCopilot.Services.Agent;
namespace AxCopilot.Views;
public partial class ChatWindow
{
// ─── Phase 34: Cowork/Code 공통 에이전트 루프 실행 ──────────────────
/// <summary>
/// Cowork/Code 탭 공통 에이전트 루프 실행.
/// 기존 Cowork/Code 분기의 중복 코드(~70줄)를 단일 메서드로 통합.
/// </summary>
private async Task<string> RunAgentLoopAsync(string tab, List<ChatMessage> sendMessages, CancellationToken ct)
{
OpenWorkflowAnalyzerIfEnabled();
_agentCumulativeInputTokens = 0;
_agentCumulativeOutputTokens = 0;
// 탭별 시스템 프롬프트 삽입
var systemPrompt = tab == "Code" ? BuildCodeSystemPrompt() : BuildCoworkSystemPrompt();
if (!string.IsNullOrEmpty(systemPrompt))
sendMessages.Insert(0, new ChatMessage { Role = "system", Content = systemPrompt });
_agentLoop.ActiveTab = tab;
_agentLoop.EventOccurred += OnAgentEvent;
_agentLoop.UserDecisionCallback = CreatePlanDecisionCallback();
try
{
var response = await _agentLoop.RunAsync(sendMessages, ct);
// 완료 알림
if (Llm.NotifyOnComplete)
{
var label = tab == "Code" ? "AX Code Agent" : "AX Cowork Agent";
var desc = tab == "Code" ? "코드 작업이 완료되었습니다." : "코워크 작업이 완료되었습니다.";
Services.NotificationService.Notify(label, desc);
}
return response;
}
finally
{
_agentLoop.EventOccurred -= OnAgentEvent;
_agentLoop.UserDecisionCallback = null;
}
}
// ─── 코워크 에이전트 지원 ────────────────────────────────────────────
private string BuildCoworkSystemPrompt()
{
var workFolder = GetCurrentWorkFolder();
var llm = Llm;
var sb = new System.Text.StringBuilder();
sb.AppendLine("You are AX Copilot Agent. You can read, write, and edit files using the provided tools.");
sb.AppendLine($"Today's date: {DateTime.Now:yyyy년 M월 d일} ({DateTime.Now:yyyy-MM-dd}, {DateTime.Now:dddd}).");
sb.AppendLine("Available skills: excel_create (.xlsx), docx_create (.docx), csv_create (.csv), markdown_create (.md), html_create (.html), script_create (.bat/.ps1), document_review (품질 검증), format_convert (포맷 변환).");
sb.AppendLine("Always explain your plan step by step BEFORE executing tools. After creating files, summarize what was created.");
sb.AppendLine("IMPORTANT: When creating documents with dates, always use today's actual date above. Never use placeholder or fictional dates.");
sb.AppendLine("IMPORTANT: When asked to create a document with multiple sections (reports, proposals, analyses, etc.), you MUST:");
sb.AppendLine(" 1. First, plan the document: decide the exact sections (headings), their order, and key points for each section based on the topic.");
sb.AppendLine(" 2. Call document_plan with sections_hint = your planned section titles (comma-separated). Example: sections_hint=\"회사 개요, 사업 현황, 재무 분석, SWOT, 전략 제언, 결론\"");
sb.AppendLine(" This ensures the document structure matches YOUR plan, not a generic template.");
sb.AppendLine(" 3. Then immediately call html_create (or docx_create/file_write) using the scaffold from document_plan.");
sb.AppendLine(" 4. Write actual detailed content for EVERY section — no skipping, no placeholders, no minimal content.");
sb.AppendLine(" 5. Do NOT call html_create directly without document_plan for multi-section documents.");
// 문서 품질 검증 루프
sb.AppendLine("\n## Document Quality Review");
sb.AppendLine("After creating any document (html_create, docx_create, excel_create, etc.), you MUST perform a self-review:");
sb.AppendLine("1. Use file_read to read the generated file and verify the content is complete");
sb.AppendLine("2. Check for logical errors: incorrect dates, inconsistent data, missing sections, broken formatting");
sb.AppendLine("3. Verify all requested topics/sections from the user's original request are covered");
sb.AppendLine("4. If issues found, fix them using file_write or file_edit, then re-verify");
sb.AppendLine("5. Report the review result to the user: what was checked and whether corrections were made");
// 문서 포맷 변환 지원
sb.AppendLine("\n## Format Conversion");
sb.AppendLine("When the user requests format conversion (e.g., HTML→Word, Excel→CSV, Markdown→HTML):");
sb.AppendLine("1. Use file_read or document_read to read the source file content");
sb.AppendLine("2. Create a new file in the target format using the appropriate skill (docx_create, html_create, etc.)");
sb.AppendLine("3. Preserve the content structure, formatting, and data as closely as possible");
// 사용자 지정 출력 포맷
var fmt = llm.DefaultOutputFormat;
if (!string.IsNullOrEmpty(fmt) && fmt != "auto")
{
var fmtMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["xlsx"] = "Excel (.xlsx) using excel_create",
["docx"] = "Word (.docx) using docx_create",
["html"] = "HTML (.html) using html_create",
["md"] = "Markdown (.md) using markdown_create",
["csv"] = "CSV (.csv) using csv_create",
};
if (fmtMap.TryGetValue(fmt, out var fmtDesc))
sb.AppendLine($"IMPORTANT: User prefers output format: {fmtDesc}. Use this format unless the user specifies otherwise.");
}
// 디자인 무드 — HTML 문서 생성 시 mood 파라미터로 전달하도록 안내
if (!string.IsNullOrEmpty(_selectedMood) && _selectedMood != "modern")
sb.AppendLine($"When creating HTML documents with html_create, use mood=\"{_selectedMood}\" for the design template.");
else
sb.AppendLine("When creating HTML documents with html_create, you can set 'mood' parameter: modern, professional, creative, minimal, elegant, dark, colorful, corporate, magazine, dashboard.");
if (!string.IsNullOrEmpty(workFolder))
sb.AppendLine($"Current work folder: {workFolder}");
sb.AppendLine($"File permission mode: {llm.FilePermission}");
// 폴더 데이터 활용 지침
switch (_folderDataUsage)
{
case "active":
sb.AppendLine("IMPORTANT: Folder Data Usage = ACTIVE. You have 'document_read' and 'folder_map' tools available.");
sb.AppendLine("Before creating reports, use folder_map to scan the work folder structure. " +
"Then EVALUATE whether each document is RELEVANT to the user's current request topic. " +
"Only use document_read on files that are clearly related to the conversation subject. " +
"Do NOT read or reference files that are unrelated to the user's request, even if they exist in the folder. " +
"In your planning step, list which files you plan to read and explain WHY they are relevant.");
break;
case "passive":
sb.AppendLine("Folder Data Usage = PASSIVE. You have 'document_read' and 'folder_map' tools. " +
"Only read folder documents when the user explicitly asks you to reference or use them.");
break;
default: // "none"
sb.AppendLine("Folder Data Usage = NONE. Do NOT read or reference documents in the work folder unless the user explicitly provides a file path.");
break;
}
// 프리셋 시스템 프롬프트가 있으면 추가
lock (_convLock)
{
if (_currentConversation != null && !string.IsNullOrEmpty(_currentConversation.SystemCommand))
sb.AppendLine("\n" + _currentConversation.SystemCommand);
}
// 프로젝트 문맥 파일 (AX.md) 주입 — Phase 27-C: @include 지시어 해석 포함
sb.Append(LoadProjectContext(workFolder));
// 프로젝트 규칙 (.ax/rules/) 자동 주입
sb.Append(BuildProjectRulesSection(workFolder));
// 에이전트 메모리 주입
sb.Append(BuildMemorySection(workFolder));
// 피드백 학습 컨텍스트 주입
sb.Append(BuildFeedbackContext());
// Phase 27-B: 현재 파일 경로 기반 자동 스킬 주입
sb.Append(BuildPathBasedSkillSection());
return sb.ToString();
}
private string BuildCodeSystemPrompt()
{
var workFolder = GetCurrentWorkFolder();
var llm = Llm;
var code = llm.Code;
var sb = new System.Text.StringBuilder();
sb.AppendLine("You are AX Copilot Code Agent — a senior software engineer for enterprise development.");
sb.AppendLine($"Today's date: {DateTime.Now:yyyy년 M월 d일} ({DateTime.Now:yyyy-MM-dd}).");
sb.AppendLine("Available tools: file_read, file_write, file_edit (supports replace_all), glob, grep (supports context_lines, case_sensitive), folder_map, process, dev_env_detect, build_run, git_tool.");
sb.AppendLine("IMPORTANT: When creating documents with dates, always use today's actual date above.");
sb.AppendLine("\n## Core Workflow (MANDATORY — follow this order)");
sb.AppendLine("1. ORIENT: Run folder_map (depth=2) to understand project structure. Check .gitignore, README, config files.");
sb.AppendLine("2. BASELINE: If tests exist, run build_run action='test' FIRST to establish baseline. Record pass/fail count.");
sb.AppendLine("3. ANALYZE: Use grep (with context_lines=2) + file_read to deeply understand the code you'll modify.");
sb.AppendLine(" - Always check callers/references: grep for function/class names to find all usage points.");
sb.AppendLine(" - Read test files related to the code you're changing to understand expected behavior.");
sb.AppendLine("4. PLAN: Present your analysis + impact assessment. List ALL files that will be modified.");
sb.AppendLine(" - Explain WHY each change is needed and what could break.");
sb.AppendLine(" - Wait for user approval before proceeding.");
sb.AppendLine("5. IMPLEMENT: Apply changes using file_edit (preferred — shows diff). Use file_write only for new files.");
sb.AppendLine(" - Make the MINIMUM changes needed. Don't refactor unrelated code.");
sb.AppendLine(" - Prefer file_edit with replace_all=false for precision edits.");
sb.AppendLine("6. VERIFY: Run build_run action='build' then action='test'. Compare results with baseline.");
sb.AppendLine(" - If tests fail that passed before, fix immediately.");
sb.AppendLine(" - If build fails, analyze error output and correct.");
sb.AppendLine("7. GIT: Use git_tool to check status, create diff, and optionally commit.");
sb.AppendLine("8. REPORT: Summarize changes, test results, and any remaining concerns.");
sb.AppendLine("\n## Development Environment");
sb.AppendLine("Use dev_env_detect to check installed IDEs, runtimes, and build tools before running commands.");
sb.AppendLine("IMPORTANT: Do NOT attempt to install compilers, IDEs, or build tools. Only use what is already installed.");
// 패키지 저장소 정보
sb.AppendLine("\n## Package Repositories");
if (!string.IsNullOrEmpty(code.NexusBaseUrl))
sb.AppendLine($"Enterprise Nexus: {code.NexusBaseUrl}");
sb.AppendLine($"NuGet (.NET): {code.NugetSource}");
sb.AppendLine($"PyPI/Conda (Python): {code.PypiSource}");
sb.AppendLine($"Maven (Java): {code.MavenSource}");
sb.AppendLine($"npm (JavaScript): {code.NpmSource}");
sb.AppendLine("When adding dependencies, use these repository URLs.");
// IDE 정보
if (!string.IsNullOrEmpty(code.PreferredIdePath))
sb.AppendLine($"\nPreferred IDE: {code.PreferredIdePath}");
// 사용자 선택 개발 언어
if (_selectedLanguage != "auto")
{
var langName = _selectedLanguage switch { "python" => "Python", "java" => "Java", "csharp" => "C# (.NET)", "cpp" => "C/C++", "javascript" => "JavaScript/TypeScript", _ => _selectedLanguage };
sb.AppendLine($"\nIMPORTANT: User selected language: {langName}. Prioritize this language for code analysis and generation.");
}
// 언어별 가이드라인
sb.AppendLine("\n## Language Guidelines");
sb.AppendLine("- C# (.NET): Use dotnet CLI. NuGet for packages. Follow Microsoft naming conventions.");
sb.AppendLine("- Python: Use conda/pip. Follow PEP8. Use type hints. Virtual env preferred.");
sb.AppendLine("- Java: Use Maven/Gradle. Follow Google Java Style Guide.");
sb.AppendLine("- C++: Use CMake for build. Follow C++ Core Guidelines.");
sb.AppendLine("- JavaScript/TypeScript: Use npm/yarn. Follow ESLint rules. Vue3 uses Composition API.");
// 코드 품질 + 안전 수칙
sb.AppendLine("\n## Code Quality & Safety");
sb.AppendLine("- NEVER delete or overwrite files without user confirmation.");
sb.AppendLine("- ALWAYS read a file before editing it. Don't guess contents.");
sb.AppendLine("- Prefer file_edit over file_write for existing files (shows diff).");
sb.AppendLine("- Use grep to find ALL references before renaming/removing anything.");
sb.AppendLine("- If unsure about a change's impact, ask the user first.");
sb.AppendLine("- For large refactors, do them incrementally with build verification between steps.");
sb.AppendLine("- Use git_tool action='diff' to review your changes before committing.");
sb.AppendLine("\n## Lint & Format");
sb.AppendLine("After code changes, check for available linters:");
sb.AppendLine("- Python: ruff, black, flake8, pylint");
sb.AppendLine("- JavaScript: eslint, prettier");
sb.AppendLine("- C#: dotnet format");
sb.AppendLine("- C++: clang-format");
sb.AppendLine("Run the appropriate linter via process tool if detected by dev_env_detect.");
if (!string.IsNullOrEmpty(workFolder))
sb.AppendLine($"\nCurrent work folder: {workFolder}");
sb.AppendLine($"File permission mode: {llm.FilePermission}");
// 폴더 데이터 활용
sb.AppendLine("\nFolder Data Usage = ACTIVE. Use folder_map and file_read to understand the codebase.");
sb.AppendLine("Analyze project structure before making changes. Read relevant files to understand context.");
// 프리셋 시스템 프롬프트
lock (_convLock)
{
if (_currentConversation?.SystemCommand is { Length: > 0 } sysCmd)
sb.AppendLine("\n" + sysCmd);
}
// 프로젝트 문맥 파일 (AX.md) 주입 — Phase 27-C: @include 지시어 해석 포함
sb.Append(LoadProjectContext(workFolder));
// 프로젝트 규칙 (.ax/rules/) 자동 주입
sb.Append(BuildProjectRulesSection(workFolder));
// 에이전트 메모리 주입
sb.Append(BuildMemorySection(workFolder));
// 피드백 학습 컨텍스트 주입
sb.Append(BuildFeedbackContext());
// Phase 27-B: 현재 파일 경로 기반 자동 스킬 주입
sb.Append(BuildPathBasedSkillSection());
return sb.ToString();
}
/// <summary>프로젝트 규칙 (.ax/rules/)을 시스템 프롬프트 섹션으로 포맷합니다.</summary>
private string BuildProjectRulesSection(string? workFolder)
{
if (string.IsNullOrEmpty(workFolder)) return "";
if (!Llm.EnableProjectRules) return "";
try
{
var rules = Services.Agent.ProjectRulesService.LoadRules(workFolder);
if (rules.Count == 0) return "";
// 컨텍스트별 필터링: Cowork=document, Code=always (기본)
var when = _activeTab == "Code" ? "always" : "always";
var filtered = Services.Agent.ProjectRulesService.FilterRules(rules, when);
return Services.Agent.ProjectRulesService.FormatForSystemPrompt(filtered);
}
catch (Exception)
{
return "";
}
}
/// <summary>에이전트 메모리를 시스템 프롬프트 섹션으로 포맷합니다.</summary>
private string BuildMemorySection(string? workFolder)
{
if (!Llm.EnableAgentMemory) return "";
var memService = CurrentApp?.MemoryService;
if (memService == null || memService.Count == 0) return "";
// 메모리를 로드 (작업 폴더 변경 시 재로드)
memService.Load(workFolder ?? "");
var all = memService.All;
if (all.Count == 0) return "";
var sb = new System.Text.StringBuilder();
sb.AppendLine("\n## 프로젝트 메모리 (이전 대화에서 학습한 내용)");
sb.AppendLine("아래는 이전 대화에서 학습한 규칙과 선호도입니다. 작업 시 참고하세요.");
sb.AppendLine("새로운 규칙이나 선호도를 발견하면 memory 도구의 save 액션으로 저장하세요.");
sb.AppendLine("사용자가 이전 학습 내용과 다른 지시를 하면 memory 도구의 delete 후 새로 save 하세요.\n");
foreach (var group in all.GroupBy(e => e.Type))
{
var label = group.Key switch
{
"rule" => "프로젝트 규칙",
"preference" => "사용자 선호",
"fact" => "프로젝트 사실",
"correction" => "이전 교정",
_ => group.Key,
};
sb.AppendLine($"[{label}]");
foreach (var e in group.OrderByDescending(e => e.UseCount).Take(15))
sb.AppendLine($"- {e.Content}");
sb.AppendLine();
}
return sb.ToString();
}
/// <summary>워크플로우 시각화 설정이 켜져있으면 분석기 창을 열고 이벤트를 구독합니다.</summary>
private void OpenWorkflowAnalyzerIfEnabled()
{
var llm = Llm;
if (!llm.DevMode || !llm.WorkflowVisualizer) return;
if (_analyzerWindow == null)
{
// 새로 생성
_analyzerWindow = new WorkflowAnalyzerWindow();
_analyzerWindow.Closed += (_, _) => _analyzerWindow = null;
// 테마 리소스 전달
foreach (var dict in System.Windows.Application.Current.Resources.MergedDictionaries)
_analyzerWindow.Resources.MergedDictionaries.Add(dict);
_analyzerWindow.Show();
}
else if (!_analyzerWindow.IsVisible)
{
// Hide()로 숨겨진 창 → 기존 내용 유지한 채 다시 표시
_analyzerWindow.Show();
_analyzerWindow.Activate();
}
else
{
// 이미 보이는 상태 → 새 에이전트 실행을 위해 초기화 후 활성화
_analyzerWindow.Reset();
_analyzerWindow.Activate();
}
// 타임라인 탭으로 전환 (새 실행 시작)
_analyzerWindow.SwitchToTimelineTab();
// 이벤트 구독 (중복 방지)
_agentLoop.EventOccurred -= _analyzerWindow.OnAgentEvent;
_agentLoop.EventOccurred += _analyzerWindow.OnAgentEvent;
}
/// <summary>워크플로우 분석기 버튼의 표시 상태를 갱신합니다.</summary>
private void UpdateAnalyzerButtonVisibility()
{
var llm = Llm;
BtnShowAnalyzer.Visibility = (llm.DevMode && llm.WorkflowVisualizer)
? Visibility.Visible : Visibility.Collapsed;
}
/// <summary>워크플로우 분석기 창을 수동으로 열거나 포커스합니다 (하단 바 버튼).</summary>
private void BtnShowAnalyzer_Click(object sender, MouseButtonEventArgs e)
{
if (_analyzerWindow == null)
{
_analyzerWindow = new WorkflowAnalyzerWindow();
_analyzerWindow.Closed += (_, _) => _analyzerWindow = null;
foreach (var dict in System.Windows.Application.Current.Resources.MergedDictionaries)
_analyzerWindow.Resources.MergedDictionaries.Add(dict);
// 에이전트 이벤트 구독
_agentLoop.EventOccurred -= _analyzerWindow.OnAgentEvent;
_agentLoop.EventOccurred += _analyzerWindow.OnAgentEvent;
_analyzerWindow.Show();
}
else if (!_analyzerWindow.IsVisible)
{
_analyzerWindow.Show();
_analyzerWindow.Activate();
}
else
{
_analyzerWindow.Activate();
}
}
// ─── Phase 18-A: 백그라운드 에이전트 완료 알림 ──────────────────────────
/// <summary>
/// 위임 에이전트(delegate_agent)가 백그라운드에서 완료되면 트레이 알림을 표시합니다.
/// </summary>
private void OnBackgroundAgentCompleted(object? sender, AgentCompletedEventArgs e)
{
// UI 스레드에서 안전하게 실행
Dispatcher.BeginInvoke(() =>
{
var task = e.Task;
if (e.Error != null)
{
Services.NotificationService.Notify(
$"에이전트 '{task.AgentType}' 실패",
$"ID {task.Id}: {e.Error[..Math.Min(80, e.Error.Length)]}");
}
else
{
Services.NotificationService.Notify(
$"에이전트 '{task.AgentType}' 완료",
$"ID {task.Id}: {task.Description[..Math.Min(60, task.Description.Length)]}");
}
});
}
/// <summary>에이전트 루프 동안 누적 토큰 (하단 바 표시용)</summary>
private int _agentCumulativeInputTokens;
private int _agentCumulativeOutputTokens;
private static readonly HashSet<string> WriteToolNames = new(StringComparer.OrdinalIgnoreCase)
{
"file_write", "file_edit", "html_create", "xlsx_create",
"docx_create", "csv_create", "md_create", "script_create",
"diff_preview", "open_external",
};
private void OnAgentEvent(AgentEvent evt)
{
// 에이전트 이벤트를 채팅 UI에 표시 (도구 호출/결과 배너)
AddAgentEventBanner(evt);
AutoScrollIfNeeded();
// 하단 상태바 업데이트
UpdateStatusBar(evt);
// 하단 바 토큰 누적 업데이트 (에이전트 루프 전체 합계)
if (evt.InputTokens > 0 || evt.OutputTokens > 0)
{
_agentCumulativeInputTokens += evt.InputTokens;
_agentCumulativeOutputTokens += evt.OutputTokens;
UpdateStatusTokens(_agentCumulativeInputTokens, _agentCumulativeOutputTokens);
}
// 스티키 진행률 바 업데이트
UpdateAgentProgressBar(evt);
// 계획 뷰어 단계 갱신
if (evt.StepCurrent > 0 && evt.StepTotal > 0)
UpdatePlanViewerStep(evt);
if (evt.Type == AgentEventType.Complete)
CompletePlanViewer();
// 파일 탐색기 자동 새로고침
if (evt.Success && !string.IsNullOrEmpty(evt.FilePath))
RefreshFileTreeIfVisible();
// suggest_actions 도구 결과 → 후속 작업 칩 표시
if (evt.Type == AgentEventType.ToolResult && evt.ToolName == "suggest_actions" && evt.Success)
RenderSuggestActionChips(evt.Summary);
// 파일 생성/수정 결과가 있으면 미리보기 자동 표시 또는 갱신
if (evt.Success && !string.IsNullOrEmpty(evt.FilePath) &&
(evt.Type == AgentEventType.ToolResult || evt.Type == AgentEventType.Complete) &&
WriteToolNames.Contains(evt.ToolName))
{
var autoPreview = Llm.AutoPreview;
if (autoPreview == "auto")
{
// 별도 창 미리보기: 이미 열린 파일이면 새로고침, 아니면 새 탭 추가
if (PreviewWindow.IsOpen)
PreviewWindow.RefreshIfOpen(evt.FilePath);
else
TryShowPreview(evt.FilePath);
// 새 파일이면 항상 표시
if (!PreviewWindow.IsOpen)
TryShowPreview(evt.FilePath);
}
}
}
}