AX Agent 도구·스킬 정합성 재구성 및 실행 품질 보강

변경 목적:
- AX Agent의 도구 이름, 내부 설정, 스킬 정책, 실행 루프 사이의 불일치를 줄이고 전체 동작 품질을 높인다.
- claw-code 수준의 일관된 동작 품질을 참고하되 AX 구조에 맞는 고유한 카탈로그·정규화 레이어로 재구성한다.

핵심 수정사항:
- 도구 canonical id, legacy alias, 탭 노출, 설정 카테고리, read-only 분류를 중앙 카탈로그로 통합했다.
- ToolRegistry, AgentLoopService, 병렬 실행 분류, 권한 처리, 훅 처리, 스킬 allowed-tools 해석이 같은 이름 체계를 사용하도록 정리했다.
- Agent 설정/일반 설정/도움말의 도구 카드와 훅 편집기, 스킬 설명을 현재 런타임 구조에 맞게 갱신했다.
- 컨텍스트 압축, intent gate, spawn agents, session learning, model prompt adapter, workspace context 관련 변경과 테스트 추가를 함께 반영했다.
- 문서 이력과 비교/로드맵 문서를 최신 상태로 갱신했다.

검증 결과:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_toolcat\ -p:IntermediateOutputPath=obj\verify_toolcat\ : 경고 0 / 오류 0
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentToolCatalogTests -p:OutputPath=bin\verify_toolcat_tests\ -p:IntermediateOutputPath=obj\verify_toolcat_tests\ : 통과 8
This commit is contained in:
2026-04-14 17:52:46 +09:00
parent fa33b98f7e
commit 8cb08576d5
200 changed files with 13522 additions and 5764 deletions

View File

@@ -2,7 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="AX Copilot — 정보"
Width="460" SizeToContent="Height"
Width="520" SizeToContent="Height" MaxHeight="720"
WindowStyle="None"
AllowsTransparency="True"
UseLayoutRounding="True"
@@ -37,6 +37,76 @@
</Setter.Value>
</Setter>
</Style>
<!-- 커스텀 스크롤바 Thumb -->
<Style x:Key="AboutScrollThumb" TargetType="Thumb">
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Border x:Name="ThumbBd"
CornerRadius="3"
Background="#CCCCDD"
Margin="1,0"/>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="ThumbBd" Property="Background" Value="#AAAACC"/>
</Trigger>
<Trigger Property="IsDragging" Value="True">
<Setter TargetName="ThumbBd" Property="Background" Value="#8888BB"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 커스텀 세로 스크롤바 -->
<Style x:Key="AboutScrollBar" TargetType="ScrollBar">
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Width" Value="6"/>
<Setter Property="MinWidth" Value="6"/>
<Setter Property="Margin" Value="0,4,4,4"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ScrollBar">
<Grid Background="Transparent">
<Track x:Name="PART_Track" IsDirectionReversed="True">
<Track.Thumb>
<Thumb Style="{StaticResource AboutScrollThumb}"/>
</Track.Thumb>
</Track>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ScrollViewer에 커스텀 스크롤바 적용 -->
<Style x:Key="AboutScrollViewer" TargetType="ScrollViewer">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ScrollViewer">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ScrollContentPresenter Grid.Column="0"/>
<ScrollBar x:Name="PART_VerticalScrollBar"
Grid.Column="1"
Style="{StaticResource AboutScrollBar}"
Orientation="Vertical"
Maximum="{TemplateBinding ScrollableHeight}"
ViewportSize="{TemplateBinding ViewportHeight}"
Value="{TemplateBinding VerticalOffset}"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid Margin="20">
@@ -49,7 +119,7 @@
<Grid>
<Grid.RowDefinitions>
<!-- 상단 헤더 그라데이션 -->
<RowDefinition Height="210"/>
<RowDefinition Height="195"/>
<!-- 본문 -->
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
@@ -190,13 +260,16 @@
</Grid>
<!-- ══ 본문 ══ -->
<StackPanel Grid.Row="1" Margin="32,24,32,20">
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
Style="{StaticResource AboutScrollViewer}">
<StackPanel Margin="28,18,28,16">
<!-- 구분선 -->
<Rectangle Height="1" Fill="#F0F0F8" Margin="0,0,0,20"/>
<Rectangle Height="1" Fill="#F0F0F8" Margin="0,0,0,14"/>
<!-- 개발 목적 -->
<Border Background="#F7F8FF" CornerRadius="12" Padding="16,13" Margin="0,0,0,20">
<Border Background="#F7F8FF" CornerRadius="12" Padding="14,11" Margin="0,0,0,14">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
<TextBlock Text="&#xE946;" FontFamily="Segoe MDL2 Assets"
@@ -214,7 +287,7 @@
</Border>
<!-- 개발자 정보 -->
<StackPanel Margin="0,0,0,16">
<StackPanel Margin="0,0,0,12">
<TextBlock Text="개발자 정보"
FontSize="10.5" FontWeight="SemiBold"
Foreground="#AAAACC" Margin="0,0,0,10"/>
@@ -291,7 +364,65 @@
</StackPanel>
<!-- 구분선 -->
<Rectangle Height="1" Fill="#F0F0F8" Margin="0,0,0,14"/>
<Rectangle Height="1" Fill="#F0F0F8" Margin="0,0,0,10"/>
<!-- 오픈소스 라이선스 -->
<StackPanel Margin="0,0,0,12">
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
<TextBlock Text="&#xE90F;" FontFamily="Segoe MDL2 Assets"
FontSize="10" Foreground="#4B5EFC"
VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="오픈소스 라이선스"
FontSize="10.5" FontWeight="SemiBold"
Foreground="#AAAACC"/>
</StackPanel>
<Border Background="#FAFBFF" CornerRadius="10" Padding="14,10" Margin="0,0,0,8">
<StackPanel>
<TextBlock Text="Agentic 엔진 참조 프로젝트" FontSize="10" FontWeight="SemiBold"
Foreground="#6668AA" Margin="0,0,0,6"/>
<TextBlock FontSize="11" Foreground="#555577" TextWrapping="Wrap" LineHeight="17">
<Run FontWeight="SemiBold">OpenHands</Run>
<Run>(전 OpenDevin) — MIT License</Run>
<LineBreak/>
<Run Foreground="#9999BB">© 2024 OpenHands Contributors</Run>
<LineBreak/>
<Run FontWeight="SemiBold">OpenCode</Run>
<Run> — Apache License 2.0</Run>
<LineBreak/>
<Run Foreground="#9999BB">© 2024 OpenCode Contributors</Run>
</TextBlock>
</StackPanel>
</Border>
<Border Background="#FAFBFF" CornerRadius="10" Padding="14,10">
<StackPanel>
<TextBlock Text="주요 라이브러리" FontSize="10" FontWeight="SemiBold"
Foreground="#6668AA" Margin="0,0,0,6"/>
<TextBlock FontSize="10.5" Foreground="#555577" TextWrapping="Wrap" LineHeight="16">
<Run FontWeight="SemiBold">Markdig</Run><Run> — BSD 2-Clause</Run>
<Run Foreground="#CCCCDD"> | </Run>
<Run FontWeight="SemiBold">DocumentFormat.OpenXml</Run><Run> — MIT</Run>
<LineBreak/>
<Run FontWeight="SemiBold">UglyToad.PdfPig</Run><Run> — Apache 2.0</Run>
<Run Foreground="#CCCCDD"> | </Run>
<Run FontWeight="SemiBold">QRCoder</Run><Run> — MIT</Run>
<LineBreak/>
<Run FontWeight="SemiBold">WebView2</Run><Run> — MIT</Run>
<Run Foreground="#CCCCDD"> | </Run>
<Run FontWeight="SemiBold">Microsoft.Data.Sqlite</Run><Run> — MIT</Run>
</TextBlock>
</StackPanel>
</Border>
<TextBlock Text="모든 오픈소스 라이선스는 상업적 사용을 허용하며, 원저작자 표기를 포함합니다."
FontSize="9.5" Foreground="#BBBBCC" Margin="0,8,0,0"
TextWrapping="Wrap" LineHeight="14"
TextAlignment="Center" HorizontalAlignment="Center"/>
</StackPanel>
<!-- 구분선 -->
<Rectangle Height="1" Fill="#F0F0F8" Margin="0,0,0,10"/>
<!-- 버전/빌드 정보 -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
@@ -304,6 +435,7 @@
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</Grid>

View File

@@ -135,8 +135,8 @@
CornerRadius="10"
BorderThickness="1"
BorderBrush="{DynamicResource BorderColor}"
Padding="10,7"
Margin="0,0,8,0"
Padding="12,8"
Margin="0,0,10,0"
MouseLeftButtonUp="AgentTabBasicCard_MouseLeftButtonUp">
<TextBlock Text="기본" FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
</Border>
@@ -145,8 +145,8 @@
CornerRadius="10"
BorderThickness="1"
BorderBrush="{DynamicResource BorderColor}"
Padding="10,7"
Margin="0,0,8,0"
Padding="12,8"
Margin="0,0,10,0"
MouseLeftButtonUp="AgentTabChatCard_MouseLeftButtonUp">
<TextBlock Text="채팅" FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
</Border>
@@ -155,8 +155,8 @@
CornerRadius="10"
BorderThickness="1"
BorderBrush="{DynamicResource BorderColor}"
Padding="10,7"
Margin="0,0,8,0"
Padding="12,8"
Margin="0,0,10,0"
MouseLeftButtonUp="AgentTabCoworkCard_MouseLeftButtonUp">
<TextBlock Text="코워크" FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
</Border>
@@ -165,8 +165,8 @@
CornerRadius="10"
BorderThickness="1"
BorderBrush="{DynamicResource BorderColor}"
Padding="10,7"
Margin="0,0,8,0"
Padding="12,8"
Margin="0,0,10,0"
MouseLeftButtonUp="AgentTabCodeCard_MouseLeftButtonUp">
<TextBlock Text="코드" FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
</Border>
@@ -175,8 +175,8 @@
CornerRadius="10"
BorderThickness="1"
BorderBrush="{DynamicResource BorderColor}"
Padding="10,7"
Margin="0,0,8,0"
Padding="12,8"
Margin="0,0,10,0"
MouseLeftButtonUp="AgentTabDevCard_MouseLeftButtonUp">
<TextBlock Text="개발자" FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
</Border>
@@ -185,8 +185,8 @@
CornerRadius="10"
BorderThickness="1"
BorderBrush="{DynamicResource BorderColor}"
Padding="10,7"
Margin="0,0,8,0"
Padding="12,8"
Margin="0,0,10,0"
MouseLeftButtonUp="AgentTabToolsCard_MouseLeftButtonUp">
<TextBlock Text="도구" FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
</Border>
@@ -195,7 +195,7 @@
CornerRadius="10"
BorderThickness="1"
BorderBrush="{DynamicResource BorderColor}"
Padding="10,7"
Padding="12,8"
MouseLeftButtonUp="AgentTabEtcCard_MouseLeftButtonUp">
<TextBlock Text="스킬/차단" FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
</Border>
@@ -208,10 +208,10 @@
<StackPanel Margin="18,14,18,16">
<StackPanel x:Name="PanelBasic">
<TextBlock Text="기본 상태"
FontSize="13"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<Grid Margin="0,8,0,0" Visibility="Collapsed">
<Grid Margin="0,10,0,0" Visibility="Collapsed">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -222,14 +222,14 @@
FontSize="12"/>
<TextBlock Text="비활성화하면 AX Agent 대화와 관련 설정이 숨겨집니다."
Foreground="{DynamicResource SecondaryText}"
FontSize="11"
Margin="0,2,0,0"/>
FontSize="11.5"
Margin="0,3,0,0"/>
</StackPanel>
<CheckBox x:Name="ChkAiEnabled"
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
</Grid>
<Grid Margin="0,8,0,0" Visibility="Collapsed">
<Grid Margin="0,10,0,0" Visibility="Collapsed">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -240,8 +240,8 @@
FontSize="12"/>
<TextBlock Text="계획, 승인 카드, 보조 설명의 정보 밀도를 조정합니다."
Foreground="{DynamicResource SecondaryText}"
FontSize="11"
Margin="0,2,0,0"/>
FontSize="11.5"
Margin="0,3,0,0"/>
</StackPanel>
<WrapPanel Grid.Column="1">
<Border x:Name="DisplayModeRichCard"
@@ -276,13 +276,13 @@
</WrapPanel>
</Grid>
<Border Height="1" Margin="0,10,0,10" Background="{DynamicResource SeparatorColor}"/>
<Border Height="1" Margin="0,14,0,14" Background="{DynamicResource SeparatorColor}"/>
<TextBlock Text="테마"
FontSize="13"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<WrapPanel Margin="0,8,0,0">
<WrapPanel Margin="0,10,0,0">
<Border x:Name="ThemeSystemCard"
Cursor="Hand"
CornerRadius="10"
@@ -315,17 +315,17 @@
</Border>
</WrapPanel>
<Border Height="1" Margin="0,10,0,10" Background="{DynamicResource SeparatorColor}"/>
<Border Height="1" Margin="0,14,0,14" Background="{DynamicResource SeparatorColor}"/>
</StackPanel>
<StackPanel x:Name="PanelChat">
<TextBlock Text="모델 및 연결"
FontSize="13"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<TextBlock Text="서비스를 선택하고 모델, 연결 옵션, 운영 모드를 조정합니다."
Margin="0,4,0,10"
FontSize="11"
FontSize="11.5"
Foreground="{DynamicResource SecondaryText}"/>
<WrapPanel>
<Border x:Name="SvcOllamaCard"
@@ -372,7 +372,7 @@
<TextBox x:Name="ModelInput"
Visibility="Collapsed"
Margin="0,6,0,8"
Padding="8,6"
Padding="10,7"
Background="{DynamicResource ItemBackground}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
@@ -391,7 +391,7 @@
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
</Grid>
<Grid Margin="0,8,0,0" Visibility="Collapsed">
<Grid Margin="0,10,0,0" Visibility="Collapsed">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -418,7 +418,7 @@
Style="{StaticResource OutlineHoverBtn}"
Click="BtnOperationMode_Click"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -445,7 +445,7 @@
Style="{StaticResource OutlineHoverBtn}"
Click="BtnDefaultOutputFormat_Click"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -472,15 +472,15 @@
Click="BtnDefaultMood_Click"/>
</Grid>
<Border Height="1" Margin="0,10,0,10" Background="{DynamicResource SeparatorColor}"/>
<Border Height="1" Margin="0,14,0,14" Background="{DynamicResource SeparatorColor}"/>
</StackPanel>
<StackPanel x:Name="PanelCowork" Visibility="Collapsed">
<TextBlock Text="권한 및 실행"
FontSize="13"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -507,7 +507,7 @@
Style="{StaticResource OutlineHoverBtn}"
Click="BtnPermissionMode_Click"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -533,7 +533,7 @@
Style="{StaticResource OutlineHoverBtn}"
Click="BtnReasoningMode_Click"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -545,7 +545,7 @@
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -557,7 +557,7 @@
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -569,7 +569,7 @@
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="120"/>
@@ -591,7 +591,7 @@
</StackPanel>
<TextBox x:Name="TxtMaxAgentIterations"
Grid.Column="1"
Padding="8,5"
Padding="10,7"
Background="{DynamicResource ItemBackground}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
@@ -602,10 +602,10 @@
<StackPanel x:Name="PanelCode" Visibility="Collapsed">
<TextBlock Text="코드 실행"
FontSize="13"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -617,7 +617,7 @@
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -629,7 +629,7 @@
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -641,7 +641,7 @@
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -653,7 +653,7 @@
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -668,13 +668,13 @@
</StackPanel>
<StackPanel x:Name="PanelDev" Visibility="Collapsed">
<Border Height="1" Margin="0,10,0,10" Background="{DynamicResource SeparatorColor}"/>
<Border Height="1" Margin="0,14,0,14" Background="{DynamicResource SeparatorColor}"/>
<TextBlock Text="컨텍스트 및 오류 관리"
FontSize="13"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -686,7 +686,7 @@
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="120"/>
@@ -696,14 +696,14 @@
VerticalAlignment="Center"/>
<TextBox x:Name="TxtContextCompactTriggerPercent"
Grid.Column="1"
Padding="8,5"
Padding="10,7"
Background="{DynamicResource ItemBackground}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
Foreground="{DynamicResource PrimaryText}"
FontSize="12"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="120"/>
@@ -723,14 +723,14 @@
</StackPanel>
<TextBox x:Name="TxtMaxContextTokens"
Grid.Column="1"
Padding="8,5"
Padding="10,7"
Background="{DynamicResource ItemBackground}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
Foreground="{DynamicResource PrimaryText}"
FontSize="12"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="120"/>
@@ -750,7 +750,7 @@
</StackPanel>
<TextBox x:Name="TxtMaxRetryOnError"
Grid.Column="1"
Padding="8,5"
Padding="10,7"
Background="{DynamicResource ItemBackground}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
@@ -758,14 +758,50 @@
FontSize="12"/>
</Grid>
<Border Height="1" Margin="0,14,0,14" Background="{DynamicResource SeparatorColor}"/>
<TextBlock Text="진단 및 디버깅"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Margin="0,0,12,0">
<StackPanel Orientation="Horizontal">
<TextBlock Text="IBM+Qwen 진단 로그"
Foreground="{DynamicResource PrimaryText}"
FontSize="12"
VerticalAlignment="Center"/>
<Border Width="16" Height="16" CornerRadius="8" Background="{DynamicResource ItemHoverBackground}" Margin="6,0,0,0" Cursor="Help" VerticalAlignment="Center">
<TextBlock Text="?" FontSize="10" FontWeight="Bold" Foreground="{DynamicResource AccentColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Border.ToolTip>
<ToolTip Style="{StaticResource HelpTooltipStyle}">
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18" MaxWidth="300">IBM watsonx + Qwen 조합 사용 시 요청/응답/인증/파싱 등 상세 진단 로그를 기록합니다. 로그 파일: %APPDATA%\AxCopilot\logs\</TextBlock>
</ToolTip>
</Border.ToolTip>
</Border>
</StackPanel>
<TextBlock Text="활성화하면 [IBM진단] 태그로 인증, 요청, 응답, 파싱 과정을 상세 기록합니다."
Foreground="{DynamicResource SecondaryText}"
FontSize="11.5"
Margin="0,3,0,0"/>
</StackPanel>
<CheckBox x:Name="ChkEnableIbmDiagnosticLog"
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
</Grid>
</StackPanel>
<StackPanel x:Name="PanelTools" Visibility="Collapsed">
<TextBlock Text="도구 및 검증"
FontSize="13"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -787,7 +823,7 @@
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -809,7 +845,7 @@
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -821,7 +857,7 @@
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -833,39 +869,39 @@
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
</Grid>
<Border Height="1" Margin="0,12,0,12" Background="{DynamicResource SeparatorColor}"/>
<Border Height="1" Margin="0,14,0,14" Background="{DynamicResource SeparatorColor}"/>
<TextBlock Text="도구 노출"
FontSize="13"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<TextBlock Text="AX Agent에 노출할 도구를 내부 설정에서 바로 켜고 끕니다."
Margin="0,4,0,8"
FontSize="11"
FontSize="11.5"
Foreground="{DynamicResource SecondaryText}"/>
<StackPanel x:Name="ToolCardsPanel"/>
<Border Height="1" Margin="0,12,0,12" Background="{DynamicResource SeparatorColor}"/>
<Border Height="1" Margin="0,14,0,14" Background="{DynamicResource SeparatorColor}"/>
<TextBlock Text="도구 훅"
FontSize="13"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<StackPanel x:Name="HookListPanel" Margin="0,8,0,0"/>
<StackPanel x:Name="HookListPanel" Margin="0,10,0,0"/>
<Button Content="훅 추가"
HorizontalAlignment="Left"
Margin="0,8,0,0"
Margin="0,10,0,0"
Style="{StaticResource OutlineHoverBtn}"
Click="BtnAddHook_Click"/>
</StackPanel>
<StackPanel x:Name="PanelEtc" Visibility="Collapsed">
<TextBlock Text="스킬/차단"
FontSize="13"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<TextBlock Text="스킬 폴더와 슬래시/드래그 동작, 폴백 모델과 MCP 서버를 관리합니다."
Margin="0,4,0,8"
FontSize="11"
FontSize="11.5"
Foreground="{DynamicResource SecondaryText}"/>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
@@ -891,8 +927,8 @@
<TextBox x:Name="TxtSkillsFolderPath"
Grid.Row="1"
Grid.Column="0"
Margin="0,8,0,0"
Padding="8,5"
Margin="0,10,0,0"
Padding="10,7"
Background="{DynamicResource ItemBackground}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
@@ -911,17 +947,17 @@
Content="열기"
Click="BtnOpenSkillFolder_Click"/>
</Grid>
<Border Height="1" Margin="0,12,0,12" Background="{DynamicResource SeparatorColor}"/>
<Border Height="1" Margin="0,14,0,14" Background="{DynamicResource SeparatorColor}"/>
<TextBlock Text="로드된 스킬"
FontSize="13"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<TextBlock Text="현재 AX Agent에서 사용할 수 있는 슬래시 스킬 목록입니다."
Margin="0,4,0,8"
FontSize="11"
FontSize="11.5"
Foreground="{DynamicResource SecondaryText}"/>
<StackPanel x:Name="SkillListPanel"/>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="120"/>
@@ -941,14 +977,14 @@
</StackPanel>
<TextBox x:Name="TxtSlashPopupPageSize"
Grid.Column="1"
Padding="8,5"
Padding="10,7"
Background="{DynamicResource ItemBackground}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
Foreground="{DynamicResource PrimaryText}"
FontSize="12"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -970,7 +1006,7 @@
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
</Grid>
<Grid Margin="0,8,0,0">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
@@ -992,13 +1028,13 @@
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
</Grid>
<Border Height="1" Margin="0,12,0,12" Background="{DynamicResource SeparatorColor}"/>
<Border Height="1" Margin="0,14,0,14" Background="{DynamicResource SeparatorColor}"/>
<TextBlock Text="폴백 모델"
FontSize="13"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<StackPanel x:Name="FallbackModelsPanel" Margin="0,8,0,0"/>
<Border Height="1" Margin="0,12,0,12" Background="{DynamicResource SeparatorColor}"/>
<StackPanel x:Name="FallbackModelsPanel" Margin="0,10,0,0"/>
<Border Height="1" Margin="0,14,0,14" Background="{DynamicResource SeparatorColor}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
@@ -1006,7 +1042,7 @@
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<TextBlock Text="MCP 서버"
FontSize="13"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"
VerticalAlignment="Center"/>
@@ -1024,7 +1060,7 @@
Content="서버 추가"
Click="BtnAddMcpServer_Click"/>
</Grid>
<StackPanel x:Name="McpServerListPanel" Margin="0,8,0,0"/>
<StackPanel x:Name="McpServerListPanel" Margin="0,10,0,0"/>
</StackPanel>
</StackPanel>
</ScrollViewer>

View File

@@ -1,4 +1,5 @@
using System.Windows;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
@@ -59,6 +60,7 @@ public partial class AgentSettingsWindow : Window
TxtContextCompactTriggerPercent.Text = Math.Clamp(_llm.ContextCompactTriggerPercent, 10, 95).ToString();
TxtMaxContextTokens.Text = Math.Max(1024, _llm.MaxContextTokens).ToString();
TxtMaxRetryOnError.Text = Math.Clamp(_llm.MaxRetryOnError, 0, 10).ToString();
ChkEnableIbmDiagnosticLog.IsChecked = _llm.EnableIbmDiagnosticLog;
ChkEnableSkillSystem.IsChecked = _llm.EnableSkillSystem;
ChkEnableToolHooks.IsChecked = _llm.EnableToolHooks;
@@ -77,7 +79,8 @@ public partial class AgentSettingsWindow : Window
TxtSlashPopupPageSize.Text = Math.Clamp(_llm.SlashPopupPageSize, 3, 20).ToString();
ChkEnableDragDropAiActions.IsChecked = _llm.EnableDragDropAiActions;
ChkDragDropAutoSend.IsChecked = _llm.DragDropAutoSend;
_disabledTools = new HashSet<string>(_llm.DisabledTools ?? new(), StringComparer.OrdinalIgnoreCase);
_disabledTools = new HashSet<string>(AgentToolCatalog.CanonicalizeMany(_llm.DisabledTools ?? new()), StringComparer.OrdinalIgnoreCase);
_llm.AgentHooks = AgentToolCatalog.CanonicalizeHooks(_llm.AgentHooks);
RefreshServiceCards();
RefreshThemeCards();
@@ -141,13 +144,27 @@ public partial class AgentSettingsWindow : Window
private void ShowPanel(string panel)
{
_activePanel = panel;
PanelBasic.Visibility = panel == "basic" ? Visibility.Visible : Visibility.Collapsed;
PanelChat.Visibility = panel == "chat" ? Visibility.Visible : Visibility.Collapsed;
PanelCowork.Visibility = panel == "cowork" ? Visibility.Visible : Visibility.Collapsed;
PanelCode.Visibility = panel == "code" ? Visibility.Visible : Visibility.Collapsed;
PanelDev.Visibility = panel == "dev" ? Visibility.Visible : Visibility.Collapsed;
PanelTools.Visibility = panel == "tools" ? Visibility.Visible : Visibility.Collapsed;
PanelEtc.Visibility = panel == "etc" ? Visibility.Visible : Visibility.Collapsed;
var panels = new (FrameworkElement Element, string Key)[]
{
(PanelBasic, "basic"), (PanelChat, "chat"), (PanelCowork, "cowork"),
(PanelCode, "code"), (PanelDev, "dev"), (PanelTools, "tools"), (PanelEtc, "etc")
};
foreach (var (element, key) in panels)
{
if (key == panel)
{
element.Opacity = 0;
element.Visibility = Visibility.Visible;
element.BeginAnimation(UIElement.OpacityProperty,
new System.Windows.Media.Animation.DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(150))
{ EasingFunction = new System.Windows.Media.Animation.QuadraticEase() });
}
else
{
element.BeginAnimation(UIElement.OpacityProperty, null);
element.Visibility = Visibility.Collapsed;
}
}
RefreshTabCards();
if (panel == "tools")
LoadToolCards();
@@ -520,6 +537,7 @@ public partial class AgentSettingsWindow : Window
_llm.ContextCompactTriggerPercent = ParseInt(TxtContextCompactTriggerPercent.Text, 80, 10, 95);
_llm.MaxContextTokens = ParseInt(TxtMaxContextTokens.Text, 4096, 1024, 200000);
_llm.MaxRetryOnError = ParseInt(TxtMaxRetryOnError.Text, 3, 0, 10);
_llm.EnableIbmDiagnosticLog = ChkEnableIbmDiagnosticLog.IsChecked == true;
_llm.EnableSkillSystem = ChkEnableSkillSystem.IsChecked == true;
_llm.EnableToolHooks = ChkEnableToolHooks.IsChecked == true;
@@ -538,7 +556,10 @@ public partial class AgentSettingsWindow : Window
_llm.SlashPopupPageSize = ParseInt(TxtSlashPopupPageSize.Text, 7, 3, 20);
_llm.EnableDragDropAiActions = ChkEnableDragDropAiActions.IsChecked == true;
_llm.DragDropAutoSend = ChkDragDropAutoSend.IsChecked == true;
_llm.DisabledTools = _disabledTools.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList();
_llm.DisabledTools = AgentToolCatalog.CanonicalizeMany(_disabledTools)
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
.ToList();
_llm.AgentHooks = AgentToolCatalog.CanonicalizeHooks(_llm.AgentHooks);
_settings.Settings.AiEnabled = true;
_settings.Settings.OperationMode = OperationModePolicy.Normalize(_operationMode);
@@ -705,24 +726,13 @@ public partial class AgentSettingsWindow : Window
_toolCardsLoaded = true;
using var tools = ToolRegistry.CreateDefault();
var categories = new Dictionary<string, List<IAgentTool>>
{
["파일/검색"] = new(),
["문서/리뷰"] = new(),
["코드/개발"] = new(),
["시스템/유틸"] = new(),
};
var toolCategoryMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["file_read"] = "파일/검색", ["file_write"] = "파일/검색", ["file_edit"] = "파일/검색", ["glob"] = "파일/검색", ["grep"] = "파일/검색",
["document_review"] = "문서/리뷰", ["format_convert"] = "문서/리뷰", ["template_render"] = "문서/리뷰", ["text_summarize"] = "문서/리뷰",
["build_run"] = "코드/개발", ["git_tool"] = "코드/개발", ["lsp"] = "코드/개발", ["code_review"] = "코드/개발", ["test_loop"] = "코드/개발",
["process"] = "시스템/유틸", ["notify"] = "시스템/유틸", ["clipboard"] = "시스템/유틸", ["env"] = "시스템/유틸", ["skill_manager"] = "시스템/유틸",
};
var categories = new Dictionary<string, List<IAgentTool>>(StringComparer.OrdinalIgnoreCase);
foreach (var tool in tools.All)
{
var category = toolCategoryMap.TryGetValue(tool.Name, out var mapped) ? mapped : "시스템/유틸";
var category = AgentToolCatalog.GetMetadata(tool.Name).SettingsCategory;
if (!categories.ContainsKey(category))
categories[category] = new List<IAgentTool>();
categories[category].Add(tool);
}
@@ -866,7 +876,7 @@ public partial class AgentSettingsWindow : Window
}
var nameBox = AddField("이름", existing?.Name ?? "");
var toolBox = AddField("대상 도구 (* = 전체)", existing?.ToolName ?? "*");
var toolBox = AddField("대상 도구 (* = 전체)", AgentToolCatalog.CanonicalizeHookTarget(existing?.ToolName ?? "*"));
var pathBox = AddField("스크립트 경로", existing?.ScriptPath ?? "");
var argsBox = AddField("인수", existing?.Arguments ?? "");
@@ -886,7 +896,7 @@ public partial class AgentSettingsWindow : Window
var entry = new AgentHookEntry
{
Name = nameBox.Text.Trim(),
ToolName = string.IsNullOrWhiteSpace(toolBox.Text) ? "*" : toolBox.Text.Trim(),
ToolName = AgentToolCatalog.CanonicalizeHookTarget(toolBox.Text),
Timing = pre.IsChecked == true ? "pre" : "post",
ScriptPath = pathBox.Text.Trim(),
Arguments = argsBox.Text.Trim(),

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
@@ -18,6 +19,7 @@ public partial class ChatWindow
private int _processFeedMergeCount;
private Border? _lastExpandedDetailPanel;
private TextBlock? _lastExpandedArrow;
private TextBlock? _lastExpandedPreview;
private readonly Dictionary<TranscriptRowKind, int> _transcriptRowKindCounts = new();
private static Color ResolveLiveProgressAccentColor(Brush accentBrush)
@@ -36,6 +38,16 @@ public partial class ChatWindow
return elapsedMs > maxReasonableElapsed ? 0 : elapsedMs;
}
private static string GetPreviewLines(string text, int maxLines)
{
if (string.IsNullOrEmpty(text)) return "";
var lines = text.Split('\n');
var preview = string.Join("\n", lines.Take(maxLines));
if (lines.Length > maxLines)
preview += $"\n… +{lines.Length - maxLines}줄";
return preview;
}
private Border CreateCompactEventPill(
string summary,
Brush primaryText,
@@ -56,10 +68,10 @@ public partial class ChatWindow
BorderBrush = liveWaitingStyle
? new SolidColorBrush(Color.FromArgb(0x46, liveAccentColor.R, liveAccentColor.G, liveAccentColor.B))
: borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(10),
Padding = new Thickness(10, 8, 10, 8),
Margin = new Thickness(12, 6, 12, 2),
BorderThickness = new Thickness(0),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(10, 6, 10, 6),
Margin = new Thickness(0, 4, 0, 4),
HorizontalAlignment = HorizontalAlignment.Center,
MaxWidth = pillMaxWidth,
Child = new Grid
@@ -124,9 +136,10 @@ public partial class ChatWindow
{
Text = text,
FontSize = 14,
FontWeight = FontWeights.Medium,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
TextWrapping = TextWrapping.Wrap,
LineHeight = 20,
}
};
}
@@ -198,16 +211,20 @@ public partial class ChatWindow
}
/// <summary>이전에 펼쳐진 상세 패널을 접고, 새 패널을 펼쳐진 상태로 등록합니다.</summary>
private void CollapseLastAndExpandNew(Border? newPanel, TextBlock? newArrow)
private void CollapseLastAndExpandNew(Border? newPanel, TextBlock? newArrow, TextBlock? newPreview = null)
{
if (_lastExpandedDetailPanel != null && _lastExpandedDetailPanel.Visibility == Visibility.Visible)
{
_lastExpandedDetailPanel.Visibility = Visibility.Collapsed;
if (_lastExpandedArrow?.RenderTransform is RotateTransform rt)
rt.Angle = 0;
// Show preview for the collapsed panel
if (_lastExpandedPreview != null)
_lastExpandedPreview.Visibility = Visibility.Visible;
}
_lastExpandedDetailPanel = newPanel;
_lastExpandedArrow = newArrow;
_lastExpandedPreview = newPreview;
}
private void TrackTranscriptRowKind(TranscriptRowKind kind)
@@ -344,13 +361,18 @@ public partial class ChatWindow
stack.Children.Add(headerRow);
// 상세 내용 패널 (최신 이벤트는 펼침, 이전 이벤트는 자동 접힘)
// 상세 내용 패널 (현재 실행 중인 도구만 펼침, 나머지는 접힘 + 미리보기)
if (hasBody)
{
// 현재 스트리밍/실행 중인 도구인지 판단 (ToolCall = 실행 중, ToolResult = 완료)
var isActivelyStreaming = evt.Type == AgentEventType.ToolCall
|| evt.Type == AgentEventType.SkillCall;
var bodyPanel = new StackPanel();
var bodyText = body.Length > 600 ? body[..600] + "…" : body;
bodyPanel.Children.Add(new TextBlock
{
Text = body.Length > 600 ? body[..600] + "…" : body,
Text = bodyText,
FontSize = 11,
Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap,
@@ -373,30 +395,55 @@ public partial class ChatWindow
});
}
// ScrollViewer로 감싸서 MaxHeight 300px 제한
var detailScroll = new ScrollViewer
{
MaxHeight = 300,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
Content = bodyPanel,
};
var detailPanel = new Border
{
Visibility = Visibility.Visible,
Visibility = isActivelyStreaming ? Visibility.Visible : Visibility.Collapsed,
Background = hintBg,
BorderThickness = new Thickness(0),
Margin = new Thickness(13, 0, 0, 4),
Padding = new Thickness(10, 6, 10, 6),
CornerRadius = new CornerRadius(4),
Child = bodyPanel,
Child = detailScroll,
};
// 접힌 상태일 때 보여줄 3줄 미리보기
var previewText = new TextBlock
{
Text = GetPreviewLines(bodyText, 3),
FontSize = 11.5,
Foreground = secondaryText,
Opacity = 0.7,
TextWrapping = TextWrapping.Wrap,
MaxHeight = 54,
TextTrimming = TextTrimming.CharacterEllipsis,
Margin = new Thickness(12, 2, 12, 0),
Visibility = isActivelyStreaming ? Visibility.Collapsed : Visibility.Visible,
};
stack.Children.Add(previewText);
stack.Children.Add(detailPanel);
// 이전 패널 접고 새 패널 등록
CollapseLastAndExpandNew(detailPanel, arrowText);
if (arrowText.RenderTransform is RotateTransform initRt)
// 이전 패널 접고 새 패널 등록 (미리보기 포함)
CollapseLastAndExpandNew(detailPanel, arrowText, previewText);
if (isActivelyStreaming && arrowText.RenderTransform is RotateTransform initRt)
initRt.Angle = 90;
// 클릭으로 토글
// 클릭으로 토글 (미리보기 연동)
headerRow.Cursor = Cursors.Hand;
headerRow.MouseLeftButtonUp += (_, e) =>
{
e.Handled = true;
var isExpanded = detailPanel.Visibility == Visibility.Visible;
detailPanel.Visibility = isExpanded ? Visibility.Collapsed : Visibility.Visible;
previewText.Visibility = isExpanded ? Visibility.Visible : Visibility.Collapsed;
if (arrowText.RenderTransform is RotateTransform rt)
rt.Angle = isExpanded ? 0 : 90;
};
@@ -677,11 +724,12 @@ public partial class ChatWindow
var summaryText = new TextBlock
{
Text = summary,
FontSize = liveWaitingStyle ? 13.5 : 12.5,
FontSize = liveWaitingStyle ? 14 : 12.5,
FontWeight = liveWaitingStyle ? FontWeights.SemiBold : FontWeights.Normal,
Foreground = liveWaitingStyle ? primaryText : secondaryText,
VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.Wrap,
LineHeight = 20,
};
var metaTextBlock = new TextBlock
@@ -833,11 +881,12 @@ public partial class ChatWindow
var summaryText = new TextBlock
{
Text = summary,
FontSize = liveWaitingStyle ? 13.5 : 12.75,
FontSize = liveWaitingStyle ? 14 : 12.75,
FontWeight = liveWaitingStyle ? FontWeights.SemiBold : FontWeights.Medium,
Foreground = primaryText,
VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.Wrap,
LineHeight = 20,
};
var metaTextBlock = new TextBlock
@@ -1943,6 +1992,13 @@ public partial class ChatWindow
}
AddTranscriptElement(stack2);
// 문서 생성 도구 완료 시 Preview 패널 자동 열기 (Claude Artifacts 스타일)
if (evt.Type == AgentEventType.ToolResult && evt.Success
&& !string.IsNullOrWhiteSpace(evt.FilePath))
{
TryAutoPreviewFile(evt.FilePath, evt.ToolName);
}
}
/// <summary>완료 이벤트: 구분선 + 요약 텍스트 (테두리/배경 없이).</summary>

View File

@@ -201,10 +201,11 @@ public partial class ChatWindow
var tb = new TextBlock
{
Text = $" {text}",
FontSize = 10.5,
FontSize = 12.5,
FontFamily = new System.Windows.Media.FontFamily("Segoe UI, Malgun Gothic"),
Foreground = secondary,
Opacity = 0.60,
LineHeight = 18,
TextTrimming = TextTrimming.CharacterEllipsis,
MaxWidth = 380,
Margin = new Thickness(0, 0, 0, 1),
@@ -972,9 +973,10 @@ public partial class ChatWindow
submitEditBtn.Child = new TextBlock
{
Text = "피드백 전송",
FontSize = 12,
FontSize = 12.5,
FontWeight = FontWeights.SemiBold,
Foreground = Brushes.White
Foreground = Brushes.White,
LineHeight = 18,
};
ApplyHoverScaleAnimation(submitEditBtn, 1.05);
submitEditBtn.MouseLeftButtonUp += (_, _) =>
@@ -1076,10 +1078,11 @@ public partial class ChatWindow
var resultLabel = new TextBlock
{
Text = resultText,
FontSize = 12,
FontSize = 12.5,
FontWeight = FontWeights.SemiBold,
Foreground = fg,
Opacity = 0.8,
LineHeight = 18,
Margin = new Thickness(40, 2, 0, 2),
};
outerStack.Children.Add(resultLabel);

View File

@@ -87,7 +87,7 @@ public partial class ChatWindow
case "toggle_devmode":
var llm = _settings.Settings.Llm;
llm.DevMode = !llm.DevMode;
_settings.Save();
ScheduleSettingsSave();
UpdateAnalyzerButtonVisibility();
ShowToast(llm.DevMode ? "개발자 모드 켜짐" : "개발자 모드 꺼짐");
break;

View File

@@ -80,10 +80,17 @@ public partial class ChatWindow
return;
// 코워크/코드 탭에서 작업 폴더 미지정 시 전송 차단
if (_activeTab is "Cowork" or "Code" && string.IsNullOrWhiteSpace(GetCurrentWorkFolder()))
// ★ SendMessageAsync(line 5317)와 동일한 엄격 검사: 대화의 WorkFolder만 본다
// (전역 폴백 허용 시 Queue는 통과하나 SendMessage에서 차단되어 입력만 지워지는 버그 방지)
if (_activeTab is "Cowork" or "Code")
{
HighlightFolderSelectButton();
return;
string? convWorkFolder;
lock (_convLock) convWorkFolder = _currentConversation?.WorkFolder;
if (string.IsNullOrWhiteSpace(convWorkFolder) || !System.IO.Directory.Exists(convWorkFolder))
{
HighlightFolderSelectButton();
return;
}
}
var text = BuildComposerDraftText();
@@ -178,6 +185,15 @@ public partial class ChatWindow
DraftPreviewText.Text = string.Empty;
RebuildDraftQueuePanel(items);
// Update queue badge
var queueCount = items?.Count(i => i.State == "queued") ?? 0;
if (QueueBadge != null)
{
QueueBadge.Visibility = queueCount > 0 ? Visibility.Visible : Visibility.Collapsed;
if (QueueBadgeText != null)
QueueBadgeText.Text = queueCount.ToString();
}
}
// --- Queue panel (Codex style) ---
@@ -442,6 +458,25 @@ public partial class ChatWindow
RefreshDraftQueueUi();
}
// Pop the last queued draft back into the editor (Alt+Up shortcut)
private void PopLastQueuedDraftToEditor()
{
string? lastId = null;
lock (_convLock)
{
var session = ChatSession;
if (session != null)
{
var lastQueued = session.GetDraftQueueItems(_activeTab)
.LastOrDefault(x => string.Equals(x.State, "queued", StringComparison.OrdinalIgnoreCase));
lastId = lastQueued?.Id;
}
}
if (lastId != null)
PopDraftToEditor(lastId);
}
// --- Icon-only button (kept for compatibility) ---
private Button CreateIconButton(string icon, string tooltip, Action onClick)
=> CreateRowIconButton(icon, tooltip, onClick);

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
@@ -100,7 +101,7 @@ public partial class ChatWindow
Dispatcher.BeginInvoke(new Action(() => ShowConversationMenu(vm.Id)), DispatcherPriority.Input);
}
private void HandleConversationItemClickById(string id, bool isSelected)
private async void HandleConversationItemClickById(string id, bool isSelected)
{
try
{
@@ -119,8 +120,15 @@ public partial class ChatWindow
// 스트리밍 중이면 포괄적 정리 (ViewModel.IsStreaming, 글로우, 타이머 등 모두 리셋)
StopStreamingIfActive();
var conv = _storage.Load(id);
if (conv == null) return;
var conv = await _storage.LoadAsync(id);
if (conv == null)
{
// 파일이 손상되었거나 존재하지 않는 유령 항목 — 메타 캐시에서 제거 후 목록 갱신
LogService.Warn($"대화 로드 실패 (유령 항목 제거): {id}");
_storage.RemoveFromMetaCache(id);
RefreshConversationList();
return;
}
lock (_convLock)
{
@@ -234,11 +242,18 @@ public partial class ChatWindow
var conv = _storage.Load(tag.Id);
if (conv == null)
{
LogService.Error($"대화 로드 실패: {tag.Id} — 파일이 손상되었거나 복호화할 수 없습니다.");
ShowToast("대화 내역을 불러올 수 없습니다. 파일이 손상되었을 수 있습니다.", "\uE783");
return;
}
lock (_convLock)
{
_currentConversation = ChatSession?.SetCurrentConversation(_activeTab, conv, _storage) ?? conv;
// 저장된 대화를 다시 열 때 실행 이력을 완전히 표시 (스트리밍 중 접힌 상태 해제)
if (_currentConversation != null)
_currentConversation.ShowExecutionHistory = true;
SyncTabConversationIdsFromSession();
}
@@ -264,6 +279,8 @@ public partial class ChatWindow
lock (_convLock)
{
_currentConversation = ChatSession?.SetCurrentConversation(_activeTab, conv, _storage) ?? conv;
if (_currentConversation != null)
_currentConversation.ShowExecutionHistory = true;
SyncTabConversationIdsFromSession();
}
SaveLastConversations();
@@ -276,9 +293,9 @@ public partial class ChatWindow
Dispatcher.BeginInvoke(new Action(() => ShowConversationMenu(tag.Id)), DispatcherPriority.Input);
}
public void RefreshConversationList()
public async void RefreshConversationList()
{
var metas = _storage.LoadAllMeta();
var metas = await Task.Run(() => _storage.LoadAllMeta());
var allPresets = Services.PresetService.GetByTabWithCustom("Cowork", _settings.Settings.Llm.CustomPresets)
.Concat(Services.PresetService.GetByTabWithCustom("Code", _settings.Settings.Llm.CustomPresets))
.Concat(Services.PresetService.GetByTabWithCustom("Chat", _settings.Settings.Llm.CustomPresets));
@@ -421,7 +438,7 @@ public partial class ChatWindow
// 저장된 스크롤 위치 복원
if (hasScrollViewer && savedScrollOffset > 0)
{
Dispatcher.BeginInvoke(new Action(() =>
_ = Dispatcher.BeginInvoke(new Action(() =>
{
ConversationListScrollViewer?.ScrollToVerticalOffset(savedScrollOffset);
}), System.Windows.Threading.DispatcherPriority.Loaded);
@@ -720,7 +737,7 @@ public partial class ChatWindow
Background = isSelected
? new SolidColorBrush(Color.FromArgb(0x10, 0x4B, 0x5E, 0xFC))
: Brushes.Transparent,
CornerRadius = new CornerRadius(5),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(7, 4.5, 7, 4.5),
Margin = isBranch ? new Thickness(10, 1, 0, 1) : new Thickness(0, 1, 0, 1),
Cursor = Cursors.Hand,

View File

@@ -37,7 +37,7 @@ public partial class ChatWindow
BuildFileTree();
}
_settings.Save();
ScheduleSettingsSave();
}
private void BtnFileBrowserRefresh_Click(object sender, RoutedEventArgs e) => BuildFileTree();
@@ -125,6 +125,18 @@ public partial class ChatWindow
}
}
/// <summary>파일 트리 노드의 원시 데이터(디스크 I/O 결과) — UI 스레드 외부에서 안전하게 수집.</summary>
private sealed class FileTreeNodeData
{
public string FullName { get; set; } = "";
public string DisplayName { get; set; } = "";
public bool IsDirectory { get; set; }
public long Size { get; set; }
public int Depth { get; set; }
public bool HasChildren { get; set; }
public List<FileTreeNodeData> Children { get; set; } = new();
}
private void BuildFileTree()
{
// A-3: Clear 전에 핸들러 명시적 해제 — 클로저 참조로 인한 GC 방해 방지
@@ -139,8 +151,132 @@ public partial class ChatWindow
}
FileBrowserTitle.Text = $"파일 탐색기 — {Path.GetFileName(folder)}";
var count = 0;
PopulateDirectory(new DirectoryInfo(folder), FileTreeView.Items, 0, ref count);
// 로딩 상태 표시
var loadingItem = new TreeViewItem { Header = "로딩 중...", IsEnabled = false };
FileTreeView.Items.Add(loadingItem);
// I/O는 백그라운드 스레드에서 수행. UI 빌드는 Dispatcher에서.
_ = Task.Run(() =>
{
var nodes = new List<FileTreeNodeData>();
var count = 0;
try
{
CollectDirectoryNodes(new DirectoryInfo(folder), nodes, 0, ref count);
}
catch { /* 읽기 실패는 무시 */ }
Dispatcher.BeginInvoke(new Action(() =>
{
try
{
if (FileTreeView.Items.Contains(loadingItem))
FileTreeView.Items.Remove(loadingItem);
foreach (var n in nodes)
{
var item = CreateTreeViewItemFromData(n);
if (item != null) FileTreeView.Items.Add(item);
}
}
catch { }
}), DispatcherPriority.Background);
});
}
/// <summary>백그라운드 스레드에서 실행 — 파일/폴더 메타데이터만 수집 (UI 객체 생성 X).</summary>
private void CollectDirectoryNodes(DirectoryInfo dir, List<FileTreeNodeData> bucket, int depth, ref int count)
{
if (depth > 4 || count > 200) return;
try
{
foreach (var subDir in dir.GetDirectories().OrderBy(d => d.Name))
{
if (count > 200) break;
if (_ignoredDirs.Contains(subDir.Name) || subDir.Name.StartsWith('.')) continue;
count++;
var node = new FileTreeNodeData
{
FullName = subDir.FullName,
DisplayName = subDir.Name,
IsDirectory = true,
Depth = depth,
HasChildren = depth < 3,
};
if (depth >= 3)
{
CollectDirectoryNodes(subDir, node.Children, depth + 1, ref count);
}
bucket.Add(node);
}
}
catch { }
try
{
foreach (var file in dir.GetFiles().OrderBy(f => f.Name))
{
if (count > 200) break;
count++;
bucket.Add(new FileTreeNodeData
{
FullName = file.FullName,
DisplayName = file.Name,
IsDirectory = false,
Size = file.Length,
Depth = depth,
});
}
}
catch { }
}
/// <summary>UI 스레드 — FileTreeNodeData → TreeViewItem 변환.</summary>
private TreeViewItem? CreateTreeViewItemFromData(FileTreeNodeData node)
{
try
{
if (node.IsDirectory)
{
var dirItem = new TreeViewItem
{
Header = CreateSurfaceFileTreeHeader("\uED25", node.DisplayName, null),
Tag = node.FullName,
IsExpanded = node.Depth < 1,
};
if (node.HasChildren)
{
dirItem.Items.Add(new TreeViewItem { Header = "로딩 중..." });
dirItem.Expanded += FileTreeItem_Expanded;
}
else
{
foreach (var child in node.Children)
{
var childItem = CreateTreeViewItemFromData(child);
if (childItem != null) dirItem.Items.Add(childItem);
}
}
return dirItem;
}
var ext = Path.GetExtension(node.DisplayName).ToLowerInvariant();
var icon = GetFileIcon(ext);
var size = FormatFileSize(node.Size);
var fileItem = new TreeViewItem
{
Header = CreateSurfaceFileTreeHeader(icon, node.DisplayName, size),
Tag = node.FullName,
};
fileItem.MouseDoubleClick += FileTreeItem_DoubleClick;
fileItem.MouseRightButtonUp += FileTreeItem_RightClick;
return fileItem;
}
catch
{
return null;
}
}
private void PopulateDirectory(DirectoryInfo dir, ItemCollection items, int depth, ref int count)

View File

@@ -33,6 +33,30 @@ public partial class ChatWindow
private bool _fileMentionIndexBuildPending;
private const int FileMentionIndexLimit = 4000;
// 키입력마다 정규식+후보 필터링이 도는 것을 막기 위한 디바운스 타이머.
private System.Windows.Threading.DispatcherTimer? _fileMentionDebounceTimer;
private string _fileMentionPendingText = "";
private void ScheduleFileMentionRefresh(string text)
{
_fileMentionPendingText = text;
if (_fileMentionDebounceTimer == null)
{
_fileMentionDebounceTimer = new System.Windows.Threading.DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(120),
};
_fileMentionDebounceTimer.Tick += (_, _) =>
{
_fileMentionDebounceTimer!.Stop();
try { RefreshFileMentionSuggestions(_fileMentionPendingText); }
catch { }
};
}
_fileMentionDebounceTimer.Stop();
_fileMentionDebounceTimer.Start();
}
private void RefreshFileMentionSuggestions(string text)
{
if (string.Equals(_activeTab, "Chat", StringComparison.OrdinalIgnoreCase)

View File

@@ -15,135 +15,7 @@ public partial class ChatWindow
if (MessageList == null) return;
if (!string.Equals(runTab, _activeTab, StringComparison.OrdinalIgnoreCase)) return;
// V2 분기
if (_settings.Settings.Llm.EnableNewChatRendering)
{
ShowAgentLiveCardV2(runTab);
return;
}
RemoveAgentLiveCard(animated: false);
_agentLiveStartTime = DateTime.UtcNow;
_agentLiveSubItemTexts.Clear();
_agentLiveCurrentCategory = null;
var msgMaxWidth = GetMessageMaxWidth();
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var container = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
Width = msgMaxWidth,
MaxWidth = msgMaxWidth,
Margin = new Thickness(0, 4, 0, 6),
Opacity = IsLightweightLiveProgressMode(runTab) ? 1 : 0,
RenderTransform = IsLightweightLiveProgressMode(runTab)
? Transform.Identity
: new TranslateTransform(0, 8),
};
if (!IsLightweightLiveProgressMode(runTab))
{
container.BeginAnimation(UIElement.OpacityProperty,
new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(260)));
((TranslateTransform)container.RenderTransform).BeginAnimation(
TranslateTransform.YProperty,
new DoubleAnimation(8, 0, TimeSpan.FromMilliseconds(280))
{
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }
});
}
var headerGrid = new Grid { Margin = new Thickness(2, 0, 0, 3) };
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
var (agentName, _, _) = GetAgentIdentity();
var (liveIconHost, livePixels, liveGlows, liveRotate, liveScale) = CreateMiniLauncherIconEx(4.0, "none");
{
// 모든 모드에서 동일한 순차 점멸 애니메이션 적용
var canvas = liveIconHost.Children.OfType<Canvas>().FirstOrDefault();
if (canvas != null)
{
var animState = new ChatIconAnimState
{
Host = liveIconHost, Canvas = canvas, Pixels = livePixels,
Glows = liveGlows, Rotate = liveRotate, Scale = liveScale,
IsRandomMode = _settings.Settings.Launcher.EnableChatIconRandomAnimation,
};
StartChatIconAnimation(animState);
}
}
Grid.SetColumn(liveIconHost, 0);
headerGrid.Children.Add(liveIconHost);
var nameTb = new TextBlock
{
Text = agentName,
FontSize = 11,
FontWeight = FontWeights.SemiBold,
Foreground = secondaryText,
Margin = new Thickness(6, 0, 0, 0),
VerticalAlignment = VerticalAlignment.Center,
};
Grid.SetColumn(nameTb, 1);
headerGrid.Children.Add(nameTb);
_agentLiveElapsedText = new TextBlock
{
Text = "",
FontSize = 10,
Foreground = secondaryText,
Opacity = 0.50,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
};
Grid.SetColumn(_agentLiveElapsedText, 2);
headerGrid.Children.Add(_agentLiveElapsedText);
container.Children.Add(headerGrid);
var card = new Border
{
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(12),
Padding = new Thickness(13, 10, 13, 10),
};
card.SetResourceReference(Border.BackgroundProperty, "ItemBackground");
var cardStack = new StackPanel();
_agentLiveStatusText = new TextBlock
{
Text = "준비 중...",
FontSize = 12,
FontFamily = s_segoeUiFont,
Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap,
};
cardStack.Children.Add(_agentLiveStatusText);
_agentLiveSubItems = new StackPanel { Margin = new Thickness(0, 6, 0, 0) };
cardStack.Children.Add(_agentLiveSubItems);
card.Child = cardStack;
container.Children.Add(card);
_agentLiveContainer = container;
AddTranscriptElement(container);
ForceScrollToEnd();
_agentLiveElapsedTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_agentLiveElapsedTimer.Tick += (_, _) =>
{
if (_agentLiveElapsedText == null)
return;
var sec = (int)(DateTime.UtcNow - _agentLiveStartTime).TotalSeconds;
_agentLiveElapsedText.Text = sec > 0 ? $"{sec}초 경과" : "";
};
_agentLiveElapsedTimer.Start();
ShowAgentLiveCardV2(runTab);
}
private void UpdateAgentLiveCard(string message, string? subItem = null,

View File

@@ -4,6 +4,7 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using AxCopilot.Models;
using AxCopilot.Services;
@@ -40,19 +41,18 @@ public partial class ChatWindow
var msgMaxWidth = GetMessageMaxWidth();
var wrapper = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
Width = msgMaxWidth,
MaxWidth = msgMaxWidth,
Margin = new Thickness(0, 4, 0, 6),
HorizontalAlignment = HorizontalAlignment.Right,
MaxWidth = msgMaxWidth * 0.85,
Margin = new Thickness(60, 8, 12, 10), // 좌측 여백 넓게 → 우측에 붙음
};
var bubble = new Border
{
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(14),
Padding = new Thickness(14, 10, 14, 10),
HorizontalAlignment = HorizontalAlignment.Stretch,
BorderThickness = new Thickness(0),
CornerRadius = new CornerRadius(18),
Padding = new Thickness(16, 12, 16, 12),
HorizontalAlignment = HorizontalAlignment.Right,
};
// DynamicResource 방식으로 바인딩 — 테마 전환 시 기존 버블도 자동 업데이트
bubble.SetResourceReference(Border.BackgroundProperty, "HintBackground");
@@ -73,10 +73,10 @@ public partial class ChatWindow
{
Text = content,
TextAlignment = TextAlignment.Left,
FontSize = 12,
FontSize = 14,
Foreground = primaryText,
TextWrapping = TextWrapping.Wrap,
LineHeight = 18,
LineHeight = 22,
};
}
@@ -86,7 +86,7 @@ public partial class ChatWindow
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Opacity = 0.8,
Opacity = 0,
Margin = new Thickness(0, 2, 0, 0),
};
var capturedUserContent = content;
@@ -110,7 +110,7 @@ public partial class ChatWindow
{
Text = timestamp.ToString("HH:mm"),
FontSize = 10.5,
Opacity = 0.52,
Opacity = 0,
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
@@ -119,8 +119,21 @@ public partial class ChatWindow
Grid.SetColumn(timestampText, 2);
userBottomBar.Children.Add(timestampText);
wrapper.Children.Add(userBottomBar);
wrapper.MouseEnter += (_, _) => userActionBar.Opacity = 1;
wrapper.MouseLeave += (_, _) => userActionBar.Opacity = ReferenceEquals(_selectedMessageActionBar, userActionBar) ? 1 : 0.8;
wrapper.MouseEnter += (_, _) =>
{
userActionBar.BeginAnimation(OpacityProperty,
new DoubleAnimation(0.7, TimeSpan.FromMilliseconds(150)));
timestampText.BeginAnimation(OpacityProperty,
new DoubleAnimation(0.5, TimeSpan.FromMilliseconds(150)));
};
wrapper.MouseLeave += (_, _) =>
{
var targetOpacity = ReferenceEquals(_selectedMessageActionBar, userActionBar) ? 1.0 : 0.0;
userActionBar.BeginAnimation(OpacityProperty,
new DoubleAnimation(targetOpacity, TimeSpan.FromMilliseconds(200)));
timestampText.BeginAnimation(OpacityProperty,
new DoubleAnimation(0, TimeSpan.FromMilliseconds(200)));
};
wrapper.MouseLeftButtonUp += (_, _) => SelectMessageActionBar(userActionBar, bubble);
var userContent = content;
@@ -150,10 +163,9 @@ public partial class ChatWindow
var assistantMaxWidth = GetMessageMaxWidth();
var container = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
Width = assistantMaxWidth,
HorizontalAlignment = HorizontalAlignment.Left,
MaxWidth = assistantMaxWidth,
Margin = new Thickness(0, 6, 0, 6),
Margin = new Thickness(12, 10, 60, 10), // 우측 여백 넓게 → 좌측에 붙음
};
if (animate)
ApplyMessageEntryAnimation(container);
@@ -165,10 +177,10 @@ public partial class ChatWindow
header.Children.Add(new TextBlock
{
Text = agentName,
FontSize = 11,
FontWeight = FontWeights.SemiBold,
FontSize = 12,
FontWeight = FontWeights.Medium,
Foreground = secondaryText,
Margin = new Thickness(4, 0, 0, 0),
Margin = new Thickness(6, 0, 0, 0),
VerticalAlignment = VerticalAlignment.Center,
});
// 아이콘 애니메이션 적용
@@ -327,7 +339,7 @@ public partial class ChatWindow
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Left,
Margin = new Thickness(2, 2, 0, 0),
Opacity = 0.8,
Opacity = 0,
};
var btnColor = secondaryText;
var capturedContent = content;
@@ -356,8 +368,17 @@ public partial class ChatWindow
if (assistantMeta != null)
container.Children.Add(assistantMeta);
container.MouseEnter += (_, _) => actionBar.Opacity = 1;
container.MouseLeave += (_, _) => actionBar.Opacity = ReferenceEquals(_selectedMessageActionBar, actionBar) ? 1 : 0.8;
container.MouseEnter += (_, _) =>
{
actionBar.BeginAnimation(OpacityProperty,
new DoubleAnimation(1, TimeSpan.FromMilliseconds(150)));
};
container.MouseLeave += (_, _) =>
{
var targetOpacity = ReferenceEquals(_selectedMessageActionBar, actionBar) ? 1.0 : 0.0;
actionBar.BeginAnimation(OpacityProperty,
new DoubleAnimation(targetOpacity, TimeSpan.FromMilliseconds(200)));
};
container.MouseLeftButtonUp += (_, _) => SelectMessageActionBar(actionBar, contentCard);
var aiContent = content;

View File

@@ -810,17 +810,17 @@ public partial class ChatWindow
}
element.Opacity = 0;
element.RenderTransform = new TranslateTransform(0, 16);
element.RenderTransform = new TranslateTransform(0, 10);
element.BeginAnimation(UIElement.OpacityProperty,
new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(350))
new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(250))
{
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }
});
((TranslateTransform)element.RenderTransform).BeginAnimation(
TranslateTransform.YProperty,
new DoubleAnimation(16, 0, TimeSpan.FromMilliseconds(400))
new DoubleAnimation(10, 0, TimeSpan.FromMilliseconds(280))
{
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }
});
}

View File

@@ -261,7 +261,7 @@ public partial class ChatWindow
if (_isInlineSettingsSyncing)
return;
_settings.Settings.Llm.Service = capturedService;
_settings.Save();
ScheduleSettingsSave();
UpdateModelLabel();
RefreshInlineSettingsPanel();
};
@@ -331,7 +331,7 @@ public partial class ChatWindow
if (_isInlineSettingsSyncing)
return;
_settings.Settings.Llm.Model = capturedId;
_settings.Save();
ScheduleSettingsSave();
UpdateModelLabel();
RefreshInlineSettingsPanel();
SetStatus($"모델 전환: {capturedLabel}", spinning: false);
@@ -360,8 +360,27 @@ public partial class ChatWindow
private void OpenAgentSettingsWindow()
{
RefreshOverlaySettingsPanel();
// Scale + fade-in animation
AgentSettingsOverlay.RenderTransform = new ScaleTransform(0.98, 0.98);
AgentSettingsOverlay.RenderTransformOrigin = new Point(0.5, 0.5);
AgentSettingsOverlay.Opacity = 0;
AgentSettingsOverlay.Visibility = Visibility.Visible;
var ease = new System.Windows.Media.Animation.CubicEase();
var scaleXAnim = new System.Windows.Media.Animation.DoubleAnimation(0.98, 1.0, TimeSpan.FromMilliseconds(200)) { EasingFunction = ease };
var scaleYAnim = new System.Windows.Media.Animation.DoubleAnimation(0.98, 1.0, TimeSpan.FromMilliseconds(200)) { EasingFunction = ease };
var fadeAnim = new System.Windows.Media.Animation.DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(180));
((ScaleTransform)AgentSettingsOverlay.RenderTransform).BeginAnimation(ScaleTransform.ScaleXProperty, scaleXAnim);
((ScaleTransform)AgentSettingsOverlay.RenderTransform).BeginAnimation(ScaleTransform.ScaleYProperty, scaleYAnim);
AgentSettingsOverlay.BeginAnimation(UIElement.OpacityProperty, fadeAnim);
InlineSettingsPanel.IsOpen = false;
// 탭 RadioButton을 "공통"으로 확실히 리셋 — 재진입 시 탭/패널 불일치 방지
if (OverlayNavBasic != null)
OverlayNavBasic.IsChecked = true;
SetOverlaySection("basic");
Dispatcher.BeginInvoke(() =>
{
@@ -425,12 +444,14 @@ public partial class ChatWindow
llm.EnableHookPermissionUpdate = ChkOverlayEnableHookPermissionUpdate?.IsChecked == true;
llm.EnableCoworkVerification = ChkOverlayEnableCoworkVerification?.IsChecked == true;
llm.CoworkOnComplete = (CmbOverlayCoworkOnComplete?.SelectedItem as System.Windows.Controls.ComboBoxItem)?.Tag?.ToString() ?? "none";
llm.AutoPreview = (CmbOverlayAutoPreview?.SelectedItem as System.Windows.Controls.ComboBoxItem)?.Tag?.ToString() ?? "off";
llm.Code.EnableCodeVerification = ChkOverlayEnableCodeVerification?.IsChecked == true;
llm.Code.EnableCodeReview = ChkOverlayEnableCodeReview?.IsChecked == true;
llm.EnableImageInput = ChkOverlayEnableImageInput?.IsChecked == true;
llm.EnableParallelTools = ChkOverlayEnableParallelTools?.IsChecked == true;
llm.EnableProjectRules = ChkOverlayEnableProjectRules?.IsChecked == true;
llm.EnableAgentMemory = ChkOverlayEnableAgentMemory?.IsChecked == true;
llm.EnableIbmDiagnosticLog = ChkOverlayEnableIbmDiagnosticLog?.IsChecked == true;
llm.Code.EnableWorktreeTools = ChkOverlayEnableWorktreeTools?.IsChecked == true;
llm.Code.EnableTeamTools = ChkOverlayEnableTeamTools?.IsChecked == true;
llm.Code.EnableCronTools = ChkOverlayEnableCronTools?.IsChecked == true;
@@ -442,8 +463,7 @@ public partial class ChatWindow
_settings.Settings.Launcher.EnableChatIconRandomAnimation = ChkOverlayEnableChatIconRandomAnim?.IsChecked == true;
_settings.Settings.Launcher.ChatIconGlowIntensity =
(CmbOverlayChatIconGlow?.SelectedItem as System.Windows.Controls.ComboBoxItem)?.Tag?.ToString() ?? "medium";
llm.EnableNewPlanViewer = ChkOverlayEnableNewPlanViewer?.IsChecked == true;
llm.EnableNewChatRendering = ChkOverlayEnableNewChatRendering?.IsChecked == true;
// V2 뷰어/렌더링 전환 완료 — 항상 V2 사용
CommitOverlayEndpointInput(normalizeOnInvalid: true);
CommitOverlayApiKeyInput();
@@ -467,7 +487,16 @@ public partial class ChatWindow
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
if (closeOverlay)
AgentSettingsOverlay.Visibility = Visibility.Collapsed;
{
var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(120));
fadeOut.Completed += (_, _) =>
{
AgentSettingsOverlay.Visibility = Visibility.Collapsed;
AgentSettingsOverlay.BeginAnimation(UIElement.OpacityProperty, null);
AgentSettingsOverlay.Opacity = 1;
};
AgentSettingsOverlay.BeginAnimation(UIElement.OpacityProperty, fadeOut);
}
if (showToast)
ShowToast("AX Agent 설정이 저장되었습니다.");
InputBox.Focus();
@@ -475,7 +504,7 @@ public partial class ChatWindow
private void PersistOverlaySettingsState(bool refreshOverlayDeferredInputs)
{
_settings.Save();
ScheduleSettingsSave();
_appState.LoadFromSettings(_settings);
ApplyAgentThemeResources();
UpdatePermissionUI();
@@ -628,6 +657,7 @@ public partial class ChatWindow
if (ChkOverlayEnableCoworkVerification != null)
ChkOverlayEnableCoworkVerification.IsChecked = llm.EnableCoworkVerification;
SelectComboBoxByTag(CmbOverlayCoworkOnComplete, llm.CoworkOnComplete);
SelectComboBoxByTag(CmbOverlayAutoPreview, llm.AutoPreview);
if (ChkOverlayEnableCodeVerification != null)
ChkOverlayEnableCodeVerification.IsChecked = llm.Code.EnableCodeVerification;
if (ChkOverlayEnableCodeReview != null)
@@ -640,6 +670,8 @@ public partial class ChatWindow
ChkOverlayEnableProjectRules.IsChecked = llm.EnableProjectRules;
if (ChkOverlayEnableAgentMemory != null)
ChkOverlayEnableAgentMemory.IsChecked = llm.EnableAgentMemory;
if (ChkOverlayEnableIbmDiagnosticLog != null)
ChkOverlayEnableIbmDiagnosticLog.IsChecked = llm.EnableIbmDiagnosticLog;
if (ChkOverlayEnableWorktreeTools != null)
ChkOverlayEnableWorktreeTools.IsChecked = llm.Code.EnableWorktreeTools;
if (ChkOverlayEnableTeamTools != null)
@@ -664,10 +696,7 @@ public partial class ChatWindow
ChkOverlayEnableChatIconRandomAnim.IsChecked = _settings.Settings.Launcher.EnableChatIconRandomAnimation;
if (CmbOverlayChatIconGlow != null)
SelectComboBoxByTag(CmbOverlayChatIconGlow, _settings.Settings.Launcher.ChatIconGlowIntensity ?? "medium");
if (ChkOverlayEnableNewPlanViewer != null)
ChkOverlayEnableNewPlanViewer.IsChecked = llm.EnableNewPlanViewer;
if (ChkOverlayEnableNewChatRendering != null)
ChkOverlayEnableNewChatRendering.IsChecked = llm.EnableNewChatRendering;
// V2 뷰어/렌더링 전환 완료 — 토글 제거됨
}
RefreshOverlayThemeCards();
@@ -1583,6 +1612,8 @@ public partial class ChatWindow
OverlayToggleCoworkVerification.Visibility = showCowork ? Visibility.Visible : Visibility.Collapsed;
if (OverlayToggleCoworkOnComplete != null)
OverlayToggleCoworkOnComplete.Visibility = showCowork ? Visibility.Visible : Visibility.Collapsed;
if (OverlayToggleAutoPreview != null)
OverlayToggleAutoPreview.Visibility = showCowork ? Visibility.Visible : Visibility.Collapsed;
if (OverlayToggleCodeVerification != null)
OverlayToggleCodeVerification.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed;
if (OverlayToggleCodeReview != null)
@@ -1593,6 +1624,8 @@ public partial class ChatWindow
OverlayToggleProjectRules.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
if (OverlayToggleAgentMemory != null)
OverlayToggleAgentMemory.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
if (OverlayToggleIbmDiagnostic != null)
OverlayToggleIbmDiagnostic.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
if (OverlayToggleWorktreeTools != null)
OverlayToggleWorktreeTools.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed;
if (OverlayToggleTeamTools != null)
@@ -1605,8 +1638,7 @@ public partial class ChatWindow
OverlaySectionGlowEffects.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed;
if (OverlaySectionIconEffects != null)
OverlaySectionIconEffects.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed;
if (OverlaySectionPlanViewer != null)
OverlaySectionPlanViewer.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed;
// V2 전환 완료 — OverlaySectionPlanViewer 제거됨
if (showTools || showSkill || showBlock)
RefreshOverlayEtcPanels();
@@ -2814,7 +2846,7 @@ public partial class ChatWindow
=> "데이터 처리",
"clipboard_tool" or "notify_tool" or "env_tool" or "zip_tool" or "http_tool" or "open_external" or "image_analyze" or "file_watch"
=> "시스템/환경",
"spawn_agent" or "wait_agents" or "memory" or "skill_manager" or "user_ask" or "task_tracker" or "todo_write" or "task_create" or "task_get" or "task_list" or "task_update" or "task_stop" or "task_output" or "enter_plan_mode" or "exit_plan_mode" or "enter_worktree" or "exit_worktree" or "team_create" or "team_delete" or "cron_create" or "cron_delete" or "cron_list" or "suggest_actions" or "checkpoint" or "playbook"
"spawn_agent" or "spawn_agents" or "wait_agents" or "memory" or "skill_manager" or "user_ask" or "task_tracker" or "todo_write" or "task_create" or "task_get" or "task_list" or "task_update" or "task_stop" or "task_output" or "enter_plan_mode" or "exit_plan_mode" or "enter_worktree" or "exit_worktree" or "team_create" or "team_delete" or "cron_create" or "cron_delete" or "cron_list" or "suggest_actions" or "checkpoint" or "playbook"
=> "에이전트",
_ => "기타"
};
@@ -2859,7 +2891,7 @@ public partial class ChatWindow
SetOverlayCardSelection(OverlayThemeLightCard, selected == "light");
SetOverlayCardSelection(OverlayThemeDarkCard, selected == "dark");
var preset = (_settings.Settings.Llm.AgentThemePreset ?? "claude").ToLowerInvariant();
SetOverlayCardSelection(OverlayThemeStyleClawCard, preset is "claw" or "claude");
SetOverlayCardSelection(OverlayThemeStyleClaudeCard, preset == "claude");
SetOverlayCardSelection(OverlayThemeStyleCodexCard, preset == "codex");
SetOverlayCardSelection(OverlayThemeStyleNordCard, preset == "nord");
SetOverlayCardSelection(OverlayThemeStyleEmberCard, preset == "ember");
@@ -3312,7 +3344,7 @@ public partial class ChatWindow
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
}
private void OverlayThemeStyleClawCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
private void OverlayThemeStyleClaudeCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_settings.Settings.Llm.AgentThemePreset = "claude";
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
@@ -3426,7 +3458,8 @@ public partial class ChatWindow
private void CmbOverlayAutoPreview_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// 제거됨 (중복 설정 항목)
if (_isOverlaySettingsSyncing) return;
ApplyOverlaySettingsChanges(showToast: false, closeOverlay: false);
}
private void CmbOverlayOperationMode_SelectionChanged(object sender, SelectionChangedEventArgs e)
@@ -3563,7 +3596,7 @@ public partial class ChatWindow
if (candidates.Count > 0 && !candidates.Any(m => m.Id == llm.Model))
llm.Model = candidates[0].Id;
_settings.Save();
ScheduleSettingsSave();
_appState.LoadFromSettings(_settings);
UpdateModelLabel();
RefreshInlineSettingsPanel();
@@ -3575,7 +3608,7 @@ public partial class ChatWindow
return;
_settings.Settings.Llm.Model = modelId;
_settings.Save();
ScheduleSettingsSave();
_appState.LoadFromSettings(_settings);
UpdateModelLabel();
RefreshInlineSettingsPanel();
@@ -3584,7 +3617,7 @@ public partial class ChatWindow
private void BtnInlineFastMode_Click(object sender, RoutedEventArgs e)
{
_settings.Settings.Llm.FreeTierMode = !_settings.Settings.Llm.FreeTierMode;
_settings.Save();
ScheduleSettingsSave();
_appState.LoadFromSettings(_settings);
RefreshInlineSettingsPanel();
RefreshOverlayVisualState(loadDeferredInputs: false);
@@ -3594,7 +3627,7 @@ public partial class ChatWindow
{
var llm = _settings.Settings.Llm;
llm.AgentDecisionLevel = NextReasoning(llm.AgentDecisionLevel);
_settings.Save();
ScheduleSettingsSave();
_appState.LoadFromSettings(_settings);
RefreshInlineSettingsPanel();
RefreshOverlayVisualState(loadDeferredInputs: false);
@@ -3604,7 +3637,7 @@ public partial class ChatWindow
{
var llm = _settings.Settings.Llm;
llm.FilePermission = NextPermission(llm.FilePermission);
_settings.Save();
ScheduleSettingsSave();
_appState.LoadFromSettings(_settings);
UpdatePermissionUI();
SaveConversationSettings();
@@ -3623,7 +3656,7 @@ public partial class ChatWindow
UpdateConditionalSkillActivation(reset: true);
}
_settings.Save();
ScheduleSettingsSave();
_appState.LoadFromSettings(_settings);
RefreshInlineSettingsPanel();

View File

@@ -85,7 +85,7 @@ public partial class ChatWindow
private void ApplyPermissionLevel(string level)
{
_settings.Settings.Llm.FilePermission = PermissionModeCatalog.NormalizeGlobalMode(level);
try { _settings.Save(); } catch { }
ScheduleSettingsSave();
_appState.LoadFromSettings(_settings);
UpdatePermissionUI();
SaveConversationSettings();
@@ -97,6 +97,14 @@ public partial class ChatWindow
private void BtnPermission_Click(object sender, RoutedEventArgs e)
{
if (PermissionPopup == null) return;
// Dynamically retarget popup to whichever button was clicked (FolderBar or Inline)
if (sender is UIElement clickedElement &&
(ReferenceEquals(clickedElement, BtnPermissionInline) || ReferenceEquals(clickedElement, BtnPermission)))
{
PermissionPopup.PlacementTarget = clickedElement;
}
InitPermissionPanelDelegation();
_lastHoveredPermBorder = null;
PermissionItems.Children.Clear();
@@ -233,7 +241,7 @@ public partial class ChatWindow
toolPermissions[existingKey ?? toolName] = PermissionModeCatalog.NormalizeToolOverride(mode);
}
try { _settings.Save(); } catch { }
ScheduleSettingsSave();
_appState.LoadFromSettings(_settings);
UpdatePermissionUI();
SaveConversationSettings();
@@ -257,7 +265,37 @@ public partial class ChatWindow
{
var map = _settings.Settings.Llm.PermissionPopupSections ??= new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
map[sectionKey] = expanded;
try { _settings.Save(); } catch { }
ScheduleSettingsSave();
}
/// <summary>Shift+Tab으로 권한 모드를 순환합니다 (Claude Code 스타일).</summary>
private void CyclePermissionMode()
{
var llm = _settings.Settings.Llm;
llm.FilePermission = NextPermission(llm.FilePermission);
ScheduleSettingsSave();
_appState.LoadFromSettings(_settings);
UpdatePermissionUI();
SaveConversationSettings();
RefreshInlineSettingsPanel();
// Toast 알림
var label = PermissionModeCatalog.ToDisplayLabel(llm.FilePermission);
var icon = PermissionModeCatalog.NormalizeGlobalMode(llm.FilePermission) switch
{
"Plan" => "\uE769",
"AcceptEdits" => "\uE73E",
"BypassPermissions" => "\uE7BA",
"Deny" => "\uE711",
_ => "\uE8D7",
};
ShowToast(label, icon);
}
private void PlanModeBannerClose_Click(object sender, MouseButtonEventArgs e)
{
if (PlanModeBanner != null)
PlanModeBanner.Visibility = Visibility.Collapsed;
}
private void BtnPermissionTopBannerClose_Click(object sender, RoutedEventArgs e)
@@ -269,18 +307,26 @@ public partial class ChatWindow
private void UpdatePermissionUI()
{
if (PermissionLabel == null || PermissionIcon == null) return;
// 계획 모드 배너 기본 숨김 — Plan 분기에서만 표시
if (PlanModeBanner != null)
PlanModeBanner.Visibility = Visibility.Collapsed;
ChatConversation? currentConversation;
lock (_convLock) currentConversation = _currentConversation;
var summary = _appState.GetPermissionSummary(currentConversation);
var perm = PermissionModeCatalog.NormalizeGlobalMode(summary.EffectiveMode);
PermissionLabel.Text = PermissionModeCatalog.ToDisplayLabel(perm);
if (PermissionLabelInline != null) PermissionLabelInline.Text = PermissionLabel.Text;
PermissionIcon.Text = perm switch
{
"AcceptEdits" => "\uE73E",
"Plan" => "\uE769",
"BypassPermissions" => "\uE7BA",
"Deny" => "\uE711",
_ => "\uE8D7",
};
if (PermissionIconInline != null) PermissionIconInline.Text = PermissionIcon.Text;
if (BtnPermission != null)
{
var operationMode = OperationModePolicy.Normalize(_settings.Settings.OperationMode);
@@ -296,7 +342,9 @@ public partial class ChatWindow
{
var activeColor = new SolidColorBrush(Color.FromRgb(0x10, 0x7C, 0x10));
PermissionLabel.Foreground = activeColor;
if (PermissionLabelInline != null) PermissionLabelInline.Foreground = activeColor;
PermissionIcon.Foreground = activeColor;
if (PermissionIconInline != null) PermissionIconInline.Foreground = activeColor;
if (BtnPermission != null)
BtnPermission.BorderBrush = BrushFromHex("#86EFAC");
if (PermissionTopBanner != null)
@@ -314,7 +362,9 @@ public partial class ChatWindow
{
var denyColor = new SolidColorBrush(Color.FromRgb(0x10, 0x7C, 0x10));
PermissionLabel.Foreground = denyColor;
if (PermissionLabelInline != null) PermissionLabelInline.Foreground = denyColor;
PermissionIcon.Foreground = denyColor;
if (PermissionIconInline != null) PermissionIconInline.Foreground = denyColor;
if (BtnPermission != null)
BtnPermission.BorderBrush = BrushFromHex("#86EFAC");
if (PermissionTopBanner != null)
@@ -324,15 +374,40 @@ public partial class ChatWindow
PermissionTopBannerIcon.Foreground = denyColor;
PermissionTopBannerTitle.Text = "현재 권한 모드 · 읽기 전용";
PermissionTopBannerTitle.Foreground = denyColor;
PermissionTopBannerText.Text = "파일 읽기만 허용하고 생성, 수정, 삭제 차단합니다.";
PermissionTopBannerText.Text = "기존 파일 읽기만 가능하며 수정/삭제 차단되고, 새 파일 생성은 가능합니다.";
PermissionTopBanner.Visibility = Visibility.Collapsed;
}
}
else if (perm == PermissionModeCatalog.Plan)
{
var planColor = new SolidColorBrush(Color.FromRgb(0xD9, 0x77, 0x06));
PermissionLabel.Foreground = planColor;
if (PermissionLabelInline != null) PermissionLabelInline.Foreground = planColor;
PermissionIcon.Foreground = planColor;
if (PermissionIconInline != null) PermissionIconInline.Foreground = planColor;
if (BtnPermission != null)
BtnPermission.BorderBrush = BrushFromHex("#FDE68A");
if (PermissionTopBanner != null)
{
PermissionTopBanner.BorderBrush = BrushFromHex("#FDE68A");
PermissionTopBannerIcon.Text = "\uE769";
PermissionTopBannerIcon.Foreground = planColor;
PermissionTopBannerTitle.Text = "현재 권한 모드 · 계획 모드";
PermissionTopBannerTitle.Foreground = planColor;
PermissionTopBannerText.Text = "파일을 읽고 분석한 뒤, 실행 전에 계획을 먼저 보여줍니다.";
PermissionTopBanner.Visibility = Visibility.Collapsed;
}
// 계획 모드 배너 표시
if (PlanModeBanner != null)
PlanModeBanner.Visibility = Visibility.Visible;
}
else if (perm == PermissionModeCatalog.BypassPermissions)
{
var autoColor = new SolidColorBrush(Color.FromRgb(0xC2, 0x41, 0x0C));
PermissionLabel.Foreground = autoColor;
if (PermissionLabelInline != null) PermissionLabelInline.Foreground = autoColor;
PermissionIcon.Foreground = autoColor;
if (PermissionIconInline != null) PermissionIconInline.Foreground = autoColor;
if (BtnPermission != null)
BtnPermission.BorderBrush = BrushFromHex("#FDBA74");
if (PermissionTopBanner != null)
@@ -351,7 +426,9 @@ public partial class ChatWindow
var defaultFg = BrushFromHex("#2563EB");
var iconFg = new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xEB));
PermissionLabel.Foreground = defaultFg;
if (PermissionLabelInline != null) PermissionLabelInline.Foreground = defaultFg;
PermissionIcon.Foreground = iconFg;
if (PermissionIconInline != null) PermissionIconInline.Foreground = iconFg;
if (BtnPermission != null)
BtnPermission.BorderBrush = BrushFromHex("#BFDBFE");
if (PermissionTopBanner != null)

View File

@@ -18,11 +18,29 @@ public partial class ChatWindow
var isToolApproval = options.Contains("확인") && !options.Contains("승인");
if (isToolApproval)
{
string? toolResult = null;
await Dispatcher.InvokeAsync(() =>
// ToolApprovalWindow.Show는 Dispatcher에서 동기 호출된다.
// 장시간 미응답 시 에이전트 루프가 멈추는 것을 방지하기 위해 10분 가드를 두고,
// 타임아웃 발생 시 CancellationToken으로 다이얼로그 자체를 닫아 UI를 정리한다.
using var toolTimeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(10));
var toolTcs = new TaskCompletionSource<string?>();
_ = Dispatcher.InvokeAsync(() =>
{
toolResult = ToolApprovalWindow.Show(this, planSummary, options);
try
{
var r = ToolApprovalWindow.Show(this, planSummary, options, toolTimeoutCts.Token);
toolTcs.TrySetResult(r);
}
catch (Exception ex)
{
toolTcs.TrySetException(ex);
}
});
var toolResult = await toolTcs.Task;
if (toolTimeoutCts.IsCancellationRequested && string.IsNullOrEmpty(toolResult))
{
// 타임아웃 — 안전한 선택(중단)으로 폴백
return options.Contains("중단") ? "중단" : (options.Contains("취소") ? "취소" : "건너뛰기");
}
return toolResult;
}
@@ -88,18 +106,9 @@ public partial class ChatWindow
if (_planViewerWindow != null && IsPlanWindowAlive())
return;
if (_settings.Settings.Llm.EnableNewPlanViewer)
{
var v2 = new PlanViewerWindowV2(this);
v2.Closing += (_, e) => { e.Cancel = true; v2.Hide(); };
_planViewerWindow = v2;
}
else
{
var v1 = new PlanViewerWindow(this);
v1.Closing += (_, e) => { e.Cancel = true; v1.Hide(); };
_planViewerWindow = v1;
}
var v2 = new PlanViewerWindowV2(this);
v2.Closing += (_, e) => { e.Cancel = true; v2.Hide(); };
_planViewerWindow = v2;
}
private bool IsPlanWindowAlive() => IsWindowAlive(_planViewerWindow as Window);

View File

@@ -103,7 +103,7 @@ public partial class ChatWindow
{
_settings.Settings.Llm.RecentWorkFolders.RemoveAll(
p => p.Equals(folderPath, StringComparison.OrdinalIgnoreCase));
_settings.Save();
ScheduleSettingsSave();
if (FolderMenuPopup.IsOpen)
ShowFolderMenu();
}));

View File

@@ -31,6 +31,16 @@ public partial class ChatWindow
private bool _webViewInitialized;
private Popup? _previewTabPopup;
// ── Code/Preview 듀얼 모드 (HTML 전용, Claude Artifacts 스타일) ──
private enum PreviewViewMode { Preview, Code }
private PreviewViewMode _previewViewMode = PreviewViewMode.Preview;
private string? _currentHtmlSourceCache; // 코드 보기용 HTML 소스 캐시
// ── 패널 슬라이드 애니메이션 ──
private DispatcherTimer? _previewAnimTimer;
private double _previewAnimTarget;
private double _previewAnimCurrent;
// ── A-2: PreviewTabPanel 이벤트 위임 ──
private bool _previewTabDelegationInitialized;
private Border? _lastHoveredPreviewTab;
@@ -42,6 +52,48 @@ public partial class ChatWindow
public Border? CloseButton { get; init; }
}
/// <summary>
/// 도구 실행 결과로 생성된 문서 파일을 Preview 패널에 자동으로 엽니다.
/// html_create, docx_create 등 문서 생성 도구 완료 시 호출됩니다.
/// </summary>
internal void TryAutoPreviewFile(string? filePath, string? toolName)
{
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
return;
var ext = Path.GetExtension(filePath);
if (!_previewableExtensions.Contains(ext))
return;
// 문서 생성 도구에서만 자동 열기
var isDocTool = toolName is "html_create" or "docx_create" or "excel_create"
or "xlsx_create" or "markdown_create" or "csv_create" or "pptx_create"
or "format_convert";
if (!isDocTool)
return;
// AutoPreview 설정 확인: off → 열지 않음, manual → 이미 열린 탭만 새로고침, auto → 자유 열기
var autoPreview = _settings.Settings.Llm.AutoPreview;
if (string.Equals(autoPreview, "off", StringComparison.OrdinalIgnoreCase))
return;
// 이미 열려 있으면 새로고침만 (manual/auto 모두)
if (_previewTabs.Contains(filePath, StringComparer.OrdinalIgnoreCase))
{
_activePreviewTab = filePath;
RebuildPreviewTabs();
LoadPreviewContent(filePath);
return;
}
// manual 모드: 패널이 이미 열려 있을 때만 새 탭 추가
if (string.Equals(autoPreview, "manual", StringComparison.OrdinalIgnoreCase)
&& PreviewPanel.Visibility != Visibility.Visible)
return;
ShowPreviewPanel(filePath);
}
private void InitPreviewTabDelegation()
{
if (_previewTabDelegationInitialized || PreviewTabPanel == null)
@@ -170,8 +222,12 @@ public partial class ChatWindow
if (PreviewColumn.Width.Value < 100)
{
PreviewColumn.Width = new GridLength(420);
var savedWidth = _settings.Settings.Llm.PreviewPanelWidth;
if (savedWidth < 200) savedWidth = 420;
SplitterColumn.Width = new GridLength(5);
PreviewPanel.Visibility = Visibility.Visible;
PreviewSplitter.Visibility = Visibility.Visible;
AnimatePreviewColumn(savedWidth);
}
PreviewPanel.Visibility = Visibility.Visible;
@@ -345,11 +401,14 @@ public partial class ChatWindow
{
var ext = Path.GetExtension(filePath).ToLowerInvariant();
SetPreviewHeader(filePath);
await UpdatePreviewModeBarAsync(filePath);
PreviewWebView.Visibility = Visibility.Collapsed;
PreviewTextScroll.Visibility = Visibility.Collapsed;
PreviewDataGrid.Visibility = Visibility.Collapsed;
PreviewEmpty.Visibility = Visibility.Collapsed;
if (BtnPreviewCopy != null) BtnPreviewCopy.Visibility = Visibility.Collapsed;
if (PreviewLineNumbers != null) PreviewLineNumbers.Text = "";
if (!File.Exists(filePath))
{
@@ -377,7 +436,7 @@ public partial class ChatWindow
case ".md":
await EnsureWebViewInitializedAsync();
var mdText = File.ReadAllText(filePath);
var mdText = await Task.Run(() => File.ReadAllText(filePath));
if (mdText.Length > 50000)
mdText = mdText[..50000];
var mdHtml = Services.Agent.TemplateService.RenderMarkdownToHtml(mdText, _selectedMood);
@@ -410,11 +469,13 @@ public partial class ChatWindow
case ".json":
case ".xml":
case ".log":
var text = File.ReadAllText(filePath);
var text = await Task.Run(() => File.ReadAllText(filePath));
if (text.Length > 50000)
text = text[..50000] + "\n\n... (이후 생략)";
PreviewTextBlock.Text = text;
UpdateLineNumbers(text);
PreviewTextScroll.Visibility = Visibility.Visible;
if (BtnPreviewCopy != null) BtnPreviewCopy.Visibility = Visibility.Visible;
break;
default:
@@ -459,6 +520,156 @@ public partial class ChatWindow
PreviewHeaderMeta.Text = state;
}
// ── Code/Preview 듀얼 모드 UI ──────────────────────────────────────
/// <summary>HTML 파일인 경우 Code/Preview 모드 바를 표시하고, 아니면 숨깁니다.</summary>
private async Task UpdatePreviewModeBarAsync(string filePath)
{
if (PreviewModeBar == null) return;
var ext = Path.GetExtension(filePath).ToLowerInvariant();
var isHtml = ext is ".html" or ".htm";
PreviewModeBar.Visibility = isHtml ? Visibility.Visible : Visibility.Collapsed;
if (isHtml)
{
_previewViewMode = PreviewViewMode.Preview;
try
{
var fi = new FileInfo(filePath);
if (fi.Exists && fi.Length < 500_000)
_currentHtmlSourceCache = await Task.Run(() => File.ReadAllText(filePath));
else
_currentHtmlSourceCache = $"(파일이 너무 큽니다: {fi.Length / 1024}KB)";
}
catch
{
_currentHtmlSourceCache = null;
}
}
else
{
_currentHtmlSourceCache = null;
}
ApplyPreviewModeTabStyle();
}
/// <summary>현재 모드에 맞게 탭 스타일(배경·글꼴)을 업데이트합니다.</summary>
private void ApplyPreviewModeTabStyle()
{
if (PreviewModeCodeTab == null || PreviewModePreviewTab == null) return;
var accentBrush = TryFindResource("AccentColor") as Brush ?? BrushFromHex("#2563EB");
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var activeBg = new SolidColorBrush(Color.FromArgb(0x18, 0x25, 0x63, 0xEB));
var isCode = _previewViewMode == PreviewViewMode.Code;
PreviewModeCodeTab.Background = isCode ? activeBg : Brushes.Transparent;
PreviewModePreviewTab.Background = !isCode ? activeBg : Brushes.Transparent;
if (PreviewModeCodeLabel != null)
{
PreviewModeCodeLabel.Foreground = isCode ? accentBrush : secondaryText;
PreviewModeCodeLabel.FontWeight = isCode ? FontWeights.SemiBold : FontWeights.Normal;
}
if (PreviewModePreviewLabel != null)
{
PreviewModePreviewLabel.Foreground = !isCode ? accentBrush : secondaryText;
PreviewModePreviewLabel.FontWeight = !isCode ? FontWeights.SemiBold : FontWeights.Normal;
}
}
private void PreviewModeCodeTab_Click(object sender, MouseButtonEventArgs e)
{
if (_previewViewMode == PreviewViewMode.Code) return;
_previewViewMode = PreviewViewMode.Code;
ApplyPreviewModeTabStyle();
ShowHtmlCodeView();
}
private void PreviewModePreviewTab_Click(object sender, MouseButtonEventArgs e)
{
if (_previewViewMode == PreviewViewMode.Preview) return;
_previewViewMode = PreviewViewMode.Preview;
ApplyPreviewModeTabStyle();
ShowHtmlPreviewView();
}
/// <summary>HTML 소스 코드 보기로 전환합니다.</summary>
private void ShowHtmlCodeView()
{
PreviewWebView.Visibility = Visibility.Collapsed;
PreviewDataGrid.Visibility = Visibility.Collapsed;
PreviewEmpty.Visibility = Visibility.Collapsed;
var source = string.IsNullOrWhiteSpace(_currentHtmlSourceCache)
? "(소스를 불러올 수 없습니다)"
: _currentHtmlSourceCache;
PreviewTextBlock.Text = source;
UpdateLineNumbers(source);
PreviewTextScroll.Visibility = Visibility.Visible;
if (BtnPreviewCopy != null) BtnPreviewCopy.Visibility = Visibility.Visible;
}
/// <summary>HTML 렌더링 미리보기로 전환합니다.</summary>
private void ShowHtmlPreviewView()
{
PreviewTextScroll.Visibility = Visibility.Collapsed;
PreviewDataGrid.Visibility = Visibility.Collapsed;
PreviewEmpty.Visibility = Visibility.Collapsed;
if (BtnPreviewCopy != null) BtnPreviewCopy.Visibility = Visibility.Collapsed;
if (!string.IsNullOrWhiteSpace(_activePreviewTab) && File.Exists(_activePreviewTab))
{
var ext = Path.GetExtension(_activePreviewTab).ToLowerInvariant();
if (ext is ".html" or ".htm")
{
try
{
PreviewWebView.Source = new Uri(_activePreviewTab);
PreviewWebView.Visibility = Visibility.Visible;
}
catch (Exception ex)
{
LogService.Warn($"HTML 미리보기 전환 실패: {ex.Message}");
PreviewEmpty.Text = "미리보기 전환 오류";
PreviewEmpty.Visibility = Visibility.Visible;
}
}
}
}
// ── 줄 번호 + 복사 ─────────────────────────────────────────
private void UpdateLineNumbers(string text)
{
if (PreviewLineNumbers == null) return;
var lineCount = text.Split('\n').Length;
var sb = new StringBuilder();
for (var i = 1; i <= lineCount; i++)
sb.AppendLine(i.ToString());
PreviewLineNumbers.Text = sb.ToString().TrimEnd();
}
private void BtnPreviewCopy_Click(object sender, RoutedEventArgs e)
{
var text = PreviewTextBlock?.Text;
if (string.IsNullOrEmpty(text)) return;
try
{
Clipboard.SetText(text);
ShowToast("클립보드에 복사했습니다", "\uE8C8");
}
catch (Exception ex)
{
LogService.Warn($"복사 실패: {ex.Message}");
}
}
private async Task EnsureWebViewInitializedAsync()
{
if (_webViewInitialized)
@@ -567,16 +778,27 @@ public partial class ChatWindow
{
_previewTabs.Clear();
_activePreviewTab = null;
_currentHtmlSourceCache = null;
if (PreviewModeBar != null)
PreviewModeBar.Visibility = Visibility.Collapsed;
if (BtnPreviewCopy != null)
BtnPreviewCopy.Visibility = Visibility.Collapsed;
if (PreviewIcon != null)
PreviewIcon.Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
UpdatePreviewChevronState();
if (PreviewHeaderTitle != null) PreviewHeaderTitle.Text = "미리보기";
if (PreviewHeaderSubtitle != null) PreviewHeaderSubtitle.Text = "선택한 파일이 여기에 표시됩니다";
if (PreviewHeaderMeta != null) PreviewHeaderMeta.Text = "파일 메타";
PreviewColumn.Width = new GridLength(0);
SplitterColumn.Width = new GridLength(0);
PreviewPanel.Visibility = Visibility.Collapsed;
PreviewSplitter.Visibility = Visibility.Collapsed;
// 현재 너비 저장 후 슬라이드 아웃 애니메이션
SavePreviewPanelWidth();
AnimatePreviewColumn(0, () =>
{
PreviewPanel.Visibility = Visibility.Collapsed;
PreviewSplitter.Visibility = Visibility.Collapsed;
SplitterColumn.Width = new GridLength(0);
});
PreviewWebView.Visibility = Visibility.Collapsed;
PreviewTextScroll.Visibility = Visibility.Collapsed;
PreviewDataGrid.Visibility = Visibility.Collapsed;
@@ -590,6 +812,53 @@ public partial class ChatWindow
}
}
// ── 슬라이드 애니메이션 ──────────────────────────────────────
private void AnimatePreviewColumn(double targetWidth, Action? onComplete = null)
{
_previewAnimTimer?.Stop();
_previewAnimTarget = targetWidth;
_previewAnimCurrent = PreviewColumn.Width.Value;
if (Math.Abs(_previewAnimCurrent - targetWidth) < 2)
{
PreviewColumn.Width = new GridLength(targetWidth);
onComplete?.Invoke();
return;
}
_previewAnimTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(12) };
_previewAnimTimer.Tick += (_, _) =>
{
_previewAnimCurrent += (_previewAnimTarget - _previewAnimCurrent) * 0.25;
if (Math.Abs(_previewAnimCurrent - _previewAnimTarget) < 2)
{
_previewAnimCurrent = _previewAnimTarget;
_previewAnimTimer.Stop();
PreviewColumn.Width = new GridLength(Math.Max(0, _previewAnimCurrent));
onComplete?.Invoke();
return;
}
PreviewColumn.Width = new GridLength(Math.Max(0, _previewAnimCurrent));
};
_previewAnimTimer.Start();
}
private void SavePreviewPanelWidth()
{
var width = PreviewColumn.ActualWidth;
if (width > 100)
{
_settings.Settings.Llm.PreviewPanelWidth = width;
ScheduleSettingsSave();
}
}
private void PreviewSplitter_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
{
SavePreviewPanelWidth();
}
private void PreviewTabBar_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (PreviewWebView.IsFocused || PreviewWebView.IsKeyboardFocusWithin)
@@ -620,19 +889,24 @@ public partial class ChatWindow
if (PreviewPanel.Visibility == Visibility.Visible)
{
PreviewPanel.Visibility = Visibility.Collapsed;
PreviewSplitter.Visibility = Visibility.Collapsed;
PreviewColumn.Width = new GridLength(0);
SplitterColumn.Width = new GridLength(0);
SavePreviewPanelWidth();
if (PreviewIcon != null) PreviewIcon.Foreground = secondaryText;
AnimatePreviewColumn(0, () =>
{
PreviewPanel.Visibility = Visibility.Collapsed;
PreviewSplitter.Visibility = Visibility.Collapsed;
SplitterColumn.Width = new GridLength(0);
});
}
else if (_previewTabs.Count > 0)
{
var savedWidth = _settings.Settings.Llm.PreviewPanelWidth;
if (savedWidth < 200) savedWidth = 420;
PreviewPanel.Visibility = Visibility.Visible;
PreviewSplitter.Visibility = Visibility.Visible;
PreviewColumn.Width = new GridLength(420);
SplitterColumn.Width = new GridLength(5);
if (PreviewIcon != null) PreviewIcon.Foreground = accentBrush;
AnimatePreviewColumn(savedWidth);
RebuildPreviewTabs();
if (_activePreviewTab != null)
LoadPreviewContent(_activePreviewTab);
@@ -875,7 +1149,7 @@ public partial class ChatWindow
_previewTabPopup.IsOpen = true;
}
private void OpenPreviewPopupWindow(string filePath)
private async void OpenPreviewPopupWindow(string filePath)
{
if (!File.Exists(filePath))
return;
@@ -927,7 +1201,7 @@ public partial class ChatWindow
var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(
userDataFolder: WebView2DataFolder);
await mdWv.EnsureCoreWebView2Async(env);
var mdSrc = File.ReadAllText(filePath);
var mdSrc = await Task.Run(() => File.ReadAllText(filePath));
if (mdSrc.Length > 100000)
mdSrc = mdSrc[..100000];
var html = Services.Agent.TemplateService.RenderMarkdownToHtml(mdSrc, mdMood);
@@ -978,7 +1252,7 @@ public partial class ChatWindow
break;
default:
var text = File.ReadAllText(filePath);
var text = await Task.Run(() => File.ReadAllText(filePath));
if (text.Length > 100000)
text = text[..100000] + "\n\n... (이후 생략)";
var sv = new ScrollViewer

View File

@@ -232,20 +232,15 @@ public partial class ChatWindow
if (viewportWidth < 200)
return false;
// claw-code처럼 메시지 축과 입력축이 같은 중심선을 공유하도록,
// 메시지 축과 입력축이 같은 중심선을 공유하도록
// 본문 폭 상한을 조금 더 낮추고 창 폭 변화에 더 부드럽게 반응시킵니다.
var contentWidth = Math.Max(360, viewportWidth - 24);
var messageWidth = Math.Clamp(contentWidth * 0.9, 360, 960);
var composerWidth = Math.Clamp(contentWidth * 0.86, 360, 900);
if (contentWidth < 760)
{
messageWidth = Math.Clamp(contentWidth - 10, 344, 820);
composerWidth = Math.Clamp(contentWidth - 14, 340, 780);
}
// 고정 최대폭 — 큰 창에서 안정적, 작은 창에서만 축소
var messageWidth = Math.Min(contentWidth - 10, 800);
var composerWidth = Math.Min(contentWidth - 24, 760);
var changed = false;
if (Math.Abs(_lastResponsiveMessageWidth - messageWidth) > 1)
if (Math.Abs(_lastResponsiveMessageWidth - messageWidth) > 8)
{
_lastResponsiveMessageWidth = messageWidth;
if (MessageList != null)
@@ -255,7 +250,7 @@ public partial class ChatWindow
changed = true;
}
if (Math.Abs(_lastResponsiveComposerWidth - composerWidth) > 1)
if (Math.Abs(_lastResponsiveComposerWidth - composerWidth) > 8)
{
_lastResponsiveComposerWidth = composerWidth;
if (ComposerShell != null)
@@ -293,6 +288,7 @@ public partial class ChatWindow
var headerGrid = new Grid { Margin = new Thickness(2, 0, 0, 2) };
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
var aiIcon = new TextBlock
@@ -320,6 +316,29 @@ public partial class ChatWindow
Grid.SetColumn(aiNameTb, 1);
headerGrid.Children.Add(aiNameTb);
// 유니코드 스피너 (에이전트 이름 옆)
var spinChars = new[] { "\u00b7", "\u2722", "\u2733", "\u2736", "\u273b", "\u273d" };
var spinIndex = 0;
var spinnerText = new TextBlock
{
Text = spinChars[0],
FontFamily = new FontFamily("Consolas"),
FontSize = 13,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(6, 0, 0, 0),
};
spinnerText.SetResourceReference(TextBlock.ForegroundProperty, "AccentColor");
var spinTimer = new System.Windows.Threading.DispatcherTimer { Interval = TimeSpan.FromMilliseconds(333) };
spinTimer.Tick += (_, _) =>
{
spinIndex = (spinIndex + 1) % spinChars.Length;
spinnerText.Text = spinChars[spinIndex];
};
spinTimer.Start();
_activeSpinnerTimer = spinTimer;
Grid.SetColumn(spinnerText, 2);
headerGrid.Children.Add(spinnerText);
// 실시간 경과 시간 (헤더 우측)
_elapsedLabel = new TextBlock
{
@@ -330,7 +349,7 @@ public partial class ChatWindow
VerticalAlignment = VerticalAlignment.Center,
Opacity = 0.5,
};
Grid.SetColumn(_elapsedLabel, 2);
Grid.SetColumn(_elapsedLabel, 3);
headerGrid.Children.Add(_elapsedLabel);
container.Children.Add(headerGrid);
@@ -601,11 +620,12 @@ public partial class ChatWindow
else
foreach (var cts in _tabStreamCts.Values) cts.Cancel();
// 대기열 정리: 실행 중 + 대기 중 항목 모두 제거 (중지는 "전부 멈춤"을 의미)
// 대기열 정리: 실행 중인 항목만 취소, 대기 중 항목은 보존
lock (_convLock)
{
_draftQueueProcessor.CancelRunning(ChatSession, _activeTab, _storage);
_draftQueueProcessor.ClearQueued(ChatSession, _activeTab, _storage);
// Queue preserved — user can manually clear or items will execute on next send
// _draftQueueProcessor.ClearQueued(ChatSession, _activeTab, _storage);
_runningDraftId = null;
}
RefreshDraftQueueUi();

View File

@@ -35,7 +35,7 @@ public partial class ChatWindow
{
var map = _settings.Settings.Llm.SlashPaletteSections ??= new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
map[sectionKey] = expanded;
try { _settings.Save(); } catch { }
ScheduleSettingsSave();
}
private bool AreAllSlashSectionsExpanded()
@@ -58,7 +58,7 @@ public partial class ChatWindow
{
_settings.Settings.Llm.FavoriteSlashCommands.Clear();
_settings.Settings.Llm.RecentSlashCommands.Clear();
try { _settings.Save(); } catch { }
ScheduleSettingsSave();
_slashPalette.SelectedIndex = GetFirstVisibleSlashIndex(_slashPalette.Matches);
RenderSlashPage();
}
@@ -104,7 +104,7 @@ public partial class ChatWindow
recent.Insert(0, cmd);
if (recent.Count > maxRecent)
recent.RemoveRange(maxRecent, recent.Count - maxRecent);
try { _settings.Save(); } catch { }
ScheduleSettingsSave();
}
private int GetFirstVisibleSlashIndex(IReadOnlyList<(string Cmd, string Label, bool IsSkill)> matches)
@@ -619,7 +619,7 @@ public partial class ChatWindow
favs.RemoveRange(maxFavorites, favs.Count - maxFavorites);
}
_settings.Save();
ScheduleSettingsSave();
if (SlashPopup.IsOpen)
{

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Text;
using AxCopilot.Models;
using AxCopilot.Services;
using AxCopilot.Services.Agent;
namespace AxCopilot.Views;
@@ -166,6 +167,9 @@ public partial class ChatWindow
sb.AppendLine("\n" + _currentConversation.SystemCommand);
}
// P4: 워크스페이스 컨텍스트 자동 생성 + 주입
sb.Append(LoadWorkspaceContext(workFolder));
// 프로젝트 문맥 파일 (AGENTS.md) 주입
sb.Append(LoadProjectContext(workFolder));
@@ -178,7 +182,7 @@ public partial class ChatWindow
// 피드백 학습 컨텍스트 주입
sb.Append(BuildFeedbackContext());
return sb.ToString();
return ApplyModelPromptAdaptation(sb.ToString());
}
private string BuildCodeSystemPrompt()
@@ -330,6 +334,9 @@ public partial class ChatWindow
sb.AppendLine("\n" + sysCmd);
}
// P4: 워크스페이스 컨텍스트 자동 생성 + 주입
sb.Append(LoadWorkspaceContext(workFolder));
// 프로젝트 문맥 파일 (AGENTS.md) 주입
sb.Append(LoadProjectContext(workFolder));
@@ -342,7 +349,52 @@ public partial class ChatWindow
// 피드백 학습 컨텍스트 주입
sb.Append(BuildFeedbackContext());
return sb.ToString();
return ApplyModelPromptAdaptation(sb.ToString());
}
/// <summary>
/// ModelPromptLevel에 따라 모델 패밀리별 프롬프트 어댑테이션을 적용합니다.
/// "off"이면 원본 그대로 반환, "basic"이면 가벼운 규칙 추가, "detailed"이면 전용 프롬프트 적용.
/// </summary>
private string ApplyModelPromptAdaptation(string basePrompt)
{
var level = _settings.Settings.Llm.ModelPromptLevel ?? "off";
if (string.Equals(level, "off", StringComparison.OrdinalIgnoreCase))
return basePrompt;
var modelFamily = ResolveCurrentModelFamily();
return ModelPromptAdapter.AdaptSystemPrompt(basePrompt, modelFamily, level);
}
/// <summary>
/// 현재 활성 모델의 프롬프트 패밀리를 결정합니다.
/// RegisteredModel.PromptFamily → 자동 감지 → "default" 순서.
/// </summary>
private string ResolveCurrentModelFamily()
{
try
{
var (_, modelName) = _llm.GetCurrentModelInfo();
// RegisteredModel에 명시적 PromptFamily가 설정되어 있으면 우선 사용
var llm = _settings.Settings.Llm;
var registered = llm.RegisteredModels.FirstOrDefault(m =>
{
var decrypted = CryptoService.DecryptIfEnabled(m.EncryptedModelName, llm.EncryptionEnabled);
return string.Equals(decrypted, modelName, StringComparison.OrdinalIgnoreCase)
|| string.Equals(m.Alias, modelName, StringComparison.OrdinalIgnoreCase);
});
if (registered != null && !string.IsNullOrWhiteSpace(registered.PromptFamily))
return registered.PromptFamily.Trim().ToLowerInvariant();
// 자동 감지
return ModelPromptAdapter.DetectModelFamily(modelName);
}
catch
{
return "default";
}
}
private static string BuildSubAgentDelegationSection(bool codeMode)

View File

@@ -88,6 +88,7 @@ public partial class ChatWindow
SelectTopic(tag.Preset);
break;
case "etc":
Services.LogService.Info($"[EmptyState] HIDE ← TopicButton_etc, tab={_activeTab}");
EmptyState.Visibility = Visibility.Collapsed;
InputBox.Focus();
break;
@@ -428,7 +429,7 @@ public partial class ChatWindow
});
}
_settings.Save();
ScheduleSettingsSave();
BuildTopicButtons();
}
@@ -491,7 +492,7 @@ public partial class ChatWindow
return;
_settings.Settings.Llm.CustomPresets.RemoveAll(item => item.Id == preset.CustomId);
_settings.Save();
ScheduleSettingsSave();
BuildTopicButtons();
};
stack.Children.Add(deleteItem);
@@ -562,6 +563,7 @@ public partial class ChatWindow
if (!hasConversation)
StartNewConversation();
ChatConversation? convToSave = null;
lock (_convLock)
{
if (_currentConversation == null)
@@ -570,25 +572,45 @@ public partial class ChatWindow
var session = ChatSession;
if (session != null)
{
// ★ 버벅임 수정: storage=null 전달로 동기 파일저장 스킵
// (아래에서 한 번에 Task.Run으로 비동기 저장)
_currentConversation = session.UpdateConversationMetadata(_activeTab, conversation =>
{
conversation.SystemCommand = preset.SystemPrompt;
conversation.Category = preset.Category;
}, _storage);
}, storage: null);
}
else
{
_currentConversation.SystemCommand = preset.SystemPrompt;
_currentConversation.Category = preset.Category;
}
convToSave = _currentConversation;
}
UpdateCategoryLabel();
SaveConversationSettings();
RefreshConversationList();
// SaveConversationSettings은 내부에서 또 동기 저장 — 여기서 인라인으로 필드만 반영하고 저장은 백그라운드로
ApplyConversationSettingsInMemory();
// 프리셋 선택은 현재 대화의 Category/SystemCommand만 바꿀 뿐,
// 대화 목록의 정렬/필터/제목에는 영향이 없으므로 RefreshConversationList 호출을 생략.
// (이전에는 매 클릭마다 _storage.LoadAllMeta() + 전체 LINQ 필터링이 UI 스레드로 돌아와 버벅임을 유발했음.)
UpdateSelectedPresetGuide();
// ★ 한 번만 비동기로 저장 — UI 스레드 차단 없음
if (convToSave != null)
{
var convCopy = convToSave;
_ = System.Threading.Tasks.Task.Run(() =>
{
try { _storage.Save(convCopy); }
catch (Exception ex) { Services.LogService.Debug($"프리셋 저장 실패: {ex.Message}"); }
});
}
if (EmptyState != null)
{
Services.LogService.Info($"[EmptyState] HIDE ← SelectTopic, tab={_activeTab}");
EmptyState.Visibility = Visibility.Collapsed;
}
InputBox.Focus();

View File

@@ -104,14 +104,12 @@ public partial class ChatWindow
_lastRenderedTimelineKeys.Clear();
_lastRenderedMessageCount = 0;
_lastRenderedEventCount = 0;
EmptyState.Visibility = System.Windows.Visibility.Visible;
StartMascotAnimation();
ShowEmptyState();
}
else
{
// 메시지가 있거나 스트리밍 중 → EmptyState 강제 숨김
EmptyState.Visibility = System.Windows.Visibility.Collapsed;
StopMascotAnimation();
HideEmptyState(animate: preserveViewport);
}
return;
}
@@ -127,77 +125,71 @@ public partial class ChatWindow
InvalidateTimelineCache();
}
EmptyState.Visibility = System.Windows.Visibility.Collapsed;
StopMascotAnimation();
HideEmptyState(animate: preserveViewport);
// V2 렌더링 분기 — 설정 토글로 Claude Code 스타일 상세 이력 UI 활성화
if (_settings.Settings.Llm.EnableNewChatRendering)
// V2 렌더링 (Claude Code 스타일 상세 이력 UI)
RenderMessagesV2(conv, visibleMessages, visibleEvents, preserveViewport,
previousScrollableHeight, previousVerticalOffset, renderStopwatch, caller);
// 렌더 완료 후 진단 — transcript에 실제로 요소가 추가되었는지 확인
var postRenderCount = GetTranscriptElementCount();
if (postRenderCount == 0 && visibleMessages.Count > 0)
Services.LogService.Warn($"[Render] POST-RENDER WARNING: transcript has 0 elements after rendering {visibleMessages.Count} messages! caller={caller}, convId={conv?.Id?[..Math.Min(8, conv?.Id?.Length ?? 0)]}");
}
/// <summary>EmptyState를 표시합니다. 진행 중인 페이드 애니메이션을 취소하고 Opacity를 복원합니다.</summary>
private void ShowEmptyState([System.Runtime.CompilerServices.CallerMemberName] string? caller = null)
{
var prev = EmptyState.Visibility;
++_emptyStateAnimationToken;
EmptyState.BeginAnimation(OpacityProperty, null);
EmptyState.Opacity = 1;
EmptyState.Visibility = System.Windows.Visibility.Visible;
Services.LogService.Info($"[EmptyState] SHOW ← {caller}, prev={prev}, opacity={EmptyState.Opacity}, tab={_activeTab}");
StartMascotAnimation();
}
/// <summary>
/// EmptyState를 숨깁니다. animate=true이면 150ms 페이드아웃, false이면 즉시 Collapsed.
/// 토큰 기반 무효화: 탭 전환이나 ShowEmptyState가 호출되면 진행 중인 Completed 콜백이 무시됩니다.
/// </summary>
private void HideEmptyState(bool animate, [System.Runtime.CompilerServices.CallerMemberName] string? caller = null)
{
if (EmptyState.Visibility != System.Windows.Visibility.Visible)
{
RenderMessagesV2(conv, visibleMessages, visibleEvents, preserveViewport,
previousScrollableHeight, previousVerticalOffset, renderStopwatch, caller);
StopMascotAnimation();
return;
}
try
Services.LogService.Info($"[EmptyState] HIDE ← {caller}, animate={animate}, tab={_activeTab}");
if (animate)
{
var renderPlan = BuildTranscriptRenderPlan(conv, visibleMessages, visibleEvents);
Services.LogService.Info($"[Render] plan: items={renderPlan.VisibleTimeline.Count}, hidden={renderPlan.HiddenCount}, " +
$"canIncremental={renderPlan.CanIncremental}, keys={renderPlan.NewKeys.Count}");
// B-3: 스트리밍 전용 빠른 경로 → 일반 인크리멘탈 → Diff(Virtual DOM) → 전체 재빌드
if (!TryApplyStreamingAppendRender(renderPlan)
&& !TryApplyIncrementalTranscriptRender(renderPlan)
&& !TryApplyDiffRender(renderPlan))
ApplyFullTranscriptRender(renderPlan);
PruneTranscriptElementCache(renderPlan.NewKeys);
_lastRenderedMessageCount = visibleMessages.Count;
_lastRenderedEventCount = visibleEvents.Count;
_lastRenderedShowHistory = renderPlan.ShowHistory;
}
catch (Exception renderEx)
{
Services.LogService.Error($"[Render] 렌더링 파이프라인 예외: {renderEx.GetType().Name}: {renderEx.Message}\n{renderEx.StackTrace}");
}
_lastRenderTicks = Environment.TickCount64; // 쓰로틀 타임스탬프 갱신
renderStopwatch.Stop();
// B-6: 스트리밍 중 로깅 빈도 축소 — 100ms 미만 렌더는 기록하지 않음 (UI 부하 감소)
if (renderStopwatch.ElapsedMilliseconds >= (_isStreaming ? 100 : 24))
{
AgentPerformanceLogService.LogMetric(
"transcript",
"render_messages",
conv.Id,
_activeTab ?? "",
renderStopwatch.ElapsedMilliseconds,
new
var token = ++_emptyStateAnimationToken;
var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(150))
{
EasingFunction = new System.Windows.Media.Animation.QuadraticEase()
};
fadeOut.Completed += (_, _) =>
{
if (token != _emptyStateAnimationToken)
{
preserveViewport,
streaming = _isStreaming,
lightweight = IsLightweightLiveProgressMode(),
visibleMessages = visibleMessages.Count,
visibleEvents = visibleEvents.Count,
transcriptElements = GetTranscriptElementCount(),
});
Services.LogService.Info($"[EmptyState] HIDE animation STALE (token {token} vs {_emptyStateAnimationToken}), tab={_activeTab}");
return;
}
EmptyState.Visibility = System.Windows.Visibility.Collapsed;
EmptyState.Opacity = 1;
Services.LogService.Info($"[EmptyState] HIDE animation COMPLETED, tab={_activeTab}");
};
EmptyState.BeginAnimation(OpacityProperty, fadeOut);
}
if (!preserveViewport)
else
{
_ = Dispatcher.InvokeAsync(ScrollTranscriptToEnd, DispatcherPriority.Background);
return;
++_emptyStateAnimationToken;
EmptyState.BeginAnimation(OpacityProperty, null);
EmptyState.Opacity = 1;
EmptyState.Visibility = System.Windows.Visibility.Collapsed;
}
_ = Dispatcher.InvokeAsync(() =>
{
if (_transcriptScrollViewer == null)
return;
var newScrollableHeight = GetTranscriptScrollableHeight();
var delta = newScrollableHeight - previousScrollableHeight;
var targetOffset = Math.Max(0, previousVerticalOffset + Math.Max(0, delta));
ScrollTranscriptToVerticalOffset(targetOffset);
}, DispatcherPriority.Background);
StopMascotAnimation();
}
}

View File

@@ -21,6 +21,26 @@ public partial class ChatWindow
{
// ─── 프로젝트 문맥 파일 (AGENTS.md) ──────────────────────────────────
/// <summary>
/// P4: 워크스페이스 컨텍스트 자동 생성 파일(.ax-context.md)을 읽어 시스템 프롬프트에 주입합니다.
/// </summary>
private static string LoadWorkspaceContext(string? workFolder)
{
if (string.IsNullOrEmpty(workFolder)) return "";
var app = System.Windows.Application.Current as App;
if (!(app?.SettingsService?.Settings.Llm.EnableAutoWorkspaceContext ?? true))
return "";
var content = Services.Agent.WorkspaceContextGenerator.LoadContext(workFolder);
if (string.IsNullOrEmpty(content)) return "";
// 비동기 자동 생성 트리거 (파일이 아직 없을 때)
_ = Task.Run(() => Services.Agent.WorkspaceContextGenerator.EnsureContextAsync(workFolder));
return $"\n## Workspace Context (auto-detected)\n{content}\n";
}
/// <summary>
/// 작업 폴더에 AGENTS.md가 있으면 내용을 읽어 시스템 프롬프트에 주입합니다.
/// 프로젝트 로컬 컨텍스트 규약 파일(AGENTS.md) 형식을 사용합니다.
@@ -111,7 +131,7 @@ public partial class ChatWindow
InputGlowBorder.Visibility = Visibility.Visible;
InputGlowBorder.Effect = new System.Windows.Media.Effects.BlurEffect { Radius = 4 };
InputGlowBorder.BeginAnimation(UIElement.OpacityProperty,
new System.Windows.Media.Animation.DoubleAnimation(0, 0.92, TimeSpan.FromMilliseconds(180)));
new System.Windows.Media.Animation.DoubleAnimation(0, 0.45, TimeSpan.FromMilliseconds(200)));
_rainbowTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(300) };
_rainbowTimer.Tick += (_, _) =>
@@ -177,21 +197,34 @@ public partial class ChatWindow
ToastIcon.Text = icon;
ToastBorder.Visibility = Visibility.Visible;
// 페이드인
// 슬라이드인 + 페이드인 (Claude 스타일)
var transform = ToastBorder.RenderTransform as System.Windows.Media.TranslateTransform;
if (transform == null)
{
transform = new System.Windows.Media.TranslateTransform();
ToastBorder.RenderTransform = transform;
}
ToastBorder.BeginAnimation(UIElement.OpacityProperty,
new System.Windows.Media.Animation.DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200)));
new System.Windows.Media.Animation.DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200))
{ EasingFunction = new System.Windows.Media.Animation.CubicEase { EasingMode = System.Windows.Media.Animation.EasingMode.EaseOut } });
transform.BeginAnimation(System.Windows.Media.TranslateTransform.YProperty,
new System.Windows.Media.Animation.DoubleAnimation(20, 0, TimeSpan.FromMilliseconds(200))
{ EasingFunction = new System.Windows.Media.Animation.CubicEase { EasingMode = System.Windows.Media.Animation.EasingMode.EaseOut } });
// 자동 숨기기 — 타이머 인스턴스를 로컬 변수로 캡처해 필드 재할당 간섭 방지
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(durationMs) };
_toastHideTimer = timer;
timer.Tick += (_, _) =>
{
if (_toastHideTimer != timer) return; // 다른 ShowToast가 교체한 경우 무시
if (_toastHideTimer != timer) return;
timer.Stop();
_toastHideTimer = null;
var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(300));
fadeOut.Completed += (_, _) => ToastBorder.Visibility = Visibility.Collapsed;
ToastBorder.BeginAnimation(UIElement.OpacityProperty, fadeOut);
var slideOut = new System.Windows.Media.Animation.DoubleAnimation(0, 20, TimeSpan.FromMilliseconds(300));
(ToastBorder.RenderTransform as System.Windows.Media.TranslateTransform)
?.BeginAnimation(System.Windows.Media.TranslateTransform.YProperty, slideOut);
};
timer.Start();
}
@@ -447,7 +480,7 @@ public partial class ChatWindow
item.MouseLeftButtonUp += (_, _) =>
{
_settings.Settings.Llm.AgentLogLevel = key;
_settings.Save();
ScheduleSettingsSave();
FormatMenuPopup.IsOpen = false;
if (_activeTab == "Cowork") BuildBottomBar();
else if (_activeTab == "Code") BuildCodeBottomBar();
@@ -668,7 +701,7 @@ public partial class ChatWindow
{
FormatMenuPopup.IsOpen = false;
_settings.Settings.Llm.DefaultOutputFormat = capturedKey;
_settings.Save();
ScheduleSettingsSave();
RefreshOverlaySettingsPanel();
BuildBottomBar();
};
@@ -815,7 +848,7 @@ public partial class ChatWindow
MoodMenuPopup.IsOpen = false;
_selectedMood = capturedMood.Key;
_settings.Settings.Llm.DefaultMood = capturedMood.Key;
_settings.Save();
ScheduleSettingsSave();
SaveConversationSettings();
RefreshOverlaySettingsPanel();
BuildBottomBar();
@@ -925,7 +958,7 @@ public partial class ChatWindow
Css = dlg.MoodCss,
});
}
_settings.Save();
ScheduleSettingsSave();
TemplateService.LoadCustomMoods(_settings.Settings.Llm.CustomMoods);
BuildBottomBar();
}
@@ -984,7 +1017,7 @@ public partial class ChatWindow
{
_settings.Settings.Llm.CustomMoods.RemoveAll(c => c.Key == moodKey);
if (_selectedMood == moodKey) _selectedMood = "modern";
_settings.Save();
ScheduleSettingsSave();
TemplateService.LoadCustomMoods(_settings.Settings.Llm.CustomMoods);
BuildBottomBar();
}

View File

@@ -185,14 +185,46 @@ public partial class ChatWindow
Margin = new Thickness(0, 6, 0, 0),
};
var detailStack = new StackPanel();
var codeBg = TryFindResource("HintBackground") as Brush ?? Brushes.DarkGray;
// 도구 입력 파라미터 (ToolInput이 있으면 표시)
var toolInput = toolCall.ToolInput;
if (!string.IsNullOrWhiteSpace(toolInput))
{
detailStack.Children.Add(new TextBlock
{
Text = "입력",
FontSize = 10,
FontWeight = FontWeights.SemiBold,
Foreground = secondaryText,
Opacity = 0.7,
Margin = new Thickness(0, 0, 0, 3),
});
detailStack.Children.Add(MarkdownRenderer.Render(
$"```json\n{TruncateForDisplay(toolInput, 1200)}\n```",
primaryText, secondaryText, accentBrush, codeBg));
}
// 도구 결과 상세 내용
var resultSummary = toolResult.Summary;
if (!string.IsNullOrWhiteSpace(resultSummary) && resultSummary.Length > 80)
if (!string.IsNullOrWhiteSpace(resultSummary))
{
var codeBg = TryFindResource("HintBackground") as Brush ?? Brushes.DarkGray;
// 입력이 이미 있으면 "결과" 라벨 추가
if (detailStack.Children.Count > 0)
{
detailStack.Children.Add(new TextBlock
{
Text = "결과",
FontSize = 10,
FontWeight = FontWeights.SemiBold,
Foreground = secondaryText,
Opacity = 0.7,
Margin = new Thickness(0, 6, 0, 3),
});
}
detailStack.Children.Add(MarkdownRenderer.Render(
$"```\n{resultSummary}\n```", primaryText, secondaryText, accentBrush, codeBg));
$"```\n{TruncateForDisplay(resultSummary, 2000)}\n```",
primaryText, secondaryText, accentBrush, codeBg));
}
// 토큰 메타 정보
@@ -215,19 +247,22 @@ public partial class ChatWindow
}
detailBorder.Child = detailStack;
// 상세 내용이 없으면 화살표도 숨김
var hasDetail = detailStack.Children.Count > 0;
cardStack.Children.Add(detailBorder);
// 펼치기 토글 화살표
// 펼치기 토글 화살표 (상세 내용이 있을 때만 표시)
var arrowTb = new TextBlock
{
Text = "\uE76C", // 아래 화살표
FontFamily = s_segoeIconFont,
FontSize = 9,
Foreground = secondaryText,
Opacity = 0.5,
Opacity = 0.7,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 3, 0, 0),
Cursor = Cursors.Hand,
Visibility = hasDetail ? Visibility.Visible : Visibility.Collapsed,
};
cardStack.Children.Add(arrowTb);
@@ -243,11 +278,20 @@ public partial class ChatWindow
arrowTb.Text = isExpanded ? "\uE76C" : "\uE76B"; // 아래↔위
};
// 호버 효과
var normalBg = hintBg;
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? hintBg;
card.MouseEnter += (_, _) => card.Background = hoverBg;
card.MouseLeave += (_, _) => card.Background = normalBg;
// 호버 효과 (부드러운 전환)
card.MouseEnter += (_, _) =>
{
card.BeginAnimation(UIElement.OpacityProperty,
new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(120)));
card.Background = TryFindResource("ItemHoverBackground") as Brush ?? hintBg;
};
card.MouseLeave += (_, _) =>
{
card.BeginAnimation(UIElement.OpacityProperty,
new DoubleAnimation(0.92, TimeSpan.FromMilliseconds(200)));
card.Background = hintBg;
};
card.Opacity = 0.92;
return outerGrid;
}
@@ -283,7 +327,7 @@ public partial class ChatWindow
var thinkLine = new Border
{
Width = 2,
Background = new SolidColorBrush(Color.FromArgb(0x40, 0x59, 0xA5, 0xF5)),
Background = new SolidColorBrush(Color.FromArgb(0x60, 0x59, 0xA5, 0xF5)),
CornerRadius = new CornerRadius(1),
Margin = new Thickness(12, 0, 8, 0),
};
@@ -292,8 +336,8 @@ public partial class ChatWindow
var thinkCard = new Border
{
Background = new SolidColorBrush(Color.FromArgb(0x0A, 0x59, 0xA5, 0xF5)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x30, 0x59, 0xA5, 0xF5)),
Background = new SolidColorBrush(Color.FromArgb(0x1C, 0x59, 0xA5, 0xF5)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, 0x59, 0xA5, 0xF5)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(10, 6, 10, 6),
@@ -316,7 +360,7 @@ public partial class ChatWindow
FontSize = 11,
FontStyle = FontStyles.Italic,
Foreground = secondaryText,
Opacity = 0.75,
Opacity = 0.88,
TextWrapping = TextWrapping.Wrap,
MaxWidth = msgMaxWidth - 60,
});
@@ -428,12 +472,12 @@ public partial class ChatWindow
var banner = new Border
{
Background = new SolidColorBrush(Color.FromArgb(0x18, 0x66, 0xBB, 0x6A)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, 0x66, 0xBB, 0x6A)),
Background = new SolidColorBrush(Color.FromArgb(0x24, 0x66, 0xBB, 0x6A)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x50, 0x66, 0xBB, 0x6A)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(12, 6, 12, 6),
Margin = new Thickness(0, 4, 0, 4),
Padding = new Thickness(14, 8, 14, 8),
Margin = new Thickness(0, 6, 0, 4),
HorizontalAlignment = HorizontalAlignment.Center,
MaxWidth = msgMaxWidth,
};
@@ -465,12 +509,12 @@ public partial class ChatWindow
var msgMaxWidth = GetMessageMaxWidth();
var banner = new Border
{
Background = new SolidColorBrush(Color.FromArgb(0x18, 0xEF, 0x53, 0x50)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, 0xEF, 0x53, 0x50)),
Background = new SolidColorBrush(Color.FromArgb(0x24, 0xEF, 0x53, 0x50)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x50, 0xEF, 0x53, 0x50)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(12, 6, 12, 6),
Margin = new Thickness(0, 4, 0, 4),
Padding = new Thickness(14, 8, 14, 8),
Margin = new Thickness(0, 6, 0, 4),
HorizontalAlignment = HorizontalAlignment.Center,
MaxWidth = msgMaxWidth,
};
@@ -598,4 +642,12 @@ public partial class ChatWindow
return path[..remaining] + ".../" + fileName;
}
/// <summary>긴 텍스트를 지정 길이로 잘라서 표시용으로 반환합니다.</summary>
private static string TruncateForDisplay(string text, int maxLength)
{
if (string.IsNullOrEmpty(text) || text.Length <= maxLength)
return text;
return text[..maxLength] + "\n… (truncated)";
}
}

View File

@@ -79,7 +79,7 @@ public partial class ChatWindow
Text = "",
FontSize = 10,
Foreground = secondaryText,
Opacity = 0.50,
Opacity = 0.70,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
};
@@ -149,8 +149,8 @@ public partial class ChatWindow
var card = new Border
{
Background = new SolidColorBrush(Color.FromArgb(0x10, accentColor.R, accentColor.G, accentColor.B)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x30, accentColor.R, accentColor.G, accentColor.B)),
Background = new SolidColorBrush(Color.FromArgb(0x1C, accentColor.R, accentColor.G, accentColor.B)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, accentColor.R, accentColor.G, accentColor.B)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(10, 6, 10, 6),
@@ -202,7 +202,8 @@ public partial class ChatWindow
_v2LiveToolCards[toolId] = card;
_v2LiveContainer.Children.Add(outerGrid);
ForceScrollToEnd();
// 사용자가 수동 스크롤 중이면 강제 스크롤하지 않음
AutoScrollIfNeeded();
break;
}
@@ -269,6 +270,30 @@ public partial class ChatWindow
: new SolidColorBrush(Color.FromArgb(0x60, 0xEF, 0x53, 0x50));
}
// ★ 섹션 종료 후 자동 접기 — 완료된 카드는 1.2초 뒤 컴팩트 형태로 축소
// 얇은 줄이 빠르게 누적되어 공간 낭비되는 문제 해결
var cardToCollapse = pendingCard;
var outerGridToCollapse = parent;
var collapseTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1200) };
collapseTimer.Tick += (_, _) =>
{
collapseTimer.Stop();
if (cardToCollapse == null) return;
// Padding 축소 + Opacity 감소로 "접힌" 느낌 연출
var padAnim = new ThicknessAnimation(
cardToCollapse.Padding,
new Thickness(8, 2, 8, 2),
TimeSpan.FromMilliseconds(200))
{ EasingFunction = new QuadraticEase() };
var opAnim = new DoubleAnimation(1.0, 0.55, TimeSpan.FromMilliseconds(200))
{ EasingFunction = new QuadraticEase() };
cardToCollapse.BeginAnimation(Border.PaddingProperty, padAnim);
cardToCollapse.BeginAnimation(UIElement.OpacityProperty, opAnim);
if (outerGridToCollapse != null)
outerGridToCollapse.Margin = new Thickness(0, 1, 0, 1);
};
collapseTimer.Start();
_v2LastLiveToolCallId = null;
}
break;
@@ -301,12 +326,13 @@ public partial class ChatWindow
FontSize = 10.5,
FontStyle = FontStyles.Italic,
Foreground = secondaryText,
Opacity = 0.65,
Opacity = 0.82,
TextTrimming = TextTrimming.CharacterEllipsis,
MaxWidth = msgMaxWidth - 60,
});
_v2LiveContainer.Children.Add(thinkRow);
ForceScrollToEnd();
// 사용자가 수동 스크롤 중이면 강제 스크롤하지 않음
AutoScrollIfNeeded();
break;
}
}

View File

@@ -29,16 +29,16 @@ public partial class ChatWindow
var msgMaxWidth = GetMessageMaxWidth();
var wrapper = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
Width = msgMaxWidth,
MaxWidth = msgMaxWidth,
Margin = new Thickness(0, 12, 0, 4),
HorizontalAlignment = HorizontalAlignment.Right,
MaxWidth = msgMaxWidth * 0.85,
Margin = new Thickness(60, 12, 12, 4),
};
// 사용자 아이콘 + 이름 헤더
var header = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Thickness(2, 0, 0, 4),
};
header.Children.Add(new TextBlock
@@ -187,7 +187,7 @@ public partial class ChatWindow
var contentPanel = new Border
{
Background = Brushes.Transparent,
Padding = new Thickness(2, 4, 2, 4),
Padding = new Thickness(6, 4, 6, 4),
};
if (IsBranchContextMessage(content))
@@ -216,7 +216,7 @@ public partial class ChatWindow
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Left,
Margin = new Thickness(2, 2, 0, 0),
Opacity = 0.7,
Opacity = 0,
};
var btnColor = secondaryText;
var capturedContent = content;
@@ -232,7 +232,7 @@ public partial class ChatWindow
{
Text = aiTimestamp.ToString("HH:mm"),
FontSize = 10,
Opacity = 0.52,
Opacity = 0.6,
Foreground = btnColor,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(4, 0, 0, 1),
@@ -245,8 +245,12 @@ public partial class ChatWindow
if (assistantMeta != null)
container.Children.Add(assistantMeta);
container.MouseEnter += (_, _) => actionBar.Opacity = 1;
container.MouseLeave += (_, _) => actionBar.Opacity = 0.7;
container.MouseEnter += (_, _) =>
actionBar.BeginAnimation(OpacityProperty,
new System.Windows.Media.Animation.DoubleAnimation(0.8, TimeSpan.FromMilliseconds(150)));
container.MouseLeave += (_, _) =>
actionBar.BeginAnimation(OpacityProperty,
new System.Windows.Media.Animation.DoubleAnimation(0, TimeSpan.FromMilliseconds(200)));
var aiContent = content;
container.MouseRightButtonUp += (_, re) =>

View File

@@ -47,19 +47,39 @@ public partial class ChatWindow
_elementCache.Clear();
}
// ★ 패널이 비어있는데 V2 캐시가 남아있는 경우 강제 리셋
// 탭 전환 시 ClearTranscriptElements()로 패널이 비워지지만
// 빈 대화 탭을 거치면 V2가 호출되지 않아 캐시가 잔류하는 버그 방지
if (GetTranscriptElementCount() == 0 && _v2LastRenderedKeys.Count > 0)
{
LogService.Info($"[V2Render] CACHE STALE RESET: panel=0 but cachedKeys={_v2LastRenderedKeys.Count}, forcing full rebuild");
_v2LastRenderedKeys.Clear();
_v2LastRenderedMessageCount = 0;
_v2LastRenderedEventCount = 0;
}
// 통합 타임라인 빌드 (메시지 + 이벤트를 시간순 병합)
var timeline = BuildV2Timeline(visibleMessages, visibleEvents);
LogService.Info($"[V2Render] timeline={timeline.Count}, msgs={visibleMessages.Count}, evts={visibleEvents.Count}, convId={conv.Id[..Math.Min(8, conv.Id.Length)]}, caller={caller}");
// 새 키 목록 생성
var newKeys = new List<string>(timeline.Count);
foreach (var item in timeline)
newKeys.Add(item.Key);
// 인크리멘탈 렌더 시도: 기존 키가 새 키의 접두사인 경우 추가분만 렌더
// ★ 패널에 실제 엘리먼트가 있어야 인크리멘탈 가능 —
// 탭 전환 시 ClearTranscriptElements()로 패널이 비워지지만
// V2 캐시(_v2LastRenderedKeys)는 유지되어 "이미 렌더됨"으로 오판하는 버그 방지
var actualElementCount = GetTranscriptElementCount();
var canIncremental = _v2LastRenderedKeys.Count > 0
&& actualElementCount > 0
&& newKeys.Count >= _v2LastRenderedKeys.Count
&& KeysArePrefixMatch(_v2LastRenderedKeys, newKeys);
LogService.Info($"[V2Render] canIncremental={canIncremental}, prevKeys={_v2LastRenderedKeys.Count}, newKeys={newKeys.Count}, preElementCount={GetTranscriptElementCount()}");
if (canIncremental)
{
// 라이브 컨테이너가 있으면 임시 제거 (맨 끝에 다시 추가)
@@ -110,6 +130,8 @@ public partial class ChatWindow
_v2LastRenderedKeys.AddRange(newKeys);
_v2LastRenderedMessageCount = visibleMessages.Count;
_v2LastRenderedEventCount = visibleEvents.Count;
LogService.Info($"[V2Render] DONE postElementCount={GetTranscriptElementCount()}, savedKeys={_v2LastRenderedKeys.Count}");
}
catch (Exception ex)
{
@@ -119,12 +141,13 @@ public partial class ChatWindow
_lastRenderTicks = Environment.TickCount64;
_lastRenderedMessageCount = visibleMessages.Count;
_lastRenderedEventCount = visibleEvents.Count;
_lastRenderedShowHistory = conv?.ShowExecutionHistory ?? true;
renderStopwatch.Stop();
if (renderStopwatch.ElapsedMilliseconds >= (_isStreaming ? 100 : 24))
{
AgentPerformanceLogService.LogMetric(
"transcript", "render_messages_v2", conv.Id, _activeTab ?? "",
"transcript", "render_messages_v2", conv?.Id ?? "", _activeTab ?? "",
renderStopwatch.ElapsedMilliseconds,
new { preserveViewport, streaming = _isStreaming, visibleMessages = visibleMessages.Count, visibleEvents = visibleEvents.Count });
}
@@ -172,12 +195,21 @@ public partial class ChatWindow
}
// 2. 실행 이벤트 추가 — ToolCall+ToolResult 쌍을 병합
// 스트리밍 중이면 라이브 카드가 이미 표시하는 현재 실행 이벤트는 타임라인에서 제외
var eventIndex = 0;
var events = visibleEvents.ToList();
var liveCardCutoff = (_isStreaming && _v2LiveContainer != null)
? _v2LiveStartTime
: DateTime.MaxValue;
for (int i = 0; i < events.Count; i++)
{
var executionEvent = events[i];
// 스트리밍 중: 라이브 카드 시작 이후 이벤트는 라이브 카드에서 표시하므로 스킵
if (executionEvent.Timestamp.ToUniversalTime() >= liveCardCutoff)
continue;
var agentEvent = ToAgentEvent(executionEvent);
// SessionStart / UserPromptSubmit 숨김

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -200,14 +200,19 @@ internal sealed class CustomMessageBox : Window
return btn;
}
private static (string icon, Brush color) GetIconInfo(MessageBoxImage image) => image switch
private static (string icon, Brush color) GetIconInfo(MessageBoxImage image)
{
MessageBoxImage.Error => ("\uEA39", new SolidColorBrush(Color.FromRgb(0xE5, 0x3E, 0x3E))),
MessageBoxImage.Warning => ("\uE7BA", new SolidColorBrush(Color.FromRgb(0xDD, 0x6B, 0x20))),
MessageBoxImage.Information => ("\uE946", new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC))),
MessageBoxImage.Question => ("\uE9CE", new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC))),
_ => ("", Brushes.Transparent),
};
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
return image switch
{
MessageBoxImage.Error => ("\uEA39", new SolidColorBrush(Color.FromRgb(0xE5, 0x3E, 0x3E))),
MessageBoxImage.Warning => ("\uE7BA", new SolidColorBrush(Color.FromRgb(0xDD, 0x6B, 0x20))),
MessageBoxImage.Information => ("\uE946", accentBrush),
MessageBoxImage.Question => ("\uE9CE", accentBrush),
_ => ("", Brushes.Transparent),
};
}
// ─── 정적 호출 메서드 (기존 MessageBox.Show 시그니처 호환) ──────────────
@@ -276,9 +281,14 @@ internal sealed class CustomMessageBox : Window
if (string.IsNullOrEmpty(iconText))
{
iconText = "\uE946"; // default info icon
iconColor = new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
iconColor = Application.Current.TryFindResource("AccentColor") as Brush
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
}
var toastFg = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var toastBg = Application.Current.TryFindResource("ItemBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0xE8, 0x2A, 0x2B, 0x40));
var panel = new StackPanel { Orientation = Orientation.Horizontal };
panel.Children.Add(new TextBlock
{
@@ -294,13 +304,13 @@ internal sealed class CustomMessageBox : Window
Text = title,
FontSize = 11.5,
FontWeight = FontWeights.SemiBold,
Foreground = Brushes.White,
Foreground = toastFg,
VerticalAlignment = VerticalAlignment.Center,
});
var toast = new Border
{
Background = new SolidColorBrush(Color.FromArgb(0xE8, 0x2A, 0x2B, 0x40)),
Background = toastBg,
CornerRadius = new CornerRadius(10),
Padding = new Thickness(16, 8, 16, 8),
HorizontalAlignment = HorizontalAlignment.Center,

View File

@@ -22,6 +22,7 @@ internal sealed class ModelRegistrationDialog : Window
private readonly TextBox _cp4dUrlBox;
private readonly TextBox _cp4dUsernameBox;
private readonly PasswordBox _cp4dPasswordBox;
private readonly ComboBox _promptFamilyBox;
public string ModelAlias => _aliasBox.Text.Trim();
public string ModelName => _modelBox.Text.Trim();
@@ -30,6 +31,7 @@ internal sealed class ModelRegistrationDialog : Window
public bool AllowInsecureTls => _allowInsecureTlsCheck.IsChecked == true;
public string AuthType => (_authTypeBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "bearer";
public string ExecutionProfile => (_executionProfileBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "balanced";
public string PromptFamily => (_promptFamilyBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "";
public string Cp4dUrl => _cp4dUrlBox.Text.Trim();
public string Cp4dUsername => _cp4dUsernameBox.Text.Trim();
public string Cp4dPassword => _cp4dPasswordBox.Password.Trim();
@@ -38,7 +40,7 @@ internal sealed class ModelRegistrationDialog : Window
string existingEndpoint = "", string existingApiKey = "", bool existingAllowInsecureTls = false,
string existingAuthType = "bearer", string existingCp4dUrl = "",
string existingCp4dUsername = "", string existingCp4dPassword = "",
string existingExecutionProfile = "balanced")
string existingExecutionProfile = "balanced", string existingPromptFamily = "")
{
bool isEdit = !string.IsNullOrEmpty(existingAlias);
Title = isEdit ? "모델 편집" : "모델 추가";
@@ -232,6 +234,73 @@ internal sealed class ModelRegistrationDialog : Window
};
stack.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _executionProfileBox });
// 프롬프트 패밀리 선택
stack.Children.Add(new TextBlock
{
Text = "프롬프트 전략",
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
Margin = new Thickness(0, 12, 0, 6),
});
stack.Children.Add(new TextBlock
{
Text = "모델별 맞춤 프롬프트 전략을 선택합니다. '자동 감지'는 모델명에서 패밀리를 판별합니다.",
FontSize = 11,
Foreground = secondaryText,
Margin = new Thickness(0, 0, 0, 8),
});
_promptFamilyBox = new ComboBox
{
FontSize = 13,
Padding = new Thickness(8, 6, 8, 6),
Foreground = primaryText,
Background = itemBg,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
};
var pfAuto = new ComboBoxItem { Content = "자동 감지", Tag = "" };
var pfQwen = new ComboBoxItem { Content = "Qwen (짧은 영어 규칙, 도구 이중 강제)", Tag = "qwen" };
var pfDeepseek = new ComboBoxItem { Content = "DeepSeek (한영 혼합, 계획 축약)", Tag = "deepseek" };
var pfKimi = new ComboBoxItem { Content = "Kimi (장문 허용, 장황함 억제)", Tag = "kimi" };
var pfGemma = new ComboBoxItem { Content = "Gemma (영어 전용, 극단적 단순화)", Tag = "gemma" };
var pfDefault = new ComboBoxItem { Content = "기본 (모델별 변환 없음)", Tag = "default" };
_promptFamilyBox.Items.Add(pfAuto);
_promptFamilyBox.Items.Add(pfQwen);
_promptFamilyBox.Items.Add(pfDeepseek);
_promptFamilyBox.Items.Add(pfKimi);
_promptFamilyBox.Items.Add(pfGemma);
_promptFamilyBox.Items.Add(pfDefault);
_promptFamilyBox.SelectedItem = (existingPromptFamily ?? "").Trim().ToLowerInvariant() switch
{
"qwen" => pfQwen,
"deepseek" => pfDeepseek,
"kimi" => pfKimi,
"gemma" => pfGemma,
"default" => pfDefault,
_ => pfAuto,
};
stack.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _promptFamilyBox });
// 모델명 입력 시 자동 감지 미리보기
_modelBox.TextChanged += (_, _) =>
{
if (_promptFamilyBox.SelectedItem == pfAuto)
{
var detected = Services.Agent.ModelPromptAdapter.DetectModelFamily(_modelBox.Text.Trim());
pfAuto.Content = detected == "default"
? "자동 감지"
: $"자동 감지 → {Services.Agent.ModelPromptAdapter.GetFamilyLabel(detected)}";
}
};
// 초기 감지 표시
if (!string.IsNullOrEmpty(existingModel))
{
var detected = Services.Agent.ModelPromptAdapter.DetectModelFamily(existingModel);
if (detected != "default")
pfAuto.Content = $"자동 감지 → {Services.Agent.ModelPromptAdapter.GetFamilyLabel(detected)}";
}
// 구분선
stack.Children.Add(new Rectangle
{

View File

@@ -36,6 +36,139 @@ internal sealed class PermissionRequestWindow : Window
bool ShowHints,
bool ShowOptionDescription);
/// <summary>
/// 테마 프리셋별 승인창 스타일 보정 파라미터.
/// 각 테마의 캐릭터(Claude=따뜻/둥글, Codex=터미널/각진, Nord=쿨/미니멀, ...)에 맞춰 조정.
/// </summary>
private sealed record ThemeDialogProfile(
string PresetKey,
double CornerRadius,
double OptionCornerRadius,
double BadgeCornerRadius,
double ShadowBlur,
double ShadowDepth,
double ShadowOpacity,
string FontFamily, // 본문 폰트
string HeaderFontFamily, // 헤더/타이틀 폰트 (강조용)
FontWeight HeaderFontWeight,
double HeaderTracking, // 자간 (emu)
Thickness RootPadding,
bool ShowAccentSidebar, // 좌측 색 포인트 막대
bool UppercaseHeader);
private static ThemeDialogProfile ResolveThemeDialogProfile()
{
var preset = "claude";
if (Application.Current is App app)
preset = (app.SettingsService?.Settings?.Llm.AgentThemePreset ?? "claude")
.Trim().ToLowerInvariant();
return preset switch
{
// Codex: 터미널/각진 모던. 낮은 라운드, 고정폭 폰트 헤더, 선명한 포인트 막대.
"codex" => new ThemeDialogProfile(
PresetKey: "codex",
CornerRadius: 8,
OptionCornerRadius: 6,
BadgeCornerRadius: 4,
ShadowBlur: 18,
ShadowDepth: 2,
ShadowOpacity: 0.5,
FontFamily: "Segoe UI",
HeaderFontFamily: "Cascadia Mono, Consolas, Segoe UI",
HeaderFontWeight: FontWeights.Bold,
HeaderTracking: 0.5,
RootPadding: new Thickness(22, 18, 22, 16),
ShowAccentSidebar: true,
UppercaseHeader: true),
// Nord: 미니멀 쿨톤. 중간 라운드, 절제된 그림자, 대문자 없음.
"nord" => new ThemeDialogProfile(
PresetKey: "nord",
CornerRadius: 12,
OptionCornerRadius: 10,
BadgeCornerRadius: 6,
ShadowBlur: 20,
ShadowDepth: 4,
ShadowOpacity: 0.28,
FontFamily: "Segoe UI",
HeaderFontFamily: "Segoe UI",
HeaderFontWeight: FontWeights.SemiBold,
HeaderTracking: 0.25,
RootPadding: new Thickness(24, 20, 24, 18),
ShowAccentSidebar: false,
UppercaseHeader: false),
// Ember: 따뜻한 강조. 중간 라운드, 진한 그림자, 포인트 막대 O.
"ember" => new ThemeDialogProfile(
PresetKey: "ember",
CornerRadius: 14,
OptionCornerRadius: 12,
BadgeCornerRadius: 8,
ShadowBlur: 28,
ShadowDepth: 8,
ShadowOpacity: 0.4,
FontFamily: "Segoe UI",
HeaderFontFamily: "Segoe UI",
HeaderFontWeight: FontWeights.SemiBold,
HeaderTracking: 0,
RootPadding: new Thickness(22, 18, 22, 16),
ShowAccentSidebar: true,
UppercaseHeader: false),
// Slate: 플랫/실용. 낮은 라운드, 얕은 그림자.
"slate" => new ThemeDialogProfile(
PresetKey: "slate",
CornerRadius: 10,
OptionCornerRadius: 8,
BadgeCornerRadius: 6,
ShadowBlur: 14,
ShadowDepth: 2,
ShadowOpacity: 0.22,
FontFamily: "Segoe UI",
HeaderFontFamily: "Segoe UI",
HeaderFontWeight: FontWeights.SemiBold,
HeaderTracking: 0,
RootPadding: new Thickness(22, 16, 22, 14),
ShowAccentSidebar: false,
UppercaseHeader: false),
// Claw: 부드러운 원형. 큰 라운드, 따뜻한 그림자.
"claw" => new ThemeDialogProfile(
PresetKey: "claw",
CornerRadius: 18,
OptionCornerRadius: 14,
BadgeCornerRadius: 10,
ShadowBlur: 26,
ShadowDepth: 6,
ShadowOpacity: 0.32,
FontFamily: "Segoe UI",
HeaderFontFamily: "Segoe UI",
HeaderFontWeight: FontWeights.SemiBold,
HeaderTracking: 0,
RootPadding: new Thickness(24, 20, 24, 18),
ShowAccentSidebar: true,
UppercaseHeader: false),
// Claude(default): 따뜻하고 둥근 기본형.
_ => new ThemeDialogProfile(
PresetKey: "claude",
CornerRadius: 16,
OptionCornerRadius: 12,
BadgeCornerRadius: 8,
ShadowBlur: 24,
ShadowDepth: 6,
ShadowOpacity: 0.35,
FontFamily: "Segoe UI",
HeaderFontFamily: "Segoe UI",
HeaderFontWeight: FontWeights.SemiBold,
HeaderTracking: 0,
RootPadding: new Thickness(22, 18, 22, 16),
ShowAccentSidebar: false,
UppercaseHeader: false),
};
}
private PermissionPromptResult _result = PermissionPromptResult.Reject;
private PermissionRequestWindow(
@@ -58,6 +191,7 @@ internal sealed class PermissionRequestWindow : Window
var profile = ResolveProfile(toolName);
var uiProfile = ResolveUiExpressionProfile();
var themeProfile = ResolveThemeDialogProfile();
var bg = Application.Current.TryFindResource("LauncherBackground") as Brush
?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
@@ -70,28 +204,41 @@ internal sealed class PermissionRequestWindow : Window
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40));
var hoverBg = Application.Current.TryFindResource("ItemHoverBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
var shadowColor = Colors.Black;
if (Application.Current.TryFindResource("ShadowColor") is SolidColorBrush shadowBrush)
shadowColor = shadowBrush.Color;
var riskBrush = new SolidColorBrush(profile.RiskColor);
// 테마별 폰트 패밀리 (헤더/본문). 존재하지 않는 폰트는 WPF가 fallback 처리.
var bodyFont = new FontFamily(themeProfile.FontFamily);
var headerFont = new FontFamily(themeProfile.HeaderFontFamily);
FontFamily = bodyFont;
var root = new Border
{
Background = bg,
BorderBrush = border,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(16),
Padding = new Thickness(22, 18, 22, 16),
CornerRadius = new CornerRadius(themeProfile.CornerRadius),
Padding = themeProfile.RootPadding,
Effect = new DropShadowEffect
{
BlurRadius = 24,
ShadowDepth = 6,
Opacity = 0.35,
Color = Colors.Black,
BlurRadius = themeProfile.ShadowBlur,
ShadowDepth = themeProfile.ShadowDepth,
Opacity = themeProfile.ShadowOpacity,
Color = shadowColor,
},
};
var stack = new StackPanel();
var header = new Grid { Margin = new Thickness(0, 0, 0, 10) };
header.MouseLeftButtonDown += (_, _) => { try { DragMove(); } catch { } };
var headerTitleText = GetHeaderTitle(profile.HeaderTitle, uiProfile);
if (themeProfile.UppercaseHeader)
headerTitleText = headerTitleText.ToUpperInvariant();
header.Children.Add(new StackPanel
{
Orientation = Orientation.Horizontal,
@@ -107,10 +254,13 @@ internal sealed class PermissionRequestWindow : Window
},
new TextBlock
{
Text = GetHeaderTitle(profile.HeaderTitle, uiProfile),
Text = headerTitleText,
FontFamily = headerFont,
FontSize = 14,
FontWeight = FontWeights.SemiBold,
FontWeight = themeProfile.HeaderFontWeight,
Foreground = primary,
// Codex 프리셋 강조: 자간을 살짝 벌려 터미널 느낌을 줌
Typography = { Capitals = FontCapitals.Normal },
}
}
});
@@ -161,7 +311,7 @@ internal sealed class PermissionRequestWindow : Window
Background = new SolidColorBrush(Color.FromArgb(0x16, profile.RiskColor.R, profile.RiskColor.G, profile.RiskColor.B)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x50, profile.RiskColor.R, profile.RiskColor.G, profile.RiskColor.B)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
CornerRadius = new CornerRadius(themeProfile.BadgeCornerRadius),
Padding = new Thickness(10, 6, 10, 6),
Margin = new Thickness(0, 0, 0, 10),
Child = new TextBlock
@@ -181,7 +331,7 @@ internal sealed class PermissionRequestWindow : Window
Background = new SolidColorBrush(Color.FromArgb(0x18, noticeColor.R, noticeColor.G, noticeColor.B)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x60, noticeColor.R, noticeColor.G, noticeColor.B)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
CornerRadius = new CornerRadius(themeProfile.BadgeCornerRadius),
Padding = new Thickness(10, 7, 10, 7),
Margin = new Thickness(0, 0, 0, 10),
Child = new StackPanel
@@ -241,7 +391,8 @@ internal sealed class PermissionRequestWindow : Window
description: uiProfile.ShowOptionDescription ? "현재 요청 1회만 허용합니다." : "",
fg: new SolidColorBrush(Color.FromRgb(0x05, 0x96, 0x69)),
bg: hoverBg,
onClick: () => CloseWith(PermissionPromptResult.AllowOnce)));
onClick: () => CloseWith(PermissionPromptResult.AllowOnce),
cornerRadius: themeProfile.OptionCornerRadius));
optionList.Children.Add(BuildOption(
icon: "\uE8FB",
@@ -249,7 +400,8 @@ internal sealed class PermissionRequestWindow : Window
description: uiProfile.ShowOptionDescription ? "현재 실행 중 같은 범위 요청을 자동 허용합니다." : "",
fg: accent,
bg: hoverBg,
onClick: () => CloseWith(PermissionPromptResult.AllowForSession)));
onClick: () => CloseWith(PermissionPromptResult.AllowForSession),
cornerRadius: themeProfile.OptionCornerRadius));
optionList.Children.Add(BuildOption(
icon: "\uE711",
@@ -257,7 +409,8 @@ internal sealed class PermissionRequestWindow : Window
description: uiProfile.ShowOptionDescription ? "요청을 차단하고 현재 작업은 중단 없이 계속 진행합니다." : "",
fg: new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)),
bg: hoverBg,
onClick: () => CloseWith(PermissionPromptResult.Reject)));
onClick: () => CloseWith(PermissionPromptResult.Reject),
cornerRadius: themeProfile.OptionCornerRadius));
stack.Children.Add(optionList);
stack.Children.Add(new TextBlock
@@ -270,7 +423,34 @@ internal sealed class PermissionRequestWindow : Window
Margin = new Thickness(2, 12, 0, 0),
});
root.Child = stack;
// 테마 프리셋 중 ShowAccentSidebar=true인 경우 좌측 액센트 막대를 추가.
// stack을 Grid로 감싼 뒤 막대+stack을 함께 배치 (stack은 살짝 안쪽으로 들여쓰기).
if (themeProfile.ShowAccentSidebar)
{
var accentColor = (accent as SolidColorBrush)?.Color ?? Color.FromRgb(0x4B, 0x5E, 0xFC);
var sidebar = new Border
{
Width = 3,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Stretch,
Margin = new Thickness(-10, 6, 0, 6),
CornerRadius = new CornerRadius(2),
Background = new LinearGradientBrush(
Color.FromArgb(0xFF, accentColor.R, accentColor.G, accentColor.B),
Color.FromArgb(0x55, accentColor.R, accentColor.G, accentColor.B),
90),
IsHitTestVisible = false,
};
stack.Margin = new Thickness(6, 0, 0, 0);
var rootGrid = new Grid();
rootGrid.Children.Add(sidebar);
rootGrid.Children.Add(stack);
root.Child = rootGrid;
}
else
{
root.Child = stack;
}
Content = root;
PreviewKeyDown += (_, e) =>
@@ -1019,12 +1199,13 @@ internal sealed class PermissionRequestWindow : Window
string description,
Brush fg,
Brush bg,
Action onClick)
Action onClick,
double cornerRadius = 10)
{
var card = new Border
{
Background = Brushes.Transparent,
CornerRadius = new CornerRadius(10),
CornerRadius = new CornerRadius(cornerRadius),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x30, 0x80, 0x80, 0x80)),
BorderThickness = new Thickness(1),
Padding = new Thickness(10, 8, 10, 8),

View File

@@ -141,8 +141,8 @@ internal sealed class PlanViewerWindow : Window, IPlanViewerWindow
{
Visibility = Visibility.Collapsed,
Margin = new Thickness(20, 8, 20, 0),
Padding = new Thickness(12, 6, 12, 6),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(14, 8, 14, 8),
CornerRadius = new CornerRadius(10),
Background = new SolidColorBrush(Color.FromArgb(0x15,
((SolidColorBrush)accentBrush).Color.R,
((SolidColorBrush)accentBrush).Color.G,
@@ -508,7 +508,7 @@ internal sealed class PlanViewerWindow : Window, IPlanViewerWindow
BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, accentColor.R, accentColor.G, accentColor.B)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(10),
Padding = new Thickness(12, 10, 12, 10),
Padding = new Thickness(14, 12, 14, 12),
Margin = new Thickness(0, 0, 0, 8),
Child = statusPanel,
});
@@ -526,7 +526,7 @@ internal sealed class PlanViewerWindow : Window, IPlanViewerWindow
var card = new Border
{
CornerRadius = new CornerRadius(10),
Padding = new Thickness(10, 7, 10, 7),
Padding = new Thickness(14, 12, 14, 12),
Margin = new Thickness(0, 0, 0, 5),
Background = isCurrent
? new SolidColorBrush(Color.FromArgb(0x18,

View File

@@ -576,8 +576,8 @@ internal sealed class PlanViewerWindowV2 : Window, IPlanViewerWindow
var navItem = new Border
{
CornerRadius = new CornerRadius(6),
Padding = new Thickness(8, 5, 8, 5),
CornerRadius = new CornerRadius(10),
Padding = new Thickness(14, 12, 14, 12),
Margin = new Thickness(leftMargin, 1, 0, 1),
Background = isSelected ? selectedBg : Brushes.Transparent,
Cursor = Cursors.Hand,
@@ -631,7 +631,7 @@ internal sealed class PlanViewerWindowV2 : Window, IPlanViewerWindow
var titleTb = new TextBlock
{
Text = displayTitle,
FontSize = section.Level <= 1 ? 12.5 : 12,
FontSize = section.Level <= 1 ? 14 : 12,
FontWeight = isSelected ? FontWeights.SemiBold : (section.Level <= 1 ? FontWeights.SemiBold : FontWeights.Normal),
Foreground = isSelected ? _primaryText : _secondaryText,
VerticalAlignment = VerticalAlignment.Center,
@@ -702,7 +702,7 @@ internal sealed class PlanViewerWindowV2 : Window, IPlanViewerWindow
sectionContainer.Children.Add(new TextBlock
{
Text = section.Title,
FontSize = section.Level <= 1 ? 22 : (section.Level == 2 ? 18 : 15),
FontSize = section.Level <= 1 ? 22 : (section.Level == 2 ? 18 : 14),
FontWeight = FontWeights.Bold,
Foreground = _primaryText,
TextWrapping = TextWrapping.Wrap,
@@ -792,8 +792,8 @@ internal sealed class PlanViewerWindowV2 : Window, IPlanViewerWindow
{
_btnPanel.Children.Clear();
// 승인 (초록)
var approveBtn = MakeActionButton("승인", new SolidColorBrush(Color.FromRgb(0x10, 0xB9, 0x81)), Brushes.White);
// 승인 — primary action uses AccentColor
var approveBtn = MakeActionButton("승인", _accentBrush, Brushes.White);
approveBtn.MouseLeftButtonUp += (_, _) => _tcs?.TrySetResult(null);
_btnPanel.Children.Add(approveBtn);
@@ -848,7 +848,7 @@ internal sealed class PlanViewerWindowV2 : Window, IPlanViewerWindow
{
var btn = new Border
{
CornerRadius = new CornerRadius(8),
CornerRadius = new CornerRadius(10),
Background = bg,
BorderBrush = borderBrush ?? Brushes.Transparent,
BorderThickness = new Thickness(borderBrush != null ? 1 : 0),

View File

@@ -4309,16 +4309,16 @@
<ToolTip Style="{StaticResource HelpTooltipStyle}">
<TextBlock TextWrapping="Wrap" Foreground="White" FontSize="12" LineHeight="18" MaxWidth="320">
마크다운 기반 재사용 워크플로우 시스템입니다.
<LineBreak/>*.skill.md 파일로 스킬을 정의하면 슬래시 명령어(/)로 호출할 수 있습니다.
<LineBreak/>사용자 스킬 폴더와 추가 폴더의 스킬을 로드해 슬래시 명령어(/)와 런타임 정책에 연결합니다.
<LineBreak/>
<LineBreak/>스킬 폴더: %APPDATA%\AxCopilot\skills\
<LineBreak/>기본 스킬 폴더: %APPDATA%\AxCopilot\skills\
<LineBreak/>예: /daily-standup, /bug-hunt, /code-explain
</TextBlock>
</ToolTip>
</Border.ToolTip>
</Border>
</StackPanel>
<TextBlock Style="{StaticResource RowHint}" Text="마크다운 기반 재사용 워크플로우. 슬래시 명령어(/)로 호출합니다."/>
<TextBlock Style="{StaticResource RowHint}" Text="마크다운 기반 재사용 워크플로우. 슬래시 명령과 런타임 정책에서 함께 활용됩니다."/>
</StackPanel>
<CheckBox Style="{StaticResource ToggleSwitch}" HorizontalAlignment="Right" VerticalAlignment="Center"
IsChecked="{Binding EnableSkillSystem, Mode=TwoWay}"/>

View File

@@ -6,6 +6,7 @@ using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using AxCopilot.Services;
using AxCopilot.Services.Agent;
using AxCopilot.ViewModels;
namespace AxCopilot.Views;
@@ -429,73 +430,21 @@ public partial class SettingsWindow : Window
{
if (AgentEtcContent == null) return;
// 도구 목록 데이터 (카테고리별)
var toolGroups = new (string Category, string Icon, string IconColor, (string Name, string Desc)[] Tools)[]
{
("파일/검색", "\uE8B7", "#F59E0B", new[]
using var registry = Services.Agent.ToolRegistry.CreateDefault();
var toolGroups = registry.All
.Select(tool => new { Tool = tool, Meta = AgentToolCatalog.GetMetadata(tool.Name) })
.GroupBy(x => x.Meta.SettingsCategory, StringComparer.OrdinalIgnoreCase)
.Select(group =>
{
("file_read", "파일 읽기 ? 텍스트/바이너리 파일의 내용을 읽습니다"),
("file_write", "파일 쓰기 ? 새 파일을 생성하거나 기존 파일을 덮어씁니다"),
("file_edit", "파일 편집 ? 줄 번호 기반으로 파일의 특정 부분을 수정합니다"),
("glob", "파일 패턴 검색 ? 글로브 패턴으로 파일을 찾습니다 (예: **/*.cs)"),
("grep_tool", "텍스트 검색 ? 정규식으로 파일 내용을 검색합니다"),
("folder_map", "폴더 구조 ? 프로젝트의 디렉토리 트리를 조회합니다"),
("document_read", "문서 읽기 ? PDF, DOCX 등 문서 파일의 텍스트를 추출합니다"),
}),
("프로세스/빌드", "\uE756", "#06B6D4", new[]
{
("process", "프로세스 실행 ? 외부 명령/프로그램을 실행합니다"),
("build_run", "빌드/테스트 ? 프로젝트 타입을 감지하여 빌드/테스트를 실행합니다"),
("dev_env_detect", "개발 환경 감지 ? IDE, 런타임, 빌드 도구를 자동으로 인식합니다"),
}),
("코드 분석", "\uE943", "#818CF8", new[]
{
("search_codebase", "코드 키워드 검색 ? TF-IDF 기반으로 관련 코드를 찾습니다"),
("code_review", "AI 코드 리뷰 ? Git diff 분석, 파일 정적 검사, PR 요약"),
("lsp", "LSP 인텔리전스 ? 정의 이동, 참조 검색, 심볼 목록 (C#/TS/Python)"),
("test_loop", "테스트 루프 ? 테스트 생성/실행/분석 자동화"),
("git_tool", "Git 작업 ? status, log, diff, commit 등 Git 명령 실행"),
("snippet_runner", "코드 실행 ? C#/Python/JavaScript 스니펫 즉시 실행"),
}),
("문서 생성", "\uE8A5", "#34D399", new[]
{
("excel_create", "Excel 생성 ? .xlsx 스프레드시트를 생성합니다"),
("docx_create", "Word 생성 ? .docx 문서를 생성합니다"),
("csv_create", "CSV 생성 ? CSV 파일을 생성합니다"),
("markdown_create", "마크다운 생성 ? .md 파일을 생성합니다"),
("html_create", "HTML 생성 ? HTML 파일을 생성합니다"),
("chart_create", "차트 생성 ? 데이터 시각화 차트를 생성합니다"),
("batch_create", "배치 생성 ? 스크립트 파일을 생성합니다"),
("document_review", "문서 검증 ? 문서 품질을 검사합니다"),
("format_convert", "포맷 변환 ? 문서 형식을 변환합니다"),
}),
("데이터 처리", "\uE9F5", "#F59E0B", new[]
{
("json_tool", "JSON 처리 ? JSON 파싱, 변환, 검증, 포맷팅"),
("regex_tool", "정규식 ? 정규식 테스트, 추출, 치환"),
("diff_tool", "텍스트 비교 ? 두 파일/텍스트 비교, 통합 diff 출력"),
("base64_tool", "인코딩 ? Base64/URL 인코딩, 디코딩"),
("hash_tool", "해시 계산 ? MD5/SHA256 해시 계산 (파일/텍스트)"),
("datetime_tool", "날짜/시간 ? 날짜 변환, 타임존, 기간 계산"),
}),
("시스템/환경", "\uE770", "#06B6D4", new[]
{
("clipboard_tool", "클립보드 ? Windows 클립보드 읽기/쓰기 (텍스트/이미지)"),
("notify_tool", "알림 ? Windows 시스템 알림 전송"),
("env_tool", "환경변수 ? 환경변수 읽기/설정 (프로세스 범위)"),
("zip_tool", "압축 ? 파일 압축(zip) / 해제"),
("http_tool", "HTTP ? 로컬/사내 HTTP API 호출 (GET/POST)"),
("sql_tool", "SQLite ? SQLite DB 쿼리 실행 (로컬 파일)"),
}),
("에이전트", "\uE99A", "#F472B6", new[]
{
("spawn_agent", "서브에이전트 ? 하위 작업을 병렬로 실행하는 서브에이전트를 생성합니다"),
("wait_agents", "에이전트 대기 ? 실행 중인 서브에이전트의 결과를 수집합니다"),
("memory", "에이전트 메모리 ? 프로젝트 규칙, 선호도를 저장/검색합니다"),
("skill_manager", "스킬 관리 ? 스킬 목록 조회, 상세 정보, 재로드"),
("project_rules", "프로젝트 지침 ? AGENTS.md 개발 지침을 읽고 씁니다"),
}),
};
var meta = group.First().Meta;
var tools = group
.OrderBy(x => x.Tool.Name, StringComparer.OrdinalIgnoreCase)
.Select(x => (Name: x.Tool.Name, Description: x.Tool.Description))
.ToArray();
return (Category: meta.SettingsCategory, Icon: meta.SettingsIcon, IconColor: meta.SettingsIconColor, Tools: tools);
})
.OrderBy(group => group.Category, StringComparer.OrdinalIgnoreCase)
.ToList();
// 도구 목록을 섹션으로 그룹화
var toolCards = new List<UIElement>();
@@ -599,8 +548,10 @@ public partial class SettingsWindow : Window
});
// 도구 아이템
foreach (var (name, toolDesc) in group.Tools)
foreach (var toolInfo in group.Tools)
{
var name = toolInfo.Name;
var toolDesc = toolInfo.Description;
var row = new Grid { Margin = new Thickness(0, 3, 0, 3) };
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) });
row.ColumnDefinitions.Add(new ColumnDefinition());
@@ -666,7 +617,7 @@ public partial class SettingsWindow : Window
skillItems.Add(new TextBlock
{
Text = "/ 명령으로 호출할 수 있는 스킬 목록입니다. 앱 내장 + 사용자 추가 스킬이 포함됩니다.\n" +
"(스킬은 사용자가 직접 /명령어를 입력해야 실행됩니다. LLM이 자동 호출하지 않습니다.)",
"(직접 호출 가능한 스킬과 런타임 정책에 연결되는 스킬을 함께 표시합니다.)",
FontSize = 11,
Foreground = new SolidColorBrush(subtleText),
Margin = new Thickness(2, 0, 0, 10),
@@ -1817,6 +1768,7 @@ public partial class SettingsWindow : Window
EncryptedModelName = Services.CryptoService.EncryptIfEnabled(dlg.ModelName, IsEncryptionEnabled),
Service = currentService,
ExecutionProfile = dlg.ExecutionProfile,
PromptFamily = dlg.PromptFamily,
Endpoint = dlg.Endpoint,
ApiKey = dlg.ApiKey,
AllowInsecureTls = dlg.AllowInsecureTls,
@@ -1841,7 +1793,7 @@ public partial class SettingsWindow : Window
var dlg = new ModelRegistrationDialog(currentService, row.Alias, currentModel,
row.Endpoint, row.ApiKey, row.AllowInsecureTls,
row.AuthType ?? "bearer", row.Cp4dUrl ?? "", row.Cp4dUsername ?? "", cp4dPw,
row.ExecutionProfile ?? "balanced");
row.ExecutionProfile ?? "balanced", row.PromptFamily ?? "");
dlg.Owner = this;
if (dlg.ShowDialog() == true)
{
@@ -1849,6 +1801,7 @@ public partial class SettingsWindow : Window
row.EncryptedModelName = Services.CryptoService.EncryptIfEnabled(dlg.ModelName, IsEncryptionEnabled);
row.Service = currentService;
row.ExecutionProfile = dlg.ExecutionProfile;
row.PromptFamily = dlg.PromptFamily;
row.Endpoint = dlg.Endpoint;
row.ApiKey = dlg.ApiKey;
row.AllowInsecureTls = dlg.AllowInsecureTls;
@@ -1956,52 +1909,17 @@ public partial class SettingsWindow : Window
var app = System.Windows.Application.Current as App;
var settings = app?.SettingsService?.Settings.Llm;
using var tools = Services.Agent.ToolRegistry.CreateDefault();
_disabledTools = new HashSet<string>(settings?.DisabledTools ?? new(), StringComparer.OrdinalIgnoreCase);
_disabledTools = new HashSet<string>(AgentToolCatalog.CanonicalizeMany(settings?.DisabledTools ?? new()), StringComparer.OrdinalIgnoreCase);
var disabled = _disabledTools;
// 카테고리 매핑
var categories = new Dictionary<string, List<Services.Agent.IAgentTool>>
{
["파일/검색"] = new(),
["문서 생성"] = new(),
["문서 품질"] = new(),
["코드/개발"] = new(),
["데이터/유틸"] = new(),
["시스템"] = new(),
};
var toolCategoryMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
// 파일/검색
["file_read"] = "파일/검색", ["file_write"] = "파일/검색", ["file_edit"] = "파일/검색",
["glob"] = "파일/검색", ["grep"] = "파일/검색", ["folder_map"] = "파일/검색",
["document_read"] = "파일/검색", ["file_watch"] = "파일/검색",
// 문서 생성
["excel_skill"] = "문서 생성", ["docx_skill"] = "문서 생성", ["csv_skill"] = "문서 생성",
["markdown_skill"] = "문서 생성", ["html_skill"] = "문서 생성", ["chart_skill"] = "문서 생성",
["batch_skill"] = "문서 생성", ["pptx_skill"] = "문서 생성",
["document_planner"] = "문서 생성", ["document_assembler"] = "문서 생성",
// 문서 품질
["document_review"] = "문서 품질", ["format_convert"] = "문서 품질",
["template_render"] = "문서 품질", ["text_summarize"] = "문서 품질",
// 코드/개발
["dev_env_detect"] = "코드/개발", ["build_run"] = "코드/개발", ["git_tool"] = "코드/개발",
["lsp"] = "코드/개발", ["sub_agent"] = "코드/개발", ["wait_agents"] = "코드/개발",
["code_search"] = "코드/개발", ["test_loop"] = "코드/개발",
["code_review"] = "코드/개발", ["project_rule"] = "코드/개발",
// 시스템
["process"] = "시스템", ["skill_manager"] = "시스템", ["memory"] = "시스템",
["clipboard"] = "시스템", ["notify"] = "시스템", ["env"] = "시스템",
["image_analyze"] = "시스템",
};
var categories = new Dictionary<string, List<Services.Agent.IAgentTool>>(StringComparer.OrdinalIgnoreCase);
foreach (var tool in tools.All)
{
var cat = toolCategoryMap.TryGetValue(tool.Name, out var c) ? c : "데이터/유틸";
if (categories.ContainsKey(cat))
categories[cat].Add(tool);
else
categories["데이터/유틸"].Add(tool);
var cat = AgentToolCatalog.GetMetadata(tool.Name).SettingsCategory;
if (!categories.ContainsKey(cat))
categories[cat] = new List<Services.Agent.IAgentTool>();
categories[cat].Add(tool);
}
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
@@ -3368,11 +3286,11 @@ public partial class SettingsWindow : Window
stack.Children.Add(new TextBlock { Text = "대상 도구 (* = 모든 도구)", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 10, 0, 4) });
var toolBox = new TextBox
{
Text = existing?.ToolName ?? "*", FontSize = 13,
Text = AgentToolCatalog.CanonicalizeHookTarget(existing?.ToolName ?? "*"), FontSize = 13,
Foreground = fgBrush, Background = itemBg,
BorderBrush = borderBrush, Padding = new Thickness(12, 8, 12, 8),
};
var toolHolder = CreatePlaceholder("예: file_write, grep_tool", subFgBrush, existing?.ToolName ?? "*");
var toolHolder = CreatePlaceholder("예: file_write, grep", subFgBrush, AgentToolCatalog.CanonicalizeHookTarget(existing?.ToolName ?? "*"));
toolBox.TextChanged += (_, _) => toolHolder.Visibility = string.IsNullOrEmpty(toolBox.Text) ? Visibility.Visible : Visibility.Collapsed;
var toolGrid = new Grid();
toolGrid.Children.Add(toolBox);
@@ -3469,7 +3387,7 @@ public partial class SettingsWindow : Window
var entry = new Models.AgentHookEntry
{
Name = nameBox.Text.Trim(),
ToolName = string.IsNullOrWhiteSpace(toolBox.Text) ? "*" : toolBox.Text.Trim(),
ToolName = AgentToolCatalog.CanonicalizeHookTarget(toolBox.Text),
Timing = preRadio.IsChecked == true ? "pre" : "post",
ScriptPath = pathBox.Text.Trim(),
Arguments = argsBox.Text.Trim(),
@@ -3498,6 +3416,7 @@ public partial class SettingsWindow : Window
if (HookListPanel == null) return;
HookListPanel.Children.Clear();
_vm.Service.Settings.Llm.AgentHooks = AgentToolCatalog.CanonicalizeHooks(_vm.Service.Settings.Llm.AgentHooks);
var hooks = _vm.Service.Settings.Llm.AgentHooks;
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
@@ -3895,7 +3814,7 @@ public partial class SettingsWindow : Window
// 도구 비활성 목록 저장
if (_toolCardsLoaded)
llm.DisabledTools = _disabledTools.ToList();
llm.DisabledTools = AgentToolCatalog.CanonicalizeMany(_disabledTools).ToList();
}
private void AddSnippet_Click(object sender, RoutedEventArgs e)
@@ -4168,5 +4087,3 @@ public partial class SettingsWindow : Window
base.OnClosed(e);
}
}

View File

@@ -19,7 +19,7 @@ internal sealed class ToolApprovalWindow : Window
private ToolApprovalWindow(string message, List<string> options)
{
Width = 480;
Width = 500;
MinWidth = 400;
MaxWidth = 600;
SizeToContent = SizeToContent.Height;
@@ -40,6 +40,10 @@ internal sealed class ToolApprovalWindow : Window
var border = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var itemBg = Application.Current.TryFindResource("ItemBackground") as Brush
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40));
var errorBrush = Application.Current.TryFindResource("ErrorColor") as Brush
?? new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26));
var hoverBg = Application.Current.TryFindResource("ItemHoverBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0x20, 0xFF, 0xFF, 0xFF));
var root = new Border
{
@@ -108,7 +112,7 @@ internal sealed class ToolApprovalWindow : Window
},
};
close.MouseLeftButtonUp += (_, _) => { _result = "취소"; Close(); };
close.MouseEnter += (_, _) => close.Background = new SolidColorBrush(Color.FromArgb(0x20, 0xFF, 0xFF, 0xFF));
close.MouseEnter += (_, _) => close.Background = hoverBg;
close.MouseLeave += (_, _) => close.Background = Brushes.Transparent;
header.Children.Add(close);
stack.Children.Add(header);
@@ -117,7 +121,7 @@ internal sealed class ToolApprovalWindow : Window
var msgBorder = new Border
{
Background = itemBg,
CornerRadius = new CornerRadius(10),
CornerRadius = new CornerRadius(12),
Padding = new Thickness(14, 11, 14, 11),
Margin = new Thickness(0, 0, 0, 14),
};
@@ -125,11 +129,11 @@ internal sealed class ToolApprovalWindow : Window
var msgText = new TextBlock
{
Text = message,
FontSize = 12.5,
FontSize = 13,
FontFamily = new FontFamily("Segoe UI"),
Foreground = primary,
TextWrapping = TextWrapping.Wrap,
LineHeight = 19,
LineHeight = 20,
};
msgBorder.Child = msgText;
stack.Children.Add(msgBorder);
@@ -143,7 +147,7 @@ internal sealed class ToolApprovalWindow : Window
foreach (var option in options)
{
var btn = CreateOptionButton(option, primary, secondary, accent, bg);
var btn = CreateOptionButton(option, primary, secondary, accent, bg, errorBrush);
btn.MouseLeftButtonUp += (_, _) => { _result = option; Close(); };
btnPanel.Children.Add(btn);
}
@@ -182,7 +186,7 @@ internal sealed class ToolApprovalWindow : Window
};
}
private Border CreateOptionButton(string label, Brush primary, Brush secondary, Brush accent, Brush bg)
private Border CreateOptionButton(string label, Brush primary, Brush secondary, Brush accent, Brush bg, Brush errorBrush)
{
Brush foreground, background, borderBrush;
switch (label)
@@ -195,9 +199,9 @@ internal sealed class ToolApprovalWindow : Window
break;
case "취소":
case "중단":
foreground = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26));
foreground = errorBrush;
background = Brushes.Transparent;
borderBrush = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26));
borderBrush = errorBrush;
break;
default:
foreground = primary;
@@ -206,18 +210,26 @@ internal sealed class ToolApprovalWindow : Window
break;
}
var btn = new Border
var isDestructive = label == "취소" || label == "중단";
// Build inner content: optional left accent bar + label
FrameworkElement child;
if (isDestructive)
{
MinWidth = 70,
Height = 32,
CornerRadius = new CornerRadius(8),
Background = background,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
Padding = new Thickness(14, 0, 14, 0),
Margin = new Thickness(6, 0, 0, 0),
Cursor = Cursors.Hand,
Child = new TextBlock
var grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
var accentBar = new Border
{
Width = 4,
CornerRadius = new CornerRadius(2),
Background = errorBrush,
Margin = new Thickness(0, 4, 8, 4),
VerticalAlignment = VerticalAlignment.Stretch,
};
Grid.SetColumn(accentBar, 0);
grid.Children.Add(accentBar);
var txt = new TextBlock
{
Text = label,
FontSize = 12,
@@ -225,7 +237,36 @@ internal sealed class ToolApprovalWindow : Window
Foreground = foreground,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
},
};
Grid.SetColumn(txt, 1);
grid.Children.Add(txt);
child = grid;
}
else
{
child = new TextBlock
{
Text = label,
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = foreground,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
}
var btn = new Border
{
MinWidth = 84,
Height = 36,
CornerRadius = new CornerRadius(10),
Background = background,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
Padding = new Thickness(14, 0, 14, 0),
Margin = new Thickness(10, 0, 0, 0),
Cursor = Cursors.Hand,
Child = child,
};
btn.MouseEnter += (_, _) => btn.Opacity = 0.85;
@@ -236,6 +277,13 @@ internal sealed class ToolApprovalWindow : Window
/// <summary>도구 승인 다이얼로그를 표시하고 결과를 반환합니다.</summary>
internal static string? Show(Window? owner, string message, List<string> options)
=> Show(owner, message, options, CancellationToken.None);
/// <summary>
/// 도구 승인 다이얼로그를 표시합니다. cancellationToken이 트리거되면 창을 자동으로 닫고 null을 반환합니다.
/// 장시간 미응답 시 타임아웃으로 에이전트 루프가 멈추는 것을 방지하기 위해 사용합니다.
/// </summary>
internal static string? Show(Window? owner, string message, List<string> options, CancellationToken cancellationToken)
{
var dialog = new ToolApprovalWindow(message, options);
if (owner != null && IsWindowAlive(owner))
@@ -244,7 +292,29 @@ internal sealed class ToolApprovalWindow : Window
dialog.Owner = owner;
}
dialog.ShowDialog();
CancellationTokenRegistration reg = default;
if (cancellationToken.CanBeCanceled)
{
reg = cancellationToken.Register(() =>
{
// UI 스레드에서 안전하게 닫기
dialog.Dispatcher.BeginInvoke(new Action(() =>
{
try { if (dialog.IsVisible) dialog.Close(); }
catch { /* 이미 닫혀있거나 파괴 중 — 무시 */ }
}));
});
}
try
{
dialog.ShowDialog();
}
finally
{
reg.Dispose();
}
return dialog._result;
}

View File

@@ -14,6 +14,8 @@ public sealed class TranscriptVisualHost : ContentControl
{
public TranscriptVisualHost()
{
HorizontalAlignment = HorizontalAlignment.Stretch;
HorizontalContentAlignment = HorizontalAlignment.Stretch;
DataContextChanged += OnDataContextChanged;
Loaded += OnLoaded;
Unloaded += OnUnloaded;

View File

@@ -65,9 +65,9 @@ public partial class TrayMenuWindow : Window
}
/// <summary>일반 메뉴 항목을 추가합니다.</summary>
public TrayMenuWindow AddItem(string glyph, string text, Action onClick)
public TrayMenuWindow AddItem(string glyph, string text, Action onClick, string? hint = null)
{
var item = CreateItemBorder(glyph, text);
var item = CreateItemBorder(glyph, text, hint);
item.MouseLeftButtonUp += (_, _) => { Hide(); onClick(); };
MenuPanel.Children.Add(item);
MarkMenuSizeDirty();
@@ -75,9 +75,9 @@ public partial class TrayMenuWindow : Window
}
/// <summary>일반 메뉴 항목을 추가하고 Border 참조를 반환합니다 (동적 가시성 제어용).</summary>
public TrayMenuWindow AddItem(string glyph, string text, Action onClick, out Border itemRef)
public TrayMenuWindow AddItem(string glyph, string text, Action onClick, out Border itemRef, string? hint = null)
{
var item = CreateItemBorder(glyph, text);
var item = CreateItemBorder(glyph, text, hint);
item.MouseLeftButtonUp += (_, _) => { Hide(); onClick(); };
MenuPanel.Children.Add(item);
itemRef = item;
@@ -238,7 +238,7 @@ public partial class TrayMenuWindow : Window
// ─── 내부 헬퍼 ────────────────────────────────────────────────────────
private Border CreateItemBorder(string glyph, string text)
private Border CreateItemBorder(string glyph, string text, string? hint = null)
{
var glyphBlock = new TextBlock
{
@@ -265,6 +265,21 @@ public partial class TrayMenuWindow : Window
Children = { glyphBlock, label },
};
// 힌트 텍스트 (click / double-click 등)
if (!string.IsNullOrWhiteSpace(hint))
{
var hintBlock = new TextBlock
{
Text = hint,
FontSize = 9.5,
Opacity = 0.45,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(6, 1, 0, 0),
};
hintBlock.SetResourceReference(TextBlock.ForegroundProperty, "SecondaryText");
panel.Children.Add(hintBlock);
}
var border = new Border
{
Child = panel,

View File

@@ -7,46 +7,51 @@
WindowStyle="None" AllowsTransparency="True" Background="Transparent"
ResizeMode="CanResizeWithGrip">
<Border Background="{DynamicResource LauncherBackground}"
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
CornerRadius="12" Margin="6"
>
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0.5"
CornerRadius="14" Margin="6">
<Border.Effect>
<DropShadowEffect BlurRadius="18" ShadowDepth="4" Opacity="0.3" Color="Black" Direction="270"/>
<DropShadowEffect BlurRadius="20" ShadowDepth="3" Opacity="0.18" Color="Black" Direction="270"/>
</Border.Effect>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="44"/> <!-- 타이틀 바 -->
<RowDefinition Height="46"/> <!-- 타이틀 바 -->
<RowDefinition Height="Auto"/> <!-- 요약 카드 -->
<RowDefinition Height="Auto"/> <!-- 탭 바 -->
<RowDefinition Height="*"/> <!-- 콘텐츠 (타임라인 또는 병목 분석) -->
<RowDefinition Height="Auto"/> <!-- 상세 패널 -->
<RowDefinition Height="32"/> <!-- 상태 바 -->
<RowDefinition Height="34"/> <!-- 상태 바 -->
</Grid.RowDefinitions>
<!-- ═══ 타이틀 바 ═══ -->
<Grid Grid.Row="0" Background="Transparent" MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="16,0,0,0">
<TextBlock Text="&#xE9D9;" FontFamily="Segoe MDL2 Assets" FontSize="14"
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center" Margin="0,0,8,0"/>
<Border Background="{DynamicResource LauncherBackground}" CornerRadius="14,14,0,0"/>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="18,0,0,0">
<TextBlock Text="&#xE9D9;" FontFamily="Segoe MDL2 Assets" FontSize="15"
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center" Margin="0,0,10,0"/>
<TextBlock Text="워크플로우 분석기" FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
<TextBlock Text="—" FontSize="12" Foreground="{DynamicResource SecondaryText}"
VerticalAlignment="Center" Margin="8,0,0,0" Opacity="0.5"/>
<TextBlock x:Name="TitleLiveIndicator" Text="● LIVE" FontSize="9" FontWeight="Bold"
Foreground="{DynamicResource SuccessColor}" VerticalAlignment="Center"
Margin="8,0,0,0" Opacity="0.8"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,8,0">
<Border x:Name="BtnClear" Width="32" Height="32" CornerRadius="6" Cursor="Hand"
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,10,0">
<Border x:Name="BtnClear" Width="30" Height="30" CornerRadius="8" Cursor="Hand"
Background="Transparent" ToolTip="초기화"
MouseLeftButtonUp="BtnClear_Click"
MouseEnter="TitleBtn_MouseEnter" MouseLeave="TitleBtn_MouseLeave">
<TextBlock Text="&#xE74D;" FontFamily="Segoe MDL2 Assets" FontSize="12"
<TextBlock Text="&#xE74D;" FontFamily="Segoe MDL2 Assets" FontSize="11"
Foreground="{DynamicResource SecondaryText}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<Border x:Name="BtnMinimize" Width="32" Height="32" CornerRadius="6" Cursor="Hand"
<Border x:Name="BtnMinimize" Width="30" Height="30" CornerRadius="8" Cursor="Hand"
Background="Transparent" ToolTip="최소화"
MouseLeftButtonUp="BtnMinimize_Click"
MouseEnter="TitleBtn_MouseEnter" MouseLeave="TitleBtn_MouseLeave">
<TextBlock Text="&#xE921;" FontFamily="Segoe MDL2 Assets" FontSize="10"
Foreground="{DynamicResource SecondaryText}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<Border x:Name="BtnClose" Width="32" Height="32" CornerRadius="6" Cursor="Hand"
<Border x:Name="BtnClose" Width="30" Height="30" CornerRadius="8" Cursor="Hand"
Background="Transparent" ToolTip="닫기"
MouseLeftButtonUp="BtnClose_Click"
MouseEnter="TitleBtn_MouseEnter" MouseLeave="TitleBtn_MouseLeave">
@@ -56,98 +61,116 @@
</StackPanel>
</Grid>
<!-- ═══ 구분선 ═══ -->
<Border Grid.Row="0" VerticalAlignment="Bottom" Height="1"
Background="{DynamicResource SeparatorColor}" Opacity="0.5" Margin="14,0"/>
<!-- ═══ 요약 카드 ═══ -->
<Border Grid.Row="1" Margin="12,0,12,6">
<Border Grid.Row="1" Margin="14,10,14,8">
<UniformGrid Columns="4" Margin="0">
<Border Background="{DynamicResource ItemBackground}" CornerRadius="8" Padding="10,8" Margin="2">
<Border Background="{DynamicResource ItemBackground}" CornerRadius="10" Padding="12,10" Margin="3"
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0.5">
<StackPanel>
<TextBlock Text="소요 시간" FontSize="10" Foreground="{DynamicResource SecondaryText}"/>
<TextBlock x:Name="CardElapsed" Text="0.0s" FontSize="16" FontWeight="Bold" Foreground="#60A5FA"/>
<TextBlock Text="소요 시간" FontSize="10" Foreground="{DynamicResource SecondaryText}" Opacity="0.8"/>
<TextBlock x:Name="CardElapsed" Text="0.0s" FontSize="18" FontWeight="Bold"
Foreground="#60A5FA" Margin="0,4,0,0"/>
</StackPanel>
</Border>
<Border Background="{DynamicResource ItemBackground}" CornerRadius="8" Padding="10,8" Margin="2">
<Border Background="{DynamicResource ItemBackground}" CornerRadius="10" Padding="12,10" Margin="3"
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0.5">
<StackPanel>
<TextBlock Text="반복" FontSize="10" Foreground="{DynamicResource SecondaryText}"/>
<TextBlock x:Name="CardIterations" Text="0" FontSize="16" FontWeight="Bold" Foreground="#A78BFA"/>
<TextBlock Text="반복" FontSize="10" Foreground="{DynamicResource SecondaryText}" Opacity="0.8"/>
<TextBlock x:Name="CardIterations" Text="0" FontSize="18" FontWeight="Bold"
Foreground="#A78BFA" Margin="0,4,0,0"/>
</StackPanel>
</Border>
<Border Background="{DynamicResource ItemBackground}" CornerRadius="8" Padding="10,8" Margin="2"
<Border Background="{DynamicResource ItemBackground}" CornerRadius="10" Padding="12,10" Margin="3"
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0.5"
ToolTip="입력 토큰 / 출력 토큰">
<StackPanel>
<TextBlock Text="토큰" FontSize="10" Foreground="{DynamicResource SecondaryText}"/>
<TextBlock x:Name="CardTokens" Text="0" FontSize="16" FontWeight="Bold" Foreground="#34D399"/>
<StackPanel Orientation="Horizontal" Margin="0,2,0,0">
<TextBlock Text="토큰" FontSize="10" Foreground="{DynamicResource SecondaryText}" Opacity="0.8"/>
<TextBlock x:Name="CardTokens" Text="0" FontSize="18" FontWeight="Bold"
Foreground="#34D399" Margin="0,4,0,0"/>
<StackPanel Orientation="Horizontal" Margin="0,3,0,0">
<TextBlock Text="↑" FontSize="9" Foreground="#3B82F6" VerticalAlignment="Center"/>
<TextBlock x:Name="CardInputTokens" Text="0" FontSize="9" Foreground="#3B82F6" Margin="1,0,6,0" VerticalAlignment="Center"/>
<TextBlock x:Name="CardInputTokens" Text="0" FontSize="9" Foreground="#3B82F6" Margin="2,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="↓" FontSize="9" Foreground="#10B981" VerticalAlignment="Center"/>
<TextBlock x:Name="CardOutputTokens" Text="0" FontSize="9" Foreground="#10B981" Margin="1,0,0,0" VerticalAlignment="Center"/>
<TextBlock x:Name="CardOutputTokens" Text="0" FontSize="9" Foreground="#10B981" Margin="2,0,0,0" VerticalAlignment="Center"/>
</StackPanel>
</StackPanel>
</Border>
<Border Background="{DynamicResource ItemBackground}" CornerRadius="8" Padding="10,8" Margin="2">
<Border Background="{DynamicResource ItemBackground}" CornerRadius="10" Padding="12,10" Margin="3"
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0.5">
<StackPanel>
<TextBlock Text="도구 호출" FontSize="10" Foreground="{DynamicResource SecondaryText}"/>
<TextBlock x:Name="CardToolCalls" Text="0" FontSize="16" FontWeight="Bold" Foreground="#FBBF24"/>
<TextBlock Text="도구 호출" FontSize="10" Foreground="{DynamicResource SecondaryText}" Opacity="0.8"/>
<TextBlock x:Name="CardToolCalls" Text="0" FontSize="18" FontWeight="Bold"
Foreground="#FBBF24" Margin="0,4,0,0"/>
</StackPanel>
</Border>
</UniformGrid>
</Border>
<!-- ═══ 탭 바 ═══ -->
<Border Grid.Row="2" Margin="12,0,12,6">
<StackPanel Orientation="Horizontal">
<Border x:Name="TabTimeline" CornerRadius="6" Padding="12,6" Margin="0,0,4,0"
Cursor="Hand" MouseLeftButtonUp="TabTimeline_Click">
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="TabTimelineIcon" Text="&#xE916;" FontFamily="Segoe MDL2 Assets" FontSize="11"
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock x:Name="TabTimelineText" Text="타임라인" FontSize="12" FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
</StackPanel>
</Border>
<Border x:Name="TabBottleneck" CornerRadius="6" Padding="12,6" Margin="0,0,4,0"
Cursor="Hand" MouseLeftButtonUp="TabBottleneck_Click">
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="TabBottleneckIcon" Text="&#xE9D2;" FontFamily="Segoe MDL2 Assets" FontSize="11"
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock x:Name="TabBottleneckText" Text="병목 분석" FontSize="12" FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
</StackPanel>
</Border>
</StackPanel>
<Border Grid.Row="2" Margin="14,0,14,8">
<Border Background="{DynamicResource ItemBackground}" CornerRadius="10" Padding="4"
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0.5">
<StackPanel Orientation="Horizontal">
<Border x:Name="TabTimeline" CornerRadius="8" Padding="14,7" Margin="0,0,2,0"
Cursor="Hand" MouseLeftButtonUp="TabTimeline_Click">
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="TabTimelineIcon" Text="&#xE916;" FontFamily="Segoe MDL2 Assets" FontSize="11"
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock x:Name="TabTimelineText" Text="타임라인" FontSize="12" FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
</StackPanel>
</Border>
<Border x:Name="TabBottleneck" CornerRadius="8" Padding="14,7" Margin="0"
Cursor="Hand" MouseLeftButtonUp="TabBottleneck_Click">
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="TabBottleneckIcon" Text="&#xE9D2;" FontFamily="Segoe MDL2 Assets" FontSize="11"
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock x:Name="TabBottleneckText" Text="병목 분석" FontSize="12" FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
</StackPanel>
</Border>
</StackPanel>
</Border>
</Border>
<!-- ═══ 콘텐츠: 타임라인 ═══ -->
<ScrollViewer Grid.Row="3" x:Name="TimelineScroller" VerticalScrollBarVisibility="Auto"
Margin="12,0,12,0" Padding="0,0,4,0">
Margin="14,0,14,0" Padding="0,0,4,0">
<StackPanel x:Name="TimelinePanel" Margin="0,0,0,8"/>
</ScrollViewer>
<!-- ═══ 콘텐츠: 병목 분석 ═══ -->
<ScrollViewer Grid.Row="3" x:Name="BottleneckScroller" VerticalScrollBarVisibility="Auto"
Margin="12,0,12,0" Padding="0,0,4,0" Visibility="Collapsed">
Margin="14,0,14,0" Padding="0,0,4,0" Visibility="Collapsed">
<StackPanel x:Name="BottleneckPanel" Margin="0,0,0,8">
<!-- 워터폴 차트 -->
<TextBlock Text="⏱ 실행 흐름 (워터폴)" FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}" Margin="4,4,0,8"/>
<Border Background="{DynamicResource ItemBackground}" CornerRadius="8"
Padding="12,8" Margin="0,0,0,12" MinHeight="60">
<Border Background="{DynamicResource ItemBackground}" CornerRadius="10"
Padding="14,10" Margin="0,0,0,14" MinHeight="60"
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0.5">
<Canvas x:Name="WaterfallCanvas" Height="120" ClipToBounds="True"/>
</Border>
<!-- 도구별 소요시간 -->
<TextBlock Text="🔧 도구별 소요시간" FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}" Margin="4,4,0,8"/>
<Border Background="{DynamicResource ItemBackground}" CornerRadius="8"
Padding="12,8" Margin="0,0,0,12">
<Border Background="{DynamicResource ItemBackground}" CornerRadius="10"
Padding="14,10" Margin="0,0,0,14"
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0.5">
<StackPanel x:Name="ToolTimePanel"/>
</Border>
<!-- 토큰 추세 -->
<TextBlock Text="📈 반복별 토큰 추세" FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}" Margin="4,4,0,8"/>
<Border Background="{DynamicResource ItemBackground}" CornerRadius="8"
Padding="12,8" Margin="0,0,0,8" MinHeight="60">
<Border Background="{DynamicResource ItemBackground}" CornerRadius="10"
Padding="14,10" Margin="0,0,0,8" MinHeight="60"
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0.5">
<Canvas x:Name="TokenTrendCanvas" Height="100" ClipToBounds="True"/>
</Border>
</StackPanel>
@@ -155,35 +178,53 @@
<!-- ═══ 상세 패널 ═══ -->
<Border Grid.Row="4" x:Name="DetailPanel" Visibility="Collapsed"
Background="{DynamicResource ItemBackground}" CornerRadius="8"
Margin="12,4,12,4" Padding="14,10" MaxHeight="180">
Background="{DynamicResource ItemBackground}" CornerRadius="10"
Margin="14,4,14,4" Padding="16,12" MaxHeight="200"
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0.5">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
<TextBlock x:Name="DetailTitle" Text="" FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryText}"/>
<TextBlock x:Name="DetailBadge" Text="" FontSize="10" Foreground="White"
Margin="8,0,0,0" VerticalAlignment="Center" Padding="6,1"
Background="#34D399"/>
<Border x:Name="DetailBadgeBorder" CornerRadius="4" Margin="8,0,0,0"
VerticalAlignment="Center" Padding="6,2">
<Border.Background>
<SolidColorBrush Color="#34D399"/>
</Border.Background>
<TextBlock x:Name="DetailBadge" Text="" FontSize="10" Foreground="White" FontWeight="SemiBold"/>
</Border>
</StackPanel>
<TextBlock x:Name="DetailMeta" Text="" FontSize="11"
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,4"/>
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,6"/>
<Border Height="1" Background="{DynamicResource SeparatorColor}" Opacity="0.4" Margin="0,0,0,6"/>
<TextBlock x:Name="DetailContent" Text="" FontSize="11.5" TextWrapping="Wrap"
Foreground="{DynamicResource PrimaryText}" MaxHeight="100"
LineHeight="17"/>
Foreground="{DynamicResource PrimaryText}" MaxHeight="110"
LineHeight="18"/>
</StackPanel>
</ScrollViewer>
</Border>
<!-- ═══ 상태 바 ═══ -->
<Border Grid.Row="5" Margin="12,0,12,6" ClipToBounds="True">
<Border Grid.Row="5" Margin="14,0,14,8" ClipToBounds="True">
<Grid>
<TextBlock x:Name="StatusText" Text="대기 중..." FontSize="11"
Foreground="{DynamicResource SecondaryText}" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" MaxWidth="400"/>
<TextBlock x:Name="LogLevelBadge" Text="" FontSize="10" HorizontalAlignment="Right"
Foreground="{DynamicResource SecondaryText}" VerticalAlignment="Center"
Padding="6,1" Opacity="0.7"/>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Border Width="6" Height="6" CornerRadius="3" Margin="0,0,6,0"
VerticalAlignment="Center">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="{DynamicResource SecondaryText}"/>
</Style>
</Border.Style>
</Border>
<TextBlock x:Name="StatusText" Text="대기 중..." FontSize="11"
Foreground="{DynamicResource SecondaryText}" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" MaxWidth="380"/>
</StackPanel>
<Border HorizontalAlignment="Right" CornerRadius="4" Padding="6,2"
Background="{DynamicResource HintBackground}" VerticalAlignment="Center">
<TextBlock x:Name="LogLevelBadge" Text="" FontSize="9" FontWeight="SemiBold"
Foreground="{DynamicResource HintText}" VerticalAlignment="Center"/>
</Border>
</Grid>
</Border>
</Grid>

View File

@@ -833,9 +833,10 @@ public partial class WorkflowAnalyzerWindow : Window
DetailTitle.Text = label;
DetailBadge.Text = evt.Success ? "성공" : "실패";
DetailBadge.Background = new SolidColorBrush(evt.Success
? Color.FromRgb(0x34, 0xD3, 0x99)
: Color.FromRgb(0xF8, 0x71, 0x71));
if (DetailBadgeBorder != null)
DetailBadgeBorder.Background = new SolidColorBrush(evt.Success
? Color.FromRgb(0x34, 0xD3, 0x99)
: Color.FromRgb(0xF8, 0x71, 0x71));
var meta = $"시간: {evt.Timestamp:HH:mm:ss.fff}";
if (evt.ElapsedMs > 0)