[Phase 17-C] 훅 이벤트 시스템 확장 및 AgentSettingsPanel 훅 UI

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
This commit is contained in:
2026-04-04 13:34:36 +09:00
parent dab633edd5
commit d6f1e85cec
5 changed files with 188 additions and 0 deletions

View File

@@ -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

View File

@@ -411,4 +411,19 @@ public partial class ChatWindow
return (null, input);
}
/// <summary>
/// 외부에서 슬래시 명령을 입력창에 주입하여 실행합니다.
/// AgentSettingsPanel 등에서 /hooks 조회 등에 사용합니다.
/// </summary>
public void InjectSlashCommand(string slashCmd)
{
Dispatcher.Invoke(() =>
{
SlashPopup.IsOpen = false;
ShowSlashChip(slashCmd);
InputBox.Text = "";
_ = SendMessageAsync();
});
}
}

View File

@@ -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()

View File

@@ -485,6 +485,85 @@
Checked="ChkDevMode_Changed" Unchecked="ChkDevMode_Changed"/>
</Grid>
<!-- ─── 훅 설정 (Phase 17-C) ───────────────────────────── -->
<Border Height="1" Background="{DynamicResource BorderColor}" Margin="0,6,0,10"/>
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE8EE;" FontFamily="Segoe MDL2 Assets" FontSize="11"
Foreground="{DynamicResource AccentColor}"
VerticalAlignment="Center" Margin="0,0,5,0"/>
<TextBlock Text="훅 이벤트" Foreground="{DynamicResource PrimaryText}"
FontSize="12" FontWeight="SemiBold" VerticalAlignment="Center"/>
</StackPanel>
<!-- 훅 관리 버튼 → 슬래시 /hooks 명령으로 활성 목록 표시 -->
<Border Grid.Column="1" CornerRadius="4" Padding="8,3" Cursor="Hand"
Background="#18FFFFFF" MouseLeftButtonUp="BtnViewHooks_Click">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#18FFFFFF"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#28FFFFFF"/>
</Trigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="/hooks 조회" FontSize="11"
Foreground="{DynamicResource AccentColor}"/>
</Border>
</Grid>
<!-- 훅 활성화 토글 -->
<Grid Margin="0,0,0,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="확장 훅 활성화" Foreground="{DynamicResource SecondaryText}" FontSize="12"
VerticalAlignment="Center"/>
<CheckBox x:Name="ChkExtendedHooks" Grid.Column="1"
Style="{StaticResource ToggleSwitch}"
Checked="ChkExtendedHooks_Changed" Unchecked="ChkExtendedHooks_Changed"/>
</Grid>
<!-- 활성 훅 요약 표시 -->
<Border Background="{DynamicResource ItemBackground}" CornerRadius="6"
Padding="10,7" Margin="0,0,0,6">
<StackPanel>
<TextBlock x:Name="HookSummaryText"
Text="훅 없음 · settings.json의 extended_hooks에서 추가"
FontSize="11"
Foreground="{DynamicResource SecondaryText}"
TextWrapping="Wrap"/>
</StackPanel>
</Border>
<!-- 훅 설정 파일 열기 -->
<Border CornerRadius="4" Padding="10,5" Cursor="Hand" Margin="0,0,0,4"
MouseLeftButtonUp="BtnOpenHookSettings_Click">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#18FFFFFF"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#28FFFFFF"/>
</Trigger>
</Style.Triggers>
</Style>
</Border.Style>
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE8A5;" FontFamily="Segoe MDL2 Assets" FontSize="11"
Foreground="{DynamicResource SecondaryText}"
VerticalAlignment="Center" Margin="0,0,5,0"/>
<TextBlock Text="settings.json 열기 (훅 편집)"
FontSize="11" Foreground="{DynamicResource SecondaryText}"/>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>

View File

@@ -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<ChatWindow>()
.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<string>();
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)