[Phase L3-9 + 버그] AX Agent 오류 수정 + 런처 미니 위젯 4종 구현
AX Agent 오류 수정: - AgentSettingsPanel.xaml에 <UserControl.Resources> 추가 - ToggleSwitch 스타일 자체 정의 (SettingsWindow 리소스 미접근 문제 해결) - 원인: XamlParseException — 'ToggleSwitch' 리소스 찾을 수 없음 (CS 로그 확인) PerformanceMonitorService.cs (신규, 138줄): - GetSystemTimes P/Invoke → CPU% (이전/현재 샘플 델타 계산) - GlobalMemoryStatusEx P/Invoke → RAM% + "6.1/16GB" 형식 텍스트 - DriveInfo → C: 드라이브 사용률/용량 텍스트 - 2초 폴링, StartPolling/StopPolling 제어 PomodoroService.cs (신규, 179줄): - 집중(25분)/휴식(5분) 타이머, 상태: Idle/Focus/Break - pomodoro.json 영속성 (경과 시간 자동 보정) - StateChanged 이벤트 → 위젯 실시간 갱신 ServerStatusService.cs (신규, 124줄): - Ollama(/api/version), LLM API, 첫 번째 MCP 서버 15초 주기 핑 - HttpClient 1.5초 타임아웃, StatusChanged 이벤트 PomoHandler.cs (신규, 130줄): - pomo prefix: 상태보기/start/break/stop/reset - PomodoroService 직접 연동 LauncherViewModel.Widgets.cs (신규, 81줄): - Widget_PerfText, Widget_PomoText, Widget_PomoRunning - Widget_NoteText, Widget_OllamaOnline, Widget_LlmOnline, Widget_McpOnline - UpdateWidgets() — 5틱마다 메모 건수 갱신 (파일 I/O 최소화) LauncherWindow.Widgets.cs (신규, 143줄): - IsVisibleChanged 이벤트로 위젯 자동 시작/중지 - DispatcherTimer 1초마다 UpdateWidgets + 서버 상태 dot 색상 직접 갱신 - 위젯 클릭 → 해당 prefix 자동 입력 (perf→info, pomo→pomo, note→note, server→port) LauncherWindow.xaml: - RowDefinition 6개 → 7개 - Row 6: 위젯 바 (시스템모니터/뽀모도로/메모/서버 4열) 빌드: 경고 0, 오류 0
This commit is contained in:
@@ -170,6 +170,8 @@ public partial class App : System.Windows.Application
|
|||||||
commandResolver.RegisterHandler(new TagHandler());
|
commandResolver.RegisterHandler(new TagHandler());
|
||||||
// Phase L3-8: 알림 센터
|
// Phase L3-8: 알림 센터
|
||||||
commandResolver.RegisterHandler(new NotifHandler());
|
commandResolver.RegisterHandler(new NotifHandler());
|
||||||
|
// Phase L3-9: 뽀모도로 타이머
|
||||||
|
commandResolver.RegisterHandler(new PomoHandler());
|
||||||
|
|
||||||
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
||||||
var pluginHost = new PluginHost(settings, commandResolver);
|
var pluginHost = new PluginHost(settings, commandResolver);
|
||||||
|
|||||||
@@ -174,6 +174,9 @@ public class NoteHandler : IActionHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>저장된 메모 건수를 반환합니다 (위젯 표시용).</summary>
|
||||||
|
public static int GetNoteCount() => ReadNotes().Count;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 특정 메모 1건 삭제. content가 일치하는 가장 최근 항목을 제거합니다.
|
/// 특정 메모 1건 삭제. content가 일치하는 가장 최근 항목을 제거합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
130
src/AxCopilot/Handlers/PomoHandler.cs
Normal file
130
src/AxCopilot/Handlers/PomoHandler.cs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
using AxCopilot.SDK;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
using AxCopilot.Themes;
|
||||||
|
|
||||||
|
namespace AxCopilot.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase L3-9: 뽀모도로 타이머 핸들러. "pomo" 프리픽스로 사용합니다.
|
||||||
|
///
|
||||||
|
/// 사용법:
|
||||||
|
/// pomo → 현재 타이머 상태 표시
|
||||||
|
/// pomo start → 집중 타이머 시작 (25분)
|
||||||
|
/// pomo break → 휴식 타이머 시작 (5분)
|
||||||
|
/// pomo stop → 타이머 중지
|
||||||
|
/// pomo reset → 타이머 초기화
|
||||||
|
///
|
||||||
|
/// Enter → 해당 명령 실행.
|
||||||
|
/// </summary>
|
||||||
|
public class PomoHandler : IActionHandler
|
||||||
|
{
|
||||||
|
public string? Prefix => "pomo";
|
||||||
|
|
||||||
|
public PluginMetadata Metadata => new(
|
||||||
|
"Pomodoro",
|
||||||
|
"뽀모도로 타이머 — pomo",
|
||||||
|
"1.0",
|
||||||
|
"AX",
|
||||||
|
"집중/휴식 타이머를 관리합니다.");
|
||||||
|
|
||||||
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var q = query.Trim().ToLowerInvariant();
|
||||||
|
var svc = PomodoroService.Instance;
|
||||||
|
var items = new List<LauncherItem>();
|
||||||
|
|
||||||
|
// ─── 명령 분기 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if (q is "start" or "focus")
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("집중 타이머 시작",
|
||||||
|
$"{svc.FocusMinutes}분 집중 모드 시작 · Enter로 실행",
|
||||||
|
null, "__START__", Symbol: Symbols.Timer));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (q is "break" or "rest")
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("휴식 타이머 시작",
|
||||||
|
$"{svc.BreakMinutes}분 휴식 모드 시작 · Enter로 실행",
|
||||||
|
null, "__BREAK__", Symbol: Symbols.Timer));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (q is "stop" or "pause")
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("타이머 중지",
|
||||||
|
"현재 타이머를 중지합니다 · Enter로 실행",
|
||||||
|
null, "__STOP__", Symbol: Symbols.MediaPlay));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (q == "reset")
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("타이머 초기화",
|
||||||
|
"타이머를 처음 상태로 초기화합니다 · Enter로 실행",
|
||||||
|
null, "__RESET__", Symbol: Symbols.Restart));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 기본: 현재 상태 + 명령 목록 ─────────────────────────────────────
|
||||||
|
|
||||||
|
var remaining = svc.Remaining;
|
||||||
|
var timeStr = $"{(int)remaining.TotalMinutes:D2}:{remaining.Seconds:D2}";
|
||||||
|
var stateStr = svc.Mode switch
|
||||||
|
{
|
||||||
|
PomodoroMode.Focus => svc.IsRunning ? $"집중 중 {timeStr} 남음" : $"집중 일시정지 {timeStr} 남음",
|
||||||
|
PomodoroMode.Break => svc.IsRunning ? $"휴식 중 {timeStr} 남음" : $"휴식 일시정지 {timeStr} 남음",
|
||||||
|
_ => "대기 중",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 상태 카드
|
||||||
|
items.Add(new LauncherItem(
|
||||||
|
"🍅 뽀모도로 타이머",
|
||||||
|
stateStr,
|
||||||
|
null, null, Symbol: Symbols.Timer));
|
||||||
|
|
||||||
|
// 빠른 명령들
|
||||||
|
if (!svc.IsRunning)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("집중 시작",
|
||||||
|
$"{svc.FocusMinutes}분 집중 모드 시작 · Enter로 실행",
|
||||||
|
null, "__START__", Symbol: Symbols.Timer));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("타이머 중지",
|
||||||
|
"현재 타이머를 중지합니다 · Enter로 실행",
|
||||||
|
null, "__STOP__", Symbol: Symbols.MediaPlay));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (svc.Mode == PomodoroMode.Focus && svc.IsRunning)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("휴식 시작",
|
||||||
|
$"{svc.BreakMinutes}분 휴식 모드로 전환 · Enter로 실행",
|
||||||
|
null, "__BREAK__", Symbol: Symbols.Timer));
|
||||||
|
}
|
||||||
|
|
||||||
|
items.Add(new LauncherItem("타이머 초기화",
|
||||||
|
"타이머를 처음 상태로 초기화합니다 · Enter로 실행",
|
||||||
|
null, "__RESET__", Symbol: Symbols.Restart));
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (item.Data is not string cmd) return Task.CompletedTask;
|
||||||
|
|
||||||
|
var svc = PomodoroService.Instance;
|
||||||
|
switch (cmd)
|
||||||
|
{
|
||||||
|
case "__START__": svc.StartFocus(); break;
|
||||||
|
case "__BREAK__": svc.StartBreak(); break;
|
||||||
|
case "__STOP__": svc.Stop(); break;
|
||||||
|
case "__RESET__": svc.Reset(); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
138
src/AxCopilot/Services/PerformanceMonitorService.cs
Normal file
138
src/AxCopilot/Services/PerformanceMonitorService.cs
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace AxCopilot.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase L3-9: 시스템 성능 모니터 서비스.
|
||||||
|
/// CPU(GetSystemTimes P/Invoke), RAM(GlobalMemoryStatusEx), Disk C(DriveInfo) 수집.
|
||||||
|
/// 싱글턴 — 런처 표시 시 StartPolling(), 숨김 시 StopPolling().
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class PerformanceMonitorService
|
||||||
|
{
|
||||||
|
public static readonly PerformanceMonitorService Instance = new();
|
||||||
|
|
||||||
|
// ─── P/Invoke ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct FILETIME { public uint Low; public uint High; }
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
|
||||||
|
private struct MEMORYSTATUSEX
|
||||||
|
{
|
||||||
|
public uint dwLength;
|
||||||
|
public uint dwMemoryLoad; // % in use
|
||||||
|
public ulong ullTotalPhys;
|
||||||
|
public ulong ullAvailPhys;
|
||||||
|
public ulong ullTotalPageFile;
|
||||||
|
public ulong ullAvailPageFile;
|
||||||
|
public ulong ullTotalVirtual;
|
||||||
|
public ulong ullAvailVirtual;
|
||||||
|
public ulong ullAvailExtendedVirtual;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern bool GetSystemTimes(out FILETIME idle, out FILETIME kernel, out FILETIME user);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||||
|
private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);
|
||||||
|
|
||||||
|
// ─── 공개 프로퍼티 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public double CpuPercent { get; private set; }
|
||||||
|
public double RamPercent { get; private set; }
|
||||||
|
public string RamText { get; private set; } = "";
|
||||||
|
public double DiskCPercent { get; private set; }
|
||||||
|
public string DiskCText { get; private set; } = "";
|
||||||
|
|
||||||
|
// ─── 내부 상태 ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private System.Threading.Timer? _timer;
|
||||||
|
private FILETIME _prevIdle, _prevKernel, _prevUser;
|
||||||
|
private bool _hasPrev;
|
||||||
|
|
||||||
|
private PerformanceMonitorService() { }
|
||||||
|
|
||||||
|
public void StartPolling()
|
||||||
|
{
|
||||||
|
if (_timer != null) return;
|
||||||
|
// 즉시 첫 샘플 + 이후 2초마다 갱신
|
||||||
|
_timer = new System.Threading.Timer(_ => Sample(), null, 0, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopPolling()
|
||||||
|
{
|
||||||
|
_timer?.Dispose();
|
||||||
|
_timer = null;
|
||||||
|
_hasPrev = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 샘플링 ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void Sample()
|
||||||
|
{
|
||||||
|
SampleCpu();
|
||||||
|
SampleRam();
|
||||||
|
SampleDisk();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SampleCpu()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!GetSystemTimes(out var idle, out var kernel, out var user)) return;
|
||||||
|
|
||||||
|
if (_hasPrev)
|
||||||
|
{
|
||||||
|
var idleDelta = ToUlong(idle) - ToUlong(_prevIdle);
|
||||||
|
var kernelDelta = ToUlong(kernel) - ToUlong(_prevKernel);
|
||||||
|
var userDelta = ToUlong(user) - ToUlong(_prevUser);
|
||||||
|
|
||||||
|
var total = kernelDelta + userDelta;
|
||||||
|
var busy = total - idleDelta;
|
||||||
|
CpuPercent = total > 0 ? Math.Max(0, Math.Min(100, 100.0 * busy / total)) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_prevIdle = idle;
|
||||||
|
_prevKernel = kernel;
|
||||||
|
_prevUser = user;
|
||||||
|
_hasPrev = true;
|
||||||
|
}
|
||||||
|
catch { /* 오류 무시 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SampleRam()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var mem = new MEMORYSTATUSEX { dwLength = (uint)Marshal.SizeOf<MEMORYSTATUSEX>() };
|
||||||
|
if (!GlobalMemoryStatusEx(ref mem)) return;
|
||||||
|
|
||||||
|
RamPercent = mem.dwMemoryLoad;
|
||||||
|
var usedGB = (mem.ullTotalPhys - mem.ullAvailPhys) / (1024.0 * 1024 * 1024);
|
||||||
|
var totalGB = mem.ullTotalPhys / (1024.0 * 1024 * 1024);
|
||||||
|
RamText = $"{usedGB:F1}/{totalGB:F0}GB";
|
||||||
|
}
|
||||||
|
catch { /* 오류 무시 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SampleDisk()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var drive = DriveInfo.GetDrives()
|
||||||
|
.FirstOrDefault(d => d.IsReady &&
|
||||||
|
d.Name.StartsWith("C", StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (drive == null) return;
|
||||||
|
|
||||||
|
var usedBytes = drive.TotalSize - drive.AvailableFreeSpace;
|
||||||
|
DiskCPercent = 100.0 * usedBytes / drive.TotalSize;
|
||||||
|
var usedGB = usedBytes / (1024.0 * 1024 * 1024);
|
||||||
|
var totalGB = drive.TotalSize / (1024.0 * 1024 * 1024);
|
||||||
|
DiskCText = $"C:{usedGB:F0}/{totalGB:F0}GB";
|
||||||
|
}
|
||||||
|
catch { /* 오류 무시 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong ToUlong(FILETIME ft) => ((ulong)ft.High << 32) | ft.Low;
|
||||||
|
}
|
||||||
179
src/AxCopilot/Services/PomodoroService.cs
Normal file
179
src/AxCopilot/Services/PomodoroService.cs
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace AxCopilot.Services;
|
||||||
|
|
||||||
|
public enum PomodoroMode { Idle, Focus, Break }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase L3-9: 뽀모도로 타이머 서비스.
|
||||||
|
/// 집중(기본 25분) / 휴식(기본 5분) 모드를 관리하며 AppData에 상태를 저장합니다.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class PomodoroService
|
||||||
|
{
|
||||||
|
public static readonly PomodoroService Instance = new();
|
||||||
|
|
||||||
|
private static readonly string StateFile = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
"AxCopilot", "pomodoro.json");
|
||||||
|
|
||||||
|
// ─── 설정 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public int FocusMinutes { get; set; } = 25;
|
||||||
|
public int BreakMinutes { get; set; } = 5;
|
||||||
|
|
||||||
|
// ─── 상태 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public PomodoroMode Mode { get; private set; } = PomodoroMode.Idle;
|
||||||
|
public bool IsRunning { get; private set; }
|
||||||
|
public TimeSpan Remaining { get; private set; }
|
||||||
|
|
||||||
|
public event EventHandler? StateChanged;
|
||||||
|
|
||||||
|
// ─── 내부 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private System.Threading.Timer? _ticker;
|
||||||
|
|
||||||
|
private PomodoroService()
|
||||||
|
{
|
||||||
|
LoadState();
|
||||||
|
// 앱 재시작 후 진행 중이던 타이머가 있으면 재개
|
||||||
|
if (IsRunning && Remaining > TimeSpan.Zero)
|
||||||
|
StartTicker();
|
||||||
|
else
|
||||||
|
IsRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 공개 API ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public void StartFocus() => StartMode(PomodoroMode.Focus);
|
||||||
|
public void StartBreak() => StartMode(PomodoroMode.Break);
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
StopTicker();
|
||||||
|
IsRunning = false;
|
||||||
|
SaveState();
|
||||||
|
StateChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
StopTicker();
|
||||||
|
Mode = PomodoroMode.Idle;
|
||||||
|
IsRunning = false;
|
||||||
|
Remaining = TimeSpan.FromMinutes(FocusMinutes);
|
||||||
|
SaveState();
|
||||||
|
StateChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Toggle()
|
||||||
|
{
|
||||||
|
if (IsRunning) Stop();
|
||||||
|
else if (Mode == PomodoroMode.Break) StartBreak();
|
||||||
|
else StartFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 내부 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void StartMode(PomodoroMode mode)
|
||||||
|
{
|
||||||
|
StopTicker();
|
||||||
|
Mode = mode;
|
||||||
|
IsRunning = true;
|
||||||
|
if (Remaining <= TimeSpan.Zero || Mode != mode)
|
||||||
|
Remaining = TimeSpan.FromMinutes(mode == PomodoroMode.Focus ? FocusMinutes : BreakMinutes);
|
||||||
|
StartTicker();
|
||||||
|
SaveState();
|
||||||
|
StateChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartTicker()
|
||||||
|
{
|
||||||
|
_ticker = new System.Threading.Timer(_ => Tick(), null, 1000, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopTicker()
|
||||||
|
{
|
||||||
|
_ticker?.Dispose();
|
||||||
|
_ticker = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Tick()
|
||||||
|
{
|
||||||
|
if (Remaining <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
// 타이머 종료
|
||||||
|
Stop();
|
||||||
|
var modeLabel = Mode == PomodoroMode.Focus ? "집중" : "휴식";
|
||||||
|
NotificationCenterService.Show("뽀모도로 타이머", $"{modeLabel} 시간이 종료되었습니다.", NotificationType.Info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Remaining -= TimeSpan.FromSeconds(1);
|
||||||
|
StateChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 영속성 ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void SaveState()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(StateFile)!);
|
||||||
|
var dto = new PomodoroStateDto
|
||||||
|
{
|
||||||
|
Mode = Mode.ToString(),
|
||||||
|
IsRunning = IsRunning,
|
||||||
|
RemainingSeconds = (int)Remaining.TotalSeconds,
|
||||||
|
FocusMinutes = FocusMinutes,
|
||||||
|
BreakMinutes = BreakMinutes,
|
||||||
|
SavedAt = DateTime.Now,
|
||||||
|
};
|
||||||
|
File.WriteAllText(StateFile, JsonSerializer.Serialize(dto));
|
||||||
|
}
|
||||||
|
catch { /* 무시 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadState()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(StateFile)) goto defaults;
|
||||||
|
var json = File.ReadAllText(StateFile);
|
||||||
|
var dto = JsonSerializer.Deserialize<PomodoroStateDto>(json);
|
||||||
|
if (dto == null) goto defaults;
|
||||||
|
|
||||||
|
FocusMinutes = dto.FocusMinutes > 0 ? dto.FocusMinutes : 25;
|
||||||
|
BreakMinutes = dto.BreakMinutes > 0 ? dto.BreakMinutes : 5;
|
||||||
|
Mode = Enum.TryParse<PomodoroMode>(dto.Mode, out var m) ? m : PomodoroMode.Idle;
|
||||||
|
IsRunning = dto.IsRunning;
|
||||||
|
|
||||||
|
// 앱이 꺼진 동안 경과 시간 보정
|
||||||
|
var elapsed = DateTime.Now - dto.SavedAt;
|
||||||
|
var saved = TimeSpan.FromSeconds(dto.RemainingSeconds);
|
||||||
|
Remaining = saved - elapsed;
|
||||||
|
if (Remaining < TimeSpan.Zero) { Remaining = TimeSpan.Zero; IsRunning = false; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch { /* fall through */ }
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
FocusMinutes = 25;
|
||||||
|
BreakMinutes = 5;
|
||||||
|
Mode = PomodoroMode.Idle;
|
||||||
|
IsRunning = false;
|
||||||
|
Remaining = TimeSpan.FromMinutes(FocusMinutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PomodoroStateDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("mode")] public string Mode { get; set; } = "Idle";
|
||||||
|
[JsonPropertyName("isRunning")] public bool IsRunning { get; set; }
|
||||||
|
[JsonPropertyName("remainingSeconds")] public int RemainingSeconds { get; set; }
|
||||||
|
[JsonPropertyName("focusMinutes")] public int FocusMinutes { get; set; } = 25;
|
||||||
|
[JsonPropertyName("breakMinutes")] public int BreakMinutes { get; set; } = 5;
|
||||||
|
[JsonPropertyName("savedAt")] public DateTime SavedAt { get; set; } = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/AxCopilot/Services/ServerStatusService.cs
Normal file
124
src/AxCopilot/Services/ServerStatusService.cs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace AxCopilot.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase L3-9: 서버 상태 모니터 서비스.
|
||||||
|
/// Ollama / LLM API / 첫 번째 활성 MCP 서버의 연결 상태를 주기적으로 확인합니다.
|
||||||
|
/// 싱글턴 — 런처 표시 시 Start(), 숨김 시 Stop().
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ServerStatusService
|
||||||
|
{
|
||||||
|
public static readonly ServerStatusService Instance = new();
|
||||||
|
|
||||||
|
private static readonly HttpClient _http = new()
|
||||||
|
{
|
||||||
|
Timeout = TimeSpan.FromMilliseconds(1500)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── 공개 상태 ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public bool OllamaOnline { get; private set; }
|
||||||
|
public bool LlmOnline { get; private set; }
|
||||||
|
public bool McpOnline { get; private set; }
|
||||||
|
public string McpName { get; private set; } = "MCP";
|
||||||
|
|
||||||
|
public event EventHandler? StatusChanged;
|
||||||
|
|
||||||
|
// ─── 내부 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private System.Threading.Timer? _timer;
|
||||||
|
private string _ollamaEndpoint = "http://localhost:11434";
|
||||||
|
private string _llmEndpoint = "";
|
||||||
|
private string _llmService = "Ollama";
|
||||||
|
private string _mcpEndpoint = "";
|
||||||
|
|
||||||
|
private ServerStatusService() { }
|
||||||
|
|
||||||
|
public void Start(AxCopilot.Models.AppSettings? settings = null)
|
||||||
|
{
|
||||||
|
LoadEndpoints(settings);
|
||||||
|
if (_timer != null) return;
|
||||||
|
_timer = new System.Threading.Timer(async _ => await CheckAllAsync(), null, 0, 15_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
_timer?.Dispose();
|
||||||
|
_timer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Refresh(AxCopilot.Models.AppSettings? settings = null)
|
||||||
|
{
|
||||||
|
LoadEndpoints(settings);
|
||||||
|
_ = CheckAllAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 내부 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void LoadEndpoints(AxCopilot.Models.AppSettings? settings)
|
||||||
|
{
|
||||||
|
var llm = settings?.Llm;
|
||||||
|
if (llm == null) return;
|
||||||
|
|
||||||
|
_ollamaEndpoint = llm.OllamaEndpoint?.TrimEnd('/') ?? "http://localhost:11434";
|
||||||
|
_llmService = llm.Service ?? "Ollama";
|
||||||
|
|
||||||
|
// LLM API 엔드포인트: vLLM이면 별도, 나머지는 Ollama와 동일
|
||||||
|
_llmEndpoint = _llmService.Equals("vLLM", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? (llm.VllmEndpoint?.TrimEnd('/') ?? "")
|
||||||
|
: _ollamaEndpoint;
|
||||||
|
|
||||||
|
// 첫 번째 활성 MCP 서버
|
||||||
|
var mcp = llm.McpServers?.FirstOrDefault(s => s.Enabled && !string.IsNullOrEmpty(s.Url));
|
||||||
|
if (mcp != null)
|
||||||
|
{
|
||||||
|
McpName = mcp.Name;
|
||||||
|
_mcpEndpoint = mcp.Url?.TrimEnd('/') ?? "";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
McpName = "MCP";
|
||||||
|
_mcpEndpoint = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CheckAllAsync()
|
||||||
|
{
|
||||||
|
var t1 = PingAsync(_ollamaEndpoint + "/api/version");
|
||||||
|
var t2 = string.IsNullOrEmpty(_llmEndpoint) || _llmEndpoint == _ollamaEndpoint
|
||||||
|
? t1
|
||||||
|
: PingAsync(_llmEndpoint);
|
||||||
|
var t3 = string.IsNullOrEmpty(_mcpEndpoint)
|
||||||
|
? Task.FromResult(false)
|
||||||
|
: PingAsync(_mcpEndpoint);
|
||||||
|
|
||||||
|
await Task.WhenAll(t1, t2, t3).ConfigureAwait(false);
|
||||||
|
|
||||||
|
bool changed =
|
||||||
|
OllamaOnline != t1.Result ||
|
||||||
|
LlmOnline != t2.Result ||
|
||||||
|
McpOnline != t3.Result;
|
||||||
|
|
||||||
|
OllamaOnline = t1.Result;
|
||||||
|
LlmOnline = t2.Result;
|
||||||
|
McpOnline = t3.Result;
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
StatusChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<bool> PingAsync(string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(url)) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var resp = await _http.GetAsync(url).ConfigureAwait(false);
|
||||||
|
return resp.IsSuccessStatusCode || (int)resp.StatusCode < 500;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/AxCopilot/ViewModels/LauncherViewModel.Widgets.cs
Normal file
81
src/AxCopilot/ViewModels/LauncherViewModel.Widgets.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
using AxCopilot.Handlers;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
|
||||||
|
namespace AxCopilot.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase L3-9: 런처 하단 미니 위젯 데이터 바인딩 프로퍼티.
|
||||||
|
/// LauncherWindow.Widgets.cs의 타이머에서 UpdateWidgets()를 호출합니다.
|
||||||
|
/// </summary>
|
||||||
|
public partial class LauncherViewModel
|
||||||
|
{
|
||||||
|
// ─── 시스템 모니터 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>위젯 텍스트 예: "CPU 34% RAM 62% C:42%"</summary>
|
||||||
|
public string Widget_PerfText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var p = PerformanceMonitorService.Instance;
|
||||||
|
return $"CPU {p.CpuPercent:F0}% RAM {p.RamPercent:F0}% {p.DiskCText}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 뽀모도로 ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>위젯 텍스트 예: "집중 18:42" / "휴식 04:58" / "대기 25:00"</summary>
|
||||||
|
public string Widget_PomoText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var p = PomodoroService.Instance;
|
||||||
|
var rem = p.Remaining;
|
||||||
|
var timeStr = $"{(int)rem.TotalMinutes:D2}:{rem.Seconds:D2}";
|
||||||
|
return p.Mode switch
|
||||||
|
{
|
||||||
|
PomodoroMode.Focus => $"집중 {timeStr}",
|
||||||
|
PomodoroMode.Break => $"휴식 {timeStr}",
|
||||||
|
_ => $"대기 {timeStr}",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>뽀모도로 실행 중이면 true → 위젯 색상 강조에 사용</summary>
|
||||||
|
public bool Widget_PomoRunning => PomodoroService.Instance.IsRunning;
|
||||||
|
|
||||||
|
// ─── 빠른 메모 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private int _noteCount;
|
||||||
|
|
||||||
|
/// <summary>위젯 텍스트 예: "메모 3건" / "메모 없음"</summary>
|
||||||
|
public string Widget_NoteText => _noteCount > 0 ? $"메모 {_noteCount}건" : "메모 없음";
|
||||||
|
|
||||||
|
// ─── 서버 상태 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public bool Widget_OllamaOnline => ServerStatusService.Instance.OllamaOnline;
|
||||||
|
public bool Widget_LlmOnline => ServerStatusService.Instance.LlmOnline;
|
||||||
|
public bool Widget_McpOnline => ServerStatusService.Instance.McpOnline;
|
||||||
|
public string Widget_McpName => ServerStatusService.Instance.McpName;
|
||||||
|
|
||||||
|
// ─── 갱신 메서드 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>1초마다 LauncherWindow.Widgets.cs에서 호출 — UI 바인딩 갱신.</summary>
|
||||||
|
public void UpdateWidgets()
|
||||||
|
{
|
||||||
|
// 메모 건수 갱신 (파일 I/O 최소화: 5회마다 1회만)
|
||||||
|
_widgetRefreshTick++;
|
||||||
|
if (_widgetRefreshTick % 5 == 0)
|
||||||
|
_noteCount = NoteHandler.GetNoteCount();
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(Widget_PerfText));
|
||||||
|
OnPropertyChanged(nameof(Widget_PomoText));
|
||||||
|
OnPropertyChanged(nameof(Widget_PomoRunning));
|
||||||
|
OnPropertyChanged(nameof(Widget_NoteText));
|
||||||
|
OnPropertyChanged(nameof(Widget_OllamaOnline));
|
||||||
|
OnPropertyChanged(nameof(Widget_LlmOnline));
|
||||||
|
OnPropertyChanged(nameof(Widget_McpOnline));
|
||||||
|
OnPropertyChanged(nameof(Widget_McpName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _widgetRefreshTick;
|
||||||
|
}
|
||||||
@@ -151,6 +151,8 @@ public partial class LauncherViewModel : INotifyPropertyChanged
|
|||||||
{ "tag", ("태그", Symbols.Tag, "#6366F1") },
|
{ "tag", ("태그", Symbols.Tag, "#6366F1") },
|
||||||
// ─── Phase L3-8 알림 센터 ─────────────────────────────────────────────
|
// ─── Phase L3-8 알림 센터 ─────────────────────────────────────────────
|
||||||
{ "notif", ("알림", Symbols.ReminderBell, "#F59E0B") },
|
{ "notif", ("알림", Symbols.ReminderBell, "#F59E0B") },
|
||||||
|
// ─── Phase L3-9 위젯 핸들러 ──────────────────────────────────────────
|
||||||
|
{ "pomo", ("타이머", Symbols.Timer, "#F59E0B") },
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── 설정 기능 토글 (런처 실동작 연결) ──────────────────────────────────
|
// ─── 설정 기능 토글 (런처 실동작 연결) ──────────────────────────────────
|
||||||
|
|||||||
@@ -7,6 +7,37 @@
|
|||||||
ChatWindow 내부에서 AX Agent 모든 설정을 인라인으로 제어.
|
ChatWindow 내부에서 AX Agent 모든 설정을 인라인으로 제어.
|
||||||
SettingsWindow의 AX Agent 탭을 완전 대체. -->
|
SettingsWindow의 AX Agent 탭을 완전 대체. -->
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<!-- ToggleSwitch: ChatWindow 내부 UserControl은 SettingsWindow 리소스에 접근 불가 → 자체 정의 -->
|
||||||
|
<Style x:Key="ToggleSwitch" TargetType="CheckBox">
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="CheckBox">
|
||||||
|
<Grid Width="46" Height="26" VerticalAlignment="Center">
|
||||||
|
<Border x:Name="Track" CornerRadius="13" Background="#D0D0E0"
|
||||||
|
Width="46" Height="26"/>
|
||||||
|
<Ellipse x:Name="Thumb" Width="20" Height="20"
|
||||||
|
Fill="White" HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Center" Margin="3,0,0,0">
|
||||||
|
<Ellipse.Effect>
|
||||||
|
<DropShadowEffect BlurRadius="5" ShadowDepth="1" Opacity="0.25" Direction="270"/>
|
||||||
|
</Ellipse.Effect>
|
||||||
|
</Ellipse>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Track" Property="Background" Value="#4B5EFC"/>
|
||||||
|
<Setter TargetName="Thumb" Property="HorizontalAlignment" Value="Right"/>
|
||||||
|
<Setter TargetName="Thumb" Property="Margin" Value="0,0,3,0"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
<UserControl.RenderTransform>
|
<UserControl.RenderTransform>
|
||||||
<TranslateTransform x:Name="SlideTransform" X="300"/>
|
<TranslateTransform x:Name="SlideTransform" X="300"/>
|
||||||
</UserControl.RenderTransform>
|
</UserControl.RenderTransform>
|
||||||
|
|||||||
143
src/AxCopilot/Views/LauncherWindow.Widgets.cs
Normal file
143
src/AxCopilot/Views/LauncherWindow.Widgets.cs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
|
||||||
|
namespace AxCopilot.Views;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase L3-9: 런처 하단 미니 위젯 바 로직.
|
||||||
|
/// 타이머로 1초마다 ViewModel 갱신 + 서버 상태 dot 색상 직접 업데이트.
|
||||||
|
/// </summary>
|
||||||
|
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));
|
||||||
|
|
||||||
|
/// <summary>LauncherWindow.xaml.cs의 Show()에서 최초 1회 호출 — IsVisibleChanged로 자동 관리.</summary>
|
||||||
|
internal void InitWidgets()
|
||||||
|
{
|
||||||
|
IsVisibleChanged -= OnLauncherVisibleChanged;
|
||||||
|
IsVisibleChanged += OnLauncherVisibleChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLauncherVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if ((bool)e.NewValue) StartWidgetUpdates();
|
||||||
|
else StopWidgetUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>런처가 표시될 때 위젯 업데이트 시작.</summary>
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 메모 건수 즉시 갱신 (최초 1회)
|
||||||
|
_vm.UpdateWidgets();
|
||||||
|
UpdateServerDots();
|
||||||
|
|
||||||
|
// 1초 타이머
|
||||||
|
if (_widgetTimer == null)
|
||||||
|
{
|
||||||
|
_widgetTimer = new DispatcherTimer(DispatcherPriority.Background)
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromSeconds(1)
|
||||||
|
};
|
||||||
|
_widgetTimer.Tick += (_, _) =>
|
||||||
|
{
|
||||||
|
_vm.UpdateWidgets();
|
||||||
|
UpdateServerDots();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_widgetTimer.Start();
|
||||||
|
|
||||||
|
// 뽀모도로 실행 중이면 위젯 색상 강조
|
||||||
|
UpdatePomoWidgetStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>런처가 숨겨질 때 타이머 중지 (뽀모도로는 계속 실행).</summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── UI 업데이트 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void UpdateServerDots()
|
||||||
|
{
|
||||||
|
var srv = ServerStatusService.Instance;
|
||||||
|
if (OllamaStatusDot != null)
|
||||||
|
OllamaStatusDot.Fill = srv.OllamaOnline ? _dotOnline : _dotOffline;
|
||||||
|
if (LlmStatusDot != null)
|
||||||
|
LlmStatusDot.Fill = srv.LlmOnline ? _dotOnline : _dotOffline;
|
||||||
|
if (McpStatusDot != null)
|
||||||
|
McpStatusDot.Fill = srv.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -201,6 +201,7 @@
|
|||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- ─── 입력 영역 ─── -->
|
<!-- ─── 입력 영역 ─── -->
|
||||||
@@ -701,9 +702,121 @@
|
|||||||
Margin="0,0,0,8"
|
Margin="0,0,0,8"
|
||||||
Opacity="0.7"/>
|
Opacity="0.7"/>
|
||||||
|
|
||||||
|
<!-- ─── Phase L3-9: 미니 위젯 바 ─── -->
|
||||||
|
<Border x:Name="WidgetBar"
|
||||||
|
Grid.Row="6"
|
||||||
|
BorderBrush="{DynamicResource SeparatorColor}"
|
||||||
|
BorderThickness="0,1,0,0"
|
||||||
|
Padding="10,7,10,9">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="6"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="6"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="6"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- A: 시스템 모니터 -->
|
||||||
|
<Border x:Name="WgtPerf" Grid.Column="0"
|
||||||
|
CornerRadius="5" Padding="8,5"
|
||||||
|
Background="#0D60A5FA"
|
||||||
|
Cursor="Hand"
|
||||||
|
MouseLeftButtonUp="WgtPerf_Click">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text=""
|
||||||
|
FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||||
|
Foreground="#60A5FA"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<TextBlock Text="{Binding Widget_PerfText}"
|
||||||
|
FontSize="10"
|
||||||
|
Foreground="{DynamicResource SecondaryText}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- B: 뽀모도로 타이머 -->
|
||||||
|
<Border x:Name="WgtPomo" Grid.Column="2"
|
||||||
|
CornerRadius="5" Padding="8,5"
|
||||||
|
Background="#0DF59E0B"
|
||||||
|
Cursor="Hand"
|
||||||
|
MouseLeftButtonUp="WgtPomo_Click">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text=""
|
||||||
|
FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||||
|
Foreground="#F59E0B"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<TextBlock x:Name="WgtPomoText"
|
||||||
|
Text="{Binding Widget_PomoText}"
|
||||||
|
FontSize="10"
|
||||||
|
Foreground="{DynamicResource SecondaryText}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- C: 빠른 메모 -->
|
||||||
|
<Border x:Name="WgtNote" Grid.Column="4"
|
||||||
|
CornerRadius="5" Padding="8,5"
|
||||||
|
Background="#0D8B5CF6"
|
||||||
|
Cursor="Hand"
|
||||||
|
MouseLeftButtonUp="WgtNote_Click">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text=""
|
||||||
|
FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||||
|
Foreground="#8B5CF6"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<TextBlock Text="{Binding Widget_NoteText}"
|
||||||
|
FontSize="10"
|
||||||
|
Foreground="{DynamicResource SecondaryText}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- D: 서버 상태 -->
|
||||||
|
<Border x:Name="WgtServer" Grid.Column="6"
|
||||||
|
CornerRadius="5" Padding="8,5"
|
||||||
|
Background="#0D10B981"
|
||||||
|
Cursor="Hand"
|
||||||
|
MouseLeftButtonUp="WgtServer_Click">
|
||||||
|
<StackPanel Orientation="Horizontal" x:Name="WgtServerContent">
|
||||||
|
<!-- Ollama 상태 -->
|
||||||
|
<Ellipse x:Name="OllamaStatusDot"
|
||||||
|
Width="6" Height="6"
|
||||||
|
Fill="#9E9E9E"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,3,0"/>
|
||||||
|
<TextBlock Text="Ollama"
|
||||||
|
FontSize="10"
|
||||||
|
Foreground="{DynamicResource SecondaryText}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||||
|
<!-- LLM API 상태 -->
|
||||||
|
<Ellipse x:Name="LlmStatusDot"
|
||||||
|
Width="6" Height="6"
|
||||||
|
Fill="#9E9E9E"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,3,0"/>
|
||||||
|
<TextBlock Text="API"
|
||||||
|
FontSize="10"
|
||||||
|
Foreground="{DynamicResource SecondaryText}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||||
|
<!-- MCP 상태 -->
|
||||||
|
<Ellipse x:Name="McpStatusDot"
|
||||||
|
Width="6" Height="6"
|
||||||
|
Fill="#9E9E9E"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,3,0"/>
|
||||||
|
<TextBlock x:Name="McpNameText"
|
||||||
|
Text="{Binding Widget_McpName}"
|
||||||
|
FontSize="10"
|
||||||
|
Foreground="{DynamicResource SecondaryText}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- ─── 토스트 오버레이 ─── -->
|
<!-- ─── 토스트 오버레이 ─── -->
|
||||||
<Border x:Name="ToastOverlay"
|
<Border x:Name="ToastOverlay"
|
||||||
Grid.Row="4" Grid.RowSpan="2"
|
Grid.Row="4" Grid.RowSpan="3"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Bottom"
|
VerticalAlignment="Bottom"
|
||||||
Margin="0,0,0,12"
|
Margin="0,0,0,12"
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ public partial class LauncherWindow : Window
|
|||||||
vm.CloseRequested += (_, _) => Hide();
|
vm.CloseRequested += (_, _) => Hide();
|
||||||
vm.NotificationRequested += (_, msg) => ShowNotification(msg);
|
vm.NotificationRequested += (_, msg) => ShowNotification(msg);
|
||||||
|
|
||||||
|
// Phase L3-9: 위젯 가시성 감지 등록 (Show/Hide에 자동 반응)
|
||||||
|
InitWidgets();
|
||||||
|
|
||||||
// InputText가 코드에서 변경될 때 커서를 끝으로 이동
|
// InputText가 코드에서 변경될 때 커서를 끝으로 이동
|
||||||
// (F1→"help", Ctrl+B→"fav", Ctrl+D→"cd Downloads" 등 자동 입력 후 위화감 해소)
|
// (F1→"help", Ctrl+B→"fav", Ctrl+D→"cd Downloads" 등 자동 입력 후 위화감 해소)
|
||||||
vm.PropertyChanged += (_, e) =>
|
vm.PropertyChanged += (_, e) =>
|
||||||
@@ -202,6 +205,8 @@ public partial class LauncherWindow : Window
|
|||||||
StopRainbowGlow();
|
StopRainbowGlow();
|
||||||
|
|
||||||
UpdateSelectionGlow();
|
UpdateSelectionGlow();
|
||||||
|
|
||||||
|
// Phase L3-9: 위젯은 IsVisibleChanged로 자동 제어 (InitWidgets에서 등록)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>아이콘 애니메이션을 중지하고 모든 픽셀을 완전 점등 상태로 복원합니다.</summary>
|
/// <summary>아이콘 애니메이션을 중지하고 모든 픽셀을 완전 점등 상태로 복원합니다.</summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user