Cowork와 Code를 동시에 실행할 때 메인 루프 번호와 라이브 진행 힌트가 서로 섞이던 문제를 수정했다. ChatWindow가 전역 단일 진행 상태를 공유하던 구조를 탭별 현재 run 상태, 진행 스텝, 라이브 힌트, 대기 UI 이벤트로 분리해 현재 탭 기준으로만 렌더링하도록 정리했다. AppStateService에 탭별 최신 run 상태 추적을 추가하고 ConversationList, TaskSummary, Timeline, V2 라이브 카드가 활성 탭의 run 메타를 읽도록 변경했다. AppStateServiceTests에 탭별 run iteration 분리 회귀 테스트를 추가했고 README와 DEVELOPMENT 문서에도 2026-04-15 22:25 (KST) 기준 이력을 반영했다. 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_tab_loop_isolation\\ -p:IntermediateOutputPath=obj\\verify_tab_loop_isolation\\ (경고 0 / 오류 0) 검증: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AppStateServiceTests" -p:OutputPath=bin\\verify_tab_loop_isolation_tests\\ -p:IntermediateOutputPath=obj\\verify_tab_loop_isolation_tests\\ (통과 45)
360 lines
14 KiB
C#
360 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);
|
|
|
|
var activeRun = GetCurrentAgentRunState(_activeTab);
|
|
if (!string.IsNullOrWhiteSpace(activeRun?.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(activeRun!.RunId)}",
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = primaryText,
|
|
FontSize = 9.75,
|
|
},
|
|
new TextBlock
|
|
{
|
|
Text = $"{GetRunStatusLabel(activeRun!.Status)} · step {activeRun.LastIteration}",
|
|
Margin = new Thickness(0, 2, 0, 0),
|
|
Foreground = GetRunStatusBrush(activeRun.Status),
|
|
FontSize = 9,
|
|
},
|
|
new TextBlock
|
|
{
|
|
Text = string.IsNullOrWhiteSpace(activeRun!.Summary) ? "요약 없음" : activeRun.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 = s_segoeIconFont,
|
|
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
|
|
};
|
|
}
|
|
}
|