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)
359 lines
14 KiB
C#
359 lines
14 KiB
C#
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
|
|
};
|
|
}
|
|
}
|