AX Agent composer·대기열 렌더 구조 분리 및 문서 갱신
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow.ComposerQueuePresentation.cs를 추가해 입력창 높이 계산, draft kind 해석, 대기열 요약/카드/배지/액션 버튼 생성 책임을 메인 창 코드에서 분리함 - ChatWindow.xaml.cs는 대기열 실행 orchestration과 세션 변경 흐름 중심으로 정리해 claw-code 기준 footer/composer 품질 개선 기반을 강화함 - README.md와 docs/DEVELOPMENT.md에 2026-04-06 08:28 (KST) 기준 변경 이력을 반영함 - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 기준 경고 0 / 오류 0 확인
This commit is contained in:
@@ -7,6 +7,10 @@ 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)
|
- 업데이트: 2026-04-06 08:12 (KST)
|
||||||
- AX Agent 하단 작업 바 관련 presentation 메서드를 메인 창 코드에서 더 분리했습니다. `ChatWindow.FooterPresentation.cs`를 추가해 폴더 바, 선택된 프리셋 안내, Git 브랜치 버튼/팝업 렌더, 요약 pill 생성 책임을 별도 partial로 옮겼습니다.
|
- AX Agent 하단 작업 바 관련 presentation 메서드를 메인 창 코드에서 더 분리했습니다. `ChatWindow.FooterPresentation.cs`를 추가해 폴더 바, 선택된 프리셋 안내, Git 브랜치 버튼/팝업 렌더, 요약 pill 생성 책임을 별도 partial로 옮겼습니다.
|
||||||
- `ChatWindow.xaml.cs`는 대화 흐름과 런타임 orchestration 중심으로 더 정리했고, claw-code 기준으로 footer/preset/Git popup 품질 작업을 계속 이어가기 쉬운 구조를 만들었습니다.
|
- `ChatWindow.xaml.cs`는 대화 흐름과 런타임 orchestration 중심으로 더 정리했고, claw-code 기준으로 footer/preset/Git popup 품질 작업을 계속 이어가기 쉬운 구조를 만들었습니다.
|
||||||
|
|||||||
@@ -4893,3 +4893,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
|
|||||||
- 이벤트 배너 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) - 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: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;
|
||||||
|
}
|
||||||
@@ -4989,36 +4989,6 @@ public partial class ChatWindow : Window
|
|||||||
ScheduleInputUiRefresh();
|
ScheduleInputUiRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
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 static void SyncLatestAssistantMessage(ChatConversation conv, string content)
|
private static void SyncLatestAssistantMessage(ChatConversation conv, string content)
|
||||||
{
|
{
|
||||||
if (conv.Messages.Count == 0)
|
if (conv.Messages.Count == 0)
|
||||||
@@ -17778,78 +17748,6 @@ public partial class ChatWindow : Window
|
|||||||
return text.Length <= max ? text : text[..max] + "…";
|
return text.Length <= max ? text : text[..max] + "…";
|
||||||
}
|
}
|
||||||
|
|
||||||
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 DraftQueueItem? EnqueueDraftRequest(string text, string priority, string? explicitKind = null)
|
private DraftQueueItem? EnqueueDraftRequest(string text, string priority, string? explicitKind = null)
|
||||||
{
|
{
|
||||||
DraftQueueItem? queuedItem = null;
|
DraftQueueItem? queuedItem = null;
|
||||||
@@ -17907,428 +17805,6 @@ public partial class ChatWindow : Window
|
|||||||
RefreshDraftQueueUi();
|
RefreshDraftQueueUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
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 summary = _appState.GetDraftQueueSummary(_activeTab);
|
|
||||||
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 void QueueDraftForImmediateRun(string draftId)
|
private void QueueDraftForImmediateRun(string draftId)
|
||||||
{
|
{
|
||||||
if (_isStreaming)
|
if (_isStreaming)
|
||||||
@@ -18413,93 +17889,6 @@ public partial class ChatWindow : Window
|
|||||||
_ = SendMessageAsync(next.Text);
|
_ = SendMessageAsync(next.Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
private void OpenCommandSkillBrowser(string seedInput)
|
private void OpenCommandSkillBrowser(string seedInput)
|
||||||
{
|
{
|
||||||
if (InputBox == null)
|
if (InputBox == null)
|
||||||
|
|||||||
Reference in New Issue
Block a user