");
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