AX Agent transcript 렌더 구조를 분리하고 권한/도구 결과 표시 체계를 정리한다
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow.xaml.cs에 몰려 있던 의견 요청, 계획 승인, 작업 요약 렌더를 partial 파일로 분리해 transcript 책임을 낮췄다. - PermissionRequestPresentationCatalog와 ToolResultPresentationCatalog를 추가해 권한 요청 및 도구 결과 badge를 타입별로 해석하도록 정리했다. - AppStateService에 OperationalStatusPresentationState를 추가하고 상태선 계산을 presentation 계층으로 한 번 더 분리했다. - README.md, docs/DEVELOPMENT.md, docs/claw-code-parity-plan.md에 2026-04-06 00:58 (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:
@@ -1113,3 +1113,7 @@ MIT License
|
|||||||
- 업데이트: 2026-04-05 22:40 (KST)
|
- 업데이트: 2026-04-05 22:40 (KST)
|
||||||
- AX Agent 테마를 다시 점검해 기존 `Claw / Codex / Slate` 외에 `Nord`, `Ember` 2종을 추가했다. `Nord`는 차분한 블루그레이 업무형 톤, `Ember`는 따뜻한 앰버 문서 작업 톤으로 구성했다.
|
- AX Agent 테마를 다시 점검해 기존 `Claw / Codex / Slate` 외에 `Nord`, `Ember` 2종을 추가했다. `Nord`는 차분한 블루그레이 업무형 톤, `Ember`는 따뜻한 앰버 문서 작업 톤으로 구성했다.
|
||||||
- 내부 설정 `테마 스타일` 카드에서도 새 프리셋을 바로 선택할 수 있게 연결했고, `system / light / dark` 모드 조합으로 같은 방식으로 적용되도록 정리했다.
|
- 내부 설정 `테마 스타일` 카드에서도 새 프리셋을 바로 선택할 수 있게 연결했고, `system / light / dark` 모드 조합으로 같은 방식으로 적용되도록 정리했다.
|
||||||
|
- 업데이트: 2026-04-06 00:58 (KST)
|
||||||
|
- AX Agent transcript 품질 향상을 위해 렌더 책임을 실제로 분리했다. [ChatWindow.InlineInteractions.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.InlineInteractions.cs), [ChatWindow.TaskSummary.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TaskSummary.cs)를 추가해 `의견 요청`, `계획 승인`, `작업 요약` UI 로직을 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에서 분리했다.
|
||||||
|
- [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)를 추가해 권한 요청과 도구 결과를 `명령/네트워크/파일`, `성공/실패/거부/취소` 기준으로 나눠 transcript badge에 재사용하도록 정리했다.
|
||||||
|
- [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs)에 `OperationalStatusPresentationState`와 `GetOperationalStatusPresentation(...)`을 추가해 status/runtime summary 계산을 전용 요약 모델로 한 번 더 계층화했다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 상태선 갱신은 이제 이 presentation summary를 소비한다.
|
||||||
|
|||||||
@@ -4874,3 +4874,8 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
|
|||||||
- 업데이트: 2026-04-05 22:40 (KST)
|
- 업데이트: 2026-04-05 22:40 (KST)
|
||||||
- AX Agent 테마 리소스 구조를 다시 점검한 뒤 `Claw / Codex / Slate` 외에 `Nord`, `Ember` 프리셋을 추가했다. 새 리소스 파일은 [AgentNordLight.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentNordLight.xaml), [AgentNordDark.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentNordDark.xaml), [AgentNordSystem.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentNordSystem.xaml), [AgentEmberLight.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentEmberLight.xaml), [AgentEmberDark.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentEmberDark.xaml), [AgentEmberSystem.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentEmberSystem.xaml) 이다.
|
- AX Agent 테마 리소스 구조를 다시 점검한 뒤 `Claw / Codex / Slate` 외에 `Nord`, `Ember` 프리셋을 추가했다. 새 리소스 파일은 [AgentNordLight.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentNordLight.xaml), [AgentNordDark.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentNordDark.xaml), [AgentNordSystem.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentNordSystem.xaml), [AgentEmberLight.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentEmberLight.xaml), [AgentEmberDark.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentEmberDark.xaml), [AgentEmberSystem.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Themes/AgentEmberSystem.xaml) 이다.
|
||||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `BuildAgentThemeDictionaryUri()`, `RefreshOverlayThemeCards()`에 새 프리셋 분기를 추가했고, [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml)의 `테마 스타일` 선택 카드에도 `Nord`, `Ember`를 노출했다.
|
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `BuildAgentThemeDictionaryUri()`, `RefreshOverlayThemeCards()`에 새 프리셋 분기를 추가했고, [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml)의 `테마 스타일` 선택 카드에도 `Nord`, `Ember`를 노출했다.
|
||||||
|
- 업데이트: 2026-04-06 00:58 (KST)
|
||||||
|
- transcript renderer 분리 1차를 반영했다. [ChatWindow.InlineInteractions.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.InlineInteractions.cs)에 `ShowInlineUserAskAsync`, `CreatePlanDecisionCallback`, `ShowPlanButton`, `EnsurePlanViewerWindow` 계열을 옮기고, [ChatWindow.TaskSummary.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TaskSummary.cs)에 `ShowTaskSummaryPopup`, `BuildTaskSummaryCard`를 옮겨 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 transcript 책임을 줄였다.
|
||||||
|
- 권한/도구 결과 presentation catalog를 추가했다. [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)는 `success/error/reject/cancel` 기준의 도구 결과 badge 메타를 제공한다.
|
||||||
|
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AddAgentEventBanner(...)`는 이제 이 catalog를 사용해 권한 요청과 도구 결과 badge를 결정한다.
|
||||||
|
- [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs)에 `OperationalStatusPresentationState`와 `GetOperationalStatusPresentation(...)`을 추가해 status line/runtime summary 계산을 presentation layer로 분리했다. `UpdateTaskSummaryIndicators()`는 presentation summary만 소비하도록 바뀌었다.
|
||||||
|
|||||||
@@ -407,3 +407,15 @@
|
|||||||
- `BuildTopicButtons()` rebuild frequency
|
- `BuildTopicButtons()` rebuild frequency
|
||||||
- `OnAgentEvent` timeline churn during long Cowork/Code runs
|
- `OnAgentEvent` timeline churn during long Cowork/Code runs
|
||||||
- compact queue summary still needs one more pass to fully match `claw-code` footer minimalism
|
- compact queue summary still needs one more pass to fully match `claw-code` footer minimalism
|
||||||
|
|
||||||
|
## Progress Notes
|
||||||
|
- 업데이트: 2026-04-06 00:58 (KST)
|
||||||
|
- transcript renderer 분리 1차 완료
|
||||||
|
- AX 적용: [ChatWindow.InlineInteractions.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.InlineInteractions.cs), [ChatWindow.TaskSummary.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TaskSummary.cs)
|
||||||
|
- 완료 조건: `plan / ask / task-summary` 렌더 helper가 메인 `ChatWindow.xaml.cs` 밖으로 이동
|
||||||
|
- permission / tool-result presentation catalog 도입
|
||||||
|
- AX 적용: [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)
|
||||||
|
- 완료 조건: `AddAgentEventBanner(...)`가 권한/도구 결과 badge 메타를 inline switch가 아니라 catalog에서 해석
|
||||||
|
- runtime summary 전용 계층 1차 반영
|
||||||
|
- AX 적용: [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs)
|
||||||
|
- 완료 조건: 상태선 UI가 `OperationalStatusPresentationState`를 소비해 strip/runtime badge visibility를 계산
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
namespace AxCopilot.Services.Agent;
|
||||||
|
|
||||||
|
internal sealed record PermissionRequestPresentation(
|
||||||
|
string Icon,
|
||||||
|
string Label,
|
||||||
|
string BackgroundHex,
|
||||||
|
string ForegroundHex);
|
||||||
|
|
||||||
|
internal static class PermissionRequestPresentationCatalog
|
||||||
|
{
|
||||||
|
public static PermissionRequestPresentation Resolve(string? toolName, bool pending)
|
||||||
|
{
|
||||||
|
var tool = toolName?.Trim().ToLowerInvariant() ?? "";
|
||||||
|
|
||||||
|
if (tool.Contains("process") || tool.Contains("bash") || tool.Contains("powershell"))
|
||||||
|
{
|
||||||
|
return pending
|
||||||
|
? new PermissionRequestPresentation("\uE756", "명령 권한 요청", "#FEF2F2", "#DC2626")
|
||||||
|
: new PermissionRequestPresentation("\uE73E", "명령 권한 허용", "#ECFDF5", "#059669");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tool.Contains("web") || tool.Contains("fetch") || tool.Contains("http"))
|
||||||
|
{
|
||||||
|
return pending
|
||||||
|
? new PermissionRequestPresentation("\uE774", "네트워크 권한 요청", "#FFF7ED", "#C2410C")
|
||||||
|
: new PermissionRequestPresentation("\uE73E", "네트워크 권한 허용", "#ECFDF5", "#059669");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tool.Contains("file"))
|
||||||
|
{
|
||||||
|
return pending
|
||||||
|
? new PermissionRequestPresentation("\uE8A5", "파일 권한 요청", "#FFF7ED", "#C2410C")
|
||||||
|
: new PermissionRequestPresentation("\uE73E", "파일 권한 허용", "#ECFDF5", "#059669");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pending
|
||||||
|
? new PermissionRequestPresentation("\uE897", "권한 요청", "#FFF7ED", "#C2410C")
|
||||||
|
: new PermissionRequestPresentation("\uE73E", "권한 허용", "#ECFDF5", "#059669");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
namespace AxCopilot.Services.Agent;
|
||||||
|
|
||||||
|
internal sealed record ToolResultPresentation(
|
||||||
|
string Icon,
|
||||||
|
string Label,
|
||||||
|
string BackgroundHex,
|
||||||
|
string ForegroundHex,
|
||||||
|
string StatusKind);
|
||||||
|
|
||||||
|
internal static class ToolResultPresentationCatalog
|
||||||
|
{
|
||||||
|
public static ToolResultPresentation Resolve(AgentEvent evt, string fallbackLabel)
|
||||||
|
{
|
||||||
|
var summary = evt.Summary?.Trim() ?? "";
|
||||||
|
|
||||||
|
if (summary.Contains("취소", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
summary.Contains("중단", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
evt.Type == AgentEventType.StopRequested)
|
||||||
|
{
|
||||||
|
return new ToolResultPresentation("\uE711", "도구 취소", "#F8FAFC", "#475569", "cancel");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summary.Contains("거부", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
summary.Contains("반려", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
summary.Contains("권한 거부", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return new ToolResultPresentation("\uE783", "도구 거부", "#FEF2F2", "#DC2626", "reject");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!evt.Success || evt.Type == AgentEventType.Error)
|
||||||
|
{
|
||||||
|
return new ToolResultPresentation("\uE783", "도구 실패", "#FEF2F2", "#DC2626", "error");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ToolResultPresentation("\uE73E", fallbackLabel, "#ECFDF5", "#16A34A", "success");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -158,6 +158,17 @@ public sealed class AppStateService
|
|||||||
public string StripText { get; init; } = "";
|
public string StripText { get; init; } = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class OperationalStatusPresentationState
|
||||||
|
{
|
||||||
|
public bool ShowRuntimeBadge { get; init; }
|
||||||
|
public string RuntimeLabel { get; init; } = "";
|
||||||
|
public bool ShowLastCompleted { get; init; }
|
||||||
|
public string LastCompletedText { get; init; } = "";
|
||||||
|
public bool ShowCompactStrip { get; init; }
|
||||||
|
public string StripKind { get; init; } = "none";
|
||||||
|
public string StripText { get; init; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
public ChatSessionStateService? ChatSession { get; private set; }
|
public ChatSessionStateService? ChatSession { get; private set; }
|
||||||
public SkillCatalogState Skills { get; } = new();
|
public SkillCatalogState Skills { get; } = new();
|
||||||
public McpCatalogState Mcp { get; } = new();
|
public McpCatalogState Mcp { get; } = new();
|
||||||
@@ -620,6 +631,26 @@ public sealed class AppStateService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OperationalStatusPresentationState GetOperationalStatusPresentation(string tab, bool hasLiveRuntimeActivity)
|
||||||
|
{
|
||||||
|
var status = GetOperationalStatus(tab);
|
||||||
|
var showCompactStrip = !string.Equals(tab, "Chat", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& (string.Equals(status.StripKind, "permission_waiting", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(status.StripKind, "failed_run", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(status.StripKind, "permission_denied", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
return new OperationalStatusPresentationState
|
||||||
|
{
|
||||||
|
ShowRuntimeBadge = status.ShowRuntimeBadge && hasLiveRuntimeActivity,
|
||||||
|
RuntimeLabel = status.RuntimeLabel,
|
||||||
|
ShowLastCompleted = status.ShowLastCompleted,
|
||||||
|
LastCompletedText = status.LastCompletedText,
|
||||||
|
ShowCompactStrip = showCompactStrip,
|
||||||
|
StripKind = showCompactStrip ? status.StripKind : "none",
|
||||||
|
StripText = showCompactStrip ? status.StripText : "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public IReadOnlyList<DraftQueueItem> GetDraftQueueItems(string tab)
|
public IReadOnlyList<DraftQueueItem> GetDraftQueueItems(string tab)
|
||||||
=> ChatSession?.GetDraftQueueItems(tab) ?? Array.Empty<DraftQueueItem>();
|
=> ChatSession?.GetDraftQueueItems(tab) ?? Array.Empty<DraftQueueItem>();
|
||||||
|
|
||||||
|
|||||||
397
src/AxCopilot/Views/ChatWindow.InlineInteractions.cs
Normal file
397
src/AxCopilot/Views/ChatWindow.InlineInteractions.cs
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Animation;
|
||||||
|
using AxCopilot.Services.Agent;
|
||||||
|
|
||||||
|
namespace AxCopilot.Views;
|
||||||
|
|
||||||
|
public partial class ChatWindow
|
||||||
|
{
|
||||||
|
private async Task<string?> ShowInlineUserAskAsync(string question, List<string> options, string defaultValue)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<string?>();
|
||||||
|
|
||||||
|
await Dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
AddUserAskCard(question, options, defaultValue, tcs);
|
||||||
|
});
|
||||||
|
|
||||||
|
var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromMinutes(5)));
|
||||||
|
if (completed != tcs.Task)
|
||||||
|
{
|
||||||
|
await Dispatcher.InvokeAsync(RemoveUserAskCard);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await tcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveUserAskCard()
|
||||||
|
{
|
||||||
|
if (_userAskCard == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
MessagePanel.Children.Remove(_userAskCard);
|
||||||
|
_userAskCard = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddUserAskCard(string question, List<string> options, string defaultValue, TaskCompletionSource<string?> tcs)
|
||||||
|
{
|
||||||
|
RemoveUserAskCard();
|
||||||
|
|
||||||
|
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||||
|
var accentColor = ((SolidColorBrush)accentBrush).Color;
|
||||||
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||||
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||||
|
var borderBrush = TryFindResource("BorderColor") as Brush
|
||||||
|
?? new SolidColorBrush(Color.FromArgb(0x24, accentColor.R, accentColor.G, accentColor.B));
|
||||||
|
var itemBg = TryFindResource("ItemBackground") as Brush
|
||||||
|
?? new SolidColorBrush(Color.FromArgb(0x10, accentColor.R, accentColor.G, accentColor.B));
|
||||||
|
var hoverBg = TryFindResource("ItemHoverBackground") as Brush
|
||||||
|
?? new SolidColorBrush(Color.FromArgb(0x16, 0xFF, 0xFF, 0xFF));
|
||||||
|
var okBrush = BrushFromHex("#10B981");
|
||||||
|
var dangerBrush = BrushFromHex("#EF4444");
|
||||||
|
|
||||||
|
var container = new Border
|
||||||
|
{
|
||||||
|
Margin = new Thickness(40, 4, 90, 8),
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Left,
|
||||||
|
MaxWidth = Math.Max(420, GetMessageMaxWidth() - 36),
|
||||||
|
Background = itemBg,
|
||||||
|
BorderBrush = borderBrush,
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(14),
|
||||||
|
Padding = new Thickness(14, 12, 14, 12),
|
||||||
|
};
|
||||||
|
|
||||||
|
var outer = new StackPanel();
|
||||||
|
outer.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "의견 요청",
|
||||||
|
FontSize = 12.5,
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = primaryText,
|
||||||
|
});
|
||||||
|
outer.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = question,
|
||||||
|
Margin = new Thickness(0, 4, 0, 10),
|
||||||
|
FontSize = 12.5,
|
||||||
|
Foreground = primaryText,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
LineHeight = 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
Border? selectedOption = null;
|
||||||
|
string selectedResponse = defaultValue;
|
||||||
|
|
||||||
|
if (options.Count > 0)
|
||||||
|
{
|
||||||
|
var optionPanel = new WrapPanel
|
||||||
|
{
|
||||||
|
Margin = new Thickness(0, 0, 0, 10),
|
||||||
|
ItemWidth = double.NaN,
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var option in options.Where(static option => !string.IsNullOrWhiteSpace(option)))
|
||||||
|
{
|
||||||
|
var optionLabel = option.Trim();
|
||||||
|
var optBorder = new Border
|
||||||
|
{
|
||||||
|
Background = Brushes.Transparent,
|
||||||
|
BorderBrush = borderBrush,
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(999),
|
||||||
|
Padding = new Thickness(10, 6, 10, 6),
|
||||||
|
Margin = new Thickness(0, 0, 8, 8),
|
||||||
|
Cursor = Cursors.Hand,
|
||||||
|
Child = new TextBlock
|
||||||
|
{
|
||||||
|
Text = optionLabel,
|
||||||
|
FontSize = 12,
|
||||||
|
Foreground = primaryText,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
optBorder.MouseEnter += (s, _) =>
|
||||||
|
{
|
||||||
|
if (!ReferenceEquals(selectedOption, s))
|
||||||
|
((Border)s).Background = hoverBg;
|
||||||
|
};
|
||||||
|
optBorder.MouseLeave += (s, _) =>
|
||||||
|
{
|
||||||
|
if (!ReferenceEquals(selectedOption, s))
|
||||||
|
((Border)s).Background = Brushes.Transparent;
|
||||||
|
};
|
||||||
|
optBorder.MouseLeftButtonUp += (_, _) =>
|
||||||
|
{
|
||||||
|
if (selectedOption != null)
|
||||||
|
{
|
||||||
|
selectedOption.Background = Brushes.Transparent;
|
||||||
|
selectedOption.BorderBrush = borderBrush;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedOption = optBorder;
|
||||||
|
selectedOption.Background = new SolidColorBrush(Color.FromArgb(0x18, accentColor.R, accentColor.G, accentColor.B));
|
||||||
|
selectedOption.BorderBrush = accentBrush;
|
||||||
|
selectedResponse = optionLabel;
|
||||||
|
};
|
||||||
|
|
||||||
|
optionPanel.Children.Add(optBorder);
|
||||||
|
}
|
||||||
|
|
||||||
|
outer.Children.Add(optionPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
outer.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "직접 입력",
|
||||||
|
FontSize = 11.5,
|
||||||
|
Foreground = secondaryText,
|
||||||
|
Margin = new Thickness(0, 0, 0, 6),
|
||||||
|
});
|
||||||
|
|
||||||
|
var inputBox = new TextBox
|
||||||
|
{
|
||||||
|
Text = defaultValue,
|
||||||
|
AcceptsReturn = true,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
MinHeight = 42,
|
||||||
|
MaxHeight = 100,
|
||||||
|
FontSize = 12.5,
|
||||||
|
Padding = new Thickness(10, 8, 10, 8),
|
||||||
|
Background = Brushes.Transparent,
|
||||||
|
Foreground = primaryText,
|
||||||
|
CaretBrush = primaryText,
|
||||||
|
BorderBrush = borderBrush,
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
};
|
||||||
|
inputBox.TextChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(inputBox.Text))
|
||||||
|
{
|
||||||
|
selectedResponse = inputBox.Text.Trim();
|
||||||
|
if (selectedOption != null)
|
||||||
|
{
|
||||||
|
selectedOption.Background = Brushes.Transparent;
|
||||||
|
selectedOption.BorderBrush = borderBrush;
|
||||||
|
selectedOption = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
outer.Children.Add(inputBox);
|
||||||
|
|
||||||
|
var buttonRow = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Right,
|
||||||
|
Margin = new Thickness(0, 12, 0, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
Border BuildActionButton(string label, Brush bg, Brush fg)
|
||||||
|
{
|
||||||
|
return new Border
|
||||||
|
{
|
||||||
|
Background = bg,
|
||||||
|
BorderBrush = bg,
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(999),
|
||||||
|
Padding = new Thickness(12, 7, 12, 7),
|
||||||
|
Margin = new Thickness(8, 0, 0, 0),
|
||||||
|
Cursor = Cursors.Hand,
|
||||||
|
Child = new TextBlock
|
||||||
|
{
|
||||||
|
Text = label,
|
||||||
|
FontSize = 12,
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = fg,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var cancelBtn = BuildActionButton("취소", Brushes.Transparent, dangerBrush);
|
||||||
|
cancelBtn.BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, 0xEF, 0x44, 0x44));
|
||||||
|
cancelBtn.MouseLeftButtonUp += (_, _) =>
|
||||||
|
{
|
||||||
|
RemoveUserAskCard();
|
||||||
|
tcs.TrySetResult(null);
|
||||||
|
};
|
||||||
|
buttonRow.Children.Add(cancelBtn);
|
||||||
|
|
||||||
|
var submitBtn = BuildActionButton("전달", okBrush, Brushes.White);
|
||||||
|
submitBtn.MouseLeftButtonUp += (_, _) =>
|
||||||
|
{
|
||||||
|
var finalResponse = !string.IsNullOrWhiteSpace(inputBox.Text)
|
||||||
|
? inputBox.Text.Trim()
|
||||||
|
: selectedResponse?.Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(finalResponse))
|
||||||
|
finalResponse = defaultValue?.Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(finalResponse))
|
||||||
|
return;
|
||||||
|
|
||||||
|
RemoveUserAskCard();
|
||||||
|
tcs.TrySetResult(finalResponse);
|
||||||
|
};
|
||||||
|
buttonRow.Children.Add(submitBtn);
|
||||||
|
|
||||||
|
outer.Children.Add(buttonRow);
|
||||||
|
container.Child = outer;
|
||||||
|
_userAskCard = container;
|
||||||
|
|
||||||
|
container.Opacity = 0;
|
||||||
|
container.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(180)));
|
||||||
|
MessagePanel.Children.Add(container);
|
||||||
|
ForceScrollToEnd();
|
||||||
|
inputBox.Focus();
|
||||||
|
inputBox.CaretIndex = inputBox.Text.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Func<string, List<string>, Task<string?>> CreatePlanDecisionCallback()
|
||||||
|
{
|
||||||
|
return async (planSummary, options) =>
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<string?>();
|
||||||
|
var steps = TaskDecomposer.ExtractSteps(planSummary);
|
||||||
|
|
||||||
|
await Dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
EnsurePlanViewerWindow();
|
||||||
|
_planViewerWindow?.LoadPlan(planSummary, steps, tcs);
|
||||||
|
ShowPlanButton(true);
|
||||||
|
AddDecisionButtons(tcs, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromMinutes(5)));
|
||||||
|
if (completed != tcs.Task)
|
||||||
|
{
|
||||||
|
await Dispatcher.InvokeAsync(() => _planViewerWindow?.Hide());
|
||||||
|
return "취소";
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await tcs.Task;
|
||||||
|
var agentDecision = result;
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
agentDecision = _planViewerWindow?.BuildApprovedDecisionPayload(AgentLoopService.ApprovedPlanDecisionPrefix);
|
||||||
|
}
|
||||||
|
else if (!string.Equals(result, "취소", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !string.Equals(result, "승인", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !string.IsNullOrWhiteSpace(result))
|
||||||
|
{
|
||||||
|
agentDecision = $"수정 요청: {result.Trim()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
await Dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
_planViewerWindow?.SwitchToExecutionMode();
|
||||||
|
_planViewerWindow?.Hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Dispatcher.InvokeAsync(() => _planViewerWindow?.Hide());
|
||||||
|
}
|
||||||
|
|
||||||
|
return agentDecision;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsurePlanViewerWindow()
|
||||||
|
{
|
||||||
|
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_planViewerWindow = new PlanViewerWindow(this);
|
||||||
|
_planViewerWindow.Closing += (_, e) =>
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
_planViewerWindow.Hide();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowPlanButton(bool show)
|
||||||
|
{
|
||||||
|
if (!show)
|
||||||
|
{
|
||||||
|
for (int i = MoodIconPanel.Children.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (MoodIconPanel.Children[i] is Border b && b.Tag?.ToString() == "PlanBtn")
|
||||||
|
{
|
||||||
|
if (i > 0 && MoodIconPanel.Children[i - 1] is Border sep && sep.Tag?.ToString() == "PlanSep")
|
||||||
|
MoodIconPanel.Children.RemoveAt(i - 1);
|
||||||
|
if (i < MoodIconPanel.Children.Count)
|
||||||
|
MoodIconPanel.Children.RemoveAt(Math.Min(i, MoodIconPanel.Children.Count - 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var child in MoodIconPanel.Children)
|
||||||
|
{
|
||||||
|
if (child is Border b && b.Tag?.ToString() == "PlanBtn")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var separator = new Border
|
||||||
|
{
|
||||||
|
Width = 1,
|
||||||
|
Height = 18,
|
||||||
|
Background = TryFindResource("SeparatorColor") as Brush ?? Brushes.Gray,
|
||||||
|
Margin = new Thickness(4, 0, 4, 0),
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Tag = "PlanSep",
|
||||||
|
};
|
||||||
|
MoodIconPanel.Children.Add(separator);
|
||||||
|
|
||||||
|
var planBtn = CreateFolderBarButton("\uE9D2", "계획", "실행 계획 보기", "#10B981");
|
||||||
|
planBtn.Tag = "PlanBtn";
|
||||||
|
planBtn.MouseLeftButtonUp += (_, e) =>
|
||||||
|
{
|
||||||
|
e.Handled = true;
|
||||||
|
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
|
||||||
|
{
|
||||||
|
_planViewerWindow.Show();
|
||||||
|
_planViewerWindow.Activate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
MoodIconPanel.Children.Add(planBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePlanViewerStep(AgentEvent evt)
|
||||||
|
{
|
||||||
|
if (_planViewerWindow == null || !IsWindowAlive(_planViewerWindow))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (evt.StepCurrent > 0)
|
||||||
|
_planViewerWindow.UpdateCurrentStep(evt.StepCurrent - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CompletePlanViewer()
|
||||||
|
{
|
||||||
|
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
|
||||||
|
_planViewerWindow.MarkComplete();
|
||||||
|
ShowPlanButton(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsWindowAlive(Window? w)
|
||||||
|
{
|
||||||
|
if (w == null)
|
||||||
|
return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var _ = w.IsVisible;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
358
src/AxCopilot/Views/ChatWindow.TaskSummary.cs
Normal file
358
src/AxCopilot/Views/ChatWindow.TaskSummary.cs
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using AxCopilot.Models;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
|
||||||
|
namespace AxCopilot.Views;
|
||||||
|
|
||||||
|
public partial class ChatWindow
|
||||||
|
{
|
||||||
|
private void RuntimeTaskSummary_Click(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
e.Handled = true;
|
||||||
|
_taskSummaryTarget = sender as UIElement ?? RuntimeActivityBadge;
|
||||||
|
ShowTaskSummaryPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowTaskSummaryPopup()
|
||||||
|
{
|
||||||
|
if (_taskSummaryTarget == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_taskSummaryPopup != null)
|
||||||
|
_taskSummaryPopup.IsOpen = false;
|
||||||
|
|
||||||
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
||||||
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray;
|
||||||
|
var popupBackground = TryFindResource("LauncherBackground") as Brush ?? Brushes.White;
|
||||||
|
var panel = new StackPanel { Margin = new Thickness(2) };
|
||||||
|
panel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "작업 요약",
|
||||||
|
FontSize = 11,
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = primaryText,
|
||||||
|
Margin = new Thickness(8, 5, 8, 2),
|
||||||
|
});
|
||||||
|
panel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "현재 상태 요약",
|
||||||
|
FontSize = 8.5,
|
||||||
|
Foreground = secondaryText,
|
||||||
|
Margin = new Thickness(8, 0, 8, 5),
|
||||||
|
});
|
||||||
|
|
||||||
|
ChatConversation? currentConversation;
|
||||||
|
lock (_convLock) currentConversation = _currentConversation;
|
||||||
|
AddTaskSummaryObservabilitySections(panel, currentConversation);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_appState.AgentRun.RunId))
|
||||||
|
{
|
||||||
|
var currentRun = new Border
|
||||||
|
{
|
||||||
|
Background = BrushFromHex("#F8FAFC"),
|
||||||
|
BorderBrush = BrushFromHex("#E2E8F0"),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(8),
|
||||||
|
Padding = new Thickness(8, 6, 8, 6),
|
||||||
|
Margin = new Thickness(6, 0, 6, 6),
|
||||||
|
Child = new StackPanel
|
||||||
|
{
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"실행 run {ShortRunId(_appState.AgentRun.RunId)}",
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = primaryText,
|
||||||
|
FontSize = 9.75,
|
||||||
|
},
|
||||||
|
new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"{GetRunStatusLabel(_appState.AgentRun.Status)} · step {_appState.AgentRun.LastIteration}",
|
||||||
|
Margin = new Thickness(0, 2, 0, 0),
|
||||||
|
Foreground = GetRunStatusBrush(_appState.AgentRun.Status),
|
||||||
|
FontSize = 9,
|
||||||
|
},
|
||||||
|
new TextBlock
|
||||||
|
{
|
||||||
|
Text = string.IsNullOrWhiteSpace(_appState.AgentRun.Summary) ? "요약 없음" : _appState.AgentRun.Summary,
|
||||||
|
Margin = new Thickness(0, 3, 0, 0),
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
Foreground = Brushes.DimGray,
|
||||||
|
FontSize = 9,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
panel.Children.Add(currentRun);
|
||||||
|
}
|
||||||
|
|
||||||
|
var recentAgentRuns = _appState.GetRecentAgentRuns(1);
|
||||||
|
if (recentAgentRuns.Count > 0)
|
||||||
|
{
|
||||||
|
panel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "마지막 실행",
|
||||||
|
FontSize = 9,
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = Brushes.DimGray,
|
||||||
|
Margin = new Thickness(8, 0, 8, 2),
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var run in recentAgentRuns)
|
||||||
|
{
|
||||||
|
var runEvents = GetExecutionEventsForRun(run.RunId, 1);
|
||||||
|
var runFilePaths = GetExecutionEventFilePaths(run.RunId, 1);
|
||||||
|
var runDisplay = _appState.GetRunDisplay(run);
|
||||||
|
var runCardStack = new StackPanel
|
||||||
|
{
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new TextBlock
|
||||||
|
{
|
||||||
|
Text = runDisplay.HeaderText,
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = GetRunStatusBrush(run.Status),
|
||||||
|
FontSize = 9.5,
|
||||||
|
},
|
||||||
|
new TextBlock
|
||||||
|
{
|
||||||
|
Text = runDisplay.MetaText,
|
||||||
|
Margin = new Thickness(0, 1, 0, 0),
|
||||||
|
Foreground = secondaryText,
|
||||||
|
FontSize = 8.25,
|
||||||
|
},
|
||||||
|
new TextBlock
|
||||||
|
{
|
||||||
|
Text = TruncateForStatus(runDisplay.SummaryText, 92),
|
||||||
|
Margin = new Thickness(0, 1.5, 0, 0),
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
Foreground = secondaryText,
|
||||||
|
FontSize = 8.5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (runEvents.Count > 0 || runFilePaths.Count > 0)
|
||||||
|
{
|
||||||
|
var activitySummary = new StackPanel();
|
||||||
|
activitySummary.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"로그 {runEvents.Count} · 파일 {runFilePaths.Count}",
|
||||||
|
FontSize = 8,
|
||||||
|
Foreground = secondaryText,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(run.RunId))
|
||||||
|
{
|
||||||
|
var capturedRunId = run.RunId;
|
||||||
|
var timelineButton = CreateTaskSummaryActionButton(
|
||||||
|
"타임라인",
|
||||||
|
"#F8FAFC",
|
||||||
|
"#CBD5E1",
|
||||||
|
"#334155",
|
||||||
|
(_, _) => ScrollToRunInTimeline(capturedRunId),
|
||||||
|
trailingMargin: false);
|
||||||
|
timelineButton.Margin = new Thickness(0, 5, 0, 0);
|
||||||
|
activitySummary.Children.Add(timelineButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
runCardStack.Children.Add(new Border
|
||||||
|
{
|
||||||
|
Background = BrushFromHex("#F8FAFC"),
|
||||||
|
BorderBrush = BrushFromHex("#E2E8F0"),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(7),
|
||||||
|
Padding = new Thickness(6, 4, 6, 4),
|
||||||
|
Margin = new Thickness(0, 5, 0, 0),
|
||||||
|
Child = activitySummary
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(run.Status, "completed", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var capturedRun = run;
|
||||||
|
var followUpButton = CreateTaskSummaryActionButton(
|
||||||
|
"후속 큐",
|
||||||
|
"#ECFDF5",
|
||||||
|
"#BBF7D0",
|
||||||
|
"#166534",
|
||||||
|
(_, _) => EnqueueFollowUpFromRun(capturedRun),
|
||||||
|
trailingMargin: false);
|
||||||
|
followUpButton.Margin = new Thickness(0, 6, 0, 0);
|
||||||
|
runCardStack.Children.Add(followUpButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(run.Status, "failed", StringComparison.OrdinalIgnoreCase) && CanRetryCurrentConversation())
|
||||||
|
{
|
||||||
|
var retryButton = CreateTaskSummaryActionButton(
|
||||||
|
"다시 시도",
|
||||||
|
"#FEF2F2",
|
||||||
|
"#FCA5A5",
|
||||||
|
"#991B1B",
|
||||||
|
(_, _) =>
|
||||||
|
{
|
||||||
|
_taskSummaryPopup?.SetCurrentValue(Popup.IsOpenProperty, false);
|
||||||
|
RetryLastUserMessageFromConversation();
|
||||||
|
},
|
||||||
|
trailingMargin: false);
|
||||||
|
retryButton.Margin = new Thickness(0, 6, 0, 0);
|
||||||
|
runCardStack.Children.Add(retryButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
panel.Children.Add(new Border
|
||||||
|
{
|
||||||
|
Background = popupBackground,
|
||||||
|
BorderBrush = BrushFromHex("#E5E7EB"),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(7),
|
||||||
|
Padding = new Thickness(7, 5, 7, 5),
|
||||||
|
Margin = new Thickness(6, 0, 6, 4),
|
||||||
|
Child = runCardStack
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeTasks = FilterTaskSummaryItems(_appState.ActiveTasks).Take(3).ToList();
|
||||||
|
var recentTasks = FilterTaskSummaryItems(_appState.RecentTasks).Take(2).ToList();
|
||||||
|
|
||||||
|
foreach (var task in activeTasks)
|
||||||
|
panel.Children.Add(BuildTaskSummaryCard(task, active: true));
|
||||||
|
|
||||||
|
if (ShouldIncludeRecentTaskSummary(activeTasks))
|
||||||
|
{
|
||||||
|
foreach (var task in recentTasks)
|
||||||
|
panel.Children.Add(BuildTaskSummaryCard(task, active: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTasks.Count == 0 && recentTasks.Count == 0)
|
||||||
|
{
|
||||||
|
panel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "표시할 작업 이력이 없습니다.",
|
||||||
|
Margin = new Thickness(10, 2, 10, 8),
|
||||||
|
Foreground = secondaryText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_taskSummaryPopup = new Popup
|
||||||
|
{
|
||||||
|
PlacementTarget = _taskSummaryTarget,
|
||||||
|
Placement = PlacementMode.Top,
|
||||||
|
AllowsTransparency = true,
|
||||||
|
StaysOpen = false,
|
||||||
|
PopupAnimation = PopupAnimation.Fade,
|
||||||
|
Child = new Border
|
||||||
|
{
|
||||||
|
Background = popupBackground,
|
||||||
|
BorderBrush = BrushFromHex("#E5E7EB"),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(12),
|
||||||
|
Padding = new Thickness(6),
|
||||||
|
Child = new ScrollViewer
|
||||||
|
{
|
||||||
|
Content = panel,
|
||||||
|
MaxHeight = 340,
|
||||||
|
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_taskSummaryPopup.IsOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Border BuildTaskSummaryCard(TaskRunStore.TaskRun task, bool active)
|
||||||
|
{
|
||||||
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
||||||
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray;
|
||||||
|
var (kindIcon, kindColor) = GetTaskKindVisual(task.Kind);
|
||||||
|
var categoryLabel = GetTranscriptTaskCategory(task);
|
||||||
|
var displayTitle = string.Equals(task.Kind, "tool", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(task.Kind, "permission", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(task.Kind, "hook", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? GetAgentItemDisplayName(task.Title)
|
||||||
|
: task.Title;
|
||||||
|
var taskStack = new StackPanel();
|
||||||
|
var headerRow = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
Margin = new Thickness(0, 0, 0, 2),
|
||||||
|
};
|
||||||
|
headerRow.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = kindIcon,
|
||||||
|
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||||
|
FontSize = 9.5,
|
||||||
|
Foreground = kindColor,
|
||||||
|
Margin = new Thickness(0, 0, 4, 0),
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
});
|
||||||
|
headerRow.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = active
|
||||||
|
? $"진행 중 · {displayTitle}"
|
||||||
|
: $"{GetTaskStatusLabel(task.Status)} · {displayTitle}",
|
||||||
|
FontSize = 9.5,
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = active ? primaryText : secondaryText,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
});
|
||||||
|
taskStack.Children.Add(headerRow);
|
||||||
|
|
||||||
|
taskStack.Children.Add(new Border
|
||||||
|
{
|
||||||
|
Background = BrushFromHex("#F8FAFC"),
|
||||||
|
BorderBrush = BrushFromHex("#E5E7EB"),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(999),
|
||||||
|
Padding = new Thickness(6, 1, 6, 1),
|
||||||
|
Margin = new Thickness(0, 0, 0, 4),
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Left,
|
||||||
|
Child = new TextBlock
|
||||||
|
{
|
||||||
|
Text = categoryLabel,
|
||||||
|
FontSize = 8,
|
||||||
|
FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = secondaryText,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(task.Summary))
|
||||||
|
{
|
||||||
|
taskStack.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = TruncateForStatus(task.Summary, 96),
|
||||||
|
FontSize = 8.75,
|
||||||
|
Foreground = secondaryText,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var reviewChipRow = BuildReviewSignalChipRow(
|
||||||
|
kind: task.Kind,
|
||||||
|
toolName: task.Title,
|
||||||
|
title: displayTitle,
|
||||||
|
summary: task.Summary);
|
||||||
|
if (reviewChipRow != null)
|
||||||
|
taskStack.Children.Add(reviewChipRow);
|
||||||
|
|
||||||
|
var actionRow = BuildTaskSummaryActionRow(task, active);
|
||||||
|
if (actionRow != null)
|
||||||
|
taskStack.Children.Add(actionRow);
|
||||||
|
|
||||||
|
return new Border
|
||||||
|
{
|
||||||
|
Background = active ? BrushFromHex("#F8FAFC") : (TryFindResource("LauncherBackground") as Brush ?? Brushes.White),
|
||||||
|
BorderBrush = BrushFromHex("#E5E7EB"),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
CornerRadius = new CornerRadius(7),
|
||||||
|
Padding = new Thickness(8, 5, 8, 5),
|
||||||
|
Margin = new Thickness(8, 0, 8, 4),
|
||||||
|
Child = taskStack
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9099,12 +9099,12 @@ public partial class ChatWindow : Window
|
|||||||
|
|
||||||
private void UpdateTaskSummaryIndicators()
|
private void UpdateTaskSummaryIndicators()
|
||||||
{
|
{
|
||||||
var status = _appState.GetOperationalStatus(_activeTab);
|
|
||||||
var hasLiveRuntimeActivity = !string.Equals(_activeTab, "Chat", StringComparison.OrdinalIgnoreCase)
|
var hasLiveRuntimeActivity = !string.Equals(_activeTab, "Chat", StringComparison.OrdinalIgnoreCase)
|
||||||
&& (_runningConversationCount > 0 || _appState.ActiveTasks.Count > 0);
|
&& (_runningConversationCount > 0 || _appState.ActiveTasks.Count > 0);
|
||||||
|
var status = _appState.GetOperationalStatusPresentation(_activeTab, hasLiveRuntimeActivity);
|
||||||
|
|
||||||
if (RuntimeActivityBadge != null)
|
if (RuntimeActivityBadge != null)
|
||||||
RuntimeActivityBadge.Visibility = status.ShowRuntimeBadge && hasLiveRuntimeActivity
|
RuntimeActivityBadge.Visibility = status.ShowRuntimeBadge
|
||||||
? Visibility.Visible
|
? Visibility.Visible
|
||||||
: Visibility.Collapsed;
|
: Visibility.Collapsed;
|
||||||
|
|
||||||
@@ -9119,12 +9119,7 @@ public partial class ChatWindow : Window
|
|||||||
|
|
||||||
if (ConversationStatusStrip != null && ConversationStatusStripLabel != null)
|
if (ConversationStatusStrip != null && ConversationStatusStripLabel != null)
|
||||||
{
|
{
|
||||||
var showCompactStrip = !string.Equals(_activeTab, "Chat", StringComparison.OrdinalIgnoreCase)
|
if (!status.ShowCompactStrip)
|
||||||
&& (string.Equals(status.StripKind, "permission_waiting", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(status.StripKind, "failed_run", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(status.StripKind, "permission_denied", StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (!showCompactStrip)
|
|
||||||
{
|
{
|
||||||
ConversationStatusStrip.Visibility = Visibility.Collapsed;
|
ConversationStatusStrip.Visibility = Visibility.Collapsed;
|
||||||
ConversationStatusStripLabel.Text = "";
|
ConversationStatusStripLabel.Text = "";
|
||||||
@@ -9735,398 +9730,6 @@ public partial class ChatWindow : Window
|
|||||||
outerStack.Children.Add(resultLabel);
|
outerStack.Children.Add(resultLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string?> ShowInlineUserAskAsync(string question, List<string> options, string defaultValue)
|
|
||||||
{
|
|
||||||
var tcs = new TaskCompletionSource<string?>();
|
|
||||||
|
|
||||||
await Dispatcher.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
AddUserAskCard(question, options, defaultValue, tcs);
|
|
||||||
});
|
|
||||||
|
|
||||||
var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromMinutes(5)));
|
|
||||||
if (completed != tcs.Task)
|
|
||||||
{
|
|
||||||
await Dispatcher.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
RemoveUserAskCard();
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await tcs.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveUserAskCard()
|
|
||||||
{
|
|
||||||
if (_userAskCard == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
MessagePanel.Children.Remove(_userAskCard);
|
|
||||||
_userAskCard = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddUserAskCard(string question, List<string> options, string defaultValue, TaskCompletionSource<string?> tcs)
|
|
||||||
{
|
|
||||||
RemoveUserAskCard();
|
|
||||||
|
|
||||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
|
||||||
var accentColor = ((SolidColorBrush)accentBrush).Color;
|
|
||||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
|
||||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
||||||
var borderBrush = TryFindResource("BorderColor") as Brush
|
|
||||||
?? new SolidColorBrush(Color.FromArgb(0x24, accentColor.R, accentColor.G, accentColor.B));
|
|
||||||
var itemBg = TryFindResource("ItemBackground") as Brush
|
|
||||||
?? new SolidColorBrush(Color.FromArgb(0x10, accentColor.R, accentColor.G, accentColor.B));
|
|
||||||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush
|
|
||||||
?? new SolidColorBrush(Color.FromArgb(0x16, 0xFF, 0xFF, 0xFF));
|
|
||||||
var okBrush = BrushFromHex("#10B981");
|
|
||||||
var dangerBrush = BrushFromHex("#EF4444");
|
|
||||||
|
|
||||||
var container = new Border
|
|
||||||
{
|
|
||||||
Margin = new Thickness(40, 4, 90, 8),
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Left,
|
|
||||||
MaxWidth = Math.Max(420, GetMessageMaxWidth() - 36),
|
|
||||||
Background = itemBg,
|
|
||||||
BorderBrush = borderBrush,
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
CornerRadius = new CornerRadius(14),
|
|
||||||
Padding = new Thickness(14, 12, 14, 12),
|
|
||||||
};
|
|
||||||
|
|
||||||
var outer = new StackPanel();
|
|
||||||
outer.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = "의견 요청",
|
|
||||||
FontSize = 12.5,
|
|
||||||
FontWeight = FontWeights.SemiBold,
|
|
||||||
Foreground = primaryText,
|
|
||||||
});
|
|
||||||
outer.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = question,
|
|
||||||
Margin = new Thickness(0, 4, 0, 10),
|
|
||||||
FontSize = 12.5,
|
|
||||||
Foreground = primaryText,
|
|
||||||
TextWrapping = TextWrapping.Wrap,
|
|
||||||
LineHeight = 20,
|
|
||||||
});
|
|
||||||
|
|
||||||
Border? selectedOption = null;
|
|
||||||
string selectedResponse = defaultValue;
|
|
||||||
|
|
||||||
if (options.Count > 0)
|
|
||||||
{
|
|
||||||
var optionPanel = new WrapPanel
|
|
||||||
{
|
|
||||||
Margin = new Thickness(0, 0, 0, 10),
|
|
||||||
ItemWidth = double.NaN,
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var option in options.Where(static option => !string.IsNullOrWhiteSpace(option)))
|
|
||||||
{
|
|
||||||
var optionLabel = option.Trim();
|
|
||||||
var optBorder = new Border
|
|
||||||
{
|
|
||||||
Background = Brushes.Transparent,
|
|
||||||
BorderBrush = borderBrush,
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
CornerRadius = new CornerRadius(999),
|
|
||||||
Padding = new Thickness(10, 6, 10, 6),
|
|
||||||
Margin = new Thickness(0, 0, 8, 8),
|
|
||||||
Cursor = Cursors.Hand,
|
|
||||||
Child = new TextBlock
|
|
||||||
{
|
|
||||||
Text = optionLabel,
|
|
||||||
FontSize = 12,
|
|
||||||
Foreground = primaryText,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
optBorder.MouseEnter += (s, _) =>
|
|
||||||
{
|
|
||||||
if (!ReferenceEquals(selectedOption, s))
|
|
||||||
((Border)s).Background = hoverBg;
|
|
||||||
};
|
|
||||||
optBorder.MouseLeave += (s, _) =>
|
|
||||||
{
|
|
||||||
if (!ReferenceEquals(selectedOption, s))
|
|
||||||
((Border)s).Background = Brushes.Transparent;
|
|
||||||
};
|
|
||||||
optBorder.MouseLeftButtonUp += (_, _) =>
|
|
||||||
{
|
|
||||||
if (selectedOption != null)
|
|
||||||
{
|
|
||||||
selectedOption.Background = Brushes.Transparent;
|
|
||||||
selectedOption.BorderBrush = borderBrush;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedOption = optBorder;
|
|
||||||
selectedOption.Background = new SolidColorBrush(Color.FromArgb(0x18, accentColor.R, accentColor.G, accentColor.B));
|
|
||||||
selectedOption.BorderBrush = accentBrush;
|
|
||||||
selectedResponse = optionLabel;
|
|
||||||
};
|
|
||||||
|
|
||||||
optionPanel.Children.Add(optBorder);
|
|
||||||
}
|
|
||||||
|
|
||||||
outer.Children.Add(optionPanel);
|
|
||||||
}
|
|
||||||
|
|
||||||
outer.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = "직접 입력",
|
|
||||||
FontSize = 11.5,
|
|
||||||
Foreground = secondaryText,
|
|
||||||
Margin = new Thickness(0, 0, 0, 6),
|
|
||||||
});
|
|
||||||
|
|
||||||
var inputBox = new TextBox
|
|
||||||
{
|
|
||||||
Text = defaultValue,
|
|
||||||
AcceptsReturn = true,
|
|
||||||
TextWrapping = TextWrapping.Wrap,
|
|
||||||
MinHeight = 42,
|
|
||||||
MaxHeight = 100,
|
|
||||||
FontSize = 12.5,
|
|
||||||
Padding = new Thickness(10, 8, 10, 8),
|
|
||||||
Background = Brushes.Transparent,
|
|
||||||
Foreground = primaryText,
|
|
||||||
CaretBrush = primaryText,
|
|
||||||
BorderBrush = borderBrush,
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
};
|
|
||||||
inputBox.TextChanged += (_, _) =>
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(inputBox.Text))
|
|
||||||
{
|
|
||||||
selectedResponse = inputBox.Text.Trim();
|
|
||||||
if (selectedOption != null)
|
|
||||||
{
|
|
||||||
selectedOption.Background = Brushes.Transparent;
|
|
||||||
selectedOption.BorderBrush = borderBrush;
|
|
||||||
selectedOption = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
outer.Children.Add(inputBox);
|
|
||||||
|
|
||||||
var buttonRow = new StackPanel
|
|
||||||
{
|
|
||||||
Orientation = Orientation.Horizontal,
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Right,
|
|
||||||
Margin = new Thickness(0, 12, 0, 0),
|
|
||||||
};
|
|
||||||
|
|
||||||
Border BuildActionButton(string label, Brush bg, Brush fg)
|
|
||||||
{
|
|
||||||
return new Border
|
|
||||||
{
|
|
||||||
Background = bg,
|
|
||||||
BorderBrush = bg,
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
CornerRadius = new CornerRadius(999),
|
|
||||||
Padding = new Thickness(12, 7, 12, 7),
|
|
||||||
Margin = new Thickness(8, 0, 0, 0),
|
|
||||||
Cursor = Cursors.Hand,
|
|
||||||
Child = new TextBlock
|
|
||||||
{
|
|
||||||
Text = label,
|
|
||||||
FontSize = 12,
|
|
||||||
FontWeight = FontWeights.SemiBold,
|
|
||||||
Foreground = fg,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var cancelBtn = BuildActionButton("취소", Brushes.Transparent, dangerBrush);
|
|
||||||
cancelBtn.BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, 0xEF, 0x44, 0x44));
|
|
||||||
cancelBtn.MouseLeftButtonUp += (_, _) =>
|
|
||||||
{
|
|
||||||
RemoveUserAskCard();
|
|
||||||
tcs.TrySetResult(null);
|
|
||||||
};
|
|
||||||
buttonRow.Children.Add(cancelBtn);
|
|
||||||
|
|
||||||
var submitBtn = BuildActionButton("전달", okBrush, Brushes.White);
|
|
||||||
submitBtn.MouseLeftButtonUp += (_, _) =>
|
|
||||||
{
|
|
||||||
var finalResponse = !string.IsNullOrWhiteSpace(inputBox.Text)
|
|
||||||
? inputBox.Text.Trim()
|
|
||||||
: selectedResponse?.Trim();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(finalResponse))
|
|
||||||
finalResponse = defaultValue?.Trim();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(finalResponse))
|
|
||||||
return;
|
|
||||||
|
|
||||||
RemoveUserAskCard();
|
|
||||||
tcs.TrySetResult(finalResponse);
|
|
||||||
};
|
|
||||||
buttonRow.Children.Add(submitBtn);
|
|
||||||
|
|
||||||
outer.Children.Add(buttonRow);
|
|
||||||
container.Child = outer;
|
|
||||||
_userAskCard = container;
|
|
||||||
|
|
||||||
container.Opacity = 0;
|
|
||||||
container.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(180)));
|
|
||||||
MessagePanel.Children.Add(container);
|
|
||||||
ForceScrollToEnd();
|
|
||||||
inputBox.Focus();
|
|
||||||
inputBox.CaretIndex = inputBox.Text.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
|
||||||
// 실행 계획 뷰어 (PlanViewerWindow) 연동
|
|
||||||
// ════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
/// <summary>PlanViewerWindow를 사용하는 UserDecisionCallback을 생성합니다.</summary>
|
|
||||||
private Func<string, List<string>, Task<string?>> CreatePlanDecisionCallback()
|
|
||||||
{
|
|
||||||
return async (planSummary, options) =>
|
|
||||||
{
|
|
||||||
var tcs = new TaskCompletionSource<string?>();
|
|
||||||
var steps = Services.Agent.TaskDecomposer.ExtractSteps(planSummary);
|
|
||||||
|
|
||||||
await Dispatcher.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
EnsurePlanViewerWindow();
|
|
||||||
_planViewerWindow?.LoadPlan(planSummary, steps, tcs);
|
|
||||||
ShowPlanButton(true);
|
|
||||||
AddDecisionButtons(tcs, options);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 5분 타임아웃
|
|
||||||
var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromMinutes(5)));
|
|
||||||
if (completed != tcs.Task)
|
|
||||||
{
|
|
||||||
await Dispatcher.InvokeAsync(() => _planViewerWindow?.Hide());
|
|
||||||
return "취소";
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await tcs.Task;
|
|
||||||
var agentDecision = result;
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
agentDecision = _planViewerWindow?.BuildApprovedDecisionPayload(AgentLoopService.ApprovedPlanDecisionPrefix);
|
|
||||||
}
|
|
||||||
else if (!string.Equals(result, "취소", StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& !string.Equals(result, "승인", StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& !string.IsNullOrWhiteSpace(result))
|
|
||||||
{
|
|
||||||
agentDecision = $"수정 요청: {result.Trim()}";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 승인된 경우 — 실행 모드로 전환
|
|
||||||
if (result == null) // null = 승인
|
|
||||||
{
|
|
||||||
await Dispatcher.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
_planViewerWindow?.SwitchToExecutionMode();
|
|
||||||
_planViewerWindow?.Hide(); // 숨기고 하단 버튼으로 다시 열기
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Dispatcher.InvokeAsync(() => _planViewerWindow?.Hide());
|
|
||||||
}
|
|
||||||
|
|
||||||
return agentDecision;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnsurePlanViewerWindow()
|
|
||||||
{
|
|
||||||
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
|
|
||||||
return;
|
|
||||||
|
|
||||||
_planViewerWindow = new PlanViewerWindow(this);
|
|
||||||
_planViewerWindow.Closing += (_, e) =>
|
|
||||||
{
|
|
||||||
e.Cancel = true;
|
|
||||||
_planViewerWindow.Hide();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>하단 바에 계획 보기 버튼을 표시/숨김합니다.</summary>
|
|
||||||
private void ShowPlanButton(bool show)
|
|
||||||
{
|
|
||||||
if (!show)
|
|
||||||
{
|
|
||||||
// 계획 버튼 제거
|
|
||||||
for (int i = MoodIconPanel.Children.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
if (MoodIconPanel.Children[i] is Border b && b.Tag?.ToString() == "PlanBtn")
|
|
||||||
{
|
|
||||||
// 앞의 구분선도 제거
|
|
||||||
if (i > 0 && MoodIconPanel.Children[i - 1] is Border sep && sep.Tag?.ToString() == "PlanSep")
|
|
||||||
MoodIconPanel.Children.RemoveAt(i - 1);
|
|
||||||
if (i < MoodIconPanel.Children.Count)
|
|
||||||
MoodIconPanel.Children.RemoveAt(Math.Min(i, MoodIconPanel.Children.Count - 1));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 이미 있으면 무시
|
|
||||||
foreach (var child in MoodIconPanel.Children)
|
|
||||||
{
|
|
||||||
if (child is Border b && b.Tag?.ToString() == "PlanBtn") return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 구분선
|
|
||||||
var separator = new Border
|
|
||||||
{
|
|
||||||
Width = 1, Height = 18,
|
|
||||||
Background = TryFindResource("SeparatorColor") as Brush ?? Brushes.Gray,
|
|
||||||
Margin = new Thickness(4, 0, 4, 0),
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
Tag = "PlanSep",
|
|
||||||
};
|
|
||||||
MoodIconPanel.Children.Add(separator);
|
|
||||||
|
|
||||||
// 계획 버튼
|
|
||||||
var planBtn = CreateFolderBarButton("\uE9D2", "계획", "실행 계획 보기", "#10B981");
|
|
||||||
planBtn.Tag = "PlanBtn";
|
|
||||||
planBtn.MouseLeftButtonUp += (_, e) =>
|
|
||||||
{
|
|
||||||
e.Handled = true;
|
|
||||||
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
|
|
||||||
{
|
|
||||||
_planViewerWindow.Show();
|
|
||||||
_planViewerWindow.Activate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
MoodIconPanel.Children.Add(planBtn);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>계획 뷰어에서 현재 실행 단계를 갱신합니다.</summary>
|
|
||||||
private void UpdatePlanViewerStep(AgentEvent evt)
|
|
||||||
{
|
|
||||||
if (_planViewerWindow == null || !IsWindowAlive(_planViewerWindow)) return;
|
|
||||||
if (evt.StepCurrent > 0)
|
|
||||||
_planViewerWindow.UpdateCurrentStep(evt.StepCurrent - 1); // 0-based
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>계획 실행 완료를 뷰어에 알립니다.</summary>
|
|
||||||
private void CompletePlanViewer()
|
|
||||||
{
|
|
||||||
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
|
|
||||||
_planViewerWindow.MarkComplete();
|
|
||||||
ShowPlanButton(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsWindowAlive(Window? w)
|
|
||||||
{
|
|
||||||
if (w == null) return false;
|
|
||||||
try { var _ = w.IsVisible; return true; }
|
|
||||||
catch { return false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
// 후속 작업 제안 칩 (suggest_actions)
|
// 후속 작업 제안 칩 (suggest_actions)
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
@@ -10557,19 +10160,29 @@ public partial class ChatWindow : Window
|
|||||||
|
|
||||||
// 전체 통계 이벤트는 별도 색상 (보라색 계열)
|
// 전체 통계 이벤트는 별도 색상 (보라색 계열)
|
||||||
var isTotalStats = evt.Type == AgentEventType.StepDone && evt.ToolName == "total_stats";
|
var isTotalStats = evt.Type == AgentEventType.StepDone && evt.ToolName == "total_stats";
|
||||||
|
var transcriptBadgeLabel = GetTranscriptBadgeLabel(evt);
|
||||||
|
var permissionPresentation = evt.Type switch
|
||||||
|
{
|
||||||
|
AgentEventType.PermissionRequest => PermissionRequestPresentationCatalog.Resolve(evt.ToolName, pending: true),
|
||||||
|
AgentEventType.PermissionGranted => PermissionRequestPresentationCatalog.Resolve(evt.ToolName, pending: false),
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
var toolResultPresentation = evt.Type == AgentEventType.ToolResult
|
||||||
|
? ToolResultPresentationCatalog.Resolve(evt, transcriptBadgeLabel)
|
||||||
|
: null;
|
||||||
|
|
||||||
var (icon, label, bgHex, fgHex) = isTotalStats
|
var (icon, label, bgHex, fgHex) = isTotalStats
|
||||||
? ("\uE9D2", "전체 통계", "#F3EEFF", "#7C3AED")
|
? ("\uE9D2", "전체 통계", "#F3EEFF", "#7C3AED")
|
||||||
: evt.Type switch
|
: evt.Type switch
|
||||||
{
|
{
|
||||||
AgentEventType.Thinking => ("\uE8BD", "분석 중", "#F0F0FF", "#6B7BC4"),
|
AgentEventType.Thinking => ("\uE8BD", "분석 중", "#F0F0FF", "#6B7BC4"),
|
||||||
AgentEventType.PermissionRequest => GetPermissionBadgeMeta(evt.ToolName, pending: true),
|
AgentEventType.PermissionRequest => (permissionPresentation!.Icon, permissionPresentation.Label, permissionPresentation.BackgroundHex, permissionPresentation.ForegroundHex),
|
||||||
AgentEventType.PermissionGranted => GetPermissionBadgeMeta(evt.ToolName, pending: false),
|
AgentEventType.PermissionGranted => (permissionPresentation!.Icon, permissionPresentation.Label, permissionPresentation.BackgroundHex, permissionPresentation.ForegroundHex),
|
||||||
AgentEventType.PermissionDenied => ("\uE783", "권한 거부", "#FEF2F2", "#DC2626"),
|
AgentEventType.PermissionDenied => ("\uE783", "권한 거부", "#FEF2F2", "#DC2626"),
|
||||||
AgentEventType.Decision => GetDecisionBadgeMeta(evt.Summary),
|
AgentEventType.Decision => GetDecisionBadgeMeta(evt.Summary),
|
||||||
AgentEventType.ToolCall => ("\uE8A7", GetTranscriptBadgeLabel(evt), "#EEF6FF", "#3B82F6"),
|
AgentEventType.ToolCall => ("\uE8A7", transcriptBadgeLabel, "#EEF6FF", "#3B82F6"),
|
||||||
AgentEventType.ToolResult => ("\uE73E", GetTranscriptBadgeLabel(evt), "#EEF9EE", "#16A34A"),
|
AgentEventType.ToolResult => (toolResultPresentation!.Icon, toolResultPresentation.Label, toolResultPresentation.BackgroundHex, toolResultPresentation.ForegroundHex),
|
||||||
AgentEventType.SkillCall => ("\uE8A5", GetTranscriptBadgeLabel(evt), "#FFF7ED", "#EA580C"),
|
AgentEventType.SkillCall => ("\uE8A5", transcriptBadgeLabel, "#FFF7ED", "#EA580C"),
|
||||||
AgentEventType.Error => ("\uE783", "오류", "#FEF2F2", "#DC2626"),
|
AgentEventType.Error => ("\uE783", "오류", "#FEF2F2", "#DC2626"),
|
||||||
AgentEventType.Complete => ("\uE930", "완료", "#F0FFF4", "#15803D"),
|
AgentEventType.Complete => ("\uE930", "완료", "#F0FFF4", "#15803D"),
|
||||||
AgentEventType.StepDone => ("\uE73E", "단계 완료", "#EEF9EE", "#16A34A"),
|
AgentEventType.StepDone => ("\uE73E", "단계 완료", "#EEF9EE", "#16A34A"),
|
||||||
@@ -18751,30 +18364,6 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
|
|||||||
return ("\uE70F", "계획 확인", "#FFF7ED", "#C2410C");
|
return ("\uE70F", "계획 확인", "#FFF7ED", "#C2410C");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (string icon, string label, string bgHex, string fgHex) GetPermissionBadgeMeta(string? toolName, bool pending)
|
|
||||||
{
|
|
||||||
var tool = toolName?.Trim().ToLowerInvariant() ?? "";
|
|
||||||
|
|
||||||
if (tool.Contains("process") || tool.Contains("bash") || tool.Contains("powershell"))
|
|
||||||
return pending
|
|
||||||
? ("\uE756", "명령 권한 요청", "#FEF2F2", "#DC2626")
|
|
||||||
: ("\uE73E", "명령 권한 허용", "#ECFDF5", "#059669");
|
|
||||||
|
|
||||||
if (tool.Contains("web") || tool.Contains("fetch") || tool.Contains("http"))
|
|
||||||
return pending
|
|
||||||
? ("\uE774", "네트워크 권한 요청", "#FFF7ED", "#C2410C")
|
|
||||||
: ("\uE73E", "네트워크 권한 허용", "#ECFDF5", "#059669");
|
|
||||||
|
|
||||||
if (tool.Contains("file"))
|
|
||||||
return pending
|
|
||||||
? ("\uE8A5", "파일 권한 요청", "#FFF7ED", "#C2410C")
|
|
||||||
: ("\uE73E", "파일 권한 허용", "#ECFDF5", "#059669");
|
|
||||||
|
|
||||||
return pending
|
|
||||||
? ("\uE897", "권한 요청", "#FFF7ED", "#C2410C")
|
|
||||||
: ("\uE73E", "권한 허용", "#ECFDF5", "#059669");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsDecisionPending(string? summary)
|
private static bool IsDecisionPending(string? summary)
|
||||||
{
|
{
|
||||||
var text = summary?.Trim() ?? "";
|
var text = summary?.Trim() ?? "";
|
||||||
@@ -20535,261 +20124,6 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
|
|||||||
RenderMessages();
|
RenderMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RuntimeTaskSummary_Click(object sender, MouseButtonEventArgs e)
|
|
||||||
{
|
|
||||||
e.Handled = true;
|
|
||||||
_taskSummaryTarget = sender as UIElement ?? RuntimeActivityBadge;
|
|
||||||
ShowTaskSummaryPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowTaskSummaryPopup()
|
|
||||||
{
|
|
||||||
if (_taskSummaryTarget == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_taskSummaryPopup != null)
|
|
||||||
_taskSummaryPopup.IsOpen = false;
|
|
||||||
|
|
||||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
|
||||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray;
|
|
||||||
var popupBackground = TryFindResource("LauncherBackground") as Brush ?? Brushes.White;
|
|
||||||
var panel = new StackPanel { Margin = new Thickness(2) };
|
|
||||||
panel.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = "작업 요약",
|
|
||||||
FontSize = 11,
|
|
||||||
FontWeight = FontWeights.SemiBold,
|
|
||||||
Foreground = primaryText,
|
|
||||||
Margin = new Thickness(8, 5, 8, 2),
|
|
||||||
});
|
|
||||||
panel.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = "현재 상태 요약",
|
|
||||||
FontSize = 8.5,
|
|
||||||
Foreground = secondaryText,
|
|
||||||
Margin = new Thickness(8, 0, 8, 5),
|
|
||||||
});
|
|
||||||
|
|
||||||
ChatConversation? currentConversation;
|
|
||||||
lock (_convLock) currentConversation = _currentConversation;
|
|
||||||
AddTaskSummaryObservabilitySections(panel, currentConversation);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(_appState.AgentRun.RunId))
|
|
||||||
{
|
|
||||||
var currentRun = new Border
|
|
||||||
{
|
|
||||||
Background = BrushFromHex("#F8FAFC"),
|
|
||||||
BorderBrush = BrushFromHex("#E2E8F0"),
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
CornerRadius = new CornerRadius(8),
|
|
||||||
Padding = new Thickness(8, 6, 8, 6),
|
|
||||||
Margin = new Thickness(6, 0, 6, 6),
|
|
||||||
Child = new StackPanel
|
|
||||||
{
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new TextBlock
|
|
||||||
{
|
|
||||||
Text = $"실행 run {ShortRunId(_appState.AgentRun.RunId)}",
|
|
||||||
FontWeight = FontWeights.SemiBold,
|
|
||||||
Foreground = primaryText,
|
|
||||||
FontSize = 9.75,
|
|
||||||
},
|
|
||||||
new TextBlock
|
|
||||||
{
|
|
||||||
Text = $"{GetRunStatusLabel(_appState.AgentRun.Status)} · step {_appState.AgentRun.LastIteration}",
|
|
||||||
Margin = new Thickness(0, 2, 0, 0),
|
|
||||||
Foreground = GetRunStatusBrush(_appState.AgentRun.Status),
|
|
||||||
FontSize = 9,
|
|
||||||
},
|
|
||||||
new TextBlock
|
|
||||||
{
|
|
||||||
Text = string.IsNullOrWhiteSpace(_appState.AgentRun.Summary) ? "요약 없음" : _appState.AgentRun.Summary,
|
|
||||||
Margin = new Thickness(0, 3, 0, 0),
|
|
||||||
TextWrapping = TextWrapping.Wrap,
|
|
||||||
Foreground = Brushes.DimGray,
|
|
||||||
FontSize = 9,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
panel.Children.Add(currentRun);
|
|
||||||
}
|
|
||||||
|
|
||||||
var recentAgentRuns = _appState.GetRecentAgentRuns(1);
|
|
||||||
if (recentAgentRuns.Count > 0)
|
|
||||||
{
|
|
||||||
panel.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = "마지막 실행",
|
|
||||||
FontSize = 9,
|
|
||||||
FontWeight = FontWeights.SemiBold,
|
|
||||||
Foreground = Brushes.DimGray,
|
|
||||||
Margin = new Thickness(8, 0, 8, 2),
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach (var run in recentAgentRuns)
|
|
||||||
{
|
|
||||||
var runEvents = GetExecutionEventsForRun(run.RunId, 1);
|
|
||||||
var runFilePaths = GetExecutionEventFilePaths(run.RunId, 1);
|
|
||||||
var runDisplay = _appState.GetRunDisplay(run);
|
|
||||||
var runCardStack = new StackPanel
|
|
||||||
{
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new TextBlock
|
|
||||||
{
|
|
||||||
Text = runDisplay.HeaderText,
|
|
||||||
FontWeight = FontWeights.SemiBold,
|
|
||||||
Foreground = GetRunStatusBrush(run.Status),
|
|
||||||
FontSize = 9.5,
|
|
||||||
},
|
|
||||||
new TextBlock
|
|
||||||
{
|
|
||||||
Text = runDisplay.MetaText,
|
|
||||||
Margin = new Thickness(0, 1, 0, 0),
|
|
||||||
Foreground = secondaryText,
|
|
||||||
FontSize = 8.25,
|
|
||||||
},
|
|
||||||
new TextBlock
|
|
||||||
{
|
|
||||||
Text = TruncateForStatus(runDisplay.SummaryText, 92),
|
|
||||||
Margin = new Thickness(0, 1.5, 0, 0),
|
|
||||||
TextWrapping = TextWrapping.Wrap,
|
|
||||||
Foreground = secondaryText,
|
|
||||||
FontSize = 8.5,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (runEvents.Count > 0 || runFilePaths.Count > 0)
|
|
||||||
{
|
|
||||||
var activitySummary = new StackPanel();
|
|
||||||
activitySummary.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = $"로그 {runEvents.Count} · 파일 {runFilePaths.Count}",
|
|
||||||
FontSize = 8,
|
|
||||||
Foreground = secondaryText,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(run.RunId))
|
|
||||||
{
|
|
||||||
var capturedRunId = run.RunId;
|
|
||||||
var timelineButton = CreateTaskSummaryActionButton(
|
|
||||||
"타임라인",
|
|
||||||
"#F8FAFC",
|
|
||||||
"#CBD5E1",
|
|
||||||
"#334155",
|
|
||||||
(_, _) => ScrollToRunInTimeline(capturedRunId),
|
|
||||||
trailingMargin: false);
|
|
||||||
timelineButton.Margin = new Thickness(0, 5, 0, 0);
|
|
||||||
activitySummary.Children.Add(timelineButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
runCardStack.Children.Add(new Border
|
|
||||||
{
|
|
||||||
Background = BrushFromHex("#F8FAFC"),
|
|
||||||
BorderBrush = BrushFromHex("#E2E8F0"),
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
CornerRadius = new CornerRadius(7),
|
|
||||||
Padding = new Thickness(6, 4, 6, 4),
|
|
||||||
Margin = new Thickness(0, 5, 0, 0),
|
|
||||||
Child = activitySummary
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(run.Status, "completed", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var capturedRun = run;
|
|
||||||
var followUpButton = CreateTaskSummaryActionButton(
|
|
||||||
"후속 큐",
|
|
||||||
"#ECFDF5",
|
|
||||||
"#BBF7D0",
|
|
||||||
"#166534",
|
|
||||||
(_, _) => EnqueueFollowUpFromRun(capturedRun),
|
|
||||||
trailingMargin: false);
|
|
||||||
followUpButton.Margin = new Thickness(0, 6, 0, 0);
|
|
||||||
runCardStack.Children.Add(followUpButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(run.Status, "failed", StringComparison.OrdinalIgnoreCase) && CanRetryCurrentConversation())
|
|
||||||
{
|
|
||||||
var retryButton = CreateTaskSummaryActionButton(
|
|
||||||
"다시 시도",
|
|
||||||
"#FEF2F2",
|
|
||||||
"#FCA5A5",
|
|
||||||
"#991B1B",
|
|
||||||
(_, _) =>
|
|
||||||
{
|
|
||||||
_taskSummaryPopup?.SetCurrentValue(Popup.IsOpenProperty, false);
|
|
||||||
RetryLastUserMessageFromConversation();
|
|
||||||
},
|
|
||||||
trailingMargin: false);
|
|
||||||
retryButton.Margin = new Thickness(0, 6, 0, 0);
|
|
||||||
runCardStack.Children.Add(retryButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
panel.Children.Add(new Border
|
|
||||||
{
|
|
||||||
Background = popupBackground,
|
|
||||||
BorderBrush = BrushFromHex("#E5E7EB"),
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
CornerRadius = new CornerRadius(7),
|
|
||||||
Padding = new Thickness(7, 5, 7, 5),
|
|
||||||
Margin = new Thickness(6, 0, 6, 4),
|
|
||||||
Child = runCardStack
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var activeTasks = FilterTaskSummaryItems(_appState.ActiveTasks).Take(3).ToList();
|
|
||||||
var recentTasks = FilterTaskSummaryItems(_appState.RecentTasks).Take(2).ToList();
|
|
||||||
|
|
||||||
foreach (var task in activeTasks)
|
|
||||||
panel.Children.Add(BuildTaskSummaryCard(task, active: true));
|
|
||||||
|
|
||||||
if (ShouldIncludeRecentTaskSummary(activeTasks))
|
|
||||||
{
|
|
||||||
foreach (var task in recentTasks)
|
|
||||||
panel.Children.Add(BuildTaskSummaryCard(task, active: false));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeTasks.Count == 0 && recentTasks.Count == 0)
|
|
||||||
{
|
|
||||||
panel.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = "표시할 작업 이력이 없습니다.",
|
|
||||||
Margin = new Thickness(10, 2, 10, 8),
|
|
||||||
Foreground = secondaryText,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_taskSummaryPopup = new Popup
|
|
||||||
{
|
|
||||||
PlacementTarget = _taskSummaryTarget,
|
|
||||||
Placement = PlacementMode.Top,
|
|
||||||
AllowsTransparency = true,
|
|
||||||
StaysOpen = false,
|
|
||||||
PopupAnimation = PopupAnimation.Fade,
|
|
||||||
Child = new Border
|
|
||||||
{
|
|
||||||
Background = popupBackground,
|
|
||||||
BorderBrush = BrushFromHex("#E5E7EB"),
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
CornerRadius = new CornerRadius(12),
|
|
||||||
Padding = new Thickness(6),
|
|
||||||
Child = new ScrollViewer
|
|
||||||
{
|
|
||||||
Content = panel,
|
|
||||||
MaxHeight = 340,
|
|
||||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_taskSummaryPopup.IsOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CanRetryCurrentConversation()
|
private bool CanRetryCurrentConversation()
|
||||||
{
|
{
|
||||||
return !string.IsNullOrWhiteSpace(GetLastUserMessageFromConversation());
|
return !string.IsNullOrWhiteSpace(GetLastUserMessageFromConversation());
|
||||||
@@ -21539,97 +20873,6 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
|
|||||||
ShowTaskSummaryPopup();
|
ShowTaskSummaryPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Border BuildTaskSummaryCard(TaskRunStore.TaskRun task, bool active)
|
|
||||||
{
|
|
||||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
|
||||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray;
|
|
||||||
var (kindIcon, kindColor) = GetTaskKindVisual(task.Kind);
|
|
||||||
var categoryLabel = GetTranscriptTaskCategory(task);
|
|
||||||
var displayTitle = string.Equals(task.Kind, "tool", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(task.Kind, "permission", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(task.Kind, "hook", StringComparison.OrdinalIgnoreCase)
|
|
||||||
? GetAgentItemDisplayName(task.Title)
|
|
||||||
: task.Title;
|
|
||||||
var taskStack = new StackPanel();
|
|
||||||
var headerRow = new StackPanel
|
|
||||||
{
|
|
||||||
Orientation = Orientation.Horizontal,
|
|
||||||
Margin = new Thickness(0, 0, 0, 2),
|
|
||||||
};
|
|
||||||
headerRow.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = kindIcon,
|
|
||||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
||||||
FontSize = 9.5,
|
|
||||||
Foreground = kindColor,
|
|
||||||
Margin = new Thickness(0, 0, 4, 0),
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
});
|
|
||||||
headerRow.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = active
|
|
||||||
? $"진행 중 · {displayTitle}"
|
|
||||||
: $"{GetTaskStatusLabel(task.Status)} · {displayTitle}",
|
|
||||||
FontSize = 9.5,
|
|
||||||
FontWeight = FontWeights.SemiBold,
|
|
||||||
Foreground = active ? primaryText : secondaryText,
|
|
||||||
TextWrapping = TextWrapping.Wrap,
|
|
||||||
});
|
|
||||||
taskStack.Children.Add(headerRow);
|
|
||||||
|
|
||||||
taskStack.Children.Add(new Border
|
|
||||||
{
|
|
||||||
Background = BrushFromHex("#F8FAFC"),
|
|
||||||
BorderBrush = BrushFromHex("#E5E7EB"),
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
CornerRadius = new CornerRadius(999),
|
|
||||||
Padding = new Thickness(6, 1, 6, 1),
|
|
||||||
Margin = new Thickness(0, 0, 0, 4),
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Left,
|
|
||||||
Child = new TextBlock
|
|
||||||
{
|
|
||||||
Text = categoryLabel,
|
|
||||||
FontSize = 8,
|
|
||||||
FontWeight = FontWeights.SemiBold,
|
|
||||||
Foreground = secondaryText,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(task.Summary))
|
|
||||||
{
|
|
||||||
taskStack.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = TruncateForStatus(task.Summary, 96),
|
|
||||||
FontSize = 8.75,
|
|
||||||
Foreground = secondaryText,
|
|
||||||
TextWrapping = TextWrapping.Wrap,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var reviewChipRow = BuildReviewSignalChipRow(
|
|
||||||
kind: task.Kind,
|
|
||||||
toolName: task.Title,
|
|
||||||
title: displayTitle,
|
|
||||||
summary: task.Summary);
|
|
||||||
if (reviewChipRow != null)
|
|
||||||
taskStack.Children.Add(reviewChipRow);
|
|
||||||
|
|
||||||
var actionRow = BuildTaskSummaryActionRow(task, active);
|
|
||||||
if (actionRow != null)
|
|
||||||
taskStack.Children.Add(actionRow);
|
|
||||||
|
|
||||||
return new Border
|
|
||||||
{
|
|
||||||
Background = active ? BrushFromHex("#F8FAFC") : (TryFindResource("LauncherBackground") as Brush ?? Brushes.White),
|
|
||||||
BorderBrush = BrushFromHex("#E5E7EB"),
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
CornerRadius = new CornerRadius(7),
|
|
||||||
Padding = new Thickness(8, 5, 8, 5),
|
|
||||||
Margin = new Thickness(8, 0, 8, 4),
|
|
||||||
Child = taskStack
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Button CreateTaskSummaryActionButton(
|
private Button CreateTaskSummaryActionButton(
|
||||||
string label,
|
string label,
|
||||||
string bg,
|
string bg,
|
||||||
|
|||||||
Reference in New Issue
Block a user