- 코워크·코드 프롬프트, 도구 선택, 문서 생성/검증 흐름을 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)
169 lines
6.6 KiB
C#
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");
|
|
}
|
|
}
|