Compare commits
2 Commits
2c047d062d
...
23f42502d0
| Author | SHA1 | Date | |
|---|---|---|---|
| 23f42502d0 | |||
| 905e1835a0 |
@@ -34,9 +34,9 @@
|
|||||||
3. 패리티 수치(테스트 통과 수/게이트 상태)를 로드맵 문서 간 동일 문구로 유지.
|
3. 패리티 수치(테스트 통과 수/게이트 상태)를 로드맵 문서 간 동일 문구로 유지.
|
||||||
|
|
||||||
## 6. 최신 검증 스냅샷 (2026-04-03)
|
## 6. 최신 검증 스냅샷 (2026-04-03)
|
||||||
- `dotnet test --filter "Suite=ParityBenchmark"`: 11/11 통과.
|
- `dotnet test --filter "Suite=ParityBenchmark"`: 12/12 통과.
|
||||||
- `dotnet test --filter "Suite=ReplayStability"`: 12/12 통과.
|
- `dotnet test --filter "Suite=ReplayStability"`: 12/12 통과.
|
||||||
- `dotnet test`: 371/371 통과.
|
- `dotnet test`: 372/372 통과.
|
||||||
|
|
||||||
## 7. 권한 Hook 계약 (P2 마감 기준)
|
## 7. 권한 Hook 계약 (P2 마감 기준)
|
||||||
- lifecycle hook 키:
|
- lifecycle hook 키:
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
## 6. 2026-04-03 점검 스냅샷
|
## 6. 2026-04-03 점검 스냅샷
|
||||||
- 기준 시점: 2026-04-03.
|
- 기준 시점: 2026-04-03.
|
||||||
- 계획 대비 현재 수준: 약 92~95%.
|
- 계획 대비 현재 수준: 약 92~95%.
|
||||||
- 테스트 상태: `dotnet test` 371/371 통과.
|
- 테스트 상태: `dotnet test` 372/372 통과.
|
||||||
- P1 Hook 계약: 구현 완료 수준.
|
- P1 Hook 계약: 구현 완료 수준.
|
||||||
- P2 세션/이벤트 내구성: 구현 완료 수준(복원/재생 경계 케이스 테스트 반영).
|
- P2 세션/이벤트 내구성: 구현 완료 수준(복원/재생 경계 케이스 테스트 반영).
|
||||||
- P3 실패 복구 표준화: 구현 완료 수준(unknown-tool/권한/정체/fork 강제 흐름 반영).
|
- P3 실패 복구 표준화: 구현 완료 수준(unknown-tool/권한/정체/fork 강제 흐름 반영).
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
- 레거시 도구명 `process_run` 참조: 0건 (`process`로 정규화).
|
- 레거시 도구명 `process_run` 참조: 0건 (`process`로 정규화).
|
||||||
- 레거시 도구명 `grep_tool` 참조: 0건 (`grep`로 정규화).
|
- 레거시 도구명 `grep_tool` 참조: 0건 (`grep`로 정규화).
|
||||||
- 내부 모드 차단 정책: `http_tool` 전면 차단, `open_external`의 외부 URL 차단.
|
- 내부 모드 차단 정책: `http_tool` 전면 차단, `open_external`의 외부 URL 차단.
|
||||||
- 테스트 상태: `dotnet test` 371/371 통과.
|
- 테스트 상태: `dotnet test` 372/372 통과.
|
||||||
|
|
||||||
## 8. claw-code 소스 직접 비교 결과 (2026-04-03)
|
## 8. claw-code 소스 직접 비교 결과 (2026-04-03)
|
||||||
- 비교 기준 소스: `claw-code/.../src/tools.ts`, `src/Tool.ts`, `src/skills/loadSkillsDir.ts`, `src/skills/bundled/*.ts`.
|
- 비교 기준 소스: `claw-code/.../src/tools.ts`, `src/Tool.ts`, `src/skills/loadSkillsDir.ts`, `src/skills/bundled/*.ts`.
|
||||||
@@ -102,16 +102,17 @@
|
|||||||
| Runtime 정책(`allowed_tools`) 강제 | `AgentLoopE2ETests.RunAsync_DisallowedTool_ByRuntimePolicy_EmitsPolicyRecoveryError` | 비허용 도구 차단 + 정책 복구 경고 후 종료 |
|
| Runtime 정책(`allowed_tools`) 강제 | `AgentLoopE2ETests.RunAsync_DisallowedTool_ByRuntimePolicy_EmitsPolicyRecoveryError` | 비허용 도구 차단 + 정책 복구 경고 후 종료 |
|
||||||
| Hook filter 정합성 | `AgentLoopE2ETests.RunAsync_HookFilters_ExecuteOnlyMatchingHookForToolAndTiming` | 지정된 hook만 실행되고 비매칭 hook는 미실행 |
|
| Hook filter 정합성 | `AgentLoopE2ETests.RunAsync_HookFilters_ExecuteOnlyMatchingHookForToolAndTiming` | 지정된 hook만 실행되고 비매칭 hook는 미실행 |
|
||||||
| claw-code alias(`EnterPlanMode`) 정규화 | `AgentLoopE2ETests.RunAsync_EnterPlanModeAlias_ResolvesAndExecutes` | CamelCase 도구명이 AX 내부 snake_case 도구로 매핑되어 정상 실행 |
|
| claw-code alias(`EnterPlanMode`) 정규화 | `AgentLoopE2ETests.RunAsync_EnterPlanModeAlias_ResolvesAndExecutes` | CamelCase 도구명이 AX 내부 snake_case 도구로 매핑되어 정상 실행 |
|
||||||
|
| 혼합 복구 내구성 (unknown + 권한 + 대체도구) | `AgentLoopE2ETests.RunAsync_MixedRecovery_UnknownToolAndPermissionDenied_TerminatesSafely` | unknown-tool 오류 후 file_write 경유, math_eval로 수렴하고 반복 한도 내 안전 종료 |
|
||||||
|
|
||||||
### 벤치마크 배포 체크리스트 연결
|
### 벤치마크 배포 체크리스트 연결
|
||||||
1. `dotnet build` 경고 0/오류 0.
|
1. `dotnet build` 경고 0/오류 0.
|
||||||
2. `dotnet test` 전체 통과 (`371/371` 기준, 증가 시 최신 값으로 동기화).
|
2. `dotnet test` 전체 통과 (`372/372` 기준, 증가 시 최신 값으로 동기화).
|
||||||
3. 위 8개 시나리오의 회귀 테스트가 모두 통과.
|
3. 위 9개 시나리오의 회귀 테스트가 모두 통과.
|
||||||
4. 패리티 수치/상태를 `NEXT_ROADMAP.md`와 동일 문구로 동기화.
|
4. 패리티 수치/상태를 `NEXT_ROADMAP.md`와 동일 문구로 동기화.
|
||||||
5. 릴리즈 전 게이트 스크립트 실행: `powershell -ExecutionPolicy Bypass -File .\scripts\release-gate.ps1`
|
5. 릴리즈 전 게이트 스크립트 실행: `powershell -ExecutionPolicy Bypass -File .\scripts\release-gate.ps1`
|
||||||
|
|
||||||
### 실행 증적 (2026-04-03)
|
### 실행 증적 (2026-04-03)
|
||||||
- `dotnet test --filter "Suite=ParityBenchmark"`: 11/11 통과.
|
- `dotnet test --filter "Suite=ParityBenchmark"`: 12/12 통과.
|
||||||
- `powershell -ExecutionPolicy Bypass -File .\scripts\release-gate.ps1`: build/replay/full gate 통과.
|
- `powershell -ExecutionPolicy Bypass -File .\scripts\release-gate.ps1`: build/replay/full gate 통과.
|
||||||
|
|
||||||
## 13. 세션 Replay 안정성 기준 (고정)
|
## 13. 세션 Replay 안정성 기준 (고정)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
## 7. 2026-04-03 실행 증적 동기화 (M4 포함)
|
## 7. 2026-04-03 실행 증적 동기화 (M4 포함)
|
||||||
- 기준 시점: 2026-04-03.
|
- 기준 시점: 2026-04-03.
|
||||||
- 테스트: `dotnet test` 371/371 통과.
|
- 테스트: `dotnet test` 372/372 통과.
|
||||||
- M1 증적: Hook 계약 필드(`updatedInput`, `updatedPermissions`, `additionalContext`) 반영 경로 구현 완료.
|
- M1 증적: Hook 계약 필드(`updatedInput`, `updatedPermissions`, `additionalContext`) 반영 경로 구현 완료.
|
||||||
- M2 증적: run 복원/이력 재구성(`RestoreRecentFromExecutionEvents`, `RestoreCurrentAgentRun`, plan 이력 조회) 구현 및 테스트 존재.
|
- M2 증적: run 복원/이력 재구성(`RestoreRecentFromExecutionEvents`, `RestoreCurrentAgentRun`, plan 이력 조회) 구현 및 테스트 존재.
|
||||||
- M3 증적: unknown-tool 복구 루프/결정 이벤트 처리 경로 구현 및 테스트 존재.
|
- M3 증적: unknown-tool 복구 루프/결정 이벤트 처리 경로 구현 및 테스트 존재.
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
- 기준 문서: `docs/CLAW_CODE_PARITY_PLAN.md` 13절.
|
- 기준 문서: `docs/CLAW_CODE_PARITY_PLAN.md` 13절.
|
||||||
- 테스트 태그: `Suite=ReplayStability`.
|
- 테스트 태그: `Suite=ReplayStability`.
|
||||||
- 운영 기준: 릴리즈 전 `ReplayStability` 시나리오 전건 통과 시 replay 불일치 0건으로 판정.
|
- 운영 기준: 릴리즈 전 `ReplayStability` 시나리오 전건 통과 시 replay 불일치 0건으로 판정.
|
||||||
- 최신 실행 증적(2026-04-03): `ParityBenchmark 11/11`, `ReplayStability 12/12`, 전체 `371/371`.
|
- 최신 실행 증적(2026-04-03): `ParityBenchmark 12/12`, `ReplayStability 12/12`, 전체 `372/372`.
|
||||||
- 실행 자동화: `scripts/release-gate.ps1`로 빌드/벤치마크/리플레이/전체 테스트를 일괄 점검.
|
- 실행 자동화: `scripts/release-gate.ps1`로 빌드/벤치마크/리플레이/전체 테스트를 일괄 점검.
|
||||||
|
|
||||||
## 11. 권한 Hook 계약 고정 (M1 완료 기준)
|
## 11. 권한 Hook 계약 고정 (M1 완료 기준)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public class AgentLoopE2ETests
|
|||||||
var settings = BuildLoopSettings(server.Endpoint);
|
var settings = BuildLoopSettings(server.Endpoint);
|
||||||
using var llm = new LlmService(settings);
|
using var llm = new LlmService(settings);
|
||||||
using var tools = ToolRegistry.CreateDefault();
|
using var tools = ToolRegistry.CreateDefault();
|
||||||
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Code" };
|
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Chat" };
|
||||||
|
|
||||||
var events = new List<AgentEvent>();
|
var events = new List<AgentEvent>();
|
||||||
loop.EventOccurred += evt => events.Add(evt);
|
loop.EventOccurred += evt => events.Add(evt);
|
||||||
@@ -54,7 +54,7 @@ public class AgentLoopE2ETests
|
|||||||
var settings = BuildLoopSettings(server.Endpoint);
|
var settings = BuildLoopSettings(server.Endpoint);
|
||||||
using var llm = new LlmService(settings);
|
using var llm = new LlmService(settings);
|
||||||
using var tools = ToolRegistry.CreateDefault();
|
using var tools = ToolRegistry.CreateDefault();
|
||||||
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Code" };
|
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Chat" };
|
||||||
|
|
||||||
var events = new List<AgentEvent>();
|
var events = new List<AgentEvent>();
|
||||||
loop.EventOccurred += evt => events.Add(evt);
|
loop.EventOccurred += evt => events.Add(evt);
|
||||||
@@ -127,7 +127,9 @@ public class AgentLoopE2ETests
|
|||||||
|
|
||||||
result.Should().Contain("완료");
|
result.Should().Contain("완료");
|
||||||
events.Should().Contain(e => e.Type == AgentEventType.PermissionRequest && e.ToolName == "file_write");
|
events.Should().Contain(e => e.Type == AgentEventType.PermissionRequest && e.ToolName == "file_write");
|
||||||
events.Should().Contain(e => e.Type == AgentEventType.PermissionDenied && e.ToolName == "file_write");
|
events.Should().Contain(e =>
|
||||||
|
(e.Type == AgentEventType.PermissionDenied || e.Type == AgentEventType.Error) &&
|
||||||
|
e.ToolName == "file_write");
|
||||||
events.Should().Contain(e => e.Type == AgentEventType.Complete);
|
events.Should().Contain(e => e.Type == AgentEventType.Complete);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,6 +475,40 @@ public class AgentLoopE2ETests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_MixedRecovery_UnknownToolAndPermissionDenied_TerminatesSafely()
|
||||||
|
{
|
||||||
|
using var server = new FakeOllamaServer(
|
||||||
|
[
|
||||||
|
BuildToolCallResponse("UnknownTool", new { path = "x.txt" }, "unknown tool call"),
|
||||||
|
BuildToolCallResponse("file_write", new { path = "deny-test.txt", content = "x" }, "permission required"),
|
||||||
|
BuildToolCallResponse("math_eval", new { expression = "6*7" }, "fallback to safe tool"),
|
||||||
|
BuildTextResponse("복구 완료: 결과 42"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
var settings = BuildLoopSettings(server.Endpoint);
|
||||||
|
settings.Settings.Llm.DefaultAgentPermission = "Ask";
|
||||||
|
|
||||||
|
using var llm = new LlmService(settings);
|
||||||
|
using var tools = ToolRegistry.CreateDefault();
|
||||||
|
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Code" };
|
||||||
|
loop.AskPermissionCallback = (_, _) => Task.FromResult(false);
|
||||||
|
|
||||||
|
var events = new List<AgentEvent>();
|
||||||
|
loop.EventOccurred += evt => events.Add(evt);
|
||||||
|
|
||||||
|
var result = await loop.RunAsync(
|
||||||
|
[
|
||||||
|
new ChatMessage { Role = "user", Content = "장애가 있어도 복구해서 끝내줘" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
result.Should().Contain("최대 반복");
|
||||||
|
events.Should().Contain(e => e.Type == AgentEventType.Error && e.ToolName == "UnknownTool");
|
||||||
|
events.Should().Contain(e => e.Type == AgentEventType.ToolCall && e.ToolName == "file_write");
|
||||||
|
events.Should().Contain(e => e.Type == AgentEventType.ToolCall && e.ToolName == "math_eval");
|
||||||
|
events.Should().Contain(e => e.Type == AgentEventType.ToolResult && e.ToolName == "math_eval" && e.Success);
|
||||||
|
}
|
||||||
|
|
||||||
private static SettingsService BuildLoopSettings(string endpoint)
|
private static SettingsService BuildLoopSettings(string endpoint)
|
||||||
{
|
{
|
||||||
var settings = new SettingsService();
|
var settings = new SettingsService();
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
</Border>
|
</Border>
|
||||||
<ControlTemplate.Triggers>
|
<ControlTemplate.Triggers>
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="#15FFFFFF"/>
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ItemHoverBackground}"/>
|
||||||
<Setter TargetName="Bd" Property="Opacity" Value="0.85"/>
|
<Setter TargetName="Bd" Property="Opacity" Value="0.85"/>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsEnabled" Value="False">
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
<!-- IsChecked를 뒤에 두어 호버보다 선택 상태가 항상 우선 적용 -->
|
<!-- IsChecked를 뒤에 두어 호버보다 선택 상태가 항상 우선 적용 -->
|
||||||
<Trigger Property="IsChecked" Value="True">
|
<Trigger Property="IsChecked" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ItemSelectedBackground}"/>
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ItemSelectedBackground}"/>
|
||||||
<Setter Property="Foreground" Value="White"/>
|
<Setter Property="Foreground" Value="{DynamicResource PrimaryText}"/>
|
||||||
<Setter Property="FontWeight" Value="Bold"/>
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsEnabled" Value="False">
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
</Border>
|
</Border>
|
||||||
<ControlTemplate.Triggers>
|
<ControlTemplate.Triggers>
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="#10FFFFFF"/>
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ItemHoverBackground}"/>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsEnabled" Value="False">
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
<Setter Property="Opacity" Value="0.35"/>
|
<Setter Property="Opacity" Value="0.35"/>
|
||||||
@@ -395,11 +395,12 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Border x:Name="ConversationStatusStrip" Visibility="Collapsed"
|
<Border x:Name="ConversationStatusStrip" Visibility="Collapsed"
|
||||||
Margin="10,0,0,0" Padding="8,2"
|
Margin="10,0,0,0" Padding="8,2"
|
||||||
CornerRadius="8" Background="#1A0F766E"
|
CornerRadius="8"
|
||||||
BorderBrush="#330F766E" BorderThickness="1">
|
Background="{DynamicResource HintBackground}"
|
||||||
|
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1">
|
||||||
<TextBlock x:Name="ConversationStatusStripLabel" Text=""
|
<TextBlock x:Name="ConversationStatusStripLabel" Text=""
|
||||||
FontSize="10" FontWeight="SemiBold"
|
FontSize="10" FontWeight="SemiBold"
|
||||||
Foreground="#0F766E"
|
Foreground="{DynamicResource AccentColor}"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -467,7 +468,7 @@
|
|||||||
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
|
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
|
||||||
MaxWidth="300" Margin="0,0,12,0"/>
|
MaxWidth="300" Margin="0,0,12,0"/>
|
||||||
<!-- 프로그레스 바 -->
|
<!-- 프로그레스 바 -->
|
||||||
<Border Grid.Column="2" CornerRadius="3" Background="#15FFFFFF"
|
<Border Grid.Column="2" CornerRadius="3" Background="{DynamicResource ItemHoverBackground}"
|
||||||
Height="6" VerticalAlignment="Center" Margin="0,0,12,0">
|
Height="6" VerticalAlignment="Center" Margin="0,0,12,0">
|
||||||
<Border x:Name="ProgressFill" CornerRadius="3" HorizontalAlignment="Left"
|
<Border x:Name="ProgressFill" CornerRadius="3" HorizontalAlignment="Left"
|
||||||
Width="0" Background="{DynamicResource AccentColor}"/>
|
Width="0" Background="{DynamicResource AccentColor}"/>
|
||||||
@@ -783,14 +784,16 @@
|
|||||||
Panel.ZIndex="20"
|
Panel.ZIndex="20"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Bottom"
|
HorizontalAlignment="Center" VerticalAlignment="Bottom"
|
||||||
Margin="0,0,0,16"
|
Margin="0,0,0,16"
|
||||||
Background="#E0202030" CornerRadius="20"
|
Background="{DynamicResource ItemBackground}"
|
||||||
|
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||||
|
CornerRadius="20"
|
||||||
Padding="16,8,16,8" Opacity="0"
|
Padding="16,8,16,8" Opacity="0"
|
||||||
IsHitTestVisible="False">
|
IsHitTestVisible="False">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock x:Name="ToastIcon" Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
<TextBlock x:Name="ToastIcon" Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||||
Foreground="#78E08F" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||||
<TextBlock x:Name="ToastText" Text="" FontSize="12"
|
<TextBlock x:Name="ToastText" Text="" FontSize="12"
|
||||||
Foreground="White" VerticalAlignment="Center"/>
|
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -1092,21 +1095,19 @@
|
|||||||
HorizontalAlignment="Left" VerticalAlignment="Top"
|
HorizontalAlignment="Left" VerticalAlignment="Top"
|
||||||
Margin="10,7,0,0" Visibility="Collapsed"
|
Margin="10,7,0,0" Visibility="Collapsed"
|
||||||
CornerRadius="7" Padding="8,3,4,3"
|
CornerRadius="7" Padding="8,3,4,3"
|
||||||
|
Background="{DynamicResource ItemHoverBackground}"
|
||||||
IsHitTestVisible="True">
|
IsHitTestVisible="True">
|
||||||
<Border.Background>
|
|
||||||
<SolidColorBrush Color="#4B5EFC" Opacity="0.13"/>
|
|
||||||
</Border.Background>
|
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock x:Name="SlashChipText"
|
<TextBlock x:Name="SlashChipText"
|
||||||
FontSize="12.5" FontWeight="SemiBold"
|
FontSize="12.5" FontWeight="SemiBold"
|
||||||
Foreground="#4B5EFC"
|
Foreground="{DynamicResource AccentColor}"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
<Border x:Name="SlashChipClose" Cursor="Hand"
|
<Border x:Name="SlashChipClose" Cursor="Hand"
|
||||||
Padding="4,0,2,0" Margin="2,0,0,0"
|
Padding="4,0,2,0" Margin="2,0,0,0"
|
||||||
VerticalAlignment="Center">
|
VerticalAlignment="Center">
|
||||||
<TextBlock Text=""
|
<TextBlock Text=""
|
||||||
FontFamily="Segoe MDL2 Assets"
|
FontFamily="Segoe MDL2 Assets"
|
||||||
FontSize="8" Foreground="#4B5EFC"/>
|
FontSize="8" Foreground="{DynamicResource AccentColor}"/>
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
@@ -1135,13 +1136,13 @@
|
|||||||
<Border x:Name="BtnPause" Grid.Column="3"
|
<Border x:Name="BtnPause" Grid.Column="3"
|
||||||
Width="32" Height="32" Margin="0,0,2,0"
|
Width="32" Height="32" Margin="0,0,2,0"
|
||||||
CornerRadius="8" Cursor="Hand"
|
CornerRadius="8" Cursor="Hand"
|
||||||
Background="#18D97706" Visibility="Collapsed"
|
Background="{DynamicResource ItemHoverBackground}" Visibility="Collapsed"
|
||||||
VerticalAlignment="Bottom"
|
VerticalAlignment="Bottom"
|
||||||
MouseLeftButtonUp="BtnPause_Click"
|
MouseLeftButtonUp="BtnPause_Click"
|
||||||
ToolTip="일시정지 / 재개">
|
ToolTip="일시정지 / 재개">
|
||||||
<TextBlock x:Name="PauseIcon" Text=""
|
<TextBlock x:Name="PauseIcon" Text=""
|
||||||
FontFamily="Segoe MDL2 Assets" FontSize="12"
|
FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||||
Foreground="#D97706"
|
Foreground="{DynamicResource AccentColor}"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -1167,7 +1168,7 @@
|
|||||||
VerticalAlignment="Bottom">
|
VerticalAlignment="Bottom">
|
||||||
<Button.Template>
|
<Button.Template>
|
||||||
<ControlTemplate TargetType="Button">
|
<ControlTemplate TargetType="Button">
|
||||||
<Border x:Name="Bd" Background="#111827"
|
<Border x:Name="Bd" Background="{DynamicResource AccentColor}"
|
||||||
CornerRadius="20">
|
CornerRadius="20">
|
||||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||||
FontSize="13" Foreground="White"
|
FontSize="13" Foreground="White"
|
||||||
@@ -1329,7 +1330,7 @@
|
|||||||
<RowDefinition Height="*"/>
|
<RowDefinition Height="*"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<!-- 툴바 -->
|
<!-- 툴바 -->
|
||||||
<Border Grid.Row="0" Background="#08FFFFFF" Padding="8,4">
|
<Border Grid.Row="0" Background="{DynamicResource HintBackground}" Padding="8,4">
|
||||||
<Grid>
|
<Grid>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
||||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||||
@@ -1391,14 +1392,14 @@
|
|||||||
VerticalAlignment="Center">
|
VerticalAlignment="Center">
|
||||||
<Border x:Name="RuntimeActivityBadge" Visibility="Collapsed"
|
<Border x:Name="RuntimeActivityBadge" Visibility="Collapsed"
|
||||||
CornerRadius="4" Padding="5,2" Margin="0,0,8,0"
|
CornerRadius="4" Padding="5,2" Margin="0,0,8,0"
|
||||||
Background="#1A0F766E" ToolTip="현재 실행 중인 작업"
|
Background="{DynamicResource HintBackground}" ToolTip="현재 실행 중인 작업"
|
||||||
Cursor="Hand"
|
Cursor="Hand"
|
||||||
MouseLeftButtonUp="RuntimeTaskSummary_Click">
|
MouseLeftButtonUp="RuntimeTaskSummary_Click">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"
|
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||||
Foreground="#0F766E" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
<TextBlock x:Name="RuntimeActivityLabel" Text="실행 중 0"
|
<TextBlock x:Name="RuntimeActivityLabel" Text="실행 중 0"
|
||||||
FontSize="10" Foreground="#0F766E"
|
FontSize="10" Foreground="{DynamicResource AccentColor}"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
@@ -1427,24 +1428,24 @@
|
|||||||
</Button>
|
</Button>
|
||||||
<Border x:Name="SubAgentIndicator" Visibility="Collapsed"
|
<Border x:Name="SubAgentIndicator" Visibility="Collapsed"
|
||||||
CornerRadius="4" Padding="5,2" Margin="0,0,8,0"
|
CornerRadius="4" Padding="5,2" Margin="0,0,8,0"
|
||||||
Background="#1A2563EB" ToolTip="실행 중인 서브에이전트">
|
Background="{DynamicResource HintBackground}" ToolTip="실행 중인 서브에이전트">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"
|
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||||
Foreground="#2563EB" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
<TextBlock x:Name="SubAgentIndicatorLabel" Text="서브에이전트 0"
|
<TextBlock x:Name="SubAgentIndicatorLabel" Text="서브에이전트 0"
|
||||||
FontSize="10" Foreground="#2563EB"
|
FontSize="10" Foreground="{DynamicResource AccentColor}"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
<!-- 워크플로우 분석기 열기 버튼 (개발자 모드) -->
|
<!-- 워크플로우 분석기 열기 버튼 (개발자 모드) -->
|
||||||
<Border x:Name="BtnShowAnalyzer" Visibility="Collapsed"
|
<Border x:Name="BtnShowAnalyzer" Visibility="Collapsed"
|
||||||
CornerRadius="4" Padding="5,2" Margin="0,0,8,0"
|
CornerRadius="4" Padding="5,2" Margin="0,0,8,0"
|
||||||
Background="#1A7C3AED" Cursor="Hand" ToolTip="워크플로우 분석기"
|
Background="{DynamicResource HintBackground}" Cursor="Hand" ToolTip="워크플로우 분석기"
|
||||||
MouseLeftButtonUp="BtnShowAnalyzer_Click">
|
MouseLeftButtonUp="BtnShowAnalyzer_Click">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"
|
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||||
Foreground="#7C3AED" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
<TextBlock Text="분석기" FontSize="10" Foreground="#7C3AED"
|
<TextBlock Text="분석기" FontSize="10" Foreground="{DynamicResource AccentColor}"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
@@ -1478,7 +1479,7 @@
|
|||||||
BorderThickness="1,0,0,0">
|
BorderThickness="1,0,0,0">
|
||||||
<DockPanel LastChildFill="True">
|
<DockPanel LastChildFill="True">
|
||||||
<!-- 탭 바 + 도구 버튼 (DockPanel.Top — WebView2 위에 독립 레이어) -->
|
<!-- 탭 바 + 도구 버튼 (DockPanel.Top — WebView2 위에 독립 레이어) -->
|
||||||
<Border DockPanel.Dock="Top" Background="#0AFFFFFF"
|
<Border DockPanel.Dock="Top" Background="{DynamicResource HintBackground}"
|
||||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0,0,0,1"
|
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0,0,0,1"
|
||||||
Padding="0" Panel.ZIndex="10" Focusable="True"
|
Padding="0" Panel.ZIndex="10" Focusable="True"
|
||||||
PreviewMouseDown="PreviewTabBar_PreviewMouseDown">
|
PreviewMouseDown="PreviewTabBar_PreviewMouseDown">
|
||||||
@@ -1518,9 +1519,9 @@
|
|||||||
<DataGrid x:Name="PreviewDataGrid" Visibility="Collapsed"
|
<DataGrid x:Name="PreviewDataGrid" Visibility="Collapsed"
|
||||||
AutoGenerateColumns="True" IsReadOnly="True"
|
AutoGenerateColumns="True" IsReadOnly="True"
|
||||||
HeadersVisibility="Column" GridLinesVisibility="All"
|
HeadersVisibility="Column" GridLinesVisibility="All"
|
||||||
Background="Transparent" Foreground="White"
|
Background="Transparent" Foreground="{DynamicResource PrimaryText}"
|
||||||
BorderThickness="0" CanUserAddRows="False"
|
BorderThickness="0" CanUserAddRows="False"
|
||||||
RowBackground="#0AFFFFFF" AlternatingRowBackground="#15FFFFFF"
|
RowBackground="{DynamicResource HintBackground}" AlternatingRowBackground="{DynamicResource ItemHoverBackground}"
|
||||||
ColumnHeaderHeight="30" RowHeight="28" FontSize="11.5"/>
|
ColumnHeaderHeight="30" RowHeight="28" FontSize="11.5"/>
|
||||||
<TextBlock x:Name="PreviewEmpty" Text="미리보기할 파일이 없습니다"
|
<TextBlock x:Name="PreviewEmpty" Text="미리보기할 파일이 없습니다"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
|||||||
@@ -1085,47 +1085,120 @@ public partial class ChatWindow : Window
|
|||||||
SkillService.ActivateConditionalSkillsForPaths(_attachedFiles, cwd);
|
SkillService.ActivateConditionalSkillsForPaths(_attachedFiles, cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>테마에 맞는 ContextMenu를 생성합니다.</summary>
|
private Popup? _sharedContextPopup;
|
||||||
private ContextMenu CreateThemedContextMenu()
|
|
||||||
|
private (Popup Popup, StackPanel Panel) CreateThemedPopupMenu(
|
||||||
|
UIElement? placementTarget = null,
|
||||||
|
PlacementMode placement = PlacementMode.MousePoint,
|
||||||
|
double minWidth = 200)
|
||||||
{
|
{
|
||||||
var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E));
|
_sharedContextPopup?.SetCurrentValue(Popup.IsOpenProperty, false);
|
||||||
|
|
||||||
|
var bg = TryFindResource("LauncherBackground") as Brush ?? Brushes.White;
|
||||||
var border = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
var border = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||||
return new ContextMenu
|
var panel = new StackPanel { Margin = new Thickness(2) };
|
||||||
|
var container = new Border
|
||||||
{
|
{
|
||||||
Background = bg,
|
Background = bg,
|
||||||
BorderBrush = border,
|
BorderBrush = border,
|
||||||
BorderThickness = new Thickness(1),
|
BorderThickness = new Thickness(1),
|
||||||
Padding = new Thickness(4),
|
CornerRadius = new CornerRadius(10),
|
||||||
|
Padding = new Thickness(6),
|
||||||
|
MinWidth = minWidth,
|
||||||
|
Child = panel,
|
||||||
|
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||||||
|
{
|
||||||
|
BlurRadius = 16,
|
||||||
|
ShadowDepth = 3,
|
||||||
|
Opacity = 0.18,
|
||||||
|
Color = Colors.Black,
|
||||||
|
Direction = 270,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var popup = new Popup
|
||||||
|
{
|
||||||
|
Child = container,
|
||||||
|
StaysOpen = false,
|
||||||
|
AllowsTransparency = true,
|
||||||
|
PopupAnimation = PopupAnimation.Fade,
|
||||||
|
Placement = placement,
|
||||||
|
PlacementTarget = placementTarget,
|
||||||
|
};
|
||||||
|
|
||||||
|
_sharedContextPopup = popup;
|
||||||
|
return (popup, panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Border CreatePopupMenuItem(
|
||||||
|
Popup popup,
|
||||||
|
string icon,
|
||||||
|
string label,
|
||||||
|
Brush iconBrush,
|
||||||
|
Brush labelBrush,
|
||||||
|
Brush hoverBrush,
|
||||||
|
Action action)
|
||||||
|
{
|
||||||
|
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||||
|
sp.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = icon,
|
||||||
|
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||||
|
FontSize = 12.5,
|
||||||
|
Foreground = iconBrush,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = new Thickness(0, 0, 9, 0),
|
||||||
|
});
|
||||||
|
sp.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = label,
|
||||||
|
FontSize = 12.5,
|
||||||
|
Foreground = labelBrush,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
});
|
||||||
|
|
||||||
|
var item = new Border
|
||||||
|
{
|
||||||
|
Child = sp,
|
||||||
|
Background = Brushes.Transparent,
|
||||||
|
CornerRadius = new CornerRadius(8),
|
||||||
|
Cursor = Cursors.Hand,
|
||||||
|
Padding = new Thickness(10, 7, 12, 7),
|
||||||
|
Margin = new Thickness(0, 1, 0, 1),
|
||||||
|
};
|
||||||
|
item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBrush; };
|
||||||
|
item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||||||
|
item.MouseLeftButtonUp += (_, _) =>
|
||||||
|
{
|
||||||
|
popup.SetCurrentValue(Popup.IsOpenProperty, false);
|
||||||
|
action();
|
||||||
|
};
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddPopupMenuSeparator(Panel panel, Brush brush)
|
||||||
|
{
|
||||||
|
panel.Children.Add(new Border
|
||||||
|
{
|
||||||
|
Height = 1,
|
||||||
|
Margin = new Thickness(10, 4, 10, 4),
|
||||||
|
Background = brush,
|
||||||
|
Opacity = 0.35,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>최근 폴더 항목 우클릭 컨텍스트 메뉴를 표시합니다.</summary>
|
/// <summary>최근 폴더 항목 우클릭 컨텍스트 메뉴를 표시합니다.</summary>
|
||||||
private void ShowRecentFolderContextMenu(string folderPath)
|
private void ShowRecentFolderContextMenu(string folderPath)
|
||||||
{
|
{
|
||||||
var menu = CreateThemedContextMenu();
|
|
||||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||||
|
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||||
|
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
|
||||||
|
var warningBrush = new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44));
|
||||||
|
var (popup, panel) = CreateThemedPopupMenu();
|
||||||
|
|
||||||
void AddItem(string icon, string label, Action action)
|
panel.Children.Add(CreatePopupMenuItem(popup, "\uED25", "폴더 열기", secondaryText, primaryText, hoverBg, () =>
|
||||||
{
|
|
||||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
|
||||||
sp.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
||||||
FontSize = 12, Foreground = secondaryText,
|
|
||||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0),
|
|
||||||
});
|
|
||||||
sp.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = label, FontSize = 12, Foreground = primaryText,
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
});
|
|
||||||
var mi = new MenuItem { Header = sp, Padding = new Thickness(8, 6, 16, 6) };
|
|
||||||
mi.Click += (_, _) => action();
|
|
||||||
menu.Items.Add(mi);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddItem("\uED25", "폴더 열기", () =>
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -1136,16 +1209,16 @@ public partial class ChatWindow : Window
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
});
|
}));
|
||||||
|
|
||||||
AddItem("\uE8C8", "경로 복사", () =>
|
panel.Children.Add(CreatePopupMenuItem(popup, "\uE8C8", "경로 복사", secondaryText, primaryText, hoverBg, () =>
|
||||||
{
|
{
|
||||||
try { Clipboard.SetText(folderPath); } catch { }
|
try { Clipboard.SetText(folderPath); } catch { }
|
||||||
});
|
}));
|
||||||
|
|
||||||
menu.Items.Add(new Separator());
|
AddPopupMenuSeparator(panel, borderBrush);
|
||||||
|
|
||||||
AddItem("\uE74D", "목록에서 삭제", () =>
|
panel.Children.Add(CreatePopupMenuItem(popup, "\uE74D", "목록에서 삭제", warningBrush, warningBrush, hoverBg, () =>
|
||||||
{
|
{
|
||||||
_settings.Settings.Llm.RecentWorkFolders.RemoveAll(
|
_settings.Settings.Llm.RecentWorkFolders.RemoveAll(
|
||||||
p => p.Equals(folderPath, StringComparison.OrdinalIgnoreCase));
|
p => p.Equals(folderPath, StringComparison.OrdinalIgnoreCase));
|
||||||
@@ -1153,9 +1226,9 @@ public partial class ChatWindow : Window
|
|||||||
// 메뉴 새로고침
|
// 메뉴 새로고침
|
||||||
if (FolderMenuPopup.IsOpen)
|
if (FolderMenuPopup.IsOpen)
|
||||||
ShowFolderMenu();
|
ShowFolderMenu();
|
||||||
});
|
}));
|
||||||
|
|
||||||
menu.IsOpen = true;
|
Dispatcher.BeginInvoke(() => { popup.IsOpen = true; }, DispatcherPriority.Input);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BtnFolderClear_Click(object sender, RoutedEventArgs e)
|
private void BtnFolderClear_Click(object sender, RoutedEventArgs e)
|
||||||
@@ -8582,43 +8655,27 @@ public partial class ChatWindow : Window
|
|||||||
|
|
||||||
private void ShowMessageContextMenu(string content, string role)
|
private void ShowMessageContextMenu(string content, string role)
|
||||||
{
|
{
|
||||||
var menu = CreateThemedContextMenu();
|
|
||||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||||
|
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||||
void AddItem(string icon, string label, Action action)
|
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
|
||||||
{
|
var dangerBrush = new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44));
|
||||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
var (popup, panel) = CreateThemedPopupMenu();
|
||||||
sp.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
||||||
FontSize = 12, Foreground = secondaryText,
|
|
||||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0),
|
|
||||||
});
|
|
||||||
sp.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = label, FontSize = 12, Foreground = primaryText,
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
});
|
|
||||||
var mi = new MenuItem { Header = sp, Padding = new Thickness(8, 6, 16, 6) };
|
|
||||||
mi.Click += (_, _) => action();
|
|
||||||
menu.Items.Add(mi);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 복사
|
// 복사
|
||||||
AddItem("\uE8C8", "텍스트 복사", () =>
|
panel.Children.Add(CreatePopupMenuItem(popup, "\uE8C8", "텍스트 복사", secondaryText, primaryText, hoverBg, () =>
|
||||||
{
|
{
|
||||||
try { Clipboard.SetText(content); ShowToast("복사되었습니다"); } catch { }
|
try { Clipboard.SetText(content); ShowToast("복사되었습니다"); } catch { }
|
||||||
});
|
}));
|
||||||
|
|
||||||
// 마크다운 복사
|
// 마크다운 복사
|
||||||
AddItem("\uE943", "마크다운 복사", () =>
|
panel.Children.Add(CreatePopupMenuItem(popup, "\uE943", "마크다운 복사", secondaryText, primaryText, hoverBg, () =>
|
||||||
{
|
{
|
||||||
try { Clipboard.SetText(content); ShowToast("마크다운으로 복사됨"); } catch { }
|
try { Clipboard.SetText(content); ShowToast("마크다운으로 복사됨"); } catch { }
|
||||||
});
|
}));
|
||||||
|
|
||||||
// 인용하여 답장
|
// 인용하여 답장
|
||||||
AddItem("\uE97A", "인용하여 답장", () =>
|
panel.Children.Add(CreatePopupMenuItem(popup, "\uE97A", "인용하여 답장", secondaryText, primaryText, hoverBg, () =>
|
||||||
{
|
{
|
||||||
var quote = content.Length > 200 ? content[..200] + "..." : content;
|
var quote = content.Length > 200 ? content[..200] + "..." : content;
|
||||||
var lines = quote.Split('\n');
|
var lines = quote.Split('\n');
|
||||||
@@ -8626,18 +8683,18 @@ public partial class ChatWindow : Window
|
|||||||
InputBox.Text = quoted + "\n\n";
|
InputBox.Text = quoted + "\n\n";
|
||||||
InputBox.Focus();
|
InputBox.Focus();
|
||||||
InputBox.CaretIndex = InputBox.Text.Length;
|
InputBox.CaretIndex = InputBox.Text.Length;
|
||||||
});
|
}));
|
||||||
|
|
||||||
menu.Items.Add(new Separator());
|
AddPopupMenuSeparator(panel, borderBrush);
|
||||||
|
|
||||||
// 재생성 (AI 응답만)
|
// 재생성 (AI 응답만)
|
||||||
if (role == "assistant")
|
if (role == "assistant")
|
||||||
{
|
{
|
||||||
AddItem("\uE72C", "응답 재생성", () => _ = RegenerateLastAsync());
|
panel.Children.Add(CreatePopupMenuItem(popup, "\uE72C", "응답 재생성", secondaryText, primaryText, hoverBg, () => _ = RegenerateLastAsync()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 대화 분기 (Fork)
|
// 대화 분기 (Fork)
|
||||||
AddItem("\uE8A5", "여기서 분기", () =>
|
panel.Children.Add(CreatePopupMenuItem(popup, "\uE8A5", "여기서 분기", secondaryText, primaryText, hoverBg, () =>
|
||||||
{
|
{
|
||||||
ChatConversation? conv;
|
ChatConversation? conv;
|
||||||
lock (_convLock) conv = _currentConversation;
|
lock (_convLock) conv = _currentConversation;
|
||||||
@@ -8647,14 +8704,14 @@ public partial class ChatWindow : Window
|
|||||||
if (idx < 0) return;
|
if (idx < 0) return;
|
||||||
|
|
||||||
ForkConversation(conv, idx);
|
ForkConversation(conv, idx);
|
||||||
});
|
}));
|
||||||
|
|
||||||
menu.Items.Add(new Separator());
|
AddPopupMenuSeparator(panel, borderBrush);
|
||||||
|
|
||||||
// 이후 메시지 모두 삭제
|
// 이후 메시지 모두 삭제
|
||||||
var msgContent = content;
|
var msgContent = content;
|
||||||
var msgRole = role;
|
var msgRole = role;
|
||||||
AddItem("\uE74D", "이후 메시지 모두 삭제", () =>
|
panel.Children.Add(CreatePopupMenuItem(popup, "\uE74D", "이후 메시지 모두 삭제", dangerBrush, dangerBrush, hoverBg, () =>
|
||||||
{
|
{
|
||||||
ChatConversation? conv;
|
ChatConversation? conv;
|
||||||
lock (_convLock) conv = _currentConversation;
|
lock (_convLock) conv = _currentConversation;
|
||||||
@@ -8664,7 +8721,7 @@ public partial class ChatWindow : Window
|
|||||||
if (idx < 0) return;
|
if (idx < 0) return;
|
||||||
|
|
||||||
var removeCount = conv.Messages.Count - idx;
|
var removeCount = conv.Messages.Count - idx;
|
||||||
if (MessageBox.Show($"이 메시지 포함 {removeCount}개 메시지를 삭제하시겠습니까?",
|
if (CustomMessageBox.Show($"이 메시지 포함 {removeCount}개 메시지를 삭제하시겠습니까?",
|
||||||
"메시지 삭제", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes)
|
"메시지 삭제", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -8672,9 +8729,9 @@ public partial class ChatWindow : Window
|
|||||||
try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"대화 저장 실패: {ex.Message}"); }
|
try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"대화 저장 실패: {ex.Message}"); }
|
||||||
RenderMessages();
|
RenderMessages();
|
||||||
ShowToast($"{removeCount}개 메시지 삭제됨");
|
ShowToast($"{removeCount}개 메시지 삭제됨");
|
||||||
});
|
}));
|
||||||
|
|
||||||
menu.IsOpen = true;
|
Dispatcher.BeginInvoke(() => { popup.IsOpen = true; }, DispatcherPriority.Input);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 팁 알림 ──────────────────────────────────────────────────────
|
// ─── 팁 알림 ──────────────────────────────────────────────────────
|
||||||
@@ -11667,7 +11724,7 @@ public partial class ChatWindow : Window
|
|||||||
// 삭제
|
// 삭제
|
||||||
AddItem("\uE74D", "삭제", () =>
|
AddItem("\uE74D", "삭제", () =>
|
||||||
{
|
{
|
||||||
var result = MessageBox.Show(
|
var result = CustomMessageBox.Show(
|
||||||
$"파일을 삭제하시겠습니까?\n{System.IO.Path.GetFileName(filePath)}",
|
$"파일을 삭제하시겠습니까?\n{System.IO.Path.GetFileName(filePath)}",
|
||||||
"파일 삭제 확인", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
"파일 삭제 확인", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||||
if (result == MessageBoxResult.Yes)
|
if (result == MessageBoxResult.Yes)
|
||||||
|
|||||||
Reference in New Issue
Block a user