런처 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

@@ -0,0 +1,209 @@
using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;
using AxCopilot.Services;
namespace AxCopilot.Views;
public partial class LauncherWindow
{
private DispatcherTimer? _widgetTimer;
private static readonly SolidColorBrush DotOnline = new(Color.FromRgb(0x10, 0xB9, 0x81));
private static readonly SolidColorBrush DotOffline = new(Color.FromRgb(0x9E, 0x9E, 0x9E));
private int _widgetBatteryTick;
private int _widgetWeatherTick;
internal void StartWidgetUpdates()
{
var settings = CurrentApp?.SettingsService?.Settings;
PerformanceMonitorService.Instance.StartPolling();
ServerStatusService.Instance.Start(settings);
PomodoroService.Instance.StateChanged -= OnPomoStateChanged;
PomodoroService.Instance.StateChanged += OnPomoStateChanged;
ServerStatusService.Instance.StatusChanged -= OnServerStatusChanged;
ServerStatusService.Instance.StatusChanged += OnServerStatusChanged;
_vm.UpdateWidgets();
UpdateServerDots();
UpdateBatteryWidget();
_ = RefreshWeatherAsync();
if (_widgetTimer == null)
{
_widgetTimer = new DispatcherTimer(DispatcherPriority.Background)
{
Interval = TimeSpan.FromSeconds(1)
};
_widgetTimer.Tick += (_, _) =>
{
_vm.UpdateWidgets();
UpdateServerDots();
if (_vm.Widget_PerfText.Length > 0 && _widgetBatteryTick++ % 30 == 0)
UpdateBatteryWidget();
if (_widgetWeatherTick++ % 120 == 0)
_ = RefreshWeatherAsync();
};
}
_widgetTimer.Start();
UpdatePomoWidgetStyle();
}
internal void StopWidgetUpdates()
{
_widgetTimer?.Stop();
PerformanceMonitorService.Instance.StopPolling();
PomodoroService.Instance.StateChanged -= OnPomoStateChanged;
ServerStatusService.Instance.StatusChanged -= OnServerStatusChanged;
}
private void OnPomoStateChanged(object? sender, EventArgs e)
{
Dispatcher.InvokeAsync(() =>
{
_vm.UpdateWidgets();
UpdatePomoWidgetStyle();
});
}
private void OnServerStatusChanged(object? sender, EventArgs e)
=> Dispatcher.InvokeAsync(UpdateServerDots);
private void UpdateServerDots()
{
var server = ServerStatusService.Instance;
if (OllamaStatusDot != null)
OllamaStatusDot.Fill = server.OllamaOnline ? DotOnline : DotOffline;
if (LlmStatusDot != null)
LlmStatusDot.Fill = server.LlmOnline ? DotOnline : DotOffline;
if (McpStatusDot != null)
McpStatusDot.Fill = server.McpOnline ? DotOnline : DotOffline;
}
private void UpdatePomoWidgetStyle()
{
if (WgtPomo == null)
return;
var running = PomodoroService.Instance.IsRunning;
WgtPomo.Background = running
? new SolidColorBrush(Color.FromArgb(0x1E, 0xF5, 0x9E, 0x0B))
: new SolidColorBrush(Color.FromArgb(0x0D, 0xF5, 0x9E, 0x0B));
}
private void WgtPerf_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
_vm.InputText = "info ";
InputBox?.Focus();
}
private void WgtPomo_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
_vm.InputText = "pomo ";
InputBox?.Focus();
}
private void WgtNote_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
_vm.InputText = "note ";
InputBox?.Focus();
}
private void WgtServer_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
var settings = CurrentApp?.SettingsService?.Settings;
ServerStatusService.Instance.Refresh(settings);
_vm.InputText = "port";
InputBox?.Focus();
}
private void WgtWeather_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
var internalMode = CurrentApp?.SettingsService?.Settings.InternalModeEnabled ?? true;
if (!internalMode)
{
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("https://wttr.in") { UseShellExecute = true });
}
catch (Exception ex)
{
LogService.Warn($"날씨 페이지 열기 실패: {ex.Message}");
}
}
else
{
WeatherWidgetService.Invalidate();
_ = RefreshWeatherAsync();
}
}
private void WgtCal_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("ms-clock:") { UseShellExecute = true });
}
catch
{
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("outlookcal:") { UseShellExecute = true });
}
catch (Exception ex)
{
LogService.Warn($"달력 열기 실패: {ex.Message}");
}
}
}
private void WgtBattery_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("ms-settings:powersleep") { UseShellExecute = true });
}
catch (Exception ex)
{
LogService.Warn($"전원 설정 열기 실패: {ex.Message}");
}
}
private void UpdateBatteryWidget()
{
try
{
var power = System.Windows.Forms.SystemInformation.PowerStatus;
var pct = power.BatteryLifePercent;
if (pct > 1.0f || pct < 0f)
{
_vm.Widget_BatteryVisible = false;
return;
}
_vm.Widget_BatteryVisible = true;
var pctInt = (int)(pct * 100);
var charging = power.PowerLineStatus == System.Windows.Forms.PowerLineStatus.Online;
_vm.Widget_BatteryText = charging ? $"{pctInt}% 충전" : $"{pctInt}%";
_vm.Widget_BatteryIcon = charging ? "\uE83E"
: pctInt >= 85 ? "\uEBA7"
: pctInt >= 70 ? "\uEBA5"
: pctInt >= 50 ? "\uEBA3"
: pctInt >= 25 ? "\uEBA1"
: "\uEBA0";
}
catch (Exception ex)
{
LogService.Warn($"배터리 위젯 갱신 실패: {ex.Message}");
_vm.Widget_BatteryVisible = false;
}
}
private async Task RefreshWeatherAsync()
{
var internalMode = CurrentApp?.SettingsService?.Settings.InternalModeEnabled ?? true;
await WeatherWidgetService.RefreshAsync(internalMode);
await Dispatcher.InvokeAsync(() => { _vm.Widget_WeatherText = WeatherWidgetService.CachedText; });
}
}