Files
AX-Copilot-Codex/src/AxCopilot/Views/ChatWindow.TaskSummary.cs
lacvet f0af86cc1e
Some checks failed
Release Gate / gate (push) Has been cancelled
AX Agent transcript 렌더 구조를 분리하고 권한/도구 결과 표시 체계를 정리한다
- 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)
2026-04-05 22:55:56 +09:00

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
};
}
}