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:
@@ -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="" 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="" 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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}"/>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ public sealed class TranscriptVisualHost : ContentControl
|
||||
{
|
||||
public TranscriptVisualHost()
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch;
|
||||
HorizontalContentAlignment = HorizontalAlignment.Stretch;
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
Loaded += OnLoaded;
|
||||
Unloaded += OnUnloaded;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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="" 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="" 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="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
<TextBlock Text="" 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="" 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="" 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="" 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="" 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="" 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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user