AX Agent 채팅 UI 백업 및 claw-code 기준 엔진 재정렬
Some checks failed
Release Gate / gate (push) Has been cancelled

- ChatWindow 현재 UI와 엔진 기준본을 etc/chat-ui-backup/2026-04-05-1215에 백업해 회귀 비교 지점을 확보함

- 메시지 컬럼과 컴포저 폭을 claw-code식 단일 축으로 다시 맞추고 입력 셸을 안정적인 하단 컬럼 구조로 정리함

- 입력창 높이를 실제 줄바꿈 수 기준으로 다시 계산해 전송 후 높이가 남는 버그를 줄임

- 메시지 편집/피드백 후 재생성 경로의 직접 UI 버블 주입을 제거하고 RenderMessages 중심으로 통합함

- SendRegenerateAsync가 Cowork/Code에서 ResolveExecutionMode와 RunAgentLoopAsync를 타도록 바꿔 재생성도 동일 엔진 축으로 정렬함

- 검증: 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-05 12:19:20 +09:00
parent f82cfc4541
commit 3ed454a98c
7 changed files with 29653 additions and 363 deletions

View File

@@ -733,6 +733,12 @@ ow + toggle 시각 언어로 통일했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0 - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 12:06 (KST) - 업데이트: 2026-04-05 12:06 (KST)
- 업데이트: 2026-04-05 12:09 (KST) - 업데이트: 2026-04-05 12:09 (KST)
- AX Agent 채팅 UI는 `claw-code` 기준으로 다시 정리하기 전에 현재 상태를 `etc/chat-ui-backup/2026-04-05-1215/`에 백업했습니다. 이 백업에는 [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml), [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs), [AxAgentExecutionEngine.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AxAgentExecutionEngine.cs) 기준본이 포함되어 있습니다.
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서는 메시지 컬럼과 빈 상태 폭을 `920px` 축으로 맞추고, 컴포저를 `760px` 기준으로 넓히면서 입력 셸을 하나의 안정적인 하단 컬럼으로 다시 정리했습니다. 같은 수정에서 컴포저 안의 `대화 내보내기` 버튼은 숨겨 `claw-code`처럼 입력과 전송에 더 집중된 구조로 단순화했습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 입력창 높이 계산은 이제 실제 줄바꿈 수만 기준으로 `Height`를 직접 다시 잡습니다. 전송 후에도 남아 있던 과도한 높이를 줄이고, `Shift+Enter`로 개행이 생길 때만 높이가 커지도록 더 강하게 고정했습니다.
- 같은 파일에서 `메시지 편집 후 재생성`, `피드백 후 재생성` 경로도 직접 `AddMessageBubble(...)`를 꽂지 않고 `RenderMessages()` 축으로 다시 돌리게 맞췄습니다. 재생성 경로 자체도 `Cowork/Code`에서는 일반 LLM 호출이 아니라 `ResolveExecutionMode(...)` + `RunAgentLoopAsync(...)`를 타도록 바꿔, 코워크/코드가 채팅 재생성 때 일반 Chat 경로로 잘못 떨어지던 문제를 줄였습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 12:24 (KST)
--- ---

View File

@@ -4482,3 +4482,10 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
- 이어서 수동 컨텍스트 압축 결과와 `/slash` 로컬 응답 경로도 conversation/session에 먼저 반영한 뒤 `RenderMessages()` 기준으로 다시 그리도록 맞췄습니다. 이 변경으로 Chat/Cowork/Code에서 “일반 전송은 모델 렌더, 로컬 응답은 직접 버블 주입”으로 두 경로가 섞여 있던 상태를 더 줄였고, 코워크/코드 엔진 정상화 작업을 계속 진행할 수 있는 공통 렌더 축을 확보했습니다. - 이어서 수동 컨텍스트 압축 결과와 `/slash` 로컬 응답 경로도 conversation/session에 먼저 반영한 뒤 `RenderMessages()` 기준으로 다시 그리도록 맞췄습니다. 이 변경으로 Chat/Cowork/Code에서 “일반 전송은 모델 렌더, 로컬 응답은 직접 버블 주입”으로 두 경로가 섞여 있던 상태를 더 줄였고, 코워크/코드 엔진 정상화 작업을 계속 진행할 수 있는 공통 렌더 축을 확보했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0 - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 12:09 (KST) - 업데이트: 2026-04-05 12:09 (KST)
- 업데이트: 2026-04-05 12:24 (KST)
- AX Agent 채팅 UI/엔진 재작업 전에 현재 기준본을 `etc/chat-ui-backup/2026-04-05-1215/`에 백업했습니다. 백업 범위는 `ChatWindow.xaml`, `ChatWindow.xaml.cs`, `Services/Agent/AxAgentExecutionEngine.cs` 3개 파일이며, `claw-code` 기준 UI/엔진 전환 중 회귀 시 즉시 비교할 수 있도록 남겼습니다.
- [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 메시지 컬럼과 빈 상태 영역 폭을 `920px` 기준으로 정리하고, 컴포저는 `760px` 고정 축으로 넓혔습니다. 입력 셸의 `InputBorder`는 라운드/패딩을 단순화하고, 컴포저 안의 `대화 내보내기` 버튼은 숨겨 `claw-code`처럼 메시지와 입력 축 중심의 레이아웃으로 더 가깝게 맞췄습니다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `UpdateInputBoxHeight()``TextBox.MinLines/MaxLines`만 믿지 않고 실제 줄바꿈 개수 기준으로 `Height`를 직접 다시 계산하도록 바꿨습니다. 이 수정은 Chat/Cowork/Code 공통으로 “전송 후 빈 상태인데 입력창 높이가 남아 있는” 버그를 더 강하게 억제하기 위한 것입니다.
- 메시지 편집 후 재생성과 수정 피드백 후 재생성도 직접 `AddMessageBubble(...)`를 삽입하지 않고, conversation 모델 갱신 후 `RenderMessages(preserveViewport: true)`로 다시 그리게 정리했습니다. 그래서 재생성 직전에 UI만 앞서 나가면서 모델과 화면이 어긋나는 경로를 더 줄였습니다.
- `SendRegenerateAsync(...)``claw-code` 기준의 단일 실행 축에 더 가깝게 맞췄습니다. 이제 Cowork/Code 재생성도 `ResolveExecutionMode(...)``BuildPromptStack(...)`를 거쳐 필요 시 `RunAgentLoopAsync(...)`를 사용하고, 최종 assistant 텍스트만 커밋합니다. 이전처럼 재생성만 일반 `_llm.SendAsync(...)`로 빠져 코워크/코드가 Chat 경로로 잘못 처리되던 분기를 제거했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0

View File

@@ -0,0 +1,128 @@
using AxCopilot.Models;
using AxCopilot.Services;
namespace AxCopilot.Services.Agent;
/// <summary>
/// AX Agent execution-prep engine.
/// Inspired by the `claw-code` split between input preparation and session execution,
/// so the UI layer stops owning message assembly and final assistant commit logic.
/// </summary>
public sealed class AxAgentExecutionEngine
{
public sealed record PreparedTurn(List<ChatMessage> Messages);
public sealed record ExecutionMode(bool UseAgentLoop, bool UseStreamingTransport, string? TaskSystemPrompt);
public IReadOnlyList<string> BuildPromptStack(
string? conversationSystem,
string? slashSystem,
string? taskSystem = null)
{
var prompts = new List<string>();
if (!string.IsNullOrWhiteSpace(conversationSystem))
prompts.Add(conversationSystem.Trim());
if (!string.IsNullOrWhiteSpace(slashSystem))
prompts.Add(slashSystem.Trim());
if (!string.IsNullOrWhiteSpace(taskSystem))
prompts.Add(taskSystem.Trim());
return prompts;
}
public ExecutionMode ResolveExecutionMode(
string runTab,
bool streamingEnabled,
string resolvedService,
string? coworkSystemPrompt,
string? codeSystemPrompt)
{
if (string.Equals(runTab, "Cowork", StringComparison.OrdinalIgnoreCase))
return new ExecutionMode(true, false, coworkSystemPrompt);
if (string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase))
return new ExecutionMode(true, false, codeSystemPrompt);
return new ExecutionMode(false, false, null);
}
public PreparedTurn PrepareTurn(
ChatConversation conversation,
IEnumerable<string?> systemPrompts,
string? fileContext = null,
IReadOnlyList<ImageAttachment>? images = null)
{
var outbound = conversation.Messages
.Select(CloneMessage)
.ToList();
if (!string.IsNullOrWhiteSpace(fileContext))
{
var lastUserIndex = outbound.FindLastIndex(m => string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase));
if (lastUserIndex >= 0)
outbound[lastUserIndex].Content = (outbound[lastUserIndex].Content ?? string.Empty) + fileContext;
}
if (images is { Count: > 0 })
{
var lastUserIndex = outbound.FindLastIndex(m => string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase));
if (lastUserIndex >= 0)
outbound[lastUserIndex].Images = images.Select(CloneImage).ToList();
}
var promptList = systemPrompts
.Where(prompt => !string.IsNullOrWhiteSpace(prompt))
.Select(prompt => prompt!.Trim())
.ToList();
for (var i = promptList.Count - 1; i >= 0; i--)
outbound.Insert(0, new ChatMessage { Role = "system", Content = promptList[i] });
return new PreparedTurn(outbound);
}
public ChatMessage CommitAssistantMessage(
ChatSessionStateService? session,
ChatConversation conversation,
string tab,
string content,
ChatStorageService? storage = null)
{
var assistant = new ChatMessage
{
Role = "assistant",
Content = content,
};
if (session != null)
{
session.AppendMessage(tab, assistant, storage);
return assistant;
}
conversation.Messages.Add(assistant);
conversation.UpdatedAt = DateTime.Now;
return assistant;
}
private static ChatMessage CloneMessage(ChatMessage source)
{
return new ChatMessage
{
Role = source.Role,
Content = source.Content,
Timestamp = source.Timestamp,
MetaRunId = source.MetaRunId,
AttachedFiles = source.AttachedFiles?.ToList(),
Images = source.Images?.Select(CloneImage).ToList(),
};
}
private static ImageAttachment CloneImage(ImageAttachment source)
{
return new ImageAttachment
{
Base64 = source.Base64,
MimeType = source.MimeType,
FileName = source.FileName,
};
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5288,16 +5288,8 @@ public partial class ChatWindow : Window
} }
} }
// UI에서 편집된 버블 이후 모두 제거 RenderMessages(preserveViewport: true);
while (MessagePanel.Children.Count > bubbleIndex + 1) AutoScrollIfNeeded();
MessagePanel.Children.RemoveAt(MessagePanel.Children.Count - 1);
// 편집된 메시지를 새 버블로 교체
MessagePanel.Children.RemoveAt(bubbleIndex);
AddMessageBubble("user", newText, animate: false);
// 마지막 위치에 삽입되도록 조정 (AddMessageBubble은 끝에 추가됨)
// bubbleIndex가 끝이 아니면 이동 — 이 경우 이후가 다 제거되었으므로 끝에 추가됨
// AI 재응답 // AI 재응답
await SendRegenerateAsync(conv); await SendRegenerateAsync(conv);
@@ -5723,10 +5715,14 @@ public partial class ChatWindow : Window
"simple" => 4, "simple" => 4,
_ => 5, _ => 5,
}; };
const double baseHeight = 42;
const double lineStep = 22;
var visibleLines = Math.Clamp(explicitLineCount, 1, maxLines);
var targetHeight = baseHeight + ((visibleLines - 1) * lineStep);
InputBox.MinLines = 1; InputBox.MinLines = 1;
InputBox.MaxLines = maxLines; InputBox.MaxLines = maxLines;
InputBox.SetCurrentValue(TextBox.HeightProperty, double.NaN); InputBox.Height = targetHeight;
InputBox.VerticalScrollBarVisibility = explicitLineCount > maxLines InputBox.VerticalScrollBarVisibility = explicitLineCount > maxLines
? ScrollBarVisibility.Auto ? ScrollBarVisibility.Auto
: ScrollBarVisibility.Disabled; : ScrollBarVisibility.Disabled;
@@ -10961,8 +10957,8 @@ public partial class ChatWindow : Window
} }
} }
// 피드백 메시지 UI 표시 RenderMessages(preserveViewport: true);
AddMessageBubble("user", $"[수정 요청] {feedback}", true); AutoScrollIfNeeded();
// 재전송 // 재전송
await SendRegenerateAsync(conv); await SendRegenerateAsync(conv);
@@ -10970,7 +10966,9 @@ public partial class ChatWindow : Window
private async Task SendRegenerateAsync(ChatConversation conv) private async Task SendRegenerateAsync(ChatConversation conv)
{ {
var runTab = NormalizeTabName(conv.Tab);
_isStreaming = true; _isStreaming = true;
_streamRunTab = runTab;
BtnSend.IsEnabled = false; BtnSend.IsEnabled = false;
BtnSend.Visibility = Visibility.Collapsed; BtnSend.Visibility = Visibility.Collapsed;
BtnStop.Visibility = Visibility.Visible; BtnStop.Visibility = Visibility.Visible;
@@ -10989,12 +10987,31 @@ public partial class ChatWindow : Window
try try
{ {
List<ChatMessage> sendMessages; var coworkSystem = string.Equals(runTab, "Cowork", StringComparison.OrdinalIgnoreCase)
lock (_convLock) sendMessages = conv.Messages.ToList(); ? BuildCoworkSystemPrompt()
if (!string.IsNullOrEmpty(conv.SystemCommand)) : null;
sendMessages.Insert(0, new ChatMessage { Role = "system", Content = conv.SystemCommand }); var codeSystem = string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase)
? BuildCodeSystemPrompt()
: null;
var (resolvedService, _) = _llm.GetCurrentModelInfo();
var executionMode = _chatEngine.ResolveExecutionMode(
runTab,
_settings.Settings.Llm.Streaming,
resolvedService,
coworkSystem,
codeSystem);
var promptStack = _chatEngine.BuildPromptStack(
conv.SystemCommand,
null,
executionMode.TaskSystemPrompt);
var response = await _llm.SendAsync(sendMessages, _streamCts.Token); List<ChatMessage> sendMessages;
lock (_convLock)
sendMessages = _chatEngine.PrepareTurn(conv, promptStack, null, null).Messages;
var response = executionMode.UseAgentLoop
? await RunAgentLoopAsync(runTab, runTab, conv, sendMessages, _streamCts.Token)
: await _llm.SendAsync(sendMessages, _streamCts.Token);
assistantContent = response; assistantContent = response;
StopAiIconPulse(); StopAiIconPulse();
_cachedStreamContent = response; _cachedStreamContent = response;
@@ -11025,14 +11042,17 @@ public partial class ChatWindow : Window
BtnSend.Visibility = Visibility.Visible; BtnSend.Visibility = Visibility.Visible;
_streamCts?.Dispose(); _streamCts?.Dispose();
_streamCts = null; _streamCts = null;
_streamRunTab = null;
SetStatusIdle(); SetStatusIdle();
} }
assistantContent = string.IsNullOrWhiteSpace(assistantContent) ? "(빈 응답)" : assistantContent; assistantContent = BuildAssistantFallbackContent(conv, runTab, assistantContent);
if (runTab is "Cowork" or "Code")
conv.ShowExecutionHistory = false;
lock (_convLock) lock (_convLock)
{ {
var session = ChatSession; var session = ChatSession;
_chatEngine.CommitAssistantMessage(session, conv, _activeTab, assistantContent, _storage); _chatEngine.CommitAssistantMessage(session, conv, runTab, assistantContent, _storage);
_currentConversation = session?.CurrentConversation ?? conv; _currentConversation = session?.CurrentConversation ?? conv;
conv = _currentConversation!; conv = _currentConversation!;
} }