");
- sb.AppendLine($"
{label} · {msg.Timestamp:HH:mm}
");
- sb.AppendLine($"
{System.Net.WebUtility.HtmlEncode(msg.Content)}
");
- sb.AppendLine("
");
- }
-
- sb.AppendLine("");
- return sb.ToString();
- }
-
- // ─── 버튼 이벤트 ──────────────────────────────────────────────────────
-
- private void ChatWindow_KeyDown(object sender, KeyEventArgs e)
- {
- var mod = Keyboard.Modifiers;
-
- // Ctrl 단축키
- if (mod == ModifierKeys.Control)
- {
- switch (e.Key)
- {
- case Key.N: BtnNewChat_Click(this, new RoutedEventArgs()); e.Handled = true; break;
- case Key.W: Close(); e.Handled = true; break;
- case Key.E: ExportConversation(); e.Handled = true; break;
- case Key.L: InputBox.Text = ""; InputBox.Focus(); e.Handled = true; break;
- case Key.B: BtnToggleSidebar_Click(this, new RoutedEventArgs()); e.Handled = true; break;
- case Key.M: BtnModelSelector_Click(this, new RoutedEventArgs()); e.Handled = true; break;
- case Key.OemComma: BtnSettings_Click(this, new RoutedEventArgs()); e.Handled = true; break;
- case Key.F: ToggleMessageSearch(); e.Handled = true; break;
- case Key.K: OpenSidebarSearch(); e.Handled = true; break;
- case Key.D1: TabChat.IsChecked = true; e.Handled = true; break;
- case Key.D2: TabCowork.IsChecked = true; e.Handled = true; break;
- case Key.D3: if (TabCode.IsEnabled) TabCode.IsChecked = true; e.Handled = true; break;
- }
- }
-
- // Ctrl+Shift 단축키
- if (mod == (ModifierKeys.Control | ModifierKeys.Shift))
- {
- switch (e.Key)
- {
- case Key.C:
- // 마지막 AI 응답 복사
- ChatConversation? conv;
- lock (_convLock) conv = _currentConversation;
- if (conv != null)
- {
- var lastAi = conv.Messages.LastOrDefault(m => m.Role == "assistant");
- if (lastAi != null)
- try { Clipboard.SetText(lastAi.Content); } catch { }
- }
- e.Handled = true;
- break;
- case Key.R:
- // 마지막 응답 재생성
- _ = RegenerateLastAsync();
- e.Handled = true;
- break;
- case Key.D:
- // 모든 대화 삭제
- BtnDeleteAll_Click(this, new RoutedEventArgs());
- e.Handled = true;
- break;
- case Key.P:
- // 커맨드 팔레트
- OpenCommandPalette();
- e.Handled = true;
- break;
- }
- }
-
- // Escape: 검색 바 닫기 또는 스트리밍 중지
- if (e.Key == Key.Escape)
- {
- if (SidebarSearchEditor?.Visibility == Visibility.Visible) { CloseSidebarSearch(clearText: true); e.Handled = true; }
- else if (MessageSearchBar.Visibility == Visibility.Visible) { CloseMessageSearch(); e.Handled = true; }
- else if (_isStreaming) { StopGeneration(); e.Handled = true; }
- }
-
- // 슬래시 명령 팝업 키 처리
- if (TryHandleSlashNavigationKey(e))
- return;
-
- if (PermissionPopup.IsOpen && e.Key == Key.Escape)
- {
- PermissionPopup.IsOpen = false;
- e.Handled = true;
- }
- }
-
- private bool TryHandleSlashNavigationKey(KeyEventArgs e)
- {
- if (!SlashPopup.IsOpen)
- return false;
-
- switch (e.Key)
- {
- case Key.Escape:
- SlashPopup.IsOpen = false;
- _slashPalette.SelectedIndex = -1;
- e.Handled = true;
- return true;
- case Key.Up:
- SlashPopup_ScrollByDelta(120);
- e.Handled = true;
- return true;
- case Key.Down:
- SlashPopup_ScrollByDelta(-120);
- e.Handled = true;
- return true;
- case Key.PageUp:
- SlashPopup_ScrollByDelta(600);
- e.Handled = true;
- return true;
- case Key.PageDown:
- SlashPopup_ScrollByDelta(-600);
- e.Handled = true;
- return true;
- case Key.Home:
- {
- var visible = GetVisibleSlashOrderedIndices();
- _slashPalette.SelectedIndex = visible.Count > 0 ? visible[0] : GetFirstVisibleSlashIndex(_slashPalette.Matches);
- UpdateSlashSelectionVisualState();
- EnsureSlashSelectionVisible();
- e.Handled = true;
- return true;
- }
- case Key.End:
- {
- var visible = GetVisibleSlashOrderedIndices();
- _slashPalette.SelectedIndex = visible.Count > 0 ? visible[^1] : GetFirstVisibleSlashIndex(_slashPalette.Matches);
- UpdateSlashSelectionVisualState();
- EnsureSlashSelectionVisible();
- e.Handled = true;
- return true;
- }
- case Key.Tab when _slashPalette.SelectedIndex >= 0:
- case Key.Enter when _slashPalette.SelectedIndex >= 0:
- ExecuteSlashSelectedItem();
- e.Handled = true;
- return true;
- default:
- return false;
- }
- }
-
- private void BtnStop_Click(object sender, RoutedEventArgs e) => StopGeneration();
-
- private void BtnPause_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
- {
- var activeLoop = GetAgentLoop(_activeTab);
- if (activeLoop.IsPaused)
- {
- activeLoop.Resume();
- PauseIcon.Text = "\uE769"; // 일시정지 아이콘
- BtnPause.ToolTip = "일시정지";
- }
- else
- {
- _ = activeLoop.PauseAsync();
- PauseIcon.Text = "\uE768"; // 재생 아이콘
- BtnPause.ToolTip = "재개";
- }
- }
- private void BtnExport_Click(object sender, RoutedEventArgs e) => ExportConversation();
-
- // ─── 메시지 내 검색 (Ctrl+F) ─────────────────────────────────────────
-
- private List