AX Agent 코워크·코드 흐름과 컨텍스트 관리를 claude-code 기준으로 대폭 정리

- 코워크·코드 프롬프트, 도구 선택, 문서 생성/검증 흐름을 claude-code 동등 품질 기준으로 재정렬함

- OpenAI/vLLM 경로의 오래된 tool history를 평탄화하고 최근 이력만 구조화해 컨텍스트 직렬화를 경량화함

- AX Agent UI를 테마 기준으로 재구성하고 플랜 승인/오버레이/이벤트 렌더링/명령 입력 상호작용을 개선함

- 파일 후보 제안, 반복 경로 정체 복구, LSP 보강, 문서·PPT 처리 개선, 설정/서비스 인터페이스 정리를 함께 반영함

- README.md 및 docs/DEVELOPMENT.md를 작업 시점별로 갱신함

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0, 오류 0)
This commit is contained in:
2026-04-12 22:02:14 +09:00
parent b8f4df1892
commit fb0bea41f7
137 changed files with 18532 additions and 1144 deletions

View File

@@ -0,0 +1,667 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using AxCopilot.Models;
using AxCopilot.Services;
using AxCopilot.Services.Agent;
namespace AxCopilot.Views;
public partial class ChatWindow
{
private static string BuildSlashSkillLabel(SkillDefinition skill)
{
var badge = string.Equals(skill.ExecutionContext, "fork", StringComparison.OrdinalIgnoreCase)
? "[FORK]"
: "[DIRECT]";
var baseLabel = $"{badge} {skill.Label}";
return skill.IsAvailable ? baseLabel : $"{baseLabel} {skill.UnavailableHint}";
}
private bool GetSlashSectionExpanded(string sectionKey, bool defaultValue = true)
{
var map = _settings.Settings.Llm.SlashPaletteSections;
if (map != null && map.TryGetValue(sectionKey, out var expanded))
return expanded;
return defaultValue;
}
private void SetSlashSectionExpanded(string sectionKey, bool expanded)
{
var map = _settings.Settings.Llm.SlashPaletteSections ??= new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
map[sectionKey] = expanded;
try { _settings.Save(); } catch { }
}
private bool AreAllSlashSectionsExpanded()
{
var commandsExpanded = GetSlashSectionExpanded("slash_commands", true);
var skillsExpanded = GetSlashSectionExpanded("slash_skills", true);
return commandsExpanded && skillsExpanded;
}
private void BtnSlashToggleGroups_Click(object sender, RoutedEventArgs e)
{
var expandAll = !AreAllSlashSectionsExpanded();
SetSlashSectionExpanded("slash_commands", expandAll);
SetSlashSectionExpanded("slash_skills", expandAll);
_slashPalette.SelectedIndex = GetFirstVisibleSlashIndex(_slashPalette.Matches);
RenderSlashPage();
}
private void BtnSlashReset_Click(object sender, RoutedEventArgs e)
{
_settings.Settings.Llm.FavoriteSlashCommands.Clear();
_settings.Settings.Llm.RecentSlashCommands.Clear();
try { _settings.Save(); } catch { }
_slashPalette.SelectedIndex = GetFirstVisibleSlashIndex(_slashPalette.Matches);
RenderSlashPage();
}
private Dictionary<string, int> BuildRecentSlashRankMap()
{
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var recent = _settings.Settings.Llm.RecentSlashCommands;
for (var i = 0; i < recent.Count; i++)
{
var key = recent[i]?.Trim();
if (string.IsNullOrWhiteSpace(key) || map.ContainsKey(key))
continue;
map[key] = i; // index 낮을수록 최근
}
return map;
}
private Dictionary<string, int> BuildFavoriteSlashRankMap()
{
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var fav = _settings.Settings.Llm.FavoriteSlashCommands;
var maxFavorites = Math.Clamp(_settings.Settings.Llm.MaxFavoriteSlashCommands, 1, 30);
for (var i = 0; i < fav.Count; i++)
{
if (i >= maxFavorites)
break;
var key = fav[i]?.Trim();
if (string.IsNullOrWhiteSpace(key) || map.ContainsKey(key))
continue;
map[key] = i; // index 낮을수록 우선
}
return map;
}
private void RegisterRecentSlashCommand(string cmd)
{
if (string.IsNullOrWhiteSpace(cmd))
return;
var recent = _settings.Settings.Llm.RecentSlashCommands;
var maxRecent = Math.Clamp(_settings.Settings.Llm.MaxRecentSlashCommands, 5, 50);
recent.RemoveAll(x => string.Equals(x, cmd, StringComparison.OrdinalIgnoreCase));
recent.Insert(0, cmd);
if (recent.Count > maxRecent)
recent.RemoveRange(maxRecent, recent.Count - maxRecent);
try { _settings.Save(); } catch { }
}
private int GetFirstVisibleSlashIndex(IReadOnlyList<(string Cmd, string Label, bool IsSkill)> matches)
{
var commandExpanded = GetSlashSectionExpanded("slash_commands", true);
var skillExpanded = GetSlashSectionExpanded("slash_skills", true);
for (var i = 0; i < matches.Count; i++)
{
var visible = matches[i].IsSkill ? skillExpanded : commandExpanded;
if (visible)
return i;
}
return -1;
}
private bool IsSlashItemVisibleByIndex(int index)
{
if (index < 0 || index >= _slashPalette.Matches.Count)
return false;
var item = _slashPalette.Matches[index];
return item.IsSkill
? GetSlashSectionExpanded("slash_skills", true)
: GetSlashSectionExpanded("slash_commands", true);
}
private IReadOnlyList<int> GetVisibleSlashOrderedIndices() => _slashVisibleAbsoluteOrder;
/// <summary>현재 슬래시 명령어 항목을 스크롤 리스트로 렌더링합니다.</summary>
private void RenderSlashPage()
{
SlashItems.Items.Clear();
_slashVisibleItemByAbsoluteIndex.Clear();
_slashVisibleAbsoluteOrder.Clear();
var total = _slashPalette.Matches.Count;
var totalSkills = _slashPalette.Matches.Count(x => x.IsSkill);
var totalCommands = total - totalSkills;
var favoriteRank = BuildFavoriteSlashRankMap();
var recentRank = BuildRecentSlashRankMap();
var expressionLevel = (_settings.Settings.Llm.AgentUiExpressionLevel ?? "balanced").Trim().ToLowerInvariant();
SlashPopupTitle.Text = "명령 및 스킬";
SlashPopupHint.Text = expressionLevel switch
{
"simple" => $"명령 {totalCommands} · 스킬 {totalSkills}",
"rich" => $"명령 {totalCommands}개 · 스킬 {totalSkills}개 · Enter 실행 · 방향키 이동",
_ => $"명령 {totalCommands}개 · 스킬 {totalSkills}개 · Enter 실행",
};
var commandsExpanded = GetSlashSectionExpanded("slash_commands", true);
var skillsExpanded = GetSlashSectionExpanded("slash_skills", true);
if (SlashToggleGroupsLabel != null)
SlashToggleGroupsLabel.Text = (commandsExpanded && skillsExpanded) ? "전체 접기" : "전체 펼치기";
Border CreateSlashSectionHeader(string key, string title, int count, bool expanded)
{
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var hoverBrushItem = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.LightGray;
var header = new Border
{
Background = Brushes.Transparent,
BorderBrush = Brushes.Transparent,
BorderThickness = new Thickness(0),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(8, 6, 8, 6),
Margin = new Thickness(0, 4, 0, 2),
Cursor = Cursors.Hand,
};
var grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
grid.Children.Add(new TextBlock
{
Text = expanded ? "\uE70D" : "\uE76C",
FontFamily = s_segoeIconFont,
FontSize = 11,
Foreground = secondaryText,
Margin = new Thickness(0, 0, 6, 0),
VerticalAlignment = VerticalAlignment.Center,
});
var titleText = new TextBlock
{
Text = $"{title} {count}",
FontSize = 10.5,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
VerticalAlignment = VerticalAlignment.Center,
};
Grid.SetColumn(titleText, 1);
grid.Children.Add(titleText);
var metaText = new TextBlock
{
Text = expanded ? "접기" : "펼치기",
FontSize = 9.5,
Foreground = secondaryText,
VerticalAlignment = VerticalAlignment.Center,
};
Grid.SetColumn(metaText, 2);
grid.Children.Add(metaText);
header.Child = grid;
header.MouseEnter += (_, _) => header.Background = hoverBrushItem;
header.MouseLeave += (_, _) => header.Background = Brushes.Transparent;
header.MouseLeftButtonDown += (_, _) =>
{
SetSlashSectionExpanded(key, !expanded);
_slashPalette.SelectedIndex = GetFirstVisibleSlashIndex(_slashPalette.Matches);
RenderSlashPage();
};
return header;
}
void AddSlashItem(int i)
{
var (cmd, label, isSkill) = _slashPalette.Matches[i];
var isFavorite = favoriteRank.ContainsKey(cmd);
var isRecent = recentRank.ContainsKey(cmd);
var capturedCmd = cmd;
var skillDef = isSkill ? SkillService.Find(cmd.TrimStart('/')) : null;
var skillAvailable = skillDef?.IsAvailable ?? true;
var absoluteIndex = i;
var hoverBrushItem = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.LightGray;
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
var item = new Border
{
Background = Brushes.Transparent,
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray,
BorderThickness = new Thickness(0, 0, 0, 1),
CornerRadius = new CornerRadius(0),
Padding = new Thickness(8, 9, 8, 9),
Margin = new Thickness(0, 0, 0, 0),
Cursor = skillAvailable ? Cursors.Hand : Cursors.Arrow,
Opacity = skillAvailable ? 1.0 : 0.5,
};
var itemGrid = new Grid();
itemGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
itemGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var leftStack = new StackPanel();
var titleRow = new StackPanel { Orientation = Orientation.Horizontal };
titleRow.Children.Add(new TextBlock
{
Text = isSkill ? "\uE768" : "\uE9CE",
FontFamily = s_segoeIconFont,
FontSize = 11,
Foreground = skillAvailable ? accent : secondaryText,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
});
titleRow.Children.Add(new TextBlock
{
Text = cmd,
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = skillAvailable ? primaryText : secondaryText,
VerticalAlignment = VerticalAlignment.Center,
});
if (isFavorite)
{
titleRow.Children.Add(new Border
{
Background = BrushFromHex("#FEF3C7"),
BorderBrush = BrushFromHex("#F59E0B"),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(5, 0, 5, 0),
Margin = new Thickness(6, 0, 0, 0),
Child = new TextBlock
{
Text = "핀",
FontSize = 9.5,
Foreground = BrushFromHex("#92400E"),
}
});
}
if (isRecent)
{
titleRow.Children.Add(new Border
{
Background = BrushFromHex("#EEF2FF"),
BorderBrush = BrushFromHex("#C7D2FE"),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(5, 0, 5, 0),
Margin = new Thickness(6, 0, 0, 0),
Child = new TextBlock
{
Text = "최근",
FontSize = 9.5,
Foreground = BrushFromHex("#3730A3"),
}
});
}
leftStack.Children.Add(titleRow);
leftStack.Children.Add(new TextBlock
{
Text = label,
FontSize = 11,
Foreground = secondaryText,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(20, 2, 0, 0),
TextTrimming = TextTrimming.CharacterEllipsis,
});
Grid.SetColumn(leftStack, 0);
itemGrid.Children.Add(leftStack);
var pinToggle = new Border
{
Background = Brushes.Transparent,
CornerRadius = new CornerRadius(6),
Padding = new Thickness(6, 4, 6, 4),
Margin = new Thickness(6, 0, 0, 0),
Cursor = Cursors.Hand,
Child = new TextBlock
{
Text = isFavorite ? "\uE77A" : "\uE718",
FontFamily = s_segoeIconFont,
FontSize = 11,
Foreground = isFavorite ? BrushFromHex("#B45309") : secondaryText,
VerticalAlignment = VerticalAlignment.Center,
},
ToolTip = isFavorite ? "핀 해제" : "핀 고정",
};
pinToggle.MouseEnter += (_, _) => pinToggle.Background = hoverBrushItem;
pinToggle.MouseLeave += (_, _) => pinToggle.Background = Brushes.Transparent;
pinToggle.MouseLeftButtonDown += (s, e) =>
{
e.Handled = true;
ToggleSlashFavorite(capturedCmd);
};
Grid.SetColumn(pinToggle, 1);
itemGrid.Children.Add(pinToggle);
item.Child = itemGrid;
if (skillAvailable)
{
item.MouseEnter += (_, _) =>
{
_slashPalette.SelectedIndex = absoluteIndex;
UpdateSlashSelectionVisualState();
};
item.MouseLeave += (_, _) => UpdateSlashSelectionVisualState();
item.MouseLeftButtonDown += (_, _) =>
{
_slashPalette.SelectedIndex = absoluteIndex;
ExecuteSlashSelectedItem();
};
}
SlashItems.Items.Add(item);
_slashVisibleItemByAbsoluteIndex[absoluteIndex] = item;
_slashVisibleAbsoluteOrder.Add(absoluteIndex);
}
SlashItems.Items.Add(CreateSlashSectionHeader("slash_commands", "명령", totalCommands, commandsExpanded));
if (commandsExpanded)
{
var commandIndices = Enumerable.Range(0, total)
.Where(i => !_slashPalette.Matches[i].IsSkill)
.OrderBy(i => favoriteRank.TryGetValue(_slashPalette.Matches[i].Cmd, out var favRank) ? favRank : int.MaxValue)
.ThenBy(i => recentRank.TryGetValue(_slashPalette.Matches[i].Cmd, out var rank) ? rank : int.MaxValue)
.ThenBy(i => _slashPalette.Matches[i].Cmd, StringComparer.OrdinalIgnoreCase);
foreach (var i in commandIndices)
{
AddSlashItem(i);
}
}
SlashItems.Items.Add(CreateSlashSectionHeader("slash_skills", "스킬", totalSkills, skillsExpanded));
if (skillsExpanded)
{
var skillIndices = Enumerable.Range(0, total)
.Where(i => _slashPalette.Matches[i].IsSkill)
.OrderBy(i => favoriteRank.TryGetValue(_slashPalette.Matches[i].Cmd, out var favRank) ? favRank : int.MaxValue)
.ThenBy(i => recentRank.TryGetValue(_slashPalette.Matches[i].Cmd, out var rank) ? rank : int.MaxValue)
.ThenBy(i => _slashPalette.Matches[i].Cmd, StringComparer.OrdinalIgnoreCase);
foreach (var i in skillIndices)
{
AddSlashItem(i);
}
}
var visibleCommandCount = commandsExpanded ? totalCommands : 0;
var visibleSkillCount = skillsExpanded ? totalSkills : 0;
if (visibleCommandCount + visibleSkillCount == 0)
{
SlashPopupFooter.Text = "모든 그룹이 접혀 있습니다 · 우측 상단에서 전체 펼치기";
}
else
{
SlashPopupFooter.Text = $"Enter 실행 · ↑↓/PgUp/PgDn 이동 · Home/End · Esc 닫기 · 표시 {visibleCommandCount + visibleSkillCount}/{total}";
}
UpdateSlashSelectionVisualState();
EnsureSlashSelectionVisible();
}
/// <summary>슬래시 팝업 마우스 휠 스크롤 처리.</summary>
private void SlashPopup_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
e.Handled = true;
SlashPopup_ScrollByDelta(e.Delta);
}
private void MoveSlashSelection(int direction)
{
var visibleOrder = GetVisibleSlashOrderedIndices();
if (visibleOrder.Count == 0)
return;
var currentPosition = -1;
for (var i = 0; i < visibleOrder.Count; i++)
{
if (visibleOrder[i] != _slashPalette.SelectedIndex)
continue;
currentPosition = i;
break;
}
if (currentPosition < 0)
{
_slashPalette.SelectedIndex = visibleOrder[0];
return;
}
if (direction < 0 && currentPosition > 0)
_slashPalette.SelectedIndex = visibleOrder[currentPosition - 1];
else if (direction > 0 && currentPosition < visibleOrder.Count - 1)
_slashPalette.SelectedIndex = visibleOrder[currentPosition + 1];
}
private int? FindSlashIndexClosestToViewportTop()
{
if (SlashScrollViewer == null || _slashVisibleAbsoluteOrder.Count == 0)
return null;
var bestIndex = -1;
var bestDistance = double.MaxValue;
foreach (var absoluteIndex in _slashVisibleAbsoluteOrder)
{
if (!_slashVisibleItemByAbsoluteIndex.TryGetValue(absoluteIndex, out var item))
continue;
try
{
var bounds = item.TransformToAncestor(SlashScrollViewer)
.TransformBounds(new Rect(0, 0, item.ActualWidth, item.ActualHeight));
// 뷰포트 상단에 가장 가까운 가시 항목을 선택 기준으로 사용.
var distance = Math.Abs(bounds.Top);
if (distance < bestDistance && bounds.Bottom >= 0)
{
bestDistance = distance;
bestIndex = absoluteIndex;
}
}
catch
{
// 레이아웃 갱신 중 transform 예외는 무시.
}
}
return bestIndex >= 0 ? bestIndex : null;
}
/// <summary>슬래시 팝업을 Delta 방향으로 스크롤합니다.</summary>
private void SlashPopup_ScrollByDelta(int delta)
{
if (_slashPalette.Matches.Count == 0)
return;
if (GetVisibleSlashOrderedIndices().Count == 0)
{
if (SlashScrollViewer != null)
SlashScrollViewer.ScrollToVerticalOffset(Math.Max(0, SlashScrollViewer.VerticalOffset - delta / 3.0));
return;
}
// 터치패드/마우스 환경 모두에서 체감이 유사하도록 스크롤뷰도 함께 이동.
if (SlashScrollViewer != null)
{
var target = Math.Max(0, Math.Min(
SlashScrollViewer.ScrollableHeight,
SlashScrollViewer.VerticalOffset - (delta / 3.0)));
SlashScrollViewer.ScrollToVerticalOffset(target);
}
var steps = Math.Max(1, (int)Math.Ceiling(Math.Abs(delta) / 120.0));
var direction = delta > 0 ? -1 : 1;
for (var i = 0; i < steps; i++)
MoveSlashSelection(direction);
var viewportTopIndex = FindSlashIndexClosestToViewportTop();
if (viewportTopIndex.HasValue)
_slashPalette.SelectedIndex = viewportTopIndex.Value;
UpdateSlashSelectionVisualState();
EnsureSlashSelectionVisible();
}
/// <summary>키보드로 선택된 슬래시 아이템을 실행합니다.</summary>
private void ExecuteSlashSelectedItem()
{
var absoluteIdx = _slashPalette.SelectedIndex;
if (absoluteIdx < 0 || absoluteIdx >= _slashPalette.Matches.Count) return;
var (cmd, _, isSkill) = _slashPalette.Matches[absoluteIdx];
var skillDef = isSkill ? SkillService.Find(cmd.TrimStart('/')) : null;
var skillAvailable = skillDef?.IsAvailable ?? true;
if (!skillAvailable) return;
RegisterRecentSlashCommand(cmd);
SlashPopup.IsOpen = false;
_slashPalette.SelectedIndex = -1;
if (cmd.Equals("/help", StringComparison.OrdinalIgnoreCase))
{
InputBox.Text = "";
ShowSlashHelpWindow();
return;
}
ShowSlashChip(cmd);
InputBox.Focus();
}
private void EnsureSlashSelectionVisible()
{
if (SlashScrollViewer == null || _slashPalette.SelectedIndex < 0)
return;
if (!_slashVisibleItemByAbsoluteIndex.TryGetValue(_slashPalette.SelectedIndex, out var item))
return;
if (!IsVisualDescendantOf(item, SlashScrollViewer))
return;
Rect bounds;
try
{
bounds = item.TransformToAncestor(SlashScrollViewer)
.TransformBounds(new Rect(0, 0, item.ActualWidth, item.ActualHeight));
}
catch
{
// 렌더 트리 갱신 중에는 transform이 실패할 수 있어 조용히 무시.
return;
}
if (bounds.Top < 0)
SlashScrollViewer.ScrollToVerticalOffset(SlashScrollViewer.VerticalOffset + bounds.Top - 8);
else if (bounds.Bottom > SlashScrollViewer.ViewportHeight)
SlashScrollViewer.ScrollToVerticalOffset(SlashScrollViewer.VerticalOffset + (bounds.Bottom - SlashScrollViewer.ViewportHeight) + 8);
}
private static bool IsVisualDescendantOf(DependencyObject? child, DependencyObject? parent)
{
if (child == null || parent == null)
return false;
var current = child;
while (current != null)
{
if (ReferenceEquals(current, parent))
return true;
current = VisualTreeHelper.GetParent(current);
}
return false;
}
private void UpdateSlashSelectionVisualState()
{
if (_slashVisibleItemByAbsoluteIndex.Count == 0)
return;
var selectedIndex = _slashPalette.SelectedIndex;
var hoverBrushItem = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.LightGray;
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
foreach (var (absoluteIndex, element) in _slashVisibleItemByAbsoluteIndex)
{
if (element is not Border border)
continue;
var selected = absoluteIndex == selectedIndex;
border.Background = selected ? hoverBrushItem : Brushes.Transparent;
border.BorderBrush = selected ? accent : borderBrush;
border.BorderThickness = selected ? new Thickness(2, 0, 0, 1) : new Thickness(0, 0, 0, 1);
}
}
/// <summary>슬래시 명령어 즐겨찾기를 토글하고 설정을 저장합니다.</summary>
private void ToggleSlashFavorite(string cmd)
{
var favs = _settings.Settings.Llm.FavoriteSlashCommands;
var maxFavorites = Math.Clamp(_settings.Settings.Llm.MaxFavoriteSlashCommands, 1, 30);
var existing = favs.FirstOrDefault(f => f.Equals(cmd, StringComparison.OrdinalIgnoreCase));
if (existing != null)
favs.Remove(existing);
else
{
favs.Add(cmd);
if (favs.Count > maxFavorites)
favs.RemoveRange(maxFavorites, favs.Count - maxFavorites);
}
_settings.Save();
if (SlashPopup.IsOpen)
{
RenderSlashPage();
return;
}
// 팝업이 닫힌 경우에만 TextChanged 트리거
var currentText = InputBox.Text;
InputBox.TextChanged -= InputBox_TextChanged;
InputBox.Text = "";
InputBox.TextChanged += InputBox_TextChanged;
InputBox.Text = currentText;
}
/// <summary>슬래시 명령어 칩을 표시하고 InputBox를 비웁니다.</summary>
private void ShowSlashChip(string cmd)
{
_slashPalette.ActiveCommand = cmd;
SlashChipText.Text = cmd;
SlashCommandChip.Visibility = Visibility.Visible;
// 칩 너비 측정 후 InputBox 왼쪽 여백 조정
SlashCommandChip.UpdateLayout();
var chipRight = SlashCommandChip.Margin.Left + SlashCommandChip.ActualWidth + 6;
InputBox.Padding = new Thickness(chipRight, 10, 14, 10);
InputBox.Text = "";
}
/// <summary>슬래시 명령어 칩을 숨깁니다.</summary>
/// <param name="restoreText">true이면 InputBox에 명령어 텍스트를 복원합니다.</param>
private void HideSlashChip(bool restoreText = false)
{
if (_slashPalette.ActiveCommand == null) return;
var prev = _slashPalette.ActiveCommand;
_slashPalette.ActiveCommand = null;
SlashCommandChip.Visibility = Visibility.Collapsed;
InputBox.Padding = new Thickness(14, 10, 14, 10);
if (restoreText)
{
InputBox.Text = prev + " ";
InputBox.CaretIndex = InputBox.Text.Length;
}
}
}