tool_result preview 복원과 슬래시 명령 합성 일원화
목적: - 긴 세션, 분기, 재시작 이후에도 tool_result preview 축약 상태를 더 안정적으로 유지합니다. - 슬래시 팔레트와 실제 /토큰 실행 해석이 어긋나지 않도록 built-in command와 skill 우선순위를 같은 규칙으로 맞춥니다. 핵심 수정: - AgentMessageInvariantHelper에 tool_use_id 기준 preview 맵/복원 helper를 추가했습니다. - ChatSessionStateService는 분기 대화 생성 시 QueryPreviewContent를 함께 복사하고, 저장된 대화를 다시 열 때 누락된 preview를 복원합니다. - ChatStorageService는 저장 직전에 누락된 tool_result preview를 먼저 채워 재시작 후 축약 상태가 흔들리지 않게 정리했습니다. - SlashCommandCatalog에 exact token 충돌 해석용 ResolvePreferredCommand를 추가하고, ChatWindow.ParseSlashCommandAsync가 built-in/skill 후보를 함께 모아 같은 우선순위 규칙으로 실행 대상을 선택하도록 맞췄습니다. - SlashCommandCatalogTests를 새로 추가하고 ChatSessionStateServiceTests를 확장해 preview 복원과 skill 우선 해석을 회귀 검증했습니다. - README.md, docs/DEVELOPMENT.md를 2026-04-15 07:16 (KST) 기준으로 갱신했습니다. 검증: - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_preview_state\\ -p:IntermediateOutputPath=obj\\verify_preview_state\\ - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentToolResultBudgetTests|ChatSessionStateServiceTests" -p:OutputPath=bin\\verify_preview_state_tests\\ -p:IntermediateOutputPath=obj\\verify_preview_state_tests\\ (통과 38) - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_command_resolution\\ -p:IntermediateOutputPath=obj\\verify_command_resolution\\ - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "SlashCommandCatalogTests|ChatSessionStateServiceTests|AgentToolResultBudgetTests|AgentCommandQueueTests" -p:OutputPath=bin\\verify_command_resolution_tests\\ -p:IntermediateOutputPath=obj\\verify_command_resolution_tests\\ (통과 50)
This commit is contained in:
@@ -3607,7 +3607,44 @@ public partial class ChatWindow : Window
|
||||
{
|
||||
var firstSpace = trimmed.IndexOf(' ');
|
||||
var commandToken = (firstSpace >= 0 ? trimmed[..firstSpace] : trimmed).Trim();
|
||||
if (SlashCommandCatalog.TryGetEntry(commandToken, out var entry))
|
||||
var isDev = _activeTab is "Cowork" or "Code";
|
||||
var exactEntries = new List<(string Cmd, string Label, bool IsSkill, int Priority)>();
|
||||
|
||||
if (SlashCommandCatalog.TryGetEntry(commandToken, out var entry)
|
||||
&& (entry.Tab == "all" || (isDev && entry.Tab == "dev")))
|
||||
{
|
||||
exactEntries.Add((
|
||||
Cmd: commandToken,
|
||||
Label: entry.Label,
|
||||
IsSkill: false,
|
||||
Priority: SlashCommandCatalog.GetBuiltInCommandPriority(commandToken)));
|
||||
}
|
||||
|
||||
if (_settings.Settings.Llm.EnableSkillSystem)
|
||||
{
|
||||
EnsureSkillSystemLoadedForCurrentWorkspace();
|
||||
exactEntries.AddRange(
|
||||
SkillService.MatchSlashCommand(commandToken)
|
||||
.Where(s => s.IsVisibleInTab(_activeTab))
|
||||
.Where(s => string.Equals("/" + s.Name, commandToken, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(s => (
|
||||
Cmd: "/" + s.Name,
|
||||
Label: BuildSlashSkillLabel(s),
|
||||
IsSkill: true,
|
||||
Priority: SkillService.GetSkillSourcePriority(s.SourceScope))));
|
||||
}
|
||||
|
||||
var preferredCommand = SlashCommandCatalog.ResolvePreferredCommand(commandToken, exactEntries);
|
||||
if (preferredCommand.HasValue && preferredCommand.Value.IsSkill)
|
||||
{
|
||||
var compiled = await SkillService.BuildSlashInvocationAsync(input, GetCurrentWorkFolder(), ct);
|
||||
if (compiled != null)
|
||||
return (compiled.SystemPrompt, compiled.DisplayText);
|
||||
}
|
||||
|
||||
if (preferredCommand.HasValue
|
||||
&& !preferredCommand.Value.IsSkill
|
||||
&& SlashCommandCatalog.TryGetEntry(commandToken, out entry))
|
||||
{
|
||||
// __HELP__는 특수 처리 (ParseSlashCommand에서는 무시)
|
||||
if (entry.SystemPrompt == "__HELP__") return (null, input);
|
||||
|
||||
@@ -128,19 +128,42 @@ internal static class SlashCommandCatalog
|
||||
if (entries == null)
|
||||
return [];
|
||||
|
||||
return OrderEntries(entries)
|
||||
.Select(entry => (entry.Cmd, entry.Label, entry.IsSkill, entry.Priority))
|
||||
.Select(entry => (entry.Cmd, entry.Label, entry.IsSkill))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
internal static (string Cmd, string Label, bool IsSkill)? ResolvePreferredCommand(
|
||||
string commandToken,
|
||||
IEnumerable<(string Cmd, string Label, bool IsSkill, int Priority)> entries)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(commandToken) || entries == null)
|
||||
return null;
|
||||
|
||||
var resolved = OrderEntries(entries.Where(entry =>
|
||||
string.Equals(entry.Cmd, commandToken, StringComparison.OrdinalIgnoreCase)))
|
||||
.FirstOrDefault();
|
||||
|
||||
return string.IsNullOrWhiteSpace(resolved.Cmd)
|
||||
? null
|
||||
: (resolved.Cmd, resolved.Label, resolved.IsSkill);
|
||||
}
|
||||
|
||||
private static IEnumerable<(string Cmd, string Label, bool IsSkill, int Priority)> OrderEntries(
|
||||
IEnumerable<(string Cmd, string Label, bool IsSkill, int Priority)> entries)
|
||||
{
|
||||
return entries
|
||||
.Where(entry => !string.IsNullOrWhiteSpace(entry.Cmd))
|
||||
.GroupBy(entry => entry.Cmd, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => group
|
||||
.OrderByDescending(entry => entry.Priority)
|
||||
.ThenBy(entry => entry.IsSkill ? 1 : 0)
|
||||
.ThenByDescending(entry => entry.IsSkill)
|
||||
.ThenBy(entry => entry.Cmd, StringComparer.OrdinalIgnoreCase)
|
||||
.First())
|
||||
.Select(entry => (entry.Cmd, entry.Label, entry.IsSkill, entry.Priority))
|
||||
.OrderByDescending(entry => entry.Priority)
|
||||
.ThenBy(entry => entry.Cmd, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(entry => (entry.Cmd, entry.Label, entry.IsSkill))
|
||||
.ToList();
|
||||
.ThenByDescending(entry => entry.IsSkill)
|
||||
.ThenBy(entry => entry.Cmd, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
internal static bool TryGetEntry(string commandToken, out (string Label, string SystemPrompt, string Tab) entry)
|
||||
|
||||
Reference in New Issue
Block a user