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:
18
README.md
18
README.md
@@ -1883,3 +1883,21 @@ MIT License
|
||||
- 테스트로 [AgentCommandQueueTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentCommandQueueTests.cs), [AgentToolResultBudgetTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentToolResultBudgetTests.cs)를 확장해 `priority batch dequeue`, `predicate matching`, `tool_use_id preview reuse`를 회귀 검증했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_queue_preview\\ -p:IntermediateOutputPath=obj\\verify_queue_preview\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentCommandQueueTests|AgentToolResultBudgetTests" -p:OutputPath=bin\\verify_queue_preview_tests\\ -p:IntermediateOutputPath=obj\\verify_queue_preview_tests\\` 통과 7
|
||||
|
||||
업데이트: 2026-04-15 07:11 (KST)
|
||||
- `tool_result` preview 안정화 2차를 반영했습니다. [AgentMessageInvariantHelper.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentMessageInvariantHelper.cs)는 `tool_use_id -> QueryPreviewContent` 맵을 만들고, 같은 tool result가 다른 메시지 객체로 다시 구성돼도 preview를 복원하는 helper를 제공합니다.
|
||||
- [ChatSessionStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/ChatSessionStateService.cs)는 분기 대화 생성 시 `QueryPreviewContent`를 함께 복사하고, 저장된 대화를 다시 열 때 누락된 preview를 `tool_use_id` 기준으로 보정합니다.
|
||||
- [ChatStorageService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/ChatStorageService.cs)는 대화를 저장하기 전에 누락된 tool_result preview를 먼저 채워 넣어 재시작 후에도 축약 상태가 더 안정적으로 유지되도록 정리했습니다.
|
||||
- 슬래시 합성도 한 단계 다듬었습니다. [SlashCommandCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SlashCommandCatalog.cs)는 같은 우선순위 충돌 시 builtin command보다 skill을 우선하고, 최종 정렬에서도 skill을 앞세워 동적 스킬이 `/` 팔레트에서 덜 가려지게 했습니다.
|
||||
- 테스트로 [ChatSessionStateServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ChatSessionStateServiceTests.cs)에 branch/save-load preview 복원 케이스를 추가했고, [SlashCommandCatalogTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/SlashCommandCatalogTests.cs)를 새로 추가해 skill 우선 dedupe와 정렬을 회귀 검증했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_preview_state\\ -p:IntermediateOutputPath=obj\\verify_preview_state\\` 경고 0 / 오류 0
|
||||
- 검증: `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_slash_compose\\ -p:IntermediateOutputPath=obj\\verify_slash_compose\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "SlashCommandCatalogTests|ChatSessionStateServiceTests|AgentToolResultBudgetTests" -p:OutputPath=bin\\verify_slash_compose_tests\\ -p:IntermediateOutputPath=obj\\verify_slash_compose_tests\\` 통과 44
|
||||
|
||||
업데이트: 2026-04-15 07:16 (KST)
|
||||
- 슬래시 명령 합성 2차를 반영했습니다. [SlashCommandCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SlashCommandCatalog.cs)는 팔레트 렌더링과 동일한 우선순위 규칙을 재사용하는 `ResolvePreferredCommand()`를 추가해, 같은 `/토큰`에 builtin command와 skill이 함께 있을 때도 일관된 해석이 가능하도록 정리했습니다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `ParseSlashCommandAsync()`는 이제 exact token 후보를 built-in/skill 양쪽에서 모은 뒤 위 합성 규칙으로 우선순위를 결정합니다. 팔레트에는 skill이 앞에 보이는데 실제 실행은 built-in이 먼저 잡히던 불일치를 없앤 변경입니다.
|
||||
- [SlashCommandCatalogTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/SlashCommandCatalogTests.cs)는 exact token 충돌 시 skill 우선 해석을 회귀 검증하도록 확장했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_command_resolution\\ -p:IntermediateOutputPath=obj\\verify_command_resolution\\` 경고 0 / 오류 0
|
||||
- 검증: `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
|
||||
|
||||
@@ -959,3 +959,14 @@ UI ?붿옄???洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾
|
||||
- 다음 배치에서는 `tool_result replacement state`를 대화 단위로 더 고정하고, 이후 `명령/스킬 합성 계층`과 `문서 포맷 마감`으로 순차 확장할 예정입니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_queue_preview\\ -p:IntermediateOutputPath=obj\\verify_queue_preview\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentCommandQueueTests|AgentToolResultBudgetTests" -p:OutputPath=bin\\verify_queue_preview_tests\\ -p:IntermediateOutputPath=obj\\verify_queue_preview_tests\\` 통과 7
|
||||
|
||||
업데이트: 2026-04-15 07:16 (KST)
|
||||
- `tool_result` preview 안정화 2차를 반영했습니다. [AgentMessageInvariantHelper.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentMessageInvariantHelper.cs)는 `tool_use_id -> QueryPreviewContent` 맵을 공용으로 만들고, 같은 tool result가 다른 메시지 객체로 다시 로드되더라도 preview를 복원하는 helper를 제공합니다.
|
||||
- [ChatSessionStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/ChatSessionStateService.cs)는 분기 대화 생성 시 `QueryPreviewContent`를 함께 복사하고, 저장된 대화를 다시 열 때 누락된 preview를 `tool_use_id` 기준으로 보정합니다. [ChatStorageService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/ChatStorageService.cs)도 저장 직전에 preview 보정을 먼저 수행해 재시작 후 축약 상태가 흔들리지 않게 맞췄습니다.
|
||||
- 슬래시 합성도 실행 경로까지 일원화했습니다. [SlashCommandCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SlashCommandCatalog.cs)는 exact token 충돌을 팔레트와 같은 우선순위로 해석하는 `ResolvePreferredCommand()`를 추가했고, [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `ParseSlashCommandAsync()`는 built-in/skill 후보를 함께 모은 뒤 같은 규칙으로 우선 대상을 선택합니다.
|
||||
- 이 변경으로 `/review`처럼 builtin command와 skill이 같은 토큰을 공유하는 경우에도 “팔레트에는 skill이 앞에 보이는데 실행은 builtin이 먼저 잡히는” 불일치가 줄어들었습니다.
|
||||
- 테스트는 [ChatSessionStateServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ChatSessionStateServiceTests.cs)에 branch/save-load preview 복원 케이스를 추가했고, [SlashCommandCatalogTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/SlashCommandCatalogTests.cs)를 새로 추가해 skill 우선 dedupe와 exact token 우선 해석을 회귀 검증했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_preview_state\\ -p:IntermediateOutputPath=obj\\verify_preview_state\\` 경고 0 / 오류 0
|
||||
- 검증: `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\\` 경고 0 / 오류 0
|
||||
- 검증: `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
|
||||
|
||||
@@ -642,7 +642,7 @@ public class ChatSessionStateServiceTests
|
||||
],
|
||||
Messages =
|
||||
[
|
||||
new ChatMessage { Role = "user", Content = "u1", Timestamp = DateTime.Now.AddMinutes(-3) },
|
||||
new ChatMessage { Role = "user", Content = "u1", Timestamp = DateTime.Now.AddMinutes(-3), QueryPreviewContent = "preview-1" },
|
||||
new ChatMessage { Role = "assistant", Content = "a1", Timestamp = DateTime.Now.AddMinutes(-2), MetaKind = "meta", MetaRunId = "run-1" },
|
||||
new ChatMessage { Role = "user", Content = "u2", Timestamp = DateTime.Now.AddMinutes(-1) }
|
||||
]
|
||||
@@ -655,11 +655,49 @@ public class ChatSessionStateServiceTests
|
||||
branch.WorkFolder.Should().Be(@"E:\workspace");
|
||||
branch.BranchLabel.Should().Contain("2");
|
||||
branch.Messages.Should().HaveCount(3);
|
||||
branch.Messages[0].QueryPreviewContent.Should().Be("preview-1");
|
||||
branch.Messages[1].MetaRunId.Should().Be("run-1");
|
||||
branch.Messages[2].MetaKind.Should().Be("branch_context");
|
||||
branch.AgentRunHistory.Should().ContainSingle();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadOrCreateConversation_RestoresMissingToolResultPreviewFromPersistedMessages()
|
||||
{
|
||||
var storage = new ChatStorageService();
|
||||
var settings = new SettingsService();
|
||||
var conversationId = $"conv-preview-{Guid.NewGuid():N}";
|
||||
var conversation = new ChatConversation
|
||||
{
|
||||
Id = conversationId,
|
||||
Tab = "Code",
|
||||
Title = "preview restore",
|
||||
Messages =
|
||||
[
|
||||
new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = """{"type":"tool_result","tool_use_id":"call-restore","tool_name":"file_read","content":"long output"}""",
|
||||
QueryPreviewContent = """{"type":"tool_result","tool_use_id":"call-restore","tool_name":"file_read","content":"preview"}"""
|
||||
},
|
||||
new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = """{"type":"tool_result","tool_use_id":"call-restore","tool_name":"file_read","content":"long output"}"""
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
storage.Save(conversation);
|
||||
|
||||
var session = new ChatSessionStateService();
|
||||
session.RememberConversation("Code", conversationId);
|
||||
var loaded = session.LoadOrCreateConversation("Code", storage, settings);
|
||||
|
||||
loaded.Messages.Should().HaveCount(2);
|
||||
loaded.Messages[1].QueryPreviewContent.Should().Be(loaded.Messages[0].QueryPreviewContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DraftStateHelpers_SelectAndTransitionQueuedItems()
|
||||
{
|
||||
|
||||
55
src/AxCopilot.Tests/Services/SlashCommandCatalogTests.cs
Normal file
55
src/AxCopilot.Tests/Services/SlashCommandCatalogTests.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using AxCopilot.Views;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class SlashCommandCatalogTests
|
||||
{
|
||||
[Fact]
|
||||
public void ComposeMatches_ShouldPreferSkillWhenPriorityTies()
|
||||
{
|
||||
var matches = SlashCommandCatalog.ComposeMatches(
|
||||
[
|
||||
(Cmd: "/review", Label: "Builtin Review", IsSkill: false, Priority: 2000),
|
||||
(Cmd: "/review", Label: "Skill Review", IsSkill: true, Priority: 2000),
|
||||
]);
|
||||
|
||||
matches.Should().ContainSingle();
|
||||
matches[0].Cmd.Should().Be("/review");
|
||||
matches[0].IsSkill.Should().BeTrue();
|
||||
matches[0].Label.Should().Be("Skill Review");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComposeMatches_ShouldSortByPriorityThenSkillBias()
|
||||
{
|
||||
var matches = SlashCommandCatalog.ComposeMatches(
|
||||
[
|
||||
(Cmd: "/status", Label: "Status", IsSkill: false, Priority: 2000),
|
||||
(Cmd: "/alpha", Label: "Alpha Skill", IsSkill: true, Priority: 2000),
|
||||
(Cmd: "/review", Label: "Review", IsSkill: false, Priority: 2100),
|
||||
]);
|
||||
|
||||
matches.Select(x => x.Cmd).Should().Equal("/review", "/alpha", "/status");
|
||||
matches[1].IsSkill.Should().BeTrue();
|
||||
matches[2].IsSkill.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolvePreferredCommand_ShouldPreferSkillForExactTokenWhenPriorityTies()
|
||||
{
|
||||
var resolved = SlashCommandCatalog.ResolvePreferredCommand(
|
||||
"/review",
|
||||
[
|
||||
(Cmd: "/review", Label: "Builtin Review", IsSkill: false, Priority: 2000),
|
||||
(Cmd: "/review", Label: "Skill Review", IsSkill: true, Priority: 2000),
|
||||
(Cmd: "/status", Label: "Status", IsSkill: false, Priority: 2000),
|
||||
]);
|
||||
|
||||
resolved.HasValue.Should().BeTrue();
|
||||
resolved!.Value.Cmd.Should().Be("/review");
|
||||
resolved.Value.IsSkill.Should().BeTrue();
|
||||
resolved.Value.Label.Should().Be("Skill Review");
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,51 @@ namespace AxCopilot.Services.Agent;
|
||||
/// </summary>
|
||||
internal static class AgentMessageInvariantHelper
|
||||
{
|
||||
public static Dictionary<string, string> BuildToolResultPreviewMap(IEnumerable<ChatMessage>? messages)
|
||||
{
|
||||
var previews = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
if (messages == null)
|
||||
return previews;
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message.QueryPreviewContent))
|
||||
continue;
|
||||
if (!TryGetToolResultId(message, out var toolResultId))
|
||||
continue;
|
||||
|
||||
previews[toolResultId] = message.QueryPreviewContent!;
|
||||
}
|
||||
|
||||
return previews;
|
||||
}
|
||||
|
||||
public static bool PopulateMissingToolResultPreviews(IList<ChatMessage>? messages)
|
||||
{
|
||||
if (messages == null || messages.Count == 0)
|
||||
return false;
|
||||
|
||||
var previews = BuildToolResultPreviewMap(messages);
|
||||
if (previews.Count == 0)
|
||||
return false;
|
||||
|
||||
var changed = false;
|
||||
foreach (var message in messages)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(message.QueryPreviewContent))
|
||||
continue;
|
||||
if (!TryGetToolResultId(message, out var toolResultId))
|
||||
continue;
|
||||
if (!previews.TryGetValue(toolResultId, out var preview))
|
||||
continue;
|
||||
|
||||
message.QueryPreviewContent = preview;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
public static int AdjustStartIndexForToolPairs(
|
||||
IReadOnlyList<ChatMessage> messages,
|
||||
int startIndex,
|
||||
|
||||
@@ -30,7 +30,7 @@ public static class AgentToolResultBudget
|
||||
var sourceById = sourceMessages?
|
||||
.Where(message => !string.IsNullOrWhiteSpace(message.MsgId))
|
||||
.ToDictionary(message => message.MsgId, StringComparer.OrdinalIgnoreCase);
|
||||
var sourcePreviewByToolResultId = BuildPreviewByToolResultId(sourceMessages);
|
||||
var sourcePreviewByToolResultId = AgentMessageInvariantHelper.BuildToolResultPreviewMap(sourceMessages);
|
||||
var nonSystemIndexes = messages
|
||||
.Select((message, index) => new { message, index })
|
||||
.Where(x => !string.Equals(x.message.Role, "system", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -100,25 +100,6 @@ public static class AgentToolResultBudget
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> BuildPreviewByToolResultId(IReadOnlyList<ChatMessage>? sourceMessages)
|
||||
{
|
||||
var previews = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
if (sourceMessages == null)
|
||||
return previews;
|
||||
|
||||
foreach (var message in sourceMessages)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message.QueryPreviewContent))
|
||||
continue;
|
||||
if (!AgentMessageInvariantHelper.TryGetToolResultId(message, out var toolResultId))
|
||||
continue;
|
||||
|
||||
previews[toolResultId] = message.QueryPreviewContent!;
|
||||
}
|
||||
|
||||
return previews;
|
||||
}
|
||||
|
||||
public static string TruncateToolResultJson(string json, int softCharLimit = DefaultSoftCharLimit)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -237,6 +237,7 @@ public sealed class ChatSessionStateService
|
||||
MetaRunId = m.MetaRunId,
|
||||
Feedback = m.Feedback,
|
||||
AttachedFiles = m.AttachedFiles?.ToList(),
|
||||
QueryPreviewContent = m.QueryPreviewContent,
|
||||
Images = m.Images?.Select(img => new ImageAttachment
|
||||
{
|
||||
FileName = img.FileName,
|
||||
@@ -680,6 +681,9 @@ public sealed class ChatSessionStateService
|
||||
conversation.AgentRunHistory ??= new List<ChatAgentRunRecord>();
|
||||
conversation.DraftQueueItems ??= new List<DraftQueueItem>();
|
||||
|
||||
if (AxCopilot.Services.Agent.AgentMessageInvariantHelper.PopulateMissingToolResultPreviews(conversation.Messages))
|
||||
changed = true;
|
||||
|
||||
var normalizedEvents = NormalizeExecutionEventsForResume(conversation.ExecutionEvents);
|
||||
if (!conversation.ExecutionEvents.SequenceEqual(normalizedEvents))
|
||||
{
|
||||
|
||||
@@ -34,6 +34,7 @@ public class ChatStorageService : IChatStorageService
|
||||
public void Save(ChatConversation conversation)
|
||||
{
|
||||
conversation.UpdatedAt = DateTime.Now;
|
||||
AxCopilot.Services.Agent.AgentMessageInvariantHelper.PopulateMissingToolResultPreviews(conversation.Messages);
|
||||
// 검색용 미리보기 자동 갱신 (첫 사용자 메시지 100자)
|
||||
if (string.IsNullOrEmpty(conversation.Preview) && conversation.Messages.Count > 0)
|
||||
{
|
||||
|
||||
@@ -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