런처 Agent Compare 기능 1차 이식 및 현재 런처 구조 연결

- Agent Compare 기준으로 런처 빠른 실행 칩, 검색 히스토리 탐색, 선택 항목 미리보기 패널을 현재 런처에 이식
- 하단 위젯 바, QuickLook(F3), 화면 OCR(F4), 관련 서비스/partial 파일을 현재 LauncherWindow/LauncherViewModel 구조에 연결
- UsageRankingService 상위 항목 조회와 SearchHistoryService를 추가해 실행 상위 경로/검색 기록이 실제 런처 동작에 반영되도록 정리
- README.md, docs/DEVELOPMENT.md에 이식 범위와 검증 결과를 2026-04-05 11:58 (KST) 기준으로 기록

검증 결과
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
This commit is contained in:
2026-04-05 11:51:43 +09:00
parent 0336904258
commit f7cafe0cfc
17 changed files with 2518 additions and 24 deletions

View File

@@ -14,6 +14,8 @@ namespace AxCopilot.Views;
public partial class LauncherWindow : Window
{
private static App? CurrentApp => System.Windows.Application.Current as App;
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
@@ -34,6 +36,7 @@ public partial class LauncherWindow : Window
private readonly LauncherViewModel _vm;
private System.Windows.Threading.DispatcherTimer? _indexStatusTimer;
private System.Windows.Threading.DispatcherTimer? _toastTimer;
/// <summary>Ctrl+, 단축키로 설정 창을 여는 콜백 (App.xaml.cs에서 주입)</summary>
public Action? OpenSettingsAction { get; set; }
@@ -71,16 +74,9 @@ public partial class LauncherWindow : Window
Dispatcher.BeginInvoke(() =>
{
var svc = app.IndexService;
IndexStatusText.Text = $"✓ {svc.LastIndexCount:N0}개 항목 색인됨 ({svc.LastIndexDuration.TotalSeconds:F1}초)";
IndexStatusText.Visibility = Visibility.Visible;
// 기존 타이머 정리 후 5초 후 자동 숨기기
_indexStatusTimer?.Stop();
_indexStatusTimer = new System.Windows.Threading.DispatcherTimer
{
Interval = TimeSpan.FromSeconds(5)
};
_indexStatusTimer.Tick += (_, _) => { IndexStatusText.Visibility = Visibility.Collapsed; _indexStatusTimer.Stop(); };
_indexStatusTimer.Start();
ShowIndexStatus(
$"✓ {svc.LastIndexCount:N0}개 항목 색인됨 ({svc.LastIndexDuration.TotalSeconds:F1}초)",
TimeSpan.FromSeconds(5));
});
};
}
@@ -88,7 +84,7 @@ public partial class LauncherWindow : Window
private void Window_Loaded(object sender, RoutedEventArgs e)
{
CenterOnScreen();
ApplyInitialPlacement();
ApplyTheme();
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input, () =>
{
@@ -151,7 +147,7 @@ public partial class LauncherWindow : Window
_vm.OnShown();
_vm.InputText = "";
base.Show();
CenterOnScreen();
ApplyInitialPlacement();
AnimateIn();
// 포그라운드 강제 + 포커스를 3단계로 보장
@@ -695,6 +691,64 @@ public partial class LauncherWindow : Window
};
}
private void ApplyInitialPlacement()
{
if (!TryRestoreRememberedPosition())
CenterOnScreen();
UpdateRememberedPositionCache();
}
private bool TryRestoreRememberedPosition()
{
var launcher = CurrentApp?.SettingsService?.Settings?.Launcher;
if (launcher == null || !launcher.RememberPosition) return false;
if (launcher.LastLeft < 0 || launcher.LastTop < 0) return false;
var rememberPoint = new Point(launcher.LastLeft, launcher.LastTop);
if (!IsVisibleOnAnyScreen(rememberPoint)) return false;
Left = launcher.LastLeft;
Top = launcher.LastTop;
return true;
}
private static bool IsVisibleOnAnyScreen(Point point)
{
foreach (var screen in FormsScreen.AllScreens)
{
var bounds = screen.WorkingArea;
if (point.X >= bounds.Left && point.X <= bounds.Right - 40 &&
point.Y >= bounds.Top && point.Y <= bounds.Bottom - 40)
{
return true;
}
}
return false;
}
private void UpdateRememberedPositionCache()
{
var launcher = CurrentApp?.SettingsService?.Settings?.Launcher;
if (launcher == null || !launcher.RememberPosition || !IsLoaded) return;
launcher.LastLeft = Left;
launcher.LastTop = Top;
}
private void SaveRememberedPosition()
{
var app = CurrentApp;
var settingsService = app?.SettingsService;
if (settingsService == null) return;
var launcher = settingsService.Settings.Launcher;
if (launcher == null || !launcher.RememberPosition || !IsLoaded) return;
UpdateRememberedPositionCache();
settingsService.Save();
}
// 지원 테마 이름 목록
private static readonly HashSet<string> KnownThemes =
new(StringComparer.OrdinalIgnoreCase)
@@ -1031,8 +1085,7 @@ public partial class LauncherWindow : Window
{
var app = (App)System.Windows.Application.Current;
_ = app.IndexService?.BuildAsync(CancellationToken.None);
IndexStatusText.Text = "⟳ 인덱스 재구축 중…";
IndexStatusText.Visibility = Visibility.Visible;
ShowIndexStatus("⟳ 인덱스 재구축 중…", TimeSpan.FromSeconds(8));
e.Handled = true;
return;
}
@@ -1256,6 +1309,36 @@ public partial class LauncherWindow : Window
return;
}
// ─── F3 → 파일 빠른 미리보기 (QuickLook 토글) ───────────────────────
if (e.Key == Key.F3)
{
ToggleQuickLook();
e.Handled = true;
return;
}
// ─── F4 → 화면 영역 OCR 즉시 실행 ─────────────────────────────────
if (e.Key == Key.F4)
{
Hide();
_ = Task.Run(async () =>
{
try
{
var handler = new Handlers.OcrHandler();
var item = new SDK.LauncherItem(
"화면 영역 텍스트 추출", "", null, "__ocr_region__");
await handler.ExecuteAsync(item, CancellationToken.None);
}
catch (Exception ex)
{
Services.LogService.Error($"F4 OCR 실행 오류: {ex.Message}");
}
});
e.Handled = true;
return;
}
// ─── Ctrl+1~9 → n번째 결과 즉시 실행 ───────────────────────────────
if (mod == ModifierKeys.Control)
{
@@ -1295,6 +1378,8 @@ public partial class LauncherWindow : Window
"[ 기능 ]",
"F1 도움말",
"F2 파일 이름 바꾸기",
"F3 파일 빠른 미리보기",
"F4 화면 OCR",
"F5 인덱스 새로 고침",
"Delete 항목 제거",
"Ctrl+, 설정",
@@ -1331,14 +1416,14 @@ public partial class LauncherWindow : Window
var fadeIn = (System.Windows.Media.Animation.Storyboard)FindResource("ToastFadeIn");
fadeIn.Begin(this);
_indexStatusTimer?.Stop();
_indexStatusTimer = new System.Windows.Threading.DispatcherTimer
_toastTimer?.Stop();
_toastTimer = new System.Windows.Threading.DispatcherTimer
{
Interval = TimeSpan.FromSeconds(2)
};
_indexStatusTimer.Tick += (_, _) =>
_toastTimer.Tick += (_, _) =>
{
_indexStatusTimer.Stop();
_toastTimer.Stop();
// 페이드아웃 후 Collapsed
var fadeOut = (System.Windows.Media.Animation.Storyboard)FindResource("ToastFadeOut");
EventHandler? onCompleted = null;
@@ -1350,6 +1435,24 @@ public partial class LauncherWindow : Window
fadeOut.Completed += onCompleted;
fadeOut.Begin(this);
};
_toastTimer.Start();
}
private void ShowIndexStatus(string message, TimeSpan duration)
{
IndexStatusText.Text = message;
IndexStatusText.Visibility = Visibility.Visible;
_indexStatusTimer?.Stop();
_indexStatusTimer = new System.Windows.Threading.DispatcherTimer
{
Interval = duration
};
_indexStatusTimer.Tick += (_, _) =>
{
_indexStatusTimer.Stop();
IndexStatusText.Visibility = Visibility.Collapsed;
};
_indexStatusTimer.Start();
}
@@ -1560,6 +1663,28 @@ public partial class LauncherWindow : Window
if (_vm.CloseOnFocusLost) Hide();
}
private void Window_LocationChanged(object sender, EventArgs e)
{
UpdateRememberedPositionCache();
}
private void Window_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue is not bool isVisible)
return;
if (isVisible)
{
StartWidgetUpdates();
return;
}
_quickLookWindow?.Close();
_quickLookWindow = null;
StopWidgetUpdates();
SaveRememberedPosition();
}
private void ScrollToSelected()
{
if (_vm.SelectedItem != null)