From d6f1e85cecfc55a1c238ded2149bf6e26260b0a3 Mon Sep 17 00:00:00 2001 From: lacvet Date: Sat, 4 Apr 2026 13:34:36 +0900 Subject: [PATCH] =?UTF-8?q?[Phase=2017-C]=20=ED=9B=85=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=ED=99=95=EC=9E=A5=20?= =?UTF-8?q?=EB=B0=8F=20AgentSettingsPanel=20=ED=9B=85=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AgentLoopService.Skills.cs: - RunSkillInForkAsync에 PreSkillExecute 훅 발화 추가 (LLM 호출 직전) - RunSkillInForkAsync에 PostSkillExecute 훅 발화 추가 (응답 수신 후, fire-and-forget) ChatWindow.WorkFolder.cs: - SetWorkFolder()에 CwdChanged 훅 발화 추가 (작업 폴더 변경 시, fire-and-forget) - toolInput=path로 변경된 경로 훅 컨텍스트에 전달 ChatWindow.SlashCommands.cs: - InjectSlashCommand() 수정: 잘못된 ResolveSlashCommand/SendUserMessageAsync 호출 제거 - ShowSlashChip + SendMessageAsync 올바른 패턴으로 교체 (빌드 오류 4개 수정) AgentSettingsPanel.xaml / .xaml.cs: - 훅 이벤트 섹션 추가: ChkExtendedHooks 토글, HookSummaryText, BtnViewHooks, BtnOpenHookSettings - RefreshHookSummary(): 이벤트 종류별 활성 훅 수 집계 표시 - BtnViewHooks_Click → InjectSlashCommand("/hooks") 연동 - BtnOpenHookSettings_Click → settings.dat 기본 편집기로 열기 빌드: 경고 0, 오류 0 --- .../Services/Agent/AgentLoopService.Skills.cs | 8 ++ .../Views/ChatWindow.SlashCommands.cs | 15 ++++ src/AxCopilot/Views/ChatWindow.WorkFolder.cs | 7 ++ .../Views/Controls/AgentSettingsPanel.xaml | 79 +++++++++++++++++++ .../Views/Controls/AgentSettingsPanel.xaml.cs | 79 +++++++++++++++++++ 5 files changed, 188 insertions(+) diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.Skills.cs b/src/AxCopilot/Services/Agent/AgentLoopService.Skills.cs index b796151..90762f7 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopService.Skills.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopService.Skills.cs @@ -98,9 +98,17 @@ public partial class AgentLoopService EmitEvent(AgentEventType.Thinking, "skill_fork", $"[Fork] '{skill.Label}' 스킬을 격리 컨텍스트에서 실행 중..."); + // Phase 17-C: PreSkillExecute 훅 발화 + _ = RunExtendedEventAsync(HookEventKind.PreSkillExecute, forkMessages, ct, + toolName: skill.Name, toolInput: preparedBody); + // 도구 없이 텍스트 응답만 생성 (격리 실행) var response = await _llm.SendAsync(forkMessages, ct); + // Phase 17-C: PostSkillExecute 훅 발화 (fire-and-forget) + _ = RunExtendedEventAsync(HookEventKind.PostSkillExecute, null, CancellationToken.None, + toolName: skill.Name, toolOutput: response ?? ""); + // SkillCompleted 이벤트 로그 기록 _ = _eventLog?.AppendAsync(AgentEventLogType.SkillCompleted, System.Text.Json.JsonSerializer.Serialize(new diff --git a/src/AxCopilot/Views/ChatWindow.SlashCommands.cs b/src/AxCopilot/Views/ChatWindow.SlashCommands.cs index 65c5b9d..e76accd 100644 --- a/src/AxCopilot/Views/ChatWindow.SlashCommands.cs +++ b/src/AxCopilot/Views/ChatWindow.SlashCommands.cs @@ -411,4 +411,19 @@ public partial class ChatWindow return (null, input); } + + /// + /// 외부에서 슬래시 명령을 입력창에 주입하여 실행합니다. + /// AgentSettingsPanel 등에서 /hooks 조회 등에 사용합니다. + /// + public void InjectSlashCommand(string slashCmd) + { + Dispatcher.Invoke(() => + { + SlashPopup.IsOpen = false; + ShowSlashChip(slashCmd); + InputBox.Text = ""; + _ = SendMessageAsync(); + }); + } } diff --git a/src/AxCopilot/Views/ChatWindow.WorkFolder.cs b/src/AxCopilot/Views/ChatWindow.WorkFolder.cs index 68fd189..043f8f6 100644 --- a/src/AxCopilot/Views/ChatWindow.WorkFolder.cs +++ b/src/AxCopilot/Views/ChatWindow.WorkFolder.cs @@ -208,6 +208,13 @@ public partial class ChatWindow if (recent.Count > maxRecent) recent.RemoveRange(maxRecent, recent.Count - maxRecent); Llm.WorkFolder = path; _settings.Save(); + + // Phase 17-C: CwdChanged 훅 발화 (fire-and-forget) + _ = _agentLoop.RunExtendedEventAsync( + Services.Agent.HookEventKind.CwdChanged, + null, + CancellationToken.None, + toolInput: path); } private string GetCurrentWorkFolder() diff --git a/src/AxCopilot/Views/Controls/AgentSettingsPanel.xaml b/src/AxCopilot/Views/Controls/AgentSettingsPanel.xaml index 69d9bf4..b3443c3 100644 --- a/src/AxCopilot/Views/Controls/AgentSettingsPanel.xaml +++ b/src/AxCopilot/Views/Controls/AgentSettingsPanel.xaml @@ -485,6 +485,85 @@ Checked="ChkDevMode_Changed" Unchecked="ChkDevMode_Changed"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AxCopilot/Views/Controls/AgentSettingsPanel.xaml.cs b/src/AxCopilot/Views/Controls/AgentSettingsPanel.xaml.cs index 8e33564..9599296 100644 --- a/src/AxCopilot/Views/Controls/AgentSettingsPanel.xaml.cs +++ b/src/AxCopilot/Views/Controls/AgentSettingsPanel.xaml.cs @@ -115,6 +115,11 @@ public partial class AgentSettingsPanel : UserControl // MCP 서버 목록 표시 BuildMcpServerList(settings); + // Phase 17-C: 훅 UI 초기화 + if (ChkExtendedHooks != null) + ChkExtendedHooks.IsChecked = llm.EnableToolHooks; + RefreshHookSummary(); + _isLoading = false; } @@ -356,6 +361,80 @@ public partial class AgentSettingsPanel : UserControl SaveSetting(s => s.Llm.DevMode = ChkDevMode.IsChecked == true); } + // ── Phase 17-C: 훅 이벤트 UI ───────────────────────────────────────── + + private void ChkExtendedHooks_Changed(object sender, RoutedEventArgs e) + { + if (_isLoading) return; + SaveSetting(s => s.Llm.EnableToolHooks = ChkExtendedHooks.IsChecked == true); + RefreshHookSummary(); + } + + private void BtnViewHooks_Click(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + // ChatWindow에서 /hooks 슬래시 명령 실행 + var chatWin = System.Windows.Application.Current?.Windows + .OfType() + .FirstOrDefault(w => w.IsVisible); + chatWin?.InjectSlashCommand("/hooks"); + } + + private void BtnOpenHookSettings_Click(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + // settings.json 파일을 기본 편집기로 열기 + var path = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "AxCopilot", "settings.dat"); + if (System.IO.File.Exists(path)) + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) + { UseShellExecute = true }); + } + + private void RefreshHookSummary() + { + if (HookSummaryText == null) return; + + var settings = CurrentApp?.SettingsService?.Settings; + if (settings == null) { HookSummaryText.Text = "설정 로드 실패"; return; } + + if (!settings.Llm.EnableToolHooks) + { + HookSummaryText.Text = "확장 훅 비활성화됨"; + return; + } + + var hooks = settings.Llm.ExtendedHooks; + var counts = new[] + { + hooks.PreToolUse.Count(h => h.Enabled), + hooks.PostToolUse.Count(h => h.Enabled), + hooks.PostToolUseFailure.Count(h => h.Enabled), + hooks.SessionStart.Count(h => h.Enabled), + hooks.SessionEnd.Count(h => h.Enabled), + hooks.UserPromptSubmit.Count(h => h.Enabled), + hooks.AgentStop.Count(h => h.Enabled), + hooks.PreCompact.Count(h => h.Enabled), + hooks.PostCompact.Count(h => h.Enabled), + hooks.FileChanged.Count(h => h.Enabled), + hooks.PermissionRequest.Count(h => h.Enabled), + }; + + var total = counts.Sum(); + if (total == 0) + { + HookSummaryText.Text = "등록된 훅 없음 · settings.json extended_hooks에서 추가"; + return; + } + + var parts = new List(); + if (hooks.PreToolUse.Any(h => h.Enabled)) parts.Add($"PreTool×{hooks.PreToolUse.Count(h => h.Enabled)}"); + if (hooks.PostToolUse.Any(h => h.Enabled)) parts.Add($"PostTool×{hooks.PostToolUse.Count(h => h.Enabled)}"); + if (hooks.SessionStart.Any(h => h.Enabled)) parts.Add($"Session×{hooks.SessionStart.Count(h => h.Enabled)}"); + if (hooks.UserPromptSubmit.Any(h => h.Enabled)) parts.Add($"Prompt×{hooks.UserPromptSubmit.Count(h => h.Enabled)}"); + + HookSummaryText.Text = $"활성 훅 {total}개 · {string.Join(", ", parts)}"; + } + // ── Phase 17-A: Reflexion 핸들러 ────────────────────────────────────── private void ChkReflexion_Changed(object sender, RoutedEventArgs e)