Compare commits
3 Commits
90bd77f945
...
68524c1c94
| Author | SHA1 | Date | |
|---|---|---|---|
| 68524c1c94 | |||
| b4a506de96 | |||
| 82b42b3ba3 |
12
README.md
12
README.md
@@ -7,10 +7,22 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
|
|||||||
개발 참고: Claw Code 동등성 작업 추적 문서
|
개발 참고: Claw Code 동등성 작업 추적 문서
|
||||||
`docs/claw-code-parity-plan.md`
|
`docs/claw-code-parity-plan.md`
|
||||||
|
|
||||||
|
- 업데이트: 2026-04-06 08:28 (KST)
|
||||||
|
- AX Agent 하단 composer와 대기열 UI 렌더를 `ChatWindow.ComposerQueuePresentation.cs`로 분리했습니다. 입력창 높이 계산, draft kind 해석, 후속 요청 큐 카드/요약 pill/배지/액션 버튼 생성 책임을 메인 창 코드에서 떼어냈습니다.
|
||||||
|
- `ChatWindow.xaml.cs`는 대기열 실행 orchestration과 세션 변경 흐름만 더 선명하게 남겨, claw-code 기준 입력부/queued command UX 개선을 계속하기 쉬운 구조로 정리했습니다.
|
||||||
|
|
||||||
|
- 업데이트: 2026-04-06 08:12 (KST)
|
||||||
|
- AX Agent 하단 작업 바 관련 presentation 메서드를 메인 창 코드에서 더 분리했습니다. `ChatWindow.FooterPresentation.cs`를 추가해 폴더 바, 선택된 프리셋 안내, Git 브랜치 버튼/팝업 렌더, 요약 pill 생성 책임을 별도 partial로 옮겼습니다.
|
||||||
|
- `ChatWindow.xaml.cs`는 대화 흐름과 런타임 orchestration 중심으로 더 정리했고, claw-code 기준으로 footer/preset/Git popup 품질 작업을 계속 이어가기 쉬운 구조를 만들었습니다.
|
||||||
|
|
||||||
- 업데이트: 2026-04-06 01:37 (KST)
|
- 업데이트: 2026-04-06 01:37 (KST)
|
||||||
- AX Agent의 계획 승인 흐름을 더 transcript 우선 구조로 정리했습니다. 인라인 승인 카드가 기본 경로를 맡고, `계획` 버튼은 저장된 계획 요약/단계를 여는 상세 보기 역할만 하도록 분리했습니다.
|
- AX Agent의 계획 승인 흐름을 더 transcript 우선 구조로 정리했습니다. 인라인 승인 카드가 기본 경로를 맡고, `계획` 버튼은 저장된 계획 요약/단계를 여는 상세 보기 역할만 하도록 분리했습니다.
|
||||||
- 상태선과 quick strip 계산도 presentation state로 더 모았습니다. runtime badge, compact strip, quick strip의 텍스트/강조색/노출 여부를 한 번에 계산해 창 코드의 직접 분기를 줄였습니다.
|
- 상태선과 quick strip 계산도 presentation state로 더 모았습니다. runtime badge, compact strip, quick strip의 텍스트/강조색/노출 여부를 한 번에 계산해 창 코드의 직접 분기를 줄였습니다.
|
||||||
|
|
||||||
|
- 업데이트: 2026-04-06 07:31 (KST)
|
||||||
|
- `ChatWindow.xaml.cs`에 몰려 있던 권한 팝업 렌더와 컨텍스트 사용량 카드 렌더를 별도 partial 파일로 분리했습니다. `ChatWindow.PermissionPresentation.cs`, `ChatWindow.ContextUsagePresentation.cs`를 추가해 권한 선택/권한 상태 배너/컨텍스트 사용량 hover 카드 책임을 메인 창 orchestration 코드에서 떼어냈습니다.
|
||||||
|
- 다음 단계에서 `permission / tool-result / footer` presentation catalog를 더 세밀하게 확장하기 쉽게 구조를 정리했고, 동작은 그대로 유지한 채 transcript/푸터 품질 개선 발판을 마련했습니다.
|
||||||
|
|
||||||
- 업데이트: 2026-04-06 00:50 (KST)
|
- 업데이트: 2026-04-06 00:50 (KST)
|
||||||
- 채팅/코워크 프리셋 카드의 hover 설명 레이어를 카드 내부 오버레이 방식에서 안정적인 tooltip형 설명으로 바꿨습니다. 카드 배경/테두리만 반응하게 정리해 hover 시 반복 깜빡임을 줄였습니다.
|
- 채팅/코워크 프리셋 카드의 hover 설명 레이어를 카드 내부 오버레이 방식에서 안정적인 tooltip형 설명으로 바꿨습니다. 카드 배경/테두리만 반응하게 정리해 hover 시 반복 깜빡임을 줄였습니다.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# AX Copilot - 媛쒕컻 臾몄꽌
|
# AX Copilot - 媛쒕컻 臾몄꽌
|
||||||
|
|
||||||
|
- Document update: 2026-04-06 07:31 (KST) - Split permission presentation logic out of `ChatWindow.xaml.cs` into `ChatWindow.PermissionPresentation.cs`. Permission popup row construction, popup refresh, section expansion persistence, and permission banner/status styling now live in a dedicated partial instead of the main window orchestration file.
|
||||||
|
- Document update: 2026-04-06 07:31 (KST) - Split context usage card/popup rendering into `ChatWindow.ContextUsagePresentation.cs`. The Cowork/Code context usage ring, tooltip popup copy, hover close behavior, and screen-coordinate hit testing are now isolated from the rest of the chat window flow.
|
||||||
- Document update: 2026-04-06 01:37 (KST) - Reworked AX Agent plan approval toward a more transcript-native flow. The inline decision card remains the primary approval path, while the `계획` affordance now opens the stored plan as a detail-only surface instead of acting like a required popup step.
|
- Document update: 2026-04-06 01:37 (KST) - Reworked AX Agent plan approval toward a more transcript-native flow. The inline decision card remains the primary approval path, while the `계획` affordance now opens the stored plan as a detail-only surface instead of acting like a required popup step.
|
||||||
- Document update: 2026-04-06 01:37 (KST) - Expanded `OperationalStatusPresentationState` so runtime badge, compact strip, and quick-strip labels/colors/visibility are calculated together. `ChatWindow` now consumes a richer presentation model instead of branching on strip kinds and quick-strip counters independently.
|
- Document update: 2026-04-06 01:37 (KST) - Expanded `OperationalStatusPresentationState` so runtime badge, compact strip, and quick-strip labels/colors/visibility are calculated together. `ChatWindow` now consumes a richer presentation model instead of branching on strip kinds and quick-strip counters independently.
|
||||||
|
|
||||||
@@ -4889,3 +4891,7 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
|
|||||||
- transcript display catalog를 `claw-code` 기준으로 정교화했다. [AgentTranscriptDisplayCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs)는 도구/스킬 이름과 badge label을 `파일 / 문서 / 빌드 / Git / 웹 / 질문 / 제안 / 에이전트` 축으로 재정의했고, summary fallback 문구도 더 자연스러운 한국어로 정리했다.
|
- transcript display catalog를 `claw-code` 기준으로 정교화했다. [AgentTranscriptDisplayCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs)는 도구/스킬 이름과 badge label을 `파일 / 문서 / 빌드 / Git / 웹 / 질문 / 제안 / 에이전트` 축으로 재정의했고, summary fallback 문구도 더 자연스러운 한국어로 정리했다.
|
||||||
- [PermissionRequestPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs)는 `명령 실행 / 웹 요청 / 스킬 실행 / 의견 요청 / 파일 수정 / 파일 접근` 권한 요청을 타입별 색상/라벨로 분기하게 바꿨다. [ToolResultPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs)는 도구 결과를 `파일 작업`, `빌드/테스트`, `Git`, `문서`, `스킬`, `웹 요청`, `명령 실행` 기준으로 성공/실패 라벨을 더 세밀하게 반환한다.
|
- [PermissionRequestPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs)는 `명령 실행 / 웹 요청 / 스킬 실행 / 의견 요청 / 파일 수정 / 파일 접근` 권한 요청을 타입별 색상/라벨로 분기하게 바꿨다. [ToolResultPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs)는 도구 결과를 `파일 작업`, `빌드/테스트`, `Git`, `문서`, `스킬`, `웹 요청`, `명령 실행` 기준으로 성공/실패 라벨을 더 세밀하게 반환한다.
|
||||||
- 이벤트 배너 renderer를 [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs) 로 분리했다. 기존 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 있던 `CreateCompactEventPill`, `AddAgentEventBanner`, `GetDecisionBadgeMeta`를 별도 partial로 옮겨, 이후 `permission/tool-result/plan` 타입별 renderer 확장을 더 쉽게 할 수 있는 구조를 마련했다.
|
- 이벤트 배너 renderer를 [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs) 로 분리했다. 기존 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 있던 `CreateCompactEventPill`, `AddAgentEventBanner`, `GetDecisionBadgeMeta`를 별도 partial로 옮겨, 이후 `permission/tool-result/plan` 타입별 renderer 확장을 더 쉽게 할 수 있는 구조를 마련했다.
|
||||||
|
- Document update: 2026-04-06 08:12 (KST) - Split footer/preset/Git popup presentation logic out of `ChatWindow.xaml.cs` into `ChatWindow.FooterPresentation.cs`. The folder bar refresh, selected preset guide, Git branch surface update, Git popup assembly, and popup summary-pill/row helpers now live in a dedicated partial instead of the main window orchestration file.
|
||||||
|
- Document update: 2026-04-06 08:12 (KST) - This pass keeps the AX Agent transcript/runtime flow closer to the `claw-code` separation model by reducing UI assembly inside the main chat window file and isolating footer/prompt-adjacent presentation code for future parity work.
|
||||||
|
- Document update: 2026-04-06 08:28 (KST) - Split composer/draft queue presentation logic out of `ChatWindow.xaml.cs` into `ChatWindow.ComposerQueuePresentation.cs`. Input-box height calculation, draft-kind inference, queue summary pills, compact queue cards, expanded queue sections, queue badges, and queue action buttons now live in a dedicated partial.
|
||||||
|
- Document update: 2026-04-06 08:28 (KST) - This keeps `ChatWindow.xaml.cs` more orchestration-focused while preserving the same runtime behavior, and it aligns AX Agent more closely with the `claw-code` model of separating prompt/footer presentation from session execution logic.
|
||||||
|
|||||||
624
src/AxCopilot/Views/ChatWindow.ComposerQueuePresentation.cs
Normal file
624
src/AxCopilot/Views/ChatWindow.ComposerQueuePresentation.cs
Normal file
@@ -0,0 +1,624 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using AxCopilot.Models;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
|
||||||
|
namespace AxCopilot.Views;
|
||||||
|
|
||||||
|
public partial class ChatWindow
|
||||||
|
{
|
||||||
|
private void UpdateInputBoxHeight()
|
||||||
|
{
|
||||||
|
if (InputBox == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var text = InputBox.Text ?? string.Empty;
|
||||||
|
var explicitLineCount = 1 + text.Count(ch => ch == '\n');
|
||||||
|
var displayMode = (_settings.Settings.Llm.AgentUiExpressionLevel ?? "balanced").Trim().ToLowerInvariant();
|
||||||
|
if (displayMode is not ("rich" or "balanced" or "simple"))
|
||||||
|
displayMode = "balanced";
|
||||||
|
|
||||||
|
var maxLines = displayMode switch
|
||||||
|
{
|
||||||
|
"rich" => 6,
|
||||||
|
"simple" => 4,
|
||||||
|
_ => 5,
|
||||||
|
};
|
||||||
|
const double baseHeight = 42;
|
||||||
|
const double lineStep = 22;
|
||||||
|
var visibleLines = Math.Clamp(explicitLineCount, 1, maxLines);
|
||||||
|
var targetHeight = baseHeight + ((visibleLines - 1) * lineStep);
|
||||||
|
|
||||||
|
InputBox.MinLines = 1;
|
||||||
|
InputBox.MaxLines = maxLines;
|
||||||
|
InputBox.Height = targetHeight;
|
||||||
|
InputBox.VerticalScrollBarVisibility = explicitLineCount > maxLines
|
||||||
|
? ScrollBarVisibility.Auto
|
||||||
|
: ScrollBarVisibility.Disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildComposerDraftText()
|
||||||
|
{
|
||||||
|
var rawText = InputBox?.Text?.Trim() ?? "";
|
||||||
|
return _slashPalette.ActiveCommand != null
|
||||||
|
? (_slashPalette.ActiveCommand + " " + rawText).Trim()
|
||||||
|
: rawText;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string InferDraftKind(string text, string? explicitKind = null)
|
||||||
|
{
|
||||||
|
var trimmed = text?.Trim() ?? "";
|
||||||
|
var requestedKind = explicitKind?.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
if (requestedKind is "followup" or "steering")
|
||||||
|
return requestedKind;
|
||||||
|
|
||||||
|
if (trimmed.StartsWith("/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "command";
|
||||||
|
|
||||||
|
if (requestedKind is "direct" or "message")
|
||||||
|
return requestedKind;
|
||||||
|
|
||||||
|
if (trimmed.StartsWith("steer:", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
trimmed.StartsWith("@steer ", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
trimmed.StartsWith("조정:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "steering";
|
||||||
|
|
||||||
|
return "message";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void QueueComposerDraft(string priority = "next", string? explicitKind = null, bool startImmediatelyWhenIdle = true)
|
||||||
|
{
|
||||||
|
if (InputBox == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var text = BuildComposerDraftText();
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_isStreaming && string.Equals(priority, "now", StringComparison.OrdinalIgnoreCase))
|
||||||
|
priority = "next";
|
||||||
|
|
||||||
|
HideSlashChip(restoreText: false);
|
||||||
|
ClearPromptCardPlaceholder();
|
||||||
|
|
||||||
|
var queuedItem = EnqueueDraftRequest(text, priority, explicitKind);
|
||||||
|
|
||||||
|
InputBox.Clear();
|
||||||
|
InputBox.Focus();
|
||||||
|
UpdateInputBoxHeight();
|
||||||
|
RefreshDraftQueueUi();
|
||||||
|
|
||||||
|
if (queuedItem == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_isStreaming && startImmediatelyWhenIdle)
|
||||||
|
{
|
||||||
|
StartNextQueuedDraftIfAny(queuedItem.Id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var toast = queuedItem.Kind switch
|
||||||
|
{
|
||||||
|
"command" => "명령이 대기열에 추가되었습니다.",
|
||||||
|
"direct" => "직접 실행 요청이 대기열에 추가되었습니다.",
|
||||||
|
"steering" => "조정 요청이 대기열에 추가되었습니다.",
|
||||||
|
"followup" => "후속 작업이 대기열에 추가되었습니다.",
|
||||||
|
_ => "메시지가 대기열에 추가되었습니다.",
|
||||||
|
};
|
||||||
|
ShowToast(toast);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshDraftQueueUi()
|
||||||
|
{
|
||||||
|
if (DraftPreviewCard == null || DraftPreviewText == null || DraftQueuePanel == null || BtnDraftEnqueue == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (_convLock)
|
||||||
|
{
|
||||||
|
var session = ChatSession;
|
||||||
|
if (session != null)
|
||||||
|
_draftQueueProcessor.PromoteReadyBlockedItems(session, _activeTab, _storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = _appState.GetDraftQueueItems(_activeTab);
|
||||||
|
|
||||||
|
DraftPreviewCard.Visibility = Visibility.Collapsed;
|
||||||
|
BtnDraftEnqueue.IsEnabled = false;
|
||||||
|
DraftPreviewText.Text = string.Empty;
|
||||||
|
|
||||||
|
RebuildDraftQueuePanel(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsDraftQueueExpanded()
|
||||||
|
=> _expandedDraftQueueTabs.Contains(_activeTab);
|
||||||
|
|
||||||
|
private void ToggleDraftQueueExpanded()
|
||||||
|
{
|
||||||
|
if (!_expandedDraftQueueTabs.Add(_activeTab))
|
||||||
|
_expandedDraftQueueTabs.Remove(_activeTab);
|
||||||
|
|
||||||
|
RefreshDraftQueueUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RebuildDraftQueuePanel(IReadOnlyList<DraftQueueItem> items)
|
||||||
|
{
|
||||||
|
if (DraftQueuePanel == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
DraftQueuePanel.Children.Clear();
|
||||||
|
|
||||||
|
var visibleItems = items
|
||||||
|
.OrderBy(GetDraftStateRank)
|
||||||
|
.ThenBy(GetDraftPriorityRank)
|
||||||
|
.ThenBy(x => x.CreatedAt)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (visibleItems.Count == 0)
|
||||||
|
{
|
||||||
|
DraftQueuePanel.Visibility = Visibility.Collapsed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary = _appState.GetDraftQueueSummary(_activeTab);
|
||||||
|
var shouldShowQueue =
|
||||||
|
IsDraftQueueExpanded()
|
||||||
|
|| summary.RunningCount > 0
|
||||||
|
|| summary.QueuedCount > 0
|
||||||
|
|| summary.FailedCount > 0;
|
||||||
|
|
||||||
|
if (!shouldShowQueue)
|
||||||
|
{
|
||||||
|
DraftQueuePanel.Visibility = Visibility.Collapsed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DraftQueuePanel.Visibility = Visibility.Visible;
|
||||||
|
DraftQueuePanel.Children.Add(CreateDraftQueueSummaryStrip(summary, IsDraftQueueExpanded()));
|
||||||
|
if (!IsDraftQueueExpanded())
|
||||||
|
{
|
||||||
|
DraftQueuePanel.Children.Add(CreateCompactDraftQueuePanel(visibleItems, summary));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int maxPerSection = 3;
|
||||||
|
var runningItems = visibleItems
|
||||||
|
.Where(item => string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Take(maxPerSection)
|
||||||
|
.ToList();
|
||||||
|
var queuedItems = visibleItems
|
||||||
|
.Where(item => string.Equals(item.State, "queued", StringComparison.OrdinalIgnoreCase) && !IsDraftBlocked(item))
|
||||||
|
.Take(maxPerSection)
|
||||||
|
.ToList();
|
||||||
|
var blockedItems = visibleItems
|
||||||
|
.Where(IsDraftBlocked)
|
||||||
|
.Take(maxPerSection)
|
||||||
|
.ToList();
|
||||||
|
var completedItems = visibleItems
|
||||||
|
.Where(item => string.Equals(item.State, "completed", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Take(maxPerSection)
|
||||||
|
.ToList();
|
||||||
|
var failedItems = visibleItems
|
||||||
|
.Where(item => string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Take(maxPerSection)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
AddDraftQueueSection("실행 중", runningItems, summary.RunningCount);
|
||||||
|
AddDraftQueueSection("다음 작업", queuedItems, summary.QueuedCount);
|
||||||
|
AddDraftQueueSection("보류", blockedItems, summary.BlockedCount);
|
||||||
|
AddDraftQueueSection("완료", completedItems, summary.CompletedCount);
|
||||||
|
AddDraftQueueSection("실패", failedItems, summary.FailedCount);
|
||||||
|
|
||||||
|
if (summary.CompletedCount > 0 || summary.FailedCount > 0)
|
||||||
|
{
|
||||||
|
var footer = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
Margin = new Thickness(0, 2, 0, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (summary.CompletedCount > 0)
|
||||||
|
footer.Children.Add(CreateDraftQueueActionButton($"완료 정리 {summary.CompletedCount}", ClearCompletedDrafts, BrushFromHex("#ECFDF5")));
|
||||||
|
|
||||||
|
if (summary.FailedCount > 0)
|
||||||
|
footer.Children.Add(CreateDraftQueueActionButton($"실패 정리 {summary.FailedCount}", ClearFailedDrafts, BrushFromHex("#FEF2F2")));
|
||||||
|
|
||||||
|
DraftQueuePanel.Children.Add(footer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UIElement CreateCompactDraftQueuePanel(IReadOnlyList<DraftQueueItem> items, AppStateService.DraftQueueSummaryState summary)
|
||||||
|
{
|
||||||
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#111827");
|
||||||
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#6B7280");
|
||||||
|
var background = TryFindResource("ItemBackground") as Brush ?? BrushFromHex("#F7F7F8");
|
||||||
|
var borderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E4E4E7");
|
||||||
|
var focusItem = items.FirstOrDefault(item => string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase))
|
||||||
|
?? items.FirstOrDefault(item => string.Equals(item.State, "queued", StringComparison.OrdinalIgnoreCase) && !IsDraftBlocked(item))
|
||||||
|
?? items.FirstOrDefault(IsDraftBlocked)
|
||||||
|
?? items.FirstOrDefault(item => string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase))
|
||||||
|
?? items.FirstOrDefault(item => string.Equals(item.State, "completed", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
var container = new Border
|
||||||
|
{
|
||||||
|
Background = background,
|
||||||
|
BorderBrush = borderBrush,
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(14),
|
||||||
|
Padding = new Thickness(12, 10, 12, 10),
|
||||||
|
Margin = new Thickness(0, 0, 0, 4),
|
||||||
|
};
|
||||||
|
|
||||||
|
var root = new Grid();
|
||||||
|
root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||||
|
root.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||||
|
container.Child = root;
|
||||||
|
|
||||||
|
var left = new StackPanel();
|
||||||
|
left.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = focusItem == null
|
||||||
|
? "대기열 항목이 준비되면 여기에서 요약됩니다."
|
||||||
|
: $"{GetDraftStateLabel(focusItem)} · {GetDraftKindLabel(focusItem)}",
|
||||||
|
FontSize = 11,
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = primaryText,
|
||||||
|
});
|
||||||
|
left.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = focusItem?.Text ?? BuildDraftQueueCompactSummaryText(summary),
|
||||||
|
FontSize = 10.5,
|
||||||
|
Foreground = secondaryText,
|
||||||
|
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||||
|
Margin = new Thickness(0, 4, 0, 0),
|
||||||
|
MaxWidth = 520,
|
||||||
|
});
|
||||||
|
Grid.SetColumn(left, 0);
|
||||||
|
root.Children.Add(left);
|
||||||
|
|
||||||
|
var action = CreateDraftQueueActionButton("상세", ToggleDraftQueueExpanded);
|
||||||
|
action.Margin = new Thickness(12, 0, 0, 0);
|
||||||
|
Grid.SetColumn(action, 1);
|
||||||
|
root.Children.Add(action);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildDraftQueueCompactSummaryText(AppStateService.DraftQueueSummaryState summary)
|
||||||
|
{
|
||||||
|
var parts = new List<string>();
|
||||||
|
if (summary.RunningCount > 0) parts.Add($"실행 {summary.RunningCount}");
|
||||||
|
if (summary.QueuedCount > 0) parts.Add($"다음 {summary.QueuedCount}");
|
||||||
|
if (summary.FailedCount > 0) parts.Add($"실패 {summary.FailedCount}");
|
||||||
|
return parts.Count == 0 ? "대기열 0" : string.Join(" · ", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddDraftQueueSection(string label, IReadOnlyList<DraftQueueItem> items, int totalCount)
|
||||||
|
{
|
||||||
|
if (DraftQueuePanel == null || totalCount <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
DraftQueuePanel.Children.Add(CreateDraftQueueSectionLabel($"{label} · {totalCount}"));
|
||||||
|
foreach (var item in items)
|
||||||
|
DraftQueuePanel.Children.Add(CreateDraftQueueCard(item));
|
||||||
|
|
||||||
|
if (totalCount > items.Count)
|
||||||
|
{
|
||||||
|
DraftQueuePanel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"추가 항목 {totalCount - items.Count}개",
|
||||||
|
Margin = new Thickness(8, -2, 0, 8),
|
||||||
|
FontSize = 10.5,
|
||||||
|
Foreground = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#7A7F87"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UIElement CreateDraftQueueSummaryStrip(AppStateService.DraftQueueSummaryState summary, bool isExpanded)
|
||||||
|
{
|
||||||
|
var root = new Grid
|
||||||
|
{
|
||||||
|
Margin = new Thickness(0, 0, 0, 8),
|
||||||
|
};
|
||||||
|
root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||||
|
root.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||||
|
|
||||||
|
var wrap = new WrapPanel();
|
||||||
|
|
||||||
|
if (summary.RunningCount > 0)
|
||||||
|
wrap.Children.Add(CreateQueueSummaryPill("실행 중", summary.RunningCount.ToString(), "#EFF6FF", "#BFDBFE", "#1D4ED8"));
|
||||||
|
if (summary.QueuedCount > 0)
|
||||||
|
wrap.Children.Add(CreateQueueSummaryPill("다음", summary.QueuedCount.ToString(), "#F5F3FF", "#DDD6FE", "#6D28D9"));
|
||||||
|
if (isExpanded && summary.BlockedCount > 0)
|
||||||
|
wrap.Children.Add(CreateQueueSummaryPill("보류", summary.BlockedCount.ToString(), "#FFF7ED", "#FDBA74", "#C2410C"));
|
||||||
|
if (isExpanded && summary.CompletedCount > 0)
|
||||||
|
wrap.Children.Add(CreateQueueSummaryPill("완료", summary.CompletedCount.ToString(), "#ECFDF5", "#BBF7D0", "#166534"));
|
||||||
|
if (summary.FailedCount > 0)
|
||||||
|
wrap.Children.Add(CreateQueueSummaryPill("실패", summary.FailedCount.ToString(), "#FEF2F2", "#FECACA", "#991B1B"));
|
||||||
|
|
||||||
|
if (wrap.Children.Count == 0)
|
||||||
|
wrap.Children.Add(CreateQueueSummaryPill("대기열", "0", "#F8FAFC", "#E2E8F0", "#475569"));
|
||||||
|
|
||||||
|
Grid.SetColumn(wrap, 0);
|
||||||
|
root.Children.Add(wrap);
|
||||||
|
|
||||||
|
var toggle = CreateDraftQueueActionButton(isExpanded ? "간단히" : "상세 보기", ToggleDraftQueueExpanded);
|
||||||
|
toggle.Margin = new Thickness(10, 0, 0, 0);
|
||||||
|
Grid.SetColumn(toggle, 1);
|
||||||
|
root.Children.Add(toggle);
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Border CreateQueueSummaryPill(string label, string value, string bgHex, string borderHex, string fgHex)
|
||||||
|
{
|
||||||
|
return new Border
|
||||||
|
{
|
||||||
|
Background = BrushFromHex(bgHex),
|
||||||
|
BorderBrush = BrushFromHex(borderHex),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(999),
|
||||||
|
Padding = new Thickness(8, 3, 8, 3),
|
||||||
|
Margin = new Thickness(0, 0, 6, 0),
|
||||||
|
Child = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new TextBlock
|
||||||
|
{
|
||||||
|
Text = label,
|
||||||
|
FontSize = 10,
|
||||||
|
Foreground = BrushFromHex(fgHex),
|
||||||
|
},
|
||||||
|
new TextBlock
|
||||||
|
{
|
||||||
|
Text = $" {value}",
|
||||||
|
FontSize = 10,
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = BrushFromHex(fgHex),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextBlock CreateDraftQueueSectionLabel(string text)
|
||||||
|
{
|
||||||
|
return new TextBlock
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
FontSize = 10.5,
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Margin = new Thickness(8, 0, 8, 6),
|
||||||
|
Foreground = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#64748B"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Border CreateDraftQueueCard(DraftQueueItem item)
|
||||||
|
{
|
||||||
|
var background = TryFindResource("ItemBackground") as Brush ?? BrushFromHex("#F7F7F8");
|
||||||
|
var borderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E4E4E7");
|
||||||
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#111827");
|
||||||
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#6B7280");
|
||||||
|
var neutralSurface = BrushFromHex("#F5F6F8");
|
||||||
|
var (kindIcon, kindForeground) = GetDraftKindVisual(item);
|
||||||
|
var (stateBackground, stateBorder, stateForeground) = GetDraftStateBadgeColors(item);
|
||||||
|
var (priorityBackground, priorityBorder, priorityForeground) = GetDraftPriorityBadgeColors(item.Priority);
|
||||||
|
|
||||||
|
var container = new Border
|
||||||
|
{
|
||||||
|
Background = background,
|
||||||
|
BorderBrush = borderBrush,
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(14),
|
||||||
|
Padding = new Thickness(12, 10, 12, 10),
|
||||||
|
Margin = new Thickness(0, 0, 0, 8),
|
||||||
|
};
|
||||||
|
|
||||||
|
var root = new Grid();
|
||||||
|
root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||||
|
root.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||||
|
container.Child = root;
|
||||||
|
|
||||||
|
var left = new StackPanel();
|
||||||
|
Grid.SetColumn(left, 0);
|
||||||
|
root.Children.Add(left);
|
||||||
|
|
||||||
|
var header = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
};
|
||||||
|
header.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = kindIcon,
|
||||||
|
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||||
|
FontSize = 11,
|
||||||
|
Foreground = kindForeground,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = new Thickness(0, 0, 6, 0),
|
||||||
|
});
|
||||||
|
header.Children.Add(CreateDraftQueueBadge(GetDraftKindLabel(item), BrushFromHex("#F8FAFC"), borderBrush, kindForeground));
|
||||||
|
header.Children.Add(CreateDraftQueueBadge(GetDraftStateLabel(item), stateBackground, stateBorder, stateForeground));
|
||||||
|
header.Children.Add(CreateDraftQueueBadge(GetDraftPriorityLabel(item.Priority), priorityBackground, priorityBorder, priorityForeground));
|
||||||
|
left.Children.Add(header);
|
||||||
|
|
||||||
|
left.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = item.Text,
|
||||||
|
FontSize = 12.5,
|
||||||
|
Foreground = primaryText,
|
||||||
|
Margin = new Thickness(0, 6, 0, 0),
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||||
|
MaxWidth = 520,
|
||||||
|
});
|
||||||
|
|
||||||
|
var meta = $"{item.CreatedAt:HH:mm}";
|
||||||
|
if (item.AttemptCount > 0)
|
||||||
|
meta += $" · 시도 {item.AttemptCount}";
|
||||||
|
if (item.NextRetryAt.HasValue && item.NextRetryAt.Value > DateTime.Now)
|
||||||
|
meta += $" · 재시도 {item.NextRetryAt.Value:HH:mm:ss}";
|
||||||
|
if (!string.IsNullOrWhiteSpace(item.LastError))
|
||||||
|
meta += $" · {TruncateForStatus(item.LastError, 36)}";
|
||||||
|
|
||||||
|
left.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = meta,
|
||||||
|
FontSize = 10.5,
|
||||||
|
Foreground = secondaryText,
|
||||||
|
Margin = new Thickness(0, 6, 0, 0),
|
||||||
|
});
|
||||||
|
|
||||||
|
var actions = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
|
};
|
||||||
|
Grid.SetColumn(actions, 1);
|
||||||
|
root.Children.Add(actions);
|
||||||
|
|
||||||
|
if (!string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase))
|
||||||
|
actions.Children.Add(CreateDraftQueueActionButton("실행", () => QueueDraftForImmediateRun(item.Id)));
|
||||||
|
|
||||||
|
if (string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(item.State, "completed", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
actions.Children.Add(CreateDraftQueueActionButton("대기", () => ResetDraftInQueue(item.Id), neutralSurface));
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.Children.Add(CreateDraftQueueActionButton("삭제", () => RemoveDraftFromQueue(item.Id), neutralSurface));
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Border CreateDraftQueueBadge(string text, Brush background, Brush borderBrush, Brush foreground)
|
||||||
|
{
|
||||||
|
return new Border
|
||||||
|
{
|
||||||
|
Background = background,
|
||||||
|
BorderBrush = borderBrush,
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(999),
|
||||||
|
Padding = new Thickness(7, 2, 7, 2),
|
||||||
|
Margin = new Thickness(0, 0, 6, 0),
|
||||||
|
Child = new TextBlock
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
FontSize = 10,
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = foreground,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button CreateDraftQueueActionButton(string label, Action onClick, Brush? background = null)
|
||||||
|
{
|
||||||
|
var btn = new Button
|
||||||
|
{
|
||||||
|
Content = label,
|
||||||
|
Margin = new Thickness(6, 0, 0, 0),
|
||||||
|
Padding = new Thickness(10, 5, 10, 5),
|
||||||
|
MinWidth = 48,
|
||||||
|
FontSize = 11,
|
||||||
|
Background = background ?? BrushFromHex("#EEF2FF"),
|
||||||
|
BorderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#D4D4D8"),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
Foreground = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#111827"),
|
||||||
|
Cursor = Cursors.Hand,
|
||||||
|
};
|
||||||
|
btn.Click += (_, _) => onClick();
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetDraftStateRank(DraftQueueItem item)
|
||||||
|
=> string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase) ? 0
|
||||||
|
: IsDraftBlocked(item) ? 1
|
||||||
|
: string.Equals(item.State, "queued", StringComparison.OrdinalIgnoreCase) ? 2
|
||||||
|
: string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase) ? 3
|
||||||
|
: 4;
|
||||||
|
|
||||||
|
private static int GetDraftPriorityRank(DraftQueueItem item)
|
||||||
|
=> item.Priority?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"now" => 0,
|
||||||
|
"next" => 1,
|
||||||
|
_ => 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string GetDraftPriorityLabel(string? priority)
|
||||||
|
=> priority?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"now" => "지금",
|
||||||
|
"later" => "나중",
|
||||||
|
_ => "다음",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string GetDraftKindLabel(DraftQueueItem item)
|
||||||
|
=> item.Kind?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"followup" => "후속 작업",
|
||||||
|
"steering" => "조정",
|
||||||
|
"command" => "명령",
|
||||||
|
"direct" => "직접 실행",
|
||||||
|
_ => "메시지",
|
||||||
|
};
|
||||||
|
|
||||||
|
private (string Icon, Brush Foreground) GetDraftKindVisual(DraftQueueItem item)
|
||||||
|
=> item.Kind?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"followup" => ("\uE8A5", BrushFromHex("#0F766E")),
|
||||||
|
"steering" => ("\uE7C3", BrushFromHex("#B45309")),
|
||||||
|
"command" => ("\uE756", BrushFromHex("#7C3AED")),
|
||||||
|
"direct" => ("\uE8A7", BrushFromHex("#2563EB")),
|
||||||
|
_ => ("\uE8BD", BrushFromHex("#475569")),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string GetDraftStateLabel(DraftQueueItem item)
|
||||||
|
=> IsDraftBlocked(item) ? "재시도 대기"
|
||||||
|
: item.State?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"running" => "실행 중",
|
||||||
|
"failed" => "실패",
|
||||||
|
"completed" => "완료",
|
||||||
|
_ => "대기",
|
||||||
|
};
|
||||||
|
|
||||||
|
private Brush GetDraftStateBrush(DraftQueueItem item)
|
||||||
|
=> IsDraftBlocked(item) ? BrushFromHex("#B45309")
|
||||||
|
: item.State?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"running" => BrushFromHex("#2563EB"),
|
||||||
|
"failed" => BrushFromHex("#DC2626"),
|
||||||
|
"completed" => BrushFromHex("#059669"),
|
||||||
|
_ => BrushFromHex("#7C3AED"),
|
||||||
|
};
|
||||||
|
|
||||||
|
private (Brush Background, Brush Border, Brush Foreground) GetDraftStateBadgeColors(DraftQueueItem item)
|
||||||
|
=> IsDraftBlocked(item)
|
||||||
|
? (BrushFromHex("#FFF7ED"), BrushFromHex("#FDBA74"), BrushFromHex("#C2410C"))
|
||||||
|
: item.State?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"running" => (BrushFromHex("#EFF6FF"), BrushFromHex("#BFDBFE"), BrushFromHex("#1D4ED8")),
|
||||||
|
"failed" => (BrushFromHex("#FEF2F2"), BrushFromHex("#FECACA"), BrushFromHex("#991B1B")),
|
||||||
|
"completed" => (BrushFromHex("#ECFDF5"), BrushFromHex("#BBF7D0"), BrushFromHex("#166534")),
|
||||||
|
_ => (BrushFromHex("#F5F3FF"), BrushFromHex("#DDD6FE"), BrushFromHex("#6D28D9")),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static (Brush Background, Brush Border, Brush Foreground) GetDraftPriorityBadgeColors(string? priority)
|
||||||
|
=> priority?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"now" => (BrushFromHex("#EEF2FF"), BrushFromHex("#C7D2FE"), BrushFromHex("#3730A3")),
|
||||||
|
"later" => (BrushFromHex("#F8FAFC"), BrushFromHex("#E2E8F0"), BrushFromHex("#475569")),
|
||||||
|
_ => (BrushFromHex("#FEF3C7"), BrushFromHex("#FDE68A"), BrushFromHex("#92400E")),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static bool IsDraftBlocked(DraftQueueItem item)
|
||||||
|
=> string.Equals(item.State, "queued", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& item.NextRetryAt.HasValue
|
||||||
|
&& item.NextRetryAt.Value > DateTime.Now;
|
||||||
|
}
|
||||||
144
src/AxCopilot/Views/ChatWindow.ContextUsagePresentation.cs
Normal file
144
src/AxCopilot/Views/ChatWindow.ContextUsagePresentation.cs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace AxCopilot.Views;
|
||||||
|
|
||||||
|
public partial class ChatWindow
|
||||||
|
{
|
||||||
|
private void RefreshContextUsageVisual()
|
||||||
|
{
|
||||||
|
if (TokenUsageCard == null || TokenUsageArc == null || TokenUsagePercentText == null
|
||||||
|
|| TokenUsageSummaryText == null || TokenUsageHintText == null
|
||||||
|
|| TokenUsageThresholdMarker == null || CompactNowLabel == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var showContextUsage = _activeTab is "Cowork" or "Code";
|
||||||
|
TokenUsageCard.Visibility = showContextUsage ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
if (!showContextUsage)
|
||||||
|
{
|
||||||
|
if (TokenUsagePopup != null)
|
||||||
|
TokenUsagePopup.IsOpen = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var llm = _settings.Settings.Llm;
|
||||||
|
var maxContextTokens = Math.Clamp(llm.MaxContextTokens, 1024, 1_000_000);
|
||||||
|
var triggerPercent = Math.Clamp(llm.ContextCompactTriggerPercent, 10, 95);
|
||||||
|
var triggerRatio = triggerPercent / 100.0;
|
||||||
|
|
||||||
|
int messageTokens;
|
||||||
|
lock (_convLock)
|
||||||
|
messageTokens = _currentConversation?.Messages?.Count > 0
|
||||||
|
? Services.TokenEstimator.EstimateMessages(_currentConversation.Messages)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
var draftText = InputBox?.Text ?? "";
|
||||||
|
var draftTokens = string.IsNullOrWhiteSpace(draftText) ? 0 : Services.TokenEstimator.Estimate(draftText) + 4;
|
||||||
|
var currentTokens = Math.Max(0, messageTokens + draftTokens);
|
||||||
|
var usageRatio = Services.TokenEstimator.GetContextUsage(currentTokens, maxContextTokens);
|
||||||
|
|
||||||
|
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue;
|
||||||
|
Brush progressBrush = accentBrush;
|
||||||
|
string summary;
|
||||||
|
string compactLabel;
|
||||||
|
|
||||||
|
if (usageRatio >= 1.0)
|
||||||
|
{
|
||||||
|
progressBrush = Brushes.IndianRed;
|
||||||
|
summary = "컨텍스트 한도 초과";
|
||||||
|
compactLabel = "지금 압축";
|
||||||
|
}
|
||||||
|
else if (usageRatio >= triggerRatio)
|
||||||
|
{
|
||||||
|
progressBrush = Brushes.DarkOrange;
|
||||||
|
summary = llm.EnableProactiveContextCompact ? "곧 자동 압축" : "압축 임계 도달";
|
||||||
|
compactLabel = "압축 권장";
|
||||||
|
}
|
||||||
|
else if (usageRatio >= triggerRatio * 0.7)
|
||||||
|
{
|
||||||
|
progressBrush = Brushes.Goldenrod;
|
||||||
|
summary = "컨텍스트 사용 증가";
|
||||||
|
compactLabel = "미리 압축";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
summary = "컨텍스트 여유";
|
||||||
|
compactLabel = "압축";
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenUsageArc.Stroke = progressBrush;
|
||||||
|
TokenUsageThresholdMarker.Fill = progressBrush;
|
||||||
|
var percentText = $"{Math.Round(usageRatio * 100):0}%";
|
||||||
|
TokenUsagePercentText.Text = percentText;
|
||||||
|
TokenUsageSummaryText.Text = $"컨텍스트 {percentText}";
|
||||||
|
TokenUsageHintText.Text = $"{Services.TokenEstimator.Format(currentTokens)} / {Services.TokenEstimator.Format(maxContextTokens)}";
|
||||||
|
CompactNowLabel.Text = compactLabel;
|
||||||
|
|
||||||
|
if (TokenUsagePopupTitle != null)
|
||||||
|
TokenUsagePopupTitle.Text = $"컨텍스트 창 {percentText}";
|
||||||
|
if (TokenUsagePopupUsage != null)
|
||||||
|
TokenUsagePopupUsage.Text = $"{Services.TokenEstimator.Format(currentTokens)}/{Services.TokenEstimator.Format(maxContextTokens)}";
|
||||||
|
if (TokenUsagePopupDetail != null)
|
||||||
|
TokenUsagePopupDetail.Text = _pendingPostCompaction ? "compact 후 첫 응답 대기 중" : $"자동 압축 시작 {triggerPercent}%";
|
||||||
|
if (TokenUsagePopupCompact != null)
|
||||||
|
TokenUsagePopupCompact.Text = "AX Agent가 컨텍스트를 자동으로 관리합니다";
|
||||||
|
|
||||||
|
TokenUsageCard.ToolTip = null;
|
||||||
|
|
||||||
|
UpdateCircularUsageArc(TokenUsageArc, usageRatio, 14, 14, 11);
|
||||||
|
PositionThresholdMarker(TokenUsageThresholdMarker, triggerRatio, 14, 14, 11, 2.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TokenUsageCard_MouseEnter(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
_tokenUsagePopupCloseTimer.Stop();
|
||||||
|
if (TokenUsagePopup != null && TokenUsageCard?.Visibility == Visibility.Visible)
|
||||||
|
TokenUsagePopup.IsOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TokenUsageCard_MouseLeave(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
_tokenUsagePopupCloseTimer.Stop();
|
||||||
|
_tokenUsagePopupCloseTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TokenUsagePopup_MouseEnter(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
_tokenUsagePopupCloseTimer.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TokenUsagePopup_MouseLeave(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
_tokenUsagePopupCloseTimer.Stop();
|
||||||
|
_tokenUsagePopupCloseTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseTokenUsagePopupIfIdle()
|
||||||
|
{
|
||||||
|
if (TokenUsagePopup == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var cardHovered = IsMouseInsideElement(TokenUsageCard);
|
||||||
|
var popupHovered = TokenUsagePopup.Child is FrameworkElement popupChild && IsMouseInsideElement(popupChild);
|
||||||
|
if (!cardHovered && !popupHovered)
|
||||||
|
TokenUsagePopup.IsOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsMouseInsideElement(FrameworkElement? element)
|
||||||
|
{
|
||||||
|
if (element == null || !element.IsVisible || element.ActualWidth <= 0 || element.ActualHeight <= 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var mouse = System.Windows.Forms.Control.MousePosition;
|
||||||
|
var point = element.PointFromScreen(new Point(mouse.X, mouse.Y));
|
||||||
|
return point.X >= 0 && point.Y >= 0 && point.X <= element.ActualWidth && point.Y <= element.ActualHeight;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return element.IsMouseOver;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
441
src/AxCopilot/Views/ChatWindow.FooterPresentation.cs
Normal file
441
src/AxCopilot/Views/ChatWindow.FooterPresentation.cs
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using AxCopilot.Models;
|
||||||
|
|
||||||
|
namespace AxCopilot.Views;
|
||||||
|
|
||||||
|
public partial class ChatWindow
|
||||||
|
{
|
||||||
|
private void UpdateFolderBar()
|
||||||
|
{
|
||||||
|
if (FolderBar == null) return;
|
||||||
|
if (_activeTab == "Chat")
|
||||||
|
{
|
||||||
|
FolderBar.Visibility = Visibility.Collapsed;
|
||||||
|
UpdateGitBranchUi(null, "", "", "", "", Visibility.Collapsed);
|
||||||
|
RefreshContextUsageVisual();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FolderBar.Visibility = Visibility.Visible;
|
||||||
|
var folder = GetCurrentWorkFolder();
|
||||||
|
if (!string.IsNullOrEmpty(folder))
|
||||||
|
{
|
||||||
|
FolderPathLabel.Text = folder;
|
||||||
|
FolderPathLabel.ToolTip = folder;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FolderPathLabel.Text = "폴더를 선택하세요";
|
||||||
|
FolderPathLabel.ToolTip = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadConversationSettings();
|
||||||
|
LoadCompactionMetricsFromConversation();
|
||||||
|
UpdatePermissionUI();
|
||||||
|
UpdateDataUsageUI();
|
||||||
|
RefreshContextUsageVisual();
|
||||||
|
ScheduleGitBranchRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateDataUsageUI()
|
||||||
|
{
|
||||||
|
_folderDataUsage = GetAutomaticFolderDataUsage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSelectedPresetGuide(ChatConversation? conversation = null)
|
||||||
|
{
|
||||||
|
if (SelectedPresetGuide == null || SelectedPresetGuideTitle == null || SelectedPresetGuideDesc == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
SelectedPresetGuide.Visibility = Visibility.Collapsed;
|
||||||
|
SelectedPresetGuideTitle.Text = "";
|
||||||
|
SelectedPresetGuideDesc.Text = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
conversation ??= _currentConversation;
|
||||||
|
var category = conversation?.Category?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(category))
|
||||||
|
{
|
||||||
|
SelectedPresetGuide.Visibility = Visibility.Collapsed;
|
||||||
|
SelectedPresetGuideTitle.Text = "";
|
||||||
|
SelectedPresetGuideDesc.Text = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var preset = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets)
|
||||||
|
.FirstOrDefault(p => string.Equals(p.Category?.Trim(), category, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (preset == null)
|
||||||
|
{
|
||||||
|
SelectedPresetGuide.Visibility = Visibility.Collapsed;
|
||||||
|
SelectedPresetGuideTitle.Text = "";
|
||||||
|
SelectedPresetGuideDesc.Text = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectedPresetGuideTitle.Text = string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? $"선택된 작업 유형 · {preset.Label}"
|
||||||
|
: $"선택된 대화 주제 · {preset.Label}";
|
||||||
|
SelectedPresetGuideDesc.Text = string.IsNullOrWhiteSpace(preset.Description)
|
||||||
|
? (preset.Placeholder ?? "")
|
||||||
|
: preset.Description;
|
||||||
|
SelectedPresetGuide.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateGitBranchUi(string? branchName, string filesText, string addedText, string deletedText, string tooltip, Visibility visibility)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
_currentGitBranchName = branchName;
|
||||||
|
_currentGitTooltip = tooltip;
|
||||||
|
|
||||||
|
if (BtnGitBranch != null)
|
||||||
|
{
|
||||||
|
BtnGitBranch.Visibility = visibility;
|
||||||
|
BtnGitBranch.ToolTip = string.IsNullOrWhiteSpace(tooltip) ? "현재 Git 브랜치 상태" : tooltip;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GitBranchLabel != null)
|
||||||
|
GitBranchLabel.Text = string.IsNullOrWhiteSpace(branchName) ? "브랜치 없음" : branchName;
|
||||||
|
if (GitBranchFilesText != null)
|
||||||
|
GitBranchFilesText.Text = filesText;
|
||||||
|
if (GitBranchAddedText != null)
|
||||||
|
GitBranchAddedText.Text = addedText;
|
||||||
|
if (GitBranchDeletedText != null)
|
||||||
|
GitBranchDeletedText.Text = deletedText;
|
||||||
|
if (GitBranchSeparator != null)
|
||||||
|
GitBranchSeparator.Visibility = visibility;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildGitBranchPopup()
|
||||||
|
{
|
||||||
|
if (GitBranchItems == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
GitBranchItems.Children.Clear();
|
||||||
|
|
||||||
|
var gitRoot = _currentGitRoot ?? ResolveGitRoot(GetCurrentWorkFolder());
|
||||||
|
var branchName = _currentGitBranchName ?? "detached";
|
||||||
|
var tooltip = _currentGitTooltip ?? "";
|
||||||
|
var fileText = GitBranchFilesText?.Text ?? "";
|
||||||
|
var addedText = GitBranchAddedText?.Text ?? "";
|
||||||
|
var deletedText = GitBranchDeletedText?.Text ?? "";
|
||||||
|
var query = (_gitBranchSearchText ?? "").Trim();
|
||||||
|
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||||
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||||
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||||
|
|
||||||
|
GitBranchItems.Children.Add(CreatePopupSummaryStrip(new[]
|
||||||
|
{
|
||||||
|
("브랜치", string.IsNullOrWhiteSpace(branchName) ? "없음" : branchName, "#F8FAFC", "#E2E8F0", "#475569"),
|
||||||
|
("파일", string.IsNullOrWhiteSpace(fileText) ? "0" : fileText, "#EFF6FF", "#BFDBFE", "#1D4ED8"),
|
||||||
|
("최근", _recentGitBranches.Count.ToString(), "#F5F3FF", "#DDD6FE", "#6D28D9"),
|
||||||
|
}));
|
||||||
|
GitBranchItems.Children.Add(CreatePopupSectionLabel("현재 브랜치", new Thickness(8, 6, 8, 4)));
|
||||||
|
GitBranchItems.Children.Add(CreatePopupMenuRow(
|
||||||
|
"\uE943",
|
||||||
|
branchName,
|
||||||
|
string.IsNullOrWhiteSpace(fileText) ? "현재 브랜치" : fileText,
|
||||||
|
true,
|
||||||
|
accentBrush,
|
||||||
|
secondaryText,
|
||||||
|
primaryText,
|
||||||
|
() => { }));
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(addedText) || !string.IsNullOrWhiteSpace(deletedText))
|
||||||
|
{
|
||||||
|
var stats = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
Margin = new Thickness(8, 2, 8, 8),
|
||||||
|
};
|
||||||
|
if (!string.IsNullOrWhiteSpace(addedText))
|
||||||
|
stats.Children.Add(CreateMetricPill(addedText, "#16A34A"));
|
||||||
|
if (!string.IsNullOrWhiteSpace(deletedText))
|
||||||
|
stats.Children.Add(CreateMetricPill(deletedText, "#DC2626"));
|
||||||
|
GitBranchItems.Children.Add(stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(gitRoot))
|
||||||
|
{
|
||||||
|
GitBranchItems.Children.Add(CreatePopupSectionLabel("저장소", new Thickness(8, 6, 8, 4)));
|
||||||
|
GitBranchItems.Children.Add(CreatePopupMenuRow(
|
||||||
|
"\uED25",
|
||||||
|
System.IO.Path.GetFileName(gitRoot.TrimEnd('\\', '/')),
|
||||||
|
gitRoot,
|
||||||
|
false,
|
||||||
|
accentBrush,
|
||||||
|
secondaryText,
|
||||||
|
primaryText,
|
||||||
|
() => { }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_currentGitUpstreamStatus))
|
||||||
|
{
|
||||||
|
GitBranchItems.Children.Add(CreatePopupMenuRow(
|
||||||
|
"\uE8AB",
|
||||||
|
"업스트림",
|
||||||
|
_currentGitUpstreamStatus!,
|
||||||
|
false,
|
||||||
|
accentBrush,
|
||||||
|
secondaryText,
|
||||||
|
primaryText,
|
||||||
|
() => { }));
|
||||||
|
}
|
||||||
|
|
||||||
|
GitBranchItems.Children.Add(CreatePopupSectionLabel("빠른 작업", new Thickness(8, 10, 8, 4)));
|
||||||
|
GitBranchItems.Children.Add(CreatePopupMenuRow(
|
||||||
|
"\uE8C8",
|
||||||
|
"상태 요약 복사",
|
||||||
|
"브랜치, 변경 파일, 추가/삭제 라인 복사",
|
||||||
|
false,
|
||||||
|
accentBrush,
|
||||||
|
secondaryText,
|
||||||
|
primaryText,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
try { Clipboard.SetText(tooltip); } catch { }
|
||||||
|
GitBranchPopup.IsOpen = false;
|
||||||
|
}));
|
||||||
|
|
||||||
|
GitBranchItems.Children.Add(CreatePopupMenuRow(
|
||||||
|
"\uE72B",
|
||||||
|
"새로고침",
|
||||||
|
"Git 상태를 다시 조회합니다",
|
||||||
|
false,
|
||||||
|
accentBrush,
|
||||||
|
secondaryText,
|
||||||
|
primaryText,
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
await RefreshGitBranchStatusAsync();
|
||||||
|
BuildGitBranchPopup();
|
||||||
|
}));
|
||||||
|
|
||||||
|
var filteredBranches = _currentGitBranches
|
||||||
|
.Where(branch => string.IsNullOrWhiteSpace(query)
|
||||||
|
|| branch.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Take(20)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var recentBranches = _recentGitBranches
|
||||||
|
.Where(branch => _currentGitBranches.Any(current => string.Equals(current, branch, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.Where(branch => string.IsNullOrWhiteSpace(query)
|
||||||
|
|| branch.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Take(5)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (recentBranches.Count > 0)
|
||||||
|
{
|
||||||
|
GitBranchItems.Children.Add(CreatePopupSectionLabel($"최근 전환 · {recentBranches.Count}", new Thickness(8, 10, 8, 4)));
|
||||||
|
|
||||||
|
foreach (var branch in recentBranches)
|
||||||
|
{
|
||||||
|
var isCurrent = string.Equals(branch, branchName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
GitBranchItems.Children.Add(CreatePopupMenuRow(
|
||||||
|
isCurrent ? "\uE73E" : "\uE8FD",
|
||||||
|
branch,
|
||||||
|
isCurrent ? "현재 브랜치" : "최근 사용 브랜치",
|
||||||
|
isCurrent,
|
||||||
|
accentBrush,
|
||||||
|
secondaryText,
|
||||||
|
primaryText,
|
||||||
|
isCurrent ? null : () => _ = SwitchGitBranchAsync(branch)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_currentGitBranches.Count > 0)
|
||||||
|
{
|
||||||
|
var branchSectionLabel = string.IsNullOrWhiteSpace(query)
|
||||||
|
? $"브랜치 전환 · {_currentGitBranches.Count}"
|
||||||
|
: $"브랜치 전환 · {filteredBranches.Count}/{_currentGitBranches.Count}";
|
||||||
|
GitBranchItems.Children.Add(CreatePopupSectionLabel(branchSectionLabel, new Thickness(8, 10, 8, 4)));
|
||||||
|
|
||||||
|
foreach (var branch in filteredBranches)
|
||||||
|
{
|
||||||
|
if (recentBranches.Any(recent => string.Equals(recent, branch, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var isCurrent = string.Equals(branch, branchName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
GitBranchItems.Children.Add(CreatePopupMenuRow(
|
||||||
|
isCurrent ? "\uE73E" : "\uE943",
|
||||||
|
branch,
|
||||||
|
isCurrent ? "현재 브랜치" : "이 브랜치로 전환",
|
||||||
|
isCurrent,
|
||||||
|
accentBrush,
|
||||||
|
secondaryText,
|
||||||
|
primaryText,
|
||||||
|
isCurrent ? null : () => _ = SwitchGitBranchAsync(branch)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(query) && filteredBranches.Count == 0)
|
||||||
|
{
|
||||||
|
GitBranchItems.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "검색 결과가 없습니다.",
|
||||||
|
FontSize = 11.5,
|
||||||
|
Foreground = secondaryText,
|
||||||
|
Margin = new Thickness(10, 6, 10, 10),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GitBranchItems.Children.Add(CreatePopupSectionLabel("브랜치 작업", new Thickness(8, 10, 8, 4)));
|
||||||
|
GitBranchItems.Children.Add(CreatePopupMenuRow(
|
||||||
|
"\uE710",
|
||||||
|
"새 브랜치 생성",
|
||||||
|
"현재 작업 기준으로 새 브랜치를 만들고 전환합니다",
|
||||||
|
false,
|
||||||
|
accentBrush,
|
||||||
|
secondaryText,
|
||||||
|
primaryText,
|
||||||
|
() => _ = CreateGitBranchAsync()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextBlock CreatePopupSectionLabel(string text, Thickness? margin = null)
|
||||||
|
{
|
||||||
|
return new TextBlock
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
FontSize = 10.5,
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||||
|
Margin = margin ?? new Thickness(8, 8, 8, 4),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private UIElement CreatePopupSummaryStrip(IEnumerable<(string Label, string Value, string BgHex, string BorderHex, string FgHex)> items)
|
||||||
|
{
|
||||||
|
var wrap = new WrapPanel
|
||||||
|
{
|
||||||
|
Margin = new Thickness(8, 6, 8, 6),
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
wrap.Children.Add(CreateMetricPill($"{item.Label} {item.Value}", item.FgHex, item.BgHex, item.BorderHex));
|
||||||
|
|
||||||
|
return wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Border CreateMetricPill(string text, string colorHex)
|
||||||
|
=> CreateMetricPill(text, colorHex, $"{colorHex}18", $"{colorHex}44");
|
||||||
|
|
||||||
|
private Border CreateMetricPill(string text, string colorHex, string bgHex, string borderHex)
|
||||||
|
{
|
||||||
|
return new Border
|
||||||
|
{
|
||||||
|
Background = BrushFromHex(bgHex),
|
||||||
|
BorderBrush = BrushFromHex(borderHex),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(999),
|
||||||
|
Padding = new Thickness(8, 3, 8, 3),
|
||||||
|
Margin = new Thickness(0, 0, 6, 0),
|
||||||
|
Child = new TextBlock
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
FontSize = 10.5,
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = BrushFromHex(colorHex),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Border CreateFlatPopupRow(string icon, string title, string description, string colorHex, bool clickable, Action? onClick)
|
||||||
|
{
|
||||||
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||||
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||||
|
var hoverBrush = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.LightGray;
|
||||||
|
var borderColor = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||||
|
|
||||||
|
var border = new Border
|
||||||
|
{
|
||||||
|
Background = Brushes.Transparent,
|
||||||
|
BorderBrush = borderColor,
|
||||||
|
BorderThickness = new Thickness(0, 0, 0, 1),
|
||||||
|
Padding = new Thickness(8, 9, 8, 9),
|
||||||
|
Cursor = clickable ? Cursors.Hand : Cursors.Arrow,
|
||||||
|
Focusable = clickable,
|
||||||
|
};
|
||||||
|
KeyboardNavigation.SetIsTabStop(border, clickable);
|
||||||
|
|
||||||
|
var grid = new Grid();
|
||||||
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||||
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||||
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||||
|
|
||||||
|
grid.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = icon,
|
||||||
|
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||||
|
FontSize = 11,
|
||||||
|
Foreground = BrushFromHex(colorHex),
|
||||||
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
|
Margin = new Thickness(0, 1, 10, 0),
|
||||||
|
});
|
||||||
|
|
||||||
|
var textStack = new StackPanel();
|
||||||
|
textStack.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = title,
|
||||||
|
FontSize = 12,
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = primaryText,
|
||||||
|
});
|
||||||
|
if (!string.IsNullOrWhiteSpace(description))
|
||||||
|
{
|
||||||
|
textStack.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = description,
|
||||||
|
FontSize = 10.5,
|
||||||
|
Foreground = secondaryText,
|
||||||
|
Margin = new Thickness(0, 2, 0, 0),
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Grid.SetColumn(textStack, 1);
|
||||||
|
grid.Children.Add(textStack);
|
||||||
|
if (clickable)
|
||||||
|
{
|
||||||
|
var chevron = new TextBlock
|
||||||
|
{
|
||||||
|
Text = "\uE76C",
|
||||||
|
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||||
|
FontSize = 10,
|
||||||
|
Foreground = secondaryText,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = new Thickness(8, 0, 0, 0),
|
||||||
|
};
|
||||||
|
Grid.SetColumn(chevron, 2);
|
||||||
|
grid.Children.Add(chevron);
|
||||||
|
}
|
||||||
|
border.Child = grid;
|
||||||
|
|
||||||
|
if (clickable && onClick != null)
|
||||||
|
{
|
||||||
|
border.MouseEnter += (_, _) => border.Background = hoverBrush;
|
||||||
|
border.MouseLeave += (_, _) => border.Background = Brushes.Transparent;
|
||||||
|
border.MouseLeftButtonUp += (_, _) => onClick();
|
||||||
|
border.KeyDown += (_, ke) =>
|
||||||
|
{
|
||||||
|
if (ke.Key is Key.Enter or Key.Space)
|
||||||
|
{
|
||||||
|
ke.Handled = true;
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return border;
|
||||||
|
}
|
||||||
|
}
|
||||||
326
src/AxCopilot/Views/ChatWindow.PermissionPresentation.cs
Normal file
326
src/AxCopilot/Views/ChatWindow.PermissionPresentation.cs
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
using AxCopilot.Models;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
using AxCopilot.Services.Agent;
|
||||||
|
|
||||||
|
namespace AxCopilot.Views;
|
||||||
|
|
||||||
|
public partial class ChatWindow
|
||||||
|
{
|
||||||
|
private void BtnPermission_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (PermissionPopup == null) return;
|
||||||
|
PermissionItems.Children.Clear();
|
||||||
|
|
||||||
|
ChatConversation? currentConversation;
|
||||||
|
lock (_convLock) currentConversation = _currentConversation;
|
||||||
|
var coreLevels = PermissionModePresentationCatalog.Ordered
|
||||||
|
.Where(item => !string.Equals(item.Mode, PermissionModeCatalog.Plan, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
var current = PermissionModeCatalog.NormalizeGlobalMode(_settings.Settings.Llm.FilePermission);
|
||||||
|
|
||||||
|
void AddPermissionRows(Panel container, IEnumerable<PermissionModePresentation> levels)
|
||||||
|
{
|
||||||
|
foreach (var item in levels)
|
||||||
|
{
|
||||||
|
var level = item.Mode;
|
||||||
|
var isActive = level.Equals(current, StringComparison.OrdinalIgnoreCase);
|
||||||
|
var rowBorder = new Border
|
||||||
|
{
|
||||||
|
Background = isActive ? BrushFromHex("#F8FAFC") : Brushes.Transparent,
|
||||||
|
BorderBrush = Brushes.Transparent,
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(12),
|
||||||
|
Padding = new Thickness(10, 10, 10, 10),
|
||||||
|
Margin = new Thickness(0, 0, 0, 4),
|
||||||
|
Cursor = Cursors.Hand,
|
||||||
|
Focusable = true,
|
||||||
|
};
|
||||||
|
KeyboardNavigation.SetIsTabStop(rowBorder, true);
|
||||||
|
|
||||||
|
var row = new Grid();
|
||||||
|
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||||
|
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||||
|
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||||
|
|
||||||
|
row.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = item.Icon,
|
||||||
|
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||||
|
FontSize = 15,
|
||||||
|
Foreground = BrushFromHex(item.ColorHex),
|
||||||
|
Margin = new Thickness(0, 0, 10, 0),
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
});
|
||||||
|
|
||||||
|
var textStack = new StackPanel();
|
||||||
|
textStack.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = item.Title,
|
||||||
|
FontSize = 13.5,
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||||
|
});
|
||||||
|
textStack.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = item.Description,
|
||||||
|
FontSize = 11.5,
|
||||||
|
Margin = new Thickness(0, 2, 0, 0),
|
||||||
|
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
LineHeight = 16,
|
||||||
|
MaxWidth = 220,
|
||||||
|
});
|
||||||
|
Grid.SetColumn(textStack, 1);
|
||||||
|
row.Children.Add(textStack);
|
||||||
|
|
||||||
|
var check = new TextBlock
|
||||||
|
{
|
||||||
|
Text = isActive ? "\uE73E" : "",
|
||||||
|
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||||
|
FontSize = 12,
|
||||||
|
FontWeight = FontWeights.Bold,
|
||||||
|
Foreground = BrushFromHex("#2563EB"),
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = new Thickness(12, 0, 0, 0),
|
||||||
|
};
|
||||||
|
Grid.SetColumn(check, 2);
|
||||||
|
row.Children.Add(check);
|
||||||
|
|
||||||
|
rowBorder.Child = row;
|
||||||
|
rowBorder.MouseEnter += (_, _) => rowBorder.Background = BrushFromHex("#F8FAFC");
|
||||||
|
rowBorder.MouseLeave += (_, _) => rowBorder.Background = isActive ? BrushFromHex("#F8FAFC") : Brushes.Transparent;
|
||||||
|
|
||||||
|
var capturedLevel = level;
|
||||||
|
void ApplyPermission()
|
||||||
|
{
|
||||||
|
_settings.Settings.Llm.FilePermission = PermissionModeCatalog.NormalizeGlobalMode(capturedLevel);
|
||||||
|
try { _settings.Save(); } catch { }
|
||||||
|
_appState.LoadFromSettings(_settings);
|
||||||
|
UpdatePermissionUI();
|
||||||
|
SaveConversationSettings();
|
||||||
|
RefreshInlineSettingsPanel();
|
||||||
|
RefreshOverlayModeButtons();
|
||||||
|
PermissionPopup.IsOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rowBorder.MouseLeftButtonDown += (_, _) => ApplyPermission();
|
||||||
|
rowBorder.KeyDown += (_, ke) =>
|
||||||
|
{
|
||||||
|
if (ke.Key is Key.Enter or Key.Space)
|
||||||
|
{
|
||||||
|
ke.Handled = true;
|
||||||
|
ApplyPermission();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
container.Children.Add(rowBorder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddPermissionRows(PermissionItems, coreLevels);
|
||||||
|
|
||||||
|
PermissionPopup.IsOpen = true;
|
||||||
|
Dispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
TryFocusFirstPermissionElement(PermissionItems);
|
||||||
|
}, System.Windows.Threading.DispatcherPriority.Input);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryFocusFirstPermissionElement(DependencyObject root)
|
||||||
|
{
|
||||||
|
if (root is UIElement ui && ui.Focusable && ui.IsEnabled && ui.Visibility == Visibility.Visible)
|
||||||
|
return ui.Focus();
|
||||||
|
|
||||||
|
var childCount = VisualTreeHelper.GetChildrenCount(root);
|
||||||
|
for (var i = 0; i < childCount; i++)
|
||||||
|
{
|
||||||
|
var child = VisualTreeHelper.GetChild(root, i);
|
||||||
|
if (TryFocusFirstPermissionElement(child))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetToolPermissionOverride(string toolName, string? mode)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(toolName)) return;
|
||||||
|
var toolPermissions = _settings.Settings.Llm.ToolPermissions ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var existingKey = toolPermissions.Keys.FirstOrDefault(x => string.Equals(x, toolName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(mode))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(existingKey))
|
||||||
|
toolPermissions.Remove(existingKey!);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toolPermissions[existingKey ?? toolName] = PermissionModeCatalog.NormalizeToolOverride(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
try { _settings.Save(); } catch { }
|
||||||
|
_appState.LoadFromSettings(_settings);
|
||||||
|
UpdatePermissionUI();
|
||||||
|
SaveConversationSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshPermissionPopup()
|
||||||
|
{
|
||||||
|
if (PermissionPopup == null) return;
|
||||||
|
BtnPermission_Click(this, new RoutedEventArgs());
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool GetPermissionPopupSectionExpanded(string sectionKey, bool defaultValue = false)
|
||||||
|
{
|
||||||
|
var map = _settings.Settings.Llm.PermissionPopupSections;
|
||||||
|
if (map != null && map.TryGetValue(sectionKey, out var expanded))
|
||||||
|
return expanded;
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetPermissionPopupSectionExpanded(string sectionKey, bool expanded)
|
||||||
|
{
|
||||||
|
var map = _settings.Settings.Llm.PermissionPopupSections ??= new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
map[sectionKey] = expanded;
|
||||||
|
try { _settings.Save(); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BtnPermissionTopBannerClose_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (PermissionTopBanner != null)
|
||||||
|
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePermissionUI()
|
||||||
|
{
|
||||||
|
if (PermissionLabel == null || PermissionIcon == null) return;
|
||||||
|
ChatConversation? currentConversation;
|
||||||
|
lock (_convLock) currentConversation = _currentConversation;
|
||||||
|
var summary = _appState.GetPermissionSummary(currentConversation);
|
||||||
|
var perm = PermissionModeCatalog.NormalizeGlobalMode(summary.EffectiveMode);
|
||||||
|
PermissionLabel.Text = PermissionModeCatalog.ToDisplayLabel(perm);
|
||||||
|
PermissionIcon.Text = perm switch
|
||||||
|
{
|
||||||
|
"AcceptEdits" => "\uE73E",
|
||||||
|
"Plan" => "\uE7C3",
|
||||||
|
"BypassPermissions" => "\uE7BA",
|
||||||
|
"Deny" => "\uE711",
|
||||||
|
_ => "\uE8D7",
|
||||||
|
};
|
||||||
|
if (BtnPermission != null)
|
||||||
|
{
|
||||||
|
var operationMode = OperationModePolicy.Normalize(_settings.Settings.OperationMode);
|
||||||
|
BtnPermission.ToolTip = $"{summary.Description}\n운영 모드: {operationMode}\n기본값 {PermissionModeCatalog.ToDisplayLabel(summary.DefaultMode)} · 예외 {summary.OverrideCount}개";
|
||||||
|
BtnPermission.Background = Brushes.Transparent;
|
||||||
|
BtnPermission.BorderThickness = new Thickness(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(_lastPermissionBannerMode, perm, StringComparison.OrdinalIgnoreCase))
|
||||||
|
_lastPermissionBannerMode = perm;
|
||||||
|
|
||||||
|
if (perm == PermissionModeCatalog.AcceptEdits)
|
||||||
|
{
|
||||||
|
var activeColor = new SolidColorBrush(Color.FromRgb(0x10, 0x7C, 0x10));
|
||||||
|
PermissionLabel.Foreground = activeColor;
|
||||||
|
PermissionIcon.Foreground = activeColor;
|
||||||
|
if (BtnPermission != null)
|
||||||
|
BtnPermission.BorderBrush = BrushFromHex("#86EFAC");
|
||||||
|
if (PermissionTopBanner != null)
|
||||||
|
{
|
||||||
|
PermissionTopBanner.BorderBrush = BrushFromHex("#86EFAC");
|
||||||
|
PermissionTopBannerIcon.Text = "\uE73E";
|
||||||
|
PermissionTopBannerIcon.Foreground = activeColor;
|
||||||
|
PermissionTopBannerTitle.Text = "현재 권한 모드 · 편집 자동 승인";
|
||||||
|
PermissionTopBannerTitle.Foreground = BrushFromHex("#166534");
|
||||||
|
PermissionTopBannerText.Text = "모든 파일 편집을 자동 승인합니다. 명령 실행은 계속 확인합니다.";
|
||||||
|
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (perm == PermissionModeCatalog.Deny)
|
||||||
|
{
|
||||||
|
var denyColor = new SolidColorBrush(Color.FromRgb(0x10, 0x7C, 0x10));
|
||||||
|
PermissionLabel.Foreground = denyColor;
|
||||||
|
PermissionIcon.Foreground = denyColor;
|
||||||
|
if (BtnPermission != null)
|
||||||
|
BtnPermission.BorderBrush = BrushFromHex("#86EFAC");
|
||||||
|
if (PermissionTopBanner != null)
|
||||||
|
{
|
||||||
|
PermissionTopBanner.BorderBrush = BrushFromHex("#86EFAC");
|
||||||
|
PermissionTopBannerIcon.Text = "\uE73E";
|
||||||
|
PermissionTopBannerIcon.Foreground = denyColor;
|
||||||
|
PermissionTopBannerTitle.Text = "현재 권한 모드 · 읽기 전용";
|
||||||
|
PermissionTopBannerTitle.Foreground = denyColor;
|
||||||
|
PermissionTopBannerText.Text = "파일 읽기만 허용하고 생성/수정/삭제는 차단합니다.";
|
||||||
|
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (perm == PermissionModeCatalog.BypassPermissions)
|
||||||
|
{
|
||||||
|
var autoColor = new SolidColorBrush(Color.FromRgb(0xC2, 0x41, 0x0C));
|
||||||
|
PermissionLabel.Foreground = autoColor;
|
||||||
|
PermissionIcon.Foreground = autoColor;
|
||||||
|
if (BtnPermission != null)
|
||||||
|
BtnPermission.BorderBrush = BrushFromHex("#FDBA74");
|
||||||
|
if (PermissionTopBanner != null)
|
||||||
|
{
|
||||||
|
PermissionTopBanner.BorderBrush = BrushFromHex("#FDBA74");
|
||||||
|
PermissionTopBannerIcon.Text = "\uE814";
|
||||||
|
PermissionTopBannerIcon.Foreground = autoColor;
|
||||||
|
PermissionTopBannerTitle.Text = "현재 권한 모드 · 권한 건너뛰기";
|
||||||
|
PermissionTopBannerTitle.Foreground = autoColor;
|
||||||
|
PermissionTopBannerText.Text = "파일 편집과 명령 실행까지 모두 자동 허용합니다. 민감한 작업 전에는 설정을 다시 확인하세요.";
|
||||||
|
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var defaultFg = BrushFromHex("#2563EB");
|
||||||
|
var iconFg = perm switch
|
||||||
|
{
|
||||||
|
"Plan" => new SolidColorBrush(Color.FromRgb(0x43, 0x38, 0xCA)),
|
||||||
|
_ => new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xEB)),
|
||||||
|
};
|
||||||
|
PermissionLabel.Foreground = defaultFg;
|
||||||
|
PermissionIcon.Foreground = iconFg;
|
||||||
|
if (BtnPermission != null)
|
||||||
|
BtnPermission.BorderBrush = perm == PermissionModeCatalog.Plan
|
||||||
|
? BrushFromHex("#C7D2FE")
|
||||||
|
: BrushFromHex("#BFDBFE");
|
||||||
|
if (PermissionTopBanner != null)
|
||||||
|
{
|
||||||
|
if (perm == PermissionModeCatalog.Plan)
|
||||||
|
{
|
||||||
|
PermissionTopBanner.BorderBrush = BrushFromHex("#C7D2FE");
|
||||||
|
PermissionTopBannerIcon.Text = "\uE7C3";
|
||||||
|
PermissionTopBannerIcon.Foreground = BrushFromHex("#4338CA");
|
||||||
|
PermissionTopBannerTitle.Text = "현재 권한 모드 · 계획 모드";
|
||||||
|
PermissionTopBannerTitle.Foreground = BrushFromHex("#4338CA");
|
||||||
|
PermissionTopBannerText.Text = "변경 전에 계획을 먼저 만들고 승인 흐름을 우선합니다.";
|
||||||
|
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
else if (perm == PermissionModeCatalog.Default)
|
||||||
|
{
|
||||||
|
PermissionTopBanner.BorderBrush = BrushFromHex("#BFDBFE");
|
||||||
|
PermissionTopBannerIcon.Text = "\uE8D7";
|
||||||
|
PermissionTopBannerIcon.Foreground = BrushFromHex("#1D4ED8");
|
||||||
|
PermissionTopBannerTitle.Text = "현재 권한 모드 · 권한 요청";
|
||||||
|
PermissionTopBannerTitle.Foreground = BrushFromHex("#1D4ED8");
|
||||||
|
PermissionTopBannerText.Text = "변경하기 전에 항상 확인합니다.";
|
||||||
|
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user