Files
AX-Copilot-Codex/src/AxCopilot/Handlers/ChatHandler.cs
T
lacvet fb0bea41f7 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)
2026-04-12 22:02:14 +09:00

169 lines
6.6 KiB
C#

using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Views;
namespace AxCopilot.Handlers;
/// <summary>
/// "!" 프리픽스 핸들러. AX Agent (AI 어시스턴트) 기능.
/// ★ DEPLOY_STUB = true → 배포용 "개발 중" 표시
/// ★ DEPLOY_STUB = false → 실제 ChatWindow 동작
/// </summary>
public class ChatHandler : IActionHandler
{
// ┌──────────────────────────────────────────────────────────────┐
// │ 배포 시 true, 개발 활성화 시 false 로 전환 │
// └──────────────────────────────────────────────────────────────┘
private const bool DEPLOY_STUB = false;
private readonly SettingsService _settings;
private readonly object _windowLock = new();
private ChatWindow? _chatWindow;
public string? Prefix => "!";
public PluginMetadata Metadata => new("ax.agent", "AX Agent", "1.0", "AX Agent — AI 어시스턴트");
public ChatHandler(SettingsService settings)
{
_settings = settings;
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
// ── 배포용 스텁 ─────────────────────────────────────────────
#pragma warning disable CS0162 // DEPLOY_STUB 플래그에 의한 의도된 비활성 코드
if (DEPLOY_STUB)
{
var stub = new List<LauncherItem>
{
new LauncherItem(
"AX Agent (개발 중)",
"이 기능은 다음 버전에서 제공될 예정입니다. 기대해 주세요!",
null, null, Symbol: "\uE8BD")
};
return Task.FromResult<IEnumerable<LauncherItem>>(stub);
}
#pragma warning restore CS0162
// ── AI 비활성화 체크 ─────────────────────────────────────────
var appSettings = (System.Windows.Application.Current as App)?.SettingsService?.Settings;
if (appSettings?.AiEnabled == false)
return Task.FromResult<IEnumerable<LauncherItem>>(Array.Empty<LauncherItem>());
// ── 실제 구현 ───────────────────────────────────────────────
var items = new List<LauncherItem>();
var q = query.Trim();
if (string.IsNullOrEmpty(q))
{
items.Add(new LauncherItem(
"AX Agent 대화하기",
"AI 비서와 대화를 시작합니다",
null, "open_chat", Symbol: "\uE8BD"));
try
{
var storage = ServiceLocator.Get<IChatStorageService>();
var metas = storage.LoadAllMeta();
foreach (var conv in metas.Take(5))
{
var ago = FormatTimeAgo(conv.UpdatedAt);
var symbol = ChatCategory.GetSymbol(conv.Category);
items.Add(new LauncherItem(
conv.Title,
$"{ago} · 메시지 {conv.Messages.Count}개",
null, $"resume:{conv.Id}", Symbol: symbol));
}
if (metas.Any())
items.Add(new LauncherItem(
"새 대화 시작",
"이전 대화와 별개의 새 대화를 시작합니다",
null, "new_chat", Symbol: "\uE710"));
}
catch { }
}
else
{
items.Add(new LauncherItem(
$"AI에게 물어보기: {(q.Length > 40 ? q[..40] + "" : q)}",
"Enter를 누르면 AX Agent이 열리고 질문이 전송됩니다",
null, $"ask:{q}", Symbol: "\uE8BD"));
items.Add(new LauncherItem(
"AX Agent 대화하기",
"질문 없이 Agent 창만 엽니다",
null, "open_chat", Symbol: "\uE8BD"));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
#pragma warning disable CS0162 // DEPLOY_STUB 플래그에 의한 의도된 비활성 코드
if (DEPLOY_STUB) return Task.CompletedTask;
#pragma warning restore CS0162
// AI 비활성화 시 실행 차단
var appSettings2 = (System.Windows.Application.Current as App)?.SettingsService?.Settings;
if (appSettings2?.AiEnabled == false) return Task.CompletedTask;
var data = item.Data as string ?? "open_chat";
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
EnsureChatWindow();
if (data.StartsWith("ask:"))
{
var question = data[4..];
_chatWindow!.Show();
_chatWindow.Activate();
_chatWindow.SendInitialMessage(question);
}
else if (data.StartsWith("resume:"))
{
var convId = data[7..];
_chatWindow!.Show();
_chatWindow.Activate();
_chatWindow.ResumeConversation(convId);
}
else if (data == "new_chat")
{
_chatWindow!.Show();
_chatWindow.Activate();
_chatWindow.StartNewAndFocus();
}
else
{
_chatWindow!.Show();
_chatWindow.Activate();
}
});
return Task.CompletedTask;
}
private void EnsureChatWindow()
{
lock (_windowLock)
{
if (_chatWindow == null || !_chatWindow.IsLoaded)
{
_chatWindow = new ChatWindow(_settings);
_chatWindow.Closed += (_, _) => { lock (_windowLock) _chatWindow = null; };
}
}
}
private static string FormatTimeAgo(DateTime dt)
{
var diff = DateTime.Now - dt;
if (diff.TotalMinutes < 1) return "방금 전";
if (diff.TotalHours < 1) return $"{(int)diff.TotalMinutes}분 전";
if (diff.TotalDays < 1) return $"{(int)diff.TotalHours}시간 전";
if (diff.TotalDays < 7) return $"{(int)diff.TotalDays}일 전";
return dt.ToString("yyyy-MM-dd");
}
}