모델 프로파일 기반 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

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
@@ -309,6 +310,10 @@ public partial class ChatWindow
private static string BuildReadableProcessFeedSummary(AgentEvent evt, string transcriptBadgeLabel, string itemDisplayName)
{
var phaseLabel = ResolveProgressPhaseLabel(evt);
if (!string.IsNullOrWhiteSpace(phaseLabel))
return phaseLabel;
return evt.Type switch
{
AgentEventType.Thinking when string.Equals(evt.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase)
@@ -412,6 +417,10 @@ public partial class ChatWindow
{
var parts = new List<string>();
var phaseMeta = ResolveProgressPhaseMeta(evt);
if (!string.IsNullOrWhiteSpace(phaseMeta))
parts.Add(phaseMeta);
var normalizedElapsedMs = NormalizeProgressElapsedMs(evt.ElapsedMs);
if (normalizedElapsedMs > 0)
{
@@ -431,6 +440,61 @@ public partial class ChatWindow
return string.Join(" · ", parts);
}
private static string? ResolveProgressPhaseLabel(AgentEvent evt)
{
var summary = (evt.Summary ?? string.Empty).Trim();
var toolName = (evt.ToolName ?? string.Empty).Trim();
if (string.Equals(toolName, "context_compaction", StringComparison.OrdinalIgnoreCase))
return "컨텍스트 압축 중...";
if (string.Equals(toolName, "agent_wait", StringComparison.OrdinalIgnoreCase))
return "처리 중...";
if (summary.Contains("html_create", StringComparison.OrdinalIgnoreCase)
|| summary.Contains("document_assemble", StringComparison.OrdinalIgnoreCase)
|| summary.Contains("docx_create", StringComparison.OrdinalIgnoreCase))
return "문서 결과 생성 중...";
if (summary.Contains("검증", StringComparison.OrdinalIgnoreCase)
|| summary.Contains("verification", StringComparison.OrdinalIgnoreCase))
return "결과 검증 중...";
if (summary.Contains("diff", StringComparison.OrdinalIgnoreCase))
return "변경 내용 확인 중...";
if (evt.Type == AgentEventType.ToolCall && !string.IsNullOrWhiteSpace(toolName))
{
return toolName switch
{
"file_read" or "directory_list" or "glob" or "grep" or "folder_map" or "multi_read" => "파일 탐색 중...",
"file_edit" or "file_write" or "html_create" or "docx_create" or "markdown_create" => "산출물 생성 중...",
"build_run" or "test_loop" => "실행 결과 확인 중...",
_ => null,
};
}
return null;
}
private static string? ResolveProgressPhaseMeta(AgentEvent evt)
{
var summary = evt.Summary ?? string.Empty;
var toolName = evt.ToolName ?? string.Empty;
if (evt.Type == AgentEventType.Planning)
return "계획";
if (evt.Type == AgentEventType.StepStart || evt.Type == AgentEventType.StepDone)
return "단계";
if (string.Equals(toolName, "context_compaction", StringComparison.OrdinalIgnoreCase))
return "압축";
if (summary.Contains("검증", StringComparison.OrdinalIgnoreCase) || summary.Contains("verification", StringComparison.OrdinalIgnoreCase))
return "검증";
if (summary.Contains("fallback", StringComparison.OrdinalIgnoreCase) || summary.Contains("자동 생성", StringComparison.OrdinalIgnoreCase))
return "폴백";
if (summary.Contains("재시도", StringComparison.OrdinalIgnoreCase) || summary.Contains("retry", StringComparison.OrdinalIgnoreCase))
return "재시도";
if (evt.Type == AgentEventType.ToolCall)
return "도구";
return null;
}
private Border CreateReadableProgressFeedCard(
string summary,
Brush primaryText,
@@ -909,6 +973,161 @@ public partial class ChatWindow
}
}
// ─── 에이전트 실행 통합 진행 카드 ─────────────────────────────────────────
private void AddLiveRunProgressCard(IReadOnlyList<AgentEvent> steps)
{
if (steps.Count == 0) return;
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray;
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
var accentColor = (accentBrush as SolidColorBrush)?.Color ?? Color.FromRgb(0x59, 0xA5, 0xF5);
var cardBg = new SolidColorBrush(Color.FromArgb(0x0D, accentColor.R, accentColor.G, accentColor.B));
var cardBorder = new SolidColorBrush(Color.FromArgb(0x2A, accentColor.R, accentColor.G, accentColor.B));
var doneBulletColor = new SolidColorBrush(Color.FromRgb(0x22, 0xC5, 0x5E));
var outerStack = new StackPanel();
// 헤더 행: 펄스 점 + "작업 진행 중"
var headerRow = new StackPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(14, 8, 14, 6),
};
var headerDot = new Border
{
Width = 8,
Height = 8,
CornerRadius = new CornerRadius(999),
Background = accentBrush,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
};
ApplyLiveWaitingPulseToMarker(headerDot);
headerRow.Children.Add(headerDot);
headerRow.Children.Add(new TextBlock
{
Text = "작업 진행 중",
FontSize = 11.5,
FontWeight = FontWeights.SemiBold,
Foreground = accentBrush,
VerticalAlignment = VerticalAlignment.Center,
});
outerStack.Children.Add(headerRow);
// 구분선
outerStack.Children.Add(new Border
{
Height = 1,
Background = cardBorder,
Margin = new Thickness(14, 0, 14, 6),
});
// 스텝 목록
var stepsPanel = new StackPanel
{
Margin = new Thickness(14, 0, 14, 8),
};
for (var i = 0; i < steps.Count; i++)
{
var step = steps[i];
var isLast = i == steps.Count - 1;
var itemGrid = new Grid { Margin = new Thickness(0, 2, 0, 2) };
itemGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(18) });
itemGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
itemGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
// 불릿: 완료는 ✓, 진행 중은 펄스 점
if (isLast)
{
var liveDot = new Border
{
Width = 7,
Height = 7,
CornerRadius = new CornerRadius(999),
Background = accentBrush,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 0, 0),
};
ApplyLiveWaitingPulseToMarker(liveDot);
itemGrid.Children.Add(liveDot);
}
else
{
itemGrid.Children.Add(new TextBlock
{
Text = "✓",
FontSize = 9,
Foreground = doneBulletColor,
VerticalAlignment = VerticalAlignment.Center,
});
}
// 스텝 레이블
var transcriptLabel = GetTranscriptBadgeLabel(step);
var itemDisplayName = GetAgentItemDisplayName(step.ToolName);
var label = BuildReadableProcessFeedSummary(step, transcriptLabel, itemDisplayName);
if (string.IsNullOrWhiteSpace(label)) label = transcriptLabel;
var labelBlock = new TextBlock
{
Text = label,
FontSize = isLast ? 12 : 11.5,
FontWeight = isLast ? FontWeights.SemiBold : FontWeights.Normal,
Foreground = isLast ? primaryText : secondaryText,
VerticalAlignment = VerticalAlignment.Center,
TextTrimming = TextTrimming.CharacterEllipsis,
Margin = new Thickness(4, 0, 0, 0),
};
Grid.SetColumn(labelBlock, 1);
itemGrid.Children.Add(labelBlock);
// 경과 시간 메타 (완료된 스텝만)
if (!isLast)
{
var meta = BuildReadableProgressMetaText(step);
if (!string.IsNullOrWhiteSpace(meta))
{
var metaBlock = new TextBlock
{
Text = meta,
FontSize = 10,
Foreground = secondaryText,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(8, 0, 0, 0),
Opacity = 0.8,
};
Grid.SetColumn(metaBlock, 2);
itemGrid.Children.Add(metaBlock);
}
}
stepsPanel.Children.Add(itemGrid);
}
outerStack.Children.Add(stepsPanel);
var card = new Border
{
Background = cardBg,
BorderBrush = cardBorder,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(12),
Margin = new Thickness(12, 4, 12, 4),
HorizontalAlignment = HorizontalAlignment.Stretch,
Child = outerStack,
};
MessagePanel.Children.Add(card);
}
private void AddAgentEventBanner(AgentEvent evt)
{
var logLevel = _settings.Settings.Llm.AgentLogLevel;

View File

@@ -81,7 +81,8 @@ public partial class ChatWindow
if (string.IsNullOrWhiteSpace(text))
return;
if (_isStreaming && string.Equals(priority, "now", StringComparison.OrdinalIgnoreCase))
// 현재 탭이 스트리밍 중일 때만 우선순위를 "next"로 낮춤 — 다른 탭 스트리밍은 무관
if (_streamingTabs.Contains(_activeTab) && string.Equals(priority, "now", StringComparison.OrdinalIgnoreCase))
priority = "next";
HideSlashChip(restoreText: false);
@@ -97,19 +98,30 @@ public partial class ChatWindow
if (queuedItem == null)
return;
if (!_isStreaming && startImmediatelyWhenIdle)
if (!_streamingTabs.Contains(_activeTab) && startImmediatelyWhenIdle)
{
StartNextQueuedDraftIfAny(queuedItem.Id);
return;
}
var runningTab = _streamRunTab;
var runningLabel = runningTab switch
{
"Cowork" => "코워크",
"Code" => "코드",
"Chat" => "채팅",
_ => runningTab ?? "다른 탭",
};
var suffix = !string.IsNullOrEmpty(runningTab) && !string.Equals(runningTab, _activeTab, StringComparison.OrdinalIgnoreCase)
? $" ({runningLabel} 실행 완료 후 자동 실행)"
: " (실행 완료 후 자동 실행)";
var toast = queuedItem.Kind switch
{
"command" => "명령이 대기열에 추가되었습니다.",
"direct" => "직접 실행 요청이 대기열에 추가되었습니다.",
"steering" => "조정 요청이 대기열에 추가되었습니다.",
"followup" => "후속 작업이 대기열에 추가되었습니다.",
_ => "메시지가 대기열에 추가되었습니다.",
"command" => "명령이 대기열에 추가되었습니다." + suffix,
"direct" => "직접 실행 요청이 대기열에 추가되었습니다." + suffix,
"steering" => "조정 요청이 대기열에 추가되었습니다." + suffix,
"followup" => "후속 작업이 대기열에 추가되었습니다." + suffix,
_ => "메시지가 대기열에 추가되었습니다." + suffix,
};
ShowToast(toast);
}
@@ -135,16 +147,7 @@ public partial class ChatWindow
RebuildDraftQueuePanel(items);
}
private bool IsDraftQueueExpanded()
=> _expandedDraftQueueTabs.Contains(_activeTab);
private void ToggleDraftQueueExpanded()
{
if (!_expandedDraftQueueTabs.Add(_activeTab))
_expandedDraftQueueTabs.Remove(_activeTab);
RefreshDraftQueueUi();
}
// --- Queue panel (Codex style) ---
private void RebuildDraftQueuePanel(IReadOnlyList<DraftQueueItem> items)
{
@@ -154,6 +157,8 @@ public partial class ChatWindow
DraftQueuePanel.Children.Clear();
var visibleItems = items
.Where(x => !string.Equals(x.State, "completed", StringComparison.OrdinalIgnoreCase) // 완료 항목 제거
&& !string.Equals(x.State, "running", StringComparison.OrdinalIgnoreCase)) // 수행 중인 항목은 채팅창에서 이미 표시됨
.OrderBy(GetDraftStateRank)
.ThenBy(GetDraftPriorityRank)
.ThenBy(x => x.CreatedAt)
@@ -165,206 +170,142 @@ public partial class ChatWindow
return;
}
var summary = _appState.GetDraftQueueSummary(_activeTab);
var shouldShowQueue =
IsDraftQueueExpanded()
|| summary.RunningCount > 0
|| summary.QueuedCount > 0
|| summary.FailedCount > 0;
if (!shouldShowQueue)
{
DraftQueuePanel.Visibility = Visibility.Collapsed;
return;
}
DraftQueuePanel.Visibility = Visibility.Visible;
DraftQueuePanel.Children.Add(CreateDraftQueueSummaryStrip(summary, IsDraftQueueExpanded()));
if (!IsDraftQueueExpanded())
// 단일 통합 컨테이너
var containerBorder = new Border
{
DraftQueuePanel.Children.Add(CreateCompactDraftQueuePanel(visibleItems, summary));
return;
}
const int maxPerSection = 3;
var runningItems = visibleItems
.Where(item => string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase))
.Take(maxPerSection)
.ToList();
var queuedItems = visibleItems
.Where(item => string.Equals(item.State, "queued", StringComparison.OrdinalIgnoreCase) && !IsDraftBlocked(item))
.Take(maxPerSection)
.ToList();
var blockedItems = visibleItems
.Where(IsDraftBlocked)
.Take(maxPerSection)
.ToList();
var completedItems = visibleItems
.Where(item => string.Equals(item.State, "completed", StringComparison.OrdinalIgnoreCase))
.Take(maxPerSection)
.ToList();
var failedItems = visibleItems
.Where(item => string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase))
.Take(maxPerSection)
.ToList();
AddDraftQueueSection("실행 중", runningItems, summary.RunningCount);
AddDraftQueueSection("다음 작업", queuedItems, summary.QueuedCount);
AddDraftQueueSection("보류", blockedItems, summary.BlockedCount);
AddDraftQueueSection("완료", completedItems, summary.CompletedCount);
AddDraftQueueSection("실패", failedItems, summary.FailedCount);
if (summary.CompletedCount > 0 || summary.FailedCount > 0)
{
var footer = new StackPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(0, 2, 0, 0),
};
if (summary.CompletedCount > 0)
footer.Children.Add(CreateDraftQueueActionButton($"완료 정리 {summary.CompletedCount}", ClearCompletedDrafts, BrushFromHex("#ECFDF5")));
if (summary.FailedCount > 0)
footer.Children.Add(CreateDraftQueueActionButton($"실패 정리 {summary.FailedCount}", ClearFailedDrafts, BrushFromHex("#FEF2F2")));
DraftQueuePanel.Children.Add(footer);
}
}
private UIElement CreateCompactDraftQueuePanel(IReadOnlyList<DraftQueueItem> items, AppStateService.DraftQueueSummaryState summary)
{
var primaryText = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#111827");
var secondaryText = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#6B7280");
var background = TryFindResource("ItemBackground") as Brush ?? BrushFromHex("#F7F7F8");
var borderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E4E4E7");
var focusItem = items.FirstOrDefault(item => string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase))
?? items.FirstOrDefault(item => string.Equals(item.State, "queued", StringComparison.OrdinalIgnoreCase) && !IsDraftBlocked(item))
?? items.FirstOrDefault(IsDraftBlocked)
?? items.FirstOrDefault(item => string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase))
?? items.FirstOrDefault(item => string.Equals(item.State, "completed", StringComparison.OrdinalIgnoreCase));
var container = new Border
{
Background = background,
BorderBrush = borderBrush,
Background = TryFindResource("ItemBackground") as Brush ?? BrushFromHex("#1E1E2A"),
BorderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#3A3A4A"),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(14),
Padding = new Thickness(12, 10, 12, 10),
CornerRadius = new CornerRadius(10),
Margin = new Thickness(0, 0, 0, 4),
};
var root = new Grid();
root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
root.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
container.Child = root;
var innerStack = new StackPanel();
containerBorder.Child = innerStack;
var left = new StackPanel();
left.Children.Add(new TextBlock
for (int i = 0; i < visibleItems.Count; i++)
{
Text = focusItem == null
? "대기열 항목이 준비되면 여기에서 요약됩니다."
: $"{GetDraftStateLabel(focusItem)} · {GetDraftKindLabel(focusItem)}",
FontSize = 11,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
});
left.Children.Add(new TextBlock
{
Text = focusItem?.Text ?? BuildDraftQueueCompactSummaryText(summary),
FontSize = 10.5,
Foreground = secondaryText,
TextTrimming = TextTrimming.CharacterEllipsis,
Margin = new Thickness(0, 4, 0, 0),
MaxWidth = 520,
});
Grid.SetColumn(left, 0);
root.Children.Add(left);
innerStack.Children.Add(CreateDraftQueueRow(visibleItems[i]));
var action = CreateDraftQueueActionButton("상세", ToggleDraftQueueExpanded);
action.Margin = new Thickness(12, 0, 0, 0);
Grid.SetColumn(action, 1);
root.Children.Add(action);
return container;
}
private static string BuildDraftQueueCompactSummaryText(AppStateService.DraftQueueSummaryState summary)
{
var parts = new List<string>();
if (summary.RunningCount > 0) parts.Add($"실행 {summary.RunningCount}");
if (summary.QueuedCount > 0) parts.Add($"다음 {summary.QueuedCount}");
if (summary.FailedCount > 0) parts.Add($"실패 {summary.FailedCount}");
return parts.Count == 0 ? "대기열 0" : string.Join(" · ", parts);
}
private void AddDraftQueueSection(string label, IReadOnlyList<DraftQueueItem> items, int totalCount)
{
if (DraftQueuePanel == null || totalCount <= 0)
return;
DraftQueuePanel.Children.Add(CreateDraftQueueSectionLabel($"{label} · {totalCount}"));
foreach (var item in items)
DraftQueuePanel.Children.Add(CreateDraftQueueCard(item));
if (totalCount > items.Count)
{
DraftQueuePanel.Children.Add(new TextBlock
// 구분선 (마지막 항목 제외)
if (i < visibleItems.Count - 1)
{
Text = $"추가 항목 {totalCount - items.Count}개",
Margin = new Thickness(8, -2, 0, 8),
FontSize = 10.5,
Foreground = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#7A7F87"),
});
innerStack.Children.Add(new Border
{
Height = 1,
Background = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#2E2E3E"),
Margin = new Thickness(10, 0, 10, 0),
});
}
}
DraftQueuePanel.Children.Add(containerBorder);
// 실패 항목 정리 버튼 (실패만 — 완료는 자동 제거됨)
var summary = _appState.GetDraftQueueSummary(_activeTab);
if (summary.FailedCount > 0)
{
var failBtn = CreateQueueFooterButton($"실패 정리 ({summary.FailedCount})", ClearFailedDrafts);
DraftQueuePanel.Children.Add(failBtn);
}
}
private UIElement CreateDraftQueueSummaryStrip(AppStateService.DraftQueueSummaryState summary, bool isExpanded)
// --- Codex-style row ---
private Border CreateDraftQueueRow(DraftQueueItem item)
{
var root = new Grid
var isRunning = string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase);
var isFailed = string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase);
var primaryText = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#E5E5EA");
var secondaryText = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#8E8E9A");
var row = new Border
{
Margin = new Thickness(0, 0, 0, 8),
Padding = new Thickness(10, 7, 8, 7),
};
root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
root.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var wrap = new WrapPanel();
var grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // state icon
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); // text
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // actions
row.Child = grid;
if (summary.RunningCount > 0)
wrap.Children.Add(CreateQueueSummaryPill("실행 중", summary.RunningCount.ToString(), "#EFF6FF", "#BFDBFE", "#1D4ED8"));
if (summary.QueuedCount > 0)
wrap.Children.Add(CreateQueueSummaryPill("다음", summary.QueuedCount.ToString(), "#F5F3FF", "#DDD6FE", "#6D28D9"));
if (isExpanded && summary.BlockedCount > 0)
wrap.Children.Add(CreateQueueSummaryPill("보류", summary.BlockedCount.ToString(), "#FFF7ED", "#FDBA74", "#C2410C"));
if (isExpanded && summary.CompletedCount > 0)
wrap.Children.Add(CreateQueueSummaryPill("완료", summary.CompletedCount.ToString(), "#ECFDF5", "#BBF7D0", "#166534"));
if (summary.FailedCount > 0)
wrap.Children.Add(CreateQueueSummaryPill("실패", summary.FailedCount.ToString(), "#FEF2F2", "#FECACA", "#991B1B"));
// State icon
var stateIcon = new TextBlock
{
Text = GetDraftStateIcon(item),
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 11,
Foreground = GetDraftStateIconBrush(item),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
Width = 14,
};
Grid.SetColumn(stateIcon, 0);
grid.Children.Add(stateIcon);
if (wrap.Children.Count == 0)
wrap.Children.Add(CreateQueueSummaryPill("대기열", "0", "#F8FAFC", "#E2E8F0", "#475569"));
// Message text
var msgText = new TextBlock
{
Text = item.Text,
FontSize = 12,
Foreground = isFailed ? (TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#8E8E9A")) : primaryText,
TextTrimming = TextTrimming.CharacterEllipsis,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
};
Grid.SetColumn(msgText, 1);
grid.Children.Add(msgText);
Grid.SetColumn(wrap, 0);
root.Children.Add(wrap);
// Right actions
var actions = new StackPanel
{
Orientation = Orientation.Horizontal,
VerticalAlignment = VerticalAlignment.Center,
};
Grid.SetColumn(actions, 2);
grid.Children.Add(actions);
var toggle = CreateDraftQueueActionButton(isExpanded ? "간단히" : "상세 보기", ToggleDraftQueueExpanded);
toggle.Margin = new Thickness(10, 0, 0, 0);
Grid.SetColumn(toggle, 1);
root.Children.Add(toggle);
if (!isRunning)
{
// Kind chip (↪ 조정 style)
actions.Children.Add(CreateKindChip(item, secondaryText));
}
return root;
if (!isRunning && !isFailed)
{
// Run now button
actions.Children.Add(CreateRowIconButton("\uE768", "지금 실행", () => QueueDraftForImmediateRun(item.Id)));
}
if (!isRunning)
{
// Edit button
actions.Children.Add(CreateRowIconButton("\uE70F", "편집", () => PopDraftToEditor(item.Id)));
}
// Delete
actions.Children.Add(CreateRowIconButton("\uE74D", isRunning ? "취소" : "삭제", () => RemoveDraftFromQueue(item.Id)));
return row;
}
private Border CreateQueueSummaryPill(string label, string value, string bgHex, string borderHex, string fgHex)
// Kind chip on the right — "↪ 조정" style
private Border CreateKindChip(DraftQueueItem item, Brush defaultForeground)
{
var (kindIcon, kindLabel) = GetDraftKindChipContent(item);
var foreground = GetDraftKindChipColor(item);
return new Border
{
Background = BrushFromHex(bgHex),
BorderBrush = BrushFromHex(borderHex),
BorderBrush = foreground,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(999),
Padding = new Thickness(8, 3, 8, 3),
Margin = new Thickness(0, 0, 6, 0),
CornerRadius = new CornerRadius(5),
Padding = new Thickness(5, 2, 6, 2),
Margin = new Thickness(0, 0, 4, 0),
VerticalAlignment = VerticalAlignment.Center,
Child = new StackPanel
{
Orientation = Orientation.Horizontal,
@@ -372,169 +313,108 @@ public partial class ChatWindow
{
new TextBlock
{
Text = label,
Text = kindIcon,
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 10,
Foreground = BrushFromHex(fgHex),
Foreground = foreground,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 3, 0),
},
new TextBlock
{
Text = $" {value}",
FontSize = 10,
FontWeight = FontWeights.SemiBold,
Foreground = BrushFromHex(fgHex),
}
Text = kindLabel,
FontSize = 10.5,
Foreground = foreground,
VerticalAlignment = VerticalAlignment.Center,
},
}
}
};
}
private TextBlock CreateDraftQueueSectionLabel(string text)
// Row icon button — flat, no border
private Button CreateRowIconButton(string icon, string tooltip, Action onClick)
{
return new TextBlock
var btn = new Button
{
Text = text,
FontSize = 10.5,
FontWeight = FontWeights.SemiBold,
Margin = new Thickness(8, 0, 8, 6),
Foreground = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#64748B"),
};
}
private Border CreateDraftQueueCard(DraftQueueItem item)
{
var background = TryFindResource("ItemBackground") as Brush ?? BrushFromHex("#F7F7F8");
var borderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E4E4E7");
var primaryText = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#111827");
var secondaryText = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#6B7280");
var neutralSurface = BrushFromHex("#F5F6F8");
var (kindIcon, kindForeground) = GetDraftKindVisual(item);
var (stateBackground, stateBorder, stateForeground) = GetDraftStateBadgeColors(item);
var (priorityBackground, priorityBorder, priorityForeground) = GetDraftPriorityBadgeColors(item.Priority);
var container = new Border
{
Background = background,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(14),
Padding = new Thickness(12, 10, 12, 10),
Margin = new Thickness(0, 0, 0, 8),
};
var root = new Grid();
root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
root.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
container.Child = root;
var left = new StackPanel();
Grid.SetColumn(left, 0);
root.Children.Add(left);
var header = new StackPanel
{
Orientation = Orientation.Horizontal,
};
header.Children.Add(new TextBlock
{
Text = kindIcon,
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 11,
Foreground = kindForeground,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 6, 0),
});
header.Children.Add(CreateDraftQueueBadge(GetDraftKindLabel(item), BrushFromHex("#F8FAFC"), borderBrush, kindForeground));
header.Children.Add(CreateDraftQueueBadge(GetDraftStateLabel(item), stateBackground, stateBorder, stateForeground));
header.Children.Add(CreateDraftQueueBadge(GetDraftPriorityLabel(item.Priority), priorityBackground, priorityBorder, priorityForeground));
left.Children.Add(header);
left.Children.Add(new TextBlock
{
Text = item.Text,
FontSize = 12.5,
Foreground = primaryText,
Margin = new Thickness(0, 6, 0, 0),
TextWrapping = TextWrapping.Wrap,
TextTrimming = TextTrimming.CharacterEllipsis,
MaxWidth = 520,
});
var meta = $"{item.CreatedAt:HH:mm}";
if (item.AttemptCount > 0)
meta += $" · 시도 {item.AttemptCount}";
if (item.NextRetryAt.HasValue && item.NextRetryAt.Value > DateTime.Now)
meta += $" · 재시도 {item.NextRetryAt.Value:HH:mm:ss}";
if (!string.IsNullOrWhiteSpace(item.LastError))
meta += $" · {TruncateForStatus(item.LastError, 36)}";
left.Children.Add(new TextBlock
{
Text = meta,
FontSize = 10.5,
Foreground = secondaryText,
Margin = new Thickness(0, 6, 0, 0),
});
var actions = new StackPanel
{
Orientation = Orientation.Horizontal,
VerticalAlignment = VerticalAlignment.Top,
};
Grid.SetColumn(actions, 1);
root.Children.Add(actions);
if (!string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase))
actions.Children.Add(CreateDraftQueueActionButton("실행", () => QueueDraftForImmediateRun(item.Id)));
if (string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase) ||
string.Equals(item.State, "completed", StringComparison.OrdinalIgnoreCase))
{
actions.Children.Add(CreateDraftQueueActionButton("대기", () => ResetDraftInQueue(item.Id), neutralSurface));
}
actions.Children.Add(CreateDraftQueueActionButton("삭제", () => RemoveDraftFromQueue(item.Id), neutralSurface));
return container;
}
private Border CreateDraftQueueBadge(string text, Brush background, Brush borderBrush, Brush foreground)
{
return new Border
{
Background = background,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(999),
Padding = new Thickness(7, 2, 7, 2),
Margin = new Thickness(0, 0, 6, 0),
Child = new TextBlock
Content = new TextBlock
{
Text = text,
FontSize = 10,
FontWeight = FontWeights.SemiBold,
Foreground = foreground,
}
Text = icon,
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 11,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center,
},
Width = 24,
Height = 24,
Padding = new Thickness(0),
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Foreground = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#8E8E9A"),
Cursor = Cursors.Hand,
ToolTip = tooltip,
};
btn.Click += (_, _) => onClick();
return btn;
}
private Button CreateDraftQueueActionButton(string label, Action onClick, Brush? background = null)
// Footer button (failed clear)
private Button CreateQueueFooterButton(string label, Action onClick)
{
var btn = new Button
{
Content = label,
Margin = new Thickness(6, 0, 0, 0),
Padding = new Thickness(10, 5, 10, 5),
MinWidth = 48,
Margin = new Thickness(0, 2, 0, 0),
Padding = new Thickness(10, 4, 10, 4),
FontSize = 11,
Background = background ?? BrushFromHex("#EEF2FF"),
BorderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#D4D4D8"),
Background = Brushes.Transparent,
BorderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#3A3A4A"),
BorderThickness = new Thickness(1),
Foreground = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#111827"),
Foreground = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#8E8E9A"),
HorizontalAlignment = HorizontalAlignment.Left,
Cursor = Cursors.Hand,
};
btn.Click += (_, _) => onClick();
return btn;
}
// Pop queued draft back into the InputBox for editing
private void PopDraftToEditor(string draftId)
{
string? text = null;
lock (_convLock)
{
var session = ChatSession;
if (session != null)
{
var item = session.GetDraftQueueItems(_activeTab)
.FirstOrDefault(x => string.Equals(x.Id, draftId, StringComparison.OrdinalIgnoreCase));
if (item != null)
{
text = item.Text;
if (session.RemoveDraft(_activeTab, draftId, _storage))
_currentConversation = session.CurrentConversation ?? _currentConversation;
}
}
}
if (InputBox != null && text != null)
{
InputBox.Text = text;
InputBox.CaretIndex = text.Length;
InputBox.Focus();
UpdateInputBoxHeight();
}
RefreshDraftQueueUi();
}
// --- Icon-only button (kept for compatibility) ---
private Button CreateIconButton(string icon, string tooltip, Action onClick)
=> CreateRowIconButton(icon, tooltip, onClick);
// --- Ranking helpers ---
private static int GetDraftStateRank(DraftQueueItem item)
=> string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase) ? 0
: IsDraftBlocked(item) ? 1
@@ -550,52 +430,74 @@ public partial class ChatWindow
_ => 2,
};
private static string GetDraftPriorityLabel(string? priority)
=> priority?.ToLowerInvariant() switch
// --- State icon ---
private static string GetDraftStateIcon(DraftQueueItem item)
{
if (IsDraftBlocked(item)) return "\uE9F5"; // 시계
return item.State?.ToLowerInvariant() switch
{
"now" => "지금",
"later" => "나중",
_ => "다음",
"running" => "\uE895", // 회전 (재생 아이콘)
"failed" => "\uE783", // 경고
"completed" => "\uE73E", // 체크
_ => "\uE76C", // 대기 점
};
}
private Brush GetDraftStateIconBrush(DraftQueueItem item)
{
if (IsDraftBlocked(item)) return BrushFromHex("#C2410C");
return item.State?.ToLowerInvariant() switch
{
"running" => TryFindResource("AccentColor") as Brush ?? BrushFromHex("#5B8AF5"),
"failed" => BrushFromHex("#DC2626"),
"completed" => BrushFromHex("#16A34A"),
_ => TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#8E8E9A"),
};
}
// --- Kind chip ---
private static (string Icon, string Label) GetDraftKindChipContent(DraftQueueItem item)
=> item.Kind?.ToLowerInvariant() switch
{
"followup" => ("\uE8A5", "후속"),
"steering" => ("\uE7C3", "조정"),
"command" => ("\uE756", "명령"),
"direct" => ("\uE8A7", "직접"),
_ => ("\uE8BD", "메시지"),
};
private Brush GetDraftKindChipColor(DraftQueueItem item)
=> item.Kind?.ToLowerInvariant() switch
{
"followup" => BrushFromHex("#0F766E"),
"steering" => BrushFromHex("#B45309"),
"command" => BrushFromHex("#7C3AED"),
"direct" => BrushFromHex("#2563EB"),
_ => TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#6B7280"),
};
// --- Legacy helpers (used by other partial classes) ---
private static string GetDraftKindLabel(DraftQueueItem item)
=> item.Kind?.ToLowerInvariant() switch
{
"followup" => "후속 작업",
"followup" => "후속",
"steering" => "조정",
"command" => "명령",
"direct" => "직접 실행",
"command" => "명령",
"direct" => "직접",
_ => "메시지",
};
private (string Icon, Brush Foreground) GetDraftKindVisual(DraftQueueItem item)
=> item.Kind?.ToLowerInvariant() switch
{
"followup" => ("\uE8A5", BrushFromHex("#0F766E")),
"steering" => ("\uE7C3", BrushFromHex("#B45309")),
"command" => ("\uE756", BrushFromHex("#7C3AED")),
"direct" => ("\uE8A7", BrushFromHex("#2563EB")),
_ => ("\uE8BD", BrushFromHex("#475569")),
};
private static string GetDraftStateLabel(DraftQueueItem item)
=> IsDraftBlocked(item) ? "재시도 대기"
: item.State?.ToLowerInvariant() switch
{
"running" => "실행 중",
"failed" => "실패",
"running" => "실행 중",
"failed" => "실패",
"completed" => "완료",
_ => "대기",
};
private Brush GetDraftStateBrush(DraftQueueItem item)
=> IsDraftBlocked(item) ? BrushFromHex("#B45309")
: item.State?.ToLowerInvariant() switch
{
"running" => BrushFromHex("#2563EB"),
"failed" => BrushFromHex("#DC2626"),
"completed" => BrushFromHex("#059669"),
_ => BrushFromHex("#7C3AED"),
_ => "대기",
};
private (Brush Background, Brush Border, Brush Foreground) GetDraftStateBadgeColors(DraftQueueItem item)
@@ -603,22 +505,17 @@ public partial class ChatWindow
? (BrushFromHex("#FFF7ED"), BrushFromHex("#FDBA74"), BrushFromHex("#C2410C"))
: item.State?.ToLowerInvariant() switch
{
"running" => (BrushFromHex("#EFF6FF"), BrushFromHex("#BFDBFE"), BrushFromHex("#1D4ED8")),
"failed" => (BrushFromHex("#FEF2F2"), BrushFromHex("#FECACA"), BrushFromHex("#991B1B")),
"running" => (BrushFromHex("#EFF6FF"), BrushFromHex("#BFDBFE"), BrushFromHex("#1D4ED8")),
"failed" => (BrushFromHex("#FEF2F2"), BrushFromHex("#FECACA"), BrushFromHex("#991B1B")),
"completed" => (BrushFromHex("#ECFDF5"), BrushFromHex("#BBF7D0"), BrushFromHex("#166534")),
_ => (BrushFromHex("#F5F3FF"), BrushFromHex("#DDD6FE"), BrushFromHex("#6D28D9")),
_ => (BrushFromHex("#F5F3FF"), BrushFromHex("#DDD6FE"), BrushFromHex("#6D28D9")),
};
private static (Brush Background, Brush Border, Brush Foreground) GetDraftPriorityBadgeColors(string? priority)
=> priority?.ToLowerInvariant() switch
{
"now" => (BrushFromHex("#EEF2FF"), BrushFromHex("#C7D2FE"), BrushFromHex("#3730A3")),
"later" => (BrushFromHex("#F8FAFC"), BrushFromHex("#E2E8F0"), BrushFromHex("#475569")),
_ => (BrushFromHex("#FEF3C7"), BrushFromHex("#FDE68A"), BrushFromHex("#92400E")),
};
private static bool IsDraftBlocked(DraftQueueItem item)
=> string.Equals(item.State, "queued", StringComparison.OrdinalIgnoreCase)
&& item.NextRetryAt.HasValue
&& item.NextRetryAt.Value > DateTime.Now;
private Button CreateDraftQueueActionButton(string label, Action onClick, Brush? background = null)
=> CreateQueueFooterButton(label, onClick);
}

View File

@@ -10,7 +10,7 @@ public partial class ChatWindow
{
if (TokenUsageCard == null || TokenUsageArc == null || TokenUsagePercentText == null
|| TokenUsageSummaryText == null || TokenUsageHintText == null
|| TokenUsageThresholdMarker == null || CompactNowLabel == null)
|| CompactNowLabel == null)
return;
var showContextUsage = _activeTab is "Cowork" or "Code";
@@ -79,7 +79,6 @@ public partial class ChatWindow
}
TokenUsageArc.Stroke = progressBrush;
TokenUsageThresholdMarker.Fill = progressBrush;
var percentText = $"{Math.Round(usageRatio * 100):0}%";
TokenUsagePercentText.Text = percentText;
TokenUsageSummaryText.Text = $"컨텍스트 {percentText}";
@@ -101,8 +100,7 @@ public partial class ChatWindow
TokenUsageCard.ToolTip = null;
UpdateCircularUsageArc(TokenUsageArc, usageRatio, 14, 14, 11);
PositionThresholdMarker(TokenUsageThresholdMarker, triggerRatio, 14, 14, 11, 2.5);
UpdateCircularUsageArc(TokenUsageArc, usageRatio, 15, 15, 11);
}
private void TokenUsageCard_MouseEnter(object sender, MouseEventArgs e)

View File

@@ -464,15 +464,16 @@ public partial class ChatWindow
return;
}
if (_isStreaming)
if (_streamingTabs.Contains(_activeTab))
{
_streamCts?.Cancel();
if (_tabStreamCts.TryGetValue(_activeTab, out var convListCts)) convListCts.Cancel();
_cursorTimer.Stop();
_typingTimer.Stop();
_elapsedTimer.Stop();
_activeStreamText = null;
_elapsedLabel = null;
_isStreaming = false;
_streamingTabs.Remove(_activeTab);
if (_tabStreamCts.Remove(_activeTab, out var removedCts)) removedCts.Dispose();
}
var conv = _storage.Load(item.Id);

View File

@@ -37,13 +37,14 @@ public partial class ChatWindow
var bubble = new Border
{
Background = userBubbleBg,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(12),
Padding = new Thickness(11, 7, 11, 7),
HorizontalAlignment = HorizontalAlignment.Right,
};
// DynamicResource 방식으로 바인딩 — 테마 전환 시 기존 버블도 자동 업데이트
bubble.SetResourceReference(Border.BackgroundProperty, "HintBackground");
if (string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase) ||
string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase))
@@ -120,6 +121,7 @@ public partial class ChatWindow
if (animate)
ApplyMessageEntryAnimation(wrapper);
if (message?.MsgId != null) _elementCache[$"m_{message.MsgId}"] = wrapper;
MessagePanel.Children.Add(wrapper);
return;
}
@@ -129,6 +131,7 @@ public partial class ChatWindow
var compactCard = CreateCompactionMetaCard(message, primaryText, secondaryText, hintBg, borderBrush, accentBrush);
if (animate)
ApplyMessageEntryAnimation(compactCard);
if (message.MsgId != null) _elementCache[$"m_{message.MsgId}"] = compactCard;
MessagePanel.Children.Add(compactCard);
return;
}
@@ -146,14 +149,7 @@ public partial class ChatWindow
var (agentName, _, _) = GetAgentIdentity();
var header = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(2, 0, 0, 1.5) };
header.Children.Add(new TextBlock
{
Text = "\uE945",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 10,
Foreground = secondaryText,
VerticalAlignment = VerticalAlignment.Center,
});
header.Children.Add(CreateMiniLauncherIcon(pixelSize: 4.0));
header.Children.Add(new TextBlock
{
Text = agentName,
@@ -167,12 +163,13 @@ public partial class ChatWindow
var contentCard = new Border
{
Background = assistantBubbleBg,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(12),
Padding = new Thickness(11, 8, 11, 8),
};
// DynamicResource 방식으로 바인딩 — 테마 전환 시 기존 버블도 자동 업데이트
contentCard.SetResourceReference(Border.BackgroundProperty, "ItemBackground");
var contentStack = new StackPanel();
var app = System.Windows.Application.Current as App;
@@ -337,6 +334,7 @@ public partial class ChatWindow
ShowMessageContextMenu(aiContent, "assistant");
};
if (message?.MsgId != null) _elementCache[$"m_{message.MsgId}"] = container;
MessagePanel.Children.Add(container);
}
}

View File

@@ -11,9 +11,57 @@ namespace AxCopilot.Views;
public partial class ChatWindow
{
/// <summary>런처 아이콘과 동일한 2×2 컬러 픽셀 다이아몬드를 생성합니다.</summary>
internal static FrameworkElement CreateMiniLauncherIcon(double pixelSize = 4.0)
{
const double gap = 0.75;
var total = pixelSize * 2 + gap;
var canvas = new System.Windows.Controls.Canvas
{
Width = total,
Height = total,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
RenderTransformOrigin = new Point(0.5, 0.5),
RenderTransform = new RotateTransform(45),
};
void AddPixel(double left, double top, string colorHex)
{
var rect = new System.Windows.Shapes.Rectangle
{
Width = pixelSize,
Height = pixelSize,
RadiusX = 1.0,
RadiusY = 1.0,
Fill = new SolidColorBrush((Color)System.Windows.Media.ColorConverter.ConvertFromString(colorHex)),
};
System.Windows.Controls.Canvas.SetLeft(rect, left);
System.Windows.Controls.Canvas.SetTop(rect, top);
canvas.Children.Add(rect);
}
AddPixel(0, 0, "#4488FF");
AddPixel(pixelSize + gap, 0, "#44DD66");
AddPixel(0, pixelSize + gap, "#44DD66");
AddPixel(pixelSize + gap, pixelSize + gap, "#FF4466");
// Wrap in a container so the rotated canvas doesn't disturb layout
var host = new Grid
{
Width = total,
Height = total,
VerticalAlignment = VerticalAlignment.Center,
};
host.Children.Add(canvas);
return host;
}
/// <summary>좋아요/싫어요 피드백 버튼을 생성합니다.</summary>
private Button CreateFeedbackButton(
string iconGlyph,
string activeGlyph,
string tooltip,
Brush normalColor,
Brush activeColor,
@@ -54,12 +102,18 @@ public partial class ChatWindow
BorderThickness = new Thickness(0),
Cursor = Cursors.Hand,
Padding = new Thickness(0),
ToolTip = tooltip
ToolTip = tooltip,
// Remove WPF default hover chrome — visual states handled entirely by chip/icon
Template = (ControlTemplate)System.Windows.Markup.XamlReader.Parse(
"<ControlTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' TargetType='Button'>" +
"<ContentPresenter/>" +
"</ControlTemplate>"),
};
void RefreshVisual()
{
var active = isActive();
icon.Text = active ? activeGlyph : iconGlyph;
icon.Foreground = active ? activeColor : normalColor;
chip.Background = active ? activeBackground : Brushes.Transparent;
chip.BorderBrush = active ? activeColor : Brushes.Transparent;
@@ -67,15 +121,22 @@ public partial class ChatWindow
RefreshVisual();
var hoverBackground = new SolidColorBrush(Color.FromArgb(18, 128, 128, 128));
btn.MouseEnter += (_, _) =>
{
if (!isActive())
{
icon.Foreground = hoverBrush;
chip.Background = hoverBackground;
}
};
btn.MouseLeave += (_, _) =>
{
if (!isActive())
{
icon.Foreground = normalColor;
chip.Background = Brushes.Transparent;
}
};
btn.Click += (_, _) =>
{
@@ -140,7 +201,8 @@ public partial class ChatWindow
}
var likeBtn = CreateFeedbackButton(
"\uE8E1",
"\uE8E1", // 좋아요 아웃라인
"\uEB51", // 좋아요 채움 (활성)
"좋아요",
btnColor,
new SolidColorBrush(Color.FromRgb(0x38, 0xA1, 0x69)),
@@ -154,7 +216,8 @@ public partial class ChatWindow
});
var dislikeBtn = CreateFeedbackButton(
"\uE8E0",
"\uE8E0", // 싫어요 아웃라인
"\uEB50", // 싫어요 채움 (활성)
"싫어요",
btnColor,
new SolidColorBrush(Color.FromRgb(0xE5, 0x3E, 0x3E)),
@@ -416,6 +479,13 @@ public partial class ChatWindow
}
}
// 수정된 메시지(및 이후 잘린 메시지) 캐시 무효화
lock (_convLock)
{
for (var i = userMsgIdx; i < conv.Messages.Count; i++)
_elementCache.Remove($"m_{conv.Messages[i].MsgId}");
}
RenderMessages(preserveViewport: true);
AutoScrollIfNeeded();

View File

@@ -46,6 +46,8 @@ public partial class ChatWindow
}
else if (!string.Equals(result, "취소", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(result, "확인", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(result, "건너뛰기", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(result, "중단", StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrWhiteSpace(result))
{
agentDecision = $"수정 요청: {result.Trim()}";

View File

@@ -56,7 +56,7 @@ public partial class ChatWindow
PreviewPanel.Visibility = Visibility.Visible;
PreviewSplitter.Visibility = Visibility.Visible;
BtnPreviewToggle.Visibility = Visibility.Visible;
if (PreviewDot != null) PreviewDot.Fill = System.Windows.Media.Brushes.LimeGreen;
RebuildPreviewTabs();
LoadPreviewContent(filePath);
@@ -434,6 +434,7 @@ public partial class ChatWindow
{
_previewTabs.Clear();
_activePreviewTab = null;
if (PreviewDot != null) PreviewDot.Fill = System.Windows.Media.Brushes.Gray;
if (PreviewHeaderTitle != null) PreviewHeaderTitle.Text = "미리보기";
if (PreviewHeaderSubtitle != null) PreviewHeaderSubtitle.Text = "선택한 파일이 여기에 표시됩니다";
if (PreviewHeaderMeta != null) PreviewHeaderMeta.Text = "파일 메타";
@@ -466,7 +467,7 @@ public partial class ChatWindow
private void BtnClosePreview_Click(object sender, RoutedEventArgs e)
{
HidePreviewPanel();
BtnPreviewToggle.Visibility = Visibility.Collapsed;
// BtnPreviewToggle은 항상 표시 — 패널만 닫힘
}
private void BtnPreviewToggle_Click(object sender, RoutedEventArgs e)
@@ -477,6 +478,7 @@ public partial class ChatWindow
PreviewSplitter.Visibility = Visibility.Collapsed;
PreviewColumn.Width = new GridLength(0);
SplitterColumn.Width = new GridLength(0);
if (PreviewDot != null) PreviewDot.Fill = System.Windows.Media.Brushes.Gray;
}
else if (_previewTabs.Count > 0)
{
@@ -484,6 +486,7 @@ public partial class ChatWindow
PreviewSplitter.Visibility = Visibility.Visible;
PreviewColumn.Width = new GridLength(420);
SplitterColumn.Width = new GridLength(5);
if (PreviewDot != null) PreviewDot.Fill = System.Windows.Media.Brushes.LimeGreen;
RebuildPreviewTabs();
if (_activePreviewTab != null)
LoadPreviewContent(_activePreviewTab);

View File

@@ -161,8 +161,8 @@ public partial class ChatWindow
private void RefreshStatusTokenAggregate()
{
var promptTokens = Math.Max(0, _agentCumulativeInputTokens);
var completionTokens = Math.Max(0, _agentCumulativeOutputTokens);
var promptTokens = (int)Math.Max(0, _tabCumulativeInputTokens.GetValueOrDefault(_activeTab));
var completionTokens = (int)Math.Max(0, _tabCumulativeOutputTokens.GetValueOrDefault(_activeTab));
if (promptTokens == 0 && completionTokens == 0)
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
@@ -32,9 +32,7 @@ public partial class ChatWindow
if (conversation?.ShowExecutionHistory ?? true)
return events;
return events
.Where(ShouldShowCollapsedProgressEvent)
.ToList();
return events.Where(ShouldShowCollapsedProgressEvent).ToList();
}
private static bool ShouldShowCollapsedProgressEvent(ChatExecutionEvent executionEvent)
@@ -53,9 +51,19 @@ public partial class ChatWindow
return true;
}
// 문서 생성 ToolResult 성공 시 항상 표시 (미리보기 카드용)
if (restoredEvent.Type == AgentEventType.ToolResult && restoredEvent.Success
&& IsDocumentCreationTool(restoredEvent.ToolName))
return true;
return IsProcessFeedEvent(restoredEvent);
}
private static bool IsDocumentCreationTool(string? toolName) =>
toolName is "html_create" or "docx_create" or "excel_create" or "xlsx_create"
or "csv_create" or "markdown_create" or "md_create" or "script_create"
or "pptx_create";
private List<(DateTime Timestamp, int Order, Action Render)> BuildTimelineRenderActions(
IReadOnlyCollection<ChatMessage> visibleMessages,
IReadOnlyCollection<ChatExecutionEvent> visibleEvents)
@@ -63,22 +71,52 @@ public partial class ChatWindow
var timeline = new List<(DateTime Timestamp, int Order, Action Render)>(visibleMessages.Count + visibleEvents.Count);
foreach (var msg in visibleMessages)
timeline.Add((msg.Timestamp, 0, () => AddMessageBubble(msg.Role, msg.Content, animate: false, message: msg)));
{
var capturedMsg = msg;
var cacheKey = $"m_{msg.MsgId}";
timeline.Add((msg.Timestamp, 0, () =>
{
// 캐시된 버블이 있으면 재생성 없이 재사용 (O(1) Add vs 전체 재생성)
if (_elementCache.TryGetValue(cacheKey, out var cached))
MessagePanel.Children.Add(cached);
else
AddMessageBubble(capturedMsg.Role, capturedMsg.Content, animate: false, message: capturedMsg);
}));
}
// 현재 실행 중인 run의 process feed 이벤트를 통합 카드로 대체 (히스토리 접힘 모드)
var showFullHistory = _currentConversation?.ShowExecutionHistory ?? true;
var activeRunId = _isStreaming ? (_appState.AgentRun.RunId ?? "") : "";
foreach (var executionEvent in visibleEvents)
{
// 스트리밍 중이고 히스토리 접힘 상태일 때, 현재 run의 process feed 이벤트는 통합 카드에서 표시
if (!showFullHistory && _isStreaming
&& !string.IsNullOrEmpty(activeRunId)
&& string.Equals(executionEvent.RunId, activeRunId, StringComparison.Ordinal))
{
var restoredCheck = ToAgentEvent(executionEvent);
if (IsProcessFeedEvent(restoredCheck))
continue; // 통합 카드로 대체 — 개별 pill 스킵
}
var restoredEvent = ToAgentEvent(executionEvent);
timeline.Add((executionEvent.Timestamp, 1, () => AddAgentEventBanner(restoredEvent)));
}
// 스트리밍 중 + 히스토리 접힘: 통합 진행 카드 삽입 (개별 pill 대체)
if (!showFullHistory && _isStreaming && _currentRunProgressSteps.Count > 0)
{
var capturedSteps = _currentRunProgressSteps.ToList();
var cardTimestamp = capturedSteps[^1].Timestamp;
timeline.Add((cardTimestamp, 1, () => AddLiveRunProgressCard(capturedSteps)));
}
var liveProgressHint = GetLiveAgentProgressHint();
if (liveProgressHint != null)
timeline.Add((liveProgressHint.Timestamp, 2, () => AddAgentEventBanner(liveProgressHint)));
return timeline
.OrderBy(x => x.Timestamp)
.ThenBy(x => x.Order)
.ToList();
return timeline.OrderBy(x => x.Timestamp).ThenBy(x => x.Order).ToList();
}
private Border CreateTimelineLoadMoreCard(int hiddenCount)
@@ -198,11 +236,7 @@ public partial class ChatWindow
};
var stack = new StackPanel();
var header = new StackPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(0, 0, 0, 6),
};
var header = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 6) };
header.Children.Add(new TextBlock
{
Text = icon,
@@ -222,12 +256,8 @@ public partial class ChatWindow
});
stack.Children.Add(header);
var lines = (message.Content ?? "")
.Replace("\r\n", "\n")
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim())
.Where(line => !string.IsNullOrWhiteSpace(line))
.ToList();
var lines = (message.Content ?? "").Replace("\r\n", "\n").Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim()).Where(line => !string.IsNullOrWhiteSpace(line)).ToList();
foreach (var line in lines)
{

View File

@@ -1055,10 +1055,10 @@
<!-- 우: 프리뷰 토글 버튼 -->
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button x:Name="BtnPreviewToggle" Style="{StaticResource GhostBtn}"
Click="BtnPreviewToggle_Click" ToolTip="미리보기 패널" Visibility="Collapsed"
Click="BtnPreviewToggle_Click" ToolTip="미리보기 패널"
Padding="5,2.5" MinWidth="0">
<StackPanel Orientation="Horizontal">
<Ellipse x:Name="PreviewDot" Width="5" Height="5" Fill="#22C55E"
<Ellipse x:Name="PreviewDot" Width="5" Height="5" Fill="Gray"
Margin="0,0,4,0" VerticalAlignment="Center"/>
<TextBlock Text="프리뷰" FontSize="10.5"
Foreground="{DynamicResource PrimaryText}"
@@ -1362,17 +1362,54 @@
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
Background="{DynamicResource LauncherBackground}"
Padding="24,12,24,8">
Padding="24,12,24,8"
UseLayoutRounding="True">
<StackPanel x:Name="MessagePanel"
Margin="0,0,0,8"
MaxWidth="960"
HorizontalAlignment="Center">
HorizontalAlignment="Center"
UseLayoutRounding="True">
<StackPanel.RenderTransform>
<TranslateTransform/>
</StackPanel.RenderTransform>
</StackPanel>
</ScrollViewer>
<!-- ── 스트리밍 상태 바 (Claude 스타일 하단 플로팅) ── -->
<Border x:Name="StreamingStatusBar" Grid.Row="3"
VerticalAlignment="Bottom" HorizontalAlignment="Center"
Margin="0,0,0,14"
Visibility="Collapsed"
Background="{DynamicResource HintBackground}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
CornerRadius="20"
Padding="14,7,16,7"
IsHitTestVisible="False">
<Border.Effect>
<DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.10" Color="Black"/>
</Border.Effect>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock x:Name="StatusBarIcon"
Text="&#xE895;"
FontFamily="Segoe MDL2 Assets"
FontSize="12"
Foreground="{DynamicResource AccentColor}"
VerticalAlignment="Center"
Margin="0,0,8,0"
RenderTransformOrigin="0.5,0.5">
<TextBlock.RenderTransform>
<RotateTransform x:Name="StatusIconRotation"/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock x:Name="StatusBarText"
Text="생각하는 중..."
FontSize="13"
Foreground="{DynamicResource SecondaryText}"
VerticalAlignment="Center"/>
</StackPanel>
</Border>
<!-- 빈 상태 -->
<Grid x:Name="EmptyState" Grid.Row="3"
HorizontalAlignment="Stretch"
@@ -1699,6 +1736,82 @@
HorizontalAlignment="Center"
VerticalAlignment="Bottom">
<StackPanel HorizontalAlignment="Stretch">
<!-- ── 펄스 닷 애니메이션 (AI 처리 중) ── -->
<Border x:Name="PulseDotBar"
Visibility="Collapsed"
HorizontalAlignment="Left"
Margin="6,0,0,10">
<StackPanel>
<!-- 레거시 펄스 점 (코드비하인드 참조용, 화면에 표시 안 함) -->
<Ellipse x:Name="PulseDot1" Width="7" Height="7" Fill="{DynamicResource AccentColor}" Opacity="0.3" Visibility="Collapsed"/>
<Ellipse x:Name="PulseDot2" Width="7" Height="7" Fill="{DynamicResource AccentColor}" Opacity="0.3" Visibility="Collapsed"/>
<Ellipse x:Name="PulseDot3" Width="7" Height="7" Fill="{DynamicResource AccentColor}" Opacity="0.3" Visibility="Collapsed"/>
<!-- 주 행: 다이아몬드 아이콘 + 상태 텍스트 -->
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<!-- 미니 다이아몬드 아이콘 (런처 아이콘 축소판, 코드비하인드에서 애니메이션 적용) -->
<Canvas x:Name="StatusDiamondIcon" Width="16" Height="16"
VerticalAlignment="Center" ClipToBounds="False"
Margin="0,1,8,0">
<Canvas.RenderTransformOrigin>0.5,0.5</Canvas.RenderTransformOrigin>
<Canvas.RenderTransform>
<TransformGroup>
<RotateTransform x:Name="StatusIconRotate" Angle="45"/>
<ScaleTransform x:Name="StatusIconScale" ScaleX="1" ScaleY="1"/>
</TransformGroup>
</Canvas.RenderTransform>
<!-- 파란 픽셀 (좌상) -->
<Rectangle x:Name="StatusPixelBlue"
Canvas.Left="0.5" Canvas.Top="0.5"
Width="6.5" Height="6.5"
RadiusX="1" RadiusY="1"
Fill="#4488FF"/>
<!-- 초록 픽셀 (우상) -->
<Rectangle x:Name="StatusPixelGreen1"
Canvas.Left="8.5" Canvas.Top="0.5"
Width="6.5" Height="6.5"
RadiusX="1" RadiusY="1"
Fill="#44DD66"/>
<!-- 초록 픽셀 (좌하) -->
<Rectangle x:Name="StatusPixelGreen2"
Canvas.Left="0.5" Canvas.Top="8.5"
Width="6.5" Height="6.5"
RadiusX="1" RadiusY="1"
Fill="#44DD66"/>
<!-- 빨간 픽셀 (우하) -->
<Rectangle x:Name="StatusPixelRed"
Canvas.Left="8.5" Canvas.Top="8.5"
Width="6.5" Height="6.5"
RadiusX="1" RadiusY="1"
Fill="#FF4466"/>
</Canvas>
<!-- MDL2 아이콘 (호환성 유지, 숨김) -->
<TextBlock x:Name="PulseDotStatusIcon"
FontFamily="Segoe MDL2 Assets"
FontSize="11"
Foreground="{DynamicResource AccentColor}"
VerticalAlignment="Center"
Margin="0,0,6,0"
Visibility="Collapsed"
Text="&#xE895;"/>
<!-- 단계 텍스트 (주) -->
<TextBlock x:Name="PulseDotStatusText"
FontSize="12"
FontFamily="Segoe UI, Malgun Gothic"
Foreground="{DynamicResource SecondaryText}"
VerticalAlignment="Center"
Text="생각하는 중..."/>
</StackPanel>
<!-- 세부 항목 목록 (파일별 진행 상황, 회색 서브텍스트) -->
<StackPanel x:Name="PulseDotSubItems"
Margin="24,3,0,0"/>
<!-- 레거시 단일 세부 텍스트 (숨김, 호환성 유지) -->
<TextBlock x:Name="PulseDotDetailText"
Visibility="Collapsed"
Text=""/>
</StackPanel>
</Border>
<Border x:Name="CodeRepoSummaryBar"
Visibility="Collapsed"
HorizontalAlignment="Stretch"
@@ -1910,12 +2023,13 @@
</StackPanel>
</Grid>
</Border>
<!-- 대기열 패널: 입력창 위에 독립적으로 표시 -->
<StackPanel x:Name="DraftQueuePanel"
Visibility="Collapsed"
Margin="0,0,0,6"/>
<!-- 무지개 글로우 + 입력 영역 (겹침 레이아웃) -->
<Grid>
<!-- 무지개 글로우 외부 테두리 (메시지 전송 시 애니메이션) -->
<StackPanel x:Name="DraftQueuePanel"
Visibility="Collapsed"
Margin="0,0,0,6"/>
<Border x:Name="InputGlowBorder" CornerRadius="18" Opacity="0"
Margin="-2" IsHitTestVisible="False">
<Border.BorderBrush>
@@ -1984,33 +2098,33 @@
Grid.Column="2"
Margin="6,0,0,0"
Padding="0"
Width="32"
Height="32"
Width="30"
Height="30"
CornerRadius="999"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
Background="{DynamicResource LauncherBackground}"
BorderThickness="0"
Background="Transparent"
SnapsToDevicePixels="True"
VerticalAlignment="Center"
Visibility="Collapsed"
Cursor="Hand"
MouseEnter="TokenUsageCard_MouseEnter"
MouseLeave="TokenUsageCard_MouseLeave">
<Grid>
<Grid Width="24" Height="24" VerticalAlignment="Center" HorizontalAlignment="Center">
<Ellipse Stroke="{DynamicResource BorderColor}"
StrokeThickness="1.75"/>
<Path x:Name="TokenUsageArc"
Stroke="{DynamicResource AccentColor}"
StrokeThickness="2.25"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"/>
<Canvas IsHitTestVisible="False">
<Ellipse x:Name="TokenUsageThresholdMarker"
Width="4"
Height="4"
Fill="{DynamicResource AccentColor}"/>
</Canvas>
<Grid Width="28" Height="28">
<!-- Ring track (subtle background ring) -->
<Ellipse Width="22" Height="22"
Stroke="{DynamicResource BorderColor}"
StrokeThickness="2"
Opacity="0.45"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<!-- Usage arc (progress) -->
<Path x:Name="TokenUsageArc"
Stroke="{DynamicResource AccentColor}"
StrokeThickness="2"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"/>
<!-- Hidden data elements (referenced in code) -->
<StackPanel Visibility="Collapsed">
<TextBlock x:Name="TokenUsagePercentText"
Text="0%"
FontSize="7.5"
@@ -2018,17 +2132,15 @@
Foreground="{DynamicResource PrimaryText}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
<StackPanel Visibility="Collapsed">
<TextBlock x:Name="TokenUsageSummaryText"
Text="컨텍스트"
FontSize="10.5"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<TextBlock x:Name="TokenUsageHintText"
Text="0 / 0"
FontSize="9"
Foreground="{DynamicResource SecondaryText}"/>
Text="0 / 0"
FontSize="9"
Foreground="{DynamicResource SecondaryText}"/>
</StackPanel>
<Button x:Name="BtnCompactNow"
Style="{StaticResource GhostBtn}"
@@ -3409,129 +3521,6 @@
Checked="ChkOverlayAiEnabled_Changed"
Unchecked="ChkOverlayAiEnabled_Changed"/>
</Grid>
<StackPanel Margin="0,4,0,12">
<TextBlock Text="대화 스타일"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<TextBlock Text="응답 결과물의 기본 형태와 문서 분위기를 여기서 저장합니다."
Margin="0,4,0,0"
FontSize="11"
Foreground="{DynamicResource SecondaryText}"/>
</StackPanel>
<Grid x:Name="OverlayDefaultOutputFormatRow" Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="문서 형태"
FontSize="12.5"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<Border Style="{StaticResource OverlayHelpBadge}">
<TextBlock Text="?"
FontSize="10"
FontWeight="Bold"
Foreground="{DynamicResource AccentColor}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<Border.ToolTip>
<ToolTip Style="{StaticResource HelpTooltipStyle}">
<TextBlock TextWrapping="Wrap"
Foreground="White"
FontSize="12"
LineHeight="18"
MaxWidth="280">문서나 결과물을 파일로 만들 때 우선 고려할 출력 형식입니다. 자동으로 두면 요청 내용을 보고 가장 맞는 형식을 선택합니다.</TextBlock>
</ToolTip>
</Border.ToolTip>
</Border>
</StackPanel>
<TextBlock Text="기본은 AI 자동 선택으로 두고 필요할 때만 바꿉니다."
Margin="0,4,0,0"
FontSize="11"
Foreground="{DynamicResource SecondaryText}"/>
</StackPanel>
<ComboBox x:Name="CmbOverlayDefaultOutputFormat"
Grid.Column="1"
MinWidth="160"
Style="{StaticResource OverlayComboBox}"
SelectionChanged="CmbOverlayDefaultOutputFormat_SelectionChanged">
<ComboBoxItem Content="AI 자동 · 자동" Tag="auto"/>
<ComboBoxItem Content="AI 자동 · Word" Tag="docx"/>
<ComboBoxItem Content="AI 자동 · HTML 보고서" Tag="html"/>
<ComboBoxItem Content="AI 자동 · Excel" Tag="xlsx"/>
<ComboBoxItem Content="AI 자동 · PDF" Tag="pdf"/>
<ComboBoxItem Content="AI 자동 · Markdown" Tag="md"/>
<ComboBoxItem Content="AI 자동 · 텍스트" Tag="txt"/>
</ComboBox>
</Grid>
<Grid x:Name="OverlayDefaultMoodRow" Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="디자인 스타일"
FontSize="12.5"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<Border Style="{StaticResource OverlayHelpBadge}">
<TextBlock Text="?"
FontSize="10"
FontWeight="Bold"
Foreground="{DynamicResource AccentColor}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<Border.ToolTip>
<ToolTip Style="{StaticResource HelpTooltipStyle}">
<TextBlock TextWrapping="Wrap"
Foreground="White"
FontSize="12"
LineHeight="18"
MaxWidth="280">HTML 보고서나 미리보기 화면을 만들 때 기본으로 적용할 분위기입니다. 같은 내용도 보고서 톤과 카드 스타일이 달라집니다.</TextBlock>
</ToolTip>
</Border.ToolTip>
</Border>
</StackPanel>
<TextBlock Text="HTML/미리보기 기본 스타일을 설정합니다."
Margin="0,4,0,0"
FontSize="11"
Foreground="{DynamicResource SecondaryText}"/>
</StackPanel>
<ComboBox x:Name="CmbOverlayDefaultMood"
Grid.Column="1"
MinWidth="160"
Style="{StaticResource OverlayComboBox}"
SelectionChanged="CmbOverlayDefaultMood_SelectionChanged"/>
</Grid>
<Grid x:Name="OverlayAutoPreviewRow" Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Text="문서 미리보기"
FontSize="12.5"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<TextBlock Text="파일 생성 시 오른쪽 프리뷰 패널을 자동으로 열지 정합니다."
Margin="0,4,0,0"
FontSize="11"
Foreground="{DynamicResource SecondaryText}"/>
</StackPanel>
<ComboBox x:Name="CmbOverlayAutoPreview"
Grid.Column="1"
MinWidth="160"
Style="{StaticResource OverlayComboBox}"
SelectionChanged="CmbOverlayAutoPreview_SelectionChanged">
<ComboBoxItem Content="자동 표시" Tag="auto"/>
<ComboBoxItem Content="수동" Tag="manual"/>
<ComboBoxItem Content="비활성화" Tag="off"/>
</ComboBox>
</Grid>
<Grid x:Name="OverlayPdfExportPathRow" Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
@@ -3576,53 +3565,6 @@
Foreground="{DynamicResource PrimaryText}"
FontSize="12"/>
</Grid>
<StackPanel Margin="0,2,0,12">
<TextBlock Text="대화 관리"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<TextBlock Text="대화 보관 기간과 저장 공간 정리를 여기서 바로 관리합니다."
Margin="0,4,0,0"
FontSize="11"
Foreground="{DynamicResource SecondaryText}"/>
</StackPanel>
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Margin="0,0,12,0">
<TextBlock Text="대화 보관 기간"
FontSize="12.5"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<TextBlock Text="설정 기간이 지나면 오래된 대화는 자동으로 정리됩니다."
Margin="0,4,0,0"
FontSize="11"
Foreground="{DynamicResource SecondaryText}"/>
</StackPanel>
<WrapPanel Grid.Column="1" HorizontalAlignment="Right">
<Button x:Name="BtnOverlayRetention7"
Content="7일"
Padding="10,6"
Margin="0,0,6,0"
Click="BtnOverlayRetention_Click"/>
<Button x:Name="BtnOverlayRetention30"
Content="30일"
Padding="10,6"
Margin="0,0,6,0"
Click="BtnOverlayRetention_Click"/>
<Button x:Name="BtnOverlayRetention90"
Content="90일"
Padding="10,6"
Margin="0,0,6,0"
Click="BtnOverlayRetention_Click"/>
<Button x:Name="BtnOverlayRetentionUnlimited"
Content="무제한"
Padding="10,6"
Click="BtnOverlayRetention_Click"/>
</WrapPanel>
</Grid>
<Border Background="{DynamicResource ItemBackground}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
@@ -3721,65 +3663,15 @@
Padding="0,12,0,0"
Margin="0,0,0,12">
<StackPanel>
<TextBlock Text="글로우 효과"
<TextBlock Text="채팅 글로우 효과"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<TextBlock Text="런처와 AX Agent의 글로우 연출을 여기서 조정합니다."
<TextBlock Text="AX Agent 채팅 입력창의 글로우 연출을 조정합니다. 런처 글로우는 트레이 아이콘 일반 설정에서 변경하세요."
Margin="0,4,0,10"
FontSize="11"
Foreground="{DynamicResource SecondaryText}"/>
<Border Style="{StaticResource OverlayAdvancedToggleRowStyle}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Margin="0,0,16,0">
<TextBlock Text="런처 무지개 글로우"
FontSize="12.5"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<TextBlock Text="AX Commander 테두리에 무지개 글로우를 표시합니다."
Margin="0,4,0,0"
FontSize="11.5"
TextWrapping="Wrap"
Foreground="{DynamicResource SecondaryText}"/>
</StackPanel>
<CheckBox x:Name="ChkOverlayEnableLauncherRainbowGlow"
Grid.Column="1"
VerticalAlignment="Center"
Style="{StaticResource ToggleSwitch}"
Checked="ChkOverlayFeatureToggle_Changed"
Unchecked="ChkOverlayFeatureToggle_Changed"/>
</Grid>
</Border>
<Border Style="{StaticResource OverlayAdvancedToggleRowStyle}" Margin="0,8,0,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Margin="0,0,16,0">
<TextBlock Text="런처 선택 글로우"
FontSize="12.5"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<TextBlock Text="선택된 런처 항목에 은은한 글로우를 표시합니다."
Margin="0,4,0,0"
FontSize="11.5"
TextWrapping="Wrap"
Foreground="{DynamicResource SecondaryText}"/>
</StackPanel>
<CheckBox x:Name="ChkOverlayEnableSelectionGlow"
Grid.Column="1"
VerticalAlignment="Center"
Style="{StaticResource ToggleSwitch}"
Checked="ChkOverlayFeatureToggle_Changed"
Unchecked="ChkOverlayFeatureToggle_Changed"/>
</Grid>
</Border>
<Border Style="{StaticResource OverlayAdvancedToggleRowStyle}" Margin="0,8,0,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
@@ -4047,7 +3939,7 @@
<Grid x:Name="OverlayTemperatureRow" Margin="0,8,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="220"/>
<ColumnDefinition Width="300"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Temperature"
@@ -4068,34 +3960,63 @@
LineHeight="18"
MaxWidth="280">낮을수록 더 일관되고 정확한 답변 쪽으로, 높을수록 더 자유롭고 창의적인 답변 쪽으로 기울어집니다. 일반 업무형 응답은 0.7 전후가 기본값입니다.</TextBlock>
</ToolTip>
</Border.ToolTip>
</Border>
</Border.ToolTip>
</Border>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<Slider x:Name="SldOverlayTemperature"
Width="150"
Minimum="0"
Maximum="2"
TickFrequency="0.1"
IsSnapToTickEnabled="True"
ValueChanged="SldOverlayTemperature_ValueChanged"
VerticalAlignment="Center"/>
<Border Width="48"
Height="28"
Margin="10,0,0,0"
CornerRadius="8"
Background="{DynamicResource ItemBackground}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1">
<TextBlock x:Name="TxtOverlayTemperatureValue"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryText}"
FontSize="11.5"/>
</Border>
<TextBox x:Name="TxtOverlayTemperature"
Visibility="Collapsed"
LostFocus="TxtOverlayTemperature_LostFocus"/>
<StackPanel Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,0,6">
<Border x:Name="OverlayTemperatureAutoCard"
Cursor="Hand"
Margin="0,0,8,0"
Padding="10,5"
CornerRadius="10"
Background="{DynamicResource ItemBackground}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
MouseLeftButtonUp="OverlayTemperatureAutoCard_MouseLeftButtonUp">
<TextBlock Text="자동"
FontSize="11.5"
Foreground="{DynamicResource PrimaryText}"/>
</Border>
<Border x:Name="OverlayTemperatureCustomCard"
Cursor="Hand"
Padding="10,5"
CornerRadius="10"
Background="{DynamicResource ItemBackground}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
MouseLeftButtonUp="OverlayTemperatureCustomCard_MouseLeftButtonUp">
<TextBlock Text="사용자 지정"
FontSize="11.5"
Foreground="{DynamicResource PrimaryText}"/>
</Border>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<Slider x:Name="SldOverlayTemperature"
Width="150"
Minimum="0"
Maximum="2"
TickFrequency="0.1"
IsSnapToTickEnabled="True"
ValueChanged="SldOverlayTemperature_ValueChanged"
VerticalAlignment="Center"/>
<Border Width="48"
Height="28"
Margin="10,0,0,0"
CornerRadius="8"
Background="{DynamicResource ItemBackground}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1">
<TextBlock x:Name="TxtOverlayTemperatureValue"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryText}"
FontSize="11.5"/>
</Border>
<TextBox x:Name="TxtOverlayTemperature"
Visibility="Collapsed"
LostFocus="TxtOverlayTemperature_LostFocus"/>
</StackPanel>
</StackPanel>
</Grid>
<Grid x:Name="OverlayMaxRetryRow" Margin="0,8,0,0">
@@ -5870,4 +5791,3 @@
</Window>

File diff suppressed because it is too large Load Diff

View File

@@ -92,9 +92,10 @@ public partial class DockBarWindow : Window
private void StartRainbowGlow()
{
if (_glowTimer != null) return; // 이미 실행 중 — Tick 핸들러 중복 추가 방지
RainbowGlowBorder.Visibility = Visibility.Visible;
_glowTimer ??= new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50) };
var startAngle = 0.0;
_glowTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(150) };
_glowTimer.Tick += (_, _) =>
{
startAngle += 2;
@@ -109,6 +110,7 @@ public partial class DockBarWindow : Window
private void StopRainbowGlow()
{
_glowTimer?.Stop();
_glowTimer = null;
RainbowGlowBorder.Visibility = Visibility.Collapsed;
}

View File

@@ -13,14 +13,18 @@ public partial class LauncherWindow
internal void StartWidgetUpdates()
{
PerformanceMonitorService.Instance.StartPolling();
SyncWidgetPollingState();
PomodoroService.Instance.StateChanged -= OnPomoStateChanged;
PomodoroService.Instance.StateChanged += OnPomoStateChanged;
_vm.UpdateWidgets();
UpdateWidgetVisibility();
UpdateBatteryWidget();
_ = RefreshWeatherAsync();
RefreshVisibleWidgets(forceWeatherRefresh: true);
if (!ShouldRunWidgetTimer())
{
_widgetTimer?.Stop();
return;
}
if (_widgetTimer == null)
{
@@ -30,11 +34,19 @@ public partial class LauncherWindow
};
_widgetTimer.Tick += (_, _) =>
{
_vm.UpdateWidgets();
UpdateWidgetVisibility();
if (_vm.Widget_PerfText.Length > 0 && _widgetBatteryTick++ % 30 == 0)
if (!ShouldRunWidgetTimer())
{
_widgetTimer?.Stop();
SyncWidgetPollingState();
UpdateWidgetVisibility();
return;
}
SyncWidgetPollingState();
RefreshVisibleWidgets(forceWeatherRefresh: false);
if (ShouldShowBatteryWidget() && _widgetBatteryTick++ % 30 == 0)
UpdateBatteryWidget();
if (_widgetWeatherTick++ % 120 == 0)
if (ShouldShowWeatherWidget() && _widgetWeatherTick++ % 120 == 0)
_ = RefreshWeatherAsync();
};
}
@@ -54,12 +66,64 @@ public partial class LauncherWindow
{
Dispatcher.InvokeAsync(() =>
{
_vm.UpdateWidgets();
RefreshVisibleWidgets(forceWeatherRefresh: false);
UpdatePomoWidgetStyle();
UpdateWidgetVisibility();
});
}
private void RefreshVisibleWidgets(bool forceWeatherRefresh)
{
var launcher = CurrentApp?.SettingsService?.Settings?.Launcher;
if (launcher == null)
return;
_vm.UpdateWidgets(
includePerf: launcher.ShowWidgetPerf,
includePomo: launcher.ShowWidgetPomo,
includeNote: launcher.ShowWidgetNote,
includeCalendar: launcher.ShowWidgetCalendar,
includeServerStatus: false);
UpdateWidgetVisibility();
if (ShouldShowBatteryWidget())
UpdateBatteryWidget();
if (forceWeatherRefresh && ShouldShowWeatherWidget())
_ = RefreshWeatherAsync();
}
private void SyncWidgetPollingState()
{
if (ShouldShowPerfWidget())
PerformanceMonitorService.Instance.StartPolling();
else
PerformanceMonitorService.Instance.StopPolling();
}
private bool ShouldRunWidgetTimer()
{
var launcher = CurrentApp?.SettingsService?.Settings?.Launcher;
if (launcher == null)
return false;
return launcher.ShowWidgetPerf
|| launcher.ShowWidgetPomo
|| launcher.ShowWidgetNote
|| launcher.ShowWidgetWeather
|| launcher.ShowWidgetCalendar
|| launcher.ShowWidgetBattery;
}
private bool ShouldShowPerfWidget()
=> CurrentApp?.SettingsService?.Settings?.Launcher?.ShowWidgetPerf ?? false;
private bool ShouldShowWeatherWidget()
=> CurrentApp?.SettingsService?.Settings?.Launcher?.ShowWidgetWeather ?? false;
private bool ShouldShowBatteryWidget()
=> (CurrentApp?.SettingsService?.Settings?.Launcher?.ShowWidgetBattery ?? false) && _vm.Widget_BatteryVisible;
private void UpdatePomoWidgetStyle()
{
if (WgtPomo == null)
@@ -100,6 +164,10 @@ public partial class LauncherWindow
if (WidgetBar != null)
WidgetBar.Visibility = hasAny ? Visibility.Visible : Visibility.Collapsed;
SyncWidgetPollingState();
if (!hasAny)
_widgetTimer?.Stop();
}
private void WgtPerf_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)

View File

@@ -227,6 +227,8 @@
VerticalAlignment="Center" Cursor="Hand"
ClipToBounds="False"
MouseLeftButtonDown="DiamondIcon_Click"
MouseEnter="DiamondIcon_MouseEnter"
MouseLeave="DiamondIcon_MouseLeave"
Visibility="{Binding HasActivePrefix, Converter={StaticResource InverseBoolToVisibilityConverter}}">
<Canvas.RenderTransformOrigin>0.5,0.5</Canvas.RenderTransformOrigin>
<Canvas.RenderTransform>
@@ -236,32 +238,37 @@
</TransformGroup>
</Canvas.RenderTransform>
<!-- 마우스 오버 시 각 픽셀 색상이 퍼져나가는 글로우 (픽셀보다 먼저 렌더링 = 아래에 배치) -->
<!-- 글로우: 기본 Collapsed(GPU 렌더 제외) — 마우스 오버 시 코드에서 활성화 -->
<Rectangle x:Name="GlowBlue"
Canvas.Left="-2.5" Canvas.Top="-2.5"
Width="14.5" Height="14.5"
RadiusX="7" RadiusY="7"
Fill="#4488FF" Opacity="0">
Fill="#4488FF" Opacity="0"
Visibility="Collapsed">
<Rectangle.Effect><BlurEffect Radius="9"/></Rectangle.Effect>
</Rectangle>
<Rectangle x:Name="GlowGreen1"
Canvas.Left="9" Canvas.Top="-2.5"
Width="14.5" Height="14.5"
RadiusX="7" RadiusY="7"
Fill="#44DD66" Opacity="0">
Fill="#44DD66" Opacity="0"
Visibility="Collapsed">
<Rectangle.Effect><BlurEffect Radius="9"/></Rectangle.Effect>
</Rectangle>
<Rectangle x:Name="GlowGreen2"
Canvas.Left="-2.5" Canvas.Top="9"
Width="14.5" Height="14.5"
RadiusX="7" RadiusY="7"
Fill="#44DD66" Opacity="0">
Fill="#44DD66" Opacity="0"
Visibility="Collapsed">
<Rectangle.Effect><BlurEffect Radius="9"/></Rectangle.Effect>
</Rectangle>
<Rectangle x:Name="GlowRed"
Canvas.Left="9" Canvas.Top="9"
Width="14.5" Height="14.5"
RadiusX="7" RadiusY="7"
Fill="#FF4466" Opacity="0">
Fill="#FF4466" Opacity="0"
Visibility="Collapsed">
<Rectangle.Effect><BlurEffect Radius="9"/></Rectangle.Effect>
</Rectangle>
@@ -290,46 +297,8 @@
RadiusX="1.5" RadiusY="1.5"
Fill="#FF4466" Opacity="1"/>
<!-- 마우스 오버 → 각 픽셀 색 글로우 페이드인/아웃 -->
<Canvas.Triggers>
<EventTrigger RoutedEvent="UIElement.MouseEnter">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="GlowBlue" Storyboard.TargetProperty="Opacity" To="0.9" Duration="0:0:0.15">
<DoubleAnimation.EasingFunction><QuadraticEase EasingMode="EaseOut"/></DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetName="GlowGreen1" Storyboard.TargetProperty="Opacity" To="0.9" Duration="0:0:0.15">
<DoubleAnimation.EasingFunction><QuadraticEase EasingMode="EaseOut"/></DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetName="GlowGreen2" Storyboard.TargetProperty="Opacity" To="0.9" Duration="0:0:0.15">
<DoubleAnimation.EasingFunction><QuadraticEase EasingMode="EaseOut"/></DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetName="GlowRed" Storyboard.TargetProperty="Opacity" To="0.9" Duration="0:0:0.15">
<DoubleAnimation.EasingFunction><QuadraticEase EasingMode="EaseOut"/></DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="UIElement.MouseLeave">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="GlowBlue" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.22">
<DoubleAnimation.EasingFunction><QuadraticEase EasingMode="EaseIn"/></DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetName="GlowGreen1" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.22">
<DoubleAnimation.EasingFunction><QuadraticEase EasingMode="EaseIn"/></DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetName="GlowGreen2" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.22">
<DoubleAnimation.EasingFunction><QuadraticEase EasingMode="EaseIn"/></DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetName="GlowRed" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.22">
<DoubleAnimation.EasingFunction><QuadraticEase EasingMode="EaseIn"/></DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Canvas.Triggers>
<!-- 애니메이션은 코드비하인드에서 5가지 효과 중 랜덤 적용 -->
<!-- 글로우 hover 애니메이션은 코드비하인드에서 관리 (Visibility 제어로 GPU 최적화) -->
<!-- 아이콘 픽셀 애니메이션도 코드비하인드에서 랜덤 적용 -->
</Canvas>
<!-- Prefix 배지 (prefix 있을 때) -->
@@ -564,6 +533,7 @@
ItemContainerStyle="{StaticResource ResultItemStyle}"
PreviewMouseLeftButtonUp="ResultList_PreviewMouseLeftButtonUp"
MouseDoubleClick="ResultList_MouseDoubleClick"
PreviewMouseRightButtonUp="ResultList_PreviewMouseRightButtonUp"
Visibility="{Binding Results.Count, Converter={StaticResource CountToVisibilityConverter}}">
<ListView.Resources>

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

View File

@@ -17,6 +17,7 @@ internal sealed class ModelRegistrationDialog : Window
// IBM/CP4D 인증 필드
private readonly ComboBox _authTypeBox;
private readonly ComboBox _executionProfileBox;
private readonly StackPanel _cp4dPanel;
private readonly TextBox _cp4dUrlBox;
private readonly TextBox _cp4dUsernameBox;
@@ -28,6 +29,7 @@ internal sealed class ModelRegistrationDialog : Window
public string ApiKey => _apiKeyBox.Text.Trim();
public bool AllowInsecureTls => _allowInsecureTlsCheck.IsChecked == true;
public string AuthType => (_authTypeBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "bearer";
public string ExecutionProfile => (_executionProfileBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "balanced";
public string Cp4dUrl => _cp4dUrlBox.Text.Trim();
public string Cp4dUsername => _cp4dUsernameBox.Text.Trim();
public string Cp4dPassword => _cp4dPasswordBox.Password.Trim();
@@ -35,7 +37,8 @@ internal sealed class ModelRegistrationDialog : Window
public ModelRegistrationDialog(string service, string existingAlias = "", string existingModel = "",
string existingEndpoint = "", string existingApiKey = "", bool existingAllowInsecureTls = false,
string existingAuthType = "bearer", string existingCp4dUrl = "",
string existingCp4dUsername = "", string existingCp4dPassword = "")
string existingCp4dUsername = "", string existingCp4dPassword = "",
string existingExecutionProfile = "balanced")
{
bool isEdit = !string.IsNullOrEmpty(existingAlias);
Title = isEdit ? "모델 편집" : "모델 추가";
@@ -185,6 +188,50 @@ internal sealed class ModelRegistrationDialog : Window
var modelBorder = new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _modelBox };
stack.Children.Add(modelBorder);
stack.Children.Add(new TextBlock
{
Text = "실행 프로파일",
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
Margin = new Thickness(0, 12, 0, 6),
});
stack.Children.Add(new TextBlock
{
Text = "모델 성향에 따라 도구 호출 강도, 읽기 속도, 문서 생성 흐름을 조절합니다.",
FontSize = 11,
Foreground = secondaryText,
Margin = new Thickness(0, 0, 0, 8),
});
_executionProfileBox = new ComboBox
{
FontSize = 13,
Padding = new Thickness(8, 6, 8, 6),
Foreground = primaryText,
Background = itemBg,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
};
var balancedProfile = new ComboBoxItem { Content = "균형", Tag = "balanced" };
var strictProfile = new ComboBoxItem { Content = "도구 호출 우선", Tag = "tool_call_strict" };
var reasoningProfile = new ComboBoxItem { Content = "추론 우선", Tag = "reasoning_first" };
var readonlyProfile = new ComboBoxItem { Content = "읽기 속도 우선", Tag = "fast_readonly" };
var documentProfile = new ComboBoxItem { Content = "문서 생성 우선", Tag = "document_heavy" };
_executionProfileBox.Items.Add(balancedProfile);
_executionProfileBox.Items.Add(strictProfile);
_executionProfileBox.Items.Add(reasoningProfile);
_executionProfileBox.Items.Add(readonlyProfile);
_executionProfileBox.Items.Add(documentProfile);
_executionProfileBox.SelectedItem = (existingExecutionProfile ?? "balanced").Trim().ToLowerInvariant() switch
{
"tool_call_strict" => strictProfile,
"reasoning_first" => reasoningProfile,
"fast_readonly" => readonlyProfile,
"document_heavy" => documentProfile,
_ => balancedProfile,
};
stack.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _executionProfileBox });
// 구분선
stack.Children.Add(new Rectangle
{

View File

@@ -1,6 +1,4 @@
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Threading;
@@ -28,7 +26,6 @@ public partial class ReminderPopupWindow : Window
// ─── 타이머 ───────────────────────────────────────────────────────────────
private readonly DispatcherTimer _timer;
private readonly EventHandler _tickHandler;
private readonly CancellationTokenSource _autoCloseCts = new();
private readonly DateTime _closeAtUtc;
private readonly int _displaySeconds;
@@ -74,17 +71,17 @@ public partial class ReminderPopupWindow : Window
};
// ── 타이머 ──
// DispatcherPriority.Normal: Background보다 높아 에이전트 루프 이벤트 홍수 때도 안정적으로 발화.
_tickHandler = (_, _) =>
{
var remainingSeconds = Math.Max(0, (_closeAtUtc - DateTime.UtcNow).TotalSeconds);
var remainingSeconds = Math.Max(0.0, (_closeAtUtc - DateTime.UtcNow).TotalSeconds);
CountdownBar.Value = remainingSeconds;
if (remainingSeconds <= 0)
if (remainingSeconds <= 0.0)
Close();
};
_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_timer = new DispatcherTimer(DispatcherPriority.Normal) { Interval = TimeSpan.FromMilliseconds(500) };
_timer.Tick += _tickHandler;
_timer.Start();
_ = StartAutoCloseAsync(_autoCloseCts.Token);
// ── Esc 키 닫기 ──
KeyDown += (_, e) =>
@@ -139,34 +136,10 @@ public partial class ReminderPopupWindow : Window
private void CloseBtn_Click(object sender, RoutedEventArgs e) => Close();
private async Task StartAutoCloseAsync(CancellationToken cancellationToken)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(_displaySeconds), cancellationToken);
if (cancellationToken.IsCancellationRequested)
return;
await Dispatcher.InvokeAsync(() =>
{
if (IsVisible)
Close();
}, DispatcherPriority.Background, cancellationToken);
}
catch (TaskCanceledException)
{
}
catch (OperationCanceledException)
{
}
}
protected override void OnClosed(EventArgs e)
{
_autoCloseCts.Cancel();
_timer.Stop();
_timer.Tick -= _tickHandler;
_autoCloseCts.Dispose();
base.OnClosed(e);
}
}

View File

@@ -894,6 +894,52 @@
</Grid>
</Border>
<!-- 런처 무지개 글로우 -->
<Border Style="{StaticResource SettingsRow}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Width="36" Height="36" CornerRadius="9"
Background="#7C3AED" Margin="0,0,14,0" VerticalAlignment="Center">
<TextBlock Text="&#xE753;" FontFamily="Segoe MDL2 Assets" FontSize="17"
Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="런처 무지개 글로우" Style="{StaticResource RowLabel}"/>
<TextBlock Text="AX Commander 테두리에 무지개 글로우를 표시합니다" Style="{StaticResource RowHint}"/>
</StackPanel>
<CheckBox x:Name="ChkLauncherRainbowGlow" Grid.Column="2"
Style="{StaticResource ToggleSwitch}"
HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Grid>
</Border>
<!-- 런처 선택 글로우 -->
<Border Style="{StaticResource SettingsRow}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Width="36" Height="36" CornerRadius="9"
Background="#5B4E7E" Margin="0,0,14,0" VerticalAlignment="Center">
<TextBlock Text="&#xE81C;" FontFamily="Segoe MDL2 Assets" FontSize="17"
Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="런처 선택 글로우" Style="{StaticResource RowLabel}"/>
<TextBlock Text="선택된 런처 항목에 은은한 글로우를 표시합니다" Style="{StaticResource RowHint}"/>
</StackPanel>
<CheckBox x:Name="ChkLauncherSelectionGlow" Grid.Column="2"
Style="{StaticResource ToggleSwitch}"
HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Grid>
</Border>
<!-- 언어 -->
<Border Style="{StaticResource SettingsRow}">
<Grid>
@@ -3892,6 +3938,12 @@
Style="{StaticResource AgentSubTabStyle}"
Margin="0,0,6,6"
Checked="AgentContextTokensCard_Checked"/>
<RadioButton x:Name="AgentContextTokens128K"
Content="128K"
GroupName="AgentContextTokens"
Style="{StaticResource AgentSubTabStyle}"
Margin="0,0,6,6"
Checked="AgentContextTokensCard_Checked"/>
<RadioButton x:Name="AgentContextTokens256K"
Content="256K"
GroupName="AgentContextTokens"

View File

@@ -169,11 +169,12 @@ public partial class SettingsWindow : Window
if (AgentOperationModeExternal != null) AgentOperationModeExternal.IsChecked = operationMode == OperationModePolicy.ExternalMode;
var maxContextTokens = _vm.LlmMaxContextTokens;
if (AgentContextTokens4K != null) AgentContextTokens4K.IsChecked = maxContextTokens <= 4096;
if (AgentContextTokens16K != null) AgentContextTokens16K.IsChecked = maxContextTokens > 4096 && maxContextTokens <= 16384;
if (AgentContextTokens64K != null) AgentContextTokens64K.IsChecked = maxContextTokens > 16384 && maxContextTokens <= 65536;
if (AgentContextTokens256K != null) AgentContextTokens256K.IsChecked = maxContextTokens > 65536 && maxContextTokens <= 262144;
if (AgentContextTokens1M != null) AgentContextTokens1M.IsChecked = maxContextTokens > 262144;
if (AgentContextTokens4K != null) AgentContextTokens4K.IsChecked = maxContextTokens <= 4_096;
if (AgentContextTokens16K != null) AgentContextTokens16K.IsChecked = maxContextTokens > 4_096 && maxContextTokens <= 16_384;
if (AgentContextTokens64K != null) AgentContextTokens64K.IsChecked = maxContextTokens > 16_384 && maxContextTokens <= 65_536;
if (AgentContextTokens128K != null) AgentContextTokens128K.IsChecked = maxContextTokens > 65_536 && maxContextTokens <= 131_072;
if (AgentContextTokens256K != null) AgentContextTokens256K.IsChecked = maxContextTokens > 131_072 && maxContextTokens <= 262_144;
if (AgentContextTokens1M != null) AgentContextTokens1M.IsChecked = maxContextTokens > 262_144;
var retentionDays = _vm.LlmRetentionDays;
if (AgentRetentionDays7 != null) AgentRetentionDays7.IsChecked = retentionDays == 7;
@@ -1170,6 +1171,14 @@ public partial class SettingsWindow : Window
ChkDockRainbowGlow.Checked += (_, _) => { launcher.DockBarRainbowGlow = true; svc.Save(); RefreshDock(); };
ChkDockRainbowGlow.Unchecked += (_, _) => { launcher.DockBarRainbowGlow = false; svc.Save(); RefreshDock(); };
ChkLauncherRainbowGlow.IsChecked = launcher.EnableRainbowGlow;
ChkLauncherRainbowGlow.Checked += (_, _) => { launcher.EnableRainbowGlow = true; svc.Save(); };
ChkLauncherRainbowGlow.Unchecked += (_, _) => { launcher.EnableRainbowGlow = false; svc.Save(); };
ChkLauncherSelectionGlow.IsChecked = launcher.EnableSelectionGlow;
ChkLauncherSelectionGlow.Checked += (_, _) => { launcher.EnableSelectionGlow = true; svc.Save(); };
ChkLauncherSelectionGlow.Unchecked += (_, _) => { launcher.EnableSelectionGlow = false; svc.Save(); };
SliderDockOpacity.Value = launcher.DockBarOpacity;
SliderDockOpacity.ValueChanged += (_, e) => { launcher.DockBarOpacity = e.NewValue; svc.Save(); RefreshDock(); };
@@ -1805,6 +1814,7 @@ public partial class SettingsWindow : Window
Alias = dlg.ModelAlias,
EncryptedModelName = Services.CryptoService.EncryptIfEnabled(dlg.ModelName, IsEncryptionEnabled),
Service = currentService,
ExecutionProfile = dlg.ExecutionProfile,
Endpoint = dlg.Endpoint,
ApiKey = dlg.ApiKey,
AllowInsecureTls = dlg.AllowInsecureTls,
@@ -1828,13 +1838,15 @@ public partial class SettingsWindow : Window
var currentService = GetCurrentServiceSubTab();
var dlg = new ModelRegistrationDialog(currentService, row.Alias, currentModel,
row.Endpoint, row.ApiKey, row.AllowInsecureTls,
row.AuthType ?? "bearer", row.Cp4dUrl ?? "", row.Cp4dUsername ?? "", cp4dPw);
row.AuthType ?? "bearer", row.Cp4dUrl ?? "", row.Cp4dUsername ?? "", cp4dPw,
row.ExecutionProfile ?? "balanced");
dlg.Owner = this;
if (dlg.ShowDialog() == true)
{
row.Alias = dlg.ModelAlias;
row.EncryptedModelName = Services.CryptoService.EncryptIfEnabled(dlg.ModelName, IsEncryptionEnabled);
row.Service = currentService;
row.ExecutionProfile = dlg.ExecutionProfile;
row.Endpoint = dlg.Endpoint;
row.ApiKey = dlg.ApiKey;
row.AllowInsecureTls = dlg.AllowInsecureTls;
@@ -2288,11 +2300,12 @@ public partial class SettingsWindow : Window
_vm.LlmMaxContextTokens = rb.Name switch
{
"AgentContextTokens16K" => 16384,
"AgentContextTokens64K" => 65536,
"AgentContextTokens256K" => 262144,
"AgentContextTokens1M" => 1_000_000,
_ => 4096,
"AgentContextTokens16K" => 16_384,
"AgentContextTokens64K" => 65_536,
"AgentContextTokens128K" => 131_072,
"AgentContextTokens256K" => 262_144,
"AgentContextTokens1M" => 1_000_000,
_ => 4_096,
};
}
@@ -2407,9 +2420,7 @@ public partial class SettingsWindow : Window
BorderBrush = isSelected
? (TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue)
: (TryFindResource("BorderColor") as Brush ?? Brushes.LightGray),
Background = isSelected
? (TryFindResource("ItemHoverBackground") as Brush ?? Brushes.AliceBlue)
: Brushes.Transparent,
Background = Brushes.Transparent, // 테마 배경색 대신 항상 투명 (다크 테마 흰 배경 방지)
Padding = new Thickness(12, 9, 12, 9),
Margin = new Thickness(0, 0, 8, 8),
Child = new StackPanel
@@ -2467,9 +2478,7 @@ public partial class SettingsWindow : Window
BorderBrush = isSelected
? (TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue)
: (TryFindResource("BorderColor") as Brush ?? Brushes.LightGray),
Background = isSelected
? (TryFindResource("ItemHoverBackground") as Brush ?? Brushes.AliceBlue)
: Brushes.Transparent,
Background = Brushes.Transparent, // 다크 테마 흰 배경 방지
Padding = new Thickness(12, 9, 12, 9),
Margin = new Thickness(0, 0, 8, 8),
Child = new StackPanel
@@ -4130,4 +4139,3 @@ public partial class SettingsWindow : Window
}

View File

@@ -13,29 +13,35 @@ namespace AxCopilot.Views;
/// </summary>
public partial class TrayMenuWindow : Window
{
private System.Windows.Threading.DispatcherTimer? _autoCloseTimer;
private readonly System.Windows.Threading.DispatcherTimer _autoCloseTimer;
private Size _cachedMenuSize;
private bool _isMenuSizeDirty = true;
private static readonly Brush HeaderTextBrush = new SolidColorBrush(Color.FromRgb(96, 96, 96));
public TrayMenuWindow()
{
InitializeComponent();
_autoCloseTimer = new System.Windows.Threading.DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(400)
};
_autoCloseTimer.Tick += (_, _) =>
{
_autoCloseTimer.Stop();
Hide();
};
// 마우스가 메뉴 밖으로 나가면 일정 시간 후 자동 닫힘
MouseLeave += (_, _) =>
{
_autoCloseTimer?.Stop();
_autoCloseTimer = new System.Windows.Threading.DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(400)
};
_autoCloseTimer.Tick += (_, _) => { _autoCloseTimer.Stop(); Hide(); };
_autoCloseTimer.Start();
};
// 마우스가 다시 메뉴 위로 오면 타이머 취소
MouseEnter += (_, _) =>
{
_autoCloseTimer?.Stop();
_autoCloseTimer.Stop();
};
}
@@ -53,6 +59,7 @@ public partial class TrayMenuWindow : Window
};
label.Foreground = HeaderTextBrush;
MenuPanel.Children.Add(label);
MarkMenuSizeDirty();
AddSeparator();
return this;
}
@@ -63,6 +70,7 @@ public partial class TrayMenuWindow : Window
var item = CreateItemBorder(glyph, text);
item.MouseLeftButtonUp += (_, _) => { Hide(); onClick(); };
MenuPanel.Children.Add(item);
MarkMenuSizeDirty();
return this;
}
@@ -73,6 +81,7 @@ public partial class TrayMenuWindow : Window
item.MouseLeftButtonUp += (_, _) => { Hide(); onClick(); };
MenuPanel.Children.Add(item);
itemRef = item;
MarkMenuSizeDirty();
return this;
}
@@ -145,6 +154,7 @@ public partial class TrayMenuWindow : Window
getChecked = () => isChecked;
setText = t => label.Text = t;
MenuPanel.Children.Add(border);
MarkMenuSizeDirty();
return this;
}
@@ -158,27 +168,29 @@ public partial class TrayMenuWindow : Window
};
sep.SetResourceReference(Border.BackgroundProperty, "SeparatorColor");
MenuPanel.Children.Add(sep);
MarkMenuSizeDirty();
return this;
}
// ─── 팝업 표시 ────────────────────────────────────────────────────────
/// <summary>표시 전에 메뉴 크기를 미리 계산해 첫 우클릭 지연을 줄입니다.</summary>
public void PrepareForDisplay()
{
EnsureMenuSize();
}
/// <summary>트레이 아이콘 근처에 메뉴를 표시합니다.</summary>
public void ShowAtTray()
{
var workArea = SystemParameters.WorkArea;
// 먼저 표시하여 ActualWidth/ActualHeight 확정
Opacity = 0;
Show();
UpdateLayout();
double menuW = ActualWidth;
double menuH = ActualHeight;
var menuSize = EnsureMenuSize();
double menuW = menuSize.Width;
double menuH = menuSize.Height;
// 마우스 커서 위치 (물리 픽셀 → WPF 논리 좌표)
var cursorPos = GetCursorPosition();
double dpiScale = VisualTreeHelper.GetDpi(this).PixelsPerDip;
double dpiScale = VisualTreeHelper.GetDpi(this).DpiScaleX;
double cx = cursorPos.X / dpiScale;
double cy = cursorPos.Y / dpiScale;
@@ -194,6 +206,12 @@ public partial class TrayMenuWindow : Window
Left = left;
Top = top;
Opacity = 0;
if (!IsVisible)
{
Show();
}
// 활성화하여 Deactivated 이벤트가 정상 작동하도록 보장
Activate();
@@ -213,6 +231,8 @@ public partial class TrayMenuWindow : Window
public void ShowWithUpdate()
{
Opening?.Invoke();
MarkMenuSizeDirty();
PrepareForDisplay();
ShowAtTray();
}
@@ -273,6 +293,28 @@ public partial class TrayMenuWindow : Window
Hide();
}
private void MarkMenuSizeDirty()
{
_isMenuSizeDirty = true;
}
private Size EnsureMenuSize()
{
if (!_isMenuSizeDirty && _cachedMenuSize.Width > 0 && _cachedMenuSize.Height > 0)
{
return _cachedMenuSize;
}
RootBorder.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
var desired = RootBorder.DesiredSize;
RootBorder.Arrange(new Rect(new Point(0, 0), desired));
RootBorder.UpdateLayout();
_cachedMenuSize = desired;
_isMenuSizeDirty = false;
return desired;
}
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetCursorPos(out POINT lpPoint);

View File

@@ -16,7 +16,7 @@ namespace AxCopilot.Views;
/// </summary>
public partial class WorkflowAnalyzerWindow : Window
{
private readonly DateTime _startTime = DateTime.Now;
private DateTime _startTime = DateTime.Now;
private int _totalToolCalls;
private int _totalInputTokens;
private int _totalOutputTokens;
@@ -141,6 +141,7 @@ public partial class WorkflowAnalyzerWindow : Window
_toolTimeAccum.Clear();
_tokenTrend.Clear();
_lastEventMs = 0;
_startTime = DateTime.Now;
UpdateSummaryCards();
ClearBottleneckCharts();
StatusText.Text = "대기 중...";