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:
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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user