");
+ 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)
+ {
+ if (_agentLoop.IsPaused)
+ {
+ _agentLoop.Resume();
+ PauseIcon.Text = "\uE769"; // 일시정지 아이콘
+ BtnPause.ToolTip = "일시정지";
+ }
+ else
+ {
+ _ = _agentLoop.PauseAsync();
+ PauseIcon.Text = "\uE768"; // 재생 아이콘
+ BtnPause.ToolTip = "재개";
+ }
+ }
+ private void BtnExport_Click(object sender, RoutedEventArgs e) => ExportConversation();
+
+ // ─── 메시지 내 검색 (Ctrl+F) ─────────────────────────────────────────
+
+ private List