Files
AX-Copilot/src/AxCopilot/App.xaml.cs
lacvet aa907d7b79 [Phase46] 대형 파일 분할 리팩터링 2차 — 19개 신규 파셜 파일 생성
## 분할 대상 및 결과

### AgentLoopService.cs (1,334줄 → 846줄)
- AgentLoopService.HtmlReport.cs (151줄): AutoSaveAsHtml, ConvertTextToHtml, EscapeHtml
- AgentLoopService.Verification.cs (349줄): 도구 분류 판별 + RunPostToolVerificationAsync + EmitEvent + CheckDecisionRequired + FormatToolCallSummary

### ChatWindow 분할 (8개 신규 파셜 파일)
- ChatWindow.PlanViewer.cs (474줄): 계획 뷰어 — AddPlanningCard, AddDecisionButtons, CollapseDecisionButtons, ShowPlanButton 등 8개 메서드
- ChatWindow.EventBanner.cs (411줄): AddAgentEventBanner, BuildFileQuickActions
- ChatWindow.TaskDecomposition.cs (1,170줄 → 307줄): RenderSuggestActionChips, BuildFeedbackContext, UpdateProgressBar, BuildDiffView 잔류
- ChatWindow.BottomBar.cs (345줄): BuildBottomBar, BuildCodeBottomBar, ShowLogLevelMenu, ShowLanguageMenu 등
- ChatWindow.MoodMenu.cs (456줄): ShowFormatMenu, ShowMoodMenu, ShowCustomMoodDialog 등
- ChatWindow.CustomPresets.cs (978줄 → 203줄): ShowCustomPresetDialog, SelectTopic 잔류
- ChatWindow.ConversationMenu.cs (255줄): ShowConversationMenu (카테고리/삭제/즐겨찾기 팝업)
- ChatWindow.ConversationTitleEdit.cs (108줄): EnterTitleEditMode

### SettingsViewModel 분할
- SettingsViewModel.LlmProperties.cs (417줄): LLM·에이전트 관련 바인딩 프로퍼티
- SettingsViewModel.Properties.cs (837줄 → 427줄): 기능 토글·테마·스니펫 등 앱 수준 프로퍼티

### TemplateService 분할
- TemplateService.Css.cs (559줄): 11종 CSS 테마 문자열 상수
- TemplateService.cs (734줄 → 179줄): 메서드 로직만 잔류

### PlanViewerWindow 분할
- PlanViewerWindow.StepRenderer.cs (616줄): RenderSteps + SwapSteps + EditStep + 버튼 빌더 9개
- PlanViewerWindow.cs (931줄 → 324줄): Win32/생성자/공개 API 잔류

### App.xaml.cs 분할 (776줄 → 452줄)
- App.Settings.cs (252줄): SetupTrayIcon, OpenSettings, ToggleDockBar, RefreshDockBar, OpenAiChat
- App.Helpers.cs (92줄): LoadAppIcon, IsAutoStartEnabled, SetAutoStart, OnExit

### LlmService.ToolUse.cs 분할 (719줄 → 115줄)
- LlmService.ClaudeTools.cs (180줄): SendClaudeWithToolsAsync, BuildClaudeToolBody
- LlmService.GeminiTools.cs (175줄): SendGeminiWithToolsAsync, BuildGeminiToolBody
- LlmService.OpenAiTools.cs (215줄): SendOpenAiWithToolsAsync, BuildOpenAiToolBody

### SettingsWindow.UI.cs 분할 (802줄 → 310줄)
- SettingsWindow.Storage.cs (167줄): RefreshStorageInfo, BtnStorageCleanup_Click 등
- SettingsWindow.HotkeyUI.cs (127줄): RefreshHotkeyBadges, EnsureHotkeyInCombo, GetKeyName 등
- SettingsWindow.DevMode.cs (90줄): DevModeCheckBox_Checked, UpdateDevModeContentVisibility

## 빌드 결과: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 20:51:26 +09:00

453 lines
22 KiB
C#

using System.Windows;
using System.Windows.Forms;
using Microsoft.Win32;
using AxCopilot.Core;
using AxCopilot.Handlers;
using AxCopilot.Services;
using AxCopilot.Services.Agent;
using AxCopilot.ViewModels;
using AxCopilot.Views;
namespace AxCopilot;
public partial class App : System.Windows.Application
{
private System.Threading.Mutex? _singleInstanceMutex;
private InputListener? _inputListener;
private LauncherWindow? _launcher;
private NotifyIcon? _trayIcon;
private Views.TrayMenuWindow? _trayMenu;
private SettingsService? _settings;
private SettingsWindow? _settingsWindow;
private PluginHost? _pluginHost;
private ClipboardHistoryService? _clipboardHistory;
private DockBarWindow? _dockBar;
private FileDialogWatcher? _fileDialogWatcher;
private volatile IndexService? _indexService;
public IndexService? IndexService => _indexService;
public SettingsService? SettingsService => _settings;
public ClipboardHistoryService? ClipboardHistoryService => _clipboardHistory;
private SessionTrackingService? _sessionTracking;
private WorktimeReminderService? _worktimeReminder;
private ScreenCaptureHandler? _captureHandler;
private AgentMemoryService? _memoryService;
public AgentMemoryService? MemoryService => _memoryService;
private LlmService? _sharedLlm;
private PluginInstallService? _pluginInstallService;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// ─── 보안: 디버거/디컴파일러 감지 (Release 빌드만 활성) ───
Security.AntiTamper.Check();
// 전역 예외 핸들러 — 미처리 예외 시 로그 + 메시지 (크래시 방지)
DispatcherUnhandledException += (_, ex) =>
{
LogService.Error($"미처리 예외: {ex.Exception}");
CustomMessageBox.Show(
$"오류가 발생했습니다:\n{ex.Exception.Message}\n\n로그 폴더를 확인하세요.",
"AX Copilot 오류", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
ex.Handled = true;
};
// ─── 중복 실행 방지 ──────────────────────────────────────────────────
_singleInstanceMutex = new System.Threading.Mutex(
initiallyOwned: true, "Global\\AXCopilot_SingleInstance", out bool createdNew);
if (!createdNew)
{
_singleInstanceMutex.Dispose();
_singleInstanceMutex = null;
CustomMessageBox.Show("AX Copilot가 이미 실행 중입니다.\n트레이 아이콘을 확인하세요.",
"AX Copilot", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information);
Shutdown();
return;
}
LogService.Info("=== AX Copilot 시작 ===");
// ─── 서비스 초기화 ────────────────────────────────────────────────────
_settings = new SettingsService();
var settings = _settings;
settings.Load();
// Phase L3 공유 서비스
_sharedLlm = new LlmService(settings);
_pluginInstallService = new PluginInstallService();
// 마이그레이션 알림 (UI가 준비된 후 표시)
if (settings.MigrationSummary != null)
{
Dispatcher.BeginInvoke(() =>
{
CustomMessageBox.Show(
settings.MigrationSummary,
"AX Copilot — 설정 업데이트",
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Information);
}, System.Windows.Threading.DispatcherPriority.Loaded);
}
// 언어 초기화
L10n.SetLanguage(settings.Settings.Launcher.Language);
// 첫 실행 시 자동 시작 등록 (레지스트리에 아직 없으면)
if (!IsAutoStartEnabled())
SetAutoStart(true);
_memoryService = new AgentMemoryService();
_memoryService.Load(settings.Settings.Llm.WorkFolder);
_indexService = new IndexService(settings);
var indexService = _indexService;
var fuzzyEngine = new FuzzyEngine(indexService);
var commandResolver = new CommandResolver(fuzzyEngine, settings);
var contextManager = new ContextManager(settings);
// ─── 세션 추적 / 사용 통계 ────────────────────────────────────────────
_sessionTracking = new SessionTrackingService();
// ─── 잠금 해제 알림 서비스 ────────────────────────────────────────────
_worktimeReminder = new WorktimeReminderService(settings, Dispatcher);
// ─── 클립보드 히스토리 서비스 ──────────────────────────────────────────
_clipboardHistory = new ClipboardHistoryService(settings);
// ─── 빌트인 핸들러 등록 ──────────────────────────────────────────────
commandResolver.RegisterHandler(new CalculatorHandler());
commandResolver.RegisterHandler(new SystemCommandHandler(settings));
commandResolver.RegisterHandler(new SnippetHandler(settings));
commandResolver.RegisterHandler(new ClipboardHistoryHandler(_clipboardHistory));
commandResolver.RegisterHandler(new WorkspaceHandler(contextManager, settings));
commandResolver.RegisterHandler(new UrlAliasHandler(settings));
commandResolver.RegisterHandler(new FolderAliasHandler(settings));
commandResolver.RegisterHandler(new BatchHandler(settings));
commandResolver.RegisterHandler(new ClipboardHandler(settings));
commandResolver.RegisterHandler(new BookmarkHandler());
commandResolver.RegisterHandler(new WebSearchHandler(settings));
commandResolver.RegisterHandler(new ProcessHandler());
commandResolver.RegisterHandler(new MediaHandler());
commandResolver.RegisterHandler(new SystemInfoHandler());
commandResolver.RegisterHandler(new StarInfoHandler()); // * 단축키 (info와 동일)
commandResolver.RegisterHandler(new EmojiHandler());
commandResolver.RegisterHandler(new ColorHandler());
commandResolver.RegisterHandler(new RecentFilesHandler());
commandResolver.RegisterHandler(new NoteHandler());
commandResolver.RegisterHandler(new UninstallHandler());
commandResolver.RegisterHandler(new PortHandler());
commandResolver.RegisterHandler(new EnvHandler());
commandResolver.RegisterHandler(new JsonHandler());
commandResolver.RegisterHandler(new EncodeHandler());
commandResolver.RegisterHandler(new SnapHandler());
_captureHandler = new ScreenCaptureHandler(settings);
commandResolver.RegisterHandler(_captureHandler);
commandResolver.RegisterHandler(new ColorPickHandler());
commandResolver.RegisterHandler(new DateCalcHandler());
commandResolver.RegisterHandler(new ServiceHandler(_clipboardHistory));
commandResolver.RegisterHandler(new ClipboardPipeHandler());
commandResolver.RegisterHandler(new JournalHandler());
commandResolver.RegisterHandler(new RoutineHandler());
commandResolver.RegisterHandler(new BatchTextHandler());
commandResolver.RegisterHandler(new DiffHandler(_clipboardHistory));
commandResolver.RegisterHandler(new WindowSwitchHandler());
commandResolver.RegisterHandler(new RunHandler());
commandResolver.RegisterHandler(new TextStatsHandler());
commandResolver.RegisterHandler(new FavoriteHandler());
commandResolver.RegisterHandler(new RenameHandler());
commandResolver.RegisterHandler(new MonitorHandler());
commandResolver.RegisterHandler(new ScaffoldHandler());
commandResolver.RegisterHandler(new EverythingHandler());
commandResolver.RegisterHandler(new HelpHandler(settings));
commandResolver.RegisterHandler(new ChatHandler(settings));
// ─── Phase L3 핸들러 ──────────────────────────────────────────────────
commandResolver.RegisterHandler(new QuickLinkHandler(settings));
var snippetTemplateSvc = new SnippetTemplateService(settings, _sharedLlm);
commandResolver.RegisterHandler(new AiSnippetHandler(settings, snippetTemplateSvc));
commandResolver.RegisterHandler(new WebSearchSummaryHandler(settings, _sharedLlm));
// ─── 플러그인 로드 ────────────────────────────────────────────────────
var pluginHost = new PluginHost(settings, commandResolver);
pluginHost.LoadAll();
_pluginHost = pluginHost;
// ─── 런처 윈도우 ──────────────────────────────────────────────────────
var vm = new LauncherViewModel(commandResolver, settings);
_launcher = new LauncherWindow(vm)
{
OpenSettingsAction = OpenSettings
};
// ─── 클립보드 히스토리 초기화 (메시지 펌프 시작 직후 — 런처 표시 불필요) ──
Dispatcher.BeginInvoke(
() => _clipboardHistory?.Initialize(),
System.Windows.Threading.DispatcherPriority.ApplicationIdle);
// ─── ChatWindow 미리 생성 (앱 유휴 시점에 숨겨진 채로 초기화) ──────────
// 이후 OpenAiChat() 시 창 생성 비용 없이 즉시 열림
Dispatcher.BeginInvoke(
() => PrewarmChatWindow(),
System.Windows.Threading.DispatcherPriority.SystemIdle);
// ─── 인덱스 빌드 (백그라운드) + 완료 후 FileSystemWatcher 시작 ────────
_ = indexService.BuildAsync().ContinueWith(_ => indexService.StartWatchers());
// ─── 글로벌 훅 + 스니펫 확장기 ───────────────────────────────────────
_inputListener = new InputListener();
_inputListener.HotkeyTriggered += OnHotkeyTriggered;
_inputListener.CaptureHotkeyTriggered += OnCaptureHotkeyTriggered;
_inputListener.HookFailed += OnHookFailed;
// 설정에 저장된 핫키로 초기화 (기본: Alt+Space)
_inputListener.UpdateHotkey(settings.Settings.Hotkey);
// 글로벌 캡처 단축키 초기화
_inputListener.UpdateCaptureHotkey(
settings.Settings.ScreenCapture.GlobalHotkey,
settings.Settings.ScreenCapture.GlobalHotkeyEnabled);
var snippetExpander = new SnippetExpander(settings);
_inputListener.KeyFilter = snippetExpander.HandleKey;
_inputListener.Start();
// ─── 시스템 트레이 ─────────────────────────────────────────────────────
SetupTrayIcon(pluginHost, settings);
// ─── 파일 대화상자 감지 ──────────────────────────────────────────────
_fileDialogWatcher = new FileDialogWatcher();
_fileDialogWatcher.FileDialogOpened += (_, hwnd) =>
{
Dispatcher.Invoke(() =>
{
if (_launcher == null || _launcher.IsVisible) return;
if (_settings?.Settings.Launcher.EnableFileDialogIntegration != true) return;
WindowTracker.Capture();
_launcher.Show();
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input,
() => _launcher.SetInputText("cd "));
});
};
_fileDialogWatcher.Start();
// 독 바 자동 표시
if (settings.Settings.Launcher.DockBarAutoShow)
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded, () => ToggleDockBar());
LogService.Info($"초기화 완료. {settings.Settings.Hotkey}로 실행하세요.");
}
private void OnHotkeyTriggered(object? sender, EventArgs e)
{
// 런처가 열리기 전 현재 활성 창을 저장 (SnapHandler, ScreenCaptureHandler용)
WindowTracker.Capture();
// 선택 텍스트 감지는 UI 스레드 밖에서 수행해야 SendInput이 정상 처리됨
var launcherSettings = _settings?.Settings.Launcher;
var enableTextAction = launcherSettings?.EnableTextAction == true;
// ── 런처 토글(닫기)은 텍스트 감지 없이 즉시 처리 ──
bool isVisible = false;
Dispatcher.Invoke(() => { isVisible = _launcher?.IsVisible == true; });
if (isVisible)
{
Dispatcher.Invoke(() => _launcher?.Hide());
return;
}
// ── 텍스트 감지가 비활성이면 즉시 런처 표시 ──
if (!enableTextAction)
{
Dispatcher.Invoke(() =>
{
if (_launcher == null) return;
UsageStatisticsService.RecordLauncherOpen();
_launcher.Show();
});
return;
}
// ── 텍스트 감지 활성: 런처를 먼저 열고, 텍스트 감지를 비동기로 처리 ──
string? selectedText = TryGetSelectedText();
Dispatcher.Invoke(() =>
{
if (_launcher == null) return;
if (!string.IsNullOrWhiteSpace(selectedText))
{
var enabledCmds = launcherSettings?.TextActionCommands ?? new();
// 활성 명령이 1개뿐이면 팝업 없이 바로 실행
if (enabledCmds.Count == 1)
{
var directAction = TextActionPopup.AvailableCommands
.FirstOrDefault(c => c.Key == enabledCmds[0]);
if (!string.IsNullOrEmpty(directAction.Key))
{
var actionResult = enabledCmds[0] switch
{
"translate" => TextActionPopup.ActionResult.Translate,
"summarize" => TextActionPopup.ActionResult.Summarize,
"grammar" => TextActionPopup.ActionResult.GrammarFix,
"explain" => TextActionPopup.ActionResult.Explain,
"rewrite" => TextActionPopup.ActionResult.Rewrite,
_ => TextActionPopup.ActionResult.None,
};
if (actionResult != TextActionPopup.ActionResult.None)
{
ExecuteTextAction(actionResult, selectedText);
return;
}
}
}
// 여러 개 → 팝업 표시
var popup = new TextActionPopup(selectedText, enabledCmds);
popup.Closed += (_, _) =>
{
switch (popup.SelectedAction)
{
case TextActionPopup.ActionResult.OpenLauncher:
UsageStatisticsService.RecordLauncherOpen();
_launcher.Show();
break;
case TextActionPopup.ActionResult.None:
break; // Esc 또는 포커스 잃음
default:
// AI 명령 실행 → AX Agent 대화로 전달
ExecuteTextAction(popup.SelectedAction, popup.SelectedText);
break;
}
};
popup.Show();
}
else
{
UsageStatisticsService.RecordLauncherOpen();
_launcher.Show();
}
});
}
/// <summary>현재 선택된 텍스트를 가져옵니다 (Ctrl+C 시뮬레이션).</summary>
/// <remarks>후크 스레드에서 호출됩니다 (UI 스레드 아님). 클립보드 접근은 STA 스레드가 필요합니다.</remarks>
private string? TryGetSelectedText()
{
try
{
// 클립보드는 STA 스레드에서만 접근 가능 → Dispatcher로 읽기
string? prevText = null;
bool hadText = false;
Dispatcher.Invoke(() =>
{
hadText = System.Windows.Clipboard.ContainsText();
prevText = hadText ? System.Windows.Clipboard.GetText() : null;
System.Windows.Clipboard.Clear();
});
System.Threading.Thread.Sleep(30);
// Ctrl+C 시뮬레이션 (후크 스레드에서 → 대상 앱이 처리)
var inputs = new[]
{
new NativeMethods.INPUT { type = 1, u = new NativeMethods.InputUnion { ki = new NativeMethods.KEYBDINPUT { wVk = 0x11 } } },
new NativeMethods.INPUT { type = 1, u = new NativeMethods.InputUnion { ki = new NativeMethods.KEYBDINPUT { wVk = 0x43 } } },
new NativeMethods.INPUT { type = 1, u = new NativeMethods.InputUnion { ki = new NativeMethods.KEYBDINPUT { wVk = 0x43, dwFlags = 0x0002 } } },
new NativeMethods.INPUT { type = 1, u = new NativeMethods.InputUnion { ki = new NativeMethods.KEYBDINPUT { wVk = 0x11, dwFlags = 0x0002 } } },
};
NativeMethods.SendInput((uint)inputs.Length, inputs, System.Runtime.InteropServices.Marshal.SizeOf<NativeMethods.INPUT>());
// 클립보드 업데이트 대기 (앱마다 다름)
System.Threading.Thread.Sleep(80);
// 클립보드에서 새 텍스트 확인 (STA 필요)
string? selected = null;
Dispatcher.Invoke(() =>
{
if (System.Windows.Clipboard.ContainsText())
{
var copied = System.Windows.Clipboard.GetText();
if (copied != prevText && !string.IsNullOrWhiteSpace(copied))
selected = copied;
}
// 클립보드 복원
if (selected != null && prevText != null)
System.Windows.Clipboard.SetText(prevText);
else if (selected != null && !hadText)
System.Windows.Clipboard.Clear();
});
return selected;
}
catch (Exception)
{
return null;
}
}
/// <summary>AI 텍스트 명령을 AX Agent 대화로 전달합니다.</summary>
private void ExecuteTextAction(TextActionPopup.ActionResult action, string text)
{
var prompt = action switch
{
TextActionPopup.ActionResult.Translate => $"다음 텍스트를 번역해줘:\n\n{text}",
TextActionPopup.ActionResult.Summarize => $"다음 텍스트를 요약해줘:\n\n{text}",
TextActionPopup.ActionResult.GrammarFix => $"다음 텍스트의 문법을 교정해줘:\n\n{text}",
TextActionPopup.ActionResult.Explain => $"다음 텍스트를 쉽게 설명해줘:\n\n{text}",
TextActionPopup.ActionResult.Rewrite => $"다음 텍스트를 다시 작성해줘:\n\n{text}",
_ => text
};
// ! 프리픽스로 AX Agent에 전달
_launcher?.Show();
_launcher?.SetInputText($"! {prompt}");
}
private static class NativeMethods
{
[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct INPUT { public uint type; public InputUnion u; }
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit)]
public struct InputUnion { [System.Runtime.InteropServices.FieldOffset(0)] public KEYBDINPUT ki; }
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
}
private void OnCaptureHotkeyTriggered(object? sender, EventArgs e)
{
// 런처 없이 직접 캡처 실행
WindowTracker.Capture();
var mode = _settings?.Settings.ScreenCapture.GlobalHotkeyMode ?? "screen";
Dispatcher.Invoke(async () =>
{
if (_captureHandler == null) return;
try
{
await _captureHandler.CaptureDirectAsync(mode);
}
catch (Exception ex)
{
LogService.Error($"글로벌 캡처 실패: {ex.Message}");
}
});
}
private void OnHookFailed(object? sender, EventArgs e)
{
Dispatcher.Invoke(() =>
{
_trayIcon?.ShowBalloonTip(3000, "AX Copilot",
$"글로벌 단축키 등록에 실패했습니다.\n다른 앱과 {_settings?.Settings.Hotkey ?? ""}가 충돌하고 있을 수 있습니다.",
ToolTipIcon.Warning);
});
}
}