Compare commits

...

2 Commits

Author SHA1 Message Date
36a9af8210 AX Agent 계획 승인 UI를 테마에 맞게 정리하고 플랜 뷰어 상호작용을 개선
계획 확인 카드와 플랜 뷰어 하단 승인 영역을 AX Agent 테마 리소스 기준으로 다시 정리했다.

승인 카드의 표면/버튼/입력 패널 간격을 통일하고 플랜 뷰어에는 검토 안내 카드, 수정 요청 입력 패널, 테마 호버를 추가했다.

README.md와 docs/DEVELOPMENT.md에 2026-04-10 08:47 (KST) 기준 변경 이력을 반영했다.

검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-10 08:48:36 +09:00
d24c1f9edc AX Agent 계획 선행 잔재 제거 및 최종 후속 권유 조건부 완화
이번 커밋은 claude-code와 비교했을 때 AX에 남아 있던 계획 선행 흐름과 과한 최종 후속 권유를 줄이는 데 집중했다.

핵심 변경 사항:

- AgentLoopService에서 사용되지 않는 plan prelude/승인용 계획 생성 블록 제거

- FinalReportGate를 review 작업 및 고영향 변경에만 적용하도록 축소

- FinalReportQuality 프롬프트에서 remaining risk/next action은 실제 미해결 사항이 있을 때만 쓰도록 완화

- Cowork 프롬프트에서 document_plan을 기본 선행 단계처럼 밀지 않고 필요 시만 사용하도록 조정

- Code REPORT 단계도 변경 내용과 검증 요약 중심으로 정리하고 미해결 리스크만 선택적으로 언급하도록 수정

문서 반영:

- README.md, docs/DEVELOPMENT.md에 2026-04-10 00:08 (KST) 기준 이력 추가

검증 결과:

- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\

- 경고 0개, 오류 0개
2026-04-10 08:39:52 +09:00
7 changed files with 1728 additions and 175 deletions

View File

@@ -7,6 +7,21 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
개발 참고: Claw Code 동등성 작업 추적 문서
`docs/claw-code-parity-plan.md`
- 업데이트: 2026-04-10 08:47 (KST)
- 계획 확인 UI를 AX Agent 테마에 맞춰 다시 정리했습니다. 채팅 안 승인 카드는 `LauncherBackground`/`ItemBackground`/`BorderColor`/`AccentColor`를 기준으로 표면, 버튼 모서리, 입력 패널 간격을 통일해 기본 컨트롤 느낌을 줄였습니다.
- 플랜 뷰어 창의 하단 승인 영역도 같은 시각 언어로 손봤습니다. 승인 전 안내 카드를 추가하고, 수정 입력 패널은 테마 배경과 테두리를 쓰도록 바꿨으며, 액션 버튼 호버도 `ItemHoverBackground` 중심으로 정리했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-10 00:08 (KST)
- `claude-code`와 비교했을 때 AX에 남아 있던 계획 선행과 후속 권유 톤을 더 줄였습니다. 기본 Cowork/Code 루프 안에 남아 있던 plan prelude/승인용 죽은 코드를 제거해, 별도 계획 생성 단계를 끼우지 않고 바로 모델+도구 실행으로 들어갑니다.
- Code 최종 보고 재강제도 review 작업과 고영향 변경으로 좁혔습니다. 일반 수정은 변경 내용과 검증 근거가 충분하면 후속 계획이나 남은 리스크를 덧붙이도록 다시 유도하지 않고 마무리할 수 있습니다.
- Cowork/Code 시스템 프롬프트도 같은 방향으로 정리했습니다. Cowork는 새 문서 생성 시 `document_plan`을 기본 선행 단계처럼 밀지 않고 필요할 때만 쓰게 했고, Code는 마지막 보고에서 미해결 사항이 있을 때만 리스크를 언급하게 바꿨습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-10 00:15 (KST)
- Virtual DOM Diff 렌더 구현 완료: React reconciliation 방식의 키 기반 diff를 WPF에 적용. 렌더 체인이 StreamingAppend → Incremental → **DiffRender** → FullRender 4단계로 확장되어, prefix-match 실패 시에도 전체 재빌드 없이 변경분만 반영합니다.
- 검증: `dotnet build` 경고 0 / 오류 0
- 업데이트: 2026-04-09 23:02 (KST)
- IBM/Qwen 후속 호환을 한 단계 더 보강했습니다. 이제 IBM 배포형 응답에서 `generated_text`, `output_text`, `message.content`, `reasoning_content`가 문자열뿐 아니라 배열/블록 형태로 와도 텍스트를 추출하고, `content` 배열 안의 `tool_use/tool_call` 블록도 직접 읽어 도구 호출로 복구합니다.
- 활성 도구 노출 순서도 다시 정리했습니다. `file_read/file_edit/glob/grep/lsp_code_intel/build_run/document_plan` 같은 기본 도구를 먼저 보여주고, `document_review/format_convert/tool_search/code_search`는 그 다음, `mcp_*`, `spawn_agent`, `wait_agents`, `task_*`는 더 뒤로 미뤄 `claude-code`처럼 기본 작업 도구가 먼저 선택되도록 했습니다.

View File

@@ -1,6 +1,22 @@
# AX Copilot - 媛쒕컻 臾몄꽌
## 계획 승인 UI 테마 정렬
- 업데이트: 2026-04-10 08:47 (KST)
- `ChatWindow.AgentStatusPresentation`의 계획 승인 카드를 AX Agent 테마 리소스 기반으로 다시 정리했습니다. 승인/수정/취소 버튼의 라운드, 간격, 입력 패널 배경을 `LauncherBackground`, `ItemBackground`, `BorderColor`, `AccentColor` 축으로 통일해 채팅 본문과 더 자연스럽게 이어지도록 조정했습니다.
- `PlanViewerWindow`의 승인 단계 UI도 같은 방향으로 손봤습니다. 승인 버튼 영역 앞에 검토 안내 카드를 추가하고, 수정 피드백 입력 패널은 테마 배경/테두리를 사용하며, 액션 버튼 호버는 단순 opacity 대신 `ItemHoverBackground` 계열로 반응하도록 바꿨습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
## claude-code식 계획/후속 권유 최소화
- 업데이트: 2026-04-10 00:08 (KST)
- `AgentLoopService`에서 과거 plan mode 잔재로 남아 있던 prelude/승인용 실행 계획 블록을 제거했습니다. 기본 Cowork/Code 루프는 이제 별도 “계획만 먼저 생성” 단계 없이 바로 메인 모델+도구 반복으로 들어갑니다. 사용자가 명시적으로 계획을 원할 때만 응답 텍스트 수준에서 계획을 제시하고, 루프 자체는 `claude-code`처럼 실행 중심으로 유지합니다.
- `AgentLoopTransitions.Verification``FinalReportGate`는 review 작업 또는 고영향 변경일 때만 구조화된 재정리를 요구합니다. 일반 수정은 변경 내용과 검증 근거만 충분하면 “remaining risk / next action”을 추가로 다시 쓰게 하지 않습니다.
- `BuildFinalReportQualityPrompt`도 같은 기준으로 완화했습니다. 남은 리스크/추가 확인은 실제로 미해결 사항이 남아 있을 때만 적도록 바꾸고, 후속 권유는 기본이 아니라 조건부 정보로 낮췄습니다.
- `ChatWindow.SystemPromptBuilder`의 Cowork/Code 프롬프트도 조정했습니다. Cowork는 새 문서 생성 시 `document_plan`을 기본 선행 단계처럼 밀지 않고 “필요할 때만” 사용하게 했고, Code의 REPORT 단계는 “변경 내용과 검증 요약”을 기본으로 하며 미해결 리스크만 선택적으로 언급하게 바꿨습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
## claude-code식 후속 호환/선택성 보강
- 업데이트: 2026-04-09 23:02 (KST)

View File

@@ -239,9 +239,6 @@ public partial class AgentLoopService
var recentTaskRetryQuality = TryGetRecentTaskRetryQuality(taskPolicy.TaskType);
maxRetry = ComputeQualityAwareMaxRetry(maxRetry, recentTaskRetryQuality, taskPolicy.TaskType);
// 플랜 prelude는 현재 정책상 비활성
var shouldGeneratePlanPrelude = false;
var context = BuildContext();
InjectTaskTypeGuidance(messages, taskPolicy);
InjectExplorationScopeGuidance(messages, explorationState.Scope);
@@ -340,148 +337,6 @@ public partial class AgentLoopService
workFolder = context.WorkFolder
}));
// ── 과거 plan mode 잔재. 현재 정책상 비활성 ──
if (shouldGeneratePlanPrelude)
{
iteration++;
EmitEvent(AgentEventType.Thinking, "", "실행 계획 생성 중...");
// 계획 생성 전용 시스템 지시를 임시 추가
var planInstruction = new ChatMessage
{
Role = "user",
Content = "[System] 도구를 호출하지 마세요. 먼저 실행 계획을 번호 매긴 단계로 작성하세요. " +
"각 단계에 사용할 도구와 대상을 구체적으로 명시하세요. " +
"계획만 제시하고 실행은 하지 마세요."
};
messages.Add(planInstruction);
// 도구 없이 텍스트만 요청
string planText;
try
{
planText = await _llm.SendAsync(messages, ct);
}
catch (Exception ex)
{
EmitEvent(AgentEventType.Error, "", $"LLM 오류: {ex.Message}");
return $"⚠ LLM 오류: {ex.Message}";
}
// 계획 지시 메시지 제거 (실제 실행 시 혼란 방지)
messages.Remove(planInstruction);
// 계획 추출
planSteps = TaskDecomposer.ExtractSteps(planText);
planExtracted = true;
if (planSteps.Count > 0)
{
EmitEvent(AgentEventType.Planning, "", $"작업 계획: {planSteps.Count}단계",
steps: planSteps);
// 사용자 승인 대기
if (UserDecisionCallback != null)
{
EmitEvent(
AgentEventType.Decision,
"",
$"계획 확인 대기 · {planSteps.Count}단계",
steps: planSteps);
var decision = await UserDecisionCallback(
planText,
new List<string> { "승인", "수정 요청", "취소" });
EmitPlanDecisionResultEvent(decision, planSteps);
if (decision == "취소")
{
EmitEvent(AgentEventType.Complete, "", "작업이 중단되었습니다");
return "작업이 중단되었습니다.";
}
else if (TryParseApprovedPlanDecision(decision, out var approvedPlanText, out var approvedPlanSteps))
{
planText = approvedPlanText;
planSteps = approvedPlanSteps;
}
else if (decision != null && decision != "승인")
{
// 수정 요청 — 피드백으로 계획 재생성
messages.Add(new ChatMessage { Role = "assistant", Content = planText });
messages.Add(new ChatMessage { Role = "user", Content = decision + "\n위 피드백을 반영하여 실행 계획을 다시 작성하세요." });
// 재생성 루프 (최대 3회)
for (int retry = 0; retry < 3; retry++)
{
try { planText = await _llm.SendAsync(messages, ct); }
catch { break; }
planSteps = TaskDecomposer.ExtractSteps(planText);
if (planSteps.Count > 0)
{
EmitEvent(AgentEventType.Planning, "", $"수정된 계획: {planSteps.Count}단계",
steps: planSteps);
}
EmitEvent(
AgentEventType.Decision,
"",
$"수정 계획 확인 대기 · {planSteps.Count}단계",
steps: planSteps);
decision = await UserDecisionCallback(
planText,
new List<string> { "승인", "수정 요청", "취소" });
EmitPlanDecisionResultEvent(decision, planSteps);
if (decision == "취소")
{
EmitEvent(AgentEventType.Complete, "", "작업이 중단되었습니다");
return "작업이 중단되었습니다.";
}
if (TryParseApprovedPlanDecision(decision, out var revisedPlanText, out var revisedPlanSteps))
{
planText = revisedPlanText;
planSteps = revisedPlanSteps;
break;
}
if (decision == null || decision == "승인") break;
// 재수정
messages.Add(new ChatMessage { Role = "assistant", Content = planText });
messages.Add(new ChatMessage { Role = "user", Content = decision + "\n위 피드백을 반영하여 실행 계획을 다시 작성하세요." });
}
}
}
// 승인된 계획을 컨텍스트에 포함하여 실행 유도
// 도구 호출을 명확히 강제하여 텍스트 응답만 반환하는 경우 방지
messages.Add(new ChatMessage { Role = "assistant", Content = planText });
// 1차 계획의 단계들을 document_plan의 sections_hint로 전달하도록 지시
// → BuildSections() 하드코딩 대신 LLM이 잡은 섹션 구조가 문서에 반영됨
var planSectionsHint = planSteps.Count > 0
? string.Join(", ", planSteps)
: "";
var sectionInstruction = !string.IsNullOrEmpty(planSectionsHint)
? $"document_plan 도구를 호출할 때 sections_hint 파라미터에 위 계획의 섹션/단계를 그대로 넣으세요: \"{planSectionsHint}\""
: "";
messages.Add(new ChatMessage { Role = "user",
Content = "계획이 승인되었습니다. 지금 즉시 1단계부터 도구(tool)를 호출하여 실행을 시작하세요. " +
"텍스트로 설명하지 말고 반드시 도구를 호출하세요." +
(string.IsNullOrEmpty(sectionInstruction) ? "" : "\n" + sectionInstruction) });
}
else
{
// 계획 추출 실패 — assistant 응답으로 추가하고 일반 모드로 진행
if (!string.IsNullOrEmpty(planText))
messages.Add(new ChatMessage { Role = "assistant", Content = planText });
}
}
while (iteration < maxIterations && !ct.IsCancellationRequested)
{
iteration++;
@@ -3538,8 +3393,8 @@ public partial class AgentLoopService
private static string BuildFinalReportQualityPrompt(TaskTypePolicy taskPolicy, bool highImpact)
{
var taskLine = taskPolicy.FinalReportTaskLine;
var riskLine = highImpact
? "고영향 변경이므로 남은 리스크나 추가 확인 필요 사항도 반드시 적으세요.\n"
var riskLine = taskPolicy.IsReviewTask || highImpact
? "남은 리스크나 추가 확인 필요 사항이 실제로 남아 있을 때만 짧게 적으세요.\n"
: "";
return "[System:FinalReportQuality] 최종 답변을 더 구조적으로 정리하세요.\n" +
@@ -3551,7 +3406,7 @@ public partial class AgentLoopService
"6. review 작업이면 이슈별로 수정 완료/미수정 상태를 분리해 적으세요\n" +
taskLine +
riskLine +
"가능하면 짧고 명확하게 요약하세요.";
"후속 권유는 실제 미해결 위험이나 추가 확인이 남았을 때만 포함하세요. 가능하면 짧고 명확하게 요약하세요.";
}
private static string BuildFinalReportQualityPrompt(string taskType, bool highImpact)

View File

@@ -55,9 +55,11 @@ public partial class AgentLoopService
var hasDiffEvidence = HasDiffEvidenceAfterLastModification(messages);
var hasRecentBuildOrTestEvidence = HasBuildOrTestEvidenceAfterLastModification(messages);
var hasSuccessfulBuildAndTestEvidence = HasSuccessfulBuildAndTestAfterLastModification(messages);
var hasLightweightCompletionEvidence = hasCodeVerificationEvidence
|| hasDiffEvidence
|| hasRecentBuildOrTestEvidence;
if (executionPolicy.CodeVerificationGateMaxRetries > 0
&& !hasCodeVerificationEvidence
&& !(hasDiffEvidence && hasRecentBuildOrTestEvidence && !requireHighImpactCodeVerification)
&& !(requireHighImpactCodeVerification ? hasCodeVerificationEvidence : hasLightweightCompletionEvidence)
&& runState.CodeVerificationGateRetry < executionPolicy.CodeVerificationGateMaxRetries)
{
runState.CodeVerificationGateRetry++;
@@ -93,9 +95,12 @@ public partial class AgentLoopService
return true;
}
var hasBlockingCodeEvidenceGap = !hasCodeVerificationEvidence
var hasBlockingCodeEvidenceGap = !(requireHighImpactCodeVerification ? hasCodeVerificationEvidence : hasLightweightCompletionEvidence)
|| (requireHighImpactCodeVerification && !hasSuccessfulBuildAndTestEvidence);
var shouldRequestStructuredFinalReport =
taskPolicy.IsReviewTask || requireHighImpactCodeVerification;
if (executionPolicy.FinalReportGateMaxRetries > 0
&& shouldRequestStructuredFinalReport
&& !hasBlockingCodeEvidenceGap
&& !HasSufficientFinalReportEvidence(textResponse, taskPolicy, requireHighImpactCodeVerification, messages)
&& runState.FinalReportGateRetry < executionPolicy.FinalReportGateMaxRetries)

File diff suppressed because it is too large Load Diff

View File

@@ -61,10 +61,10 @@ public partial class ChatWindow
sb.AppendLine($"Today's date: {DateTime.Now:yyyy년 M월 d일} ({DateTime.Now:yyyy-MM-dd}, {DateTime.Now:dddd}).");
sb.AppendLine("Available skills: excel_create (.xlsx), docx_create (.docx), csv_create (.csv), markdown_create (.md), html_create (.html), script_create (.bat/.ps1), document_review (품질 검증), format_convert (포맷 변환).");
sb.AppendLine("\nOnly present a step-by-step plan when the user explicitly asks for a plan or when the task is too ambiguous to execute safely.");
sb.AppendLine("\nOnly present a step-by-step plan when the user explicitly asks for a plan or a short execution outline is required to unblock the task safely.");
sb.AppendLine("For ordinary Cowork requests, proceed directly with the work and focus on producing the real artifact.");
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 document_plan when helpful, then immediately call the creation tool such as docx_create, html_create, markdown_create, excel_create, or document_assemble.");
sb.AppendLine("In that case, go straight to the creation tool. Use document_plan only when it materially improves a multi-section document.");
sb.AppendLine("After creating files, summarize what was created and include the actual output path.");
sb.AppendLine("Do not stop after a single step. Continue autonomously until the request is completed or a concrete blocker (permission denial, missing dependency, hard error) is encountered.");
sb.AppendLine("When adapting external references, rewrite names/structure/comments to AX Copilot style. Avoid clone-like outputs.");
@@ -239,7 +239,7 @@ public partial class ChatWindow
sb.AppendLine("3. IMPLEMENT: Apply the smallest safe edit. Use file_edit for existing files and file_write for new files.");
sb.AppendLine("4. VERIFY: Run build_run/test_loop when the change affects buildable or testable behavior, or when the user explicitly asks for verification.");
sb.AppendLine(" - Use git_tool(diff) when it helps confirm the final change set or explain what changed.");
sb.AppendLine("5. REPORT: Summarize what changed, what was verified, and any remaining risk.");
sb.AppendLine("5. REPORT: Summarize what changed and what was verified. Mention remaining risk only when something is actually unresolved.");
sb.AppendLine("\n## Development Environment");
sb.AppendLine("Use dev_env_detect to check installed IDEs, runtimes, and build tools before running commands.");

View File

@@ -859,11 +859,30 @@ internal sealed class PlanViewerWindow : Window
_btnPanel.Children.Clear();
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
var secondaryText = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var accentColor = accentBrush is SolidColorBrush accentSolid ? accentSolid.Color : Color.FromRgb(0x4B, 0x5E, 0xFC);
var approveLabel = _uiExpressionLevel == "simple" ? "승인" : "승인 후 실행";
var editLabel = _uiExpressionLevel == "simple" ? "수정" : "수정 피드백";
var rejectLabel = _uiExpressionLevel == "simple" ? "취소" : "거부";
_btnPanel.Children.Add(new Border
{
Background = new SolidColorBrush(Color.FromArgb(0x16, accentColor.R, accentColor.G, accentColor.B)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, accentColor.R, accentColor.G, accentColor.B)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(10),
Padding = new Thickness(12, 8, 12, 8),
Margin = new Thickness(0, 0, 12, 0),
Child = new TextBlock
{
Text = "검토가 끝나면 바로 실행하거나 방향만 짧게 남길 수 있습니다.",
FontSize = 11.5,
Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap,
}
});
var approveBtn = CreateActionButton("\uE73E", approveLabel, accentBrush, Brushes.White, true);
approveBtn.MouseLeftButtonUp += (_, _) =>
{
@@ -904,20 +923,31 @@ internal sealed class PlanViewerWindow : Window
private void ShowEditInput()
{
var itemBackground = Application.Current.TryFindResource("ItemBackground") as Brush
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40));
var launcherBackground = Application.Current.TryFindResource("LauncherBackground") as Brush
?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
var primaryText = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var borderBrush = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
var editPanel = new Border
{
Margin = new Thickness(20, 0, 20, 12),
Padding = new Thickness(12, 8, 12, 8),
CornerRadius = new CornerRadius(10),
Background = Application.Current.TryFindResource("ItemBackground") as Brush
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40)),
Padding = new Thickness(14, 12, 14, 12),
CornerRadius = new CornerRadius(12),
Background = itemBackground,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
};
var editStack = new StackPanel();
editStack.Children.Add(new TextBlock
{
Text = "수정 사항을 입력하세요:",
Text = "어떤 방향으로 바꾸면 좋을지 짧게 남겨주세요.",
FontSize = 11.5,
Foreground = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
Foreground = secondaryText,
Margin = new Thickness(0, 0, 0, 6),
});
var textBox = new TextBox
@@ -927,29 +957,32 @@ internal sealed class PlanViewerWindow : Window
AcceptsReturn = true,
TextWrapping = TextWrapping.Wrap,
FontSize = 13,
Background = Application.Current.TryFindResource("LauncherBackground") as Brush
?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E)),
Foreground = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White,
CaretBrush = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White,
BorderBrush = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray,
Background = launcherBackground,
Foreground = primaryText,
CaretBrush = primaryText,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
Padding = new Thickness(10, 8, 10, 8),
};
editStack.Children.Add(textBox);
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
var actionRow = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Thickness(0, 8, 0, 0),
};
var sendBtn = new Border
{
Background = accentBrush,
CornerRadius = new CornerRadius(8),
Padding = new Thickness(14, 6, 14, 6),
Margin = new Thickness(0, 8, 0, 0),
Margin = new Thickness(0, 0, 8, 0),
Cursor = Cursors.Hand,
HorizontalAlignment = HorizontalAlignment.Right,
Child = new TextBlock
{
Text = "전송", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White,
Text = "수정 요청 보내기", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White,
},
};
sendBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85;
@@ -960,7 +993,36 @@ internal sealed class PlanViewerWindow : Window
if (string.IsNullOrEmpty(feedback)) return;
_tcs?.TrySetResult(feedback);
};
editStack.Children.Add(sendBtn);
actionRow.Children.Add(sendBtn);
var closeBtn = new Border
{
Background = Brushes.Transparent,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(14, 6, 14, 6),
Cursor = Cursors.Hand,
Child = new TextBlock
{
Text = "닫기",
FontSize = 12.5,
FontWeight = FontWeights.SemiBold,
Foreground = secondaryText,
},
};
closeBtn.MouseEnter += (s, _) => ((Border)s).Background = itemBackground;
closeBtn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent;
closeBtn.MouseLeftButtonUp += (_, _) =>
{
if (editPanel.Parent is Grid grid)
{
grid.Children.Remove(editPanel);
_btnPanel.Margin = new Thickness(20, 12, 20, 16);
}
};
actionRow.Children.Add(closeBtn);
editStack.Children.Add(actionRow);
editPanel.Child = editStack;
if (_btnPanel.Parent is Grid parentGrid)
@@ -1008,10 +1070,12 @@ internal sealed class PlanViewerWindow : Window
Brush textColor, bool filled)
{
var color = ((SolidColorBrush)borderColor).Color;
var hoverBg = Application.Current?.TryFindResource("ItemHoverBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
var btn = new Border
{
CornerRadius = new CornerRadius(12),
Padding = new Thickness(16, 8, 16, 8),
CornerRadius = new CornerRadius(14),
Padding = new Thickness(16, 9, 16, 9),
Margin = new Thickness(4, 0, 4, 0),
Cursor = Cursors.Hand,
Background = filled ? borderColor
@@ -1033,8 +1097,20 @@ internal sealed class PlanViewerWindow : Window
Foreground = filled ? Brushes.White : textColor,
});
btn.Child = sp;
btn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85;
btn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
btn.MouseEnter += (s, _) =>
{
var border = (Border)s;
border.Opacity = 0.96;
if (!filled)
border.Background = hoverBg;
};
btn.MouseLeave += (s, _) =>
{
var border = (Border)s;
border.Opacity = 1.0;
if (!filled)
border.Background = new SolidColorBrush(Color.FromArgb(0x18, color.R, color.G, color.B));
};
return btn;
}