???? ???? ?? ????????? ????? ?? ????? PPT ?? ???? ??

??:
- ?? ?? ???? ?? ?? ??, ?? ? ?? ?? ??, ?????? ???? ??? ? ?? ?????.
- ?? ?? ??? ??? ?? PPT? ?? ??? ?? ???? ?? ????? ????.

?? ????:
- AgentCommandQueue? steering, permission continuation, resume, user decision ? ??? ???? AgentLoopService?? ?? ???? ????? ??
- CodeLanguageCatalog? LspClientService? ??? Go, Rust, PHP, Ruby, Kotlin, Swift? ?? LSP ?? ???? ??
- SettingsWindow? SettingsViewModel?? ?? ? ?? ??? ?? ?? / LSP / ?? ???? ????? ??
- WorkspaceContextGenerator? Language Snapshot, Agent Context, Key Manifests ??? ???? .claude/skills, .ax/rules, AXMEMORY.md ??? ??
- DeckRepairGuideService? ???? PptxSkill ??? Deck repair guide? ?? ??
- ?? ?? ???? ?? ???? ?? ? ??

??:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_master_batch\\ -p:IntermediateOutputPath=obj\\verify_master_batch\\
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentCommandQueueTests,CodeLanguageCatalogTests,WorkspaceContextGeneratorTests,PptxSkillConsultingDeckTests,DeckRepairGuideServiceTests -p:OutputPath=bin\\verify_master_batch_tests\\ -p:IntermediateOutputPath=obj\\verify_master_batch_tests\\
This commit is contained in:
2026-04-15 00:21:15 +09:00
parent 59ec4a1371
commit f33ee7f7db
16 changed files with 422 additions and 14 deletions

View File

@@ -17,6 +17,10 @@ public enum AgentCommandKind
{
Prompt,
Notification,
Steering,
PermissionContinuation,
Resume,
UserDecision,
}
public sealed record AgentQueuedCommand(
@@ -45,6 +49,18 @@ public sealed class AgentCommandQueue
public void EnqueueNotification(string content, string priority = "later")
=> Enqueue(AgentCommandKind.Notification, content, priority, requestInterrupt: false);
public void EnqueueSteering(string content, string priority = "now", bool requestInterrupt = true)
=> Enqueue(AgentCommandKind.Steering, content, priority, requestInterrupt);
public void EnqueuePermissionContinuation(string content, string priority = "now")
=> Enqueue(AgentCommandKind.PermissionContinuation, content, priority, requestInterrupt: true);
public void EnqueueResume(string content, string priority = "now")
=> Enqueue(AgentCommandKind.Resume, content, priority, requestInterrupt: false);
public void EnqueueUserDecision(string content, string priority = "next", bool requestInterrupt = false)
=> Enqueue(AgentCommandKind.UserDecision, content, priority, requestInterrupt);
public void Clear()
{
while (_now.TryDequeue(out _)) { }

View File

@@ -49,20 +49,60 @@ public partial class AgentLoopService
requestInterrupt: IsRunning);
}
/// <summary>실행 중 사용자 지시 보강 메시지를 우선순위 높게 주입합니다.</summary>
public void InjectSteeringMessage(string message, bool requestInterrupt = true)
{
if (!string.IsNullOrWhiteSpace(message))
_pendingCommands.EnqueueSteering(
message,
IsRunning ? "now" : "next",
requestInterrupt: IsRunning && requestInterrupt);
}
/// <summary>권한 승인 후 이어서 진행할 내용을 큐에 넣습니다.</summary>
public void EnqueuePermissionContinuation(string toolName, string? target, string decisionSummary)
{
if (string.IsNullOrWhiteSpace(decisionSummary))
return;
var targetLabel = string.IsNullOrWhiteSpace(target) ? "" : $" ({target})";
_pendingCommands.EnqueuePermissionContinuation(
$"[permission continuation] Continue {toolName}{targetLabel}: {decisionSummary}");
}
/// <summary>도구 실행 중 참고용 시스템 알림을 큐에 넣습니다.</summary>
public void EnqueueNotification(string message, string priority = "later")
{
if (!string.IsNullOrWhiteSpace(message))
_pendingCommands.EnqueueNotification(message, priority);
}
/// <summary>사용자 의사결정 결과를 다음 턴 입력으로 주입합니다.</summary>
public void EnqueueUserDecision(string message, bool requestInterrupt = false)
{
if (!string.IsNullOrWhiteSpace(message))
_pendingCommands.EnqueueUserDecision(
message,
IsRunning ? "next" : "now",
requestInterrupt && IsRunning);
}
private void DrainPendingCommands(List<ChatMessage> messages)
{
var drained = _pendingCommands.DrainAll();
if (drained.Count == 0)
return;
var interruptingPrompts = drained.Count(x => x.Kind == AgentCommandKind.Prompt && x.RequestInterrupt);
if (interruptingPrompts > 0)
var interruptingCommands = drained.Count(x =>
x.RequestInterrupt &&
x.Kind is AgentCommandKind.Prompt or AgentCommandKind.Steering or AgentCommandKind.UserDecision or AgentCommandKind.PermissionContinuation);
if (interruptingCommands > 0)
{
messages.Add(new ChatMessage
{
Role = "system",
MetaKind = "queued_input_interrupt",
Content = $"[queued input] {interruptingPrompts} new prompt(s) arrived during execution. Prioritize the newest user direction before continuing.",
Content = $"[queued input] {interruptingCommands} new instruction(s) arrived during execution. Prioritize the newest user direction before continuing.",
Timestamp = DateTime.Now,
});
}
@@ -82,6 +122,40 @@ public partial class AgentLoopService
EmitEvent(AgentEventType.Thinking, "", item.Content);
break;
case AgentCommandKind.PermissionContinuation:
messages.Add(new ChatMessage
{
Role = "system",
MetaKind = "queue_permission_continuation",
Content = item.Content,
Timestamp = item.CreatedAt,
});
EmitEvent(AgentEventType.Thinking, "", item.Content);
break;
case AgentCommandKind.Resume:
messages.Add(new ChatMessage
{
Role = "system",
MetaKind = "queue_resume",
Content = item.Content,
Timestamp = item.CreatedAt,
});
EmitEvent(AgentEventType.Thinking, "", item.Content);
break;
case AgentCommandKind.Steering:
case AgentCommandKind.UserDecision:
messages.Add(new ChatMessage
{
Role = "user",
MetaKind = item.Kind == AgentCommandKind.Steering ? "queued_steering" : "queued_user_decision",
Content = item.Content,
Timestamp = item.CreatedAt,
});
EmitEvent(AgentEventType.UserMessage, "", item.Content);
break;
default:
messages.Add(new ChatMessage
{
@@ -209,6 +283,8 @@ public partial class AgentLoopService
{
// 이미 릴리즈된 상태 — 무시
}
if (IsRunning)
_pendingCommands.EnqueueResume("Execution resumed after pause. Re-evaluate the latest queued context before proceeding.");
EmitEvent(AgentEventType.Resumed, "", "에이전트가 재개되었습니다");
}

View File

@@ -0,0 +1,55 @@
namespace AxCopilot.Services.Agent;
public static class DeckRepairGuideService
{
public static string BuildGuide(DeckQualityReport review)
{
if (review.Issues.Count == 0)
return "Deck repair guide: none";
var actions = new List<string>();
foreach (var issue in review.Issues)
{
var action = issue.Message switch
{
var message when message.Contains("Executive Summary", StringComparison.OrdinalIgnoreCase)
=> "Add or strengthen the Executive Summary with 2-3 evidence-backed takeaways",
var message when message.Contains("Recommendation", StringComparison.OrdinalIgnoreCase)
=> "Add a clear recommendation or decision request slide near the end of the deck",
var message when message.Contains("roadmap", StringComparison.OrdinalIgnoreCase)
=> "Add a roadmap slide with phases, owners, and timing",
var message when message.Contains("headline is too long", StringComparison.OrdinalIgnoreCase)
=> "Tighten slide headlines to one clear message sentence",
var message when message.Contains("content density is high", StringComparison.OrdinalIgnoreCase)
=> "Reduce text density and convert bullets into cards, visuals, or sharper evidence points",
var message when message.Contains("comparison slide", StringComparison.OrdinalIgnoreCase)
=> "Expand the comparison slide to show at least two real options and a verdict",
var message when message.Contains("chart slide", StringComparison.OrdinalIgnoreCase)
=> "Provide chart labels and values or replace the slide with a comparison/evidence layout",
var message when message.Contains("table slide", StringComparison.OrdinalIgnoreCase)
=> "Provide complete table headers and rows or simplify the slide to key callouts",
var message when message.Contains("text-heavy", StringComparison.OrdinalIgnoreCase)
=> "Convert text-heavy slides into message-led visuals with fewer bullets",
var message when message.Contains("Evidence slides", StringComparison.OrdinalIgnoreCase)
=> "Add evidence slides such as charts, tables, appendix evidence, or structured comparisons",
var message when message.Contains("placeholder", StringComparison.OrdinalIgnoreCase)
=> "Replace placeholder text before export and verify each slide has final copy",
_ => null,
};
if (!string.IsNullOrWhiteSpace(action) &&
!actions.Contains(action, StringComparer.OrdinalIgnoreCase))
{
actions.Add(action);
}
if (actions.Count >= 3)
break;
}
if (actions.Count == 0)
return "Deck repair guide: review slide alerts and reinforce storyline, evidence, and decision ask";
return "Deck repair guide: " + string.Join(" | ", actions);
}
}

View File

@@ -1029,7 +1029,10 @@ public class PptxSkill : IAgentTool
if (!string.IsNullOrWhiteSpace(templatePackName))
outputParts.Add($"Template pack: {templatePackName}");
if (deckReview != null)
{
outputParts.Add(deckReview.ToToolSummary());
outputParts.Add(DeckRepairGuideService.BuildGuide(deckReview));
}
return ToolResult.Ok(string.Join("\n", outputParts), fullPath);
}
catch (Exception ex)

View File

@@ -84,6 +84,15 @@ internal static class WorkspaceContextGenerator
sb.AppendLine();
}
var languageSnapshot = BuildLanguageSnapshot(extDist);
if (languageSnapshot.Count > 0)
{
sb.AppendLine("## Language Snapshot");
foreach (var line in languageSnapshot)
sb.AppendLine($"- {line}");
sb.AppendLine();
}
// 4. 기존 컨텍스트 파일 감지
var contextFiles = DetectContextFiles(workFolder);
if (contextFiles.Count > 0)
@@ -95,6 +104,24 @@ internal static class WorkspaceContextGenerator
}
// 5. README 요약
var agentContextSummary = DetectAgentContextSummary(workFolder);
if (agentContextSummary.Count > 0)
{
sb.AppendLine("## Agent Context");
foreach (var line in agentContextSummary)
sb.AppendLine($"- {line}");
sb.AppendLine();
}
var keyManifests = DetectKeyManifests(workFolder);
if (keyManifests.Count > 0)
{
sb.AppendLine("## Key Manifests");
foreach (var line in keyManifests)
sb.AppendLine($"- {line}");
sb.AppendLine();
}
var readmeSummary = ExtractReadmeSummary(workFolder);
if (readmeSummary != null)
{
@@ -328,6 +355,91 @@ internal static class WorkspaceContextGenerator
return files;
}
private static List<string> DetectAgentContextSummary(string folder)
{
var lines = new List<string>();
try
{
var claudeSkillsDir = Path.Combine(folder, ".claude", "skills");
if (Directory.Exists(claudeSkillsDir))
{
var skillFiles = Directory.GetFiles(claudeSkillsDir, "SKILL.md", SearchOption.AllDirectories);
if (skillFiles.Length > 0)
lines.Add($".claude/skills 호환 스킬 {skillFiles.Length}개 감지");
}
}
catch { }
try
{
var axRulesDir = Path.Combine(folder, ".ax", "rules");
if (Directory.Exists(axRulesDir))
{
var ruleFiles = Directory.GetFiles(axRulesDir, "*.md", SearchOption.TopDirectoryOnly);
if (ruleFiles.Length > 0)
lines.Add($".ax/rules 규칙 {ruleFiles.Length}개 감지");
}
}
catch { }
try
{
var memoryFile = Path.Combine(folder, "AXMEMORY.md");
if (File.Exists(memoryFile))
lines.Add("AXMEMORY.md 메모리 파일 감지");
}
catch { }
return lines;
}
private static List<string> DetectKeyManifests(string folder)
{
var manifests = new List<string>();
var patterns = new (string Pattern, string Label)[]
{
("*.sln", "Solution"),
("*.csproj", ".NET project"),
("package.json", "Node package"),
("pyproject.toml", "Python project"),
("requirements.txt", "Python requirements"),
("Cargo.toml", "Rust package"),
("go.mod", "Go module"),
("pom.xml", "Maven project"),
("build.gradle", "Gradle build"),
};
foreach (var (pattern, label) in patterns)
{
try
{
var matches = Directory.GetFiles(folder, pattern, SearchOption.TopDirectoryOnly)
.Select(Path.GetFileName)
.Where(name => !string.IsNullOrWhiteSpace(name))
.Cast<string>()
.Take(3)
.ToList();
if (matches.Count > 0)
manifests.Add($"{label}: {string.Join(", ", matches)}");
}
catch { }
}
return manifests;
}
private static List<string> BuildLanguageSnapshot(List<KeyValuePair<string, int>> extDist)
=> extDist
.Where(kv => kv.Value > 0)
.Select(kv => new { Language = GetLanguageName(kv.Key), kv.Value })
.GroupBy(x => x.Language, StringComparer.OrdinalIgnoreCase)
.Select(group => new { Language = group.Key, Count = group.Sum(item => item.Value) })
.OrderByDescending(x => x.Count)
.Take(6)
.Select(x => $"{x.Language}: {x.Count} file(s)")
.ToList();
private static async Task<(string? Branch, string? Remote)> GetGitInfoAsync(
string folder, CancellationToken ct)
{

View File

@@ -119,7 +119,8 @@ public static class CodeLanguageCatalog
[
"Preserve package boundaries, error-first flow, and gofmt-style formatting.",
"Check interfaces, exported identifiers, and concurrency-sensitive changes together."
]),
],
LspLanguageId: "go"),
new(
"rust",
"Rust",
@@ -127,7 +128,8 @@ public static class CodeLanguageCatalog
[
"Respect Cargo workspace structure, ownership/borrowing rules, and crate boundaries.",
"Prefer explicit enums/results and verify compiler diagnostics after edits."
]),
],
LspLanguageId: "rust"),
new(
"php",
"PHP",
@@ -135,7 +137,8 @@ public static class CodeLanguageCatalog
[
"Follow the framework and autoloading structure already present in the project.",
"Be careful with runtime includes, container wiring, and mixed template/application files."
]),
],
LspLanguageId: "php"),
new(
"ruby",
"Ruby",
@@ -143,7 +146,8 @@ public static class CodeLanguageCatalog
[
"Preserve gem structure, Rails or plain Ruby conventions already used in the repository.",
"Check dynamic dispatch, concerns/modules, and tests together after edits."
]),
],
LspLanguageId: "ruby"),
new(
"kotlin",
"Kotlin",
@@ -151,7 +155,8 @@ public static class CodeLanguageCatalog
[
"Preserve Gradle structure, package layout, and nullability intent.",
"Be careful with JVM interop boundaries and Android-specific module structure when present."
]),
],
LspLanguageId: "kotlin"),
new(
"swift",
"Swift",
@@ -159,7 +164,8 @@ public static class CodeLanguageCatalog
[
"Preserve target structure, Apple framework imports, and protocol-oriented design already in use.",
"Check app lifecycle and platform-specific behavior after edits."
]),
],
LspLanguageId: "swift"),
new(
"scala",
"Scala",
@@ -255,6 +261,9 @@ public static class CodeLanguageCatalog
public static string GetQuickSelectLabel(string? key)
=> FindByKey(key)?.DisplayName ?? key ?? "Auto";
public static string BuildQuickSelectSupportDescription()
=> string.Join(", ", QuickSelectLanguages.Select(x => x.DisplayName));
public static string BuildSelectedLanguagePrompt(string? key)
{
if (string.IsNullOrWhiteSpace(key) || string.Equals(key, "auto", StringComparison.OrdinalIgnoreCase))
@@ -292,6 +301,9 @@ public static class CodeLanguageCatalog
public static string BuildStaticSupportDescription()
=> string.Join(", ", s_all.Select(x => x.DisplayName));
public static string BuildSupportSummaryDescription()
=> $"빠른 선택: {BuildQuickSelectSupportDescription()} | 내장 분석: {BuildStaticSupportDescription()} | LSP 심화 분석: {BuildLspSupportDescription()}";
public static string BuildCodeTabSupportDescription()
{
var sb = new StringBuilder();

View File

@@ -9,7 +9,8 @@ namespace AxCopilot.Services;
/// <summary>
/// Language Server Protocol 클라이언트.
/// 외부 언어 서버 프로세스와 JSON-RPC 2.0으로 통신합니다.
/// 지원: OmniSharp (C#), typescript-language-server, pyright, clangd
/// 지원: OmniSharp (C#), typescript-language-server, pyright/pylsp, clangd, jdtls,
/// gopls, rust-analyzer, intelephense, solargraph, kotlin-language-server, sourcekit-lsp
/// </summary>
public class LspClientService : IDisposable
{
@@ -637,6 +638,18 @@ public class LspClientService : IDisposable
FindCommand("C/C++", new[] { "clangd" }, Array.Empty<string>()),
"java" =>
FindCommand("Java", new[] { "jdtls" }, Array.Empty<string>()),
"go" =>
FindCommand("Go", new[] { "gopls" }, Array.Empty<string>()),
"rust" =>
FindCommand("Rust", new[] { "rust-analyzer" }, Array.Empty<string>()),
"php" =>
FindCommand("PHP", new[] { "intelephense" }, new[] { "--stdio" }),
"ruby" =>
FindCommand("Ruby", new[] { "solargraph" }, new[] { "stdio" }),
"kotlin" =>
FindCommand("Kotlin", new[] { "kotlin-language-server" }, Array.Empty<string>()),
"swift" =>
FindCommand("Swift", new[] { "sourcekit-lsp" }, Array.Empty<string>()),
_ => (null, Array.Empty<string>())
};

View File

@@ -138,9 +138,10 @@ public class SettingsViewModel : INotifyPropertyChanged
/// <summary>CodeSettings 바인딩용 프로퍼티. XAML에서 {Binding Code.EnableLsp} 등으로 접근.</summary>
public Models.CodeSettings Code => _service.Settings.Llm.Code;
public string CodeQuickSelectLanguagesText => CodeLanguageCatalog.BuildQuickSelectSupportDescription();
public string CodeLspSupportedLanguagesText => CodeLanguageCatalog.BuildLspSupportDescription();
public string CodeStaticSupportedLanguagesText => CodeLanguageCatalog.BuildStaticSupportDescription();
public string CodeTabSupportSummaryText => CodeLanguageCatalog.BuildCodeTabSupportDescription();
public string CodeTabSupportSummaryText => CodeLanguageCatalog.BuildSupportSummaryDescription();
// ─── 작업 복사본 ───────────────────────────────────────────────────────
private string _hotkey;

View File

@@ -5448,7 +5448,7 @@
<Grid>
<StackPanel HorizontalAlignment="Left" Margin="0,0,60,0">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource RowLabel}" Text="LSP 코드 분석"/>
<TextBlock Style="{StaticResource RowLabel}" Text="LSP 코드 분석"/>
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Border.ToolTip>
@@ -5466,6 +5466,7 @@
</Border>
</StackPanel>
<TextBlock Style="{StaticResource RowHint}" Text="언어 서버로 정의 이동, 참조 검색 등 코드 분석을 지원합니다"/>
<TextBlock Style="{StaticResource RowHint}" Margin="0,4,0,0" Text="{Binding CodeQuickSelectLanguagesText, StringFormat=빠른 선택 언어: {0}}"/>
<TextBlock Style="{StaticResource RowHint}" Margin="0,4,0,0" Text="{Binding CodeLspSupportedLanguagesText, StringFormat=지원 언어(LSP): {0}}"/>
<TextBlock Style="{StaticResource RowHint}" Margin="0,2,0,0" Text="{Binding CodeStaticSupportedLanguagesText, StringFormat=코드 탭 기본 지원: {0}}"/>
<TextBlock Style="{StaticResource RowHint}" Margin="0,2,0,0" Text="격리 환경에서는 내장 분석과 로컬에 이미 설치된 언어 서버만 활용합니다"/>