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 SchedulerService? _schedulerService; 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)); // Phase L3-5: 파일 태그 시스템 commandResolver.RegisterHandler(new TagHandler()); // Phase L3-8: 알림 센터 commandResolver.RegisterHandler(new NotifHandler()); // Phase L3-9: 뽀모도로 타이머 commandResolver.RegisterHandler(new PomoHandler()); // ─── Phase L4 핸들러 ────────────────────────────────────────────────── // Phase L4-1: 인라인 파일 탐색기 (Prefix=null, 경로 패턴 감지) commandResolver.RegisterHandler(new FileBrowserHandler()); // ─── Phase L5 핸들러 ────────────────────────────────────────────────── // Phase L5-1: 전용 핫키 목록 관리 (prefix=hotkey) commandResolver.RegisterHandler(new HotkeyHandler(settings)); // Phase L5-2: OCR 화면 텍스트 추출 (prefix=ocr) commandResolver.RegisterHandler(new OcrHandler()); // Phase L5-4: 앱 세션 스냅 (prefix=session) commandResolver.RegisterHandler(new SessionHandler(settings)); // Phase L5-5: 배치 파일 이름변경 (prefix=batchren) commandResolver.RegisterHandler(new BatchRenameHandler()); // Phase L5-6: 자동화 스케줄러 (prefix=sched) var schedulerService = new SchedulerService(settings); schedulerService.Start(); _schedulerService = schedulerService; commandResolver.RegisterHandler(new ScheduleHandler(settings)); // Phase L6-2: 런처 매크로 (prefix=macro) commandResolver.RegisterHandler(new MacroHandler(settings)); // Phase L6-3: 컨텍스트 감지 자동완성 (prefix=ctx) commandResolver.RegisterHandler(new ContextHandler()); // ─── Phase L7 핸들러 ────────────────────────────────────────────────── // L7-1: Git 빠른 조회 (prefix=git) commandResolver.RegisterHandler(new GitHandler()); // L7-2: 정규식 테스터 (prefix=re) commandResolver.RegisterHandler(new RegexHandler()); // L7-3: 시간대 변환기 (prefix=tz) commandResolver.RegisterHandler(new TimeZoneHandler()); // L7-4: 네트워크 진단 (prefix=net) commandResolver.RegisterHandler(new NetDiagHandler()); // ─── Phase L8 핸들러 ────────────────────────────────────────────────── // L8-1: 파일 해시 검증 (prefix=hash) commandResolver.RegisterHandler(new FileHashHandler()); // L8-2: 아카이브 관리 (prefix=zip) commandResolver.RegisterHandler(new ZipHandler()); // L8-3: 시스템 이벤트 로그 (prefix=evt) commandResolver.RegisterHandler(new EventLogHandler()); // L8-4: SSH 퀵 커넥트 (prefix=ssh) commandResolver.RegisterHandler(new SshHandler(settings)); // ─── Phase L9 핸들러 ────────────────────────────────────────────────── // L9-1: 비밀번호 생성기 (prefix=pwd) commandResolver.RegisterHandler(new PasswordGenHandler()); // L9-2: IP 서브넷 계산기 (prefix=subnet) commandResolver.RegisterHandler(new SubnetHandler()); // L9-3: 시스템 정리 (prefix=clean) commandResolver.RegisterHandler(new CleanHandler()); // L9-4: 진수 변환기 (prefix=base) commandResolver.RegisterHandler(new BaseConvertHandler()); // ─── Phase L10 핸들러 ───────────────────────────────────────────────── // L10-1: XML 포맷터·검증기·XPath (prefix=xml) commandResolver.RegisterHandler(new XmlHandler()); // L10-2: UUID/GUID 생성기 (prefix=uuid) commandResolver.RegisterHandler(new UuidHandler()); // L10-3: SSL 인증서 체커 (prefix=cert) commandResolver.RegisterHandler(new CertHandler()); // L10-4: Lorem Ipsum 더미 텍스트 (prefix=lorem) commandResolver.RegisterHandler(new LoremHandler()); // ─── Phase L11 핸들러 ───────────────────────────────────────────────── // L11-1: CSV 뷰어·파서 (prefix=csv) commandResolver.RegisterHandler(new CsvHandler()); // L11-2: JWT 토큰 디코더 (prefix=jwt) commandResolver.RegisterHandler(new JwtHandler()); // L11-3: Cron 표현식 설명기 (prefix=cron) commandResolver.RegisterHandler(new CronHandler()); // L11-4: 유니코드 문자 조회 (prefix=unicode) commandResolver.RegisterHandler(new UnicodeHandler()); // ─── Phase L12 핸들러 ───────────────────────────────────────────────── // L12-1: HTTP 요청 테스터 (prefix=http) commandResolver.RegisterHandler(new HttpTesterHandler()); // L12-2: hosts 파일 관리 (prefix=hosts) commandResolver.RegisterHandler(new HostsHandler()); // L12-3: 모스 부호 변환기 (prefix=morse) commandResolver.RegisterHandler(new MorseHandler()); // L12-4: 시작 프로그램 조회 (prefix=startup) commandResolver.RegisterHandler(new StartupHandler()); // ─── Phase L13 핸들러 ───────────────────────────────────────────────── // L13-1: DNS 레코드 조회 (prefix=dns) commandResolver.RegisterHandler(new DnsQueryHandler()); // L13-2: PATH 환경변수 뷰어 (prefix=path) commandResolver.RegisterHandler(new PathHandler()); // L13-3: 드라이브 정보 (prefix=drive) commandResolver.RegisterHandler(new DriveHandler()); // L13-4: 나이·D-day 계산기 (prefix=age) commandResolver.RegisterHandler(new AgeHandler()); // ─── Phase L14 핸들러 ───────────────────────────────────────────────── // L14-1: Wake-on-LAN (prefix=wol) commandResolver.RegisterHandler(new WolHandler()); // L14-2: 레지스트리 조회 (prefix=reg) commandResolver.RegisterHandler(new RegHandler()); // L14-3: 팁·할인·분할 계산기 (prefix=tip) commandResolver.RegisterHandler(new TipHandler()); // L14-4: 시스템 폰트 목록 (prefix=font) commandResolver.RegisterHandler(new FontHandler()); // ─── Phase L15 핸들러 ───────────────────────────────────────────────── // L15-1: WSL 관리 (prefix=wsl) commandResolver.RegisterHandler(new WslHandler()); // L15-2: 환율 변환기 (prefix=currency) commandResolver.RegisterHandler(new CurrencyHandler()); // L15-3: BMI·건강 계산기 (prefix=bmi) commandResolver.RegisterHandler(new BmiHandler()); // L15-4: Markdown 분석기 (prefix=md) commandResolver.RegisterHandler(new MdHandler()); // ─── Phase L16 핸들러 ───────────────────────────────────────────────── // L16-1: ping·tracert 실행기 (prefix=ping) commandResolver.RegisterHandler(new PingHandler()); // L16-2: Docker 컨테이너·이미지 조회 (prefix=docker) commandResolver.RegisterHandler(new DockerHandler()); // L16-3: 할 일 목록 (prefix=todo) commandResolver.RegisterHandler(new TodoHandler()); // L16-4: 텍스트 → 표 변환기 (prefix=table) commandResolver.RegisterHandler(new TableHandler()); // ─── Phase L17 핸들러 ───────────────────────────────────────────────── // L17-1: 단위 변환기 (prefix=unit) commandResolver.RegisterHandler(new UnitHandler()); // L17-2: 숫자 포맷·읽기 변환기 (prefix=num) commandResolver.RegisterHandler(new NumHandler()); // L17-3: YAML 파서·포맷터 (prefix=yaml) commandResolver.RegisterHandler(new YamlHandler()); // L17-4: .gitignore 생성기 (prefix=gitignore) commandResolver.RegisterHandler(new GitignoreHandler()); // ─── Phase L18 핸들러 ───────────────────────────────────────────────── // L18-1: SQL 포맷터·분석기 (prefix=sql) commandResolver.RegisterHandler(new SqlHandler()); // L18-2: 텍스트 케이스 변환기 (prefix=text) commandResolver.RegisterHandler(new TextCaseHandler()); // L18-3: 화면 비율·해상도 계산기 (prefix=aspect) commandResolver.RegisterHandler(new AspectHandler()); // L18-4: IT·개발 약어 사전 (prefix=abbr) commandResolver.RegisterHandler(new AbbrHandler()); // L19-1: 공학 계산기 (prefix=calc) commandResolver.RegisterHandler(new CalcHandler()); // L19-2: 타이머·알람 (prefix=timer) commandResolver.RegisterHandler(new TimerHandler()); // L19-3: IP 주소 유틸리티 (prefix=ip) commandResolver.RegisterHandler(new IpInfoHandler()); // L19-4: npm/yarn/pnpm 명령어 생성기 (prefix=npm) commandResolver.RegisterHandler(new NpmHandler()); // L20-1: 16진수·바이트 변환기 (prefix=hex) commandResolver.RegisterHandler(new HexHandler()); // L20-2: 랜덤 생성기 (prefix=rand) commandResolver.RegisterHandler(new RandHandler()); // L20-3: 문자열 조작 도구 (prefix=str) commandResolver.RegisterHandler(new StrHandler()); // L20-4: Unix 파일 권한 계산기 (prefix=perm) commandResolver.RegisterHandler(new PermHandler()); // L21-1: TOML 파서·분석기 (prefix=toml) commandResolver.RegisterHandler(new TomlHandler()); // L21-2: 로그 파일 분석기 (prefix=log) commandResolver.RegisterHandler(new LogHandler()); // L21-3: PowerShell 명령 생성기 (prefix=ps) commandResolver.RegisterHandler(new PsHandler()); // L21-4: 키보드 단축키 참조 사전 (prefix=key) commandResolver.RegisterHandler(new KeyHandler()); // L22-1: 프로세스 상세 조회·정리 (prefix=proc) commandResolver.RegisterHandler(new ProcHandler()); // L22-2: Excel 함수 레퍼런스 (prefix=xl) commandResolver.RegisterHandler(new XlHandler()); // L22-3: Python pip 명령 생성기 (prefix=pip) commandResolver.RegisterHandler(new PipHandler()); // L22-4: 업무 양식·문서 구조 템플릿 (prefix=form) commandResolver.RegisterHandler(new FormHandler()); // ─── 플러그인 로드 ──────────────────────────────────────────────────── 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); // ─── 파일 아이콘 캐시 워밍업 (앱 유휴 시점에 자주 쓰는 확장자 미리 추출) ── Dispatcher.BeginInvoke( () => AxCopilot.Services.IconCacheService.WarmUp(), System.Windows.Threading.DispatcherPriority.SystemIdle); // ─── 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.CustomHotkeyTriggered += OnCustomHotkeyTriggered; _inputListener.HookFailed += OnHookFailed; // 설정에 저장된 핫키로 초기화 (기본: Alt+Space) _inputListener.UpdateHotkey(settings.Settings.Hotkey); // 글로벌 캡처 단축키 초기화 _inputListener.UpdateCaptureHotkey( settings.Settings.ScreenCapture.GlobalHotkey, settings.Settings.ScreenCapture.GlobalHotkeyEnabled); // 항목별 전용 핫키 초기화 _inputListener.UpdateCustomHotkeys(settings.Settings.CustomHotkeys); 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(); } }); } /// 현재 선택된 텍스트를 가져옵니다 (Ctrl+C 시뮬레이션). /// 후크 스레드에서 호출됩니다 (UI 스레드 아님). 클립보드 접근은 STA 스레드가 필요합니다. 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()); // 클립보드 업데이트 대기 (앱마다 다름) 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; } } /// AI 텍스트 명령을 AX Agent 대화로 전달합니다. 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 OnCustomHotkeyTriggered(object? sender, Core.CustomHotkeyEventArgs e) { // 전용 핫키 발동: 런처를 열지 않고 대상을 직접 실행 Handlers.HotkeyHandler.ExecuteHotkeyTarget(e.Target, e.Type); } private void OnHookFailed(object? sender, EventArgs e) { Dispatcher.Invoke(() => { _trayIcon?.ShowBalloonTip(3000, "AX Copilot", $"글로벌 단축키 등록에 실패했습니다.\n다른 앱과 {_settings?.Settings.Hotkey ?? "단축키"}가 충돌하고 있을 수 있습니다.", ToolTipIcon.Warning); }); } }