diff --git a/README.md b/README.md
index efab616..878d88b 100644
--- a/README.md
+++ b/README.md
@@ -726,6 +726,11 @@ ow + toggle 시각 언어로 통일했습니다.
- 런처 보조 기능/설정 연결을 `Agent Compare` 기준으로 다시 대조하면서, 입력을 비웠을 때 이전 선택 항목과 미리보기 패널이 남아 있던 상태를 정리했습니다. [LauncherViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/LauncherViewModel.cs) 에서 빈 입력 시 `SelectedItem`과 미리보기 바인딩이 같이 초기화되도록 맞춰, 빠른 실행 칩/검색 히스토리/미리보기 패널 전환이 더 이상 이전 검색 상태를 끌고 가지 않게 했습니다.
- 검증: `Agent Compare`의 런처 설정 항목(`ShowNumberBadges`, `CloseOnFocusLost`, `RememberPosition`, `EnableActionMode`, `EnableRandomPlaceholder`, `ShowLauncherBorder` 등)과 현재 [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml), [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [LauncherWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/LauncherWindow.xaml.cs), [LauncherViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/LauncherViewModel.cs) 연결을 재검토했고, 런처 테마 동일화 작업은 제외한 상태에서 보조 기능/설정 연결 위주로 1차 마무리했습니다.
- 업데이트: 2026-04-05 11:56 (KST)
+- AX Agent 채팅 엔진 정상화 1차로, [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs) 와 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 전송 흐름을 `준비 → 실행 → 최종 assistant 커밋 → 재렌더` 중심으로 다시 정리했습니다. Chat/Cowork/Code 공통으로 임시 assistant 카드와 임시 스트리밍 컨테이너를 먼저 만들지 않도록 바꿔, 토큰은 올라가는데 채팅 본문이 비거나 빈 버블이 남는 증상을 줄였습니다.
+- 같은 수정에서 에이전트 실행 로그도 화면에 즉시 배너를 직접 꽂지 않고, 대화 모델의 `ExecutionEvents`에 먼저 쌓은 뒤 `RenderMessages()` 기준으로만 다시 그리게 바꿨습니다. 그래서 Cowork/Code에서 실행 로그 문구가 플래시처럼 잔상으로 남거나 중복 표시되던 흐름을 줄이는 쪽으로 정리했습니다.
+- 재생성 경로도 동일하게 정리해서, 피드백 후 재생성 시 빈 assistant 메시지를 먼저 추가하지 않고 최종 응답만 커밋하도록 맞췄습니다.
+- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
+- 업데이트: 2026-04-05 12:06 (KST)
---
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index a57b010..b7e5780 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -4475,3 +4475,8 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
- 업데이트: 2026-04-05 11:56 (KST)
- `Agent Compare` 기준으로 런처 보조 기능/설정 연결을 다시 대조하면서, 입력을 지웠을 때 이전 선택 항목과 미리보기 상태가 남아 빠른 실행 칩·검색 히스토리·미리보기 패널 전환이 어색해지던 흐름을 정리했습니다. [LauncherViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/LauncherViewModel.cs) 는 빈 입력과 런처 재오픈 시 `SelectedItem`을 함께 초기화해, 현재 쿼리와 무관한 미리보기/선택 상태가 잔류하지 않게 했습니다.
- 설정 연결 검증도 같이 진행했습니다. `Agent Compare`의 런처 설정 항목(`ShowNumberBadges`, `CloseOnFocusLost`, `RememberPosition`, `EnableActionMode`, `EnableRandomPlaceholder`, `EnableIconAnimation`, `EnableSelectionGlow`, `ShowLauncherBorder`, `EnableFavorites`, `EnableRecent`, `ShowPrefixBadge`)을 현재 [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml), [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [LauncherWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/LauncherWindow.xaml.cs), [LauncherViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/LauncherViewModel.cs), [CommandResolver.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Core/CommandResolver.cs), [SnippetExpander.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Core/SnippetExpander.cs), [WebSearchHandler.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Handlers/WebSearchHandler.cs), [App.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/App.xaml.cs), [ClipboardHistoryService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/ClipboardHistoryService.cs) 기준으로 다시 점검했고, 테마 동일화는 제외한 채 보조 기능 디테일과 설정 반영 경로 위주로 마감 기준을 맞췄습니다.
+- 업데이트: 2026-04-05 12:06 (KST)
+- AX Agent 채팅 엔진 정상화 1차에서 [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs)의 Chat 실행 모드를 `최종 응답 커밋형`으로 고정하고, [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `SendMessageAsync()` / `SendRegenerateAsync()`가 임시 assistant 메시지, 임시 스트리밍 컨테이너, 직접 UI 버블 삽입에 의존하지 않도록 다시 정리했습니다.
+- 현재 흐름은 `사용자 메시지 저장 → 전송 메시지 준비 → LLM 또는 AgentLoop 실행 → 최종 assistant 텍스트 확정 → conversation/session에 커밋 → RenderMessages()` 순서입니다. 이로써 토큰은 집계되지만 본문이 비거나, assistant 빈 버블이 남거나, 재생성 시 메시지 모델과 화면 상태가 어긋나는 증상을 줄이는 쪽으로 맞췄습니다.
+- `OnAgentEvent(...)`도 직접 `AddAgentEventBanner()`를 바로 꽂는 방식을 중단하고, `AppendConversationExecutionEvent()` 후 `ShowExecutionHistory`가 켜진 경우에만 `RenderMessages(preserveViewport: true)`를 다시 타게 바꿨습니다. 이 수정은 Cowork/Code에서 실행 로그가 플래시처럼 남거나 중복 잔상으로 보이던 문제를 줄이기 위한 것입니다.
+- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
diff --git a/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs b/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs
new file mode 100644
index 0000000..0638697
--- /dev/null
+++ b/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs
@@ -0,0 +1,128 @@
+using AxCopilot.Models;
+using AxCopilot.Services;
+
+namespace AxCopilot.Services.Agent;
+
+///
+/// AX Agent execution-prep engine.
+/// Inspired by the `claw-code` split between input preparation and session execution,
+/// so the UI layer stops owning message assembly and final assistant commit logic.
+///
+public sealed class AxAgentExecutionEngine
+{
+ public sealed record PreparedTurn(List Messages);
+ public sealed record ExecutionMode(bool UseAgentLoop, bool UseStreamingTransport, string? TaskSystemPrompt);
+
+ public IReadOnlyList BuildPromptStack(
+ string? conversationSystem,
+ string? slashSystem,
+ string? taskSystem = null)
+ {
+ var prompts = new List();
+ if (!string.IsNullOrWhiteSpace(conversationSystem))
+ prompts.Add(conversationSystem.Trim());
+ if (!string.IsNullOrWhiteSpace(slashSystem))
+ prompts.Add(slashSystem.Trim());
+ if (!string.IsNullOrWhiteSpace(taskSystem))
+ prompts.Add(taskSystem.Trim());
+ return prompts;
+ }
+
+ public ExecutionMode ResolveExecutionMode(
+ string runTab,
+ bool streamingEnabled,
+ string resolvedService,
+ string? coworkSystemPrompt,
+ string? codeSystemPrompt)
+ {
+ if (string.Equals(runTab, "Cowork", StringComparison.OrdinalIgnoreCase))
+ return new ExecutionMode(true, false, coworkSystemPrompt);
+
+ if (string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase))
+ return new ExecutionMode(true, false, codeSystemPrompt);
+
+ return new ExecutionMode(false, false, null);
+ }
+
+ public PreparedTurn PrepareTurn(
+ ChatConversation conversation,
+ IEnumerable systemPrompts,
+ string? fileContext = null,
+ IReadOnlyList? images = null)
+ {
+ var outbound = conversation.Messages
+ .Select(CloneMessage)
+ .ToList();
+
+ if (!string.IsNullOrWhiteSpace(fileContext))
+ {
+ var lastUserIndex = outbound.FindLastIndex(m => string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase));
+ if (lastUserIndex >= 0)
+ outbound[lastUserIndex].Content = (outbound[lastUserIndex].Content ?? string.Empty) + fileContext;
+ }
+
+ if (images is { Count: > 0 })
+ {
+ var lastUserIndex = outbound.FindLastIndex(m => string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase));
+ if (lastUserIndex >= 0)
+ outbound[lastUserIndex].Images = images.Select(CloneImage).ToList();
+ }
+
+ var promptList = systemPrompts
+ .Where(prompt => !string.IsNullOrWhiteSpace(prompt))
+ .Select(prompt => prompt!.Trim())
+ .ToList();
+
+ for (var i = promptList.Count - 1; i >= 0; i--)
+ outbound.Insert(0, new ChatMessage { Role = "system", Content = promptList[i] });
+
+ return new PreparedTurn(outbound);
+ }
+
+ public ChatMessage CommitAssistantMessage(
+ ChatSessionStateService? session,
+ ChatConversation conversation,
+ string tab,
+ string content,
+ ChatStorageService? storage = null)
+ {
+ var assistant = new ChatMessage
+ {
+ Role = "assistant",
+ Content = content,
+ };
+
+ if (session != null)
+ {
+ session.AppendMessage(tab, assistant, storage);
+ return assistant;
+ }
+
+ conversation.Messages.Add(assistant);
+ conversation.UpdatedAt = DateTime.Now;
+ return assistant;
+ }
+
+ private static ChatMessage CloneMessage(ChatMessage source)
+ {
+ return new ChatMessage
+ {
+ Role = source.Role,
+ Content = source.Content,
+ Timestamp = source.Timestamp,
+ MetaRunId = source.MetaRunId,
+ AttachedFiles = source.AttachedFiles?.ToList(),
+ Images = source.Images?.Select(CloneImage).ToList(),
+ };
+ }
+
+ private static ImageAttachment CloneImage(ImageAttachment source)
+ {
+ return new ImageAttachment
+ {
+ Base64 = source.Base64,
+ MimeType = source.MimeType,
+ FileName = source.FileName,
+ };
+ }
+}
diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs
index 694ec8f..15434ea 100644
--- a/src/AxCopilot/Views/ChatWindow.xaml.cs
+++ b/src/AxCopilot/Views/ChatWindow.xaml.cs
@@ -17,12 +17,14 @@ namespace AxCopilot.Views;
public partial class ChatWindow : Window
{
private const string UnifiedAdminPassword = "axgo123!";
+ private bool IsOverlayEncryptionEnabled => _settings.Settings.Llm.EncryptionEnabled;
private readonly SettingsService _settings;
private readonly ChatStorageService _storage;
private readonly DraftQueueProcessorService _draftQueueProcessor = new();
private readonly LlmService _llm;
private readonly ToolRegistry _toolRegistry;
private readonly AgentLoopService _agentLoop;
+ private readonly AxAgentExecutionEngine _chatEngine;
private readonly ModelRouterService _router;
private AppStateService _appState => (System.Windows.Application.Current as App)?.AppState ?? new AppStateService();
private readonly object _convLock = new();
@@ -45,6 +47,7 @@ public partial class ChatWindow : Window
["Cowork"] = true,
["Code"] = true,
};
+ private readonly Dictionary _overlaySectionExpandedStates = new(StringComparer.OrdinalIgnoreCase);
private bool _failedOnlyFilter;
private bool _runningOnlyFilter;
private int _failedConversationCount;
@@ -78,6 +81,8 @@ public partial class ChatWindow : Window
// 타이핑 효과
private readonly DispatcherTimer _typingTimer;
private readonly DispatcherTimer _gitRefreshTimer;
+ private readonly DispatcherTimer _conversationSearchTimer;
+ private readonly DispatcherTimer _inputUiRefreshTimer;
private CancellationTokenSource? _gitStatusRefreshCts;
private int _displayedLength; // 현재 화면에 표시된 글자 수
private ResourceDictionary? _agentThemeDictionary;
@@ -156,6 +161,7 @@ public partial class ChatWindow : Window
_llm = new LlmService(settings);
_router = new ModelRouterService(settings);
_toolRegistry = ToolRegistry.CreateDefault();
+ _chatEngine = new AxAgentExecutionEngine();
_agentLoop = new AgentLoopService(_llm, _toolRegistry, settings)
{
Dispatcher = action =>
@@ -214,6 +220,19 @@ public partial class ChatWindow : Window
_gitRefreshTimer.Stop();
await RefreshGitBranchStatusAsync();
};
+ _conversationSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(140) };
+ _conversationSearchTimer.Tick += (_, _) =>
+ {
+ _conversationSearchTimer.Stop();
+ RefreshConversationList();
+ };
+ _inputUiRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(90) };
+ _inputUiRefreshTimer.Tick += (_, _) =>
+ {
+ _inputUiRefreshTimer.Stop();
+ RefreshContextUsageVisual();
+ RefreshDraftQueueUi();
+ };
KeyDown += ChatWindow_KeyDown;
UpdateConversationFailureFilterUi();
@@ -233,6 +252,8 @@ public partial class ChatWindow : Window
ApplyExpressionLevelUi();
UpdateSidebarModeMenu();
RefreshContextUsageVisual();
+ UpdateTopicPresetScrollMode();
+ UpdateInputBoxHeight();
InputBox.Focus();
MessageScroll.ScrollChanged += MessageScroll_ScrollChanged;
@@ -313,6 +334,7 @@ public partial class ChatWindow : Window
ApplyHoverScaleAnimation(BtnSend, 1.12);
ApplyHoverScaleAnimation(BtnStop, 1.12);
};
+ SizeChanged += (_, _) => UpdateTopicPresetScrollMode();
Closed += (_, _) =>
{
_settings.SettingsChanged -= Settings_SettingsChanged;
@@ -321,6 +343,8 @@ public partial class ChatWindow : Window
_cursorTimer.Stop();
_elapsedTimer.Stop();
_typingTimer.Stop();
+ _conversationSearchTimer.Stop();
+ _inputUiRefreshTimer.Stop();
_llm.Dispose();
};
}
@@ -691,7 +715,8 @@ public partial class ChatWindow : Window
// 전체 보기
var allLabel = _activeTab switch
{
- "Cowork" or "Code" => "모든 프로젝트",
+ "Cowork" => "모든 작업 유형",
+ "Code" => "모든 프로젝트",
_ => "모든 주제",
};
stack.Children.Add(CreateCatItem("\uE8BD", allLabel, secondaryText,
@@ -700,9 +725,32 @@ public partial class ChatWindow : Window
stack.Children.Add(CreateSep());
- if (_activeTab == "Cowork" || _activeTab == "Code")
+ if (_activeTab == "Cowork")
{
- // 코워크/코드: 워크스페이스 기반 필터
+ // 코워크: 작업 유형 기반 필터
+ var presets = Services.PresetService.GetByTabWithCustom("Cowork", _settings.Settings.Llm.CustomPresets);
+ var categoryItems = presets
+ .Select(p => (Category: p.Category, Label: p.Label, Symbol: p.Symbol, Color: p.Color))
+ .Concat(_storage.LoadAllMeta()
+ .Where(c => string.Equals(NormalizeTabName(c.Tab), "Cowork", StringComparison.OrdinalIgnoreCase))
+ .Select(c => (Category: c.Category?.Trim() ?? "", Label: c.Category?.Trim() ?? "", Symbol: "\uE8BD", Color: "#6B7280")))
+ .Where(item => !string.IsNullOrWhiteSpace(item.Category))
+ .DistinctBy(item => item.Category, StringComparer.OrdinalIgnoreCase)
+ .OrderBy(item => item.Label, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ foreach (var categoryItem in categoryItems)
+ {
+ var capturedCategory = categoryItem.Category;
+ var brush = BrushFromHex(categoryItem.Color);
+ stack.Children.Add(CreateCatItem(categoryItem.Symbol, categoryItem.Label, brush,
+ string.Equals(_selectedCategory, capturedCategory, StringComparison.OrdinalIgnoreCase),
+ () => { _selectedCategory = capturedCategory; UpdateCategoryLabel(); RefreshConversationList(); }));
+ }
+ }
+ else if (_activeTab == "Code")
+ {
+ // 코드: 워크스페이스 기반 필터
var workspaces = _storage.LoadAllMeta()
.Where(c => string.Equals(NormalizeTabName(c.Tab), _activeTab, StringComparison.OrdinalIgnoreCase))
.Select(c => c.WorkFolder?.Trim() ?? "")
@@ -756,15 +804,27 @@ public partial class ChatWindow : Window
{
if (string.IsNullOrEmpty(_selectedCategory))
{
- CategoryLabel.Text = _activeTab switch { "Cowork" or "Code" => "모든 프로젝트", _ => "주제 선택" };
+ CategoryLabel.Text = _activeTab switch
+ {
+ "Cowork" => "모든 작업 유형",
+ "Code" => "모든 프로젝트",
+ _ => "주제 선택",
+ };
CategoryIcon.Text = "\uE8BD";
}
- else if (_activeTab == "Cowork" || _activeTab == "Code")
+ else if (_activeTab == "Code")
{
var displayName = System.IO.Path.GetFileName(_selectedCategory.TrimEnd(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar));
CategoryLabel.Text = string.IsNullOrWhiteSpace(displayName) ? _selectedCategory : displayName;
CategoryIcon.Text = "\uE8B7";
}
+ else if (_activeTab == "Cowork")
+ {
+ var preset = Services.PresetService.GetByTabWithCustom("Cowork", _settings.Settings.Llm.CustomPresets)
+ .FirstOrDefault(p => string.Equals(p.Category, _selectedCategory, StringComparison.OrdinalIgnoreCase));
+ CategoryLabel.Text = preset?.Label ?? _selectedCategory;
+ CategoryIcon.Text = preset?.Symbol ?? "\uE8BD";
+ }
else if (_selectedCategory == "__custom__")
{
CategoryLabel.Text = "커스텀 프리셋";
@@ -1181,10 +1241,11 @@ public partial class ChatWindow : Window
InputBox.FontSize = level == "simple" ? 13 : 14;
InputBox.MaxHeight = level switch
{
- "rich" => 220,
- "simple" => 120,
- _ => 160,
+ "rich" => 120,
+ "simple" => 96,
+ _ => 108,
};
+ UpdateInputBoxHeight();
}
if (InputWatermark != null)
@@ -1765,14 +1826,28 @@ public partial class ChatWindow : Window
if (conv == null) return;
try
{
- conv.Permission = PermissionModeCatalog.NormalizeGlobalMode(_settings.Settings.Llm.FilePermission);
- conv.DataUsage = _folderDataUsage;
- conv.Mood = _selectedMood;
+ var normalizedPermission = PermissionModeCatalog.NormalizeGlobalMode(_settings.Settings.Llm.FilePermission);
+ var dataUsage = _folderDataUsage ?? "none";
+ var mood = _selectedMood ?? (_settings.Settings.Llm.DefaultMood ?? "modern");
+ var outputFormat = conv.OutputFormat ?? "auto";
+
+ var unchanged =
+ string.Equals(conv.Permission ?? "", normalizedPermission, StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(conv.DataUsage ?? "", dataUsage, StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(conv.Mood ?? "", mood, StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(conv.OutputFormat ?? "auto", outputFormat, StringComparison.OrdinalIgnoreCase);
+
+ if (unchanged)
+ return;
+
+ conv.Permission = normalizedPermission;
+ conv.DataUsage = dataUsage;
+ conv.Mood = mood;
var session = ChatSession;
if (session != null)
{
lock (_convLock)
- _currentConversation = session.SaveConversationSettings(_activeTab, _settings.Settings.Llm.FilePermission, _folderDataUsage, conv.OutputFormat, _selectedMood, _storage);
+ _currentConversation = session.SaveConversationSettings(_activeTab, normalizedPermission, dataUsage, outputFormat, mood, _storage);
}
else
{
@@ -2515,8 +2590,8 @@ public partial class ChatWindow : Window
if (BtnDataUsage != null)
{
BtnDataUsage.Background = Brushes.Transparent;
- BtnDataUsage.BorderBrush = BrushFromHex($"{color}66");
- BtnDataUsage.BorderThickness = new Thickness(1);
+ BtnDataUsage.BorderBrush = Brushes.Transparent;
+ BtnDataUsage.BorderThickness = new Thickness(0);
BtnDataUsage.ToolTip = _folderDataUsage switch
{
"none" => "폴더 문서와 파일을 읽지 않거나 참조하지 않습니다.",
@@ -2757,7 +2832,6 @@ public partial class ChatWindow : Window
IconBarColumn.Width = new GridLength(0);
IconBarPanel.Visibility = Visibility.Collapsed;
SidebarPanel.Visibility = Visibility.Visible;
- ToggleSidebarIcon.Text = "\uE76B";
if (animated)
{
@@ -2772,7 +2846,6 @@ public partial class ChatWindow : Window
}
SidebarColumn.MinWidth = 0;
- ToggleSidebarIcon.Text = "\uE76C";
if (animated)
{
AnimateSidebar(270, 0, () =>
@@ -2888,7 +2961,14 @@ public partial class ChatWindow : Window
UpdateConversationQuickStripUi();
// 상단 필터 적용
- if (_activeTab == "Cowork" || _activeTab == "Code")
+ if (_activeTab == "Cowork")
+ {
+ if (!string.IsNullOrEmpty(_selectedCategory))
+ {
+ items = items.Where(i => string.Equals(i.Category, _selectedCategory, StringComparison.OrdinalIgnoreCase)).ToList();
+ }
+ }
+ else if (_activeTab == "Code")
{
if (!string.IsNullOrEmpty(_selectedCategory))
{
@@ -3895,7 +3975,8 @@ public partial class ChatWindow : Window
private void SearchBox_TextChanged(object sender, TextChangedEventArgs e)
{
- RefreshConversationList();
+ _conversationSearchTimer.Stop();
+ _conversationSearchTimer.Start();
}
private void BtnFailedOnlyFilter_Click(object sender, RoutedEventArgs e) { }
@@ -4018,7 +4099,17 @@ public partial class ChatWindow : Window
lock (_convLock) conv = _currentConversation;
_appState.RestoreAgentRunHistory(conv?.AgentRunHistory);
- var visibleMessages = conv?.Messages?.Where(msg => msg.Role != "system").ToList() ?? new List();
+ var visibleMessages = conv?.Messages?.Where(msg =>
+ {
+ if (string.Equals(msg.Role, "system", StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ if (string.Equals(msg.Role, "assistant", StringComparison.OrdinalIgnoreCase)
+ && string.IsNullOrWhiteSpace(msg.Content))
+ return false;
+
+ return true;
+ }).ToList() ?? new List();
var visibleEvents = (conv?.ShowExecutionHistory ?? true)
? conv?.ExecutionEvents?.ToList() ?? new List()
: new List();
@@ -5573,7 +5664,8 @@ public partial class ChatWindow : Window
private void InputBox_TextChanged(object sender, TextChangedEventArgs e)
{
UpdateWatermarkVisibility();
- RefreshContextUsageVisual();
+ UpdateInputBoxHeight();
+ ScheduleInputUiRefresh();
var text = InputBox.Text;
// 칩이 활성화된 상태에서 사용자가 /를 타이핑하면 칩 해제
@@ -5606,12 +5698,87 @@ public partial class ChatWindow : Window
_slashPalette.SelectedIndex = GetFirstVisibleSlashIndex(matches);
RenderSlashPage();
SlashPopup.IsOpen = true;
- RefreshDraftQueueUi();
+ ScheduleInputUiRefresh();
return;
}
}
SlashPopup.IsOpen = false;
- RefreshDraftQueueUi();
+ ScheduleInputUiRefresh();
+ }
+
+ private void UpdateInputBoxHeight()
+ {
+ if (InputBox == null)
+ return;
+
+ var text = InputBox.Text ?? string.Empty;
+ var explicitLineCount = 1 + text.Count(ch => ch == '\n');
+ var displayMode = (_settings.Settings.Llm.AgentUiExpressionLevel ?? "balanced").Trim().ToLowerInvariant();
+ if (displayMode is not ("rich" or "balanced" or "simple"))
+ displayMode = "balanced";
+
+ var maxLines = displayMode switch
+ {
+ "rich" => 6,
+ "simple" => 4,
+ _ => 5,
+ };
+
+ InputBox.MinLines = 1;
+ InputBox.MaxLines = maxLines;
+ InputBox.SetCurrentValue(TextBox.HeightProperty, double.NaN);
+ InputBox.VerticalScrollBarVisibility = explicitLineCount > maxLines
+ ? ScrollBarVisibility.Auto
+ : ScrollBarVisibility.Disabled;
+ }
+
+ private static string BuildAssistantFallbackContent(ChatConversation conv, string runTab, string? content)
+ {
+ if (!string.IsNullOrWhiteSpace(content))
+ return content;
+
+ var latestEventSummary = conv.ExecutionEvents?
+ .Where(evt => !string.IsNullOrWhiteSpace(evt.Summary))
+ .OrderByDescending(evt => evt.Timestamp)
+ .Select(evt => evt.Summary.Trim())
+ .FirstOrDefault();
+
+ if (!string.IsNullOrWhiteSpace(latestEventSummary))
+ {
+ return runTab switch
+ {
+ "Cowork" => $"코워크 작업이 완료되었습니다.\n\n{latestEventSummary}",
+ "Code" => $"코드 작업이 완료되었습니다.\n\n{latestEventSummary}",
+ _ => latestEventSummary,
+ };
+ }
+
+ return "(빈 응답)";
+ }
+
+ private static void SyncLatestAssistantMessage(ChatConversation conv, string content)
+ {
+ if (conv.Messages.Count == 0)
+ return;
+
+ for (var i = conv.Messages.Count - 1; i >= 0; i--)
+ {
+ var message = conv.Messages[i];
+ if (!string.Equals(message.Role, "assistant", StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ message.Content = content;
+ return;
+ }
+ }
+
+ private void ScheduleInputUiRefresh()
+ {
+ if (_inputUiRefreshTimer == null)
+ return;
+
+ _inputUiRefreshTimer.Stop();
+ _inputUiRefreshTimer.Start();
}
private static string BuildSlashSkillLabel(SkillDefinition skill)
@@ -6673,6 +6840,8 @@ public partial class ChatWindow : Window
if (_currentConversation == null)
_currentConversation = ChatSession?.EnsureCurrentConversation(runTab) ?? new ChatConversation { Tab = runTab };
conv = _currentConversation;
+ if (runTab is "Cowork" or "Code")
+ conv.ShowExecutionHistory = false;
}
var userMsg = new ChatMessage { Role = "user", Content = commandText };
@@ -8176,9 +8345,10 @@ public partial class ChatWindow : Window
TryPersistConversation(force: true);
UpdateChatTitle();
- AddMessageBubble("user", text);
InputBox.Text = "";
+ UpdateInputBoxHeight();
EmptyState.Visibility = Visibility.Collapsed;
+ RenderMessages(preserveViewport: true);
// 대화 통계 기록
Services.UsageStatisticsService.RecordChat(runTab);
@@ -8195,36 +8365,12 @@ public partial class ChatWindow : Window
BtnPause.Visibility = Visibility.Visible;
_streamCts = new CancellationTokenSource();
- var assistantMsg = new ChatMessage { Role = "assistant", Content = "" };
- lock (_convLock)
- {
- var session = ChatSession;
- if (session != null)
- {
- session.AppendMessage(runTab, assistantMsg);
- _currentConversation = session.CurrentConversation;
- conv = _currentConversation!;
- }
- else
- {
- conv.Messages.Add(assistantMsg);
- }
- }
- TryPersistConversation(force: true);
-
- // 어시스턴트 스트리밍 컨테이너
- var streamContainer = CreateStreamingContainer(out var streamText);
- MessagePanel.Children.Add(streamContainer);
- ForceScrollToEnd(); // 응답 시작 시 강제 하단 이동
-
- var sb = new System.Text.StringBuilder();
- _activeStreamText = streamText;
+ var assistantContent = string.Empty;
+ _activeStreamText = null;
_cachedStreamContent = "";
_displayedLength = 0;
_cursorVisible = true;
_aiIconPulseStopped = false;
- _cursorTimer.Start();
- _typingTimer.Start();
_streamStartTime = DateTime.UtcNow;
_elapsedTimer.Start();
SetStatus("응답 생성 중...", spinning: true);
@@ -8235,50 +8381,54 @@ public partial class ChatWindow : Window
try
{
List sendMessages;
- lock (_convLock) sendMessages = conv.Messages.SkipLast(1).ToList();
-
- // 시스템 명령어가 있으면 삽입
- if (!string.IsNullOrEmpty(conv.SystemCommand))
- sendMessages.Insert(0, new ChatMessage { Role = "system", Content = conv.SystemCommand });
-
- // 슬래시 명령어 시스템 프롬프트 삽입
- if (!string.IsNullOrEmpty(slashSystem))
- sendMessages.Insert(0, new ChatMessage { Role = "system", Content = slashSystem });
// 첨부 파일 컨텍스트 삽입
+ string? fileContext = null;
if (_attachedFiles.Count > 0)
{
- var fileContext = BuildFileContextPrompt();
+ fileContext = BuildFileContextPrompt();
if (!string.IsNullOrEmpty(fileContext))
{
- var lastUserIdx = sendMessages.FindLastIndex(m => m.Role == "user");
- if (lastUserIdx >= 0)
- sendMessages[lastUserIdx] = new ChatMessage { Role = "user", Content = sendMessages[lastUserIdx].Content + fileContext };
+ // 전송용 메시지에는 엔진이 적용하고, 원본 대화에는 첨부 기록만 남김
}
- // 첨부 파일 목록 기록 후 항상 정리 (파일 읽기 실패해도)
userMsg.AttachedFiles = _attachedFiles.ToList();
_attachedFiles.Clear();
RefreshAttachedFilesUI();
}
// ── 이미지 첨부 ──
+ List? outboundImages = null;
if (_pendingImages.Count > 0)
{
userMsg.Images = _pendingImages.ToList();
- // 마지막 사용자 메시지에 이미지 데이터 연결
- var lastUserIdx = sendMessages.FindLastIndex(m => m.Role == "user");
- if (lastUserIdx >= 0)
- sendMessages[lastUserIdx] = new ChatMessage
- {
- Role = "user",
- Content = sendMessages[lastUserIdx].Content,
- Images = _pendingImages.ToList(),
- };
+ outboundImages = _pendingImages.ToList();
_pendingImages.Clear();
AttachedFilesPanel.Items.Clear();
if (_attachedFiles.Count == 0) AttachedFilesPanel.Visibility = Visibility.Collapsed;
}
+ var coworkSystem = string.Equals(runTab, "Cowork", StringComparison.OrdinalIgnoreCase)
+ ? BuildCoworkSystemPrompt()
+ : null;
+ var codeSystem = string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase)
+ ? BuildCodeSystemPrompt()
+ : null;
+ var (resolvedService, _) = _llm.GetCurrentModelInfo();
+ var executionMode = _chatEngine.ResolveExecutionMode(
+ runTab,
+ _settings.Settings.Llm.Streaming,
+ resolvedService,
+ coworkSystem,
+ codeSystem);
+ var promptStack = _chatEngine.BuildPromptStack(
+ conv.SystemCommand,
+ slashSystem,
+ executionMode.TaskSystemPrompt);
+
+ sendMessages = _chatEngine
+ .PrepareTurn(conv, promptStack, fileContext, outboundImages)
+ .Messages;
+
// ── 전송 전 컨텍스트 사전 압축 ──
{
var llm = _settings.Settings.Llm;
@@ -8309,124 +8459,33 @@ public partial class ChatWindow : Window
}
}
- if (runTab == "Cowork")
+ if (executionMode.UseAgentLoop)
{
- // 워크플로우 분석기 자동 열기
- OpenWorkflowAnalyzerIfEnabled();
-
- // 누적 토큰 초기화
- _agentCumulativeInputTokens = 0;
- _agentCumulativeOutputTokens = 0;
-
- // 코워크 탭: 에이전트 루프 사용
- _agentLoop.EventOccurred += OnAgentEvent;
- // 사용자 의사결정 콜백 — PlanViewerWindow로 계획 표시
- _agentLoop.UserDecisionCallback = CreatePlanDecisionCallback();
- try
- {
- // 코워크 시스템 프롬프트 삽입
- var coworkSystem = BuildCoworkSystemPrompt();
- if (!string.IsNullOrEmpty(coworkSystem))
- sendMessages.Insert(0, new ChatMessage { Role = "system", Content = coworkSystem });
-
- _agentLoop.ActiveTab = runTab;
- var response = await _agentLoop.RunAsync(sendMessages, _streamCts!.Token);
- sb.Append(response);
- assistantMsg.Content = response;
- StopAiIconPulse();
- _cachedStreamContent = response;
- TryPersistConversation(force: true);
-
- // 완료 알림
- if (_settings.Settings.Llm.NotifyOnComplete)
- Services.NotificationService.Notify("AX Cowork Agent", "코워크 작업이 완료되었습니다.");
- }
- finally
- {
- _agentLoop.EventOccurred -= OnAgentEvent;
- _agentLoop.UserDecisionCallback = null;
- }
- }
- else if (runTab == "Code")
- {
- // 워크플로우 분석기 자동 열기
- OpenWorkflowAnalyzerIfEnabled();
-
- // 누적 토큰 초기화
- _agentCumulativeInputTokens = 0;
- _agentCumulativeOutputTokens = 0;
-
- // Code 탭: 에이전트 루프 사용 (Cowork과 동일 패턴)
- _agentLoop.EventOccurred += OnAgentEvent;
- _agentLoop.UserDecisionCallback = CreatePlanDecisionCallback();
- try
- {
- var codeSystem = BuildCodeSystemPrompt();
- if (!string.IsNullOrEmpty(codeSystem))
- sendMessages.Insert(0, new ChatMessage { Role = "system", Content = codeSystem });
-
- _agentLoop.ActiveTab = runTab;
- var response = await _agentLoop.RunAsync(sendMessages, _streamCts!.Token);
- sb.Append(response);
- assistantMsg.Content = response;
- StopAiIconPulse();
- _cachedStreamContent = response;
- TryPersistConversation(force: true);
-
- // 완료 알림
- if (_settings.Settings.Llm.NotifyOnComplete)
- Services.NotificationService.Notify("AX Code Agent", "코드 작업이 완료되었습니다.");
- }
- finally
- {
- _agentLoop.EventOccurred -= OnAgentEvent;
- _agentLoop.UserDecisionCallback = null;
- }
- }
- else if (_settings.Settings.Llm.Streaming)
- {
- await foreach (var chunk in _llm.StreamAsync(sendMessages, _streamCts.Token))
- {
- sb.Append(chunk);
- StopAiIconPulse();
- _cachedStreamContent = sb.ToString();
- assistantMsg.Content = _cachedStreamContent;
- TryPersistConversation();
- // UI 스레드에 제어 양보 — DispatcherTimer가 화면 갱신할 수 있도록
- await Dispatcher.InvokeAsync(() => { }, DispatcherPriority.Background);
- }
- _cachedStreamContent = sb.ToString();
- assistantMsg.Content = _cachedStreamContent;
-
- // 타이핑 애니메이션이 남은 버퍼를 소진할 때까지 대기 (최대 600ms)
- var drainStart = DateTime.UtcNow;
- while (_displayedLength < _cachedStreamContent.Length
- && (DateTime.UtcNow - drainStart).TotalMilliseconds < 600)
- {
- await Dispatcher.InvokeAsync(() => { }, DispatcherPriority.Background);
- }
+ var response = await RunAgentLoopAsync(runTab, originTab, conv, sendMessages, _streamCts!.Token);
+ assistantContent = response;
+ StopAiIconPulse();
+ _cachedStreamContent = response;
}
else
{
var response = await _llm.SendAsync(sendMessages, _streamCts.Token);
- sb.Append(response);
- assistantMsg.Content = response;
+ assistantContent = response;
+ StopAiIconPulse();
+ _cachedStreamContent = response;
}
draftSucceeded = true;
}
catch (OperationCanceledException)
{
- if (sb.Length == 0) sb.Append("(취소됨)");
- assistantMsg.Content = sb.ToString();
+ assistantContent = string.IsNullOrWhiteSpace(assistantContent) ? "(취소됨)" : assistantContent;
draftCancelled = true;
draftFailure = "사용자가 작업을 중단했습니다.";
}
catch (Exception ex)
{
var errMsg = $"⚠ 오류: {ex.Message}";
- sb.Clear(); sb.Append(errMsg);
- assistantMsg.Content = errMsg;
+ assistantContent = errMsg;
AddRetryButton();
draftFailure = ex.Message;
}
@@ -8457,8 +8516,21 @@ public partial class ChatWindow : Window
SetStatusIdle();
}
- // 스트리밍 plaintext → 마크다운 렌더링으로 교체
- FinalizeStreamingContainer(streamContainer, streamText, assistantMsg.Content, assistantMsg);
+ assistantContent = BuildAssistantFallbackContent(conv, runTab, assistantContent);
+ if (runTab is "Cowork" or "Code")
+ conv.ShowExecutionHistory = false;
+
+ lock (_convLock)
+ {
+ var session = ChatSession;
+ _chatEngine.CommitAssistantMessage(session, conv, runTab, assistantContent, _storage);
+ _currentConversation = session?.CurrentConversation ?? conv;
+ conv = _currentConversation!;
+ }
+
+ lock (_convLock)
+ _currentConversation = conv;
+ RenderMessages(preserveViewport: true);
AutoScrollIfNeeded();
try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"대화 저장 실패: {ex.Message}"); }
@@ -8490,6 +8562,54 @@ public partial class ChatWindow : Window
_ = Dispatcher.BeginInvoke(new Action(() => StartNextQueuedDraftIfAny()), DispatcherPriority.Background);
}
+ private async Task RunAgentLoopAsync(
+ string runTab,
+ string originTab,
+ ChatConversation conversation,
+ IReadOnlyList sendMessages,
+ CancellationToken cancellationToken)
+ {
+ OpenWorkflowAnalyzerIfEnabled();
+
+ _agentCumulativeInputTokens = 0;
+ _agentCumulativeOutputTokens = 0;
+
+ _agentLoop.EventOccurred += OnAgentEvent;
+ _agentLoop.UserDecisionCallback = CreatePlanDecisionCallback();
+ try
+ {
+ _agentLoop.ActiveTab = runTab;
+ var response = await _agentLoop.RunAsync(sendMessages.ToList(), cancellationToken);
+ try
+ {
+ _storage.Save(conversation);
+ ChatSession?.RememberConversation(originTab, conversation.Id);
+ }
+ catch (Exception ex)
+ {
+ Services.LogService.Debug($"에이전트 루프 저장 실패: {ex.Message}");
+ }
+
+ if (_settings.Settings.Llm.NotifyOnComplete)
+ {
+ var title = string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase)
+ ? "AX Code Agent"
+ : "AX Cowork Agent";
+ var message = string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase)
+ ? "코드 작업이 완료되었습니다."
+ : "코워크 작업이 완료되었습니다.";
+ Services.NotificationService.Notify(title, message);
+ }
+
+ return response;
+ }
+ finally
+ {
+ _agentLoop.EventOccurred -= OnAgentEvent;
+ _agentLoop.UserDecisionCallback = null;
+ }
+ }
+
// ─── 코워크 에이전트 지원 ────────────────────────────────────────────
private string BuildCoworkSystemPrompt()
@@ -8887,10 +9007,13 @@ public partial class ChatWindow : Window
{
var eventTab = string.IsNullOrWhiteSpace(_streamRunTab) ? _activeTab : _streamRunTab!;
- // 에이전트 이벤트를 채팅 UI에 표시 (도구 호출/결과 배너)
- if (string.Equals(eventTab, _activeTab, StringComparison.OrdinalIgnoreCase))
- AddAgentEventBanner(evt);
+ // 실행 로그는 직접 배너를 먼저 꽂지 않고, 대화 모델에 누적한 뒤 재렌더합니다.
+ // 그래야 중간 배너 잔상과 최종 재렌더 중복이 줄어듭니다.
+ var shouldShowExecutionHistory = _currentConversation?.ShowExecutionHistory ?? false;
AppendConversationExecutionEvent(evt, eventTab);
+ if (shouldShowExecutionHistory
+ && string.Equals(eventTab, _activeTab, StringComparison.OrdinalIgnoreCase))
+ RenderMessages(preserveViewport: true);
AutoScrollIfNeeded();
// 하단 상태바 업데이트
@@ -10862,35 +10985,14 @@ public partial class ChatWindow : Window
BtnSend.Visibility = Visibility.Collapsed;
BtnStop.Visibility = Visibility.Visible;
_streamCts = new CancellationTokenSource();
-
- var assistantMsg = new ChatMessage { Role = "assistant", Content = "" };
- lock (_convLock)
- {
- var session = ChatSession;
- if (session != null)
- {
- session.AppendMessage(_activeTab, assistantMsg, _storage);
- _currentConversation = session.CurrentConversation;
- conv = _currentConversation!;
- }
- else
- {
- conv.Messages.Add(assistantMsg);
- }
- }
-
- var streamContainer = CreateStreamingContainer(out var streamText);
- MessagePanel.Children.Add(streamContainer);
ForceScrollToEnd(); // 응답 시작 시 강제 하단 이동
- var sb = new System.Text.StringBuilder();
- _activeStreamText = streamText;
+ var assistantContent = string.Empty;
+ _activeStreamText = null;
_cachedStreamContent = "";
_displayedLength = 0;
_cursorVisible = true;
_aiIconPulseStopped = false;
- _cursorTimer.Start();
- _typingTimer.Start();
_streamStartTime = DateTime.UtcNow;
_elapsedTimer.Start();
SetStatus("에이전트 작업 중...", spinning: true);
@@ -10898,38 +11000,23 @@ public partial class ChatWindow : Window
try
{
List sendMessages;
- lock (_convLock) sendMessages = conv.Messages.SkipLast(1).ToList();
+ lock (_convLock) sendMessages = conv.Messages.ToList();
if (!string.IsNullOrEmpty(conv.SystemCommand))
sendMessages.Insert(0, new ChatMessage { Role = "system", Content = conv.SystemCommand });
- await foreach (var chunk in _llm.StreamAsync(sendMessages, _streamCts.Token))
- {
- sb.Append(chunk);
- StopAiIconPulse();
- _cachedStreamContent = sb.ToString();
- await Dispatcher.InvokeAsync(() => { }, DispatcherPriority.Background);
- }
- _cachedStreamContent = sb.ToString();
- assistantMsg.Content = _cachedStreamContent;
-
- // 타이핑 애니메이션이 남은 버퍼를 소진할 때까지 대기 (최대 600ms)
- var drainStart2 = DateTime.UtcNow;
- while (_displayedLength < _cachedStreamContent.Length
- && (DateTime.UtcNow - drainStart2).TotalMilliseconds < 600)
- {
- await Dispatcher.InvokeAsync(() => { }, DispatcherPriority.Background);
- }
+ var response = await _llm.SendAsync(sendMessages, _streamCts.Token);
+ assistantContent = response;
+ StopAiIconPulse();
+ _cachedStreamContent = response;
}
catch (OperationCanceledException)
{
- if (sb.Length == 0) sb.Append("(취소됨)");
- assistantMsg.Content = sb.ToString();
+ assistantContent = string.IsNullOrWhiteSpace(assistantContent) ? "(취소됨)" : assistantContent;
}
catch (Exception ex)
{
var errMsg = $"⚠ 오류: {ex.Message}";
- sb.Clear(); sb.Append(errMsg);
- assistantMsg.Content = errMsg;
+ assistantContent = errMsg;
AddRetryButton();
}
finally
@@ -10951,7 +11038,15 @@ public partial class ChatWindow : Window
SetStatusIdle();
}
- FinalizeStreamingContainer(streamContainer, streamText, assistantMsg.Content, assistantMsg);
+ assistantContent = string.IsNullOrWhiteSpace(assistantContent) ? "(빈 응답)" : assistantContent;
+ lock (_convLock)
+ {
+ var session = ChatSession;
+ _chatEngine.CommitAssistantMessage(session, conv, _activeTab, assistantContent, _storage);
+ _currentConversation = session?.CurrentConversation ?? conv;
+ conv = _currentConversation!;
+ }
+ RenderMessages(preserveViewport: true);
AutoScrollIfNeeded();
try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"대화 저장 실패: {ex.Message}"); }
@@ -10960,14 +11055,18 @@ public partial class ChatWindow : Window
RefreshConversationList();
}
- /// 메시지 버블의 MaxWidth를 창 너비에 비례하여 계산합니다 (더 넓은 본문 레이아웃).
+ /// 채팅 본문 폭을 세 탭에서 동일한 기준으로 맞춥니다.
private double GetMessageMaxWidth()
{
- var scrollWidth = MessageScroll.ActualWidth;
- if (scrollWidth < 100) scrollWidth = 700; // 초기화 전 기본값
- // 좌우 여백을 더 줄여 본문과 composer가 넓게 보이도록 조정
- var maxW = (scrollWidth - 56) * 0.975;
- return Math.Clamp(maxW, 620, 1560);
+ var hostWidth = ComposerShell?.ActualWidth ?? 0;
+ if (hostWidth < 100)
+ hostWidth = MessageScroll?.ActualWidth ?? 0;
+ if (hostWidth < 100)
+ hostWidth = 1120; // 초기화 전 기준 폭
+
+ // 컴포저와 메시지 버블이 같은 레이아웃 축을 쓰도록 여백만 제외한 폭을 공통 적용
+ var maxW = hostWidth - 44;
+ return Math.Clamp(maxW, 320, 720);
}
private StackPanel CreateStreamingContainer(out TextBlock streamText)
@@ -11061,6 +11160,8 @@ public partial class ChatWindow : Window
private void FinalizeStreamingContainer(StackPanel container, TextBlock streamText, string finalContent, ChatMessage? message = null)
{
+ finalContent = string.IsNullOrWhiteSpace(finalContent) ? "(빈 응답)" : finalContent;
+
// 스트리밍 plaintext 카드 제거
if (streamText.Parent is Border streamCard)
container.Children.Remove(streamCard);
@@ -12057,13 +12158,16 @@ public partial class ChatWindow : Window
private void BuildTopicButtons()
{
TopicButtonPanel.Children.Clear();
-
TopicButtonPanel.Visibility = Visibility.Visible;
+ if (TopicPresetScrollViewer != null)
+ TopicPresetScrollViewer.Visibility = Visibility.Visible;
// 탭별 EmptyState 텍스트
if (_activeTab == "Cowork" || _activeTab == "Code")
{
- if (EmptyStateTitle != null) EmptyStateTitle.Text = "작업 유형을 선택하세요";
+ if (EmptyStateTitle != null) EmptyStateTitle.Text = _activeTab == "Code"
+ ? "코드 작업을 입력하세요"
+ : "작업 유형을 선택하세요";
if (EmptyStateDesc != null) EmptyStateDesc.Text = _activeTab == "Code"
? "코딩 에이전트가 코드 분석, 수정, 빌드, 테스트를 수행합니다"
: "에이전트가 상세한 데이터를 작성합니다";
@@ -12074,6 +12178,14 @@ public partial class ChatWindow : Window
if (EmptyStateDesc != null) EmptyStateDesc.Text = "주제에 맞는 전문 프리셋이 자동 적용됩니다";
}
+ if (_activeTab == "Code")
+ {
+ TopicButtonPanel.Visibility = Visibility.Collapsed;
+ if (TopicPresetScrollViewer != null)
+ TopicPresetScrollViewer.Visibility = Visibility.Collapsed;
+ return;
+ }
+
var presets = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets);
foreach (var preset in presets)
@@ -12089,7 +12201,7 @@ public partial class ChatWindow : Window
Margin = new Thickness(6, 6, 6, 10),
Cursor = Cursors.Hand,
Width = 124,
- Height = 108,
+ Height = 116,
ClipToBounds = true,
RenderTransformOrigin = new Point(0.5, 0.5),
RenderTransform = new ScaleTransform(1, 1),
@@ -12133,7 +12245,7 @@ public partial class ChatWindow : Window
Text = preset.Description,
FontSize = 9, TextWrapping = TextWrapping.Wrap,
TextTrimming = TextTrimming.CharacterEllipsis,
- MaxHeight = 28,
+ MaxHeight = 32,
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 2, 0, 0),
@@ -12219,7 +12331,7 @@ public partial class ChatWindow : Window
Margin = new Thickness(6, 6, 6, 10),
Cursor = Cursors.Hand,
Width = 124,
- Height = 108,
+ Height = 116,
ClipToBounds = true,
RenderTransformOrigin = new Point(0.5, 0.5),
RenderTransform = new ScaleTransform(1, 1),
@@ -12258,7 +12370,7 @@ public partial class ChatWindow : Window
Text = "프리셋 없이 자유롭게 대화합니다",
FontSize = 9, TextWrapping = TextWrapping.Wrap,
TextTrimming = TextTrimming.CharacterEllipsis,
- MaxHeight = 28,
+ MaxHeight = 32,
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 2, 0, 0),
@@ -12298,7 +12410,7 @@ public partial class ChatWindow : Window
Padding = new Thickness(14, 14, 14, 14),
Margin = new Thickness(6, 6, 6, 10),
Cursor = Cursors.Hand,
- Width = 124, Height = 108,
+ Width = 124, Height = 116,
ClipToBounds = true,
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray,
BorderThickness = new Thickness(1.5),
@@ -12349,6 +12461,29 @@ public partial class ChatWindow : Window
addBorder.MouseLeftButtonDown += (_, _) => ShowCustomPresetDialog();
TopicButtonPanel.Children.Add(addBorder);
}
+
+ UpdateTopicPresetScrollMode();
+ }
+
+ private void UpdateTopicPresetScrollMode()
+ {
+ if (TopicPresetScrollViewer == null || TopicButtonPanel == null)
+ return;
+
+ Dispatcher.BeginInvoke(new Action(() =>
+ {
+ if (TopicPresetScrollViewer == null || TopicButtonPanel == null)
+ return;
+
+ TopicPresetScrollViewer.UpdateLayout();
+ var shouldScroll = TopicPresetScrollViewer.ExtentHeight > TopicPresetScrollViewer.ViewportHeight + 1;
+ TopicPresetScrollViewer.VerticalScrollBarVisibility = shouldScroll
+ ? ScrollBarVisibility.Auto
+ : ScrollBarVisibility.Disabled;
+ TopicPresetScrollViewer.Padding = shouldScroll
+ ? new Thickness(0, 2, 6, 0)
+ : new Thickness(0, 2, 0, 0);
+ }), System.Windows.Threading.DispatcherPriority.Loaded);
}
// ─── 커스텀 프리셋 관리 ─────────────────────────────────────────────
@@ -12567,59 +12702,14 @@ public partial class ChatWindow : Window
private void BuildBottomBar()
{
MoodIconPanel.Children.Clear();
-
- var workspaceBtn = CreateWorkspaceFolderBarButton();
- workspaceBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ShowFolderMenu(); };
- MoodIconPanel.Children.Add(workspaceBtn);
-
- // ── 파일 탐색기 토글 버튼 ──
- var fileBrowserBtn = CreateFolderBarButton("\uED25", "파일", "파일 탐색기 열기/닫기", "#D97706");
- fileBrowserBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ToggleFileBrowser(); };
- MoodIconPanel.Children.Add(fileBrowserBtn);
-
- // ── 실행 이력 상세도 버튼 ──
- AppendLogLevelButton();
-
- // 구분선 표시
- if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Visible;
+ if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Collapsed;
}
/// Code 탭 하단 바: 로컬 / 브랜치 / 워크트리 흐름 중심.
private void BuildCodeBottomBar()
{
MoodIconPanel.Children.Clear();
-
- var workspaceBtn = CreateWorkspaceFolderBarButton();
- workspaceBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ShowFolderMenu(); };
- MoodIconPanel.Children.Add(workspaceBtn);
-
- var localBtn = CreateFolderBarButton("\uED25", "로컬", "원본 워크스페이스로 전환", "#6B7280");
- localBtn.MouseLeftButtonUp += (_, e) =>
- {
- e.Handled = true;
- var currentFolder = GetCurrentWorkFolder();
- if (string.IsNullOrWhiteSpace(currentFolder) || !Directory.Exists(currentFolder))
- {
- ShowFolderMenu();
- return;
- }
-
- var root = WorktreeStateStore.ResolveRoot(currentFolder);
- if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root))
- {
- ShowFolderMenu();
- return;
- }
-
- SwitchToWorkspace(root, root);
- };
- MoodIconPanel.Children.Add(localBtn);
-
- var worktreeBtn = CreateFolderBarButton("\uE8B7", "워크트리", "분리된 작업 복사본 생성 또는 전환", "#2563EB");
- worktreeBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ShowWorktreeMenu(worktreeBtn); };
- MoodIconPanel.Children.Add(worktreeBtn);
-
- if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Visible;
+ if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Collapsed;
}
private Border CreateWorkspaceFolderBarButton()
@@ -13937,6 +14027,10 @@ public partial class ChatWindow : Window
return [];
}
+ private static bool SupportsOverlayRegisteredModels(string service)
+ => string.Equals(service, "ollama", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(service, "vllm", StringComparison.OrdinalIgnoreCase);
+
private void RefreshInlineSettingsPanel()
{
if (InlineSettingsPanel == null) return;
@@ -14200,11 +14294,11 @@ public partial class ChatWindow : Window
RefreshOverlaySettingsPanel();
AgentSettingsOverlay.Visibility = Visibility.Visible;
InlineSettingsPanel.IsOpen = false;
- SetOverlaySection("common");
+ SetOverlaySection("basic");
Dispatcher.BeginInvoke(() =>
{
- if (OverlayNavCommon != null)
- OverlayNavCommon.Focus();
+ if (OverlayNavBasic != null)
+ OverlayNavBasic.Focus();
else
InputBox.Focus();
}, DispatcherPriority.Input);
@@ -14248,7 +14342,7 @@ public partial class ChatWindow : Window
{
var llm = _settings.Settings.Llm;
- _settings.Settings.AiEnabled = ChkOverlayAiEnabled?.IsChecked == true;
+ _settings.Settings.AiEnabled = true;
llm.EnableProactiveContextCompact = ChkOverlayEnableProactiveCompact?.IsChecked == true;
llm.EnableSkillSystem = ChkOverlayEnableSkillSystem?.IsChecked == true;
llm.EnableToolHooks = ChkOverlayEnableToolHooks?.IsChecked == true;
@@ -14256,7 +14350,18 @@ public partial class ChatWindow : Window
llm.EnableHookPermissionUpdate = ChkOverlayEnableHookPermissionUpdate?.IsChecked == true;
llm.EnableCoworkVerification = ChkOverlayEnableCoworkVerification?.IsChecked == true;
llm.Code.EnableCodeVerification = ChkOverlayEnableCodeVerification?.IsChecked == true;
+ llm.Code.EnableCodeReview = ChkOverlayEnableCodeReview?.IsChecked == true;
+ llm.EnableImageInput = ChkOverlayEnableImageInput?.IsChecked == true;
llm.EnableParallelTools = ChkOverlayEnableParallelTools?.IsChecked == true;
+ llm.EnableProjectRules = ChkOverlayEnableProjectRules?.IsChecked == true;
+ llm.EnableAgentMemory = ChkOverlayEnableAgentMemory?.IsChecked == true;
+ llm.Code.EnablePlanModeTools = ChkOverlayEnablePlanModeTools?.IsChecked == true;
+ llm.Code.EnableWorktreeTools = ChkOverlayEnableWorktreeTools?.IsChecked == true;
+ llm.Code.EnableTeamTools = ChkOverlayEnableTeamTools?.IsChecked == true;
+ llm.Code.EnableCronTools = ChkOverlayEnableCronTools?.IsChecked == true;
+ llm.WorkflowVisualizer = ChkOverlayWorkflowVisualizer?.IsChecked == true;
+ llm.ShowTotalCallStats = ChkOverlayShowTotalCallStats?.IsChecked == true;
+ llm.EnableAuditLog = ChkOverlayEnableAuditLog?.IsChecked == true;
llm.FolderDataUsage = _folderDataUsage;
CommitOverlayEndpointInput(normalizeOnInvalid: true);
@@ -14264,7 +14369,20 @@ public partial class ChatWindow : Window
CommitOverlayModelInput(normalizeOnInvalid: true);
CommitOverlayNumericInput(TxtOverlayContextCompactTriggerPercent, llm.ContextCompactTriggerPercent, 10, 95, value => llm.ContextCompactTriggerPercent = value, normalizeOnInvalid: true);
CommitOverlayNumericInput(TxtOverlayMaxContextTokens, llm.MaxContextTokens, 1024, 1_000_000, value => llm.MaxContextTokens = value, normalizeOnInvalid: true);
+ CommitOverlayTemperatureInput(normalizeOnInvalid: true);
CommitOverlayNumericInput(TxtOverlayMaxRetryOnError, llm.MaxRetryOnError, 0, 10, value => llm.MaxRetryOnError = value, normalizeOnInvalid: true);
+ CommitOverlayNumericInput(TxtOverlayMaxAgentIterations, llm.MaxAgentIterations, 1, 200, value => llm.MaxAgentIterations = value, normalizeOnInvalid: true);
+ CommitOverlayNumericInput(TxtOverlayFreeTierDelaySeconds, llm.FreeTierDelaySeconds, 0, 60, value => llm.FreeTierDelaySeconds = value, normalizeOnInvalid: true);
+ CommitOverlayNumericInput(TxtOverlayMaxSubAgents, llm.MaxSubAgents, 1, 10, value => llm.MaxSubAgents = value, normalizeOnInvalid: true);
+ CommitOverlayNumericInput(TxtOverlayToolHookTimeoutMs, llm.ToolHookTimeoutMs, 3000, 30000, value => llm.ToolHookTimeoutMs = value, normalizeOnInvalid: true);
+ CommitOverlayNumericInput(TxtOverlayMaxFavoriteSlashCommands, llm.MaxFavoriteSlashCommands, 1, 30, value => llm.MaxFavoriteSlashCommands = value, normalizeOnInvalid: true);
+ CommitOverlayNumericInput(TxtOverlayMaxRecentSlashCommands, llm.MaxRecentSlashCommands, 5, 50, value => llm.MaxRecentSlashCommands = value, normalizeOnInvalid: true);
+ CommitOverlayNumericInput(TxtOverlayPlanDiffMediumCount, llm.PlanDiffSeverityMediumCount, 1, 999, value => llm.PlanDiffSeverityMediumCount = value, normalizeOnInvalid: true);
+ CommitOverlayNumericInput(TxtOverlayPlanDiffHighCount, llm.PlanDiffSeverityHighCount, 1, 999, value => llm.PlanDiffSeverityHighCount = value, normalizeOnInvalid: true);
+ CommitOverlayNumericInput(TxtOverlayPlanDiffMediumRatio, llm.PlanDiffSeverityMediumRatioPercent, 1, 100, value => llm.PlanDiffSeverityMediumRatioPercent = value, normalizeOnInvalid: true);
+ CommitOverlayNumericInput(TxtOverlayPlanDiffHighRatio, llm.PlanDiffSeverityHighRatioPercent, 1, 100, value => llm.PlanDiffSeverityHighRatioPercent = value, normalizeOnInvalid: true);
+ if (TxtOverlayPdfExportPath != null)
+ llm.PdfExportPath = TxtOverlayPdfExportPath.Text.Trim();
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
if (closeOverlay)
@@ -14341,10 +14459,12 @@ public partial class ChatWindow : Window
? "미선택"
: (models.FirstOrDefault(m => string.Equals(m.Id, llm.Model, StringComparison.OrdinalIgnoreCase)).Label ?? llm.Model);
+ PopulateOverlayMoodCombo();
+
if (loadDeferredInputs)
{
if (ChkOverlayAiEnabled != null)
- ChkOverlayAiEnabled.IsChecked = _settings.Settings.AiEnabled;
+ ChkOverlayAiEnabled.IsChecked = true;
if (TxtOverlayServiceEndpoint != null)
TxtOverlayServiceEndpoint.Text = GetOverlayServiceEndpoint(service);
if (TxtOverlayServiceApiKey != null)
@@ -14353,8 +14473,64 @@ public partial class ChatWindow : Window
TxtOverlayContextCompactTriggerPercent.Text = Math.Clamp(llm.ContextCompactTriggerPercent, 10, 95).ToString();
if (TxtOverlayMaxContextTokens != null)
TxtOverlayMaxContextTokens.Text = Math.Clamp(llm.MaxContextTokens, 1024, 1_000_000).ToString();
+ if (TxtOverlayTemperature != null)
+ TxtOverlayTemperature.Text = Math.Round(Math.Clamp(llm.Temperature, 0.0, 2.0), 1).ToString("0.0");
+ if (SldOverlayTemperature != null)
+ SldOverlayTemperature.Value = Math.Round(Math.Clamp(llm.Temperature, 0.0, 2.0), 1);
+ if (TxtOverlayTemperatureValue != null)
+ TxtOverlayTemperatureValue.Text = Math.Round(Math.Clamp(llm.Temperature, 0.0, 2.0), 1).ToString("0.0");
if (TxtOverlayMaxRetryOnError != null)
TxtOverlayMaxRetryOnError.Text = Math.Clamp(llm.MaxRetryOnError, 0, 10).ToString();
+ if (SldOverlayMaxRetryOnError != null)
+ SldOverlayMaxRetryOnError.Value = Math.Clamp(llm.MaxRetryOnError, 0, 10);
+ if (TxtOverlayMaxRetryOnErrorValue != null)
+ TxtOverlayMaxRetryOnErrorValue.Text = Math.Clamp(llm.MaxRetryOnError, 0, 10).ToString();
+ if (TxtOverlayMaxAgentIterations != null)
+ TxtOverlayMaxAgentIterations.Text = Math.Clamp(llm.MaxAgentIterations, 1, 200).ToString();
+ if (SldOverlayMaxAgentIterations != null)
+ SldOverlayMaxAgentIterations.Value = Math.Clamp(llm.MaxAgentIterations, 1, 100);
+ if (TxtOverlayMaxAgentIterationsValue != null)
+ TxtOverlayMaxAgentIterationsValue.Text = Math.Clamp(llm.MaxAgentIterations, 1, 100).ToString();
+ if (TxtOverlayFreeTierDelaySeconds != null)
+ TxtOverlayFreeTierDelaySeconds.Text = Math.Clamp(llm.FreeTierDelaySeconds, 0, 60).ToString();
+ if (SldOverlayFreeTierDelaySeconds != null)
+ SldOverlayFreeTierDelaySeconds.Value = Math.Clamp(llm.FreeTierDelaySeconds, 0, 60);
+ if (TxtOverlayFreeTierDelaySecondsValue != null)
+ TxtOverlayFreeTierDelaySecondsValue.Text = Math.Clamp(llm.FreeTierDelaySeconds, 0, 60).ToString();
+ if (TxtOverlayMaxSubAgents != null)
+ TxtOverlayMaxSubAgents.Text = Math.Clamp(llm.MaxSubAgents, 1, 10).ToString();
+ if (SldOverlayMaxSubAgents != null)
+ SldOverlayMaxSubAgents.Value = Math.Clamp(llm.MaxSubAgents, 1, 10);
+ if (TxtOverlayMaxSubAgentsValue != null)
+ TxtOverlayMaxSubAgentsValue.Text = Math.Clamp(llm.MaxSubAgents, 1, 10).ToString();
+ if (TxtOverlayToolHookTimeoutMs != null)
+ TxtOverlayToolHookTimeoutMs.Text = Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000).ToString();
+ if (SldOverlayToolHookTimeoutMs != null)
+ SldOverlayToolHookTimeoutMs.Value = Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000);
+ if (TxtOverlayToolHookTimeoutMsValue != null)
+ TxtOverlayToolHookTimeoutMsValue.Text = $"{Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000) / 1000}s";
+ if (TxtOverlayMaxFavoriteSlashCommands != null)
+ TxtOverlayMaxFavoriteSlashCommands.Text = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30).ToString();
+ if (SldOverlayMaxFavoriteSlashCommands != null)
+ SldOverlayMaxFavoriteSlashCommands.Value = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30);
+ if (TxtOverlayMaxFavoriteSlashCommandsValue != null)
+ TxtOverlayMaxFavoriteSlashCommandsValue.Text = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30).ToString();
+ if (TxtOverlayMaxRecentSlashCommands != null)
+ TxtOverlayMaxRecentSlashCommands.Text = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50).ToString();
+ if (SldOverlayMaxRecentSlashCommands != null)
+ SldOverlayMaxRecentSlashCommands.Value = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50);
+ if (TxtOverlayMaxRecentSlashCommandsValue != null)
+ TxtOverlayMaxRecentSlashCommandsValue.Text = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50).ToString();
+ if (TxtOverlayPlanDiffMediumCount != null)
+ TxtOverlayPlanDiffMediumCount.Text = Math.Clamp(llm.PlanDiffSeverityMediumCount, 1, 999).ToString();
+ if (TxtOverlayPlanDiffHighCount != null)
+ TxtOverlayPlanDiffHighCount.Text = Math.Clamp(llm.PlanDiffSeverityHighCount, 1, 999).ToString();
+ if (TxtOverlayPlanDiffMediumRatio != null)
+ TxtOverlayPlanDiffMediumRatio.Text = Math.Clamp(llm.PlanDiffSeverityMediumRatioPercent, 1, 100).ToString();
+ if (TxtOverlayPlanDiffHighRatio != null)
+ TxtOverlayPlanDiffHighRatio.Text = Math.Clamp(llm.PlanDiffSeverityHighRatioPercent, 1, 100).ToString();
+ if (TxtOverlayPdfExportPath != null)
+ TxtOverlayPdfExportPath.Text = llm.PdfExportPath ?? "";
if (ChkOverlayEnableProactiveCompact != null)
ChkOverlayEnableProactiveCompact.IsChecked = llm.EnableProactiveContextCompact;
if (ChkOverlayEnableSkillSystem != null)
@@ -14369,23 +14545,39 @@ public partial class ChatWindow : Window
ChkOverlayEnableCoworkVerification.IsChecked = llm.EnableCoworkVerification;
if (ChkOverlayEnableCodeVerification != null)
ChkOverlayEnableCodeVerification.IsChecked = llm.Code.EnableCodeVerification;
+ if (ChkOverlayEnableCodeReview != null)
+ ChkOverlayEnableCodeReview.IsChecked = llm.Code.EnableCodeReview;
+ if (ChkOverlayEnableImageInput != null)
+ ChkOverlayEnableImageInput.IsChecked = llm.EnableImageInput;
if (ChkOverlayEnableParallelTools != null)
ChkOverlayEnableParallelTools.IsChecked = llm.EnableParallelTools;
- }
-
- if (BtnOverlayDefaultOutputFormat != null)
- BtnOverlayDefaultOutputFormat.Content = $"AI 자동 · {GetFormatLabel(llm.DefaultOutputFormat ?? "auto")}";
- if (BtnOverlayDefaultMood != null)
- {
- var mood = TemplateService.AllMoods.FirstOrDefault(m => m.Key == (_selectedMood ?? llm.DefaultMood ?? "modern"));
- BtnOverlayDefaultMood.Content = mood == null ? "모던" : $"{mood.Icon} {mood.Label}";
+ if (ChkOverlayEnableProjectRules != null)
+ ChkOverlayEnableProjectRules.IsChecked = llm.EnableProjectRules;
+ if (ChkOverlayEnableAgentMemory != null)
+ ChkOverlayEnableAgentMemory.IsChecked = llm.EnableAgentMemory;
+ if (ChkOverlayEnablePlanModeTools != null)
+ ChkOverlayEnablePlanModeTools.IsChecked = llm.Code.EnablePlanModeTools;
+ if (ChkOverlayEnableWorktreeTools != null)
+ ChkOverlayEnableWorktreeTools.IsChecked = llm.Code.EnableWorktreeTools;
+ if (ChkOverlayEnableTeamTools != null)
+ ChkOverlayEnableTeamTools.IsChecked = llm.Code.EnableTeamTools;
+ if (ChkOverlayEnableCronTools != null)
+ ChkOverlayEnableCronTools.IsChecked = llm.Code.EnableCronTools;
+ if (ChkOverlayWorkflowVisualizer != null)
+ ChkOverlayWorkflowVisualizer.IsChecked = llm.WorkflowVisualizer;
+ if (ChkOverlayShowTotalCallStats != null)
+ ChkOverlayShowTotalCallStats.IsChecked = llm.ShowTotalCallStats;
+ if (ChkOverlayEnableAuditLog != null)
+ ChkOverlayEnableAuditLog.IsChecked = llm.EnableAuditLog;
}
RefreshOverlayThemeCards();
RefreshOverlayServiceCards();
RefreshOverlayModeButtons();
+ RefreshOverlayTokenPresetCards();
RefreshOverlayServiceFieldLabels(service);
BuildOverlayModelChips(service);
+ BuildOverlayRegisteredModelsPanel(service);
RefreshOverlayAdvancedChoiceButtons();
}
finally
@@ -14508,6 +14700,9 @@ public partial class ChatWindow : Window
return true;
}
+ private LlmSettings? TryGetOverlayLlmSettings()
+ => _settings?.Settings?.Llm;
+
private void MarkOverlayValidation(Control? control, string message)
{
if (control == null)
@@ -14558,7 +14753,7 @@ public partial class ChatWindow : Window
if (_isOverlaySettingsSyncing)
return;
- _settings.Settings.AiEnabled = ChkOverlayAiEnabled?.IsChecked == true;
+ _settings.Settings.AiEnabled = true;
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
}
@@ -14617,6 +14812,195 @@ public partial class ChatWindow : Window
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
}
+ private bool CommitOverlayTemperatureInput(bool normalizeOnInvalid)
+ {
+ if (TxtOverlayTemperature == null)
+ return false;
+
+ var raw = TxtOverlayTemperature.Text.Trim();
+ if (!double.TryParse(raw, out var parsed))
+ {
+ ClearOverlayValidation(TxtOverlayTemperature);
+ if (normalizeOnInvalid)
+ TxtOverlayTemperature.Text = Math.Round(Math.Clamp(_settings.Settings.Llm.Temperature, 0.0, 2.0), 1).ToString("0.0");
+ return false;
+ }
+
+ var normalized = Math.Round(Math.Clamp(parsed, 0.0, 2.0), 1);
+ var changed = Math.Abs(_settings.Settings.Llm.Temperature - normalized) > 0.0001;
+ _settings.Settings.Llm.Temperature = normalized;
+ ClearOverlayValidation(TxtOverlayTemperature);
+ if (normalizeOnInvalid || changed)
+ TxtOverlayTemperature.Text = normalized.ToString("0.0");
+ return changed;
+ }
+
+ private void TxtOverlayTemperature_LostFocus(object sender, RoutedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing)
+ return;
+
+ if (CommitOverlayTemperatureInput(normalizeOnInvalid: false))
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void SldOverlayTemperature_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ var llm = TryGetOverlayLlmSettings();
+ if (_isOverlaySettingsSyncing || llm == null)
+ return;
+
+ var value = Math.Round(Math.Clamp(e.NewValue, 0.0, 2.0), 1);
+ llm.Temperature = value;
+ if (TxtOverlayTemperature != null)
+ TxtOverlayTemperature.Text = value.ToString("0.0");
+ if (TxtOverlayTemperatureValue != null)
+ TxtOverlayTemperatureValue.Text = value.ToString("0.0");
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void SldOverlayMaxRetryOnError_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ var llm = TryGetOverlayLlmSettings();
+ if (_isOverlaySettingsSyncing || llm == null)
+ return;
+
+ var value = (int)Math.Round(Math.Clamp(e.NewValue, 0, 10));
+ llm.MaxRetryOnError = value;
+ if (TxtOverlayMaxRetryOnError != null)
+ TxtOverlayMaxRetryOnError.Text = value.ToString();
+ if (TxtOverlayMaxRetryOnErrorValue != null)
+ TxtOverlayMaxRetryOnErrorValue.Text = value.ToString();
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void SldOverlayMaxAgentIterations_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ var llm = TryGetOverlayLlmSettings();
+ if (_isOverlaySettingsSyncing || llm == null)
+ return;
+
+ var value = (int)Math.Round(Math.Clamp(e.NewValue, 1, 100));
+ llm.MaxAgentIterations = value;
+ if (TxtOverlayMaxAgentIterations != null)
+ TxtOverlayMaxAgentIterations.Text = value.ToString();
+ if (TxtOverlayMaxAgentIterationsValue != null)
+ TxtOverlayMaxAgentIterationsValue.Text = value.ToString();
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void SldOverlayFreeTierDelaySeconds_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ var llm = TryGetOverlayLlmSettings();
+ if (_isOverlaySettingsSyncing || llm == null)
+ return;
+
+ var value = (int)Math.Round(Math.Clamp(e.NewValue, 0, 60));
+ llm.FreeTierDelaySeconds = value;
+ if (TxtOverlayFreeTierDelaySeconds != null)
+ TxtOverlayFreeTierDelaySeconds.Text = value.ToString();
+ if (TxtOverlayFreeTierDelaySecondsValue != null)
+ TxtOverlayFreeTierDelaySecondsValue.Text = value.ToString();
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void SldOverlayMaxSubAgents_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ var llm = TryGetOverlayLlmSettings();
+ if (_isOverlaySettingsSyncing || llm == null)
+ return;
+
+ var value = (int)Math.Round(Math.Clamp(e.NewValue, 1, 10));
+ llm.MaxSubAgents = value;
+ if (TxtOverlayMaxSubAgents != null)
+ TxtOverlayMaxSubAgents.Text = value.ToString();
+ if (TxtOverlayMaxSubAgentsValue != null)
+ TxtOverlayMaxSubAgentsValue.Text = value.ToString();
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void SldOverlayToolHookTimeoutMs_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ var llm = TryGetOverlayLlmSettings();
+ if (_isOverlaySettingsSyncing || llm == null)
+ return;
+
+ var value = (int)Math.Round(Math.Clamp(e.NewValue, 3000, 30000));
+ llm.ToolHookTimeoutMs = value;
+ if (TxtOverlayToolHookTimeoutMs != null)
+ TxtOverlayToolHookTimeoutMs.Text = value.ToString();
+ if (TxtOverlayToolHookTimeoutMsValue != null)
+ TxtOverlayToolHookTimeoutMsValue.Text = $"{value / 1000}s";
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void SldOverlaySlashPopupPageSize_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ var llm = TryGetOverlayLlmSettings();
+ if (_isOverlaySettingsSyncing || llm == null)
+ return;
+
+ var value = (int)Math.Round(Math.Clamp(e.NewValue, 3, 20));
+ llm.SlashPopupPageSize = value;
+ if (TxtOverlaySlashPopupPageSize != null)
+ TxtOverlaySlashPopupPageSize.Text = value.ToString();
+ if (TxtOverlaySlashPopupPageSizeValue != null)
+ TxtOverlaySlashPopupPageSizeValue.Text = value.ToString();
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void SldOverlayMaxFavoriteSlashCommands_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ var llm = TryGetOverlayLlmSettings();
+ if (_isOverlaySettingsSyncing || llm == null)
+ return;
+
+ var value = (int)Math.Round(Math.Clamp(e.NewValue, 1, 30));
+ llm.MaxFavoriteSlashCommands = value;
+ if (TxtOverlayMaxFavoriteSlashCommands != null)
+ TxtOverlayMaxFavoriteSlashCommands.Text = value.ToString();
+ if (TxtOverlayMaxFavoriteSlashCommandsValue != null)
+ TxtOverlayMaxFavoriteSlashCommandsValue.Text = value.ToString();
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void SldOverlayMaxRecentSlashCommands_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ var llm = TryGetOverlayLlmSettings();
+ if (_isOverlaySettingsSyncing || llm == null)
+ return;
+
+ var value = (int)Math.Round(Math.Clamp(e.NewValue, 5, 50));
+ llm.MaxRecentSlashCommands = value;
+ if (TxtOverlayMaxRecentSlashCommands != null)
+ TxtOverlayMaxRecentSlashCommands.Text = value.ToString();
+ if (TxtOverlayMaxRecentSlashCommandsValue != null)
+ TxtOverlayMaxRecentSlashCommandsValue.Text = value.ToString();
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void OverlayCompactPresetCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
+ {
+ if (sender is not Border border || border.Tag is not string tag || !int.TryParse(tag, out var value))
+ return;
+
+ _settings.Settings.Llm.ContextCompactTriggerPercent = Math.Clamp(value, 10, 95);
+ if (TxtOverlayContextCompactTriggerPercent != null)
+ TxtOverlayContextCompactTriggerPercent.Text = _settings.Settings.Llm.ContextCompactTriggerPercent.ToString();
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void OverlayContextPresetCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
+ {
+ if (sender is not Border border || border.Tag is not string tag || !int.TryParse(tag, out var value))
+ return;
+
+ _settings.Settings.Llm.MaxContextTokens = Math.Clamp(value, 1024, 1_000_000);
+ if (TxtOverlayMaxContextTokens != null)
+ TxtOverlayMaxContextTokens.Text = _settings.Settings.Llm.MaxContextTokens.ToString();
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
private void TxtOverlayMaxRetryOnError_LostFocus(object sender, RoutedEventArgs e)
{
if (_isOverlaySettingsSyncing)
@@ -14626,6 +15010,173 @@ public partial class ChatWindow : Window
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
}
+ private void TxtOverlayMaxAgentIterations_LostFocus(object sender, RoutedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing)
+ return;
+
+ if (CommitOverlayNumericInput(TxtOverlayMaxAgentIterations, _settings.Settings.Llm.MaxAgentIterations, 1, 200, value => _settings.Settings.Llm.MaxAgentIterations = value, normalizeOnInvalid: false))
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void TxtOverlayFreeTierDelaySeconds_LostFocus(object sender, RoutedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing)
+ return;
+
+ if (CommitOverlayNumericInput(TxtOverlayFreeTierDelaySeconds, _settings.Settings.Llm.FreeTierDelaySeconds, 0, 60, value => _settings.Settings.Llm.FreeTierDelaySeconds = value, normalizeOnInvalid: false))
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void TxtOverlayMaxSubAgents_LostFocus(object sender, RoutedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing)
+ return;
+
+ if (CommitOverlayNumericInput(TxtOverlayMaxSubAgents, _settings.Settings.Llm.MaxSubAgents, 1, 10, value => _settings.Settings.Llm.MaxSubAgents = value, normalizeOnInvalid: false))
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void TxtOverlaySlashPopupPageSize_LostFocus(object sender, RoutedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing)
+ return;
+
+ if (CommitOverlayNumericInput(TxtOverlaySlashPopupPageSize, _settings.Settings.Llm.SlashPopupPageSize, 3, 20, value => _settings.Settings.Llm.SlashPopupPageSize = value, normalizeOnInvalid: false))
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void TxtOverlayToolHookTimeoutMs_LostFocus(object sender, RoutedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing)
+ return;
+
+ if (CommitOverlayNumericInput(TxtOverlayToolHookTimeoutMs, _settings.Settings.Llm.ToolHookTimeoutMs, 3000, 30000, value => _settings.Settings.Llm.ToolHookTimeoutMs = value, normalizeOnInvalid: false))
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void TxtOverlayMaxFavoriteSlashCommands_LostFocus(object sender, RoutedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing)
+ return;
+
+ if (CommitOverlayNumericInput(TxtOverlayMaxFavoriteSlashCommands, _settings.Settings.Llm.MaxFavoriteSlashCommands, 1, 30, value => _settings.Settings.Llm.MaxFavoriteSlashCommands = value, normalizeOnInvalid: false))
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void TxtOverlayMaxRecentSlashCommands_LostFocus(object sender, RoutedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing)
+ return;
+
+ if (CommitOverlayNumericInput(TxtOverlayMaxRecentSlashCommands, _settings.Settings.Llm.MaxRecentSlashCommands, 5, 50, value => _settings.Settings.Llm.MaxRecentSlashCommands = value, normalizeOnInvalid: false))
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void TxtOverlayPdfExportPath_LostFocus(object sender, RoutedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing || TxtOverlayPdfExportPath == null)
+ return;
+
+ _settings.Settings.Llm.PdfExportPath = TxtOverlayPdfExportPath.Text.Trim();
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void TxtOverlayPlanDiffMediumCount_LostFocus(object sender, RoutedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing)
+ return;
+
+ if (CommitOverlayNumericInput(TxtOverlayPlanDiffMediumCount, _settings.Settings.Llm.PlanDiffSeverityMediumCount, 1, 999, value => _settings.Settings.Llm.PlanDiffSeverityMediumCount = value, normalizeOnInvalid: false))
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void TxtOverlayPlanDiffHighCount_LostFocus(object sender, RoutedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing)
+ return;
+
+ if (CommitOverlayNumericInput(TxtOverlayPlanDiffHighCount, _settings.Settings.Llm.PlanDiffSeverityHighCount, 1, 999, value => _settings.Settings.Llm.PlanDiffSeverityHighCount = value, normalizeOnInvalid: false))
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void TxtOverlayPlanDiffMediumRatio_LostFocus(object sender, RoutedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing)
+ return;
+
+ if (CommitOverlayNumericInput(TxtOverlayPlanDiffMediumRatio, _settings.Settings.Llm.PlanDiffSeverityMediumRatioPercent, 1, 100, value => _settings.Settings.Llm.PlanDiffSeverityMediumRatioPercent = value, normalizeOnInvalid: false))
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void TxtOverlayPlanDiffHighRatio_LostFocus(object sender, RoutedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing)
+ return;
+
+ if (CommitOverlayNumericInput(TxtOverlayPlanDiffHighRatio, _settings.Settings.Llm.PlanDiffSeverityHighRatioPercent, 1, 100, value => _settings.Settings.Llm.PlanDiffSeverityHighRatioPercent = value, normalizeOnInvalid: false))
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void OverlayBrowseSkillFolderBtn_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
+ {
+ var dlg = new System.Windows.Forms.FolderBrowserDialog
+ {
+ Description = "스킬 파일이 있는 폴더를 선택하세요",
+ ShowNewFolderButton = true,
+ };
+
+ var current = _settings.Settings.Llm.SkillsFolderPath;
+ if (!string.IsNullOrWhiteSpace(current) && Directory.Exists(current))
+ dlg.SelectedPath = current;
+
+ if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK)
+ return;
+
+ _settings.Settings.Llm.SkillsFolderPath = dlg.SelectedPath;
+ SkillService.LoadSkills(dlg.SelectedPath);
+ RefreshOverlayEtcPanels();
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void OverlayOpenSkillFolderBtn_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
+ {
+ var folder = !string.IsNullOrWhiteSpace(_settings.Settings.Llm.SkillsFolderPath) && Directory.Exists(_settings.Settings.Llm.SkillsFolderPath)
+ ? _settings.Settings.Llm.SkillsFolderPath
+ : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills");
+
+ if (!Directory.Exists(folder))
+ Directory.CreateDirectory(folder);
+
+ try { System.Diagnostics.Process.Start("explorer.exe", folder); } catch { }
+ }
+
+ private void OverlayAddHookBtn_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
+ => ShowOverlayHookEditDialog(null, -1);
+
+ private void OverlayOpenAuditLogBtn_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
+ {
+ try { System.Diagnostics.Process.Start("explorer.exe", Services.AuditLogService.GetAuditFolder()); } catch { }
+ }
+
+ private void ChkOverlayEnableDragDropAiActions_Changed(object sender, RoutedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing || ChkOverlayEnableDragDropAiActions == null)
+ return;
+
+ _settings.Settings.Llm.EnableDragDropAiActions = ChkOverlayEnableDragDropAiActions.IsChecked == true;
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void ChkOverlayDragDropAutoSend_Changed(object sender, RoutedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing || ChkOverlayDragDropAutoSend == null)
+ return;
+
+ _settings.Settings.Llm.DragDropAutoSend = ChkOverlayDragDropAutoSend.IsChecked == true;
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
private void OverlayNav_Checked(object sender, RoutedEventArgs e)
{
if (sender is not RadioButton rb || rb.Tag is not string tag)
@@ -14639,56 +15190,1099 @@ public partial class ChatWindow : Window
if (OverlaySectionService == null || OverlaySectionQuick == null || OverlaySectionDetail == null)
return;
- var section = string.IsNullOrWhiteSpace(tag) ? "common" : tag.Trim().ToLowerInvariant();
- var showCommon = section == "common";
- var showService = section == "service";
- var showPermission = section == "permission";
- var showAdvanced = section == "advanced";
+ var section = string.IsNullOrWhiteSpace(tag) ? "basic" : tag.Trim().ToLowerInvariant();
+ var showBasic = section == "basic";
+ var showChat = section == "chat";
+ var showShared = section == "shared";
+ var showCowork = section == "cowork";
+ var showCode = section == "code";
+ var showDev = section == "dev";
+ var showTools = section == "tools";
+ var showEtc = section == "etc";
- OverlaySectionService.Visibility = showCommon || showService ? Visibility.Visible : Visibility.Collapsed;
- OverlaySectionQuick.Visibility = showCommon || showPermission ? Visibility.Visible : Visibility.Collapsed;
+ OverlaySectionService.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed;
+ OverlaySectionQuick.Visibility = showShared || showCowork || showCode ? Visibility.Visible : Visibility.Collapsed;
OverlaySectionDetail.Visibility = Visibility.Visible;
+ var headingTitle = section switch
+ {
+ "chat" => "채팅 설정",
+ "shared" => "코워크/코드 공통 설정",
+ "cowork" => "코워크 설정",
+ "code" => "코드 설정",
+ "dev" => "개발자 설정",
+ "tools" => "도구 설정",
+ "etc" => "스킬/차단 설정",
+ _ => "공통 설정"
+ };
+ var headingDescription = section switch
+ {
+ "chat" => "Chat 탭에서 쓰는 입력/내보내기 같은 채팅 전용 설정입니다.",
+ "shared" => "Cowork와 Code에서 함께 쓰는 문맥/압축 관련 기본 설정입니다.",
+ "cowork" => "문서/업무 협업 흐름에 맞춘 코워크 전용 설정입니다.",
+ "code" => "코드 작업, 검증, 개발 도구 사용에 맞춘 설정입니다.",
+ "dev" => "실행 이력, 감사, 시각화 같은 개발자용 설정입니다.",
+ "tools" => "AX Agent가 사용할 도구와 훅 동작을 관리합니다.",
+ "etc" => "스킬 로드와 차단 규칙, 보조 연결을 관리합니다.",
+ _ => "Chat, Cowork, Code에서 공통으로 쓰는 기본 설정입니다."
+ };
+
+ if (OverlayTopHeadingTitle != null)
+ OverlayTopHeadingTitle.Text = headingTitle;
+ if (OverlayTopHeadingDescription != null)
+ OverlayTopHeadingDescription.Text = headingDescription;
if (OverlayAnchorCommon != null)
- OverlayAnchorCommon.Text = section switch
- {
- "service" => "서비스 설정",
- "permission" => "권한 설정",
- "advanced" => "고급 설정",
- _ => "일반 설정"
- };
+ OverlayAnchorCommon.Text = headingTitle;
if (OverlayAiEnabledRow != null)
- OverlayAiEnabledRow.Visibility = showCommon ? Visibility.Visible : Visibility.Collapsed;
+ OverlayAiEnabledRow.Visibility = Visibility.Collapsed;
if (OverlayThemePanel != null)
- OverlayThemePanel.Visibility = showCommon ? Visibility.Visible : Visibility.Collapsed;
+ OverlayThemePanel.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed;
if (OverlayThemeStylePanel != null)
- OverlayThemeStylePanel.Visibility = showCommon ? Visibility.Visible : Visibility.Collapsed;
+ OverlayThemeStylePanel.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed;
if (OverlayDefaultOutputFormatRow != null)
- OverlayDefaultOutputFormatRow.Visibility = showCommon ? Visibility.Visible : Visibility.Collapsed;
+ OverlayDefaultOutputFormatRow.Visibility = showCowork ? Visibility.Visible : Visibility.Collapsed;
if (OverlayDefaultMoodRow != null)
- OverlayDefaultMoodRow.Visibility = showCommon ? Visibility.Visible : Visibility.Collapsed;
+ OverlayDefaultMoodRow.Visibility = showCowork ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayPdfExportPathRow != null)
+ OverlayPdfExportPathRow.Visibility = showChat ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayToggleImageInput != null)
+ OverlayToggleImageInput.Visibility = showChat ? Visibility.Visible : Visibility.Collapsed;
if (OverlayModelEditorPanel != null)
- OverlayModelEditorPanel.Visibility = showCommon || showService ? Visibility.Visible : Visibility.Collapsed;
+ OverlayModelEditorPanel.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed;
if (OverlayAnchorPermission != null)
- OverlayAnchorPermission.Visibility = showPermission ? Visibility.Visible : Visibility.Collapsed;
+ OverlayAnchorPermission.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed;
if (OverlayFolderDataUsageRow != null)
- OverlayFolderDataUsageRow.Visibility = showCommon || showPermission ? Visibility.Visible : Visibility.Collapsed;
+ OverlayFolderDataUsageRow.Visibility = showShared || showCowork ? Visibility.Visible : Visibility.Collapsed;
if (OverlayTlsRow != null)
- OverlayTlsRow.Visibility = showCommon || showService || showPermission ? Visibility.Visible : Visibility.Collapsed;
+ OverlayTlsRow.Visibility = showChat ? Visibility.Visible : Visibility.Collapsed;
if (OverlayAnchorAdvanced != null)
- OverlayAnchorAdvanced.Visibility = showAdvanced ? Visibility.Visible : Visibility.Collapsed;
+ OverlayAnchorAdvanced.Visibility = showShared ? Visibility.Visible : Visibility.Collapsed;
+ if (TxtOverlayContextCompactTriggerPercent != null)
+ TxtOverlayContextCompactTriggerPercent.Visibility = Visibility.Collapsed;
if (OverlayMaxContextTokensRow != null)
- OverlayMaxContextTokensRow.Visibility = showAdvanced ? Visibility.Visible : Visibility.Collapsed;
+ OverlayMaxContextTokensRow.Visibility = showShared ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayTemperatureRow != null)
+ OverlayTemperatureRow.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
if (OverlayMaxRetryRow != null)
- OverlayMaxRetryRow.Visibility = showAdvanced ? Visibility.Visible : Visibility.Collapsed;
+ OverlayMaxRetryRow.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayMaxAgentIterationsRow != null)
+ OverlayMaxAgentIterationsRow.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayDeveloperRuntimePanel != null)
+ OverlayDeveloperRuntimePanel.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayDeveloperExtraPanel != null)
+ OverlayDeveloperExtraPanel.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
if (OverlayAdvancedTogglePanel != null)
- OverlayAdvancedTogglePanel.Visibility = showAdvanced ? Visibility.Visible : Visibility.Collapsed;
+ OverlayAdvancedTogglePanel.Visibility = showDev || showCowork || showCode || showTools || showEtc ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayToolsInfoPanel != null)
+ OverlayToolsInfoPanel.Visibility = showTools ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayToolsRuntimePanel != null)
+ OverlayToolsRuntimePanel.Visibility = showTools ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayEtcInfoPanel != null)
+ OverlayEtcInfoPanel.Visibility = showEtc ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayEtcRuntimePanel != null)
+ OverlayEtcRuntimePanel.Visibility = showEtc ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayToggleProactiveCompact != null)
+ OverlayToggleProactiveCompact.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayToggleSkillSystem != null)
+ OverlayToggleSkillSystem.Visibility = showEtc ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayToggleToolHooks != null)
+ OverlayToggleToolHooks.Visibility = showTools ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayToggleHookInputMutation != null)
+ OverlayToggleHookInputMutation.Visibility = showTools ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayToggleHookPermissionUpdate != null)
+ OverlayToggleHookPermissionUpdate.Visibility = showTools ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayToggleCoworkVerification != null)
+ OverlayToggleCoworkVerification.Visibility = showCowork ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayToggleCodeVerification != null)
+ OverlayToggleCodeVerification.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayToggleCodeReview != null)
+ OverlayToggleCodeReview.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayToggleParallelTools != null)
+ OverlayToggleParallelTools.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayToggleProjectRules != null)
+ OverlayToggleProjectRules.Visibility = showEtc ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayToggleAgentMemory != null)
+ OverlayToggleAgentMemory.Visibility = showEtc ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayTogglePlanModeTools != null)
+ OverlayTogglePlanModeTools.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayToggleWorktreeTools != null)
+ OverlayToggleWorktreeTools.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayToggleTeamTools != null)
+ OverlayToggleTeamTools.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed;
+ if (OverlayToggleCronTools != null)
+ OverlayToggleCronTools.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed;
+
+ if (showTools || showEtc)
+ RefreshOverlayEtcPanels();
}
private void RefreshOverlaySettingsPanel()
{
RefreshOverlayVisualState(loadDeferredInputs: true);
+ RefreshOverlayEtcPanels();
+ }
+
+ private void RefreshOverlayEtcPanels()
+ {
+ var llm = _settings.Settings.Llm;
+
+ if (OverlaySkillsFolderPathText != null)
+ {
+ var defaultFolder = System.IO.Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "AxCopilot",
+ "skills");
+ OverlaySkillsFolderPathText.Text = string.IsNullOrWhiteSpace(llm.SkillsFolderPath)
+ ? defaultFolder
+ : llm.SkillsFolderPath.Trim();
+ }
+
+ if (TxtOverlaySlashPopupPageSize != null)
+ TxtOverlaySlashPopupPageSize.Text = Math.Clamp(llm.SlashPopupPageSize, 3, 20).ToString();
+ if (SldOverlaySlashPopupPageSize != null)
+ SldOverlaySlashPopupPageSize.Value = Math.Clamp(llm.SlashPopupPageSize, 3, 20);
+ if (TxtOverlaySlashPopupPageSizeValue != null)
+ TxtOverlaySlashPopupPageSizeValue.Text = Math.Clamp(llm.SlashPopupPageSize, 3, 20).ToString();
+ if (TxtOverlayToolHookTimeoutMs != null)
+ TxtOverlayToolHookTimeoutMs.Text = Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000).ToString();
+ if (SldOverlayToolHookTimeoutMs != null)
+ SldOverlayToolHookTimeoutMs.Value = Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000);
+ if (TxtOverlayToolHookTimeoutMsValue != null)
+ TxtOverlayToolHookTimeoutMsValue.Text = $"{Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000) / 1000}s";
+ if (TxtOverlayMaxFavoriteSlashCommands != null)
+ TxtOverlayMaxFavoriteSlashCommands.Text = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30).ToString();
+ if (SldOverlayMaxFavoriteSlashCommands != null)
+ SldOverlayMaxFavoriteSlashCommands.Value = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30);
+ if (TxtOverlayMaxFavoriteSlashCommandsValue != null)
+ TxtOverlayMaxFavoriteSlashCommandsValue.Text = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30).ToString();
+ if (TxtOverlayMaxRecentSlashCommands != null)
+ TxtOverlayMaxRecentSlashCommands.Text = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50).ToString();
+ if (SldOverlayMaxRecentSlashCommands != null)
+ SldOverlayMaxRecentSlashCommands.Value = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50);
+ if (TxtOverlayMaxRecentSlashCommandsValue != null)
+ TxtOverlayMaxRecentSlashCommandsValue.Text = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50).ToString();
+ if (ChkOverlayEnableDragDropAiActions != null)
+ ChkOverlayEnableDragDropAiActions.IsChecked = llm.EnableDragDropAiActions;
+ if (ChkOverlayDragDropAutoSend != null)
+ ChkOverlayDragDropAutoSend.IsChecked = llm.DragDropAutoSend;
+
+ BuildOverlayBlockedItems();
+ BuildOverlayHookCards();
+ BuildOverlaySkillListPanel();
+ BuildOverlayFallbackModelsPanel();
+ BuildOverlayMcpServerCards();
+ BuildOverlayToolRegistryPanel();
+ }
+
+ private void BuildOverlayBlockedItems()
+ {
+ if (OverlayBlockedPathsPanel != null)
+ {
+ OverlayBlockedPathsPanel.Children.Clear();
+ foreach (var path in _settings.Settings.Llm.BlockedPaths.Where(x => !string.IsNullOrWhiteSpace(x)))
+ {
+ OverlayBlockedPathsPanel.Children.Add(new Border
+ {
+ Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.White,
+ BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(6),
+ Padding = new Thickness(8, 4, 8, 4),
+ Margin = new Thickness(0, 0, 0, 4),
+ Child = new TextBlock
+ {
+ Text = path,
+ FontSize = 11.5,
+ FontFamily = new FontFamily("Consolas, Malgun Gothic"),
+ Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray
+ }
+ });
+ }
+ }
+
+ if (OverlayBlockedExtensionsPanel != null)
+ {
+ OverlayBlockedExtensionsPanel.Children.Clear();
+ foreach (var ext in _settings.Settings.Llm.BlockedExtensions.Where(x => !string.IsNullOrWhiteSpace(x)))
+ {
+ OverlayBlockedExtensionsPanel.Children.Add(new Border
+ {
+ Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.White,
+ BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(6),
+ Padding = new Thickness(8, 4, 8, 4),
+ Margin = new Thickness(0, 0, 6, 6),
+ Child = new TextBlock
+ {
+ Text = ext,
+ FontSize = 11.5,
+ FontFamily = new FontFamily("Consolas, Malgun Gothic"),
+ Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray
+ }
+ });
+ }
+ }
+ }
+
+ private void BuildOverlaySkillListPanel()
+ {
+ if (OverlaySkillListPanel == null)
+ return;
+
+ OverlaySkillListPanel.Children.Clear();
+ var skills = SkillService.Skills.ToList();
+ if (skills.Count == 0)
+ {
+ OverlaySkillListPanel.Children.Add(new TextBlock
+ {
+ Text = "로드된 스킬이 없습니다.",
+ FontSize = 11,
+ Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray
+ });
+ return;
+ }
+
+ var unavailable = skills
+ .Where(skill => !skill.IsAvailable)
+ .OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ var autoSkills = skills
+ .Where(skill => skill.IsAvailable && (!skill.UserInvocable || !string.IsNullOrWhiteSpace(skill.Paths)))
+ .OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ var directSkills = skills
+ .Where(skill => skill.IsAvailable && skill.UserInvocable && string.IsNullOrWhiteSpace(skill.Paths))
+ .OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ AddOverlaySkillSection("overlay-skill-direct", "직접 호출 스킬", "슬래시(/)로 직접 실행하는 스킬입니다.", directSkills, "#2563EB");
+ AddOverlaySkillSection("overlay-skill-auto", "자동/조건부 스킬", "조건에 따라 자동으로 붙거나 보조적으로 동작하는 스킬입니다.", autoSkills, "#0F766E");
+ AddOverlaySkillSection("overlay-skill-unavailable", "현재 사용 불가", "필요한 런타임이 없어 지금은 호출되지 않는 스킬입니다.", unavailable, "#9A3412");
+ }
+
+ private void AddOverlaySkillSection(string key, string title, string subtitle, List skills, string accentHex)
+ {
+ if (OverlaySkillListPanel == null || skills.Count == 0)
+ return;
+
+ var body = new StackPanel();
+ foreach (var skill in skills)
+ {
+ var card = new Border
+ {
+ Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.White,
+ BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(8),
+ Padding = new Thickness(10, 8, 10, 8),
+ Margin = new Thickness(0, 0, 0, 6),
+ };
+
+ var stack = new StackPanel();
+ stack.Children.Add(new TextBlock
+ {
+ Text = "/" + skill.Name,
+ FontSize = 12.5,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black
+ });
+
+ if (!string.IsNullOrWhiteSpace(skill.Label) &&
+ !string.Equals(skill.Label.Trim(), skill.Name, StringComparison.OrdinalIgnoreCase))
+ {
+ stack.Children.Add(new TextBlock
+ {
+ Text = skill.Label.Trim(),
+ Margin = new Thickness(0, 3, 0, 0),
+ FontSize = 11,
+ Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray
+ });
+ }
+
+ stack.Children.Add(new TextBlock
+ {
+ Text = string.IsNullOrWhiteSpace(skill.Description)
+ ? (string.IsNullOrWhiteSpace(skill.Label) ? skill.Name : skill.Label)
+ : skill.Description,
+ Margin = new Thickness(0, 4, 0, 0),
+ FontSize = 11,
+ TextWrapping = TextWrapping.Wrap,
+ Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray
+ });
+
+ card.Child = stack;
+ body.Children.Add(card);
+ }
+
+ OverlaySkillListPanel.Children.Add(CreateOverlayCollapsibleSection(
+ key,
+ $"{title} ({skills.Count})",
+ subtitle,
+ body,
+ defaultExpanded: false,
+ accentHex: accentHex));
+ }
+
+ private void BuildOverlayFallbackModelsPanel()
+ {
+ if (OverlayFallbackModelsPanel == null)
+ return;
+
+ OverlayFallbackModelsPanel.Children.Clear();
+ var llm = _settings.Settings.Llm;
+ var sections = new[]
+ {
+ ("ollama", "Ollama"),
+ ("vllm", "vLLM"),
+ ("gemini", "Gemini"),
+ ("claude", "Claude")
+ };
+
+ foreach (var (service, label) in sections)
+ {
+ var candidates = GetModelCandidates(service);
+ OverlayFallbackModelsPanel.Children.Add(new TextBlock
+ {
+ Text = label,
+ FontSize = 11.5,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
+ Margin = new Thickness(0, 0, 0, 6)
+ });
+
+ if (candidates.Count == 0)
+ {
+ OverlayFallbackModelsPanel.Children.Add(new TextBlock
+ {
+ Text = "등록된 모델 없음",
+ FontSize = 10.5,
+ Margin = new Thickness(8, 0, 0, 8),
+ Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray
+ });
+ continue;
+ }
+
+ foreach (var candidate in candidates)
+ {
+ var enabled = llm.FallbackModels.Any(x => x.Equals(candidate.Id, StringComparison.OrdinalIgnoreCase));
+ var grid = new Grid();
+ grid.ColumnDefinitions.Add(new ColumnDefinition());
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+
+ var nameText = new TextBlock
+ {
+ Text = candidate.Label,
+ FontSize = 11.5,
+ Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black
+ };
+ var stateText = new TextBlock
+ {
+ Text = enabled ? "사용" : "미사용",
+ FontSize = 10.5,
+ Foreground = enabled
+ ? (TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue)
+ : (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray),
+ Margin = new Thickness(12, 0, 0, 0)
+ };
+ Grid.SetColumn(stateText, 1);
+ grid.Children.Add(nameText);
+ grid.Children.Add(stateText);
+
+ OverlayFallbackModelsPanel.Children.Add(new Border
+ {
+ Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.White,
+ BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(8),
+ Padding = new Thickness(10, 7, 10, 7),
+ Margin = new Thickness(0, 0, 0, 6),
+ Child = grid
+ });
+ }
+ }
+ }
+
+ private void BuildOverlayMcpServerCards()
+ {
+ if (OverlayMcpServerListPanel == null)
+ return;
+
+ OverlayMcpServerListPanel.Children.Clear();
+ var servers = _settings.Settings.Llm.McpServers;
+ if (servers == null || servers.Count == 0)
+ {
+ OverlayMcpServerListPanel.Children.Add(new TextBlock
+ {
+ Text = "등록된 MCP 서버가 없습니다.",
+ FontSize = 11,
+ Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray
+ });
+ return;
+ }
+
+ foreach (var server in servers)
+ {
+ var card = new Border
+ {
+ Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.White,
+ BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(8),
+ Padding = new Thickness(10, 8, 10, 8),
+ Margin = new Thickness(0, 0, 0, 6)
+ };
+ var stack = new StackPanel();
+ stack.Children.Add(new TextBlock
+ {
+ Text = server.Name,
+ FontSize = 12,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black
+ });
+ stack.Children.Add(new TextBlock
+ {
+ Text = $"{server.Command} {(server.Enabled ? "· 활성" : "· 비활성")}",
+ Margin = new Thickness(0, 3, 0, 0),
+ FontSize = 10.5,
+ TextWrapping = TextWrapping.Wrap,
+ Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray
+ });
+ card.Child = stack;
+ OverlayMcpServerListPanel.Children.Add(card);
+ }
+ }
+
+ private void BuildOverlayToolRegistryPanel()
+ {
+ if (OverlayToolRegistryPanel == null)
+ return;
+
+ OverlayToolRegistryPanel.Children.Clear();
+ var grouped = _toolRegistry.All
+ .GroupBy(tool => GetOverlayToolCategory(tool.Name))
+ .OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ foreach (var group in grouped)
+ {
+ var body = new StackPanel();
+ foreach (var tool in group.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase))
+ {
+ var isDisabled = _settings.Settings.Llm.DisabledTools.Contains(tool.Name, StringComparer.OrdinalIgnoreCase);
+ var row = new Border
+ {
+ Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.White,
+ BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(8),
+ Padding = new Thickness(10, 8, 10, 8),
+ Margin = new Thickness(0, 0, 0, 6),
+ };
+
+ var grid = new Grid();
+ grid.ColumnDefinitions.Add(new ColumnDefinition());
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+
+ var rowStack = new StackPanel { Margin = new Thickness(0, 0, 12, 0) };
+ rowStack.Children.Add(new TextBlock
+ {
+ Text = tool.Name,
+ FontSize = 12,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = isDisabled
+ ? (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray)
+ : (TryFindResource("PrimaryText") as Brush ?? Brushes.Black)
+ });
+ rowStack.Children.Add(new TextBlock
+ {
+ Text = (isDisabled ? "비활성" : "활성") + " · " + tool.Description,
+ Margin = new Thickness(0, 4, 0, 0),
+ FontSize = 10.8,
+ TextWrapping = TextWrapping.Wrap,
+ Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray
+ });
+ grid.Children.Add(rowStack);
+
+ var toggle = new CheckBox
+ {
+ IsChecked = !isDisabled,
+ VerticalAlignment = VerticalAlignment.Center,
+ Style = TryFindResource("ToggleSwitch") as Style,
+ };
+ toggle.Checked += (_, _) =>
+ {
+ _settings.Settings.Llm.DisabledTools.RemoveAll(name => string.Equals(name, tool.Name, StringComparison.OrdinalIgnoreCase));
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ RefreshOverlayEtcPanels();
+ };
+ toggle.Unchecked += (_, _) =>
+ {
+ if (!_settings.Settings.Llm.DisabledTools.Contains(tool.Name, StringComparer.OrdinalIgnoreCase))
+ _settings.Settings.Llm.DisabledTools.Add(tool.Name);
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ RefreshOverlayEtcPanels();
+ };
+ Grid.SetColumn(toggle, 1);
+ grid.Children.Add(toggle);
+
+ row.Child = grid;
+ body.Children.Add(row);
+ }
+
+ OverlayToolRegistryPanel.Children.Add(CreateOverlayCollapsibleSection(
+ "overlay-tool-" + group.Key,
+ $"{group.Key} ({group.Count()})",
+ "카테고리별 도구 목록입니다. 펼치면 상세 이름과 사용 여부를 바로 바꿀 수 있습니다.",
+ body,
+ defaultExpanded: false,
+ accentHex: "#4F46E5"));
+ }
+ }
+
+ private void BuildOverlayHookCards()
+ {
+ if (OverlayHookListPanel == null)
+ return;
+
+ OverlayHookListPanel.Children.Clear();
+
+ var hooks = _settings.Settings.Llm.AgentHooks;
+ if (hooks.Count == 0)
+ {
+ OverlayHookListPanel.Children.Add(new TextBlock
+ {
+ Text = "등록된 훅이 없습니다.",
+ FontSize = 11,
+ Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray
+ });
+ return;
+ }
+
+ var body = new StackPanel();
+ var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
+ var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
+ var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue;
+
+ for (int i = 0; i < hooks.Count; i++)
+ {
+ var hook = hooks[i];
+ var idx = i;
+ var card = new Border
+ {
+ Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke,
+ BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(8),
+ Padding = new Thickness(10, 8, 10, 8),
+ Margin = new Thickness(0, 0, 0, 6),
+ };
+
+ var grid = new Grid();
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ grid.ColumnDefinitions.Add(new ColumnDefinition());
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+
+ var toggle = new CheckBox
+ {
+ IsChecked = hook.Enabled,
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = new Thickness(0, 0, 8, 0),
+ Style = TryFindResource("ToggleSwitch") as Style,
+ };
+ toggle.Checked += (_, _) =>
+ {
+ hook.Enabled = true;
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ };
+ toggle.Unchecked += (_, _) =>
+ {
+ hook.Enabled = false;
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ };
+ Grid.SetColumn(toggle, 0);
+ grid.Children.Add(toggle);
+
+ var info = new StackPanel();
+ var header = new StackPanel { Orientation = Orientation.Horizontal };
+ header.Children.Add(new TextBlock
+ {
+ Text = hook.Name,
+ FontSize = 12.5,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = primaryText,
+ });
+ header.Children.Add(new Border
+ {
+ Background = BrushFromHex(hook.Timing == "pre" ? "#FFEDD5" : "#DCFCE7"),
+ BorderBrush = BrushFromHex(hook.Timing == "pre" ? "#FDBA74" : "#86EFAC"),
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(5),
+ Padding = new Thickness(5, 1, 5, 1),
+ Margin = new Thickness(6, 0, 0, 0),
+ Child = new TextBlock
+ {
+ Text = hook.Timing == "pre" ? "PRE" : "POST",
+ FontSize = 9.5,
+ FontWeight = FontWeights.Bold,
+ Foreground = BrushFromHex(hook.Timing == "pre" ? "#9A3412" : "#166534"),
+ }
+ });
+ if (!string.IsNullOrWhiteSpace(hook.ToolName) && hook.ToolName != "*")
+ {
+ header.Children.Add(new Border
+ {
+ Background = BrushFromHex("#EEF2FF"),
+ BorderBrush = BrushFromHex("#C7D2FE"),
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(5),
+ Padding = new Thickness(5, 1, 5, 1),
+ Margin = new Thickness(6, 0, 0, 0),
+ Child = new TextBlock
+ {
+ Text = hook.ToolName,
+ FontSize = 9.5,
+ Foreground = BrushFromHex("#3730A3"),
+ }
+ });
+ }
+ info.Children.Add(header);
+ info.Children.Add(new TextBlock
+ {
+ Text = Path.GetFileName(hook.ScriptPath),
+ Margin = new Thickness(0, 4, 0, 0),
+ FontSize = 11,
+ Foreground = secondaryText
+ });
+ if (!string.IsNullOrWhiteSpace(hook.Arguments))
+ {
+ info.Children.Add(new TextBlock
+ {
+ Text = hook.Arguments,
+ Margin = new Thickness(0, 3, 0, 0),
+ FontSize = 10.5,
+ TextWrapping = TextWrapping.Wrap,
+ Foreground = secondaryText
+ });
+ }
+ Grid.SetColumn(info, 1);
+ grid.Children.Add(info);
+
+ var editBtn = new Border
+ {
+ Cursor = Cursors.Hand,
+ Padding = new Thickness(6),
+ Margin = new Thickness(6, 0, 2, 0),
+ VerticalAlignment = VerticalAlignment.Center,
+ Child = new TextBlock
+ {
+ Text = "\uE70F",
+ FontFamily = new FontFamily("Segoe MDL2 Assets"),
+ FontSize = 12,
+ Foreground = accentBrush,
+ }
+ };
+ editBtn.MouseLeftButtonUp += (_, _) => ShowOverlayHookEditDialog(hooks[idx], idx);
+ Grid.SetColumn(editBtn, 2);
+ grid.Children.Add(editBtn);
+
+ var deleteBtn = new Border
+ {
+ Cursor = Cursors.Hand,
+ Padding = new Thickness(6),
+ VerticalAlignment = VerticalAlignment.Center,
+ Child = new TextBlock
+ {
+ Text = "\uE74D",
+ FontFamily = new FontFamily("Segoe MDL2 Assets"),
+ FontSize = 12,
+ Foreground = BrushFromHex("#DC2626"),
+ }
+ };
+ deleteBtn.MouseLeftButtonUp += (_, _) =>
+ {
+ hooks.RemoveAt(idx);
+ BuildOverlayHookCards();
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ };
+ Grid.SetColumn(deleteBtn, 3);
+ grid.Children.Add(deleteBtn);
+
+ card.Child = grid;
+ body.Children.Add(card);
+ }
+
+ OverlayHookListPanel.Children.Add(CreateOverlayCollapsibleSection(
+ "overlay-hooks",
+ $"등록된 훅 ({hooks.Count})",
+ "도구 실행 전후에 연결되는 스크립트입니다.",
+ body,
+ defaultExpanded: false,
+ accentHex: "#0F766E"));
+ }
+
+ private Border CreateOverlayCollapsibleSection(string key, string title, string subtitle, UIElement content, bool defaultExpanded, string accentHex)
+ {
+ var itemBackground = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke;
+ var launcherBackground = TryFindResource("LauncherBackground") as Brush ?? Brushes.White;
+ var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray;
+ var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
+ var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
+ var expanded = _overlaySectionExpandedStates.TryGetValue(key, out var stored) ? stored : defaultExpanded;
+
+ var bodyBorder = new Border
+ {
+ Margin = new Thickness(0, 10, 0, 0),
+ Child = content,
+ Visibility = expanded ? Visibility.Visible : Visibility.Collapsed
+ };
+
+ var caret = new TextBlock
+ {
+ Text = expanded ? "\uE70D" : "\uE76C",
+ FontFamily = new FontFamily("Segoe MDL2 Assets"),
+ FontSize = 11,
+ Foreground = secondaryText,
+ VerticalAlignment = VerticalAlignment.Center
+ };
+
+ var headerGrid = new Grid();
+ headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+
+ var headerText = new StackPanel();
+ headerText.Children.Add(new TextBlock
+ {
+ Text = title,
+ FontSize = 12.5,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = primaryText
+ });
+ if (!string.IsNullOrWhiteSpace(subtitle))
+ {
+ headerText.Children.Add(new TextBlock
+ {
+ Text = subtitle,
+ Margin = new Thickness(0, 4, 0, 0),
+ FontSize = 10.8,
+ TextWrapping = TextWrapping.Wrap,
+ Foreground = secondaryText
+ });
+ }
+ headerGrid.Children.Add(headerText);
+ Grid.SetColumn(caret, 1);
+ headerGrid.Children.Add(caret);
+
+ var headerBorder = new Border
+ {
+ Background = launcherBackground,
+ BorderBrush = BrushFromHex(accentHex),
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(8),
+ Padding = new Thickness(10, 8, 10, 8),
+ Cursor = Cursors.Hand,
+ Child = headerGrid
+ };
+
+ void Toggle()
+ {
+ var nextExpanded = bodyBorder.Visibility != Visibility.Visible;
+ bodyBorder.Visibility = nextExpanded ? Visibility.Visible : Visibility.Collapsed;
+ caret.Text = nextExpanded ? "\uE70D" : "\uE76C";
+ _overlaySectionExpandedStates[key] = nextExpanded;
+ }
+
+ headerBorder.MouseLeftButtonUp += (_, _) => Toggle();
+
+ return new Border
+ {
+ Background = itemBackground,
+ BorderBrush = borderBrush,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(10),
+ Padding = new Thickness(10),
+ Margin = new Thickness(0, 0, 0, 8),
+ Child = new StackPanel
+ {
+ Children =
+ {
+ headerBorder,
+ bodyBorder
+ }
+ }
+ };
+ }
+
+ private static TextBlock CreateOverlayPlaceholder(string text, Brush foreground, string? currentValue)
+ {
+ return new TextBlock
+ {
+ Text = text,
+ FontSize = 13,
+ Foreground = foreground,
+ Opacity = 0.45,
+ IsHitTestVisible = false,
+ VerticalAlignment = VerticalAlignment.Center,
+ Padding = new Thickness(14, 8, 14, 8),
+ Visibility = string.IsNullOrEmpty(currentValue) ? Visibility.Visible : Visibility.Collapsed,
+ };
+ }
+
+ private void ShowOverlayHookEditDialog(AgentHookEntry? existing, int index)
+ {
+ var bgBrush = TryFindResource("LauncherBackground") as Brush ?? BrushFromHex("#FFFFFF");
+ var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
+ var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
+ var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray;
+ var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke;
+ var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
+
+ var isNew = existing == null;
+ var dlg = new Window
+ {
+ Title = isNew ? "훅 추가" : "훅 편집",
+ Width = 420,
+ SizeToContent = SizeToContent.Height,
+ WindowStartupLocation = WindowStartupLocation.CenterOwner,
+ Owner = this,
+ ResizeMode = ResizeMode.NoResize,
+ WindowStyle = WindowStyle.None,
+ AllowsTransparency = true,
+ Background = Brushes.Transparent,
+ ShowInTaskbar = false
+ };
+
+ var border = new Border
+ {
+ Background = bgBrush,
+ CornerRadius = new CornerRadius(12),
+ BorderBrush = borderBrush,
+ BorderThickness = new Thickness(1),
+ Padding = new Thickness(20)
+ };
+ var stack = new StackPanel();
+ stack.Children.Add(new TextBlock
+ {
+ Text = isNew ? "훅 추가" : "훅 편집",
+ FontSize = 15,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = fgBrush,
+ Margin = new Thickness(0, 0, 0, 14)
+ });
+
+ dlg.KeyDown += (_, e) => { if (e.Key == Key.Escape) dlg.Close(); };
+
+ stack.Children.Add(new TextBlock { Text = "훅 이름", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 4) });
+ var nameBox = new TextBox
+ {
+ Text = existing?.Name ?? "",
+ FontSize = 13,
+ Foreground = fgBrush,
+ Background = itemBg,
+ BorderBrush = borderBrush,
+ Padding = new Thickness(12, 8, 12, 8)
+ };
+ var nameHolder = CreateOverlayPlaceholder("예: 코드 리뷰 후 알림", subFgBrush, existing?.Name);
+ nameBox.TextChanged += (_, _) => nameHolder.Visibility = string.IsNullOrEmpty(nameBox.Text) ? Visibility.Visible : Visibility.Collapsed;
+ var nameGrid = new Grid();
+ nameGrid.Children.Add(nameBox);
+ nameGrid.Children.Add(nameHolder);
+ stack.Children.Add(nameGrid);
+
+ stack.Children.Add(new TextBlock { Text = "대상 도구 (* = 모든 도구)", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 10, 0, 4) });
+ var toolBox = new TextBox
+ {
+ Text = existing?.ToolName ?? "*",
+ FontSize = 13,
+ Foreground = fgBrush,
+ Background = itemBg,
+ BorderBrush = borderBrush,
+ Padding = new Thickness(12, 8, 12, 8)
+ };
+ var toolHolder = CreateOverlayPlaceholder("예: file_write, grep_tool", subFgBrush, existing?.ToolName ?? "*");
+ toolBox.TextChanged += (_, _) => toolHolder.Visibility = string.IsNullOrEmpty(toolBox.Text) ? Visibility.Visible : Visibility.Collapsed;
+ var toolGrid = new Grid();
+ toolGrid.Children.Add(toolBox);
+ toolGrid.Children.Add(toolHolder);
+ stack.Children.Add(toolGrid);
+
+ stack.Children.Add(new TextBlock { Text = "실행 타이밍", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 10, 0, 4) });
+ var timingPanel = new StackPanel { Orientation = Orientation.Horizontal };
+ var preRadio = new RadioButton
+ {
+ Content = "Pre (실행 전)",
+ Foreground = fgBrush,
+ FontSize = 13,
+ Margin = new Thickness(0, 0, 16, 0),
+ IsChecked = (existing?.Timing ?? "post") == "pre"
+ };
+ var postRadio = new RadioButton
+ {
+ Content = "Post (실행 후)",
+ Foreground = fgBrush,
+ FontSize = 13,
+ IsChecked = (existing?.Timing ?? "post") != "pre"
+ };
+ timingPanel.Children.Add(preRadio);
+ timingPanel.Children.Add(postRadio);
+ stack.Children.Add(timingPanel);
+
+ stack.Children.Add(new TextBlock { Text = "스크립트 경로 (.bat / .cmd / .ps1)", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 10, 0, 4) });
+ var pathGrid = new Grid();
+ pathGrid.ColumnDefinitions.Add(new ColumnDefinition());
+ pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ var pathInnerGrid = new Grid();
+ var pathBox = new TextBox
+ {
+ Text = existing?.ScriptPath ?? "",
+ FontSize = 13,
+ Foreground = fgBrush,
+ Background = itemBg,
+ BorderBrush = borderBrush,
+ Padding = new Thickness(12, 8, 12, 8)
+ };
+ var pathHolder = CreateOverlayPlaceholder("예: C:\\scripts\\review-notify.bat", subFgBrush, existing?.ScriptPath);
+ pathBox.TextChanged += (_, _) => pathHolder.Visibility = string.IsNullOrEmpty(pathBox.Text) ? Visibility.Visible : Visibility.Collapsed;
+ pathInnerGrid.Children.Add(pathBox);
+ pathInnerGrid.Children.Add(pathHolder);
+ pathGrid.Children.Add(pathInnerGrid);
+
+ var browseBtn = new Border
+ {
+ Background = itemBg,
+ CornerRadius = new CornerRadius(6),
+ Padding = new Thickness(10, 6, 10, 6),
+ Margin = new Thickness(6, 0, 0, 0),
+ Cursor = Cursors.Hand,
+ VerticalAlignment = VerticalAlignment.Center,
+ Child = new TextBlock
+ {
+ Text = "...",
+ FontSize = 13,
+ Foreground = accentBrush
+ }
+ };
+ browseBtn.MouseLeftButtonUp += (_, _) =>
+ {
+ var ofd = new OpenFileDialog
+ {
+ Filter = "스크립트 파일|*.bat;*.cmd;*.ps1|모든 파일|*.*",
+ Title = "훅 스크립트 선택",
+ };
+ if (ofd.ShowDialog() == true)
+ pathBox.Text = ofd.FileName;
+ };
+ Grid.SetColumn(browseBtn, 1);
+ pathGrid.Children.Add(browseBtn);
+ stack.Children.Add(pathGrid);
+
+ stack.Children.Add(new TextBlock { Text = "추가 인수 (선택)", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 10, 0, 4) });
+ var argsBox = new TextBox
+ {
+ Text = existing?.Arguments ?? "",
+ FontSize = 13,
+ Foreground = fgBrush,
+ Background = itemBg,
+ BorderBrush = borderBrush,
+ Padding = new Thickness(12, 8, 12, 8)
+ };
+ var argsHolder = CreateOverlayPlaceholder("예: --verbose --output log.txt", subFgBrush, existing?.Arguments);
+ argsBox.TextChanged += (_, _) => argsHolder.Visibility = string.IsNullOrEmpty(argsBox.Text) ? Visibility.Visible : Visibility.Collapsed;
+ var argsGrid = new Grid();
+ argsGrid.Children.Add(argsBox);
+ argsGrid.Children.Add(argsHolder);
+ stack.Children.Add(argsGrid);
+
+ var btnRow = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ HorizontalAlignment = HorizontalAlignment.Right,
+ Margin = new Thickness(0, 16, 0, 0)
+ };
+ var cancelBorder = new Border
+ {
+ Background = itemBg,
+ CornerRadius = new CornerRadius(8),
+ Padding = new Thickness(16, 8, 16, 8),
+ Margin = new Thickness(0, 0, 8, 0),
+ Cursor = Cursors.Hand,
+ Child = new TextBlock { Text = "취소", FontSize = 13, Foreground = subFgBrush }
+ };
+ cancelBorder.MouseLeftButtonUp += (_, _) => dlg.Close();
+ btnRow.Children.Add(cancelBorder);
+
+ var saveBorder = new Border
+ {
+ Background = accentBrush,
+ CornerRadius = new CornerRadius(8),
+ Padding = new Thickness(16, 8, 16, 8),
+ Cursor = Cursors.Hand,
+ Child = new TextBlock
+ {
+ Text = isNew ? "추가" : "저장",
+ FontSize = 13,
+ Foreground = Brushes.White,
+ FontWeight = FontWeights.SemiBold
+ }
+ };
+ saveBorder.MouseLeftButtonUp += (_, _) =>
+ {
+ if (string.IsNullOrWhiteSpace(nameBox.Text) || string.IsNullOrWhiteSpace(pathBox.Text))
+ {
+ CustomMessageBox.Show("훅 이름과 스크립트 경로를 입력하세요.", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ var entry = new AgentHookEntry
+ {
+ Name = nameBox.Text.Trim(),
+ ToolName = string.IsNullOrWhiteSpace(toolBox.Text) ? "*" : toolBox.Text.Trim(),
+ Timing = preRadio.IsChecked == true ? "pre" : "post",
+ ScriptPath = pathBox.Text.Trim(),
+ Arguments = argsBox.Text.Trim(),
+ Enabled = existing?.Enabled ?? true,
+ };
+
+ var hooks = _settings.Settings.Llm.AgentHooks;
+ if (isNew)
+ hooks.Add(entry);
+ else if (index >= 0 && index < hooks.Count)
+ hooks[index] = entry;
+
+ BuildOverlayHookCards();
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ dlg.Close();
+ };
+ btnRow.Children.Add(saveBorder);
+ stack.Children.Add(btnRow);
+
+ border.Child = stack;
+ dlg.Content = border;
+ dlg.ShowDialog();
+ }
+
+ private static string GetOverlayToolCategory(string toolName)
+ {
+ return toolName switch
+ {
+ "file_read" or "file_write" or "file_edit" or "glob" or "grep_tool" or "folder_map" or "document_read" or "file_manage" or "file_info" or "multi_read"
+ => "파일/검색",
+ "process" or "build_run" or "dev_env_detect" or "snippet_runner"
+ => "프로세스/빌드",
+ "search_codebase" or "code_search" or "code_review" or "lsp" or "test_loop" or "git_tool" or "project_rules" or "project_rule" or "diff_preview"
+ => "코드 분석",
+ "excel_create" or "docx_create" or "csv_create" or "markdown_create" or "html_create" or "chart_create" or "batch_create" or "pptx_create" or "document_review" or "format_convert" or "document_planner" or "document_assembler" or "template_render"
+ => "문서 생성",
+ "json_tool" or "regex_tool" or "diff_tool" or "base64_tool" or "hash_tool" or "datetime_tool" or "math_tool" or "xml_tool" or "sql_tool" or "data_pivot" or "text_summarize"
+ => "데이터 처리",
+ "clipboard_tool" or "notify_tool" or "env_tool" or "zip_tool" or "http_tool" or "open_external" or "image_analyze" or "file_watch"
+ => "시스템/환경",
+ "spawn_agent" or "wait_agents" or "memory" or "skill_manager" or "user_ask" or "task_tracker" or "todo_write" or "task_create" or "task_get" or "task_list" or "task_update" or "task_stop" or "task_output" or "enter_plan_mode" or "exit_plan_mode" or "enter_worktree" or "exit_worktree" or "team_create" or "team_delete" or "cron_create" or "cron_delete" or "cron_list" or "suggest_actions" or "checkpoint" or "playbook"
+ => "에이전트",
+ _ => "기타"
+ };
}
private void CmbOverlayService_SelectionChanged(object sender, SelectionChangedEventArgs e)
@@ -14744,41 +16338,79 @@ public partial class ChatWindow : Window
SetOverlayCardSelection(OverlaySvcClaudeCard, service is "claude" or "sigmoid");
}
+ private void RefreshOverlayTokenPresetCards()
+ {
+ var llm = _settings.Settings.Llm;
+ var compact = llm.ContextCompactTriggerPercent switch
+ {
+ <= 60 => 60,
+ <= 70 => 70,
+ <= 80 => 80,
+ _ => 90
+ };
+ SetOverlayCardSelection(OverlayCompact60Card, compact == 60);
+ SetOverlayCardSelection(OverlayCompact70Card, compact == 70);
+ SetOverlayCardSelection(OverlayCompact80Card, compact == 80);
+ SetOverlayCardSelection(OverlayCompact90Card, compact == 90);
+
+ var context = llm.MaxContextTokens switch
+ {
+ <= 4096 => 4096,
+ <= 16384 => 16384,
+ <= 65536 => 65536,
+ <= 262144 => 262144,
+ _ => 1_000_000
+ };
+ SetOverlayCardSelection(OverlayContext4KCard, context == 4096);
+ SetOverlayCardSelection(OverlayContext16KCard, context == 16384);
+ SetOverlayCardSelection(OverlayContext64KCard, context == 65536);
+ SetOverlayCardSelection(OverlayContext256KCard, context == 262144);
+ SetOverlayCardSelection(OverlayContext1MCard, context == 1_000_000);
+ }
+
private void RefreshOverlayModeButtons()
{
var llm = _settings.Settings.Llm;
- var operationModeLabel = OperationModePolicy.Normalize(_settings.Settings.OperationMode) == OperationModePolicy.ExternalMode
- ? "사외 모드"
- : "사내 모드";
- var dataUsageLabel = _folderDataUsage switch
- {
- "active" => "적극 활용",
- "passive" => "소극 활용",
- _ => "활용하지 않음",
- };
- var permissionLabel = PermissionModeCatalog.ToDisplayLabel(PermissionModeCatalog.NormalizeGlobalMode(llm.FilePermission));
- var planLabel = PlanModeLabel(llm.PlanMode);
- var reasoningLabel = ReasoningLabel(llm.AgentDecisionLevel);
-
- BtnOverlayOperationMode.Content = GetQuickActionLabel("모드", operationModeLabel);
- BtnOverlayFolderDataUsage.Content = GetQuickActionLabel("데이터", dataUsageLabel);
- BtnOverlayPermission.Content = GetQuickActionLabel("권한", permissionLabel);
- BtnOverlayPlanMode.Content = GetQuickActionLabel("계획", planLabel);
- BtnOverlayReasoning.Content = GetQuickActionLabel("추론", reasoningLabel);
- if (BtnOverlayFastMode != null)
- {
- BtnOverlayFastMode.Content = GetQuickActionLabel("Fast", llm.FreeTierMode ? "켜짐" : "꺼짐");
- ApplyQuickActionVisual(BtnOverlayFastMode, llm.FreeTierMode, "#ECFDF5", "#166534");
- }
- ApplyQuickActionVisual(BtnOverlayReasoning, !string.Equals(llm.AgentDecisionLevel, "normal", StringComparison.OrdinalIgnoreCase), "#EEF2FF", "#1D4ED8");
- ApplyQuickActionVisual(BtnOverlayPlanMode, !string.Equals(llm.PlanMode, "off", StringComparison.OrdinalIgnoreCase), "#EEF2FF", "#4338CA");
- ApplyQuickActionVisual(BtnOverlayPermission,
- !string.Equals(PermissionModeCatalog.NormalizeGlobalMode(llm.FilePermission), PermissionModeCatalog.Deny, StringComparison.OrdinalIgnoreCase),
- "#FFF7ED",
- "#C2410C");
+ SelectComboTag(CmbOverlayOperationMode, OperationModePolicy.Normalize(_settings.Settings.OperationMode));
+ SelectComboTag(CmbOverlayFolderDataUsage, _folderDataUsage);
+ SelectComboTag(CmbOverlayPermission, PermissionModeCatalog.NormalizeGlobalMode(llm.FilePermission));
+ SelectComboTag(CmbOverlayPlanMode, llm.PlanMode);
+ SelectComboTag(CmbOverlayReasoning, llm.AgentDecisionLevel);
+ SelectComboTag(CmbOverlayFastMode, llm.FreeTierMode ? "on" : "off");
+ SelectComboTag(CmbOverlayDefaultOutputFormat, llm.DefaultOutputFormat ?? "auto");
+ SelectComboTag(CmbOverlayDefaultMood, _selectedMood ?? llm.DefaultMood ?? "modern");
+ SelectComboTag(CmbOverlayAgentLogLevel, llm.AgentLogLevel ?? "simple");
UpdateDataUsageUI();
}
+ private static void SelectComboTag(ComboBox? combo, string? tag)
+ {
+ if (combo == null) return;
+ var normalized = (tag ?? "").Trim();
+ combo.SelectedItem = combo.Items
+ .OfType()
+ .FirstOrDefault(item => string.Equals(item.Tag as string, normalized, StringComparison.OrdinalIgnoreCase));
+ }
+
+ private void PopulateOverlayMoodCombo()
+ {
+ if (CmbOverlayDefaultMood == null)
+ return;
+
+ var selected = _selectedMood ?? _settings.Settings.Llm.DefaultMood ?? "modern";
+ CmbOverlayDefaultMood.Items.Clear();
+ foreach (var mood in TemplateService.AllMoods)
+ {
+ CmbOverlayDefaultMood.Items.Add(new ComboBoxItem
+ {
+ Content = $"{mood.Icon} {mood.Label}",
+ Tag = mood.Key
+ });
+ }
+
+ SelectComboTag(CmbOverlayDefaultMood, selected);
+ }
+
private static string GetQuickActionLabel(string title, string value)
=> $"{title} · {value}";
@@ -14907,6 +16539,226 @@ public partial class ChatWindow : Window
}
}
+ private void BuildOverlayRegisteredModelsPanel(string service)
+ {
+ if (OverlayRegisteredModelsPanel == null || OverlayRegisteredModelsHeader == null || BtnOverlayAddModel == null)
+ return;
+
+ var normalized = NormalizeOverlayService(service);
+ var supportsRegistered = SupportsOverlayRegisteredModels(normalized);
+ OverlayRegisteredModelsHeader.Visibility = supportsRegistered ? Visibility.Visible : Visibility.Collapsed;
+ OverlayRegisteredModelsPanel.Visibility = supportsRegistered ? Visibility.Visible : Visibility.Collapsed;
+ BtnOverlayAddModel.Visibility = supportsRegistered ? Visibility.Visible : Visibility.Collapsed;
+
+ OverlayRegisteredModelsPanel.Children.Clear();
+ if (!supportsRegistered)
+ return;
+
+ var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
+ var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
+ var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
+ var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.White;
+ var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? BrushFromHex("#F8FAFC");
+ var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue;
+
+ var models = _settings.Settings.Llm.RegisteredModels
+ .Where(m => string.Equals(m.Service, normalized, StringComparison.OrdinalIgnoreCase))
+ .OrderBy(m => m.Alias, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ if (models.Count == 0)
+ {
+ OverlayRegisteredModelsPanel.Children.Add(new Border
+ {
+ CornerRadius = new CornerRadius(10),
+ BorderBrush = borderBrush,
+ BorderThickness = new Thickness(1),
+ Background = Brushes.Transparent,
+ Padding = new Thickness(12, 10, 12, 10),
+ Child = new TextBlock
+ {
+ Text = "등록된 모델이 없습니다. `모델 추가`로 사내 모델을 먼저 등록하세요.",
+ FontSize = 11.5,
+ TextWrapping = TextWrapping.Wrap,
+ Foreground = secondaryText,
+ }
+ });
+ return;
+ }
+
+ foreach (var model in models)
+ {
+ var decryptedModelName = Services.CryptoService.DecryptIfEnabled(model.EncryptedModelName, IsOverlayEncryptionEnabled);
+ var displayName = string.IsNullOrWhiteSpace(model.Alias) ? decryptedModelName : model.Alias;
+ var endpointText = string.IsNullOrWhiteSpace(model.Endpoint) ? "기본 서버 사용" : model.Endpoint;
+ var authLabel = string.Equals(model.AuthType, "cp4d", StringComparison.OrdinalIgnoreCase) ? "CP4D" : "Bearer";
+ var isActive = string.Equals(model.EncryptedModelName, _settings.Settings.Llm.Model, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(decryptedModelName, _settings.Settings.Llm.Model, StringComparison.OrdinalIgnoreCase);
+
+ var row = new Border
+ {
+ CornerRadius = new CornerRadius(10),
+ BorderBrush = isActive ? accentBrush : borderBrush,
+ BorderThickness = new Thickness(1),
+ Background = isActive ? (TryFindResource("HintBackground") as Brush ?? BrushFromHex("#EFF6FF")) : itemBg,
+ Padding = new Thickness(12, 10, 12, 10),
+ Margin = new Thickness(0, 0, 0, 8),
+ };
+
+ var grid = new Grid();
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+
+ var info = new StackPanel();
+ info.Children.Add(new TextBlock
+ {
+ Text = displayName,
+ FontSize = 12,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = primaryText,
+ });
+ info.Children.Add(new TextBlock
+ {
+ Text = string.IsNullOrWhiteSpace(decryptedModelName) ? "(모델명 없음)" : decryptedModelName,
+ Margin = new Thickness(0, 3, 0, 0),
+ FontSize = 11,
+ Foreground = secondaryText,
+ });
+ info.Children.Add(new TextBlock
+ {
+ Text = $"엔드포인트: {endpointText} · 인증: {authLabel}",
+ Margin = new Thickness(0, 4, 0, 0),
+ FontSize = 10.5,
+ TextWrapping = TextWrapping.Wrap,
+ Foreground = secondaryText,
+ });
+ Grid.SetColumn(info, 0);
+ grid.Children.Add(info);
+
+ var actions = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ HorizontalAlignment = HorizontalAlignment.Right,
+ VerticalAlignment = VerticalAlignment.Top,
+ };
+
+ Button CreateAction(string text, RoutedEventHandler onClick, Brush foreground)
+ {
+ var button = new Button
+ {
+ Content = text,
+ Style = TryFindResource("GhostBtn") as Style,
+ Padding = new Thickness(8, 4, 8, 4),
+ Margin = new Thickness(6, 0, 0, 0),
+ Foreground = foreground,
+ Cursor = Cursors.Hand,
+ };
+ button.Click += onClick;
+ return button;
+ }
+
+ actions.Children.Add(CreateAction("선택", (_, _) =>
+ {
+ CommitOverlayModelSelection(model.EncryptedModelName);
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }, accentBrush));
+
+ actions.Children.Add(CreateAction("편집", (_, _) => EditOverlayRegisteredModel(model), primaryText));
+ actions.Children.Add(CreateAction("삭제", (_, _) => DeleteOverlayRegisteredModel(model), BrushFromHex("#DC2626")));
+
+ Grid.SetColumn(actions, 1);
+ grid.Children.Add(actions);
+
+ row.Child = grid;
+ row.MouseEnter += (_, _) =>
+ {
+ if (!isActive)
+ row.Background = hoverBg;
+ };
+ row.MouseLeave += (_, _) =>
+ {
+ if (!isActive)
+ row.Background = itemBg;
+ };
+
+ OverlayRegisteredModelsPanel.Children.Add(row);
+ }
+ }
+
+ private void BtnOverlayAddModel_Click(object sender, RoutedEventArgs e)
+ {
+ var service = NormalizeOverlayService(_settings.Settings.Llm.Service);
+ if (!SupportsOverlayRegisteredModels(service))
+ return;
+
+ var dlg = new ModelRegistrationDialog(service) { Owner = this };
+ if (dlg.ShowDialog() != true)
+ return;
+
+ _settings.Settings.Llm.RegisteredModels.Add(new RegisteredModel
+ {
+ Alias = dlg.ModelAlias,
+ EncryptedModelName = Services.CryptoService.EncryptIfEnabled(dlg.ModelName, IsOverlayEncryptionEnabled),
+ Service = service,
+ Endpoint = dlg.Endpoint,
+ ApiKey = dlg.ApiKey,
+ AllowInsecureTls = dlg.AllowInsecureTls,
+ AuthType = dlg.AuthType,
+ Cp4dUrl = dlg.Cp4dUrl,
+ Cp4dUsername = dlg.Cp4dUsername,
+ Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsOverlayEncryptionEnabled),
+ });
+
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: true);
+ }
+
+ private void EditOverlayRegisteredModel(RegisteredModel model)
+ {
+ var currentModel = Services.CryptoService.DecryptIfEnabled(model.EncryptedModelName, IsOverlayEncryptionEnabled);
+ var cp4dPassword = Services.CryptoService.DecryptIfEnabled(model.Cp4dPassword ?? "", IsOverlayEncryptionEnabled);
+ var service = NormalizeOverlayService(model.Service);
+
+ var dlg = new ModelRegistrationDialog(
+ service,
+ model.Alias,
+ currentModel,
+ model.Endpoint,
+ model.ApiKey,
+ model.AllowInsecureTls,
+ model.AuthType ?? "bearer",
+ model.Cp4dUrl ?? "",
+ model.Cp4dUsername ?? "",
+ cp4dPassword)
+ { Owner = this };
+
+ if (dlg.ShowDialog() != true)
+ return;
+
+ model.Alias = dlg.ModelAlias;
+ model.EncryptedModelName = Services.CryptoService.EncryptIfEnabled(dlg.ModelName, IsOverlayEncryptionEnabled);
+ model.Service = service;
+ model.Endpoint = dlg.Endpoint;
+ model.ApiKey = dlg.ApiKey;
+ model.AllowInsecureTls = dlg.AllowInsecureTls;
+ model.AuthType = dlg.AuthType;
+ model.Cp4dUrl = dlg.Cp4dUrl;
+ model.Cp4dUsername = dlg.Cp4dUsername;
+ model.Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsOverlayEncryptionEnabled);
+
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: true);
+ }
+
+ private void DeleteOverlayRegisteredModel(RegisteredModel model)
+ {
+ var result = CustomMessageBox.Show($"'{model.Alias}' 모델을 삭제하시겠습니까?", "모델 삭제",
+ MessageBoxButton.YesNo, MessageBoxImage.Question);
+ if (result != MessageBoxResult.Yes)
+ return;
+
+ _settings.Settings.Llm.RegisteredModels.Remove(model);
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: true);
+ }
+
private void SetOverlayCardSelection(Border border, bool selected)
{
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue;
@@ -15000,6 +16852,103 @@ public partial class ChatWindow : Window
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
}
+ private void CmbOverlayFastMode_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing || CmbOverlayFastMode.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag)
+ return;
+
+ _settings.Settings.Llm.FreeTierMode = string.Equals(tag, "on", StringComparison.OrdinalIgnoreCase);
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void CmbOverlayReasoning_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing || CmbOverlayReasoning.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag)
+ return;
+
+ _settings.Settings.Llm.AgentDecisionLevel = tag;
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void CmbOverlayPlanMode_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing || CmbOverlayPlanMode.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag)
+ return;
+
+ _settings.Settings.Llm.PlanMode = tag;
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void CmbOverlayPermission_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing || CmbOverlayPermission.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag)
+ return;
+
+ var normalized = PermissionModeCatalog.NormalizeGlobalMode(tag);
+ var llm = _settings.Settings.Llm;
+ llm.FilePermission = normalized;
+ llm.DefaultAgentPermission = normalized;
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void CmbOverlayDefaultOutputFormat_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing || CmbOverlayDefaultOutputFormat.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag)
+ return;
+
+ _settings.Settings.Llm.DefaultOutputFormat = tag;
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void CmbOverlayDefaultMood_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing || CmbOverlayDefaultMood.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag)
+ return;
+
+ _selectedMood = tag;
+ _settings.Settings.Llm.DefaultMood = tag;
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void CmbOverlayOperationMode_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing || CmbOverlayOperationMode.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag)
+ return;
+
+ var normalized = OperationModePolicy.Normalize(tag);
+ var current = OperationModePolicy.Normalize(_settings.Settings.OperationMode);
+ if (string.Equals(normalized, current, StringComparison.OrdinalIgnoreCase))
+ return;
+
+ if (string.Equals(normalized, OperationModePolicy.ExternalMode, StringComparison.OrdinalIgnoreCase) &&
+ !PromptOverlayPasswordDialog("운영 모드 변경", "사내/사외 모드 변경", "비밀번호를 입력하세요."))
+ {
+ RefreshOverlayModeButtons();
+ return;
+ }
+
+ _settings.Settings.OperationMode = normalized;
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void CmbOverlayFolderDataUsage_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing || CmbOverlayFolderDataUsage.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag)
+ return;
+
+ _folderDataUsage = tag;
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
+ private void CmbOverlayAgentLogLevel_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_isOverlaySettingsSyncing || CmbOverlayAgentLogLevel.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag)
+ return;
+
+ _settings.Settings.Llm.AgentLogLevel = tag;
+ PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
+ }
+
private void BtnOpenFullSettings_Click(object sender, RoutedEventArgs e)
{
if (System.Windows.Application.Current is App app)
@@ -16682,6 +18631,13 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
|| TokenUsageThresholdMarker == null || CompactNowLabel == null)
return;
+ var showContextUsage = _activeTab is "Cowork" or "Code";
+ TokenUsageCard.Visibility = showContextUsage ? Visibility.Visible : Visibility.Collapsed;
+ if (!showContextUsage)
+ {
+ return;
+ }
+
var llm = _settings.Settings.Llm;
var maxContextTokens = Math.Clamp(llm.MaxContextTokens, 1024, 1_000_000);
var triggerPercent = Math.Clamp(llm.ContextCompactTriggerPercent, 10, 95);
@@ -17573,6 +19529,7 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
InputBox.Clear();
InputBox.Focus();
+ UpdateInputBoxHeight();
RefreshDraftQueueUi();
if (queuedItem == null)
@@ -17628,6 +19585,7 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
InputBox.Clear();
InputBox.Focus();
+ UpdateInputBoxHeight();
RefreshDraftQueueUi();
}
@@ -17643,16 +19601,12 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
_draftQueueProcessor.PromoteReadyBlockedItems(session, _activeTab, _storage);
}
- var inputText = InputBox?.Text?.Trim() ?? "";
- var hasInput = !string.IsNullOrWhiteSpace(inputText);
var summary = _appState.GetDraftQueueSummary(_activeTab);
var items = _appState.GetDraftQueueItems(_activeTab);
- DraftPreviewCard.Visibility = hasInput ? Visibility.Visible : Visibility.Collapsed;
- BtnDraftEnqueue.IsEnabled = hasInput;
- DraftPreviewText.Text = hasInput
- ? $"{TruncateForStatus(inputText, 96)}{(summary.TotalCount > 0 ? $" · 대기 {summary.QueuedCount} · 실행 {summary.RunningCount}" : "")}"
- : "";
+ DraftPreviewCard.Visibility = Visibility.Collapsed;
+ BtnDraftEnqueue.IsEnabled = false;
+ DraftPreviewText.Text = string.Empty;
RebuildDraftQueuePanel(items);
}
@@ -18033,6 +19987,7 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
InputBox.Text = next.Text;
InputBox.CaretIndex = InputBox.Text.Length;
InputBox.Focus();
+ UpdateInputBoxHeight();
RefreshDraftQueueUi();
_ = SendMessageAsync();
}