[Phase 18] 코디네이터 에이전트·플러그인 갤러리·SlashRegistry·watchPaths 완성
Phase 18-A1 — CoordinatorAgentService (신규): - CoordinatorAgentService.cs (264줄): LLM 계획 수립 → JSON 파싱 → 의존성 토폴로지 정렬 → 병렬 서브에이전트 실행 → 결과 합성 - AgentLoopService.Coordinator.cs (100줄): RunCoordinatorModeAsync 파셜 + FormatCoordinatorPlan - AppSettings.LlmSettings.cs: EnableCoordinatorMode bool 설정 추가 - AgentLoopService.cs: EnableCoordinatorMode 체크 → RunCoordinatorModeAsync fallback 로직 Phase 17-C4 — watchPaths FileWatcherService 연동 (AgentLoopService.ExtendedHooks.cs): - SessionStart 훅 결과의 WatchPaths → FileWatcherService.Watch() 등록 - 파일 변경 시 FileChanged 훅 fire-and-forget 트리거 Phase 18-C1 — 플러그인 갤러리 (SettingsWindow): - SettingsWindow.Plugins.cs (249줄): InitPluginGallery, RenderPluginList, 활성화 토글, 제거 버튼 - SettingsWindow.xaml: 플러그인 탭 추가 (PluginListPanel, 설치/새로고침 버튼) - SettingsWindow.xaml.cs: Loaded에서 InitPluginGallery() 호출 SlashCommandRegistry ChatWindow 통합: - ChatWindow.SlashContext.cs (175줄): IAgentChatContext 구현, GetRegistrySlashMatches, TryExecuteRegistrySlashCommandAsync - ChatWindow.SlashCommands.cs: 레지스트리 명령 팝업 후보 추가 - ChatWindow.Sending.cs: 전송 전 레지스트리 슬래시 명령 우선 처리 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -231,7 +231,7 @@
|
||||
| 17-C1 | **훅 이벤트 확장** | UserPromptSubmit, PreCompact/PostCompact, FileChanged, CwdChanged, SessionEnd, ConfigChange 추가 | 최고 |
|
||||
| 17-C2 | **훅 타입 확장** | type:prompt (LLM 보안검사), type:agent (에이전트 루프 검증) 추가 | 최고 |
|
||||
| 17-C3 | **훅 속성·출력 고도화** | if/once/async/statusMessage 속성. additionalContext·permissionDecision·updatedInput 출력 | 높음 |
|
||||
| 17-C4 | **SessionStart 확장** | watchPaths (파일 감시 등록), initialUserMessage, additionalContext | 중간 |
|
||||
| 17-C4 ✅ | **SessionStart 확장** | watchPaths → FileWatcherService 등록 완료. SessionStart 훅 결과의 WatchPaths가 FileChanged 훅을 fire-and-forget으로 트리거 | 중간 |
|
||||
|
||||
### Group D — 스킬 시스템 고도화 (CC 스킬 문서 기반)
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
|
||||
| # | 기능 | 설명 | 우선순위 | 갭 |
|
||||
|---|------|------|----------|----|
|
||||
| 18-A1 | **코디네이터 에이전트 모드** | 계획·라우팅 전담, 구현은 서브에이전트 위임 | 최고 | G1 |
|
||||
| 18-A1 ✅ | **코디네이터 에이전트 모드** | CoordinatorAgentService 구현 완료. LLM 계획 수립 → 의존성 기반 토폴로지 정렬 → 병렬 서브에이전트 실행 → 결과 합성. EnableCoordinatorMode 설정 연동. AgentLoopService.Coordinator.cs 파셜 분리 | 최고 | G1 |
|
||||
| 18-A2 ✅ | **Worktree 격리 서브에이전트** | WorktreeManager 구현 완료 + DelegateAgentTool에 주입 (isolation:"worktree" 파라미터 지원) | 최고 | — |
|
||||
| 18-A3 ✅ | **에이전트 팀 위임 (delegate 도구)** | DelegateAgentTool 구현 + ToolRegistry 등록 완료. BackgroundAgentService + WorktreeManager 통합 | 최고 | G1 |
|
||||
| 18-A4 ✅ | **백그라운드 에이전트 + 완료 알림** | BackgroundAgentService.AgentCompleted → ChatWindow 트레이 알림 연결 완료 | 높음 | — |
|
||||
@@ -293,7 +293,7 @@
|
||||
| # | 기능 | 설명 | 우선순위 | 갭 |
|
||||
|---|------|------|----------|----|
|
||||
| 18-B1 ✅ | **에이전트 리플레이/디버깅** | AgentReplayService + ReplayTimelineViewModel 구현. WorkflowAnalyzerWindow에 "리플레이" 탭 통합 (세션 선택·재생 컨트롤·이벤트 스트림) | 높음 | G8 |
|
||||
| 18-C1 | **플러그인 갤러리 + 레지스트리** | 로컬 NAS/Git 기반 인앱 갤러리. zip 설치 | 높음 | — |
|
||||
| 18-C1 ✅ | **플러그인 갤러리 + 레지스트리** | PluginGalleryViewModel + PluginInstallService + SettingsWindow 플러그인 탭 구현 완료. zip 설치·활성화 토글·제거 UI 완비 | 높음 | — |
|
||||
| 18-C2 | **AI 스니펫** | ;email {수신자} {주제} 등 LLM 초안 자동 생성 | 중간 | — |
|
||||
| 18-C3 | **파라미터 퀵링크** | jira {번호} → URL 변수 치환 | 중간 | — |
|
||||
| 18-C4 | **오프라인 AI (ONNX Runtime)** | 로컬 소형 모델, 별도 배포 | 낮음 | — |
|
||||
|
||||
@@ -368,6 +368,10 @@ public class LlmSettings
|
||||
[JsonPropertyName("enableDiffTracker")]
|
||||
public bool EnableDiffTracker { get; set; } = true;
|
||||
|
||||
/// <summary>Phase 18-A1: 코디네이터 에이전트 모드 활성화. true이면 LLM이 먼저 계획을 수립 후 서브에이전트에 위임. 기본 false.</summary>
|
||||
[JsonPropertyName("enableCoordinatorMode")]
|
||||
public bool EnableCoordinatorMode { get; set; } = false;
|
||||
|
||||
/// <summary>추가 스킬 폴더 경로. 빈 문자열이면 기본 폴더만 사용.</summary>
|
||||
[JsonPropertyName("skillsFolderPath")]
|
||||
public string SkillsFolderPath { get; set; } = "";
|
||||
|
||||
100
src/AxCopilot/Services/Agent/AgentLoopService.Coordinator.cs
Normal file
100
src/AxCopilot/Services/Agent/AgentLoopService.Coordinator.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
/// <summary>
|
||||
/// Phase 18-A1: 코디네이터 에이전트 모드.
|
||||
/// LLM이 계획을 수립하고 서브에이전트들에게 태스크를 위임합니다.
|
||||
/// </summary>
|
||||
public partial class AgentLoopService
|
||||
{
|
||||
/// <summary>
|
||||
/// 코디네이터 모드 실행.
|
||||
/// 성공하면 최종 합성 결과를 반환, 실패 시 null 반환 (일반 에이전트 루프로 fallback).
|
||||
/// </summary>
|
||||
internal async Task<string?> RunCoordinatorModeAsync(
|
||||
string userRequest,
|
||||
string workFolder,
|
||||
CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var coordService = new CoordinatorAgentService(_llm);
|
||||
|
||||
// 1단계: 계획 수립
|
||||
var reqPreview = userRequest.Length > 60 ? userRequest[..57] + "…" : userRequest;
|
||||
EmitEvent(AgentEventType.Planning, "coordinator",
|
||||
$"코디네이터 모드: 요청 분석 중... [{reqPreview}]");
|
||||
|
||||
var plan = await coordService.PlanAsync(userRequest, ct);
|
||||
|
||||
if (plan.Tasks.Count == 0)
|
||||
return null; // 계획 실패 → 일반 루프 fallback
|
||||
|
||||
// 계획 요약 이벤트
|
||||
var planSummary = string.Join(", ", plan.Tasks.Select(t => $"{t.AgentType}:{t.Id}"));
|
||||
EmitEvent(AgentEventType.Planning, "coordinator",
|
||||
$"계획 수립 완료 ({plan.Tasks.Count}개 태스크): {planSummary}");
|
||||
|
||||
// 2단계: 사용자 승인 (UserDecisionCallback이 있는 경우)
|
||||
if (UserDecisionCallback != null)
|
||||
{
|
||||
var planText = FormatCoordinatorPlan(plan);
|
||||
var options = new List<string> { "실행", "취소" };
|
||||
var answer = await UserDecisionCallback(planText, options);
|
||||
|
||||
if (answer == null || answer.Contains("취소", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
EmitEvent(AgentEventType.Complete, "coordinator", "사용자가 코디네이터 계획을 취소했습니다.");
|
||||
return "코디네이터 계획이 취소되었습니다.";
|
||||
}
|
||||
}
|
||||
|
||||
// 3단계: 각 서브태스크 실행
|
||||
var result = await coordService.ExecuteAsync(
|
||||
plan,
|
||||
workFolder,
|
||||
RunSubAgentAsync,
|
||||
onTaskStarted: (id, desc) =>
|
||||
EmitEvent(AgentEventType.ToolCall, $"coordinator/{id}",
|
||||
$"[{id}] {desc}"),
|
||||
onTaskCompleted: (id, res) =>
|
||||
EmitEvent(AgentEventType.ToolResult, $"coordinator/{id}",
|
||||
$"[{id}] 완료: {(res.Length > 100 ? res[..97] + "…" : res)}"),
|
||||
ct);
|
||||
|
||||
EmitEvent(AgentEventType.Complete, "coordinator", "코디네이터 모드 완료");
|
||||
return result;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
EmitEvent(AgentEventType.Complete, "coordinator", "취소됨");
|
||||
return "코디네이터 모드가 취소되었습니다.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"[Coordinator] 오류: {ex.Message}. 일반 에이전트 루프로 fallback.");
|
||||
EmitEvent(AgentEventType.Error, "coordinator", $"코디네이터 오류: {ex.Message} — 일반 모드로 전환");
|
||||
return null; // fallback to normal loop
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>코디네이터 계획을 사용자에게 보여줄 텍스트로 포맷합니다.</summary>
|
||||
private static string FormatCoordinatorPlan(CoordinatorPlan plan)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine($"📋 코디네이터 계획 ({plan.Tasks.Count}개 태스크)");
|
||||
sb.AppendLine($"요청: {plan.OriginalRequest}");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var task in plan.Tasks)
|
||||
{
|
||||
var deps = task.Dependencies.Count > 0
|
||||
? $" (선행: {string.Join(", ", task.Dependencies)})"
|
||||
: "";
|
||||
sb.AppendLine($" [{task.Id}] {task.AgentType}{deps}");
|
||||
sb.AppendLine($" {task.Description}");
|
||||
}
|
||||
|
||||
sb.AppendLine("\n이 계획을 실행할까요?");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,32 @@ public partial class AgentLoopService
|
||||
var result = await ExtendedHookRunner.RunEventAsync(hooks, ctx, ct, _llm);
|
||||
ApplyExtendedHookResult(result, messages);
|
||||
|
||||
// Phase 17-C4: SessionStart 훅 WatchPaths → FileWatcherService 등록
|
||||
if (kind == HookEventKind.SessionStart &&
|
||||
result.WatchPaths != null && result.WatchPaths.Count > 0)
|
||||
{
|
||||
_fileWatcher.ClearAll(); // 이전 세션 감시 경로 초기화
|
||||
var workFolder = _settings.Settings.Llm.WorkFolder ?? "";
|
||||
foreach (var pathPattern in result.WatchPaths)
|
||||
{
|
||||
// 상대 경로는 workFolder 기준으로 해석
|
||||
var absPattern = System.IO.Path.IsPathRooted(pathPattern)
|
||||
? pathPattern
|
||||
: System.IO.Path.Combine(workFolder, pathPattern);
|
||||
_fileWatcher.Watch(absPattern, changedPath =>
|
||||
{
|
||||
// FileChanged 훅 fire-and-forget
|
||||
_ = RunExtendedEventAsync(
|
||||
HookEventKind.FileChanged, null, CancellationToken.None,
|
||||
changedFile: changedPath);
|
||||
EmitEvent(AgentEventType.Thinking, "file_watcher",
|
||||
$"파일 변경 감지: {System.IO.Path.GetFileName(changedPath)}");
|
||||
});
|
||||
}
|
||||
EmitEvent(AgentEventType.Thinking, "file_watcher",
|
||||
$"watchPaths 등록 완료 ({result.WatchPaths.Count}개 패턴)");
|
||||
}
|
||||
|
||||
// HookFired 이벤트 로그 기록
|
||||
if (_eventLog != null && hooks.Count > 0)
|
||||
_ = _eventLog.AppendAsync(AgentEventLogType.HookFired,
|
||||
|
||||
@@ -78,6 +78,9 @@ public partial class AgentLoopService
|
||||
/// <summary>위임 에이전트 도구 참조 (서브에이전트 실행기 주입용).</summary>
|
||||
private DelegateAgentTool? _delegateAgentTool;
|
||||
|
||||
/// <summary>Phase 17-C4: watchPaths 파일 감시 서비스.</summary>
|
||||
private readonly FileWatcherService _fileWatcher = new();
|
||||
|
||||
/// <summary>일시정지 제어용 세마포어. 1이면 진행, 0이면 대기.</summary>
|
||||
private readonly SemaphoreSlim _pauseSemaphore = new(1, 1);
|
||||
|
||||
@@ -224,6 +227,18 @@ public partial class AgentLoopService
|
||||
|
||||
var context = BuildContext(activeTabSnapshot);
|
||||
|
||||
// Phase 18-A1: 코디네이터 에이전트 모드 — 계획 수립 후 서브에이전트 위임
|
||||
if (llm.EnableCoordinatorMode && !string.IsNullOrWhiteSpace(userQuery))
|
||||
{
|
||||
var coordResult = await RunCoordinatorModeAsync(userQuery, llm.WorkFolder ?? "", ct);
|
||||
if (coordResult != null)
|
||||
{
|
||||
IsRunning = false;
|
||||
return coordResult;
|
||||
}
|
||||
// null 반환 시 일반 에이전트 루프로 fallback
|
||||
}
|
||||
|
||||
// Phase 17-A: Reflexion — 과거 교훈을 시스템 메시지에 주입
|
||||
await InjectReflexionContextAsync(messages, userQuery);
|
||||
|
||||
|
||||
265
src/AxCopilot/Services/Agent/CoordinatorAgentService.cs
Normal file
265
src/AxCopilot/Services/Agent/CoordinatorAgentService.cs
Normal file
@@ -0,0 +1,265 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
/// <summary>
|
||||
/// Phase 18-A1: 코디네이터 에이전트 서비스.
|
||||
/// 사용자 요청을 분석하여 멀티 서브태스크 계획을 수립하고,
|
||||
/// 각 태스크를 특화 서브에이전트에 위임하여 실행합니다.
|
||||
/// </summary>
|
||||
public class CoordinatorAgentService
|
||||
{
|
||||
private readonly LlmService _llm;
|
||||
|
||||
// 사용 가능한 에이전트 유형
|
||||
private static readonly string[] KnownAgentTypes =
|
||||
{
|
||||
"implementer", // 코드 작성·수정
|
||||
"researcher", // 조사·분석·요약
|
||||
"reviewer", // 코드·문서 리뷰
|
||||
"documenter", // 문서·보고서 작성
|
||||
"tester", // 테스트 생성·실행
|
||||
};
|
||||
|
||||
private const string PlanSystemPrompt = @"당신은 복잡한 작업을 분석하여 서브태스크로 분해하는 코디네이터 에이전트입니다.
|
||||
사용자의 요청을 받아 실행 계획을 JSON 형식으로 반환하세요.
|
||||
|
||||
사용 가능한 에이전트 유형:
|
||||
- implementer: 코드 작성 및 수정
|
||||
- researcher: 조사, 분석, 요약
|
||||
- reviewer: 코드·문서 리뷰
|
||||
- documenter: 문서·보고서 작성
|
||||
- tester: 테스트 생성 및 실행
|
||||
|
||||
반환 형식 (JSON만 반환, 설명 없음):
|
||||
{
|
||||
""original_request"": ""원본 요청"",
|
||||
""tasks"": [
|
||||
{
|
||||
""id"": ""task1"",
|
||||
""agent_type"": ""researcher"",
|
||||
""description"": ""태스크 설명"",
|
||||
""dependencies"": []
|
||||
},
|
||||
{
|
||||
""id"": ""task2"",
|
||||
""agent_type"": ""implementer"",
|
||||
""description"": ""태스크 설명"",
|
||||
""dependencies"": [""task1""]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
규칙:
|
||||
- 독립적으로 실행 가능한 태스크는 의존성 없이 병렬 실행 가능
|
||||
- 선행 태스크 결과가 필요한 경우에만 dependencies에 추가
|
||||
- 최대 6개 태스크로 분해
|
||||
- JSON 외 다른 텍스트 없이 순수 JSON만 반환";
|
||||
|
||||
public CoordinatorAgentService(LlmService llm)
|
||||
{
|
||||
_llm = llm;
|
||||
}
|
||||
|
||||
/// <summary>사용자 요청을 분석하여 CoordinatorPlan을 생성합니다.</summary>
|
||||
public async Task<CoordinatorPlan> PlanAsync(string request, CancellationToken ct)
|
||||
{
|
||||
var messages = new List<ChatMessage>
|
||||
{
|
||||
new() { Role = "system", Content = PlanSystemPrompt },
|
||||
new() { Role = "user", Content = $"다음 요청을 분석하여 실행 계획을 수립하세요:\n\n{request}" }
|
||||
};
|
||||
|
||||
var response = await _llm.SendAsync(messages, ct);
|
||||
return ParsePlan(request, response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CoordinatorPlan을 실행합니다. 의존성 순서에 따라 태스크를 실행하고
|
||||
/// 각 태스크 결과를 후속 태스크에 컨텍스트로 전달합니다.
|
||||
/// </summary>
|
||||
/// <param name="plan">실행할 계획</param>
|
||||
/// <param name="workFolder">작업 폴더</param>
|
||||
/// <param name="subAgentRunner">서브에이전트 실행기 (agentType, task, context, ct) → result</param>
|
||||
/// <param name="onTaskStarted">태스크 시작 콜백 (taskId, description)</param>
|
||||
/// <param name="onTaskCompleted">태스크 완료 콜백 (taskId, result)</param>
|
||||
/// <param name="ct">취소 토큰</param>
|
||||
public async Task<string> ExecuteAsync(
|
||||
CoordinatorPlan plan,
|
||||
string workFolder,
|
||||
Func<string, string, string, CancellationToken, Task<string>> subAgentRunner,
|
||||
Action<string, string>? onTaskStarted = null,
|
||||
Action<string, string>? onTaskCompleted = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var results = new Dictionary<string, string>(); // taskId → result
|
||||
var remaining = plan.Tasks.ToList();
|
||||
|
||||
// 의존성 기반 토폴로지 정렬 실행
|
||||
while (remaining.Count > 0 && !ct.IsCancellationRequested)
|
||||
{
|
||||
// 현재 실행 가능한 태스크 찾기 (의존성이 모두 완료된 것)
|
||||
var ready = remaining
|
||||
.Where(t => t.Dependencies.All(dep => results.ContainsKey(dep)))
|
||||
.ToList();
|
||||
|
||||
if (ready.Count == 0)
|
||||
{
|
||||
// 순환 의존성 또는 누락된 의존성 — 강제 진행
|
||||
ready = new List<CoordinatorSubTask> { remaining[0] };
|
||||
LogService.Warn($"[Coordinator] 의존성 해결 실패, 강제 실행: {remaining[0].Id}");
|
||||
}
|
||||
|
||||
// 의존성 없는 태스크는 병렬 실행
|
||||
var parallelTasks = ready.Select(async task =>
|
||||
{
|
||||
task.Status = SubTaskStatus.Running;
|
||||
onTaskStarted?.Invoke(task.Id, task.Description);
|
||||
|
||||
// 선행 태스크 결과를 컨텍스트로 구성
|
||||
var context = BuildContext(task, results, plan.OriginalRequest);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await subAgentRunner(task.AgentType, task.Description, context, ct);
|
||||
task.Result = result;
|
||||
task.Status = SubTaskStatus.Completed;
|
||||
onTaskCompleted?.Invoke(task.Id, result);
|
||||
return (task.Id, result, (Exception?)null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
task.Error = ex.Message;
|
||||
task.Status = SubTaskStatus.Failed;
|
||||
onTaskCompleted?.Invoke(task.Id, $"[오류] {ex.Message}");
|
||||
return (task.Id, $"[오류] {ex.Message}", ex);
|
||||
}
|
||||
});
|
||||
|
||||
var parallelResults = await Task.WhenAll(parallelTasks);
|
||||
foreach (var (id, result, _) in parallelResults)
|
||||
results[id] = result;
|
||||
|
||||
// 완료된 태스크 제거
|
||||
foreach (var t in ready)
|
||||
remaining.Remove(t);
|
||||
}
|
||||
|
||||
// 최종 결과 합성
|
||||
return await SynthesizeResultAsync(plan, results, ct);
|
||||
}
|
||||
|
||||
/// <summary>모든 서브태스크 결과를 종합하여 최종 응답을 생성합니다.</summary>
|
||||
private async Task<string> SynthesizeResultAsync(
|
||||
CoordinatorPlan plan,
|
||||
Dictionary<string, string> results,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (results.Count == 0) return "실행된 태스크가 없습니다.";
|
||||
|
||||
// 단일 태스크면 바로 반환
|
||||
if (plan.Tasks.Count == 1 && results.Count == 1)
|
||||
return results.Values.First();
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine($"원본 요청: {plan.OriginalRequest}");
|
||||
sb.AppendLine();
|
||||
foreach (var task in plan.Tasks)
|
||||
{
|
||||
if (results.TryGetValue(task.Id, out var result))
|
||||
{
|
||||
sb.AppendLine($"[{task.AgentType}] {task.Description}:");
|
||||
sb.AppendLine(result.Length > 500 ? result[..500] + "…" : result);
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
var synthMessages = new List<ChatMessage>
|
||||
{
|
||||
new() { Role = "system", Content = "당신은 여러 서브에이전트 결과를 종합하는 코디네이터입니다. 결과를 간결하게 통합하여 최종 응답을 제공하세요." },
|
||||
new() { Role = "user", Content = sb.ToString() }
|
||||
};
|
||||
|
||||
return await _llm.SendAsync(synthMessages, ct);
|
||||
}
|
||||
|
||||
// ─── 내부 헬퍼 ──────────────────────────────────────────────────────────
|
||||
|
||||
private static string BuildContext(
|
||||
CoordinatorSubTask task,
|
||||
Dictionary<string, string> previousResults,
|
||||
string originalRequest)
|
||||
{
|
||||
var ctx = new System.Text.StringBuilder();
|
||||
ctx.AppendLine($"원본 요청: {originalRequest}");
|
||||
ctx.AppendLine($"현재 태스크: {task.Description}");
|
||||
|
||||
if (task.Dependencies.Count > 0)
|
||||
{
|
||||
ctx.AppendLine("\n선행 태스크 결과:");
|
||||
foreach (var dep in task.Dependencies)
|
||||
{
|
||||
if (previousResults.TryGetValue(dep, out var depResult))
|
||||
ctx.AppendLine($"- {dep}: {(depResult.Length > 300 ? depResult[..300] + "…" : depResult)}");
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.ToString();
|
||||
}
|
||||
|
||||
private static CoordinatorPlan ParsePlan(string originalRequest, string llmResponse)
|
||||
{
|
||||
try
|
||||
{
|
||||
// JSON 블록 추출 (```json ... ``` 또는 { ... })
|
||||
var json = ExtractJson(llmResponse);
|
||||
if (string.IsNullOrEmpty(json))
|
||||
return FallbackPlan(originalRequest);
|
||||
|
||||
var plan = JsonSerializer.Deserialize<CoordinatorPlan>(json,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
if (plan == null || plan.Tasks.Count == 0)
|
||||
return FallbackPlan(originalRequest);
|
||||
|
||||
// 에이전트 유형 검증
|
||||
foreach (var task in plan.Tasks)
|
||||
{
|
||||
if (!KnownAgentTypes.Contains(task.AgentType, StringComparer.OrdinalIgnoreCase))
|
||||
task.AgentType = "implementer";
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"[Coordinator] 계획 파싱 실패: {ex.Message}");
|
||||
return FallbackPlan(originalRequest);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExtractJson(string text)
|
||||
{
|
||||
// ```json ... ``` 블록
|
||||
var m = Regex.Match(text, @"```(?:json)?\s*([\s\S]*?)\s*```");
|
||||
if (m.Success) return m.Groups[1].Value.Trim();
|
||||
|
||||
// 첫 번째 { ... } 블록
|
||||
var start = text.IndexOf('{');
|
||||
var end = text.LastIndexOf('}');
|
||||
if (start >= 0 && end > start) return text[start..(end + 1)];
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private static CoordinatorPlan FallbackPlan(string request) => new()
|
||||
{
|
||||
OriginalRequest = request,
|
||||
Tasks = new List<CoordinatorSubTask>
|
||||
{
|
||||
new() { Id = "task1", AgentType = "implementer", Description = request, Dependencies = new() }
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -264,6 +264,14 @@ public partial class ChatWindow
|
||||
// placeholder 정리
|
||||
ClearPromptCardPlaceholder();
|
||||
|
||||
// Phase 18: SlashCommandRegistry 명령 우선 실행 (/compact, /memory, /plan 등)
|
||||
if (text.StartsWith("/"))
|
||||
{
|
||||
using var regCts = new CancellationTokenSource();
|
||||
if (await TryExecuteRegistrySlashCommandAsync(text, regCts.Token))
|
||||
return; // registry 명령이 처리됨 (내장 SlashCommands dict는 폴백)
|
||||
}
|
||||
|
||||
// 슬래시 명령어 처리
|
||||
var (slashSystem, displayText) = ParseSlashCommand(text);
|
||||
|
||||
|
||||
@@ -68,6 +68,14 @@ public partial class ChatWindow
|
||||
.Select(kv => (Cmd: kv.Key, Label: kv.Value.Label, IsSkill: false))
|
||||
.ToList();
|
||||
|
||||
// SlashCommandRegistry 명령 매칭 (/compact, /memory, /plan 등)
|
||||
foreach (var rm in GetRegistrySlashMatches(text))
|
||||
{
|
||||
// 내장 dict나 스킬에 이미 있는 것 제외 (중복 방지)
|
||||
if (!matches.Any(m => m.Cmd.Equals(rm.Cmd, StringComparison.OrdinalIgnoreCase)))
|
||||
matches.Add(rm);
|
||||
}
|
||||
|
||||
// 스킬 슬래시 명령어 매칭 (탭별 필터)
|
||||
if (Llm.EnableSkillSystem)
|
||||
{
|
||||
|
||||
184
src/AxCopilot/Views/ChatWindow.SlashContext.cs
Normal file
184
src/AxCopilot/Views/ChatWindow.SlashContext.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using System.Windows;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Services.Agent;
|
||||
using AxCopilot.Services.Agent.SlashCommands;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// Phase 18: SlashCommandRegistry IAgentChatContext 구현.
|
||||
/// ChatWindow가 IAgentChatContext를 구현하여 SlashCommandRegistry 명령에
|
||||
/// LLM, 설정, UI 조작 기능을 제공합니다.
|
||||
/// </summary>
|
||||
public partial class ChatWindow : IAgentChatContext
|
||||
{
|
||||
// ─── SlashCommandRegistry 연결 ──────────────────────────────────────────
|
||||
|
||||
private readonly SlashCommandRegistry _slashRegistry = SlashCommandRegistry.CreateDefault();
|
||||
|
||||
// IAgentChatContext 구현
|
||||
|
||||
LlmService IAgentChatContext.Llm => _llm;
|
||||
|
||||
SettingsService IAgentChatContext.Settings => _settings;
|
||||
|
||||
IReadOnlyList<SkillDefinition> IAgentChatContext.LoadedSkills => SkillService.Skills;
|
||||
|
||||
IReadOnlyList<ExtendedHookEntry> IAgentChatContext.Hooks
|
||||
{
|
||||
get
|
||||
{
|
||||
var cfg = _settings.Settings.Llm.ExtendedHooks;
|
||||
var allHooks = new List<ExtendedHookEntry>();
|
||||
|
||||
void AddHooks(IEnumerable<ExtendedHookEntryConfig>? configs, HookEventKind kind)
|
||||
{
|
||||
if (configs == null) return;
|
||||
foreach (var c in configs.Where(x => x.Enabled))
|
||||
allHooks.Add(new ExtendedHookEntry
|
||||
{
|
||||
Name = c.Name,
|
||||
Event = kind,
|
||||
Matcher = c.Matcher,
|
||||
Mode = ParseHookMode(c.Mode),
|
||||
ScriptPath = c.ScriptPath,
|
||||
Url = c.Url,
|
||||
Prompt = c.Prompt,
|
||||
Model = c.Model,
|
||||
Enabled = c.Enabled,
|
||||
Once = c.Once,
|
||||
IsAsync = c.IsAsync,
|
||||
TimeoutSeconds = c.TimeoutSeconds > 0 ? c.TimeoutSeconds : 30,
|
||||
StatusMessage = c.StatusMessage,
|
||||
});
|
||||
}
|
||||
|
||||
AddHooks(cfg.PreToolUse, HookEventKind.PreToolUse);
|
||||
AddHooks(cfg.PostToolUse, HookEventKind.PostToolUse);
|
||||
AddHooks(cfg.PostToolUseFailure, HookEventKind.PostToolUseFailure);
|
||||
AddHooks(cfg.SessionStart, HookEventKind.SessionStart);
|
||||
AddHooks(cfg.SessionEnd, HookEventKind.SessionEnd);
|
||||
AddHooks(cfg.UserPromptSubmit, HookEventKind.UserPromptSubmit);
|
||||
AddHooks(cfg.AgentStop, HookEventKind.AgentStop);
|
||||
AddHooks(cfg.PreCompact, HookEventKind.PreCompact);
|
||||
AddHooks(cfg.PostCompact, HookEventKind.PostCompact);
|
||||
AddHooks(cfg.FileChanged, HookEventKind.FileChanged);
|
||||
|
||||
return allHooks;
|
||||
}
|
||||
}
|
||||
|
||||
string IAgentChatContext.ActiveTab => _activeTab;
|
||||
|
||||
string IAgentChatContext.WorkFolder => GetCurrentWorkFolder();
|
||||
|
||||
SlashCommandRegistry IAgentChatContext.CommandRegistry => _slashRegistry;
|
||||
|
||||
async Task IAgentChatContext.SendSystemMessageAsync(string message)
|
||||
{
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
AddMessageBubble("assistant", $"ℹ️ {message}");
|
||||
AutoScrollIfNeeded();
|
||||
});
|
||||
}
|
||||
|
||||
async Task<string?> IAgentChatContext.PromptUserAsync(string question)
|
||||
{
|
||||
string? response = null;
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
response = UserAskDialog.Show(question, new System.Collections.Generic.List<string>());
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
void IAgentChatContext.ClearMessages()
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
MessagePanel.Children.Clear();
|
||||
EmptyState.Visibility = Visibility.Visible;
|
||||
lock (_convLock)
|
||||
{
|
||||
_currentConversation?.Messages.Clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void IAgentChatContext.UpdateSessionHeader()
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
UpdateModelLabel();
|
||||
UpdatePlanModeUI();
|
||||
});
|
||||
}
|
||||
|
||||
void IAgentChatContext.ToggleSettingsPanel() => Dispatcher.Invoke(ToggleSettingsPanel);
|
||||
|
||||
// ─── SlashCommandRegistry → ChatWindow 팝업 통합 ──────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// SlashCommandRegistry 명령을 팝업 후보 목록에 추가합니다.
|
||||
/// ChatWindow.SlashCommands.cs의 InputBox_TextChanged에서 호출됩니다.
|
||||
/// </summary>
|
||||
internal IEnumerable<(string Cmd, string Label, bool IsSkill)> GetRegistrySlashMatches(string prefix)
|
||||
{
|
||||
return _slashRegistry.GetAll()
|
||||
.Where(c => ("/" + c.Name).StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(c => (
|
||||
Cmd: "/" + c.Name,
|
||||
Label: c.Description,
|
||||
IsSkill: false
|
||||
));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 입력 텍스트가 SlashCommandRegistry 명령이면 실행합니다.
|
||||
/// 실행 성공 시 true 반환.
|
||||
/// </summary>
|
||||
internal async Task<bool> TryExecuteRegistrySlashCommandAsync(
|
||||
string fullInput, CancellationToken ct)
|
||||
{
|
||||
// "/compact 추가 지시사항" → name="compact", args="추가 지시사항"
|
||||
var trimmed = fullInput.TrimStart('/');
|
||||
var spaceIdx = trimmed.IndexOf(' ');
|
||||
var name = spaceIdx >= 0 ? trimmed[..spaceIdx] : trimmed;
|
||||
var args = spaceIdx >= 0 ? trimmed[(spaceIdx + 1)..].Trim() : "";
|
||||
|
||||
var command = _slashRegistry.Resolve("/" + name);
|
||||
if (command == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
_isStreaming = true;
|
||||
Dispatcher.Invoke(() => BtnSend.IsEnabled = false);
|
||||
await command.ExecuteAsync(args, this, ct);
|
||||
}
|
||||
catch (OperationCanceledException) { /* 취소 */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
await ((IAgentChatContext)this).SendSystemMessageAsync($"명령 실행 오류: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isStreaming = false;
|
||||
Dispatcher.Invoke(() => BtnSend.IsEnabled = true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─── 내부 헬퍼 ──────────────────────────────────────────────────────────
|
||||
|
||||
private static HookExecutionMode ParseHookMode(string? mode) =>
|
||||
mode?.ToLowerInvariant() switch
|
||||
{
|
||||
"http" => HookExecutionMode.Http,
|
||||
"prompt" => HookExecutionMode.Prompt,
|
||||
"agent" => HookExecutionMode.Agent,
|
||||
_ => HookExecutionMode.Command,
|
||||
};
|
||||
}
|
||||
249
src/AxCopilot/Views/SettingsWindow.Plugins.cs
Normal file
249
src/AxCopilot/Views/SettingsWindow.Plugins.cs
Normal file
@@ -0,0 +1,249 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using AxCopilot.Services.Agent;
|
||||
using AxCopilot.ViewModels;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// Phase 18-C1: SettingsWindow — 플러그인 갤러리 탭.
|
||||
/// PluginGalleryViewModel을 코드비하인드에서 바인딩하여 플러그인 설치·관리를 지원합니다.
|
||||
/// </summary>
|
||||
public partial class SettingsWindow
|
||||
{
|
||||
private PluginGalleryViewModel? _pluginVm;
|
||||
|
||||
// ─── 초기화 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void InitPluginGallery()
|
||||
{
|
||||
if (_pluginVm != null) return;
|
||||
var installService = new PluginInstallService();
|
||||
_pluginVm = new PluginGalleryViewModel(installService);
|
||||
_pluginVm.Refresh();
|
||||
RenderPluginList();
|
||||
}
|
||||
|
||||
// ─── 버튼 이벤트 ─────────────────────────────────────────────────────────
|
||||
|
||||
private async void BtnInstallPlugin_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var dlg = new OpenFileDialog
|
||||
{
|
||||
Title = "플러그인 zip 파일 선택",
|
||||
Filter = "플러그인 패키지 (*.zip)|*.zip",
|
||||
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
|
||||
};
|
||||
|
||||
if (dlg.ShowDialog() != true) return;
|
||||
|
||||
PluginStatusText.Text = "설치 중...";
|
||||
BtnInstallPlugin.IsEnabled = false;
|
||||
|
||||
try
|
||||
{
|
||||
var svc = new PluginInstallService();
|
||||
var result = await svc.InstallFromZipAsync(dlg.FileName);
|
||||
PluginStatusText.Text = result.Success
|
||||
? $"✓ {result.Message}"
|
||||
: $"✗ {result.Message}";
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
_pluginVm?.Refresh();
|
||||
RenderPluginList();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
BtnInstallPlugin.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnRefreshPlugins_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_pluginVm?.Refresh();
|
||||
RenderPluginList();
|
||||
PluginStatusText.Text = "새로고침 완료";
|
||||
}
|
||||
|
||||
// ─── 플러그인 목록 렌더링 ──────────────────────────────────────────────────
|
||||
|
||||
private void RenderPluginList()
|
||||
{
|
||||
if (_pluginVm == null || PluginListPanel == null) return;
|
||||
PluginListPanel.Children.Clear();
|
||||
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var itemBg = TryFindResource("ItemBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x15, 0xFF, 0xFF, 0xFF));
|
||||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x20, 0xFF, 0xFF, 0xFF));
|
||||
var accentColor = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
|
||||
if (_pluginVm.Plugins.Count == 0)
|
||||
{
|
||||
PluginListPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "설치된 플러그인이 없습니다.\n'zip에서 설치' 버튼으로 플러그인을 추가하세요.",
|
||||
FontSize = 12,
|
||||
Foreground = secondaryText,
|
||||
TextWrapping = System.Windows.TextWrapping.Wrap,
|
||||
Margin = new Thickness(0, 8, 0, 8),
|
||||
LineHeight = 20,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var plugin in _pluginVm.Plugins)
|
||||
{
|
||||
var card = new Border
|
||||
{
|
||||
Background = itemBg,
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(16, 12, 16, 12),
|
||||
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 infoStack = new StackPanel();
|
||||
|
||||
// 이름 + 유형 배지
|
||||
var nameRow = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
nameRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = plugin.Name,
|
||||
FontSize = 13,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
});
|
||||
var typeBadge = new Border
|
||||
{
|
||||
Background = GetTypeBadgeColor(plugin.Type),
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Padding = new Thickness(6, 2, 6, 2),
|
||||
Margin = new Thickness(8, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
typeBadge.Child = new TextBlock
|
||||
{
|
||||
Text = plugin.Type,
|
||||
FontSize = 10,
|
||||
Foreground = Brushes.White,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
};
|
||||
nameRow.Children.Add(typeBadge);
|
||||
infoStack.Children.Add(nameRow);
|
||||
|
||||
infoStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = plugin.Description,
|
||||
FontSize = 11,
|
||||
Foreground = secondaryText,
|
||||
Margin = new Thickness(0, 4, 0, 4),
|
||||
TextWrapping = System.Windows.TextWrapping.Wrap,
|
||||
});
|
||||
|
||||
infoStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"v{plugin.Version} | {plugin.Author} | 설치일: {plugin.InstalledAt}",
|
||||
FontSize = 10,
|
||||
Foreground = secondaryText,
|
||||
});
|
||||
|
||||
Grid.SetColumn(infoStack, 0);
|
||||
grid.Children.Add(infoStack);
|
||||
|
||||
// 오른쪽: 활성화 토글 + 제거
|
||||
var actionStack = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
|
||||
// 활성화 토글
|
||||
var isEnabled = plugin.IsEnabled;
|
||||
var toggleBorder = new Border
|
||||
{
|
||||
Width = 40, Height = 22, CornerRadius = new CornerRadius(11),
|
||||
Background = isEnabled ? accentColor : itemBg,
|
||||
BorderBrush = secondaryText, BorderThickness = new Thickness(1),
|
||||
Cursor = Cursors.Hand,
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
Tag = plugin,
|
||||
};
|
||||
var toggleThumb = new Border
|
||||
{
|
||||
Width = 16, Height = 16, CornerRadius = new CornerRadius(8),
|
||||
Background = Brushes.White,
|
||||
HorizontalAlignment = isEnabled ? HorizontalAlignment.Right : HorizontalAlignment.Left,
|
||||
Margin = new Thickness(3),
|
||||
};
|
||||
toggleBorder.Child = toggleThumb;
|
||||
var capturedPlugin = plugin;
|
||||
toggleBorder.MouseLeftButtonUp += async (_, _) =>
|
||||
{
|
||||
capturedPlugin.IsEnabled = !capturedPlugin.IsEnabled;
|
||||
var svc = new PluginInstallService();
|
||||
await svc.SetEnabledAsync(capturedPlugin.Id, capturedPlugin.IsEnabled);
|
||||
RenderPluginList(); // 재렌더링
|
||||
};
|
||||
actionStack.Children.Add(toggleBorder);
|
||||
|
||||
// 제거 버튼
|
||||
var removeBorder = new Border
|
||||
{
|
||||
Width = 28, Height = 28, CornerRadius = new CornerRadius(6),
|
||||
Background = Brushes.Transparent, Cursor = Cursors.Hand,
|
||||
};
|
||||
removeBorder.Child = new TextBlock
|
||||
{
|
||||
Text = "\uE74D", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 13,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0xF8, 0x71, 0x71)),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
removeBorder.MouseEnter += (_, _) => removeBorder.Background = hoverBg;
|
||||
removeBorder.MouseLeave += (_, _) => removeBorder.Background = Brushes.Transparent;
|
||||
removeBorder.MouseLeftButtonUp += async (_, _) =>
|
||||
{
|
||||
var confirm = CustomMessageBox.Show(
|
||||
$"'{capturedPlugin.Name}' 플러그인을 제거하시겠습니까?",
|
||||
"플러그인 제거",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (confirm != MessageBoxResult.Yes) return;
|
||||
|
||||
var svc = new PluginInstallService();
|
||||
await svc.UninstallAsync(capturedPlugin.Id);
|
||||
_pluginVm?.Refresh();
|
||||
RenderPluginList();
|
||||
PluginStatusText.Text = $"'{capturedPlugin.Name}' 제거 완료";
|
||||
};
|
||||
actionStack.Children.Add(removeBorder);
|
||||
|
||||
Grid.SetColumn(actionStack, 1);
|
||||
grid.Children.Add(actionStack);
|
||||
|
||||
card.Child = grid;
|
||||
PluginListPanel.Children.Add(card);
|
||||
}
|
||||
}
|
||||
|
||||
private static Brush GetTypeBadgeColor(string type) => type?.ToLowerInvariant() switch
|
||||
{
|
||||
"skill" => new SolidColorBrush(Color.FromRgb(0x60, 0xA5, 0xFA)),
|
||||
"tool" => new SolidColorBrush(Color.FromRgb(0x34, 0xD3, 0x99)),
|
||||
"theme" => new SolidColorBrush(Color.FromRgb(0xFB, 0xBF, 0x24)),
|
||||
_ => new SolidColorBrush(Color.FromRgb(0x94, 0xA3, 0xB8)),
|
||||
};
|
||||
}
|
||||
@@ -5179,6 +5179,62 @@
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<!-- Phase 18-C1: 플러그인 갤러리 탭 -->
|
||||
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||
<TabItem Header="플러그인" Tag="" Style="{StaticResource SideNavItem}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 헤더 -->
|
||||
<StackPanel Grid.Row="0" Margin="24,20,24,12">
|
||||
<TextBlock Text="플러그인 관리" FontSize="16" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<TextBlock Text="로컬 zip 파일로 스킬·도구·테마 플러그인을 설치합니다."
|
||||
FontSize="12" Foreground="{DynamicResource SecondaryText}"
|
||||
Margin="0,4,0,0" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 플러그인 목록 -->
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Margin="24,0,24,0">
|
||||
<StackPanel x:Name="PluginListPanel" Margin="0,0,0,12"/>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- 하단 설치 버튼 -->
|
||||
<Border Grid.Row="2" Margin="24,8,24,16">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
||||
<Border x:Name="BtnInstallPlugin" CornerRadius="8" Padding="16,8"
|
||||
Background="{DynamicResource AccentColor}" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnInstallPlugin_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="13"
|
||||
Foreground="White" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="zip에서 설치" FontSize="13" Foreground="White"
|
||||
FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border x:Name="BtnRefreshPlugins" CornerRadius="8" Padding="12,8"
|
||||
Background="{DynamicResource ItemBackground}" Cursor="Hand"
|
||||
Margin="8,0,0,0" MouseLeftButtonUp="BtnRefreshPlugins_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="13"
|
||||
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="새로고침" FontSize="13"
|
||||
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<TextBlock x:Name="PluginStatusText" Text="" FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center" Margin="16,0,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
</TabControl>
|
||||
|
||||
<!-- ═════════════════════════════════════════════════════════════════ -->
|
||||
|
||||
@@ -77,6 +77,7 @@ public partial class SettingsWindow : Window
|
||||
BuildToolRegistryPanel();
|
||||
LoadAdvancedSettings();
|
||||
RefreshStorageInfo();
|
||||
InitPluginGallery(); // Phase 18-C1: 플러그인 갤러리 초기화
|
||||
// 개발자 모드는 저장된 설정 유지 (끄면 하위 기능 모두 비활성)
|
||||
UpdateDevModeContentVisibility();
|
||||
// AI 기능 토글 초기화
|
||||
|
||||
Reference in New Issue
Block a user