모델 프로파일 기반 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:
2026-04-08 13:41:57 +09:00
parent b391dfdfb3
commit a2c952879d
552 changed files with 8094 additions and 13595 deletions

View File

@@ -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();
}