diff --git a/README.md b/README.md
index b96920f..9a3c43c 100644
--- a/README.md
+++ b/README.md
@@ -2269,3 +2269,13 @@ MIT License
업데이트: 2026-04-15 21:23 (KST)
- AX Agent 라이브 진행 문구와 시간·토큰 표시가 서로 분리돼 보이던 입력 영역 레이아웃을 다시 다듬었습니다. src/AxCopilot/Views/ChatWindow.xaml에서 StreamMetricsLabel을 독립 줄로 두지 않고 라이브 진행 Grid의 우측 하단에 다시 배치해, 진행 문구와 입력창이 한 덩어리처럼 붙어 보이면서도 시간·토큰 표시는 입력창 바로 위 우측에 유지되도록 조정했습니다.
- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_stream_metrics_compact\\ -p:IntermediateOutputPath=obj\\verify_stream_metrics_compact\\ 경고 0 / 오류 0
+
+업데이트: 2026-04-15 21:48 (KST)
+- AX Agent 채팅창의 Windows 제목을 대화명과 분리해 `AX Agent`로 고정했습니다. `src/AxCopilot/Views/ChatWindow.xaml`의 창 제목이 더 이상 현재 대화 제목과 섞여 `AX Copilot — ...` 형태로 보이지 않습니다.
+- PPT 템플릿 해상도 경로를 `Assets/ppt` manifest 기반으로 다시 정리했습니다. `src/AxCopilot/Assets/ppt/templates.manifest.json`, `src/AxCopilot/Services/Agent/PptxTemplateManifestCatalog.cs`, `src/AxCopilot/Services/Agent/PptxSkill.cs`가 이제 템플릿 키/별칭/기본 테마를 manifest에서 읽고, 실행 폴더·소스 루트·%APPDATA% 템플릿 폴더를 상향 탐색해 실제 `.pptx`를 찾습니다. 기존 개발 출력 경로에서 `src/AxCopilot/Assets/ppt`를 제대로 못 찾아 `color fallback`으로 빠지던 원인을 줄였습니다.
+- PPT 생성 결과도 `asset missing`과 `color fallback`을 구분해 표시하도록 바꿨습니다. 템플릿 파일 자체를 못 찾은 경우에는 `Template diagnostics` 라인을 남겨 경로 문제와 마스터 복제 실패를 바로 구분할 수 있습니다.
+- Cowork의 `pptx_create`는 이제 성공 직후 바로 종료되지 않고, `src/AxCopilot/Services/Agent/PptQualityGatePolicy.cs`와 `src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs`에서 품질 점수/슬라이드 경고/Needs work를 읽어 기준 미만이면 최대 2회까지 재생성을 요구합니다. 저품질 덱이 한 번 생성됐다는 이유만으로 루프가 끝나던 흐름을 막았습니다.
+- `src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs`와 `src/AxCopilot/skills/pptx-creator.skill.md`도 PPT는 품질이 중요한 업무형 덱일수록 `document_plan -> pptx_create`를 기본 경로로 쓰고, 낮은 품질 요약이 나오면 바로 종료하지 말고 재생성하라고 맞췄습니다.
+- 검증:
+ - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_quality_gate\\ -p:IntermediateOutputPath=obj\\verify_ppt_quality_gate\\` 경고 0 / 오류 0
+ - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "PptxSkillTemplatePackTests|PptxSkillAutoRepairTests|PptxSkillGoldenDeckTests|PptQualityGatePolicyTests|PptxTemplateManifestCatalogTests" -p:OutputPath=bin\\verify_ppt_quality_gate_tests\\ -p:IntermediateOutputPath=obj\\verify_ppt_quality_gate_tests\\` 통과 12
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index 0de9d2b..84a5cfa 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -1588,3 +1588,17 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫
- AX Agent 입력 영역 상단 레이아웃을 다시 정리했습니다. src/AxCopilot/Views/ChatWindow.xaml에서 StreamMetricsLabel을 독립 줄로 분리했던 배치 때문에 라이브 진행 문구와 입력창 사이가 넓게 벌어져 보였고, 우측 시간·토큰 표시만 아래로 내려간 상태였습니다.
- 같은 파일의 라이브 진행 Grid를 2열 구조로 바꾸고 StreamMetricsLabel을 우측 하단에 붙여, 진행 문구는 입력창과 시각적으로 붙은 상태를 유지하면서도 시간·토큰은 항상 입력창 바로 위 우측에 고정되도록 조정했습니다.
- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_stream_metrics_compact\\ -p:IntermediateOutputPath=obj\\verify_stream_metrics_compact\\ 경고 0 / 오류 0
+
+업데이트: 2026-04-15 21:48 (KST)
+- AX Agent 채팅창의 Windows 제목을 `AX Agent`로 고정했습니다. `src/AxCopilot/Views/ChatWindow.xaml`이 더 이상 대화 제목을 창 제목에 섞어 보여주지 않아, 작업 표시줄/Alt+Tab/미리보기에서 제품명만 안정적으로 보입니다.
+- PPT 템플릿 해상도 경로를 manifest 기반으로 재구성했습니다. `src/AxCopilot/Assets/ppt/templates.manifest.json`에 템플릿 키, 파일명, fallback theme, 태그/pack hint를 선언하고 `src/AxCopilot/Services/Agent/PptxTemplateManifestCatalog.cs`가 실행 폴더, 현재 작업 폴더, `%APPDATA%\\AXCopilot\\templates\\ppt`, 소스 루트의 `src/AxCopilot/Assets/ppt`를 상향 탐색해 실제 템플릿 파일을 찾도록 바꿨습니다. 이전 `ResolveTemplatePath(...)`는 `bin\\...\\Assets\\ppt`처럼 잘못된 상대 경로에 의존해 개발 빌드 출력에서 `basic100/core100`을 놓칠 수 있었습니다.
+- `src/AxCopilot/Services/Agent/PptxSkill.cs`는 manifest 해상도 결과를 받아 명시적 템플릿/pack 템플릿의 fallback theme를 더 정확히 고르고, 결과 요약에서 `color fallback`과 `asset missing -> built-in fallback`을 구분해 남깁니다. 템플릿 파일을 못 찾은 경우에는 `Template diagnostics` 라인에 후보 디렉터리 수를 함께 남겨 원인 추적이 쉬워졌습니다.
+- Cowork PPT 품질 게이트를 추가했습니다. `src/AxCopilot/Services/Agent/PptQualityGatePolicy.cs`가 `pptx_create` 출력에서 `PPT quality`, `Slide alerts`, `Needs work`, `color fallback`을 파싱하고, `src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs`는 Cowork에서 `pptx_create` 성공 직후 품질이 기준 미만이면 즉시 종료하지 않고 최대 2회까지 `document_plan`/`pptx_create` 재생성을 요구합니다. 그동안 `AgentLoopService`의 terminal document completion 때문에 저품질 PPT도 첫 성공 후 바로 끝나던 문제가 있었습니다.
+- Cowork 시스템 프롬프트와 PPT 스킬 지시도 품질 기준으로 정렬했습니다. `src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs`는 전략/제안/경영진용 PPT에서 `document_plan`을 먼저 쓰게 유도하고, `src/AxCopilot/skills/pptx-creator.skill.md`는 `document_plan + pptx_create`를 기본 경로로 명시하며 낮은 품질 요약이 나오면 바로 종료하지 말고 재생성하도록 안내합니다.
+- 테스트:
+ - `src/AxCopilot.Tests/Services/PptQualityGatePolicyTests.cs`
+ - `src/AxCopilot.Tests/Services/PptxTemplateManifestCatalogTests.cs`
+ - `src/AxCopilot.Tests/Services/PptxSkillTemplatePackTests.cs`
+- 검증:
+ - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_quality_gate\\ -p:IntermediateOutputPath=obj\\verify_ppt_quality_gate\\` 경고 0 / 오류 0
+ - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "PptxSkillTemplatePackTests|PptxSkillAutoRepairTests|PptxSkillGoldenDeckTests|PptQualityGatePolicyTests|PptxTemplateManifestCatalogTests" -p:OutputPath=bin\\verify_ppt_quality_gate_tests\\ -p:IntermediateOutputPath=obj\\verify_ppt_quality_gate_tests\\` 통과 12
diff --git a/src/AxCopilot.Tests/Services/PptQualityGatePolicyTests.cs b/src/AxCopilot.Tests/Services/PptQualityGatePolicyTests.cs
new file mode 100644
index 0000000..bc175bb
--- /dev/null
+++ b/src/AxCopilot.Tests/Services/PptQualityGatePolicyTests.cs
@@ -0,0 +1,56 @@
+using AxCopilot.Services.Agent;
+using FluentAssertions;
+using Xunit;
+
+namespace AxCopilot.Tests.Services;
+
+public class PptQualityGatePolicyTests
+{
+ [Fact]
+ public void TryAssess_ShouldRequestRetry_ForLowScoreDeck()
+ {
+ var result = ToolResult.Ok(
+ "PPTX created: E:\\docu\\deck.pptx (10 slides, template:basic100 (color fallback), 16:9)\n" +
+ "PPT quality 64/100 | Strengths: Includes title slide | Slide alerts: 4 | Needs work: Too many slides are text-heavy.");
+
+ var parsed = PptQualityGatePolicy.TryAssess(result, out var assessment);
+
+ parsed.Should().BeTrue();
+ assessment.Score.Should().Be(64);
+ assessment.SlideAlertCount.Should().Be(4);
+ assessment.HasColorFallback.Should().BeTrue();
+ assessment.RequiresRetry.Should().BeTrue();
+ }
+
+ [Fact]
+ public void TryAssess_ShouldPass_ForCleanDeck()
+ {
+ var result = ToolResult.Ok(
+ "PPTX created: E:\\docu\\deck.pptx (6 slides, template:basic100 (master cloned), 16:9)\n" +
+ "PPT quality 88/100 | Strengths: Includes title slide | Needs work: none");
+
+ var parsed = PptQualityGatePolicy.TryAssess(result, out var assessment);
+
+ parsed.Should().BeTrue();
+ assessment.Score.Should().Be(88);
+ assessment.RequiresRetry.Should().BeFalse();
+ }
+
+ [Fact]
+ public void BuildRetryPrompt_ShouldRecommendDocumentPlan_WhenStorylineWasNotPrepared()
+ {
+ var assessment = new PptQualityGateAssessment(
+ 64,
+ 3,
+ HasColorFallback: false,
+ "Too many slides are text-heavy.",
+ RequiresRetry: true,
+ Summary: "점수 64/100 (기준 80), 슬라이드 경고 3건");
+
+ var prompt = PptQualityGatePolicy.BuildRetryPrompt(assessment, "E:\\docu\\deck.pptx", documentPlanWasCalled: false);
+
+ prompt.Should().Contain("document_plan");
+ prompt.Should().Contain("pptx_create");
+ prompt.Should().Contain("E:\\docu\\deck.pptx");
+ }
+}
diff --git a/src/AxCopilot.Tests/Services/PptxSkillTemplatePackTests.cs b/src/AxCopilot.Tests/Services/PptxSkillTemplatePackTests.cs
index a719d62..0ef5c22 100644
--- a/src/AxCopilot.Tests/Services/PptxSkillTemplatePackTests.cs
+++ b/src/AxCopilot.Tests/Services/PptxSkillTemplatePackTests.cs
@@ -49,6 +49,7 @@ public class PptxSkillTemplatePackTests
result.Success.Should().BeTrue();
result.Output.Should().Contain("Template pack: board");
+ result.Output.Should().NotContain("asset missing");
File.Exists(Path.Combine(workDir, "board-pack-deck.pptx")).Should().BeTrue();
}
finally
diff --git a/src/AxCopilot.Tests/Services/PptxTemplateManifestCatalogTests.cs b/src/AxCopilot.Tests/Services/PptxTemplateManifestCatalogTests.cs
new file mode 100644
index 0000000..db35885
--- /dev/null
+++ b/src/AxCopilot.Tests/Services/PptxTemplateManifestCatalogTests.cs
@@ -0,0 +1,43 @@
+using System.IO;
+using AxCopilot.Services.Agent;
+using FluentAssertions;
+using Xunit;
+
+namespace AxCopilot.Tests.Services;
+
+public class PptxTemplateManifestCatalogTests
+{
+ [Fact]
+ public void Resolve_ShouldReturnManifestEntry_ForKnownTemplate()
+ {
+ var entry = PptxTemplateManifestCatalog.Resolve("basic100");
+
+ entry.Should().NotBeNull();
+ entry!.FileName.Should().Be("BASIC100 기준 템플릿 V1.pptx");
+ entry.FallbackTheme.Should().Be("professional");
+ }
+
+ [Fact]
+ public void ResolveTemplate_ShouldFindBundledSourceAsset_FromTestRuntime()
+ {
+ var resolution = PptxTemplateManifestCatalog.ResolveTemplate("basic100");
+
+ resolution.Entry.Should().NotBeNull();
+ resolution.IsResolved.Should().BeTrue();
+ resolution.Status.Should().Be("resolved");
+ File.Exists(resolution.ResolvedPath).Should().BeTrue();
+ Path.GetFileName(resolution.ResolvedPath!).Should().Be(resolution.Entry!.FileName);
+ }
+
+ [Fact]
+ public void EnumerateCandidateDirectories_ShouldIncludeProjectSourceAssets()
+ {
+ var directories = PptxTemplateManifestCatalog.EnumerateCandidateDirectoriesForTesting(
+ AppContext.BaseDirectory,
+ Environment.CurrentDirectory,
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));
+
+ directories.Should().Contain(path =>
+ path.EndsWith(Path.Combine("src", "AxCopilot", "Assets", "ppt"), StringComparison.OrdinalIgnoreCase));
+ }
+}
diff --git a/src/AxCopilot/Assets/ppt/templates.manifest.json b/src/AxCopilot/Assets/ppt/templates.manifest.json
new file mode 100644
index 0000000..2b2321a
--- /dev/null
+++ b/src/AxCopilot/Assets/ppt/templates.manifest.json
@@ -0,0 +1,76 @@
+{
+ "templates": [
+ {
+ "key": "basic100",
+ "fileName": "BASIC100 기준 템플릿 V1.pptx",
+ "displayName": "BASIC100 기준 템플릿 V1",
+ "fallbackTheme": "professional",
+ "aliases": ["basic", "consulting_basic"],
+ "tags": ["executive", "strategy", "consulting", "brand"],
+ "packHints": ["strategy", "pmo", "sales", "operating_model"]
+ },
+ {
+ "key": "core100",
+ "fileName": "CORE100 기준템플릿 V1.pptx",
+ "displayName": "CORE100 기준 템플릿 V1",
+ "fallbackTheme": "corporate",
+ "aliases": ["core", "board_core"],
+ "tags": ["board", "governance", "finance", "corporate"],
+ "packHints": ["board", "finance"]
+ },
+ {
+ "key": "frame_blue",
+ "fileName": "P01_01_프레임디자인_PPT템플릿_블루(PPT_편집용).pptx",
+ "displayName": "프레임디자인 블루",
+ "fallbackTheme": "modern",
+ "aliases": ["blue_frame"],
+ "tags": ["frame", "blue", "report", "proposal"],
+ "packHints": ["sales", "operating_model"]
+ },
+ {
+ "key": "mr_ppt_01",
+ "fileName": "미스터 피피티 템플릿 01_원본.pptx",
+ "displayName": "미스터 피피티 템플릿 01",
+ "fallbackTheme": "modern",
+ "aliases": ["mrppt01"],
+ "tags": ["creative", "proposal", "sales"],
+ "packHints": ["sales"]
+ },
+ {
+ "key": "mr_ppt_02",
+ "fileName": "미스터 피피티 템플릿 02_원본.pptx",
+ "displayName": "미스터 피피티 템플릿 02",
+ "fallbackTheme": "vibrant",
+ "aliases": ["mrppt02"],
+ "tags": ["creative", "marketing", "proposal"],
+ "packHints": ["sales"]
+ },
+ {
+ "key": "mr_ppt_03",
+ "fileName": "미스터 피피티 03_원본.pptx",
+ "displayName": "미스터 피피티 템플릿 03",
+ "fallbackTheme": "minimal",
+ "aliases": ["mrppt03"],
+ "tags": ["minimal", "analysis", "report"],
+ "packHints": ["strategy", "finance"]
+ },
+ {
+ "key": "mr_ppt_04",
+ "fileName": "미스터 피피티 템플릿 04_원본.pptx",
+ "displayName": "미스터 피피티 템플릿 04",
+ "fallbackTheme": "slate",
+ "aliases": ["mrppt04"],
+ "tags": ["dark", "executive", "review"],
+ "packHints": ["board", "finance"]
+ },
+ {
+ "key": "mr_ppt_05",
+ "fileName": "미스터 피피티 템플릿_05_원본.pptx",
+ "displayName": "미스터 피피티 템플릿 05",
+ "fallbackTheme": "ocean",
+ "aliases": ["mrppt05"],
+ "tags": ["blue", "modern", "report"],
+ "packHints": ["pmo", "operating_model"]
+ }
+ ]
+}
diff --git a/src/AxCopilot/AxCopilot.csproj b/src/AxCopilot/AxCopilot.csproj
index aae989a..965c43f 100644
--- a/src/AxCopilot/AxCopilot.csproj
+++ b/src/AxCopilot/AxCopilot.csproj
@@ -121,6 +121,9 @@
+
+ PreserveNewest
+
diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.cs b/src/AxCopilot/Services/Agent/AgentLoopService.cs
index eae8064..80ae5b8 100644
--- a/src/AxCopilot/Services/Agent/AgentLoopService.cs
+++ b/src/AxCopilot/Services/Agent/AgentLoopService.cs
@@ -1756,7 +1756,8 @@ public partial class AgentLoopService
executionPolicy,
context,
ct,
- documentPlanCalled);
+ documentPlanCalled,
+ runState);
if (terminalCompleted)
{
if (consumedExtraIteration)
diff --git a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs
index 7662bdd..a89a431 100644
--- a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs
+++ b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Documents.cs
@@ -94,11 +94,15 @@ public partial class AgentLoopService
ModelExecutionProfileCatalog.ExecutionPolicy executionPolicy,
AgentContext context,
CancellationToken ct,
- bool documentPlanWasCalled = false)
+ bool documentPlanWasCalled = false,
+ RunState? runState = null)
{
if (!result.Success || !IsTerminalDocumentTool(call.ToolName) || toolCalls.Count != 1)
return (false, false);
+ if (TryApplyPptQualityGateTransition(call, result, messages, documentPlanWasCalled, runState))
+ return (false, false);
+
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
{
EmitEvent(AgentEventType.Complete, "", "에이전트 작업 완료");
@@ -124,6 +128,53 @@ public partial class AgentLoopService
return (true, consumedExtraIteration);
}
+ private bool TryApplyPptQualityGateTransition(
+ ContentBlock call,
+ ToolResult result,
+ List messages,
+ bool documentPlanWasCalled,
+ RunState? runState)
+ {
+ if (!string.Equals(ActiveTab, "Cowork", StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ if (!string.Equals(call.ToolName, "pptx_create", StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ if (!PptQualityGatePolicy.TryAssess(result, out var assessment))
+ return false;
+
+ if (!assessment.RequiresRetry)
+ return false;
+
+ if (runState == null)
+ return false;
+
+ if (runState.PptQualityGateRetry >= PptQualityGatePolicy.DefaultMaxRetries)
+ {
+ EmitEvent(
+ AgentEventType.Thinking,
+ "",
+ $"PPT 품질 재생성 한도에 도달해 현재 결과로 종료합니다 ({assessment.Summary})");
+ return false;
+ }
+
+ runState.PptQualityGateRetry++;
+ messages.Add(new ChatMessage
+ {
+ Role = "user",
+ Content = PptQualityGatePolicy.BuildRetryPrompt(
+ assessment,
+ result.FilePath,
+ documentPlanWasCalled)
+ });
+ EmitEvent(
+ AgentEventType.Thinking,
+ "",
+ $"PPT 품질이 기준 미만이라 재생성을 요청합니다 ({runState.PptQualityGateRetry}/{PptQualityGatePolicy.DefaultMaxRetries})");
+ return true;
+ }
+
private async Task TryApplyPostToolVerificationTransitionAsync(
ContentBlock call,
ToolResult result,
diff --git a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs
index 0a8df55..e39a6a1 100644
--- a/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs
+++ b/src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs
@@ -1242,6 +1242,7 @@ public partial class AgentLoopService
public int TransientLlmErrorRetries;
public int DocumentArtifactGateRetry;
public int DocumentVerificationGateRetry;
+ public int PptQualityGateRetry;
public int NoProgressRecoveryRetry;
public int TerminalEvidenceGateRetry;
public bool WorkspaceAppearsEmpty;
diff --git a/src/AxCopilot/Services/Agent/PptQualityGatePolicy.cs b/src/AxCopilot/Services/Agent/PptQualityGatePolicy.cs
new file mode 100644
index 0000000..a92db90
--- /dev/null
+++ b/src/AxCopilot/Services/Agent/PptQualityGatePolicy.cs
@@ -0,0 +1,92 @@
+using System.Text.RegularExpressions;
+
+namespace AxCopilot.Services.Agent;
+
+public sealed record PptQualityGateAssessment(
+ int Score,
+ int SlideAlertCount,
+ bool HasColorFallback,
+ string NeedsWork,
+ bool RequiresRetry,
+ string Summary);
+
+public static class PptQualityGatePolicy
+{
+ public const int DefaultMinScore = 80;
+ public const int DefaultMaxRetries = 2;
+
+ public static bool TryAssess(ToolResult result, out PptQualityGateAssessment assessment, int minScore = DefaultMinScore)
+ {
+ assessment = new PptQualityGateAssessment(0, 0, false, "", false, "");
+ if (!result.Success || string.IsNullOrWhiteSpace(result.Output))
+ return false;
+
+ var output = result.Output;
+ var scoreMatch = Regex.Match(output, @"PPT quality\s+(?\d{1,3})/100", RegexOptions.IgnoreCase);
+ if (!scoreMatch.Success || !int.TryParse(scoreMatch.Groups["score"].Value, out var score))
+ return false;
+
+ var alertMatch = Regex.Match(output, @"Slide alerts:\s*(?\d+)", RegexOptions.IgnoreCase);
+ var needsWorkMatch = Regex.Match(output, @"Needs work:\s*(?.+?)(?:\||\r?\n|$)", RegexOptions.IgnoreCase);
+ var slideAlerts = alertMatch.Success && int.TryParse(alertMatch.Groups["count"].Value, out var parsedAlerts)
+ ? parsedAlerts
+ : 0;
+ var needsWork = needsWorkMatch.Success ? needsWorkMatch.Groups["value"].Value.Trim() : "";
+ var hasColorFallback = output.Contains("color fallback", StringComparison.OrdinalIgnoreCase);
+ var requiresRetry = score < minScore
+ || slideAlerts > 0
+ || (!string.IsNullOrWhiteSpace(needsWork)
+ && !string.Equals(needsWork, "none", StringComparison.OrdinalIgnoreCase));
+ var summary = BuildSummary(score, slideAlerts, hasColorFallback, needsWork, minScore);
+
+ assessment = new PptQualityGateAssessment(
+ score,
+ slideAlerts,
+ hasColorFallback,
+ needsWork,
+ requiresRetry,
+ summary);
+ return true;
+ }
+
+ public static string BuildRetryPrompt(
+ PptQualityGateAssessment assessment,
+ string? filePath,
+ bool documentPlanWasCalled)
+ {
+ var target = string.IsNullOrWhiteSpace(filePath) ? "방금 생성한 PPT 파일" : $"'{filePath}'";
+ var planStep = documentPlanWasCalled
+ ? "기존 스토리라인을 더 날카롭게 다듬고 같은 파일 경로로 다시 생성하세요."
+ : "먼저 document_plan으로 스토리라인을 재정리한 뒤, 같은 파일 경로로 pptx_create를 다시 호출하세요.";
+ var templateStep = assessment.HasColorFallback
+ ? "템플릿 마스터 복제가 끝까지 적용되지 않았으니 template/template_pack 선택을 다시 점검하고 브랜드 템플릿을 우선 사용하세요."
+ : "template_pack(strategy/board/pmo/finance/sales/operating_model) 또는 적절한 브랜드 템플릿을 적극 사용하세요.";
+ var needsWorkLine = string.IsNullOrWhiteSpace(assessment.NeedsWork)
+ ? "- 주요 보완: 슬라이드당 메시지를 하나로 줄이고, 근거 슬라이드와 결론 슬라이드의 대비를 분명히 하세요.\n"
+ : $"- 주요 보완: {assessment.NeedsWork}\n";
+
+ return "[System:PptQualityGate] 방금 생성한 PPT 품질이 기준 미만입니다.\n" +
+ $"- 대상 파일: {target}\n" +
+ $"- 품질 요약: {assessment.Summary}\n" +
+ needsWorkLine +
+ "- 필수 개선 방향:\n" +
+ " 1. 한 슬라이드 한 메시지 원칙으로 텍스트 밀도를 낮추세요.\n" +
+ " 2. Executive Summary → Options/Comparison → Recommendation → Roadmap → Appendix/Evidence 흐름을 분명히 하세요.\n" +
+ " 3. 표/차트/비교 슬라이드로 정량 근거를 늘리고, 헤드라인을 더 결론형 문장으로 바꾸세요.\n" +
+ $" 4. {templateStep}\n" +
+ $" 5. {planStep}\n" +
+ "개선된 버전을 만든 뒤 다시 품질 요약이 좋아졌는지 확인하세요.";
+ }
+
+ private static string BuildSummary(int score, int slideAlerts, bool hasColorFallback, string needsWork, int minScore)
+ {
+ var parts = new List { $"점수 {score}/100 (기준 {minScore})" };
+ if (slideAlerts > 0)
+ parts.Add($"슬라이드 경고 {slideAlerts}건");
+ if (hasColorFallback)
+ parts.Add("template color fallback 발생");
+ if (!string.IsNullOrWhiteSpace(needsWork) && !string.Equals(needsWork, "none", StringComparison.OrdinalIgnoreCase))
+ parts.Add($"Needs work: {needsWork}");
+ return string.Join(", ", parts);
+ }
+}
diff --git a/src/AxCopilot/Services/Agent/PptxSkill.cs b/src/AxCopilot/Services/Agent/PptxSkill.cs
index ac7746f..178f881 100644
--- a/src/AxCopilot/Services/Agent/PptxSkill.cs
+++ b/src/AxCopilot/Services/Agent/PptxSkill.cs
@@ -220,58 +220,21 @@ public class PptxSkill : IAgentTool
// ── 통합 테마 레코드 ──────────────────────────────────────────────────────
private record FullTheme(ThemeColors Colors, ThemeLayout Layout);
- // ── 고품질 템플릿 레지스트리 ────────────────────────────────────────────────
- // 이름 → Assets/ppt/ 아래 파일명 매핑. 실행 시 디스크에서 열어 마스터 복제.
- // 토큰 컨텍스트 영향 0 — 바이너리 파일은 LLM에 전달되지 않음.
- private static readonly Dictionary TemplateRegistry = new(StringComparer.OrdinalIgnoreCase)
+ private static FullTheme ResolveTemplateFallbackTheme(
+ string templateName,
+ PptxTemplateResolution? resolution,
+ FullTheme defaultTheme)
{
- ["basic100"] = "BASIC100 기준 템플릿 V1.pptx",
- ["core100"] = "CORE100 기준템플릿 V1.pptx",
- ["frame_blue"] = "P01_01_프레임디자인_PPT템플릿_블루(PPT_편집용).pptx",
- ["mr_ppt_01"] = "미스터 피피티 템플릿 01_원본.pptx",
- ["mr_ppt_02"] = "미스터 피피티 템플릿 02_원본.pptx",
- ["mr_ppt_03"] = "미스터 피피티 03_원본.pptx",
- ["mr_ppt_04"] = "미스터 피피티 템플릿 04_원본.pptx",
- ["mr_ppt_05"] = "미스터 피피티 템플릿_05_원본.pptx",
- };
+ if (FullThemes.TryGetValue(templateName, out var namedTheme))
+ return namedTheme;
- ///
- /// 템플릿 이름으로 .pptx 파일 전체 경로를 반환합니다.
- /// 설치파일 용량 문제로 템플릿은 빌드에 포함하지 않으며,
- /// 여러 알려진 경로를 순서대로 탐색합니다.
- ///
- private static string? ResolveTemplatePath(string templateName)
- {
- if (!TemplateRegistry.TryGetValue(templateName, out var fileName))
- return null;
-
- var candidates = new List();
- var exeDir = AppContext.BaseDirectory;
-
- // 1) 실행 파일 옆 Assets/ppt/ (수동 배치)
- candidates.Add(Path.Combine(exeDir, "Assets", "ppt", fileName));
-
- // 2) %APPDATA%/AXCopilot/templates/ppt/ (사용자 설치)
- var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
- if (!string.IsNullOrEmpty(appData))
- candidates.Add(Path.Combine(appData, "AXCopilot", "templates", "ppt", fileName));
-
- // 3) 소스 프로젝트 디렉토리 (개발 환경 — bin/Release/net8.0-windows... 기준)
- candidates.Add(Path.GetFullPath(Path.Combine(exeDir, "..", "..", "..", "Assets", "ppt", fileName)));
-
- // 4) 워킹 디렉토리/src/AxCopilot/Assets/ppt/ (개발 환경 루트에서 실행)
- candidates.Add(Path.Combine("src", "AxCopilot", "Assets", "ppt", fileName));
-
- // 5) 프로젝트 루트 추정 (E:\AX Copilot - Claude 등)
- candidates.Add(Path.GetFullPath(Path.Combine(exeDir, "..", "..", "..", "..", "..", "Assets", "ppt", fileName)));
-
- foreach (var path in candidates)
+ if (resolution?.Entry != null &&
+ FullThemes.TryGetValue(resolution.Entry.FallbackTheme, out var manifestTheme))
{
- if (File.Exists(path))
- return Path.GetFullPath(path);
+ return manifestTheme;
}
- return null;
+ return defaultTheme;
}
// ── 20가지 테마 사전 ──────────────────────────────────────────────────────
@@ -588,6 +551,8 @@ public class PptxSkill : IAgentTool
: null);
var templatePackName = templatePack?.Name;
var packTemplateName = templatePack?.PreferredTemplate;
+ PptxTemplateResolution? explicitTemplateResolution = null;
+ PptxTemplateResolution? packTemplateResolution = null;
if (renderDeck != null)
{
@@ -647,24 +612,24 @@ public class PptxSkill : IAgentTool
if (!string.IsNullOrWhiteSpace(templateName))
{
- // 고품질 템플릿: 마스터 복제 시도 → 실패 시 내장 메타데이터 폴백
- templatePptxPath = ResolveTemplatePath(templateName!);
+ explicitTemplateResolution = PptxTemplateManifestCatalog.ResolveTemplate(templateName!);
+ templatePptxPath = explicitTemplateResolution.ResolvedPath;
+ var templateFallbackTheme = ResolveTemplateFallbackTheme(
+ templateName!,
+ explicitTemplateResolution,
+ FullThemes["professional"]);
- // 내장 테마에 템플릿 이름이 등록되어 있으면 해당 색상/레이아웃 사용
- if (FullThemes.TryGetValue(templateName!, out var templateTheme))
- fullTheme = templateTheme;
- else if (templatePptxPath != null)
+ if (templatePptxPath != null)
{
- // 미등록 템플릿이지만 파일이 있으면 색상 추출
var extracted = ExtractThemeFromPptx(templatePptxPath);
- var baseLayout = FullThemes["professional"].Layout;
+ var baseLayout = templateFallbackTheme.Layout;
fullTheme = extracted != null
? new FullTheme(extracted, baseLayout)
- : FullThemes["professional"];
+ : templateFallbackTheme;
}
else
{
- fullTheme = FullThemes["professional"];
+ fullTheme = templateFallbackTheme;
}
}
else if (args.SafeTryGetProperty("theme_file", out var tfEl) && !string.IsNullOrEmpty(tfEl.SafeGetString()))
@@ -683,7 +648,10 @@ public class PptxSkill : IAgentTool
else if (templatePack != null)
{
if (!string.IsNullOrWhiteSpace(packTemplateName))
- templatePptxPath = ResolveTemplatePath(packTemplateName!);
+ {
+ packTemplateResolution = PptxTemplateManifestCatalog.ResolveTemplate(packTemplateName!);
+ templatePptxPath = packTemplateResolution.ResolvedPath;
+ }
if (!FullThemes.TryGetValue(templatePack.FallbackTheme, out var packTheme))
packTheme = FullThemes["professional"];
@@ -1047,16 +1015,32 @@ public class PptxSkill : IAgentTool
? $"theme_file:{cloneInfo_srcLabel} (master cloned{(cloneAll ? " + slides cloned" : "")})"
: theme
: !string.IsNullOrWhiteSpace(templateName)
- ? $"template:{templateName} (color fallback)"
+ ? templatePptxPath == null
+ ? $"template:{templateName} (asset missing -> built-in fallback)"
+ : $"template:{templateName} (color fallback)"
: templatePptxPath != null && !string.IsNullOrWhiteSpace(packTemplateName)
? $"template:{packTemplateName} (color fallback)"
- : templatePptxPath != null
- ? $"theme_file:{cloneInfo_srcLabel} (master cloned{(cloneAll ? " + slides cloned" : "")})"
- : theme;
+ : !string.IsNullOrWhiteSpace(packTemplateName)
+ ? $"template:{packTemplateName} (asset missing -> pack fallback)"
+ : templatePptxPath != null
+ ? $"theme_file:{cloneInfo_srcLabel} (color fallback)"
+ : theme;
var outputParts = new List
{
$"PPTX created: {fullPath} ({slideCount_final} slides, {themeLabel}, {(isWide ? "16:9" : "4:3")})"
};
+ if (explicitTemplateResolution != null && explicitTemplateResolution.Status == "asset_missing")
+ {
+ outputParts.Add(
+ $"Template diagnostics: '{explicitTemplateResolution.RequestedName}' asset not found. " +
+ $"Searched {explicitTemplateResolution.CandidateDirectories.Count} candidate PPT directories.");
+ }
+ else if (packTemplateResolution != null && packTemplateResolution.Status == "asset_missing")
+ {
+ outputParts.Add(
+ $"Template diagnostics: pack template '{packTemplateResolution.RequestedName}' asset not found. " +
+ $"Searched {packTemplateResolution.CandidateDirectories.Count} candidate PPT directories.");
+ }
if (renderDeck != null)
outputParts.Add(renderDeck.ToToolSummary());
if (!string.IsNullOrWhiteSpace(templatePackName))
diff --git a/src/AxCopilot/Services/Agent/PptxTemplateManifestCatalog.cs b/src/AxCopilot/Services/Agent/PptxTemplateManifestCatalog.cs
new file mode 100644
index 0000000..efef15a
--- /dev/null
+++ b/src/AxCopilot/Services/Agent/PptxTemplateManifestCatalog.cs
@@ -0,0 +1,262 @@
+using System.IO;
+using System.Text.Json;
+
+namespace AxCopilot.Services.Agent;
+
+public sealed record PptxTemplateManifestEntry(
+ string Key,
+ string FileName,
+ string DisplayName,
+ string FallbackTheme,
+ IReadOnlyList Aliases,
+ IReadOnlyList Tags,
+ IReadOnlyList PackHints);
+
+public sealed class PptxTemplateManifest
+{
+ public List Templates { get; init; } = new();
+}
+
+public sealed record PptxTemplateResolution(
+ string RequestedName,
+ PptxTemplateManifestEntry? Entry,
+ string? ResolvedPath,
+ string Status,
+ IReadOnlyList CandidateDirectories)
+{
+ public bool IsResolved => !string.IsNullOrWhiteSpace(ResolvedPath);
+}
+
+public static class PptxTemplateManifestCatalog
+{
+ private const string ManifestFileName = "templates.manifest.json";
+ private static readonly JsonSerializerOptions s_jsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true,
+ };
+
+ private static readonly object s_manifestLock = new();
+ private static string? s_cachedManifestPath;
+ private static DateTime s_cachedManifestWriteTimeUtc;
+ private static PptxTemplateManifest? s_cachedManifest;
+
+ public static IReadOnlyList GetAll()
+ => LoadManifest().Templates;
+
+ public static PptxTemplateManifestEntry? Resolve(string? requestedName)
+ {
+ if (string.IsNullOrWhiteSpace(requestedName))
+ return null;
+
+ var normalized = requestedName.Trim();
+ return LoadManifest().Templates.FirstOrDefault(entry =>
+ string.Equals(entry.Key, normalized, StringComparison.OrdinalIgnoreCase) ||
+ entry.Aliases.Any(alias => string.Equals(alias, normalized, StringComparison.OrdinalIgnoreCase)));
+ }
+
+ public static PptxTemplateResolution ResolveTemplate(string? requestedName)
+ {
+ if (string.IsNullOrWhiteSpace(requestedName))
+ return new PptxTemplateResolution("", null, null, "empty", Array.Empty());
+
+ var trimmed = requestedName.Trim();
+ if (LooksLikePath(trimmed))
+ {
+ var explicitPath = ResolveExplicitPath(trimmed);
+ return new PptxTemplateResolution(
+ trimmed,
+ null,
+ explicitPath,
+ explicitPath != null ? "explicit_path" : "explicit_path_missing",
+ explicitPath != null ? new[] { Path.GetDirectoryName(explicitPath)! } : Array.Empty());
+ }
+
+ var entry = Resolve(trimmed);
+ var candidateDirectories = EnumerateCandidateDirectories(
+ AppContext.BaseDirectory,
+ Environment.CurrentDirectory,
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));
+
+ if (entry == null)
+ return new PptxTemplateResolution(trimmed, null, null, "unknown_template", candidateDirectories);
+
+ foreach (var directory in candidateDirectories)
+ {
+ var path = Path.Combine(directory, entry.FileName);
+ if (!File.Exists(path))
+ continue;
+
+ return new PptxTemplateResolution(
+ trimmed,
+ entry,
+ Path.GetFullPath(path),
+ "resolved",
+ candidateDirectories);
+ }
+
+ return new PptxTemplateResolution(trimmed, entry, null, "asset_missing", candidateDirectories);
+ }
+
+ internal static IReadOnlyList EnumerateCandidateDirectoriesForTesting(
+ string baseDirectory,
+ string currentDirectory,
+ string? appDataPath)
+ => EnumerateCandidateDirectories(baseDirectory, currentDirectory, appDataPath);
+
+ private static PptxTemplateManifest LoadManifest()
+ {
+ var manifestPath = FindManifestPath();
+ if (string.IsNullOrWhiteSpace(manifestPath) || !File.Exists(manifestPath))
+ return CreateFallbackManifest();
+
+ var fullPath = Path.GetFullPath(manifestPath);
+ var writeTimeUtc = File.GetLastWriteTimeUtc(fullPath);
+
+ lock (s_manifestLock)
+ {
+ if (s_cachedManifest != null &&
+ string.Equals(s_cachedManifestPath, fullPath, StringComparison.OrdinalIgnoreCase) &&
+ s_cachedManifestWriteTimeUtc == writeTimeUtc)
+ {
+ return s_cachedManifest;
+ }
+
+ try
+ {
+ var json = File.ReadAllText(fullPath);
+ var manifest = JsonSerializer.Deserialize(json, s_jsonOptions);
+ if (manifest?.Templates.Count > 0)
+ {
+ s_cachedManifestPath = fullPath;
+ s_cachedManifestWriteTimeUtc = writeTimeUtc;
+ s_cachedManifest = manifest;
+ return manifest;
+ }
+ }
+ catch
+ {
+ // fall through to the bundled fallback manifest
+ }
+
+ s_cachedManifestPath = null;
+ s_cachedManifestWriteTimeUtc = DateTime.MinValue;
+ s_cachedManifest = CreateFallbackManifest();
+ return s_cachedManifest;
+ }
+ }
+
+ private static string? FindManifestPath()
+ {
+ var candidates = EnumerateCandidateDirectories(
+ AppContext.BaseDirectory,
+ Environment.CurrentDirectory,
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));
+
+ foreach (var directory in candidates)
+ {
+ var manifestPath = Path.Combine(directory, ManifestFileName);
+ if (File.Exists(manifestPath))
+ return Path.GetFullPath(manifestPath);
+ }
+
+ return null;
+ }
+
+ private static IReadOnlyList EnumerateCandidateDirectories(
+ string baseDirectory,
+ string currentDirectory,
+ string? appDataPath)
+ {
+ var directories = new List();
+ var seen = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ void Add(string? candidate)
+ {
+ if (string.IsNullOrWhiteSpace(candidate))
+ return;
+
+ string normalized;
+ try
+ {
+ normalized = Path.GetFullPath(candidate);
+ }
+ catch
+ {
+ return;
+ }
+
+ if (seen.Add(normalized))
+ directories.Add(normalized);
+ }
+
+ if (!string.IsNullOrWhiteSpace(appDataPath))
+ Add(Path.Combine(appDataPath, "AXCopilot", "templates", "ppt"));
+
+ Add(Path.Combine(baseDirectory, "Assets", "ppt"));
+ Add(Path.Combine(currentDirectory, "Assets", "ppt"));
+ Add(Path.Combine(currentDirectory, "src", "AxCopilot", "Assets", "ppt"));
+
+ foreach (var anchor in new[] { baseDirectory, currentDirectory })
+ {
+ var current = SafeDirectoryInfo(anchor);
+ for (var depth = 0; current != null && depth < 10; depth++, current = current.Parent)
+ {
+ Add(Path.Combine(current.FullName, "Assets", "ppt"));
+ Add(Path.Combine(current.FullName, "src", "AxCopilot", "Assets", "ppt"));
+ }
+ }
+
+ return directories;
+ }
+
+ private static DirectoryInfo? SafeDirectoryInfo(string? path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ return null;
+
+ try
+ {
+ return new DirectoryInfo(Path.GetFullPath(path));
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static bool LooksLikePath(string value)
+ => value.Contains('\\', StringComparison.Ordinal)
+ || value.Contains('/', StringComparison.Ordinal)
+ || Path.IsPathRooted(value);
+
+ private static string? ResolveExplicitPath(string path)
+ {
+ try
+ {
+ var fullPath = Path.GetFullPath(path);
+ return File.Exists(fullPath) ? fullPath : null;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static PptxTemplateManifest CreateFallbackManifest()
+ {
+ return new PptxTemplateManifest
+ {
+ Templates =
+ [
+ new("basic100", "BASIC100 기준 템플릿 V1.pptx", "BASIC100 기준 템플릿 V1", "professional", ["basic", "consulting_basic"], ["executive", "strategy", "consulting", "brand"], ["strategy", "pmo", "sales", "operating_model"]),
+ new("core100", "CORE100 기준템플릿 V1.pptx", "CORE100 기준 템플릿 V1", "corporate", ["core", "board_core"], ["board", "governance", "finance", "corporate"], ["board", "finance"]),
+ new("frame_blue", "P01_01_프레임디자인_PPT템플릿_블루(PPT_편집용).pptx", "프레임디자인 블루", "modern", ["blue_frame"], ["frame", "blue", "report", "proposal"], ["sales", "operating_model"]),
+ new("mr_ppt_01", "미스터 피피티 템플릿 01_원본.pptx", "미스터 피피티 템플릿 01", "modern", ["mrppt01"], ["creative", "proposal", "sales"], ["sales"]),
+ new("mr_ppt_02", "미스터 피피티 템플릿 02_원본.pptx", "미스터 피피티 템플릿 02", "vibrant", ["mrppt02"], ["creative", "marketing", "proposal"], ["sales"]),
+ new("mr_ppt_03", "미스터 피피티 03_원본.pptx", "미스터 피피티 템플릿 03", "minimal", ["mrppt03"], ["minimal", "analysis", "report"], ["strategy", "finance"]),
+ new("mr_ppt_04", "미스터 피피티 템플릿 04_원본.pptx", "미스터 피피티 템플릿 04", "slate", ["mrppt04"], ["dark", "executive", "review"], ["board", "finance"]),
+ new("mr_ppt_05", "미스터 피피티 템플릿_05_원본.pptx", "미스터 피피티 템플릿 05", "ocean", ["mrppt05"], ["blue", "modern", "report"], ["pmo", "operating_model"]),
+ ]
+ };
+ }
+}
diff --git a/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs b/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs
index 10a742d..6797dfc 100644
--- a/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs
+++ b/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs
@@ -68,7 +68,8 @@ public partial class ChatWindow
sb.AppendLine("For ordinary Cowork requests where no plan is requested, proceed directly with the work and focus on producing the requested result.");
sb.AppendLine("If the user asks for a brand-new report, proposal, analysis, manual, or other document and does not explicitly ask to reference workspace files, do NOT start with glob, grep, document_read, or folder_map.");
sb.AppendLine("In that case, go straight to the creation tool. Use document_plan only when multi-section structure work clearly improves the result or when the user explicitly requests a plan.");
- sb.AppendLine("If the user asks for a presentation, slide deck, or PPT, prefer calling pptx_create directly. Use document_plan first only when the user explicitly asks for an outline or when the deck structure is genuinely unclear.");
+ sb.AppendLine("If the user asks for a presentation, slide deck, or PPT, prefer document_plan first for executive, business, strategy, review, proposal, or board-style decks, and especially when the deck is more than a few slides.");
+ sb.AppendLine("Use pptx_create directly only for clearly simple decks, explicit slide-by-slide edits, or when the user already provided a strong structure.");
sb.AppendLine("When writing a new document, avoid repetitive same-shape sections. Tailor the structure to the purpose and use summaries, findings, comparison tables, timelines, recommendations, appendices, or action items when they improve clarity.");
sb.AppendLine("Prefer concrete and useful content over filler. If a section benefits from bullets, tables, or structured comparison, use them instead of flat generic paragraphs.");
sb.AppendLine("After creating files, summarize what was created and include the actual output path.");
@@ -91,6 +92,7 @@ public partial class ChatWindow
sb.AppendLine(" 4. Fill every section with real content. Do not stop at an outline or plan only.");
sb.AppendLine(" 4-1. Use richer section patterns when they help: summary box, key findings bullets, comparison table, timeline, checklist, action items, appendix.");
sb.AppendLine(" 5. The task is complete only after the actual document file has been created or updated.");
+ sb.AppendLine(" 6. For PPT work, if the quality summary mentions low score, slide alerts, or needs work, revise the storyline and regenerate instead of stopping after the first pptx_create call.");
// 문서 품질 검증 루프
sb.AppendLine("\n## Document Quality Review");
diff --git a/src/AxCopilot/Views/ChatWindow.xaml b/src/AxCopilot/Views/ChatWindow.xaml
index 8e6f08a..efbf84f 100644
--- a/src/AxCopilot/Views/ChatWindow.xaml
+++ b/src/AxCopilot/Views/ChatWindow.xaml
@@ -5,7 +5,7 @@
xmlns:local="clr-namespace:AxCopilot.Views"
xmlns:conv="clr-namespace:AxCopilot.Themes"
xmlns:vm="clr-namespace:AxCopilot.ViewModels"
- Title="AX Copilot — AX Agent"
+ Title="AX Agent"
Width="1180" Height="880"
MinWidth="780" MinHeight="560"
WindowStyle="None"
diff --git a/src/AxCopilot/skills/pptx-creator.skill.md b/src/AxCopilot/skills/pptx-creator.skill.md
index b78b93b..8c3e803 100644
--- a/src/AxCopilot/skills/pptx-creator.skill.md
+++ b/src/AxCopilot/skills/pptx-creator.skill.md
@@ -14,14 +14,16 @@ allowed-tools:
tabs: cowork
---
-Use `pptx_create` as the default path. Do not start with ad-hoc slide arrays unless the user already gave a very detailed deck structure.
+Use `document_plan` plus `pptx_create` as the default path for business decks. Do not start with ad-hoc slide arrays unless the user already gave a very detailed deck structure.
## Default workflow
1. Review the workspace for existing presentation files, reports, or templates.
2. Use `document_plan` with `document_type: presentation` to define the storyline first.
3. Build a deck brief with `audience`, `objective`, `decision_ask`, and optional `storyline`.
4. Generate the deck with `pptx_create`.
-5. Return the file path together with the storyline and quality summary.
+5. Prefer `template_pack` or a bundled template (`basic100`, `core100`, etc.) when the deck needs branded polish.
+6. If the returned quality summary is weak, revise the storyline and regenerate the same file instead of stopping.
+7. Return the file path together with the storyline and quality summary.
## Quality rules
- Keep one message per slide.