빌드 부산물 추적 해제와 AX Agent 대기열·composer UI 정리
Some checks failed
Release Gate / gate (push) Has been cancelled

- .gitignore에 bin/obj/publish 및 IDE/OS/비밀정보 패턴 추가
- Git 인덱스에서 publish 및 src 하위 bin/obj 빌드 부산물 추적을 해제하여 저장소 노이즈를 정리
- DraftQueue를 실행 대기/최근 결과 섹션과 상태 요약 pill 구조로 재정리
- composer 상단 모델/컨텍스트/프리셋 줄과 하단 작업 위치 칩 UI를 더 평평한 시각 언어로 통일
- 워크스페이스·브랜치·워크트리 패널에 공통 row 및 요약 strip을 적용해 panel UX를 정돈
- README.md와 docs/DEVELOPMENT.md, docs/AGENT_ROADMAP.md, AGENTS.md 이력을 갱신

검증
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\
- 경고 0개, 오류 0개
This commit is contained in:
2026-04-04 23:03:42 +09:00
parent a027ea4f9a
commit 72a8c0d541
908 changed files with 5661 additions and 77215 deletions

View File

@@ -1,4 +1,5 @@
using System.Linq;
using System.Windows.Data;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
@@ -60,6 +61,7 @@ public partial class SettingsWindow : Window
BuildDockBarSettings();
BuildTextActionCommandsPanel();
MoveBlockSectionToEtc();
BuildServiceModelPanels();
// 스킬이 아직 로드되지 않았으면 백그라운드에서 로드 후 UI 구성
var app = System.Windows.Application.Current as App;
@@ -83,15 +85,62 @@ public partial class SettingsWindow : Window
ApplyAiEnabledState(app?.SettingsService?.Settings.AiEnabled ?? false, init: true);
ApplyOperationModeState(app?.SettingsService?.Settings.OperationMode);
InitializeDisplayModeUi();
SyncAgentSelectionCards();
};
}
private void SyncAgentSelectionCards()
{
var service = (_vm.LlmService ?? "").Trim().ToLowerInvariant();
if (AgentServiceCardOllama != null) AgentServiceCardOllama.IsChecked = service == "ollama";
if (AgentServiceCardVllm != null) AgentServiceCardVllm.IsChecked = service == "vllm";
if (AgentServiceCardGemini != null) AgentServiceCardGemini.IsChecked = service == "gemini";
if (AgentServiceCardClaude != null) AgentServiceCardClaude.IsChecked = service == "claude";
var permission = Services.Agent.PermissionModeCatalog.NormalizeGlobalMode(_vm.DefaultAgentPermission);
if (AgentPermissionCardAsk != null) AgentPermissionCardAsk.IsChecked = permission == "Ask";
if (AgentPermissionCardPlan != null) AgentPermissionCardPlan.IsChecked = permission == "Plan";
if (AgentPermissionCardAuto != null) AgentPermissionCardAuto.IsChecked = permission == "Auto";
if (AgentPermissionCardDeny != null) AgentPermissionCardDeny.IsChecked = permission == "Deny";
var decision = (_vm.AgentDecisionLevel ?? "normal").Trim().ToLowerInvariant();
if (AgentDecisionCardMinimal != null) AgentDecisionCardMinimal.IsChecked = decision == "minimal";
if (AgentDecisionCardNormal != null) AgentDecisionCardNormal.IsChecked = decision == "normal";
if (AgentDecisionCardDetailed != null) AgentDecisionCardDetailed.IsChecked = decision == "detailed";
var planMode = (_vm.PlanMode ?? "off").Trim().ToLowerInvariant();
if (AgentPlanModeCardOff != null) AgentPlanModeCardOff.IsChecked = planMode == "off";
if (AgentPlanModeCardAlways != null) AgentPlanModeCardAlways.IsChecked = planMode == "always";
if (AgentPlanModeCardAuto != null) AgentPlanModeCardAuto.IsChecked = planMode == "auto";
var operationMode = OperationModePolicy.Normalize(_vm.OperationMode);
if (AgentOperationModeInternal != null) AgentOperationModeInternal.IsChecked = operationMode == OperationModePolicy.InternalMode;
if (AgentOperationModeExternal != null) AgentOperationModeExternal.IsChecked = operationMode == OperationModePolicy.ExternalMode;
var maxContextTokens = _vm.LlmMaxContextTokens;
if (AgentContextTokens4K != null) AgentContextTokens4K.IsChecked = maxContextTokens <= 4096;
if (AgentContextTokens16K != null) AgentContextTokens16K.IsChecked = maxContextTokens > 4096 && maxContextTokens <= 16384;
if (AgentContextTokens64K != null) AgentContextTokens64K.IsChecked = maxContextTokens > 16384 && maxContextTokens <= 65536;
if (AgentContextTokens256K != null) AgentContextTokens256K.IsChecked = maxContextTokens > 65536 && maxContextTokens <= 262144;
if (AgentContextTokens1M != null) AgentContextTokens1M.IsChecked = maxContextTokens > 262144;
var retentionDays = _vm.LlmRetentionDays;
if (AgentRetentionDays7 != null) AgentRetentionDays7.IsChecked = retentionDays == 7;
if (AgentRetentionDays30 != null) AgentRetentionDays30.IsChecked = retentionDays == 30;
if (AgentRetentionDays90 != null) AgentRetentionDays90.IsChecked = retentionDays == 90;
if (AgentRetentionDaysUnlimited != null) AgentRetentionDaysUnlimited.IsChecked = retentionDays == 0;
var logLevel = (_vm.AgentLogLevel ?? "simple").Trim().ToLowerInvariant();
if (AgentLogLevelSimple != null) AgentLogLevelSimple.IsChecked = logLevel == "simple";
if (AgentLogLevelDetailed != null) AgentLogLevelDetailed.IsChecked = logLevel == "detailed";
if (AgentLogLevelDebug != null) AgentLogLevelDebug.IsChecked = logLevel == "debug";
}
private void InitializeDisplayModeUi()
{
var app = System.Windows.Application.Current as App;
if (app?.SettingsService?.Settings?.Llm != null)
app.SettingsService.Settings.Llm.AgentUiExpressionLevel = "rich";
SetDisplayMode("rich", persist: false);
var saved = app?.SettingsService?.Settings?.Llm?.AgentUiExpressionLevel;
SetDisplayMode(saved ?? "balanced", persist: false);
}
private void DisplayMode_Checked(object sender, RoutedEventArgs e)
@@ -107,6 +156,19 @@ public partial class SettingsWindow : Window
SetDisplayMode(next, persist: true);
}
private void AgentDisplayMode_Checked(object sender, RoutedEventArgs e)
{
if (_isDisplayModeSyncing) return;
var rb = sender as RadioButton;
var next = rb?.Name switch
{
"AgentDisplayModeRich" => "rich",
"AgentDisplayModeSimple" => "simple",
_ => "balanced",
};
SetDisplayMode(next, persist: true);
}
private void SetDisplayMode(string mode, bool persist)
{
mode = NormalizeDisplayMode(mode);
@@ -117,9 +179,15 @@ public partial class SettingsWindow : Window
var rich = GetDisplayModeRadio("DisplayModeRich");
var balanced = GetDisplayModeRadio("DisplayModeBalanced");
var simple = GetDisplayModeRadio("DisplayModeSimple");
var agentRich = GetDisplayModeRadio("AgentDisplayModeRich");
var agentBalanced = GetDisplayModeRadio("AgentDisplayModeBalanced");
var agentSimple = GetDisplayModeRadio("AgentDisplayModeSimple");
if (rich != null) rich.IsChecked = mode == "rich";
if (balanced != null) balanced.IsChecked = mode == "balanced";
if (simple != null) simple.IsChecked = mode == "simple";
if (agentRich != null) agentRich.IsChecked = mode == "rich";
if (agentBalanced != null) agentBalanced.IsChecked = mode == "balanced";
if (agentSimple != null) agentSimple.IsChecked = mode == "simple";
}
finally
{
@@ -183,21 +251,17 @@ public partial class SettingsWindow : Window
if (AgentTabCommon == null) return;
SetSubTabVisible(AgentTabCommon, true);
SetSubTabVisible(AgentTabChat, mode != "simple");
SetSubTabVisible(AgentTabChat, true);
SetSubTabVisible(AgentTabCoworkCode, true);
SetSubTabVisible(AgentTabCowork, mode == "rich");
SetSubTabVisible(AgentTabCode, mode == "rich");
SetSubTabVisible(AgentTabTools, mode != "simple");
SetSubTabVisible(AgentTabEtc, mode != "simple");
SetSubTabVisible(AgentTabDev, mode == "rich");
SetSubTabVisible(AgentTabDev, true);
SetSubTabVisible(AgentTabCowork, false);
SetSubTabVisible(AgentTabCode, false);
SetSubTabVisible(AgentTabTools, false);
SetSubTabVisible(AgentTabEtc, false);
if (AgentTabCommon.IsChecked != true
&& AgentTabChat.IsChecked != true
&& AgentTabCoworkCode.IsChecked != true
&& AgentTabCowork.IsChecked != true
&& AgentTabCode.IsChecked != true
&& AgentTabTools.IsChecked != true
&& AgentTabEtc.IsChecked != true
&& AgentTabDev.IsChecked != true)
{
AgentTabCommon.IsChecked = true;
@@ -327,66 +391,66 @@ public partial class SettingsWindow : Window
{
("파일/검색", "\uE8B7", "#F59E0B", new[]
{
("file_read", "파일 읽기 텍스트/바이너리 파일의 내용을 읽습니다"),
("file_write", "파일 쓰기 새 파일을 생성하거나 기존 파일을 덮어씁니다"),
("file_edit", "파일 편집 줄 번호 기반으로 파일의 특정 부분을 수정합니다"),
("glob", "파일 패턴 검색 글로브 패턴으로 파일을 찾습니다 (예: **/*.cs)"),
("grep_tool", "텍스트 검색 정규식으로 파일 내용을 검색합니다"),
("folder_map", "폴더 구조 프로젝트의 디렉토리 트리를 조회합니다"),
("document_read", "문서 읽기 PDF, DOCX 등 문서 파일의 텍스트를 추출합니다"),
("file_read", "파일 읽기 ? 텍스트/바이너리 파일의 내용을 읽습니다"),
("file_write", "파일 쓰기 ? 새 파일을 생성하거나 기존 파일을 덮어씁니다"),
("file_edit", "파일 편집 ? 줄 번호 기반으로 파일의 특정 부분을 수정합니다"),
("glob", "파일 패턴 검색 ? 글로브 패턴으로 파일을 찾습니다 (예: **/*.cs)"),
("grep_tool", "텍스트 검색 ? 정규식으로 파일 내용을 검색합니다"),
("folder_map", "폴더 구조 ? 프로젝트의 디렉토리 트리를 조회합니다"),
("document_read", "문서 읽기 ? PDF, DOCX 등 문서 파일의 텍스트를 추출합니다"),
}),
("프로세스/빌드", "\uE756", "#06B6D4", new[]
{
("process", "프로세스 실행 외부 명령/프로그램을 실행합니다"),
("build_run", "빌드/테스트 프로젝트 타입을 감지하여 빌드/테스트를 실행합니다"),
("dev_env_detect", "개발 환경 감지 IDE, 런타임, 빌드 도구를 자동으로 인식합니다"),
("process", "프로세스 실행 ? 외부 명령/프로그램을 실행합니다"),
("build_run", "빌드/테스트 ? 프로젝트 타입을 감지하여 빌드/테스트를 실행합니다"),
("dev_env_detect", "개발 환경 감지 ? IDE, 런타임, 빌드 도구를 자동으로 인식합니다"),
}),
("코드 분석", "\uE943", "#818CF8", new[]
{
("search_codebase", "코드 키워드 검색 TF-IDF 기반으로 관련 코드를 찾습니다"),
("code_review", "AI 코드 리뷰 Git diff 분석, 파일 정적 검사, PR 요약"),
("lsp", "LSP 인텔리전스 정의 이동, 참조 검색, 심볼 목록 (C#/TS/Python)"),
("test_loop", "테스트 루프 테스트 생성/실행/분석 자동화"),
("git_tool", "Git 작업 status, log, diff, commit 등 Git 명령 실행"),
("snippet_runner", "코드 실행 C#/Python/JavaScript 스니펫 즉시 실행"),
("search_codebase", "코드 키워드 검색 ? TF-IDF 기반으로 관련 코드를 찾습니다"),
("code_review", "AI 코드 리뷰 ? Git diff 분석, 파일 정적 검사, PR 요약"),
("lsp", "LSP 인텔리전스 ? 정의 이동, 참조 검색, 심볼 목록 (C#/TS/Python)"),
("test_loop", "테스트 루프 ? 테스트 생성/실행/분석 자동화"),
("git_tool", "Git 작업 ? status, log, diff, commit 등 Git 명령 실행"),
("snippet_runner", "코드 실행 ? C#/Python/JavaScript 스니펫 즉시 실행"),
}),
("문서 생성", "\uE8A5", "#34D399", new[]
{
("excel_create", "Excel 생성 .xlsx 스프레드시트를 생성합니다"),
("docx_create", "Word 생성 .docx 문서를 생성합니다"),
("csv_create", "CSV 생성 CSV 파일을 생성합니다"),
("markdown_create", "마크다운 생성 .md 파일을 생성합니다"),
("html_create", "HTML 생성 HTML 파일을 생성합니다"),
("chart_create", "차트 생성 데이터 시각화 차트를 생성합니다"),
("batch_create", "배치 생성 스크립트 파일을 생성합니다"),
("document_review", "문서 검증 문서 품질을 검사합니다"),
("format_convert", "포맷 변환 문서 형식을 변환합니다"),
("excel_create", "Excel 생성 ? .xlsx 스프레드시트를 생성합니다"),
("docx_create", "Word 생성 ? .docx 문서를 생성합니다"),
("csv_create", "CSV 생성 ? CSV 파일을 생성합니다"),
("markdown_create", "마크다운 생성 ? .md 파일을 생성합니다"),
("html_create", "HTML 생성 ? HTML 파일을 생성합니다"),
("chart_create", "차트 생성 ? 데이터 시각화 차트를 생성합니다"),
("batch_create", "배치 생성 ? 스크립트 파일을 생성합니다"),
("document_review", "문서 검증 ? 문서 품질을 검사합니다"),
("format_convert", "포맷 변환 ? 문서 형식을 변환합니다"),
}),
("데이터 처리", "\uE9F5", "#F59E0B", new[]
{
("json_tool", "JSON 처리 JSON 파싱, 변환, 검증, 포맷팅"),
("regex_tool", "정규식 정규식 테스트, 추출, 치환"),
("diff_tool", "텍스트 비교 두 파일/텍스트 비교, 통합 diff 출력"),
("base64_tool", "인코딩 Base64/URL 인코딩, 디코딩"),
("hash_tool", "해시 계산 MD5/SHA256 해시 계산 (파일/텍스트)"),
("datetime_tool", "날짜/시간 날짜 변환, 타임존, 기간 계산"),
("json_tool", "JSON 처리 ? JSON 파싱, 변환, 검증, 포맷팅"),
("regex_tool", "정규식 ? 정규식 테스트, 추출, 치환"),
("diff_tool", "텍스트 비교 ? 두 파일/텍스트 비교, 통합 diff 출력"),
("base64_tool", "인코딩 ? Base64/URL 인코딩, 디코딩"),
("hash_tool", "해시 계산 ? MD5/SHA256 해시 계산 (파일/텍스트)"),
("datetime_tool", "날짜/시간 ? 날짜 변환, 타임존, 기간 계산"),
}),
("시스템/환경", "\uE770", "#06B6D4", new[]
{
("clipboard_tool", "클립보드 Windows 클립보드 읽기/쓰기 (텍스트/이미지)"),
("notify_tool", "알림 Windows 시스템 알림 전송"),
("env_tool", "환경변수 환경변수 읽기/설정 (프로세스 범위)"),
("zip_tool", "압축 파일 압축(zip) / 해제"),
("http_tool", "HTTP 로컬/사내 HTTP API 호출 (GET/POST)"),
("sql_tool", "SQLite SQLite DB 쿼리 실행 (로컬 파일)"),
("clipboard_tool", "클립보드 ? Windows 클립보드 읽기/쓰기 (텍스트/이미지)"),
("notify_tool", "알림 ? Windows 시스템 알림 전송"),
("env_tool", "환경변수 ? 환경변수 읽기/설정 (프로세스 범위)"),
("zip_tool", "압축 ? 파일 압축(zip) / 해제"),
("http_tool", "HTTP ? 로컬/사내 HTTP API 호출 (GET/POST)"),
("sql_tool", "SQLite ? SQLite DB 쿼리 실행 (로컬 파일)"),
}),
("에이전트", "\uE99A", "#F472B6", new[]
{
("spawn_agent", "서브에이전트 하위 작업을 병렬로 실행하는 서브에이전트를 생성합니다"),
("wait_agents", "에이전트 대기 실행 중인 서브에이전트의 결과를 수집합니다"),
("memory", "에이전트 메모리 프로젝트 규칙, 선호도를 저장/검색합니다"),
("skill_manager", "스킬 관리 스킬 목록 조회, 상세 정보, 재로드"),
("project_rules", "프로젝트 지침 AGENTS.md 개발 지침을 읽고 씁니다"),
("spawn_agent", "서브에이전트 ? 하위 작업을 병렬로 실행하는 서브에이전트를 생성합니다"),
("wait_agents", "에이전트 대기 ? 실행 중인 서브에이전트의 결과를 수집합니다"),
("memory", "에이전트 메모리 ? 프로젝트 규칙, 선호도를 저장/검색합니다"),
("skill_manager", "스킬 관리 ? 스킬 목록 조회, 상세 정보, 재로드"),
("project_rules", "프로젝트 지침 ? AGENTS.md 개발 지침을 읽고 씁니다"),
}),
};
@@ -878,7 +942,7 @@ public partial class SettingsWindow : Window
VerticalAlignment = VerticalAlignment.Center,
Child = new TextBlock
{
Text = " 사용 가능",
Text = "? 사용 가능",
FontSize = 9,
Foreground = new SolidColorBrush(Color.FromRgb(0x34, 0xD3, 0x99)),
FontWeight = FontWeights.SemiBold,
@@ -891,7 +955,7 @@ public partial class SettingsWindow : Window
// 아래 줄: 설명 (뱃지와 구분되도록 위 여백 추가)
row.Children.Add(new TextBlock
{
Text = $"{skill.Label} {skill.Description}",
Text = $"{skill.Label} ? {skill.Description}",
FontSize = 11.5,
Foreground = TryFindResource("SecondaryText") as Brush
?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6666AA")),
@@ -1061,7 +1125,7 @@ public partial class SettingsWindow : Window
ChkDockAutoShow.Unchecked += (_, _) =>
{
launcher.DockBarAutoShow = false; svc.Save();
// 끄기 시에는 설정만 저장 독 바를 토글하지 않음 (이미 표시 중이면 유지, 다음 재시작 시 안 뜸)
// 끄기 시에는 설정만 저장 ? 독 바를 토글하지 않음 (이미 표시 중이면 유지, 다음 재시작 시 안 뜸)
};
ChkDockRainbowGlow.IsChecked = launcher.DockBarRainbowGlow;
@@ -1229,7 +1293,7 @@ public partial class SettingsWindow : Window
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x22, 0xFF, 0xFF, 0xFF));
var shadowColor = TryFindResource("ShadowColor") is Color sc ? sc : Colors.Black;
// 보관 기간 선택 팝업 커스텀 버튼으로 날짜 선택
// 보관 기간 선택 팝업 ? 커스텀 버튼으로 날짜 선택
var popup = new Window
{
WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent,
@@ -1396,7 +1460,7 @@ public partial class SettingsWindow : Window
// ─── 핫키 (콤보박스 선택 방식) ──────────────────────────────────────────
/// <summary>이전 녹화기에서 호출되던 초기화 콤보박스 전환 후 무연산 (호환용)</summary>
/// <summary>이전 녹화기에서 호출되던 초기화 ? 콤보박스 전환 후 무연산 (호환용)</summary>
private void RefreshHotkeyBadges() { /* 콤보박스 SelectedValue 바인딩으로 대체 */ }
/// <summary>현재 핫키가 콤보박스 목록에 없으면 항목으로 추가합니다.</summary>
@@ -1423,7 +1487,7 @@ public partial class SettingsWindow : Window
HotkeyCombo.SelectedIndex = 0;
}
/// <summary>Window-level PreviewKeyDown 핫키 녹화 제거 후 잔여 호출 보호</summary>
/// <summary>Window-level PreviewKeyDown ? 핫키 녹화 제거 후 잔여 호출 보호</summary>
private void Window_PreviewKeyDown(object sender, KeyEventArgs e) { }
/// <summary>WPF Key → HotkeyParser가 인식하는 문자열 이름.</summary>
@@ -1444,11 +1508,11 @@ public partial class SettingsWindow : Window
Key.Up => "Up",
Key.Down => "Down",
Key.Insert => "Insert",
// AZ
// A?Z
>= Key.A and <= Key.Z => key.ToString(),
// 09 (메인 키보드)
// 0?9 (메인 키보드)
>= Key.D0 and <= Key.D9 => ((int)(key - Key.D0)).ToString(),
// F1F12
// F1?F12
>= Key.F1 and <= Key.F12 => key.ToString(),
// 기호
Key.OemTilde => "`",
@@ -1549,7 +1613,7 @@ public partial class SettingsWindow : Window
var count = Services.Agent.SkillService.ImportSkills(dlg.FileName);
if (count > 0)
CustomMessageBox.Show($"스킬 {count}개를 성공적으로 가져왔습니<EFBFBD><EFBFBD>.\n스킬 목록이 갱신됩니다.", "스킬 가져오기");
CustomMessageBox.Show($"스킬 {count}개를 성공적으로 가져왔습니??.\n스킬 목록이 갱신됩니다.", "스킬 가져오기");
else
CustomMessageBox.Show("스킬 가져오기에 실패했습니다.\nzip 파일에 .skill.md 또는 SKILL.md 파일이 포함되어야 합니다.", "스킬 가져오기");
}
@@ -1607,7 +1671,7 @@ public partial class SettingsWindow : Window
{
var cb = new CheckBox
{
Content = $"/{skill.Name} {skill.Label}",
Content = $"/{skill.Name} ? {skill.Label}",
FontSize = 12.5,
Foreground = fgBrush,
Margin = new Thickness(0, 3, 0, 3),
@@ -1710,6 +1774,7 @@ public partial class SettingsWindow : Window
Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsEncryptionEnabled),
});
BuildFallbackModelsPanel();
BuildServiceModelPanels();
}
}
@@ -1738,6 +1803,7 @@ public partial class SettingsWindow : Window
row.Cp4dUsername = dlg.Cp4dUsername;
row.Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsEncryptionEnabled);
BuildFallbackModelsPanel();
BuildServiceModelPanels();
}
}
@@ -1750,6 +1816,7 @@ public partial class SettingsWindow : Window
{
_vm.RegisteredModels.Remove(row);
BuildFallbackModelsPanel();
BuildServiceModelPanels();
}
}
@@ -1795,21 +1862,30 @@ public partial class SettingsWindow : Window
private void AgentSubTab_Checked(object sender, RoutedEventArgs e)
{
if (AgentPanelCommon == null) return; // 초기화 전 방어
AgentPanelCommon.Visibility = AgentTabCommon.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
AgentPanelChat.Visibility = AgentTabChat.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
AgentPanelCoworkCode.Visibility = AgentTabCoworkCode.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
AgentPanelCowork.Visibility = AgentTabCowork.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
AgentPanelCode.Visibility = AgentTabCode.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
var showCommon = AgentTabCommon.IsChecked == true;
var showService = AgentTabChat.IsChecked == true;
var showPermission = AgentTabCoworkCode.IsChecked == true;
var showAdvanced = AgentTabDev.IsChecked == true;
AgentPanelCommon.Visibility = showCommon || showService ? Visibility.Visible : Visibility.Collapsed;
if (AgentCommonOverviewSection != null)
AgentCommonOverviewSection.Visibility = showCommon ? Visibility.Visible : Visibility.Collapsed;
if (AgentCommonRuntimeSection != null)
AgentCommonRuntimeSection.Visibility = showCommon ? Visibility.Visible : Visibility.Collapsed;
if (AgentServiceSection != null)
AgentServiceSection.Visibility = showService ? Visibility.Visible : Visibility.Collapsed;
AgentPanelCoworkCode.Visibility = showPermission ? Visibility.Visible : Visibility.Collapsed;
AgentPanelChat.Visibility = Visibility.Collapsed;
AgentPanelCowork.Visibility = Visibility.Collapsed;
AgentPanelCode.Visibility = Visibility.Collapsed;
if (AgentPanelDev != null)
AgentPanelDev.Visibility = AgentTabDev.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
AgentPanelDev.Visibility = showAdvanced ? Visibility.Visible : Visibility.Collapsed;
if (AgentPanelEtc != null)
AgentPanelEtc.Visibility = AgentTabEtc.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
AgentPanelEtc.Visibility = Visibility.Collapsed;
if (AgentPanelTools != null)
{
var show = AgentTabTools.IsChecked == true;
AgentPanelTools.Visibility = show ? Visibility.Visible : Visibility.Collapsed;
if (show) LoadToolCards();
}
AgentPanelTools.Visibility = Visibility.Collapsed;
}
// ─── 도구 관리 카드 UI ──────────────────────────────────────────────
@@ -2085,6 +2161,320 @@ public partial class SettingsWindow : Window
SvcPanelVllm.Visibility = SvcTabVllm.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
SvcPanelGemini.Visibility = SvcTabGemini.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
SvcPanelSigmoid.Visibility = SvcTabSigmoid.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
BuildServiceModelPanels();
SyncAgentSelectionCards();
}
private void AgentServiceCard_Checked(object sender, RoutedEventArgs e)
{
if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true) return;
var service = rb.Name switch
{
"AgentServiceCardVllm" => "vllm",
"AgentServiceCardGemini" => "gemini",
"AgentServiceCardClaude" => "claude",
_ => "ollama",
};
_vm.LlmService = service;
if (SvcTabOllama != null) SvcTabOllama.IsChecked = service == "ollama";
if (SvcTabVllm != null) SvcTabVllm.IsChecked = service == "vllm";
if (SvcTabGemini != null) SvcTabGemini.IsChecked = service == "gemini";
if (SvcTabSigmoid != null) SvcTabSigmoid.IsChecked = service == "claude";
BuildServiceModelPanels();
}
private void AgentPermissionCard_Checked(object sender, RoutedEventArgs e)
{
if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true) return;
_vm.DefaultAgentPermission = rb.Name switch
{
"AgentPermissionCardPlan" => "Plan",
"AgentPermissionCardAuto" => "Auto",
"AgentPermissionCardDeny" => "Deny",
_ => "Ask",
};
}
private void AgentDecisionCard_Checked(object sender, RoutedEventArgs e)
{
if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true) return;
_vm.AgentDecisionLevel = rb.Name switch
{
"AgentDecisionCardMinimal" => "minimal",
"AgentDecisionCardDetailed" => "detailed",
_ => "normal",
};
}
private void AgentPlanModeCard_Checked(object sender, RoutedEventArgs e)
{
if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true) return;
_vm.PlanMode = rb.Name switch
{
"AgentPlanModeCardAlways" => "always",
"AgentPlanModeCardAuto" => "auto",
_ => "off",
};
}
private void AgentOperationModeCard_Checked(object sender, RoutedEventArgs e)
{
if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true)
return;
var next = rb.Name == "AgentOperationModeExternal"
? OperationModePolicy.ExternalMode
: OperationModePolicy.InternalMode;
var app = System.Windows.Application.Current as App;
var settings = app?.SettingsService?.Settings;
if (settings == null)
return;
var current = OperationModePolicy.Normalize(settings.OperationMode);
if (string.Equals(next, current, StringComparison.OrdinalIgnoreCase))
return;
var ok = PromptPasswordDialog(
"운영 모드 변경 ? 비밀번호 확인",
"\U0001f512 사내/사외 모드 변경",
"비밀번호를 입력하세요:");
if (!ok)
{
ApplyOperationModeState(settings.OperationMode);
return;
}
settings.OperationMode = next;
_vm.OperationMode = next;
app?.SettingsService?.Save();
ApplyOperationModeState(next);
}
private void AgentContextTokensCard_Checked(object sender, RoutedEventArgs e)
{
if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true)
return;
_vm.LlmMaxContextTokens = rb.Name switch
{
"AgentContextTokens16K" => 16384,
"AgentContextTokens64K" => 65536,
"AgentContextTokens256K" => 262144,
"AgentContextTokens1M" => 1_000_000,
_ => 4096,
};
}
private void AgentRetentionDaysCard_Checked(object sender, RoutedEventArgs e)
{
if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true)
return;
_vm.LlmRetentionDays = rb.Name switch
{
"AgentRetentionDays30" => 30,
"AgentRetentionDays90" => 90,
"AgentRetentionDaysUnlimited" => 0,
_ => 7,
};
}
private void AgentLogLevelCard_Checked(object sender, RoutedEventArgs e)
{
if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true)
return;
_vm.AgentLogLevel = rb.Name switch
{
"AgentLogLevelDetailed" => "detailed",
"AgentLogLevelDebug" => "debug",
_ => "simple",
};
}
private void BuildServiceModelPanels()
{
BuildRegisteredModelList("ollama", OllamaRegisteredModelsList);
BuildRegisteredModelList("vllm", VllmRegisteredModelsList);
BuildServiceModelChips("ollama", OllamaModelChipPanel, _vm.OllamaModel, value => _vm.OllamaModel = value);
BuildServiceModelChips("vllm", VllmModelChipPanel, _vm.VllmModel, value => _vm.VllmModel = value);
BuildPresetModelChips(
GeminiModelChipPanel,
_vm.GeminiModel,
value => _vm.GeminiModel = value,
new (string Id, string Label, string Hint)[]
{
("gemini-3.1-pro-preview", "Gemini 3.1 Pro", "최고 성능"),
("gemini-3-flash", "Gemini 3 Flash", "빠른 응답"),
("gemini-3.1-flash-lite-preview", "Gemini 3.1 Flash Lite", "경량"),
("gemini-2.5-flash", "Gemini 2.5 Flash", "균형"),
("gemini-2.5-flash-lite", "Gemini 2.5 Flash Lite", "저비용"),
});
BuildPresetModelChips(
ClaudeModelChipPanel,
_vm.ClaudeModel,
value => _vm.ClaudeModel = value,
new (string Id, string Label, string Hint)[]
{
(ProviderModelIds.SigmoidOpus46, "Claude Opus 4.6", "최고 성능"),
(ProviderModelIds.SigmoidSonnet46, "Claude Sonnet 4.6", "균형"),
(ProviderModelIds.SigmoidHaiku45_20251001, "Claude Haiku 4.5", "경량"),
(ProviderModelIds.SigmoidSonnet45_20250929, "Claude Sonnet 4.5", "이전 안정판"),
(ProviderModelIds.SigmoidOpus4_20250514, "Claude Opus 4", "이전 최고급"),
});
}
private void BuildRegisteredModelList(string service, ItemsControl? target)
{
if (target == null) return;
var view = new ListCollectionView(_vm.RegisteredModels);
view.Filter = item =>
{
if (item is not RegisteredModelRow row) return false;
return string.Equals(row.Service, service, StringComparison.OrdinalIgnoreCase);
};
target.ItemsSource = view;
}
private void BuildServiceModelChips(string service, WrapPanel? panel, string selectedValue, Action<string> applySelection)
{
if (panel == null)
return;
panel.Children.Clear();
var models = _vm.RegisteredModels
.Where(row => string.Equals(row.Service, service, StringComparison.OrdinalIgnoreCase))
.ToList();
if (models.Count == 0)
{
panel.Children.Add(new Border
{
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke,
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(10),
Padding = new Thickness(12, 8, 12, 8),
Child = new TextBlock
{
Text = "등록된 모델이 없습니다. + 모델 추가로 먼저 등록하세요.",
FontSize = 11.5,
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
}
});
return;
}
foreach (var model in models)
{
var isSelected = string.Equals(model.EncryptedModelName, selectedValue, StringComparison.OrdinalIgnoreCase);
var chip = new Border
{
Cursor = Cursors.Hand,
CornerRadius = new CornerRadius(10),
BorderThickness = new Thickness(1),
BorderBrush = isSelected
? (TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue)
: (TryFindResource("BorderColor") as Brush ?? Brushes.LightGray),
Background = isSelected
? (TryFindResource("ItemHoverBackground") as Brush ?? Brushes.AliceBlue)
: Brushes.Transparent,
Padding = new Thickness(12, 9, 12, 9),
Margin = new Thickness(0, 0, 8, 8),
Child = new StackPanel
{
Children =
{
new TextBlock
{
Text = string.IsNullOrWhiteSpace(model.Alias) ? "(이름 없음)" : model.Alias,
FontSize = 12,
FontWeight = isSelected ? FontWeights.SemiBold : FontWeights.Normal,
Foreground = isSelected
? (TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue)
: (TryFindResource("PrimaryText") as Brush ?? Brushes.Black),
},
new TextBlock
{
Text = string.IsNullOrWhiteSpace(model.Endpoint) ? "기본 서버 사용" : model.Endpoint,
FontSize = 10.5,
Margin = new Thickness(0, 3, 0, 0),
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
}
}
}
};
var captured = model.EncryptedModelName;
chip.MouseLeftButtonUp += (_, _) =>
{
applySelection(captured);
BuildServiceModelPanels();
};
panel.Children.Add(chip);
}
}
private void BuildPresetModelChips(
WrapPanel? panel,
string? selectedValue,
Action<string> applySelection,
IEnumerable<(string Id, string Label, string Hint)> models)
{
if (panel == null)
return;
panel.Children.Clear();
foreach (var model in models)
{
var isSelected = string.Equals(model.Id, selectedValue, StringComparison.OrdinalIgnoreCase);
var chip = new Border
{
Cursor = Cursors.Hand,
CornerRadius = new CornerRadius(12),
BorderThickness = new Thickness(1),
BorderBrush = isSelected
? (TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue)
: (TryFindResource("BorderColor") as Brush ?? Brushes.LightGray),
Background = isSelected
? (TryFindResource("ItemHoverBackground") as Brush ?? Brushes.AliceBlue)
: Brushes.Transparent,
Padding = new Thickness(12, 9, 12, 9),
Margin = new Thickness(0, 0, 8, 8),
Child = new StackPanel
{
Children =
{
new TextBlock
{
Text = model.Label,
FontSize = 12,
FontWeight = isSelected ? FontWeights.SemiBold : FontWeights.Normal,
Foreground = isSelected
? (TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue)
: (TryFindResource("PrimaryText") as Brush ?? Brushes.Black),
},
new TextBlock
{
Text = model.Hint,
FontSize = 10.5,
Margin = new Thickness(0, 3, 0, 0),
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
}
}
}
};
var capturedId = model.Id;
chip.MouseLeftButtonUp += (_, _) =>
{
applySelection(capturedId);
BuildServiceModelPanels();
};
panel.Children.Add(chip);
}
}
private void ThemeCard_Click(object sender, RoutedEventArgs e)
@@ -2115,7 +2505,7 @@ public partial class SettingsWindow : Window
// 비밀번호 확인 다이얼로그
var dlg = new Window
{
Title = "개발자 모드 비밀번호 확인",
Title = "개발자 모드 ? 비밀번호 확인",
Width = 340, SizeToContent = SizeToContent.Height,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Owner = this, ResizeMode = ResizeMode.NoResize,
@@ -2169,7 +2559,7 @@ public partial class SettingsWindow : Window
if (dlg.ShowDialog() != true)
{
// 비밀번호 실패/취소 체크 해제 + DevMode 강제 false
// 비밀번호 실패/취소 ? 체크 해제 + DevMode 강제 false
_vm.DevMode = false;
cb.IsChecked = false;
}
@@ -2198,24 +2588,66 @@ public partial class SettingsWindow : Window
{
AiEnabledToggle.IsChecked = enabled;
}
// AX Agent 상세 설정은 전용 창으로 분리 (탭은 숨김)
if (AgentAiEnabledToggle != null && AgentAiEnabledToggle.IsChecked != enabled)
{
AgentAiEnabledToggle.IsChecked = enabled;
}
if (AgentTabItem != null)
AgentTabItem.Visibility = Visibility.Collapsed;
AgentTabItem.Visibility = enabled ? Visibility.Visible : Visibility.Collapsed;
ApplyMainTabVisibility(NormalizeDisplayMode((System.Windows.Application.Current as App)?.SettingsService?.Settings?.Llm?.AgentUiExpressionLevel));
}
public void SelectAgentSettingsTab()
{
if (AgentTabItem == null || MainSettingsTab == null || AgentTabItem.Visibility != Visibility.Visible)
return;
MainSettingsTab.SelectedItem = AgentTabItem;
if (AgentTabCommon != null)
AgentTabCommon.IsChecked = true;
AgentSubTab_Checked(this, new RoutedEventArgs());
Activate();
}
private void BtnAgentSettingsBack_Click(object sender, RoutedEventArgs e)
{
if (MainSettingsTab == null)
return;
var fallback = MainSettingsTab.Items
.OfType<TabItem>()
.FirstOrDefault(t => t != AgentTabItem && t.Visibility == Visibility.Visible);
if (fallback != null)
MainSettingsTab.SelectedItem = fallback;
}
private void BtnAgentShortcut_Click(object sender, RoutedEventArgs e)
{
SelectAgentSettingsTab();
}
private void ApplyOperationModeState(string? mode)
{
if (OperationModeCombo == null)
var normalized = OperationModePolicy.Normalize(mode);
SyncOperationModeCombo(OperationModeCombo, normalized);
if (AgentOperationModeInternal != null) AgentOperationModeInternal.IsChecked = normalized == OperationModePolicy.InternalMode;
if (AgentOperationModeExternal != null) AgentOperationModeExternal.IsChecked = normalized == OperationModePolicy.ExternalMode;
}
private static void SyncOperationModeCombo(ComboBox? combo, string normalized)
{
if (combo == null)
return;
var normalized = OperationModePolicy.Normalize(mode);
foreach (var item in OperationModeCombo.Items.OfType<ComboBoxItem>())
foreach (var item in combo.Items.OfType<ComboBoxItem>())
{
var key = item.Tag as string;
if (string.Equals(key, normalized, StringComparison.OrdinalIgnoreCase))
{
if (!ReferenceEquals(OperationModeCombo.SelectedItem, item))
OperationModeCombo.SelectedItem = item;
if (!ReferenceEquals(combo.SelectedItem, item))
combo.SelectedItem = item;
return;
}
}
@@ -2307,7 +2739,8 @@ public partial class SettingsWindow : Window
private void AiEnabled_Changed(object sender, RoutedEventArgs e)
{
if (!IsLoaded) return;
var tryEnable = AiEnabledToggle?.IsChecked == true;
var sourceToggle = sender as CheckBox;
var tryEnable = sourceToggle?.IsChecked == true;
// 비활성화는 즉시 적용 (비밀번호 불필요)
if (!tryEnable)
@@ -2335,7 +2768,7 @@ public partial class SettingsWindow : Window
var dlg = new Window
{
Title = "AI 기능 활성화 비밀번호 확인",
Title = "AI 기능 활성화 ? 비밀번호 확인",
Width = 340, SizeToContent = SizeToContent.Height,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Owner = this, ResizeMode = ResizeMode.NoResize,
@@ -2399,8 +2832,9 @@ public partial class SettingsWindow : Window
}
else
{
// 취소/실패 토글 원상복구
// 취소/실패 ? 토글 원상복구
if (AiEnabledToggle != null) AiEnabledToggle.IsChecked = false;
if (AgentAiEnabledToggle != null) AgentAiEnabledToggle.IsChecked = false;
}
}
@@ -2421,7 +2855,7 @@ public partial class SettingsWindow : Window
return;
var ok = PromptPasswordDialog(
"운영 모드 변경 비밀번호 확인",
"운영 모드 변경 ? 비밀번호 확인",
"\U0001f512 사내/사외 모드 변경",
"비밀번호를 입력하세요:");
if (!ok)
@@ -2449,7 +2883,7 @@ public partial class SettingsWindow : Window
var dlg = new Window
{
Title = "스텝 바이 스텝 승인 비밀번호 확인",
Title = "스텝 바이 스텝 승인 ? 비밀번호 확인",
Width = 340, SizeToContent = SizeToContent.Height,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Owner = this, ResizeMode = ResizeMode.NoResize,
@@ -2983,7 +3417,7 @@ public partial class SettingsWindow : Window
if (section.Models != null && !string.IsNullOrEmpty(modelName) && !section.Models.Contains(modelName))
section.Models.Add(modelName);
}
// 2) AppSettings의 RegisteredModels (기존 저장된 것 ViewModel에 없는 경우 보완)
// 2) AppSettings의 RegisteredModels (기존 저장된 것 ? ViewModel에 없는 경우 보완)
foreach (var m in llm.RegisteredModels)
{
var svc = (m.Service ?? "").ToLowerInvariant();
@@ -3013,7 +3447,7 @@ public partial class SettingsWindow : Window
})
if (!sections[3].Models.Contains(cm)) sections[3].Models.Add(cm);
// 렌더링 모델이 없는 섹션도 헤더는 표시
// 렌더링 ? 모델이 없는 섹션도 헤더는 표시
foreach (var (service, svcLabel, svcColor, models) in sections)
{
FallbackModelsPanel.Children.Add(new TextBlock
@@ -3331,7 +3765,7 @@ public partial class SettingsWindow : Window
var result = CustomMessageBox.Show(
"설정을 불러오면 현재 설정이 덮어씌워집니다.\n계속하시겠습니까?",
"AX Copilot 설정 불러오기",
"AX Copilot ? 설정 불러오기",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
@@ -3344,7 +3778,7 @@ public partial class SettingsWindow : Window
var json = CryptoService.PortableDecrypt(fileContent);
if (string.IsNullOrEmpty(json))
{
// 평문 JSON일 수 있음 직접 파싱 시도
// 평문 JSON일 수 있음 ? 직접 파싱 시도
try
{
var test = System.Text.Json.JsonSerializer.Deserialize<Models.AppSettings>(fileContent);
@@ -3395,3 +3829,6 @@ public partial class SettingsWindow : Window
base.OnClosed(e);
}
}