스킬 정책 제어와 inline shell 안전장치 추가
스킬 시스템 설정에 프로젝트 스킬 탐색, 플러그인 스킬 탐색, 레거시 command 스킬 호환, inline shell 허용 여부와 시간/출력 제한을 추가하고 일반 설정 및 AX Agent 설정 UI에 연결했다. SkillService는 로드 시그니처에 실제 스킬 파일 수와 최근 수정 시각을 반영하도록 보강해 같은 폴더라도 스킬 파일이 바뀌면 다음 로드 요청에서 자동으로 재탐색되도록 정리했다. inline shell 실행기는 설정 기반 비활성화, timeout, 최대 출력 길이 제한을 적용하고 스킬 편집기/갤러리는 lazy prompt body 경로와 ReloadFromCurrentSettings()를 사용하도록 맞췄다. 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase4b\\ -p:IntermediateOutputPath=obj\\verify_phase4b\\ (경고 0 / 오류 0) 검증: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentToolCatalogTests|SkillServiceRuntimePolicyTests" -p:OutputPath=bin\\verify_phase4b_tests\\ -p:IntermediateOutputPath=obj\\verify_phase4b_tests\\ (통과 18, 기존 WorkspaceContextGeneratorTests nullable 경고 1건 유지)
This commit is contained in:
@@ -1296,6 +1296,24 @@ public class LlmSettings
|
||||
[JsonPropertyName("additionalSkillFolders")]
|
||||
public List<string> AdditionalSkillFolders { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("enableProjectSkillDiscovery")]
|
||||
public bool EnableProjectSkillDiscovery { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("enablePluginSkillDiscovery")]
|
||||
public bool EnablePluginSkillDiscovery { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("enableLegacyCommandSkills")]
|
||||
public bool EnableLegacyCommandSkills { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("enableSkillInlineShell")]
|
||||
public bool EnableSkillInlineShell { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("skillInlineShellTimeoutSeconds")]
|
||||
public int SkillInlineShellTimeoutSeconds { get; set; } = 8;
|
||||
|
||||
[JsonPropertyName("skillInlineShellMaxOutputChars")]
|
||||
public int SkillInlineShellMaxOutputChars { get; set; } = 4000;
|
||||
|
||||
/// <summary>슬래시 명령어 팝업 한 번에 표시할 최대 항목 수 (3~20). 기본 7.</summary>
|
||||
[JsonPropertyName("slashPopupPageSize")]
|
||||
public int SlashPopupPageSize { get; set; } = 7;
|
||||
|
||||
@@ -23,6 +23,13 @@ public static class SkillService
|
||||
/// <summary>로드된 스킬 목록.</summary>
|
||||
public static IReadOnlyList<SkillDefinition> Skills => _skills;
|
||||
|
||||
public static void ReloadFromCurrentSettings(string? projectRoot = null)
|
||||
{
|
||||
var app = System.Windows.Application.Current as App;
|
||||
var llm = app?.SettingsService?.Settings.Llm;
|
||||
LoadSkills(llm?.SkillsFolderPath, projectRoot ?? llm?.WorkFolder, llm?.AdditionalSkillFolders);
|
||||
}
|
||||
|
||||
/// <summary>스킬 폴더에서 *.skill.md / SKILL.md 파일을 로드합니다.</summary>
|
||||
public static void LoadSkills(string? customFolder = null, string? projectRoot = null, IEnumerable<string>? additionalFolders = null)
|
||||
{
|
||||
@@ -30,7 +37,7 @@ public static class SkillService
|
||||
var normalizedProjectRoot = NormalizeExistingDirectory(projectRoot);
|
||||
var normalizedAdditionalFolders = NormalizeDistinctDirectories(additionalFolders);
|
||||
var sources = BuildSkillSources(normalizedCustomFolder, normalizedProjectRoot, normalizedAdditionalFolders).ToList();
|
||||
var loadSignature = string.Join("|", sources.Select(source => $"{source.Kind}:{source.Scope}:{source.Directory}"));
|
||||
var loadSignature = ComputeLoadSignature(sources);
|
||||
if (_skills.Count > 0 && string.Equals(_lastLoadSignature, loadSignature, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
@@ -324,6 +331,8 @@ public static class SkillService
|
||||
string? projectRoot,
|
||||
IEnumerable<string> additionalFolders)
|
||||
{
|
||||
var app = System.Windows.Application.Current as App;
|
||||
var llm = app?.SettingsService?.Settings.Llm;
|
||||
var sources = new List<SkillSourceDescriptor>();
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -344,18 +353,44 @@ public static class SkillService
|
||||
foreach (var folder in additionalFolders)
|
||||
AddSource(folder, "additional");
|
||||
|
||||
foreach (var folder in EnumeratePluginSkillFolders())
|
||||
AddSource(folder, "plugin");
|
||||
if (llm?.EnablePluginSkillDiscovery ?? true)
|
||||
{
|
||||
foreach (var folder in EnumeratePluginSkillFolders())
|
||||
AddSource(folder, "plugin");
|
||||
}
|
||||
|
||||
foreach (var folder in EnumerateProjectSkillFolders(projectRoot))
|
||||
AddSource(folder, "project");
|
||||
if (llm?.EnableProjectSkillDiscovery ?? true)
|
||||
{
|
||||
foreach (var folder in EnumerateProjectSkillFolders(projectRoot))
|
||||
AddSource(folder, "project");
|
||||
}
|
||||
|
||||
foreach (var folder in EnumerateLegacyCommandFolders(projectRoot))
|
||||
AddSource(folder, "legacy", SkillSourceKind.LegacyCommand);
|
||||
if (llm?.EnableLegacyCommandSkills ?? true)
|
||||
{
|
||||
foreach (var folder in EnumerateLegacyCommandFolders(projectRoot))
|
||||
AddSource(folder, "legacy", SkillSourceKind.LegacyCommand);
|
||||
}
|
||||
|
||||
return sources;
|
||||
}
|
||||
|
||||
private static string ComputeLoadSignature(IEnumerable<SkillSourceDescriptor> sources)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var files = source.Kind == SkillSourceKind.LegacyCommand
|
||||
? EnumerateLegacyCommandFiles(source.Directory).ToList()
|
||||
: EnumerateSkillFiles(source.Directory).ToList();
|
||||
var latestWrite = files.Count == 0
|
||||
? Directory.GetLastWriteTimeUtc(source.Directory).Ticks
|
||||
: files.Max(file => File.GetLastWriteTimeUtc(file).Ticks);
|
||||
parts.Add($"{source.Kind}:{source.Scope}:{source.Directory}:{files.Count}:{latestWrite}");
|
||||
}
|
||||
|
||||
return string.Join("|", parts);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateProjectSkillFolders(string? projectRoot)
|
||||
{
|
||||
foreach (var root in EnumerateAncestorDirectories(projectRoot))
|
||||
@@ -1332,8 +1367,17 @@ public static class SkillService
|
||||
string workFolder,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var app = System.Windows.Application.Current as App;
|
||||
var llm = app?.SettingsService?.Settings.Llm;
|
||||
if (llm is { EnableSkillInlineShell: false })
|
||||
return "[inline-shell disabled by settings]";
|
||||
|
||||
var timeoutSeconds = Math.Clamp(llm?.SkillInlineShellTimeoutSeconds ?? 8, 1, 30);
|
||||
var maxOutputChars = Math.Clamp(llm?.SkillInlineShellMaxOutputChars ?? 4000, 200, 20000);
|
||||
try
|
||||
{
|
||||
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
timeoutCts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
|
||||
var startInfo = shellKind == "powershell"
|
||||
? new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
@@ -1362,14 +1406,18 @@ public static class SkillService
|
||||
|
||||
using var process = new System.Diagnostics.Process { StartInfo = startInfo };
|
||||
process.Start();
|
||||
var stdout = await process.StandardOutput.ReadToEndAsync(ct).ConfigureAwait(false);
|
||||
var stderr = await process.StandardError.ReadToEndAsync(ct).ConfigureAwait(false);
|
||||
await process.WaitForExitAsync(ct).ConfigureAwait(false);
|
||||
var stdout = await process.StandardOutput.ReadToEndAsync(timeoutCts.Token).ConfigureAwait(false);
|
||||
var stderr = await process.StandardError.ReadToEndAsync(timeoutCts.Token).ConfigureAwait(false);
|
||||
await process.WaitForExitAsync(timeoutCts.Token).ConfigureAwait(false);
|
||||
var output = string.IsNullOrWhiteSpace(stdout) ? stderr : stdout;
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
return "[inline-shell: no output]";
|
||||
output = output.Trim();
|
||||
return output.Length > 4000 ? output[..4000] : output;
|
||||
return output.Length > maxOutputChars ? output[..maxOutputChars] : output;
|
||||
}
|
||||
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
||||
{
|
||||
return $"[inline-shell timeout] exceeded {timeoutSeconds}s";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -546,6 +546,48 @@ public class SettingsViewModel : INotifyPropertyChanged
|
||||
set { _additionalSkillFoldersText = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private bool _enableProjectSkillDiscovery = true;
|
||||
public bool EnableProjectSkillDiscovery
|
||||
{
|
||||
get => _enableProjectSkillDiscovery;
|
||||
set { _enableProjectSkillDiscovery = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private bool _enablePluginSkillDiscovery = true;
|
||||
public bool EnablePluginSkillDiscovery
|
||||
{
|
||||
get => _enablePluginSkillDiscovery;
|
||||
set { _enablePluginSkillDiscovery = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private bool _enableLegacyCommandSkills = true;
|
||||
public bool EnableLegacyCommandSkills
|
||||
{
|
||||
get => _enableLegacyCommandSkills;
|
||||
set { _enableLegacyCommandSkills = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private bool _enableSkillInlineShell = true;
|
||||
public bool EnableSkillInlineShell
|
||||
{
|
||||
get => _enableSkillInlineShell;
|
||||
set { _enableSkillInlineShell = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private int _skillInlineShellTimeoutSeconds = 8;
|
||||
public int SkillInlineShellTimeoutSeconds
|
||||
{
|
||||
get => _skillInlineShellTimeoutSeconds;
|
||||
set { _skillInlineShellTimeoutSeconds = Math.Clamp(value, 1, 30); OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private int _skillInlineShellMaxOutputChars = 4000;
|
||||
public int SkillInlineShellMaxOutputChars
|
||||
{
|
||||
get => _skillInlineShellMaxOutputChars;
|
||||
set { _skillInlineShellMaxOutputChars = Math.Clamp(value, 200, 20000); OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private int _slashPopupPageSize = 6;
|
||||
public int SlashPopupPageSize
|
||||
{
|
||||
@@ -1217,6 +1259,12 @@ public class SettingsViewModel : INotifyPropertyChanged
|
||||
_enableForkSkillDelegationEnforcement = llm.EnableForkSkillDelegationEnforcement;
|
||||
_skillsFolderPath = llm.SkillsFolderPath;
|
||||
_additionalSkillFoldersText = string.Join(Environment.NewLine, llm.AdditionalSkillFolders ?? new List<string>());
|
||||
_enableProjectSkillDiscovery = llm.EnableProjectSkillDiscovery;
|
||||
_enablePluginSkillDiscovery = llm.EnablePluginSkillDiscovery;
|
||||
_enableLegacyCommandSkills = llm.EnableLegacyCommandSkills;
|
||||
_enableSkillInlineShell = llm.EnableSkillInlineShell;
|
||||
_skillInlineShellTimeoutSeconds = Math.Clamp(llm.SkillInlineShellTimeoutSeconds, 1, 30);
|
||||
_skillInlineShellMaxOutputChars = Math.Clamp(llm.SkillInlineShellMaxOutputChars, 200, 20000);
|
||||
_slashPopupPageSize = llm.SlashPopupPageSize > 0 ? Math.Clamp(llm.SlashPopupPageSize, 3, 10) : 6;
|
||||
_maxFavoriteSlashCommands = llm.MaxFavoriteSlashCommands > 0 ? Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30) : 10;
|
||||
_maxRecentSlashCommands = llm.MaxRecentSlashCommands > 0 ? Math.Clamp(llm.MaxRecentSlashCommands, 5, 50) : 20;
|
||||
@@ -1668,6 +1716,12 @@ public class SettingsViewModel : INotifyPropertyChanged
|
||||
s.Llm.EnableForkSkillDelegationEnforcement = _enableForkSkillDelegationEnforcement;
|
||||
s.Llm.SkillsFolderPath = _skillsFolderPath;
|
||||
s.Llm.AdditionalSkillFolders = ParseAdditionalSkillFolders(_additionalSkillFoldersText);
|
||||
s.Llm.EnableProjectSkillDiscovery = _enableProjectSkillDiscovery;
|
||||
s.Llm.EnablePluginSkillDiscovery = _enablePluginSkillDiscovery;
|
||||
s.Llm.EnableLegacyCommandSkills = _enableLegacyCommandSkills;
|
||||
s.Llm.EnableSkillInlineShell = _enableSkillInlineShell;
|
||||
s.Llm.SkillInlineShellTimeoutSeconds = _skillInlineShellTimeoutSeconds;
|
||||
s.Llm.SkillInlineShellMaxOutputChars = _skillInlineShellMaxOutputChars;
|
||||
s.Llm.SlashPopupPageSize = _slashPopupPageSize;
|
||||
s.Llm.MaxFavoriteSlashCommands = _maxFavoriteSlashCommands;
|
||||
s.Llm.MaxRecentSlashCommands = _maxRecentSlashCommands;
|
||||
|
||||
@@ -869,6 +869,54 @@
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,10,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="프로젝트 스킬 자동 탐색"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<CheckBox x:Name="ChkEnableProjectSkillDiscovery"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,10,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="플러그인 스킬 탐색"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<CheckBox x:Name="ChkEnablePluginSkillDiscovery"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,10,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="레거시 명령 스킬 호환"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<CheckBox x:Name="ChkEnableLegacyCommandSkills"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,10,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="스킬 inline shell 허용"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<CheckBox x:Name="ChkEnableSkillInlineShell"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ToggleSwitch}"/>
|
||||
</Grid>
|
||||
<StackPanel Margin="0,12,0,0">
|
||||
<TextBlock Text="보조 스킬 폴더 목록"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
@@ -888,6 +936,40 @@
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
FontSize="12"/>
|
||||
</StackPanel>
|
||||
<Grid Margin="0,12,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="110"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="inline shell 시간 제한(초)"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<TextBox x:Name="TxtSkillInlineShellTimeoutSeconds"
|
||||
Grid.Column="1"
|
||||
Padding="10,7"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="1"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
FontSize="12"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,10,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="110"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="inline shell 출력 제한"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<TextBox x:Name="TxtSkillInlineShellMaxOutputChars"
|
||||
Grid.Column="1"
|
||||
Padding="10,7"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="1"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
FontSize="12"/>
|
||||
</Grid>
|
||||
<Border Height="1" Margin="0,14,0,14" Background="{DynamicResource SeparatorColor}"/>
|
||||
<TextBlock Text="도구 노출"
|
||||
FontSize="14"
|
||||
|
||||
@@ -66,6 +66,10 @@ public partial class AgentSettingsWindow : Window
|
||||
ChkEnableToolHooks.IsChecked = _llm.EnableToolHooks;
|
||||
ChkEnableHookInputMutation.IsChecked = _llm.EnableHookInputMutation;
|
||||
ChkEnableHookPermissionUpdate.IsChecked = _llm.EnableHookPermissionUpdate;
|
||||
ChkEnableProjectSkillDiscovery.IsChecked = _llm.EnableProjectSkillDiscovery;
|
||||
ChkEnablePluginSkillDiscovery.IsChecked = _llm.EnablePluginSkillDiscovery;
|
||||
ChkEnableLegacyCommandSkills.IsChecked = _llm.EnableLegacyCommandSkills;
|
||||
ChkEnableSkillInlineShell.IsChecked = _llm.EnableSkillInlineShell;
|
||||
ChkEnableCoworkVerification.IsChecked = _llm.EnableCoworkVerification;
|
||||
ChkEnableProjectRules.IsChecked = _llm.EnableProjectRules;
|
||||
ChkEnableAgentMemory.IsChecked = _llm.EnableAgentMemory;
|
||||
@@ -77,6 +81,8 @@ public partial class AgentSettingsWindow : Window
|
||||
ChkEnableCronTools.IsChecked = _llm.Code.EnableCronTools;
|
||||
TxtSkillsFolderPath.Text = _llm.SkillsFolderPath ?? "";
|
||||
TxtAdditionalSkillFolders.Text = string.Join(Environment.NewLine, _llm.AdditionalSkillFolders ?? new());
|
||||
TxtSkillInlineShellTimeoutSeconds.Text = Math.Clamp(_llm.SkillInlineShellTimeoutSeconds, 1, 30).ToString();
|
||||
TxtSkillInlineShellMaxOutputChars.Text = Math.Clamp(_llm.SkillInlineShellMaxOutputChars, 200, 20000).ToString();
|
||||
TxtSlashPopupPageSize.Text = Math.Clamp(_llm.SlashPopupPageSize, 3, 20).ToString();
|
||||
ChkEnableDragDropAiActions.IsChecked = _llm.EnableDragDropAiActions;
|
||||
ChkDragDropAutoSend.IsChecked = _llm.DragDropAutoSend;
|
||||
@@ -544,6 +550,10 @@ public partial class AgentSettingsWindow : Window
|
||||
_llm.EnableToolHooks = ChkEnableToolHooks.IsChecked == true;
|
||||
_llm.EnableHookInputMutation = ChkEnableHookInputMutation.IsChecked == true;
|
||||
_llm.EnableHookPermissionUpdate = ChkEnableHookPermissionUpdate.IsChecked == true;
|
||||
_llm.EnableProjectSkillDiscovery = ChkEnableProjectSkillDiscovery.IsChecked == true;
|
||||
_llm.EnablePluginSkillDiscovery = ChkEnablePluginSkillDiscovery.IsChecked == true;
|
||||
_llm.EnableLegacyCommandSkills = ChkEnableLegacyCommandSkills.IsChecked == true;
|
||||
_llm.EnableSkillInlineShell = ChkEnableSkillInlineShell.IsChecked == true;
|
||||
_llm.EnableCoworkVerification = ChkEnableCoworkVerification.IsChecked == true;
|
||||
_llm.EnableProjectRules = ChkEnableProjectRules.IsChecked == true;
|
||||
_llm.EnableAgentMemory = ChkEnableAgentMemory.IsChecked == true;
|
||||
@@ -555,6 +565,8 @@ public partial class AgentSettingsWindow : Window
|
||||
_llm.Code.EnableCronTools = ChkEnableCronTools.IsChecked == true;
|
||||
_llm.SkillsFolderPath = TxtSkillsFolderPath.Text?.Trim() ?? "";
|
||||
_llm.AdditionalSkillFolders = ParseAdditionalSkillFolders(TxtAdditionalSkillFolders.Text);
|
||||
_llm.SkillInlineShellTimeoutSeconds = ParseInt(TxtSkillInlineShellTimeoutSeconds.Text, 8, 1, 30);
|
||||
_llm.SkillInlineShellMaxOutputChars = ParseInt(TxtSkillInlineShellMaxOutputChars.Text, 4000, 200, 20000);
|
||||
_llm.SlashPopupPageSize = ParseInt(TxtSlashPopupPageSize.Text, 7, 3, 20);
|
||||
_llm.EnableDragDropAiActions = ChkEnableDragDropAiActions.IsChecked == true;
|
||||
_llm.DragDropAutoSend = ChkDragDropAutoSend.IsChecked == true;
|
||||
|
||||
@@ -4364,6 +4364,84 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Style="{StaticResource SettingsRow}">
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||
<TextBlock Style="{StaticResource RowLabel}" Text="프로젝트 스킬 자동 탐색"/>
|
||||
<TextBlock Style="{StaticResource RowHint}" Text="작업 폴더 상위 경로의 `.claude/skills`를 따라가며 프로젝트 스킬을 자동으로 찾습니다."/>
|
||||
</StackPanel>
|
||||
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
IsChecked="{Binding EnableProjectSkillDiscovery, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Style="{StaticResource SettingsRow}">
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||
<TextBlock Style="{StaticResource RowLabel}" Text="플러그인 스킬 탐색"/>
|
||||
<TextBlock Style="{StaticResource RowHint}" Text="설치된 플러그인 폴더 안의 `skills` 또는 `.claude-plugin/skills`를 함께 읽습니다."/>
|
||||
</StackPanel>
|
||||
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
IsChecked="{Binding EnablePluginSkillDiscovery, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Style="{StaticResource SettingsRow}">
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||
<TextBlock Style="{StaticResource RowLabel}" Text="레거시 명령 스킬 호환"/>
|
||||
<TextBlock Style="{StaticResource RowHint}" Text="`.claude/commands`의 markdown command를 레거시 스킬처럼 읽어 호환합니다."/>
|
||||
</StackPanel>
|
||||
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
IsChecked="{Binding EnableLegacyCommandSkills, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Style="{StaticResource SettingsRow}">
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
|
||||
<TextBlock Style="{StaticResource RowLabel}" Text="스킬 inline shell 허용"/>
|
||||
<TextBlock Style="{StaticResource RowHint}" Text="스킬 본문의 inline shell 블록 실행을 허용합니다. 꺼두면 실행 없이 안내 문구로 대체됩니다."/>
|
||||
</StackPanel>
|
||||
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
IsChecked="{Binding EnableSkillInlineShell, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Style="{StaticResource SettingsRow}">
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Left">
|
||||
<TextBlock Style="{StaticResource RowLabel}" Text="스킬 inline shell 시간 제한"/>
|
||||
<TextBlock Style="{StaticResource RowHint}" Text="inline shell 명령의 최대 실행 시간입니다. (1~30초)"/>
|
||||
</StackPanel>
|
||||
<StackPanel HorizontalAlignment="Right" VerticalAlignment="Center" Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding SkillInlineShellTimeoutSeconds}" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center"
|
||||
MinWidth="24" TextAlignment="Right" Margin="0,0,8,0"/>
|
||||
<Slider Width="140" Minimum="1" Maximum="30" TickFrequency="1"
|
||||
IsSnapToTickEnabled="True"
|
||||
Value="{Binding SkillInlineShellTimeoutSeconds, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Style="{StaticResource SettingsRow}">
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Left">
|
||||
<TextBlock Style="{StaticResource RowLabel}" Text="스킬 inline shell 출력 제한"/>
|
||||
<TextBlock Style="{StaticResource RowHint}" Text="inline shell 결과를 프롬프트에 붙일 최대 문자 수입니다. (200~20000자)"/>
|
||||
</StackPanel>
|
||||
<StackPanel HorizontalAlignment="Right" VerticalAlignment="Center" Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding SkillInlineShellMaxOutputChars}" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center"
|
||||
MinWidth="48" TextAlignment="Right" Margin="0,0,8,0"/>
|
||||
<Slider Width="140" Minimum="200" Maximum="20000" TickFrequency="200"
|
||||
IsSnapToTickEnabled="True"
|
||||
Value="{Binding SkillInlineShellMaxOutputChars, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Style="{StaticResource SettingsRow}">
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Left">
|
||||
|
||||
@@ -482,7 +482,7 @@ public partial class SkillEditorWindow : Window
|
||||
try
|
||||
{
|
||||
File.WriteAllText(savePath, content, System.Text.Encoding.UTF8);
|
||||
SkillService.LoadSkills();
|
||||
SkillService.ReloadFromCurrentSettings();
|
||||
|
||||
StatusText.Text = $"✓ 저장 완료: {Path.GetFileName(savePath)}";
|
||||
StatusText.Foreground = new SolidColorBrush(Color.FromRgb(0x34, 0xD3, 0x99));
|
||||
|
||||
@@ -279,7 +279,7 @@ public partial class SkillGalleryWindow : Window
|
||||
var editor = new SkillEditorWindow(skill) { Owner = this };
|
||||
if (editor.ShowDialog() == true)
|
||||
{
|
||||
SkillService.LoadSkills();
|
||||
SkillService.ReloadFromCurrentSettings();
|
||||
BuildCategoryFilter();
|
||||
RenderSkills();
|
||||
}
|
||||
@@ -307,7 +307,7 @@ public partial class SkillGalleryWindow : Window
|
||||
destPath = Path.Combine(destFolder, $"{srcName}_copy{counter++}.skill.md");
|
||||
|
||||
File.Copy(skill.FilePath, destPath);
|
||||
SkillService.LoadSkills();
|
||||
SkillService.ReloadFromCurrentSettings();
|
||||
BuildCategoryFilter();
|
||||
RenderSkills();
|
||||
}
|
||||
@@ -342,7 +342,7 @@ public partial class SkillGalleryWindow : Window
|
||||
try
|
||||
{
|
||||
File.Delete(skill.FilePath);
|
||||
SkillService.LoadSkills();
|
||||
SkillService.ReloadFromCurrentSettings();
|
||||
BuildCategoryFilter();
|
||||
RenderSkills();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user