[Phase48] 대형 파일 분할 리팩터링 4차 — 4개 신규 파셜 파일 생성

## 분할 대상 및 결과

### ChatWindow.Controls.cs (595줄 → 372줄)
- ChatWindow.TabSwitching.cs (232줄, 신규): _activeTab 필드, _tabConversationId 필드
  TabChat/Cowork/Code_Checked, UpdateTabUI, BtnPlanMode_Click, UpdatePlanModeUI,
  SwitchToTabConversation, SaveCurrentTabConversationId, StopStreamingIfActive

### ChatWindow.SlashCommands.cs (579줄 → 406줄)
- ChatWindow.DropActions.cs (160줄, 신규): DropActions 딕셔너리, CodeExtensions, DataExtensions,
  _dropActionPopup 필드, ShowDropActionMenu() 메서드

### WorkflowAnalyzerWindow.Charts.cs (667줄 → 397줄)
- WorkflowAnalyzerWindow.Timeline.cs (281줄, 신규): CreateTimelineNode, GetEventVisual, CreateBadge,
  ShowDetail, UpdateSummaryCards, FormatMs, Truncate, 윈도우 이벤트 핸들러, WndProc

### SkillGalleryWindow.xaml.cs (631줄 → ~430줄)
- SkillGalleryWindow.SkillDetail.cs (197줄, 신규): ShowSkillDetail() 메서드 전체
  (스킬 상세 보기 팝업 — 메타정보·프롬프트 미리보기·Action 버튼)

## 빌드 결과: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 21:12:38 +09:00
parent 27bd8de83a
commit 39e07dd947
8 changed files with 906 additions and 866 deletions

View File

@@ -369,227 +369,4 @@ public partial class ChatWindow
MaximizeIcon.Text = WindowState == WindowState.Maximized ? "\uE923" : "\uE739"; // 복원/최대화 아이콘
}
private void BtnClose_Click(object sender, RoutedEventArgs e) => Close();
// ─── 탭 전환 ──────────────────────────────────────────────────────────
private string _activeTab = "Chat";
private void SaveCurrentTabConversationId()
{
lock (_convLock)
{
if (_currentConversation != null && _currentConversation.Messages.Count > 0)
{
_tabConversationId[_activeTab] = _currentConversation.Id;
// 탭 전환 시 현재 대화를 즉시 저장 (스트리밍 중이어도 진행 중인 내용 보존)
try { _storage.Save(_currentConversation); } catch (Exception) { /* 대화 저장 실패 — UI 차단 방지 */ }
}
}
// 탭별 마지막 대화 ID를 설정에 영속 저장 (앱 재시작 시 복원용)
SaveLastConversations();
}
/// <summary>탭 전환 전 스트리밍 중이면 즉시 중단합니다.</summary>
private void StopStreamingIfActive()
{
if (!_isStreaming) return;
// 스트리밍 중단
_streamCts?.Cancel();
_cursorTimer.Stop();
_elapsedTimer.Stop();
_typingTimer.Stop();
StopRainbowGlow();
HideStickyProgress();
_activeStreamText = null;
_elapsedLabel = null;
_cachedStreamContent = "";
_isStreaming = false;
BtnSend.IsEnabled = true;
BtnStop.Visibility = Visibility.Collapsed;
BtnPause.Visibility = Visibility.Collapsed;
PauseIcon.Text = "\uE769"; // 리셋
BtnSend.Visibility = Visibility.Visible;
_streamCts?.Dispose();
_streamCts = null;
SetStatusIdle();
}
private void TabChat_Checked(object sender, RoutedEventArgs e)
{
if (_activeTab == "Chat") return;
StopStreamingIfActive();
SaveCurrentTabConversationId();
_activeTab = "Chat";
_selectedCategory = ""; UpdateCategoryLabel();
UpdateTabUI();
UpdatePlanModeUI();
}
private void TabCowork_Checked(object sender, RoutedEventArgs e)
{
if (_activeTab == "Cowork") return;
StopStreamingIfActive();
SaveCurrentTabConversationId();
_activeTab = "Cowork";
_selectedCategory = ""; UpdateCategoryLabel();
UpdateTabUI();
UpdatePlanModeUI();
}
private void TabCode_Checked(object sender, RoutedEventArgs e)
{
if (_activeTab == "Code") return;
StopStreamingIfActive();
SaveCurrentTabConversationId();
_activeTab = "Code";
_selectedCategory = ""; UpdateCategoryLabel();
UpdateTabUI();
UpdatePlanModeUI();
}
/// <summary>탭별로 마지막으로 활성화된 대화 ID를 기억.</summary>
private readonly Dictionary<string, string?> _tabConversationId = new()
{
["Chat"] = null, ["Cowork"] = null, ["Code"] = null,
};
private void UpdateTabUI()
{
// 폴더 바는 Cowork/Code 탭에서만 표시
if (FolderBar != null)
FolderBar.Visibility = _activeTab != "Chat" ? Visibility.Visible : Visibility.Collapsed;
// 탭별 입력 안내 문구
if (InputWatermark != null)
{
InputWatermark.Text = _activeTab switch
{
"Cowork" => "에이전트에게 작업을 요청하세요 (파일 읽기/쓰기, 문서 생성...)",
"Code" => "코드 관련 작업을 요청하세요...",
_ => _promptCardPlaceholder,
};
}
// 권한 기본값 적용 (Cowork/Code 탭은 설정의 기본값 사용)
ApplyTabDefaultPermission();
// 포맷/디자인 드롭다운은 Cowork 탭에서만 표시
if (_activeTab == "Cowork")
{
BuildBottomBar();
if (Llm.ShowFileBrowser && FileBrowserPanel != null)
{
FileBrowserPanel.Visibility = Visibility.Visible;
BuildFileTree();
}
}
else if (_activeTab == "Code")
{
// Code 탭: 언어 선택기 + 파일 탐색기
BuildCodeBottomBar();
if (Llm.ShowFileBrowser && FileBrowserPanel != null)
{
FileBrowserPanel.Visibility = Visibility.Visible;
BuildFileTree();
}
}
else
{
MoodIconPanel.Children.Clear();
if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Collapsed;
if (FileBrowserPanel != null) FileBrowserPanel.Visibility = Visibility.Collapsed;
}
// 탭별 프리셋 버튼 재구성
BuildTopicButtons();
// 현재 대화를 해당 탭 대화로 전환
SwitchToTabConversation();
// Cowork/Code 탭 전환 시 팁 표시
ShowRandomTip();
}
private void BtnPlanMode_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
// 3단 순환: off → auto → always → off
Llm.PlanMode = Llm.PlanMode switch
{
"auto" => "always",
"always" => "off",
_ => "auto"
};
_settings.Save();
UpdatePlanModeUI();
}
private void UpdatePlanModeUI()
{
var planMode = Llm.PlanMode ?? "off";
if (PlanModeValue == null) return;
PlanModeValue.Text = planMode switch
{
"auto" => "Auto",
"always" => "Always",
_ => "Off"
};
var isActive = planMode != "off";
var activeBrush = ThemeResourceHelper.Accent(this);
var secondaryBrush = ThemeResourceHelper.Secondary(this);
if (PlanModeIcon != null) PlanModeIcon.Foreground = isActive ? activeBrush : secondaryBrush;
if (PlanModeLabel != null) PlanModeLabel.Foreground = isActive ? activeBrush : secondaryBrush;
if (BtnPlanMode != null)
BtnPlanMode.Background = isActive
? new SolidColorBrush(Color.FromArgb(0x1A, 0x4B, 0x5E, 0xFC))
: Brushes.Transparent;
}
private void SwitchToTabConversation()
{
// 이전 탭의 대화 저장
lock (_convLock)
{
if (_currentConversation != null && _currentConversation.Messages.Count > 0)
{
try { _storage.Save(_currentConversation); } catch (Exception) { /* 대화 저장 실패 — UI 차단 방지 */ }
}
}
// 현재 탭에 기억된 대화가 있으면 복원
var savedId = _tabConversationId.GetValueOrDefault(_activeTab);
if (!string.IsNullOrEmpty(savedId))
{
var conv = _storage.Load(savedId);
if (conv != null)
{
if (string.IsNullOrEmpty(conv.Tab)) conv.Tab = _activeTab;
lock (_convLock) _currentConversation = conv;
MessagePanel.Children.Clear();
foreach (var msg in conv.Messages)
AddMessageBubble(msg.Role, msg.Content, animate: false, message: msg);
EmptyState.Visibility = conv.Messages.Count > 0 ? Visibility.Collapsed : Visibility.Visible;
UpdateChatTitle();
RefreshConversationList();
UpdateFolderBar();
return;
}
}
// 기억된 대화가 없으면 새 대화
lock (_convLock)
{
_currentConversation = new ChatConversation { Tab = _activeTab };
var workFolder = Llm.WorkFolder;
if (!string.IsNullOrEmpty(workFolder) && _activeTab != "Chat")
_currentConversation.WorkFolder = workFolder;
}
MessagePanel.Children.Clear();
EmptyState.Visibility = Visibility.Visible;
_attachedFiles.Clear();
RefreshAttachedFilesUI();
UpdateChatTitle();
RefreshConversationList();
UpdateFolderBar();
}
}

View File

@@ -0,0 +1,183 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
namespace AxCopilot.Views;
public partial class ChatWindow
{
// ─── 드래그 앤 드롭 AI 액션 팝업 ─────────────────────────────────────
private static readonly Dictionary<string, List<(string Label, string Icon, string Prompt)>> DropActions = new(StringComparer.OrdinalIgnoreCase)
{
["code"] =
[
("코드 리뷰", "\uE943", "첨부된 코드를 리뷰해 주세요. 버그, 성능 이슈, 보안 취약점, 개선점을 찾아 구체적으로 제안하세요."),
("코드 설명", "\uE946", "첨부된 코드를 상세히 설명해 주세요. 주요 함수, 데이터 흐름, 설계 패턴을 포함하세요."),
("리팩토링 제안", "\uE70F", "첨부된 코드의 리팩토링 방안을 제안해 주세요. 가독성, 유지보수성, 성능을 고려하세요."),
("테스트 생성", "\uE9D5", "첨부된 코드에 대한 단위 테스트 코드를 생성해 주세요."),
],
["document"] =
[
("요약", "\uE8AB", "첨부된 문서를 핵심 포인트 위주로 간결하게 요약해 주세요."),
("분석", "\uE9D9", "첨부된 문서의 내용을 분석하고 주요 인사이트를 도출해 주세요."),
("번역", "\uE8C1", "첨부된 문서를 영어로 번역해 주세요. 원문의 톤과 뉘앙스를 유지하세요."),
],
["data"] =
[
("데이터 분석", "\uE9D9", "첨부된 데이터를 분석해 주세요. 통계, 추세, 이상치를 찾아 보고하세요."),
("시각화 제안", "\uE9D9", "첨부된 데이터를 시각화할 최적의 차트 유형을 제안하고 chart_create로 생성해 주세요."),
("포맷 변환", "\uE8AB", "첨부된 데이터를 다른 형식으로 변환해 주세요. (CSV↔JSON↔Excel 등)"),
],
["image"] =
[
("이미지 설명", "\uE946", "첨부된 이미지를 자세히 설명해 주세요. 내용, 레이아웃, 텍스트를 분석하세요."),
("UI 리뷰", "\uE70F", "첨부된 UI 스크린샷을 리뷰해 주세요. UX 개선점, 접근성, 디자인 일관성을 평가하세요."),
],
};
private static readonly HashSet<string> CodeExtensions = new(StringComparer.OrdinalIgnoreCase)
{ ".cs", ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".cpp", ".c", ".h", ".go", ".rs", ".rb", ".php", ".swift", ".kt", ".scala", ".sh", ".ps1", ".bat", ".cmd", ".sql", ".xaml", ".vue" };
private static readonly HashSet<string> DataExtensions = new(StringComparer.OrdinalIgnoreCase)
{ ".csv", ".json", ".xml", ".yaml", ".yml", ".tsv" };
// ImageExtensions는 이미지 첨부 영역에서 정의됨 — 재사용
private Popup? _dropActionPopup;
private void ShowDropActionMenu(string[] files)
{
// 파일 유형 판별
var ext = System.IO.Path.GetExtension(files[0]).ToLowerInvariant();
string category;
if (CodeExtensions.Contains(ext)) category = "code";
else if (DataExtensions.Contains(ext)) category = "data";
else if (ImageExtensions.Contains(ext)) category = "image";
else category = "document";
var actions = DropActions.GetValueOrDefault(category) ?? DropActions["document"];
// 팝업 생성
_dropActionPopup?.SetValue(Popup.IsOpenProperty, false);
var panel = new StackPanel();
// 헤더
var header = new TextBlock
{
Text = $"📎 {System.IO.Path.GetFileName(files[0])}{(files.Length > 1 ? $" {files.Length - 1}" : "")}",
FontSize = 11,
Foreground = ThemeResourceHelper.Secondary(this),
Margin = new Thickness(12, 8, 12, 6),
};
panel.Children.Add(header);
// 액션 항목
var hoverBrush = ThemeResourceHelper.HoverBg(this);
var accentBrush = ThemeResourceHelper.Accent(this);
var textBrush = ThemeResourceHelper.Primary(this);
foreach (var (label, icon, prompt) in actions)
{
var capturedPrompt = prompt;
var row = new Border
{
Background = Brushes.Transparent,
CornerRadius = new CornerRadius(6),
Padding = new Thickness(12, 7, 12, 7),
Margin = new Thickness(4, 1, 4, 1),
Cursor = Cursors.Hand,
};
var stack = new StackPanel { Orientation = Orientation.Horizontal };
stack.Children.Add(new TextBlock
{
Text = icon,
FontFamily = ThemeResourceHelper.SegoeMdl2,
FontSize = 13, Foreground = accentBrush,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
});
stack.Children.Add(new TextBlock
{
Text = label, FontSize = 13, FontWeight = FontWeights.SemiBold,
Foreground = textBrush, VerticalAlignment = VerticalAlignment.Center,
});
row.Child = stack;
row.MouseEnter += (_, _) => row.Background = hoverBrush;
row.MouseLeave += (_, _) => row.Background = Brushes.Transparent;
row.MouseLeftButtonUp += (_, _) =>
{
if (_dropActionPopup != null) _dropActionPopup.IsOpen = false;
foreach (var f in files) AddAttachedFile(f);
InputBox.Text = capturedPrompt;
InputBox.CaretIndex = InputBox.Text.Length;
InputBox.Focus();
if (Llm.DragDropAutoSend)
_ = SendMessageAsync();
};
panel.Children.Add(row);
}
// "첨부만" 항목
var attachOnly = new Border
{
Background = Brushes.Transparent,
CornerRadius = new CornerRadius(6),
Padding = new Thickness(12, 7, 12, 7),
Margin = new Thickness(4, 1, 4, 1),
Cursor = Cursors.Hand,
};
var attachStack = new StackPanel { Orientation = Orientation.Horizontal };
attachStack.Children.Add(new TextBlock
{
Text = "\uE723",
FontFamily = ThemeResourceHelper.SegoeMdl2,
FontSize = 13, Foreground = ThemeResourceHelper.Secondary(this),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
});
attachStack.Children.Add(new TextBlock
{
Text = "첨부만", FontSize = 13,
Foreground = ThemeResourceHelper.Secondary(this),
VerticalAlignment = VerticalAlignment.Center,
});
attachOnly.Child = attachStack;
attachOnly.MouseEnter += (_, _) => attachOnly.Background = hoverBrush;
attachOnly.MouseLeave += (_, _) => attachOnly.Background = Brushes.Transparent;
attachOnly.MouseLeftButtonUp += (_, _) =>
{
if (_dropActionPopup != null) _dropActionPopup.IsOpen = false;
foreach (var f in files) AddAttachedFile(f);
InputBox.Focus();
};
panel.Children.Add(attachOnly);
var container = new Border
{
Background = ThemeResourceHelper.Background(this),
CornerRadius = new CornerRadius(12),
BorderBrush = ThemeResourceHelper.Border(this),
BorderThickness = new Thickness(1),
Padding = new Thickness(4, 4, 4, 6),
Child = panel,
MinWidth = 200,
Effect = new System.Windows.Media.Effects.DropShadowEffect
{
Color = Colors.Black, BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3,
},
};
_dropActionPopup = new Popup
{
PlacementTarget = InputBorder,
Placement = PlacementMode.Top,
StaysOpen = false,
AllowsTransparency = true,
PopupAnimation = PopupAnimation.Fade,
Child = container,
};
_dropActionPopup.IsOpen = true;
}
}

View File

@@ -403,177 +403,4 @@ public partial class ChatWindow
return (null, input);
}
// ─── 드래그 앤 드롭 AI 액션 팝업 ─────────────────────────────────────
private static readonly Dictionary<string, List<(string Label, string Icon, string Prompt)>> DropActions = new(StringComparer.OrdinalIgnoreCase)
{
["code"] =
[
("코드 리뷰", "\uE943", "첨부된 코드를 리뷰해 주세요. 버그, 성능 이슈, 보안 취약점, 개선점을 찾아 구체적으로 제안하세요."),
("코드 설명", "\uE946", "첨부된 코드를 상세히 설명해 주세요. 주요 함수, 데이터 흐름, 설계 패턴을 포함하세요."),
("리팩토링 제안", "\uE70F", "첨부된 코드의 리팩토링 방안을 제안해 주세요. 가독성, 유지보수성, 성능을 고려하세요."),
("테스트 생성", "\uE9D5", "첨부된 코드에 대한 단위 테스트 코드를 생성해 주세요."),
],
["document"] =
[
("요약", "\uE8AB", "첨부된 문서를 핵심 포인트 위주로 간결하게 요약해 주세요."),
("분석", "\uE9D9", "첨부된 문서의 내용을 분석하고 주요 인사이트를 도출해 주세요."),
("번역", "\uE8C1", "첨부된 문서를 영어로 번역해 주세요. 원문의 톤과 뉘앙스를 유지하세요."),
],
["data"] =
[
("데이터 분석", "\uE9D9", "첨부된 데이터를 분석해 주세요. 통계, 추세, 이상치를 찾아 보고하세요."),
("시각화 제안", "\uE9D9", "첨부된 데이터를 시각화할 최적의 차트 유형을 제안하고 chart_create로 생성해 주세요."),
("포맷 변환", "\uE8AB", "첨부된 데이터를 다른 형식으로 변환해 주세요. (CSV↔JSON↔Excel 등)"),
],
["image"] =
[
("이미지 설명", "\uE946", "첨부된 이미지를 자세히 설명해 주세요. 내용, 레이아웃, 텍스트를 분석하세요."),
("UI 리뷰", "\uE70F", "첨부된 UI 스크린샷을 리뷰해 주세요. UX 개선점, 접근성, 디자인 일관성을 평가하세요."),
],
};
private static readonly HashSet<string> CodeExtensions = new(StringComparer.OrdinalIgnoreCase)
{ ".cs", ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".cpp", ".c", ".h", ".go", ".rs", ".rb", ".php", ".swift", ".kt", ".scala", ".sh", ".ps1", ".bat", ".cmd", ".sql", ".xaml", ".vue" };
private static readonly HashSet<string> DataExtensions = new(StringComparer.OrdinalIgnoreCase)
{ ".csv", ".json", ".xml", ".yaml", ".yml", ".tsv" };
// ImageExtensions는 이미지 첨부 영역에서 정의됨 — 재사용
private Popup? _dropActionPopup;
private void ShowDropActionMenu(string[] files)
{
// 파일 유형 판별
var ext = System.IO.Path.GetExtension(files[0]).ToLowerInvariant();
string category;
if (CodeExtensions.Contains(ext)) category = "code";
else if (DataExtensions.Contains(ext)) category = "data";
else if (ImageExtensions.Contains(ext)) category = "image";
else category = "document";
var actions = DropActions.GetValueOrDefault(category) ?? DropActions["document"];
// 팝업 생성
_dropActionPopup?.SetValue(Popup.IsOpenProperty, false);
var panel = new StackPanel();
// 헤더
var header = new TextBlock
{
Text = $"📎 {System.IO.Path.GetFileName(files[0])}{(files.Length > 1 ? $" {files.Length - 1}" : "")}",
FontSize = 11,
Foreground = ThemeResourceHelper.Secondary(this),
Margin = new Thickness(12, 8, 12, 6),
};
panel.Children.Add(header);
// 액션 항목
var hoverBrush = ThemeResourceHelper.HoverBg(this);
var accentBrush = ThemeResourceHelper.Accent(this);
var textBrush = ThemeResourceHelper.Primary(this);
foreach (var (label, icon, prompt) in actions)
{
var capturedPrompt = prompt;
var row = new Border
{
Background = Brushes.Transparent,
CornerRadius = new CornerRadius(6),
Padding = new Thickness(12, 7, 12, 7),
Margin = new Thickness(4, 1, 4, 1),
Cursor = Cursors.Hand,
};
var stack = new StackPanel { Orientation = Orientation.Horizontal };
stack.Children.Add(new TextBlock
{
Text = icon,
FontFamily = ThemeResourceHelper.SegoeMdl2,
FontSize = 13, Foreground = accentBrush,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
});
stack.Children.Add(new TextBlock
{
Text = label, FontSize = 13, FontWeight = FontWeights.SemiBold,
Foreground = textBrush, VerticalAlignment = VerticalAlignment.Center,
});
row.Child = stack;
row.MouseEnter += (_, _) => row.Background = hoverBrush;
row.MouseLeave += (_, _) => row.Background = Brushes.Transparent;
row.MouseLeftButtonUp += (_, _) =>
{
if (_dropActionPopup != null) _dropActionPopup.IsOpen = false;
foreach (var f in files) AddAttachedFile(f);
InputBox.Text = capturedPrompt;
InputBox.CaretIndex = InputBox.Text.Length;
InputBox.Focus();
if (Llm.DragDropAutoSend)
_ = SendMessageAsync();
};
panel.Children.Add(row);
}
// "첨부만" 항목
var attachOnly = new Border
{
Background = Brushes.Transparent,
CornerRadius = new CornerRadius(6),
Padding = new Thickness(12, 7, 12, 7),
Margin = new Thickness(4, 1, 4, 1),
Cursor = Cursors.Hand,
};
var attachStack = new StackPanel { Orientation = Orientation.Horizontal };
attachStack.Children.Add(new TextBlock
{
Text = "\uE723",
FontFamily = ThemeResourceHelper.SegoeMdl2,
FontSize = 13, Foreground = ThemeResourceHelper.Secondary(this),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
});
attachStack.Children.Add(new TextBlock
{
Text = "첨부만", FontSize = 13,
Foreground = ThemeResourceHelper.Secondary(this),
VerticalAlignment = VerticalAlignment.Center,
});
attachOnly.Child = attachStack;
attachOnly.MouseEnter += (_, _) => attachOnly.Background = hoverBrush;
attachOnly.MouseLeave += (_, _) => attachOnly.Background = Brushes.Transparent;
attachOnly.MouseLeftButtonUp += (_, _) =>
{
if (_dropActionPopup != null) _dropActionPopup.IsOpen = false;
foreach (var f in files) AddAttachedFile(f);
InputBox.Focus();
};
panel.Children.Add(attachOnly);
var container = new Border
{
Background = ThemeResourceHelper.Background(this),
CornerRadius = new CornerRadius(12),
BorderBrush = ThemeResourceHelper.Border(this),
BorderThickness = new Thickness(1),
Padding = new Thickness(4, 4, 4, 6),
Child = panel,
MinWidth = 200,
Effect = new System.Windows.Media.Effects.DropShadowEffect
{
Color = Colors.Black, BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3,
},
};
_dropActionPopup = new Popup
{
PlacementTarget = InputBorder,
Placement = PlacementMode.Top,
StaysOpen = false,
AllowsTransparency = true,
PopupAnimation = PopupAnimation.Fade,
Child = container,
};
_dropActionPopup.IsOpen = true;
}
}

View File

@@ -0,0 +1,232 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using AxCopilot.Services;
namespace AxCopilot.Views;
public partial class ChatWindow
{
// ─── 탭 전환 ──────────────────────────────────────────────────────────
private string _activeTab = "Chat";
/// <summary>탭별로 마지막으로 활성화된 대화 ID를 기억.</summary>
private readonly Dictionary<string, string?> _tabConversationId = new()
{
["Chat"] = null, ["Cowork"] = null, ["Code"] = null,
};
private void SaveCurrentTabConversationId()
{
lock (_convLock)
{
if (_currentConversation != null && _currentConversation.Messages.Count > 0)
{
_tabConversationId[_activeTab] = _currentConversation.Id;
// 탭 전환 시 현재 대화를 즉시 저장 (스트리밍 중이어도 진행 중인 내용 보존)
try { _storage.Save(_currentConversation); } catch (Exception) { /* 대화 저장 실패 — UI 차단 방지 */ }
}
}
// 탭별 마지막 대화 ID를 설정에 영속 저장 (앱 재시작 시 복원용)
SaveLastConversations();
}
/// <summary>탭 전환 전 스트리밍 중이면 즉시 중단합니다.</summary>
private void StopStreamingIfActive()
{
if (!_isStreaming) return;
// 스트리밍 중단
_streamCts?.Cancel();
_cursorTimer.Stop();
_elapsedTimer.Stop();
_typingTimer.Stop();
StopRainbowGlow();
HideStickyProgress();
_activeStreamText = null;
_elapsedLabel = null;
_cachedStreamContent = "";
_isStreaming = false;
BtnSend.IsEnabled = true;
BtnStop.Visibility = Visibility.Collapsed;
BtnPause.Visibility = Visibility.Collapsed;
PauseIcon.Text = "\uE769"; // 리셋
BtnSend.Visibility = Visibility.Visible;
_streamCts?.Dispose();
_streamCts = null;
SetStatusIdle();
}
private void TabChat_Checked(object sender, RoutedEventArgs e)
{
if (_activeTab == "Chat") return;
StopStreamingIfActive();
SaveCurrentTabConversationId();
_activeTab = "Chat";
_selectedCategory = ""; UpdateCategoryLabel();
UpdateTabUI();
UpdatePlanModeUI();
}
private void TabCowork_Checked(object sender, RoutedEventArgs e)
{
if (_activeTab == "Cowork") return;
StopStreamingIfActive();
SaveCurrentTabConversationId();
_activeTab = "Cowork";
_selectedCategory = ""; UpdateCategoryLabel();
UpdateTabUI();
UpdatePlanModeUI();
}
private void TabCode_Checked(object sender, RoutedEventArgs e)
{
if (_activeTab == "Code") return;
StopStreamingIfActive();
SaveCurrentTabConversationId();
_activeTab = "Code";
_selectedCategory = ""; UpdateCategoryLabel();
UpdateTabUI();
UpdatePlanModeUI();
}
private void UpdateTabUI()
{
// 폴더 바는 Cowork/Code 탭에서만 표시
if (FolderBar != null)
FolderBar.Visibility = _activeTab != "Chat" ? Visibility.Visible : Visibility.Collapsed;
// 탭별 입력 안내 문구
if (InputWatermark != null)
{
InputWatermark.Text = _activeTab switch
{
"Cowork" => "에이전트에게 작업을 요청하세요 (파일 읽기/쓰기, 문서 생성...)",
"Code" => "코드 관련 작업을 요청하세요...",
_ => _promptCardPlaceholder,
};
}
// 권한 기본값 적용 (Cowork/Code 탭은 설정의 기본값 사용)
ApplyTabDefaultPermission();
// 포맷/디자인 드롭다운은 Cowork 탭에서만 표시
if (_activeTab == "Cowork")
{
BuildBottomBar();
if (Llm.ShowFileBrowser && FileBrowserPanel != null)
{
FileBrowserPanel.Visibility = Visibility.Visible;
BuildFileTree();
}
}
else if (_activeTab == "Code")
{
// Code 탭: 언어 선택기 + 파일 탐색기
BuildCodeBottomBar();
if (Llm.ShowFileBrowser && FileBrowserPanel != null)
{
FileBrowserPanel.Visibility = Visibility.Visible;
BuildFileTree();
}
}
else
{
MoodIconPanel.Children.Clear();
if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Collapsed;
if (FileBrowserPanel != null) FileBrowserPanel.Visibility = Visibility.Collapsed;
}
// 탭별 프리셋 버튼 재구성
BuildTopicButtons();
// 현재 대화를 해당 탭 대화로 전환
SwitchToTabConversation();
// Cowork/Code 탭 전환 시 팁 표시
ShowRandomTip();
}
private void BtnPlanMode_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
// 3단 순환: off → auto → always → off
Llm.PlanMode = Llm.PlanMode switch
{
"auto" => "always",
"always" => "off",
_ => "auto"
};
_settings.Save();
UpdatePlanModeUI();
}
private void UpdatePlanModeUI()
{
var planMode = Llm.PlanMode ?? "off";
if (PlanModeValue == null) return;
PlanModeValue.Text = planMode switch
{
"auto" => "Auto",
"always" => "Always",
_ => "Off"
};
var isActive = planMode != "off";
var activeBrush = ThemeResourceHelper.Accent(this);
var secondaryBrush = ThemeResourceHelper.Secondary(this);
if (PlanModeIcon != null) PlanModeIcon.Foreground = isActive ? activeBrush : secondaryBrush;
if (PlanModeLabel != null) PlanModeLabel.Foreground = isActive ? activeBrush : secondaryBrush;
if (BtnPlanMode != null)
BtnPlanMode.Background = isActive
? new SolidColorBrush(Color.FromArgb(0x1A, 0x4B, 0x5E, 0xFC))
: Brushes.Transparent;
}
private void SwitchToTabConversation()
{
// 이전 탭의 대화 저장
lock (_convLock)
{
if (_currentConversation != null && _currentConversation.Messages.Count > 0)
{
try { _storage.Save(_currentConversation); } catch (Exception) { /* 대화 저장 실패 — UI 차단 방지 */ }
}
}
// 현재 탭에 기억된 대화가 있으면 복원
var savedId = _tabConversationId.GetValueOrDefault(_activeTab);
if (!string.IsNullOrEmpty(savedId))
{
var conv = _storage.Load(savedId);
if (conv != null)
{
if (string.IsNullOrEmpty(conv.Tab)) conv.Tab = _activeTab;
lock (_convLock) _currentConversation = conv;
MessagePanel.Children.Clear();
foreach (var msg in conv.Messages)
AddMessageBubble(msg.Role, msg.Content, animate: false, message: msg);
EmptyState.Visibility = conv.Messages.Count > 0 ? Visibility.Collapsed : Visibility.Visible;
UpdateChatTitle();
RefreshConversationList();
UpdateFolderBar();
return;
}
}
// 기억된 대화가 없으면 새 대화
lock (_convLock)
{
_currentConversation = new Models.ChatConversation { Tab = _activeTab };
var workFolder = Llm.WorkFolder;
if (!string.IsNullOrEmpty(workFolder) && _activeTab != "Chat")
_currentConversation.WorkFolder = workFolder;
}
MessagePanel.Children.Clear();
EmptyState.Visibility = Visibility.Visible;
_attachedFiles.Clear();
RefreshAttachedFilesUI();
UpdateChatTitle();
RefreshConversationList();
UpdateFolderBar();
}
}

View File

@@ -0,0 +1,210 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using AxCopilot.Services.Agent;
namespace AxCopilot.Views;
public partial class SkillGalleryWindow
{
// ─── 스킬 상세 보기 팝업 ───────────────────────────────────────────────
private void ShowSkillDetail(SkillDefinition skill)
{
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? Brushes.Black;
var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var subBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent;
var borderBr = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var popup = new Window
{
Title = $"/{skill.Name}",
Width = 580,
Height = 480,
WindowStyle = WindowStyle.None,
AllowsTransparency = true,
Background = Brushes.Transparent,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Owner = this,
};
var outerBorder = new Border
{
Background = bgBrush,
CornerRadius = new CornerRadius(12),
BorderBrush = borderBr,
BorderThickness = new Thickness(1, 1, 1, 1),
Effect = new System.Windows.Media.Effects.DropShadowEffect
{
BlurRadius = 20,
ShadowDepth = 4,
Opacity = 0.3,
Color = Colors.Black,
},
};
var mainGrid = new Grid();
mainGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(44) });
mainGrid.RowDefinitions.Add(new RowDefinition());
// ── 타이틀바 ──
var titleBar = new Border
{
CornerRadius = new CornerRadius(12, 12, 0, 0),
Background = itemBg,
};
titleBar.MouseLeftButtonDown += (_, e) =>
{
var pos = e.GetPosition(popup);
if (pos.X > popup.ActualWidth - 50) return;
popup.DragMove();
};
var titleGrid = new Grid { Margin = new Thickness(16, 0, 12, 0) };
var titleLeft = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
titleLeft.Children.Add(new TextBlock
{
Text = skill.Icon,
FontFamily = ThemeResourceHelper.SegoeMdl2,
FontSize = 14,
Foreground = new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 1, 8, 0),
});
titleLeft.Children.Add(new TextBlock
{
Text = $"/{skill.Name} {skill.Label}",
FontSize = 14,
FontWeight = FontWeights.SemiBold,
Foreground = fgBrush,
VerticalAlignment = VerticalAlignment.Center,
});
titleGrid.Children.Add(titleLeft);
var closeBtn = new Border
{
Width = 28, Height = 28,
CornerRadius = new CornerRadius(6),
Cursor = Cursors.Hand,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
};
closeBtn.Child = new TextBlock
{
Text = "\uE8BB",
FontFamily = ThemeResourceHelper.SegoeMdl2,
FontSize = 10,
Foreground = subBrush,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
closeBtn.MouseEnter += (s, _) => ((Border)s).Background =
new SolidColorBrush(Color.FromArgb(0x33, 0xFF, 0x44, 0x44));
closeBtn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent;
closeBtn.MouseLeftButtonUp += (_, _) => popup.Close();
titleGrid.Children.Add(closeBtn);
titleBar.Child = titleGrid;
Grid.SetRow(titleBar, 0);
// ── 본문: 스킬 정보 + 프롬프트 미리보기 ──
var body = new ScrollViewer
{
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
Padding = new Thickness(20, 16, 20, 16),
};
var bodyPanel = new StackPanel();
// 메타 정보
var metaGrid = new Grid { Margin = new Thickness(0, 0, 0, 14) };
metaGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(80) });
metaGrid.ColumnDefinitions.Add(new ColumnDefinition());
void AddMetaRow(string label, string value, int row)
{
if (string.IsNullOrEmpty(value)) return;
metaGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
var lb = new TextBlock
{
Text = label, FontSize = 11.5, Foreground = subBrush,
Margin = new Thickness(0, 2, 0, 2),
};
Grid.SetRow(lb, row); Grid.SetColumn(lb, 0);
metaGrid.Children.Add(lb);
var vb = new TextBlock
{
Text = value, FontSize = 11.5, Foreground = fgBrush,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 2, 0, 2),
};
Grid.SetRow(vb, row); Grid.SetColumn(vb, 1);
metaGrid.Children.Add(vb);
}
var metaRow = 0;
AddMetaRow("명령어", $"/{skill.Name}", metaRow++);
AddMetaRow("라벨", skill.Label, metaRow++);
AddMetaRow("설명", skill.Description, metaRow++);
if (!string.IsNullOrEmpty(skill.Requires))
AddMetaRow("런타임", skill.Requires, metaRow++);
if (!string.IsNullOrEmpty(skill.AllowedTools))
AddMetaRow("허용 도구", skill.AllowedTools, metaRow++);
AddMetaRow("상태", skill.IsAvailable ? "✓ 사용 가능" : $"✗ {skill.UnavailableHint}", metaRow++);
AddMetaRow("경로", skill.FilePath, metaRow++);
bodyPanel.Children.Add(metaGrid);
// 구분선
bodyPanel.Children.Add(new Border
{
Height = 1,
Background = borderBr,
Margin = new Thickness(0, 4, 0, 12),
});
// 프롬프트 내용 미리보기
bodyPanel.Children.Add(new TextBlock
{
Text = "시스템 프롬프트 (미리보기)",
FontSize = 11,
FontWeight = FontWeights.SemiBold,
Foreground = subBrush,
Margin = new Thickness(0, 0, 0, 6),
});
var promptBorder = new Border
{
Background = itemBg,
CornerRadius = new CornerRadius(8),
Padding = new Thickness(12, 10, 12, 10),
};
var promptText = skill.SystemPrompt;
if (promptText.Length > 2000)
promptText = promptText[..2000] + "\n\n... (이하 생략)";
promptBorder.Child = new TextBlock
{
Text = promptText,
FontSize = 11.5,
FontFamily = ThemeResourceHelper.ConsolasCode,
Foreground = fgBrush,
TextWrapping = TextWrapping.Wrap,
Opacity = 0.85,
};
bodyPanel.Children.Add(promptBorder);
body.Content = bodyPanel;
Grid.SetRow(body, 1);
mainGrid.Children.Add(titleBar);
mainGrid.Children.Add(body);
outerBorder.Child = mainGrid;
popup.Content = outerBorder;
popup.ShowDialog();
}
}

View File

@@ -428,204 +428,4 @@ public partial class SkillGalleryWindow : Window
btn.MouseLeftButtonUp += (_, _) => action();
return btn;
}
// ─── 스킬 상세 보기 팝업 ───────────────────────────────────────────────
private void ShowSkillDetail(SkillDefinition skill)
{
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? Brushes.Black;
var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var subBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent;
var borderBr = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var popup = new Window
{
Title = $"/{skill.Name}",
Width = 580,
Height = 480,
WindowStyle = WindowStyle.None,
AllowsTransparency = true,
Background = Brushes.Transparent,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Owner = this,
};
var outerBorder = new Border
{
Background = bgBrush,
CornerRadius = new CornerRadius(12),
BorderBrush = borderBr,
BorderThickness = new Thickness(1, 1, 1, 1),
Effect = new System.Windows.Media.Effects.DropShadowEffect
{
BlurRadius = 20,
ShadowDepth = 4,
Opacity = 0.3,
Color = Colors.Black,
},
};
var mainGrid = new Grid();
mainGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(44) });
mainGrid.RowDefinitions.Add(new RowDefinition());
// ── 타이틀바 ──
var titleBar = new Border
{
CornerRadius = new CornerRadius(12, 12, 0, 0),
Background = itemBg,
};
titleBar.MouseLeftButtonDown += (_, e) =>
{
var pos = e.GetPosition(popup);
if (pos.X > popup.ActualWidth - 50) return;
popup.DragMove();
};
var titleGrid = new Grid { Margin = new Thickness(16, 0, 12, 0) };
var titleLeft = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
titleLeft.Children.Add(new TextBlock
{
Text = skill.Icon,
FontFamily = ThemeResourceHelper.SegoeMdl2,
FontSize = 14,
Foreground = new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 1, 8, 0),
});
titleLeft.Children.Add(new TextBlock
{
Text = $"/{skill.Name} {skill.Label}",
FontSize = 14,
FontWeight = FontWeights.SemiBold,
Foreground = fgBrush,
VerticalAlignment = VerticalAlignment.Center,
});
titleGrid.Children.Add(titleLeft);
var closeBtn = new Border
{
Width = 28, Height = 28,
CornerRadius = new CornerRadius(6),
Cursor = Cursors.Hand,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
};
closeBtn.Child = new TextBlock
{
Text = "\uE8BB",
FontFamily = ThemeResourceHelper.SegoeMdl2,
FontSize = 10,
Foreground = subBrush,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
closeBtn.MouseEnter += (s, _) => ((Border)s).Background =
new SolidColorBrush(Color.FromArgb(0x33, 0xFF, 0x44, 0x44));
closeBtn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent;
closeBtn.MouseLeftButtonUp += (_, _) => popup.Close();
titleGrid.Children.Add(closeBtn);
titleBar.Child = titleGrid;
Grid.SetRow(titleBar, 0);
// ── 본문: 스킬 정보 + 프롬프트 미리보기 ──
var body = new ScrollViewer
{
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
Padding = new Thickness(20, 16, 20, 16),
};
var bodyPanel = new StackPanel();
// 메타 정보
var metaGrid = new Grid { Margin = new Thickness(0, 0, 0, 14) };
metaGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(80) });
metaGrid.ColumnDefinitions.Add(new ColumnDefinition());
void AddMetaRow(string label, string value, int row)
{
if (string.IsNullOrEmpty(value)) return;
metaGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
var lb = new TextBlock
{
Text = label, FontSize = 11.5, Foreground = subBrush,
Margin = new Thickness(0, 2, 0, 2),
};
Grid.SetRow(lb, row); Grid.SetColumn(lb, 0);
metaGrid.Children.Add(lb);
var vb = new TextBlock
{
Text = value, FontSize = 11.5, Foreground = fgBrush,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 2, 0, 2),
};
Grid.SetRow(vb, row); Grid.SetColumn(vb, 1);
metaGrid.Children.Add(vb);
}
var metaRow = 0;
AddMetaRow("명령어", $"/{skill.Name}", metaRow++);
AddMetaRow("라벨", skill.Label, metaRow++);
AddMetaRow("설명", skill.Description, metaRow++);
if (!string.IsNullOrEmpty(skill.Requires))
AddMetaRow("런타임", skill.Requires, metaRow++);
if (!string.IsNullOrEmpty(skill.AllowedTools))
AddMetaRow("허용 도구", skill.AllowedTools, metaRow++);
AddMetaRow("상태", skill.IsAvailable ? "✓ 사용 가능" : $"✗ {skill.UnavailableHint}", metaRow++);
AddMetaRow("경로", skill.FilePath, metaRow++);
bodyPanel.Children.Add(metaGrid);
// 구분선
bodyPanel.Children.Add(new Border
{
Height = 1,
Background = borderBr,
Margin = new Thickness(0, 4, 0, 12),
});
// 프롬프트 내용 미리보기
bodyPanel.Children.Add(new TextBlock
{
Text = "시스템 프롬프트 (미리보기)",
FontSize = 11,
FontWeight = FontWeights.SemiBold,
Foreground = subBrush,
Margin = new Thickness(0, 0, 0, 6),
});
var promptBorder = new Border
{
Background = itemBg,
CornerRadius = new CornerRadius(8),
Padding = new Thickness(12, 10, 12, 10),
};
var promptText = skill.SystemPrompt;
if (promptText.Length > 2000)
promptText = promptText[..2000] + "\n\n... (이하 생략)";
promptBorder.Child = new TextBlock
{
Text = promptText,
FontSize = 11.5,
FontFamily = ThemeResourceHelper.ConsolasCode,
Foreground = fgBrush,
TextWrapping = TextWrapping.Wrap,
Opacity = 0.85,
};
bodyPanel.Children.Add(promptBorder);
body.Content = bodyPanel;
Grid.SetRow(body, 1);
mainGrid.Children.Add(titleBar);
mainGrid.Children.Add(body);
outerBorder.Child = mainGrid;
popup.Content = outerBorder;
popup.ShowDialog();
}
}

View File

@@ -394,274 +394,4 @@ public partial class WorkflowAnalyzerWindow
});
return sp;
}
// ─── 타임라인 노드 생성 ────────────────────────────────────────
private Border CreateTimelineNode(AgentEvent evt)
{
var (icon, iconColor, label) = GetEventVisual(evt);
var node = new Border
{
Background = Brushes.Transparent,
Margin = new Thickness(0, 1, 0, 1),
Padding = new Thickness(8, 6, 8, 6),
CornerRadius = new CornerRadius(8),
Cursor = Cursors.Hand,
Tag = evt,
};
var grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(28) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(3) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
// 아이콘 원
var iconBorder = new Border
{
Width = 22, Height = 22,
CornerRadius = new CornerRadius(11),
Background = new SolidColorBrush(iconColor),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 2, 0, 0),
};
iconBorder.Child = new TextBlock
{
Text = icon,
FontFamily = ThemeResourceHelper.SegoeMdl2,
FontSize = 10,
Foreground = Brushes.White,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
Grid.SetColumn(iconBorder, 0);
grid.Children.Add(iconBorder);
// 타임라인 세로 선
var line = new Rectangle
{
Width = 2,
Fill = TryFindResource("BorderColor") as Brush ?? Brushes.Gray,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Stretch,
Opacity = 0.4,
};
Grid.SetColumn(line, 1);
grid.Children.Add(line);
// 내용
var content = new StackPanel { Margin = new Thickness(8, 0, 0, 0) };
var headerPanel = new StackPanel { Orientation = Orientation.Horizontal };
headerPanel.Children.Add(new TextBlock
{
Text = label,
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
VerticalAlignment = VerticalAlignment.Center,
});
if (evt.ElapsedMs > 0)
{
headerPanel.Children.Add(new TextBlock
{
Text = evt.ElapsedMs >= 1000 ? $" {evt.ElapsedMs / 1000.0:F1}s" : $" {evt.ElapsedMs}ms",
FontSize = 10,
Foreground = evt.ElapsedMs > 3000
? Brushes.OrangeRed
: TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
VerticalAlignment = VerticalAlignment.Center,
});
}
headerPanel.Children.Add(new TextBlock
{
Text = $" {evt.Timestamp:HH:mm:ss}",
FontSize = 9,
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
VerticalAlignment = VerticalAlignment.Center,
Opacity = 0.6,
});
content.Children.Add(headerPanel);
if (!string.IsNullOrEmpty(evt.Summary))
{
content.Children.Add(new TextBlock
{
Text = Truncate(evt.Summary, 120),
FontSize = 11,
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 2, 0, 0),
LineHeight = 16,
});
}
if (evt.InputTokens > 0 || evt.OutputTokens > 0)
{
var tokenPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 0) };
tokenPanel.Children.Add(CreateBadge($"↑{evt.InputTokens:#,0}", "#3B82F6"));
tokenPanel.Children.Add(CreateBadge($"↓{evt.OutputTokens:#,0}", "#10B981"));
content.Children.Add(tokenPanel);
}
Grid.SetColumn(content, 2);
grid.Children.Add(content);
node.Child = grid;
var hoverBrush = TryFindResource("ItemHoverBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
node.MouseEnter += (_, _) => node.Background = hoverBrush;
node.MouseLeave += (_, _) => node.Background = Brushes.Transparent;
node.MouseLeftButtonUp += (_, _) => ShowDetail(evt);
return node;
}
private static (string Icon, Color Color, string Label) GetEventVisual(AgentEvent evt)
{
return evt.Type switch
{
AgentEventType.Thinking => ("\uE7BA", Color.FromRgb(0x60, 0xA5, 0xFA), "사고"),
AgentEventType.Planning => ("\uE9D5", Color.FromRgb(0xA7, 0x8B, 0xFA), "계획"),
AgentEventType.StepStart => ("\uE72A", Color.FromRgb(0x60, 0xA5, 0xFA), $"단계 {evt.StepCurrent}/{evt.StepTotal}"),
AgentEventType.StepDone => ("\uE73E", Color.FromRgb(0x34, 0xD3, 0x99), $"단계 완료"),
AgentEventType.ToolCall => ("\uE756", Color.FromRgb(0xFB, 0xBF, 0x24), evt.ToolName),
AgentEventType.ToolResult=> ("\uE73E", Color.FromRgb(0x34, 0xD3, 0x99), $"{evt.ToolName} ✓"),
AgentEventType.SkillCall => ("\uE768", Color.FromRgb(0xA7, 0x8B, 0xFA), $"스킬: {evt.ToolName}"),
AgentEventType.Error => ("\uE783", Color.FromRgb(0xF8, 0x71, 0x71), $"{evt.ToolName} ✗"),
AgentEventType.Complete => ("\uE930", Color.FromRgb(0x34, 0xD3, 0x99), "완료"),
AgentEventType.Decision => ("\uE8C8", Color.FromRgb(0xFB, 0xBF, 0x24), "의사결정"),
_ => ("\uE946", Color.FromRgb(0x6B, 0x72, 0x80), "이벤트"),
};
}
private static Border CreateBadge(string text, string colorHex)
{
var color = (Color)ColorConverter.ConvertFromString(colorHex);
return new Border
{
Background = new SolidColorBrush(Color.FromArgb(0x30, color.R, color.G, color.B)),
CornerRadius = new CornerRadius(4),
Padding = new Thickness(5, 1, 5, 1),
Margin = new Thickness(0, 0, 4, 0),
Child = new TextBlock
{
Text = text,
FontSize = 9,
Foreground = new SolidColorBrush(color),
FontWeight = FontWeights.SemiBold,
},
};
}
// ─── 상세 패널 ─────────────────────────────────────────────
private void ShowDetail(AgentEvent evt)
{
DetailPanel.Visibility = Visibility.Visible;
DetailPanel.Tag = evt;
var (_, color, label) = GetEventVisual(evt);
DetailTitle.Text = label;
DetailBadge.Text = evt.Success ? "성공" : "실패";
DetailBadge.Background = new SolidColorBrush(evt.Success
? Color.FromRgb(0x34, 0xD3, 0x99)
: Color.FromRgb(0xF8, 0x71, 0x71));
var meta = $"시간: {evt.Timestamp:HH:mm:ss.fff}";
if (evt.ElapsedMs > 0)
meta += $" | 소요: {(evt.ElapsedMs >= 1000 ? $"{evt.ElapsedMs / 1000.0:F1}s" : $"{evt.ElapsedMs}ms")}";
if (evt.InputTokens > 0 || evt.OutputTokens > 0)
meta += $" | 토큰: 입력 {evt.InputTokens:#,0} / 출력 {evt.OutputTokens:#,0}";
if (evt.Iteration > 0)
meta += $" | 반복 #{evt.Iteration}";
DetailMeta.Text = meta;
var contentText = evt.Summary ?? "";
if (!string.IsNullOrEmpty(evt.ToolInput))
contentText += $"\n\n파라미터:\n{Truncate(evt.ToolInput, 500)}";
if (!string.IsNullOrEmpty(evt.FilePath))
contentText += $"\n파일: {evt.FilePath}";
DetailContent.Text = contentText;
}
// ─── 요약 카드 업데이트 ──────────────────────────────────────
private void UpdateSummaryCards()
{
var elapsed = (DateTime.Now - _startTime).TotalSeconds;
CardElapsed.Text = elapsed >= 60 ? $"{elapsed / 60:F0}m {elapsed % 60:F0}s" : $"{elapsed:F1}s";
CardIterations.Text = _maxIteration.ToString();
var totalTokens = _totalInputTokens + _totalOutputTokens;
CardTokens.Text = totalTokens >= 1000 ? $"{totalTokens / 1000.0:F1}k" : totalTokens.ToString();
CardInputTokens.Text = _totalInputTokens >= 1000 ? $"{_totalInputTokens / 1000.0:F1}k" : _totalInputTokens.ToString();
CardOutputTokens.Text = _totalOutputTokens >= 1000 ? $"{_totalOutputTokens / 1000.0:F1}k" : _totalOutputTokens.ToString();
CardToolCalls.Text = _totalToolCalls.ToString();
}
// ─── 유틸리티 ────────────────────────────────────────────────
private static string FormatMs(long ms)
=> ms >= 1000 ? $"{ms / 1000.0:F1}s" : $"{ms}ms";
// ─── 윈도우 이벤트 ────────────────────────────────────────────
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// 타이틀 바 버튼 영역 클릭 시 드래그 무시 (DragMove가 마우스를 캡처하여 MouseLeftButtonUp 차단 방지)
var src = e.OriginalSource as DependencyObject;
while (src != null && src != sender)
{
if (src is Border b && b.Name is "BtnClose" or "BtnMinimize" or "BtnClear")
return;
src = VisualTreeHelper.GetParent(src);
}
if (e.ClickCount == 1) DragMove();
}
private void BtnClose_Click(object sender, MouseButtonEventArgs e) => Hide();
private void BtnMinimize_Click(object sender, MouseButtonEventArgs e) => WindowState = WindowState.Minimized;
private void BtnClear_Click(object sender, MouseButtonEventArgs e) => Reset();
private void TitleBtn_MouseEnter(object sender, MouseEventArgs e)
{
if (sender is Border b)
b.Background = TryFindResource("ItemHoverBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
}
private void TitleBtn_MouseLeave(object sender, MouseEventArgs e)
{
if (sender is Border b)
b.Background = Brushes.Transparent;
}
private static string Truncate(string text, int maxLen)
=> text.Length <= maxLen ? text : text[..maxLen] + "…";
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == WM_NCHITTEST)
{
var pt = PointFromScreen(new Point(
(short)(lParam.ToInt32() & 0xFFFF),
(short)((lParam.ToInt32() >> 16) & 0xFFFF)));
const double grip = 8;
var w = ActualWidth;
var h = ActualHeight;
if (pt.X < grip && pt.Y < grip) { handled = true; return (IntPtr)HTTOPLEFT; }
if (pt.X > w - grip && pt.Y < grip) { handled = true; return (IntPtr)HTTOPRIGHT; }
if (pt.X < grip && pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOMLEFT; }
if (pt.X > w - grip && pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOMRIGHT; }
if (pt.X < grip) { handled = true; return (IntPtr)HTLEFT; }
if (pt.X > w - grip) { handled = true; return (IntPtr)HTRIGHT; }
if (pt.Y < grip) { handled = true; return (IntPtr)HTTOP; }
if (pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOM; }
}
return IntPtr.Zero;
}
}

View File

@@ -0,0 +1,281 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using AxCopilot.Services.Agent;
namespace AxCopilot.Views;
public partial class WorkflowAnalyzerWindow
{
// ─── 타임라인 노드 생성 ────────────────────────────────────────
private Border CreateTimelineNode(AgentEvent evt)
{
var (icon, iconColor, label) = GetEventVisual(evt);
var node = new Border
{
Background = Brushes.Transparent,
Margin = new Thickness(0, 1, 0, 1),
Padding = new Thickness(8, 6, 8, 6),
CornerRadius = new CornerRadius(8),
Cursor = Cursors.Hand,
Tag = evt,
};
var grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(28) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(3) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
// 아이콘 원
var iconBorder = new Border
{
Width = 22, Height = 22,
CornerRadius = new CornerRadius(11),
Background = new SolidColorBrush(iconColor),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 2, 0, 0),
};
iconBorder.Child = new TextBlock
{
Text = icon,
FontFamily = ThemeResourceHelper.SegoeMdl2,
FontSize = 10,
Foreground = Brushes.White,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
Grid.SetColumn(iconBorder, 0);
grid.Children.Add(iconBorder);
// 타임라인 세로 선
var line = new Rectangle
{
Width = 2,
Fill = TryFindResource("BorderColor") as Brush ?? Brushes.Gray,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Stretch,
Opacity = 0.4,
};
Grid.SetColumn(line, 1);
grid.Children.Add(line);
// 내용
var content = new StackPanel { Margin = new Thickness(8, 0, 0, 0) };
var headerPanel = new StackPanel { Orientation = Orientation.Horizontal };
headerPanel.Children.Add(new TextBlock
{
Text = label,
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
VerticalAlignment = VerticalAlignment.Center,
});
if (evt.ElapsedMs > 0)
{
headerPanel.Children.Add(new TextBlock
{
Text = evt.ElapsedMs >= 1000 ? $" {evt.ElapsedMs / 1000.0:F1}s" : $" {evt.ElapsedMs}ms",
FontSize = 10,
Foreground = evt.ElapsedMs > 3000
? Brushes.OrangeRed
: TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
VerticalAlignment = VerticalAlignment.Center,
});
}
headerPanel.Children.Add(new TextBlock
{
Text = $" {evt.Timestamp:HH:mm:ss}",
FontSize = 9,
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
VerticalAlignment = VerticalAlignment.Center,
Opacity = 0.6,
});
content.Children.Add(headerPanel);
if (!string.IsNullOrEmpty(evt.Summary))
{
content.Children.Add(new TextBlock
{
Text = Truncate(evt.Summary, 120),
FontSize = 11,
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 2, 0, 0),
LineHeight = 16,
});
}
if (evt.InputTokens > 0 || evt.OutputTokens > 0)
{
var tokenPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 0) };
tokenPanel.Children.Add(CreateBadge($"↑{evt.InputTokens:#,0}", "#3B82F6"));
tokenPanel.Children.Add(CreateBadge($"↓{evt.OutputTokens:#,0}", "#10B981"));
content.Children.Add(tokenPanel);
}
Grid.SetColumn(content, 2);
grid.Children.Add(content);
node.Child = grid;
var hoverBrush = TryFindResource("ItemHoverBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
node.MouseEnter += (_, _) => node.Background = hoverBrush;
node.MouseLeave += (_, _) => node.Background = Brushes.Transparent;
node.MouseLeftButtonUp += (_, _) => ShowDetail(evt);
return node;
}
private static (string Icon, Color Color, string Label) GetEventVisual(AgentEvent evt)
{
return evt.Type switch
{
AgentEventType.Thinking => ("\uE7BA", Color.FromRgb(0x60, 0xA5, 0xFA), "사고"),
AgentEventType.Planning => ("\uE9D5", Color.FromRgb(0xA7, 0x8B, 0xFA), "계획"),
AgentEventType.StepStart => ("\uE72A", Color.FromRgb(0x60, 0xA5, 0xFA), $"단계 {evt.StepCurrent}/{evt.StepTotal}"),
AgentEventType.StepDone => ("\uE73E", Color.FromRgb(0x34, 0xD3, 0x99), $"단계 완료"),
AgentEventType.ToolCall => ("\uE756", Color.FromRgb(0xFB, 0xBF, 0x24), evt.ToolName),
AgentEventType.ToolResult=> ("\uE73E", Color.FromRgb(0x34, 0xD3, 0x99), $"{evt.ToolName} ✓"),
AgentEventType.SkillCall => ("\uE768", Color.FromRgb(0xA7, 0x8B, 0xFA), $"스킬: {evt.ToolName}"),
AgentEventType.Error => ("\uE783", Color.FromRgb(0xF8, 0x71, 0x71), $"{evt.ToolName} ✗"),
AgentEventType.Complete => ("\uE930", Color.FromRgb(0x34, 0xD3, 0x99), "완료"),
AgentEventType.Decision => ("\uE8C8", Color.FromRgb(0xFB, 0xBF, 0x24), "의사결정"),
_ => ("\uE946", Color.FromRgb(0x6B, 0x72, 0x80), "이벤트"),
};
}
private static Border CreateBadge(string text, string colorHex)
{
var color = (Color)ColorConverter.ConvertFromString(colorHex);
return new Border
{
Background = new SolidColorBrush(Color.FromArgb(0x30, color.R, color.G, color.B)),
CornerRadius = new CornerRadius(4),
Padding = new Thickness(5, 1, 5, 1),
Margin = new Thickness(0, 0, 4, 0),
Child = new TextBlock
{
Text = text,
FontSize = 9,
Foreground = new SolidColorBrush(color),
FontWeight = FontWeights.SemiBold,
},
};
}
// ─── 상세 패널 ─────────────────────────────────────────────
private void ShowDetail(AgentEvent evt)
{
DetailPanel.Visibility = Visibility.Visible;
DetailPanel.Tag = evt;
var (_, color, label) = GetEventVisual(evt);
DetailTitle.Text = label;
DetailBadge.Text = evt.Success ? "성공" : "실패";
DetailBadge.Background = new SolidColorBrush(evt.Success
? Color.FromRgb(0x34, 0xD3, 0x99)
: Color.FromRgb(0xF8, 0x71, 0x71));
var meta = $"시간: {evt.Timestamp:HH:mm:ss.fff}";
if (evt.ElapsedMs > 0)
meta += $" | 소요: {(evt.ElapsedMs >= 1000 ? $"{evt.ElapsedMs / 1000.0:F1}s" : $"{evt.ElapsedMs}ms")}";
if (evt.InputTokens > 0 || evt.OutputTokens > 0)
meta += $" | 토큰: 입력 {evt.InputTokens:#,0} / 출력 {evt.OutputTokens:#,0}";
if (evt.Iteration > 0)
meta += $" | 반복 #{evt.Iteration}";
DetailMeta.Text = meta;
var contentText = evt.Summary ?? "";
if (!string.IsNullOrEmpty(evt.ToolInput))
contentText += $"\n\n파라미터:\n{Truncate(evt.ToolInput, 500)}";
if (!string.IsNullOrEmpty(evt.FilePath))
contentText += $"\n파일: {evt.FilePath}";
DetailContent.Text = contentText;
}
// ─── 요약 카드 업데이트 ──────────────────────────────────────
private void UpdateSummaryCards()
{
var elapsed = (DateTime.Now - _startTime).TotalSeconds;
CardElapsed.Text = elapsed >= 60 ? $"{elapsed / 60:F0}m {elapsed % 60:F0}s" : $"{elapsed:F1}s";
CardIterations.Text = _maxIteration.ToString();
var totalTokens = _totalInputTokens + _totalOutputTokens;
CardTokens.Text = totalTokens >= 1000 ? $"{totalTokens / 1000.0:F1}k" : totalTokens.ToString();
CardInputTokens.Text = _totalInputTokens >= 1000 ? $"{_totalInputTokens / 1000.0:F1}k" : _totalInputTokens.ToString();
CardOutputTokens.Text = _totalOutputTokens >= 1000 ? $"{_totalOutputTokens / 1000.0:F1}k" : _totalOutputTokens.ToString();
CardToolCalls.Text = _totalToolCalls.ToString();
}
// ─── 유틸리티 ────────────────────────────────────────────────
private static string FormatMs(long ms)
=> ms >= 1000 ? $"{ms / 1000.0:F1}s" : $"{ms}ms";
private static string Truncate(string text, int maxLen)
=> text.Length <= maxLen ? text : text[..maxLen] + "…";
// ─── 윈도우 이벤트 ────────────────────────────────────────────
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// 타이틀 바 버튼 영역 클릭 시 드래그 무시 (DragMove가 마우스를 캡처하여 MouseLeftButtonUp 차단 방지)
var src = e.OriginalSource as DependencyObject;
while (src != null && src != sender)
{
if (src is Border b && b.Name is "BtnClose" or "BtnMinimize" or "BtnClear")
return;
src = VisualTreeHelper.GetParent(src);
}
if (e.ClickCount == 1) DragMove();
}
private void BtnClose_Click(object sender, MouseButtonEventArgs e) => Hide();
private void BtnMinimize_Click(object sender, MouseButtonEventArgs e) => WindowState = WindowState.Minimized;
private void BtnClear_Click(object sender, MouseButtonEventArgs e) => Reset();
private void TitleBtn_MouseEnter(object sender, MouseEventArgs e)
{
if (sender is Border b)
b.Background = TryFindResource("ItemHoverBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
}
private void TitleBtn_MouseLeave(object sender, MouseEventArgs e)
{
if (sender is Border b)
b.Background = Brushes.Transparent;
}
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == WM_NCHITTEST)
{
var pt = PointFromScreen(new Point(
(short)(lParam.ToInt32() & 0xFFFF),
(short)((lParam.ToInt32() >> 16) & 0xFFFF)));
const double grip = 8;
var w = ActualWidth;
var h = ActualHeight;
if (pt.X < grip && pt.Y < grip) { handled = true; return (IntPtr)HTTOPLEFT; }
if (pt.X > w - grip && pt.Y < grip) { handled = true; return (IntPtr)HTTOPRIGHT; }
if (pt.X < grip && pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOMLEFT; }
if (pt.X > w - grip && pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOMRIGHT; }
if (pt.X < grip) { handled = true; return (IntPtr)HTLEFT; }
if (pt.X > w - grip) { handled = true; return (IntPtr)HTRIGHT; }
if (pt.Y < grip) { handled = true; return (IntPtr)HTTOP; }
if (pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOM; }
}
return IntPtr.Zero;
}
}