모델 프로파일 기반 Cowork/Code 루프와 진행 UX 고도화 반영
- 등록 모델 실행 프로파일을 검증 게이트, 문서 fallback, post-tool verification까지 확장 적용 - Cowork/Code 진행 카드에 계획/도구/검증/압축/폴백/재시도 단계 메타를 추가해 대기 상태 가시성 강화 - OpenAI/vLLM tool 요청에 병렬 도구 호출 힌트를 추가하고 회귀 프롬프트 문서를 프로파일 기준으로 전면 정리 - 검증: 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:
@@ -81,7 +81,8 @@ public partial class ChatWindow
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return;
|
||||
|
||||
if (_isStreaming && string.Equals(priority, "now", StringComparison.OrdinalIgnoreCase))
|
||||
// 현재 탭이 스트리밍 중일 때만 우선순위를 "next"로 낮춤 — 다른 탭 스트리밍은 무관
|
||||
if (_streamingTabs.Contains(_activeTab) && string.Equals(priority, "now", StringComparison.OrdinalIgnoreCase))
|
||||
priority = "next";
|
||||
|
||||
HideSlashChip(restoreText: false);
|
||||
@@ -97,19 +98,30 @@ public partial class ChatWindow
|
||||
if (queuedItem == null)
|
||||
return;
|
||||
|
||||
if (!_isStreaming && startImmediatelyWhenIdle)
|
||||
if (!_streamingTabs.Contains(_activeTab) && startImmediatelyWhenIdle)
|
||||
{
|
||||
StartNextQueuedDraftIfAny(queuedItem.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
var runningTab = _streamRunTab;
|
||||
var runningLabel = runningTab switch
|
||||
{
|
||||
"Cowork" => "코워크",
|
||||
"Code" => "코드",
|
||||
"Chat" => "채팅",
|
||||
_ => runningTab ?? "다른 탭",
|
||||
};
|
||||
var suffix = !string.IsNullOrEmpty(runningTab) && !string.Equals(runningTab, _activeTab, StringComparison.OrdinalIgnoreCase)
|
||||
? $" ({runningLabel} 실행 완료 후 자동 실행)"
|
||||
: " (실행 완료 후 자동 실행)";
|
||||
var toast = queuedItem.Kind switch
|
||||
{
|
||||
"command" => "명령이 대기열에 추가되었습니다.",
|
||||
"direct" => "직접 실행 요청이 대기열에 추가되었습니다.",
|
||||
"steering" => "조정 요청이 대기열에 추가되었습니다.",
|
||||
"followup" => "후속 작업이 대기열에 추가되었습니다.",
|
||||
_ => "메시지가 대기열에 추가되었습니다.",
|
||||
"command" => "명령이 대기열에 추가되었습니다." + suffix,
|
||||
"direct" => "직접 실행 요청이 대기열에 추가되었습니다." + suffix,
|
||||
"steering" => "조정 요청이 대기열에 추가되었습니다." + suffix,
|
||||
"followup" => "후속 작업이 대기열에 추가되었습니다." + suffix,
|
||||
_ => "메시지가 대기열에 추가되었습니다." + suffix,
|
||||
};
|
||||
ShowToast(toast);
|
||||
}
|
||||
@@ -135,16 +147,7 @@ public partial class ChatWindow
|
||||
RebuildDraftQueuePanel(items);
|
||||
}
|
||||
|
||||
private bool IsDraftQueueExpanded()
|
||||
=> _expandedDraftQueueTabs.Contains(_activeTab);
|
||||
|
||||
private void ToggleDraftQueueExpanded()
|
||||
{
|
||||
if (!_expandedDraftQueueTabs.Add(_activeTab))
|
||||
_expandedDraftQueueTabs.Remove(_activeTab);
|
||||
|
||||
RefreshDraftQueueUi();
|
||||
}
|
||||
// --- Queue panel (Codex style) ---
|
||||
|
||||
private void RebuildDraftQueuePanel(IReadOnlyList<DraftQueueItem> items)
|
||||
{
|
||||
@@ -154,6 +157,8 @@ public partial class ChatWindow
|
||||
DraftQueuePanel.Children.Clear();
|
||||
|
||||
var visibleItems = items
|
||||
.Where(x => !string.Equals(x.State, "completed", StringComparison.OrdinalIgnoreCase) // 완료 항목 제거
|
||||
&& !string.Equals(x.State, "running", StringComparison.OrdinalIgnoreCase)) // 수행 중인 항목은 채팅창에서 이미 표시됨
|
||||
.OrderBy(GetDraftStateRank)
|
||||
.ThenBy(GetDraftPriorityRank)
|
||||
.ThenBy(x => x.CreatedAt)
|
||||
@@ -165,206 +170,142 @@ public partial class ChatWindow
|
||||
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())
|
||||
|
||||
// 단일 통합 컨테이너
|
||||
var containerBorder = new Border
|
||||
{
|
||||
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,
|
||||
Background = TryFindResource("ItemBackground") as Brush ?? BrushFromHex("#1E1E2A"),
|
||||
BorderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#3A3A4A"),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(14),
|
||||
Padding = new Thickness(12, 10, 12, 10),
|
||||
CornerRadius = new CornerRadius(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 innerStack = new StackPanel();
|
||||
containerBorder.Child = innerStack;
|
||||
|
||||
var left = new StackPanel();
|
||||
left.Children.Add(new TextBlock
|
||||
for (int i = 0; i < visibleItems.Count; i++)
|
||||
{
|
||||
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);
|
||||
innerStack.Children.Add(CreateDraftQueueRow(visibleItems[i]));
|
||||
|
||||
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
|
||||
// 구분선 (마지막 항목 제외)
|
||||
if (i < visibleItems.Count - 1)
|
||||
{
|
||||
Text = $"추가 항목 {totalCount - items.Count}개",
|
||||
Margin = new Thickness(8, -2, 0, 8),
|
||||
FontSize = 10.5,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#7A7F87"),
|
||||
});
|
||||
innerStack.Children.Add(new Border
|
||||
{
|
||||
Height = 1,
|
||||
Background = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#2E2E3E"),
|
||||
Margin = new Thickness(10, 0, 10, 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
DraftQueuePanel.Children.Add(containerBorder);
|
||||
|
||||
// 실패 항목 정리 버튼 (실패만 — 완료는 자동 제거됨)
|
||||
var summary = _appState.GetDraftQueueSummary(_activeTab);
|
||||
if (summary.FailedCount > 0)
|
||||
{
|
||||
var failBtn = CreateQueueFooterButton($"실패 정리 ({summary.FailedCount})", ClearFailedDrafts);
|
||||
DraftQueuePanel.Children.Add(failBtn);
|
||||
}
|
||||
}
|
||||
|
||||
private UIElement CreateDraftQueueSummaryStrip(AppStateService.DraftQueueSummaryState summary, bool isExpanded)
|
||||
// --- Codex-style row ---
|
||||
|
||||
private Border CreateDraftQueueRow(DraftQueueItem item)
|
||||
{
|
||||
var root = new Grid
|
||||
var isRunning = string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase);
|
||||
var isFailed = string.Equals(item.State, "failed", StringComparison.OrdinalIgnoreCase);
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#E5E5EA");
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#8E8E9A");
|
||||
|
||||
var row = new Border
|
||||
{
|
||||
Margin = new Thickness(0, 0, 0, 8),
|
||||
Padding = new Thickness(10, 7, 8, 7),
|
||||
};
|
||||
root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
root.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var wrap = new WrapPanel();
|
||||
var grid = new Grid();
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // state icon
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); // text
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // actions
|
||||
row.Child = grid;
|
||||
|
||||
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"));
|
||||
// State icon
|
||||
var stateIcon = new TextBlock
|
||||
{
|
||||
Text = GetDraftStateIcon(item),
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 11,
|
||||
Foreground = GetDraftStateIconBrush(item),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
Width = 14,
|
||||
};
|
||||
Grid.SetColumn(stateIcon, 0);
|
||||
grid.Children.Add(stateIcon);
|
||||
|
||||
if (wrap.Children.Count == 0)
|
||||
wrap.Children.Add(CreateQueueSummaryPill("대기열", "0", "#F8FAFC", "#E2E8F0", "#475569"));
|
||||
// Message text
|
||||
var msgText = new TextBlock
|
||||
{
|
||||
Text = item.Text,
|
||||
FontSize = 12,
|
||||
Foreground = isFailed ? (TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#8E8E9A")) : primaryText,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
};
|
||||
Grid.SetColumn(msgText, 1);
|
||||
grid.Children.Add(msgText);
|
||||
|
||||
Grid.SetColumn(wrap, 0);
|
||||
root.Children.Add(wrap);
|
||||
// Right actions
|
||||
var actions = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(actions, 2);
|
||||
grid.Children.Add(actions);
|
||||
|
||||
var toggle = CreateDraftQueueActionButton(isExpanded ? "간단히" : "상세 보기", ToggleDraftQueueExpanded);
|
||||
toggle.Margin = new Thickness(10, 0, 0, 0);
|
||||
Grid.SetColumn(toggle, 1);
|
||||
root.Children.Add(toggle);
|
||||
if (!isRunning)
|
||||
{
|
||||
// Kind chip (↪ 조정 style)
|
||||
actions.Children.Add(CreateKindChip(item, secondaryText));
|
||||
}
|
||||
|
||||
return root;
|
||||
if (!isRunning && !isFailed)
|
||||
{
|
||||
// Run now button
|
||||
actions.Children.Add(CreateRowIconButton("\uE768", "지금 실행", () => QueueDraftForImmediateRun(item.Id)));
|
||||
}
|
||||
|
||||
if (!isRunning)
|
||||
{
|
||||
// Edit button
|
||||
actions.Children.Add(CreateRowIconButton("\uE70F", "편집", () => PopDraftToEditor(item.Id)));
|
||||
}
|
||||
|
||||
// Delete
|
||||
actions.Children.Add(CreateRowIconButton("\uE74D", isRunning ? "취소" : "삭제", () => RemoveDraftFromQueue(item.Id)));
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
private Border CreateQueueSummaryPill(string label, string value, string bgHex, string borderHex, string fgHex)
|
||||
// Kind chip on the right — "↪ 조정" style
|
||||
private Border CreateKindChip(DraftQueueItem item, Brush defaultForeground)
|
||||
{
|
||||
var (kindIcon, kindLabel) = GetDraftKindChipContent(item);
|
||||
var foreground = GetDraftKindChipColor(item);
|
||||
|
||||
return new Border
|
||||
{
|
||||
Background = BrushFromHex(bgHex),
|
||||
BorderBrush = BrushFromHex(borderHex),
|
||||
BorderBrush = foreground,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Padding = new Thickness(8, 3, 8, 3),
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
CornerRadius = new CornerRadius(5),
|
||||
Padding = new Thickness(5, 2, 6, 2),
|
||||
Margin = new Thickness(0, 0, 4, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
@@ -372,169 +313,108 @@ public partial class ChatWindow
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
Text = kindIcon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 10,
|
||||
Foreground = BrushFromHex(fgHex),
|
||||
Foreground = foreground,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 3, 0),
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = $" {value}",
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = BrushFromHex(fgHex),
|
||||
}
|
||||
Text = kindLabel,
|
||||
FontSize = 10.5,
|
||||
Foreground = foreground,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private TextBlock CreateDraftQueueSectionLabel(string text)
|
||||
// Row icon button — flat, no border
|
||||
private Button CreateRowIconButton(string icon, string tooltip, Action onClick)
|
||||
{
|
||||
return new TextBlock
|
||||
var btn = new Button
|
||||
{
|
||||
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
|
||||
Content = new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = foreground,
|
||||
}
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 11,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
},
|
||||
Width = 24,
|
||||
Height = 24,
|
||||
Padding = new Thickness(0),
|
||||
Background = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#8E8E9A"),
|
||||
Cursor = Cursors.Hand,
|
||||
ToolTip = tooltip,
|
||||
};
|
||||
btn.Click += (_, _) => onClick();
|
||||
return btn;
|
||||
}
|
||||
|
||||
private Button CreateDraftQueueActionButton(string label, Action onClick, Brush? background = null)
|
||||
// Footer button (failed clear)
|
||||
private Button CreateQueueFooterButton(string label, Action onClick)
|
||||
{
|
||||
var btn = new Button
|
||||
{
|
||||
Content = label,
|
||||
Margin = new Thickness(6, 0, 0, 0),
|
||||
Padding = new Thickness(10, 5, 10, 5),
|
||||
MinWidth = 48,
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
Padding = new Thickness(10, 4, 10, 4),
|
||||
FontSize = 11,
|
||||
Background = background ?? BrushFromHex("#EEF2FF"),
|
||||
BorderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#D4D4D8"),
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#3A3A4A"),
|
||||
BorderThickness = new Thickness(1),
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#111827"),
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#8E8E9A"),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
btn.Click += (_, _) => onClick();
|
||||
return btn;
|
||||
}
|
||||
|
||||
// Pop queued draft back into the InputBox for editing
|
||||
private void PopDraftToEditor(string draftId)
|
||||
{
|
||||
string? text = null;
|
||||
lock (_convLock)
|
||||
{
|
||||
var session = ChatSession;
|
||||
if (session != null)
|
||||
{
|
||||
var item = session.GetDraftQueueItems(_activeTab)
|
||||
.FirstOrDefault(x => string.Equals(x.Id, draftId, StringComparison.OrdinalIgnoreCase));
|
||||
if (item != null)
|
||||
{
|
||||
text = item.Text;
|
||||
if (session.RemoveDraft(_activeTab, draftId, _storage))
|
||||
_currentConversation = session.CurrentConversation ?? _currentConversation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (InputBox != null && text != null)
|
||||
{
|
||||
InputBox.Text = text;
|
||||
InputBox.CaretIndex = text.Length;
|
||||
InputBox.Focus();
|
||||
UpdateInputBoxHeight();
|
||||
}
|
||||
|
||||
RefreshDraftQueueUi();
|
||||
}
|
||||
|
||||
// --- Icon-only button (kept for compatibility) ---
|
||||
private Button CreateIconButton(string icon, string tooltip, Action onClick)
|
||||
=> CreateRowIconButton(icon, tooltip, onClick);
|
||||
|
||||
// --- Ranking helpers ---
|
||||
|
||||
private static int GetDraftStateRank(DraftQueueItem item)
|
||||
=> string.Equals(item.State, "running", StringComparison.OrdinalIgnoreCase) ? 0
|
||||
: IsDraftBlocked(item) ? 1
|
||||
@@ -550,52 +430,74 @@ public partial class ChatWindow
|
||||
_ => 2,
|
||||
};
|
||||
|
||||
private static string GetDraftPriorityLabel(string? priority)
|
||||
=> priority?.ToLowerInvariant() switch
|
||||
// --- State icon ---
|
||||
|
||||
private static string GetDraftStateIcon(DraftQueueItem item)
|
||||
{
|
||||
if (IsDraftBlocked(item)) return "\uE9F5"; // 시계
|
||||
return item.State?.ToLowerInvariant() switch
|
||||
{
|
||||
"now" => "지금",
|
||||
"later" => "나중",
|
||||
_ => "다음",
|
||||
"running" => "\uE895", // 회전 (재생 아이콘)
|
||||
"failed" => "\uE783", // 경고
|
||||
"completed" => "\uE73E", // 체크
|
||||
_ => "\uE76C", // 대기 점
|
||||
};
|
||||
}
|
||||
|
||||
private Brush GetDraftStateIconBrush(DraftQueueItem item)
|
||||
{
|
||||
if (IsDraftBlocked(item)) return BrushFromHex("#C2410C");
|
||||
return item.State?.ToLowerInvariant() switch
|
||||
{
|
||||
"running" => TryFindResource("AccentColor") as Brush ?? BrushFromHex("#5B8AF5"),
|
||||
"failed" => BrushFromHex("#DC2626"),
|
||||
"completed" => BrushFromHex("#16A34A"),
|
||||
_ => TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#8E8E9A"),
|
||||
};
|
||||
}
|
||||
|
||||
// --- Kind chip ---
|
||||
|
||||
private static (string Icon, string Label) GetDraftKindChipContent(DraftQueueItem item)
|
||||
=> item.Kind?.ToLowerInvariant() switch
|
||||
{
|
||||
"followup" => ("\uE8A5", "후속"),
|
||||
"steering" => ("\uE7C3", "조정"),
|
||||
"command" => ("\uE756", "명령"),
|
||||
"direct" => ("\uE8A7", "직접"),
|
||||
_ => ("\uE8BD", "메시지"),
|
||||
};
|
||||
|
||||
private Brush GetDraftKindChipColor(DraftQueueItem item)
|
||||
=> item.Kind?.ToLowerInvariant() switch
|
||||
{
|
||||
"followup" => BrushFromHex("#0F766E"),
|
||||
"steering" => BrushFromHex("#B45309"),
|
||||
"command" => BrushFromHex("#7C3AED"),
|
||||
"direct" => BrushFromHex("#2563EB"),
|
||||
_ => TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#6B7280"),
|
||||
};
|
||||
|
||||
// --- Legacy helpers (used by other partial classes) ---
|
||||
|
||||
private static string GetDraftKindLabel(DraftQueueItem item)
|
||||
=> item.Kind?.ToLowerInvariant() switch
|
||||
{
|
||||
"followup" => "후속 작업",
|
||||
"followup" => "후속",
|
||||
"steering" => "조정",
|
||||
"command" => "명령",
|
||||
"direct" => "직접 실행",
|
||||
"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" => "실패",
|
||||
"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)
|
||||
@@ -603,22 +505,17 @@ public partial class ChatWindow
|
||||
? (BrushFromHex("#FFF7ED"), BrushFromHex("#FDBA74"), BrushFromHex("#C2410C"))
|
||||
: item.State?.ToLowerInvariant() switch
|
||||
{
|
||||
"running" => (BrushFromHex("#EFF6FF"), BrushFromHex("#BFDBFE"), BrushFromHex("#1D4ED8")),
|
||||
"failed" => (BrushFromHex("#FEF2F2"), BrushFromHex("#FECACA"), BrushFromHex("#991B1B")),
|
||||
"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")),
|
||||
_ => (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 Button CreateDraftQueueActionButton(string label, Action onClick, Brush? background = null)
|
||||
=> CreateQueueFooterButton(label, onClick);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user