");
+ 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.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 (Exception) { /* 클립보드 접근 실패 */ }
+ }
+ 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 (MessageSearchBar.Visibility == Visibility.Visible) { CloseMessageSearch(); e.Handled = true; }
+ else if (_isStreaming) { StopGeneration(); e.Handled = true; }
+ }
+
+ // 슬래시 명령 팝업 키 처리
+ if (SlashPopup.IsOpen)
+ {
+ if (e.Key == Key.Escape)
+ {
+ SlashPopup.IsOpen = false;
+ _slashSelectedIndex = -1;
+ e.Handled = true;
+ }
+ else if (e.Key == Key.Up)
+ {
+ SlashPopup_ScrollByDelta(120); // 위로 1칸
+ e.Handled = true;
+ }
+ else if (e.Key == Key.Down)
+ {
+ SlashPopup_ScrollByDelta(-120); // 아래로 1칸
+ e.Handled = true;
+ }
+ else if (e.Key == Key.Enter && _slashSelectedIndex >= 0)
+ {
+ e.Handled = true;
+ ExecuteSlashSelectedItem();
+ }
+ }
+ }
+
+ 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