모델 프로파일 기반 Cowork/Code 루프와 진행 UX 고도화 반영
- 등록 모델 실행 프로파일을 검증 게이트, 문서 fallback, post-tool verification까지 확장 적용 - Cowork/Code 진행 카드에 계획/도구/검증/압축/폴백/재시도 단계 메타를 추가해 대기 상태 가시성 강화 - OpenAI/vLLM tool 요청에 병렬 도구 호출 힌트를 추가하고 회귀 프롬프트 문서를 프로파일 기준으로 전면 정리 - 검증: 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:
@@ -41,6 +41,9 @@ public partial class LauncherWindow : Window
|
||||
/// <summary>Ctrl+, 단축키로 설정 창을 여는 콜백 (App.xaml.cs에서 주입)</summary>
|
||||
public Action? OpenSettingsAction { get; set; }
|
||||
|
||||
/// <summary>항목을 AX Chat으로 보내는 콜백 (App.xaml.cs에서 주입). 인자: 전송할 메시지 텍스트.</summary>
|
||||
public Action<string>? SendToChatAction { get; set; }
|
||||
|
||||
public LauncherWindow(LauncherViewModel vm)
|
||||
{
|
||||
_vm = vm;
|
||||
@@ -204,7 +207,10 @@ public partial class LauncherWindow : Window
|
||||
}
|
||||
|
||||
if (_vm.EnableIconAnimation && IsVisible)
|
||||
ApplyRandomIconAnimation();
|
||||
{
|
||||
if (_iconStoryboard == null) // 이미 실행 중이면 재시작 하지 않음 (설정 변경 시 버벅임 방지)
|
||||
ApplyRandomIconAnimation();
|
||||
}
|
||||
else
|
||||
ResetIconAnimation();
|
||||
|
||||
@@ -315,7 +321,7 @@ public partial class LauncherWindow : Window
|
||||
|
||||
case 6: // 💫 바운스 등장 — 작아졌다가 탄력적으로 커짐 (PPT 바운스)
|
||||
{
|
||||
sb.RepeatBehavior = RepeatBehavior.Forever;
|
||||
sb.RepeatBehavior = new RepeatBehavior(3); // 3회 반복 후 Completed → 다음 애니메이션
|
||||
var bx = new DoubleAnimationUsingKeyFrames();
|
||||
bx.KeyFrames.Add(new LinearDoubleKeyFrame(0.7, KT(0)));
|
||||
bx.KeyFrames.Add(new EasingDoubleKeyFrame(1.15, KT(0.35), new BounceEase { Bounces = 2, Bounciness = 3 }));
|
||||
@@ -571,6 +577,9 @@ public partial class LauncherWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
// 30fps로 제한 — 두 창이 동시에 열릴 때 렌더링 부하 절감
|
||||
Timeline.SetDesiredFrameRate(sb, 30);
|
||||
|
||||
_iconStoryboard = sb;
|
||||
sb.Completed += (_, _) =>
|
||||
{
|
||||
@@ -580,6 +589,39 @@ public partial class LauncherWindow : Window
|
||||
sb.Begin(this, true);
|
||||
}
|
||||
|
||||
/// <summary>글로우 요소들: hover 시 Visible, 비호버 시 Collapsed (BlurEffect GPU 렌더 최적화)</summary>
|
||||
private void DiamondIcon_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
{
|
||||
var glows = new[] { GlowBlue, GlowGreen1, GlowGreen2, GlowRed };
|
||||
foreach (var glow in glows)
|
||||
{
|
||||
if (glow == null) continue;
|
||||
glow.Visibility = Visibility.Visible;
|
||||
glow.BeginAnimation(UIElement.OpacityProperty,
|
||||
new DoubleAnimation(0, 0.9, TimeSpan.FromMilliseconds(150))
|
||||
{ EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } });
|
||||
}
|
||||
}
|
||||
|
||||
private void DiamondIcon_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
{
|
||||
var glows = new[] { GlowBlue, GlowGreen1, GlowGreen2, GlowRed };
|
||||
foreach (var glow in glows)
|
||||
{
|
||||
if (glow == null) continue;
|
||||
var anim = new DoubleAnimation(0.9, 0, TimeSpan.FromMilliseconds(220))
|
||||
{ EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseIn } };
|
||||
var capturedGlow = glow;
|
||||
anim.Completed += (_, _) =>
|
||||
{
|
||||
capturedGlow.BeginAnimation(UIElement.OpacityProperty, null);
|
||||
capturedGlow.Opacity = 0;
|
||||
capturedGlow.Visibility = Visibility.Collapsed;
|
||||
};
|
||||
glow.BeginAnimation(UIElement.OpacityProperty, anim);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>아이콘 클릭 시 다른 랜덤 애니메이션으로 전환.</summary>
|
||||
private void DiamondIcon_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
@@ -628,12 +670,13 @@ public partial class LauncherWindow : Window
|
||||
/// <summary>런처 테두리 무지개 그라데이션 회전을 시작합니다.</summary>
|
||||
private void StartRainbowGlow()
|
||||
{
|
||||
_rainbowTimer?.Stop();
|
||||
if (_rainbowTimer != null) return; // 이미 실행 중
|
||||
if (LauncherRainbowBrush == null || RainbowGlowBorder == null) return;
|
||||
|
||||
RainbowGlowBorder.Opacity = 1; // StopRainbowGlow에서 0으로 설정된 불투명도 복원
|
||||
_rainbowTimer = new System.Windows.Threading.DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(40)
|
||||
Interval = TimeSpan.FromMilliseconds(150)
|
||||
};
|
||||
var startTime = DateTime.UtcNow;
|
||||
_rainbowTimer.Tick += (_, _) =>
|
||||
@@ -1016,12 +1059,19 @@ public partial class LauncherWindow : Window
|
||||
break;
|
||||
|
||||
case Key.Right:
|
||||
// 커서가 입력 끝에 있고 선택된 항목이 파일/앱이면 액션 서브메뉴 진입
|
||||
if (InputBox.CaretIndex == InputBox.Text.Length
|
||||
&& InputBox.Text.Length > 0
|
||||
&& _vm.CanEnterActionMode())
|
||||
// 커서가 입력 끝에 있을 때
|
||||
if (InputBox.CaretIndex == InputBox.Text.Length && InputBox.Text.Length > 0)
|
||||
{
|
||||
_vm.EnterActionMode(_vm.SelectedItem!);
|
||||
if (_vm.CanEnterActionMode())
|
||||
{
|
||||
// 파일/앱이면 액션 서브메뉴 진입
|
||||
_vm.EnterActionMode(_vm.SelectedItem!);
|
||||
}
|
||||
else if (_vm.SelectedItem != null)
|
||||
{
|
||||
// 그 외 아이템이면 컨텍스트 메뉴 표시
|
||||
ShowItemContextMenu(_vm.SelectedItem);
|
||||
}
|
||||
e.Handled = true;
|
||||
}
|
||||
break;
|
||||
@@ -1459,21 +1509,17 @@ public partial class LauncherWindow : Window
|
||||
IndexStatusText.Text = message;
|
||||
IndexStatusText.Visibility = Visibility.Visible;
|
||||
|
||||
_indexStatusTimer ??= new System.Windows.Threading.DispatcherTimer();
|
||||
_indexStatusTimer.Stop();
|
||||
_indexStatusTimer.Interval = duration;
|
||||
_indexStatusTimer.Tick -= IndexStatusTimer_Tick;
|
||||
_indexStatusTimer.Tick += IndexStatusTimer_Tick;
|
||||
_indexStatusTimer.Start();
|
||||
}
|
||||
|
||||
private void IndexStatusTimer_Tick(object? sender, EventArgs e)
|
||||
{
|
||||
if (_indexStatusTimer == null)
|
||||
return;
|
||||
|
||||
_indexStatusTimer.Stop();
|
||||
IndexStatusText.Visibility = Visibility.Collapsed;
|
||||
// 매번 새 타이머를 생성 — 이전 타이머의 stopped/fired 잔여 상태 영향 없음
|
||||
_indexStatusTimer?.Stop();
|
||||
var timer = new System.Windows.Threading.DispatcherTimer { Interval = duration };
|
||||
_indexStatusTimer = timer;
|
||||
timer.Tick += (_, _) =>
|
||||
{
|
||||
timer.Stop();
|
||||
if (ReferenceEquals(_indexStatusTimer, timer)) // 더 새로운 타이머가 없을 때만 숨김
|
||||
IndexStatusText.Visibility = Visibility.Collapsed;
|
||||
};
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1677,6 +1723,258 @@ public partial class LauncherWindow : Window
|
||||
_ = _vm.ExecuteSelectedAsync();
|
||||
}
|
||||
|
||||
private void ResultList_PreviewMouseRightButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 클릭한 ListViewItem 찾기
|
||||
var dep = e.OriginalSource as DependencyObject;
|
||||
while (dep != null && dep is not System.Windows.Controls.ListViewItem)
|
||||
dep = System.Windows.Media.VisualTreeHelper.GetParent(dep);
|
||||
if (dep is not System.Windows.Controls.ListViewItem lvi) return;
|
||||
|
||||
var item = lvi.Content as SDK.LauncherItem;
|
||||
if (item == null) return;
|
||||
|
||||
// 해당 아이템을 선택 상태로 만들고 컨텍스트 메뉴 표시
|
||||
_vm.SelectedItem = item;
|
||||
ShowItemContextMenu(item);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>선택된 런처 아이템에 대한 컨텍스트 메뉴를 표시합니다.</summary>
|
||||
private void ShowItemContextMenu(SDK.LauncherItem item)
|
||||
{
|
||||
var menu = new System.Windows.Controls.ContextMenu
|
||||
{
|
||||
Background = (System.Windows.Media.Brush)FindResource("LauncherBackground"),
|
||||
BorderBrush = (System.Windows.Media.Brush)FindResource("BorderColor"),
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(4),
|
||||
HasDropShadow = true
|
||||
};
|
||||
|
||||
// ── 타입별 액션 추가 ────────────────────────────────────────────────
|
||||
bool hasItems = false;
|
||||
|
||||
// 파일/폴더 아이템
|
||||
if (item.Data is Services.IndexEntry entry)
|
||||
{
|
||||
var expandedPath = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
bool isFolder = entry.Type == Services.IndexEntryType.Folder;
|
||||
|
||||
AddMenuItem(menu, "\uE8A5", "열기", () =>
|
||||
{
|
||||
Hide();
|
||||
_ = _vm.ExecuteSelectedAsync();
|
||||
});
|
||||
|
||||
AddMenuItem(menu, "\uEC50", isFolder ? "탐색기에서 열기" : "파일 위치 열기", () =>
|
||||
{
|
||||
Hide();
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (isFolder)
|
||||
System.Diagnostics.Process.Start("explorer.exe", $"\"{expandedPath}\"");
|
||||
else
|
||||
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{expandedPath}\"");
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
});
|
||||
|
||||
AddSeparator(menu);
|
||||
|
||||
AddMenuItem(menu, "\uE8C8", "이름 복사", () =>
|
||||
{
|
||||
System.Windows.Clipboard.SetText(System.IO.Path.GetFileName(expandedPath));
|
||||
ShowToast("이름 복사됨");
|
||||
});
|
||||
|
||||
AddMenuItem(menu, "\uE71B", "경로 복사", () =>
|
||||
{
|
||||
System.Windows.Clipboard.SetText(expandedPath);
|
||||
ShowToast("경로 복사됨");
|
||||
});
|
||||
|
||||
if (SendToChatAction != null)
|
||||
{
|
||||
AddSeparator(menu);
|
||||
AddMenuItem(menu, "\uE8BD", "AX Chat에서 분석하기", () =>
|
||||
{
|
||||
Hide();
|
||||
var msg = isFolder
|
||||
? $"다음 폴더를 분석해 주세요: {expandedPath}"
|
||||
: $"다음 파일의 내용을 분석해 주세요: {expandedPath}";
|
||||
SendToChatAction(msg);
|
||||
});
|
||||
AddMenuItem(menu, "\uE7C3", "AX Chat에 경로 붙여넣기", () =>
|
||||
{
|
||||
Hide();
|
||||
SendToChatAction(expandedPath);
|
||||
});
|
||||
}
|
||||
hasItems = true;
|
||||
}
|
||||
|
||||
// 클립보드 아이템
|
||||
else if (item.Data is Services.ClipboardEntry clipEntry)
|
||||
{
|
||||
AddMenuItem(menu, "\uE8C8", "클립보드에 복사", () =>
|
||||
{
|
||||
try { System.Windows.Clipboard.SetText(clipEntry.Text); }
|
||||
catch { }
|
||||
ShowToast("복사됨");
|
||||
});
|
||||
|
||||
if (SendToChatAction != null)
|
||||
{
|
||||
AddSeparator(menu);
|
||||
var preview = clipEntry.Text.Length > 80
|
||||
? clipEntry.Text[..80].TrimEnd() + "…"
|
||||
: clipEntry.Text;
|
||||
AddMenuItem(menu, "\uE8BD", "AI에게 보내기", () =>
|
||||
{
|
||||
Hide();
|
||||
SendToChatAction(clipEntry.Text);
|
||||
});
|
||||
}
|
||||
hasItems = true;
|
||||
}
|
||||
|
||||
// URL이 있는 아이템
|
||||
else if (!string.IsNullOrEmpty(item.ActionUrl))
|
||||
{
|
||||
var url = item.ActionUrl;
|
||||
AddMenuItem(menu, "\uE8A7", "브라우저에서 열기", () =>
|
||||
{
|
||||
Hide();
|
||||
try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(url) { UseShellExecute = true }); }
|
||||
catch { }
|
||||
});
|
||||
AddMenuItem(menu, "\uE71B", "URL 복사", () =>
|
||||
{
|
||||
System.Windows.Clipboard.SetText(url);
|
||||
ShowToast("URL 복사됨");
|
||||
});
|
||||
if (SendToChatAction != null)
|
||||
{
|
||||
AddSeparator(menu);
|
||||
AddMenuItem(menu, "\uE8BD", "AI에게 링크 보내기", () =>
|
||||
{
|
||||
Hide();
|
||||
SendToChatAction($"다음 링크를 분석해 주세요: {url}");
|
||||
});
|
||||
}
|
||||
hasItems = true;
|
||||
}
|
||||
|
||||
// 일반 텍스트/기타 (제목이라도 복사)
|
||||
if (!hasItems || (item.Data is string || item.Data == null))
|
||||
{
|
||||
AddMenuItem(menu, "\uE8C8", "제목 복사", () =>
|
||||
{
|
||||
System.Windows.Clipboard.SetText(item.Title);
|
||||
ShowToast("복사됨");
|
||||
});
|
||||
if (!string.IsNullOrEmpty(item.Subtitle))
|
||||
{
|
||||
AddMenuItem(menu, "\uE8C8", "부제목 복사", () =>
|
||||
{
|
||||
System.Windows.Clipboard.SetText(item.Subtitle);
|
||||
ShowToast("복사됨");
|
||||
});
|
||||
}
|
||||
if (SendToChatAction != null && !hasItems)
|
||||
{
|
||||
AddSeparator(menu);
|
||||
AddMenuItem(menu, "\uE8BD", "AI에게 보내기", () =>
|
||||
{
|
||||
Hide();
|
||||
SendToChatAction(item.Title);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 메뉴가 비어있으면 표시하지 않음
|
||||
if (menu.Items.Count == 0) return;
|
||||
|
||||
menu.PlacementTarget = ResultList;
|
||||
menu.Placement = System.Windows.Controls.Primitives.PlacementMode.MousePoint;
|
||||
menu.IsOpen = true;
|
||||
}
|
||||
|
||||
private void AddMenuItem(System.Windows.Controls.ContextMenu menu, string symbol, string header, Action action)
|
||||
{
|
||||
var item = new System.Windows.Controls.MenuItem
|
||||
{
|
||||
Header = BuildMenuItemHeader(symbol, header),
|
||||
Background = System.Windows.Media.Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
Padding = new Thickness(8, 5, 12, 5),
|
||||
};
|
||||
item.Click += (_, _) => action();
|
||||
// 컨텍스트 메뉴 항목 스타일 — 호버 시 강조색 배경
|
||||
item.Style = BuildMenuItemStyle();
|
||||
menu.Items.Add(item);
|
||||
}
|
||||
|
||||
private static UIElement BuildMenuItemHeader(string symbol, string text)
|
||||
{
|
||||
var panel = new System.Windows.Controls.StackPanel { Orientation = System.Windows.Controls.Orientation.Horizontal };
|
||||
panel.Children.Add(new System.Windows.Controls.TextBlock
|
||||
{
|
||||
Text = symbol,
|
||||
FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 11,
|
||||
Foreground = System.Windows.Media.Brushes.Gray,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Width = 20,
|
||||
TextAlignment = TextAlignment.Center
|
||||
});
|
||||
panel.Children.Add(new System.Windows.Controls.TextBlock
|
||||
{
|
||||
Text = text,
|
||||
FontFamily = new System.Windows.Media.FontFamily("Segoe UI, Malgun Gothic"),
|
||||
FontSize = 12,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(4, 0, 0, 0)
|
||||
});
|
||||
return panel;
|
||||
}
|
||||
|
||||
private System.Windows.Style BuildMenuItemStyle()
|
||||
{
|
||||
var style = new System.Windows.Style(typeof(System.Windows.Controls.MenuItem));
|
||||
style.Setters.Add(new Setter(System.Windows.Controls.MenuItem.ForegroundProperty,
|
||||
FindResource("PrimaryText") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.White));
|
||||
style.Setters.Add(new Setter(System.Windows.Controls.MenuItem.BackgroundProperty,
|
||||
System.Windows.Media.Brushes.Transparent));
|
||||
|
||||
var hoverTrigger = new Trigger
|
||||
{
|
||||
Property = System.Windows.Controls.MenuItem.IsHighlightedProperty,
|
||||
Value = true
|
||||
};
|
||||
var accentBrush = FindResource("AccentColor") as System.Windows.Media.SolidColorBrush;
|
||||
var hoverColor = accentBrush != null
|
||||
? System.Windows.Media.Color.FromArgb(30, accentBrush.Color.R, accentBrush.Color.G, accentBrush.Color.B)
|
||||
: System.Windows.Media.Color.FromArgb(30, 75, 94, 252);
|
||||
hoverTrigger.Setters.Add(new Setter(System.Windows.Controls.MenuItem.BackgroundProperty,
|
||||
new System.Windows.Media.SolidColorBrush(hoverColor)));
|
||||
style.Triggers.Add(hoverTrigger);
|
||||
return style;
|
||||
}
|
||||
|
||||
private static void AddSeparator(System.Windows.Controls.ContextMenu menu)
|
||||
{
|
||||
menu.Items.Add(new System.Windows.Controls.Separator
|
||||
{
|
||||
Margin = new Thickness(6, 2, 6, 2),
|
||||
Opacity = 0.3
|
||||
});
|
||||
}
|
||||
|
||||
private void Window_Deactivated(object sender, EventArgs e)
|
||||
{
|
||||
// 설정 › 기능 › "포커스 잃으면 닫기"가 켜진 경우에만 자동 숨김
|
||||
@@ -1688,6 +1986,10 @@ public partial class LauncherWindow : Window
|
||||
if (CurrentApp?.SettingsService != null)
|
||||
CurrentApp.SettingsService.SettingsChanged -= OnSettingsChanged;
|
||||
|
||||
StopRainbowGlow();
|
||||
_iconStoryboard?.Stop();
|
||||
_iconStoryboard = null;
|
||||
|
||||
base.OnClosed(e);
|
||||
}
|
||||
|
||||
@@ -1704,12 +2006,16 @@ public partial class LauncherWindow : Window
|
||||
if (isVisible)
|
||||
{
|
||||
StartWidgetUpdates();
|
||||
ApplyVisualSettings(); // 숨겨져 있는 동안 중지된 애니메이션 재개
|
||||
return;
|
||||
}
|
||||
|
||||
_quickLookWindow?.Close();
|
||||
_quickLookWindow = null;
|
||||
StopWidgetUpdates();
|
||||
StopRainbowGlow(); // 숨김 상태에서 CPU 낭비 방지
|
||||
_iconStoryboard?.Stop(); // 숨김 상태에서 애니메이션 중지
|
||||
_iconStoryboard = null;
|
||||
SaveRememberedPosition();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user