AX Agent 채팅 UI 백업 및 claw-code 기준 엔진 재정렬
Some checks failed
Release Gate / gate (push) Has been cancelled
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:
@@ -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
|
||||
- 업데이트: 2026-04-05 12:06 (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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4482,3 +4482,10 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
|
||||
- 이어서 수동 컨텍스트 압축 결과와 `/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
|
||||
- 업데이트: 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
|
||||
|
||||
128
etc/chat-ui-backup/2026-04-05-1215/AxAgentExecutionEngine.cs
Normal file
128
etc/chat-ui-backup/2026-04-05-1215/AxAgentExecutionEngine.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
5175
etc/chat-ui-backup/2026-04-05-1215/ChatWindow.xaml
Normal file
5175
etc/chat-ui-backup/2026-04-05-1215/ChatWindow.xaml
Normal file
File diff suppressed because it is too large
Load Diff
21806
etc/chat-ui-backup/2026-04-05-1215/ChatWindow.xaml.cs
Normal file
21806
etc/chat-ui-backup/2026-04-05-1215/ChatWindow.xaml.cs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5288,16 +5288,8 @@ public partial class ChatWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
// UI에서 편집된 버블 이후 모두 제거
|
||||
while (MessagePanel.Children.Count > bubbleIndex + 1)
|
||||
MessagePanel.Children.RemoveAt(MessagePanel.Children.Count - 1);
|
||||
|
||||
// 편집된 메시지를 새 버블로 교체
|
||||
MessagePanel.Children.RemoveAt(bubbleIndex);
|
||||
AddMessageBubble("user", newText, animate: false);
|
||||
|
||||
// 마지막 위치에 삽입되도록 조정 (AddMessageBubble은 끝에 추가됨)
|
||||
// bubbleIndex가 끝이 아니면 이동 — 이 경우 이후가 다 제거되었으므로 끝에 추가됨
|
||||
RenderMessages(preserveViewport: true);
|
||||
AutoScrollIfNeeded();
|
||||
|
||||
// AI 재응답
|
||||
await SendRegenerateAsync(conv);
|
||||
@@ -5723,10 +5715,14 @@ public partial class ChatWindow : Window
|
||||
"simple" => 4,
|
||||
_ => 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.MaxLines = maxLines;
|
||||
InputBox.SetCurrentValue(TextBox.HeightProperty, double.NaN);
|
||||
InputBox.Height = targetHeight;
|
||||
InputBox.VerticalScrollBarVisibility = explicitLineCount > maxLines
|
||||
? ScrollBarVisibility.Auto
|
||||
: ScrollBarVisibility.Disabled;
|
||||
@@ -10961,8 +10957,8 @@ public partial class ChatWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
// 피드백 메시지 UI 표시
|
||||
AddMessageBubble("user", $"[수정 요청] {feedback}", true);
|
||||
RenderMessages(preserveViewport: true);
|
||||
AutoScrollIfNeeded();
|
||||
|
||||
// 재전송
|
||||
await SendRegenerateAsync(conv);
|
||||
@@ -10970,7 +10966,9 @@ public partial class ChatWindow : Window
|
||||
|
||||
private async Task SendRegenerateAsync(ChatConversation conv)
|
||||
{
|
||||
var runTab = NormalizeTabName(conv.Tab);
|
||||
_isStreaming = true;
|
||||
_streamRunTab = runTab;
|
||||
BtnSend.IsEnabled = false;
|
||||
BtnSend.Visibility = Visibility.Collapsed;
|
||||
BtnStop.Visibility = Visibility.Visible;
|
||||
@@ -10989,12 +10987,31 @@ public partial class ChatWindow : Window
|
||||
|
||||
try
|
||||
{
|
||||
List<ChatMessage> sendMessages;
|
||||
lock (_convLock) sendMessages = conv.Messages.ToList();
|
||||
if (!string.IsNullOrEmpty(conv.SystemCommand))
|
||||
sendMessages.Insert(0, new ChatMessage { Role = "system", Content = conv.SystemCommand });
|
||||
var coworkSystem = string.Equals(runTab, "Cowork", StringComparison.OrdinalIgnoreCase)
|
||||
? BuildCoworkSystemPrompt()
|
||||
: null;
|
||||
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;
|
||||
StopAiIconPulse();
|
||||
_cachedStreamContent = response;
|
||||
@@ -11025,14 +11042,17 @@ public partial class ChatWindow : Window
|
||||
BtnSend.Visibility = Visibility.Visible;
|
||||
_streamCts?.Dispose();
|
||||
_streamCts = null;
|
||||
_streamRunTab = null;
|
||||
SetStatusIdle();
|
||||
}
|
||||
|
||||
assistantContent = string.IsNullOrWhiteSpace(assistantContent) ? "(빈 응답)" : assistantContent;
|
||||
assistantContent = BuildAssistantFallbackContent(conv, runTab, assistantContent);
|
||||
if (runTab is "Cowork" or "Code")
|
||||
conv.ShowExecutionHistory = false;
|
||||
lock (_convLock)
|
||||
{
|
||||
var session = ChatSession;
|
||||
_chatEngine.CommitAssistantMessage(session, conv, _activeTab, assistantContent, _storage);
|
||||
_chatEngine.CommitAssistantMessage(session, conv, runTab, assistantContent, _storage);
|
||||
_currentConversation = session?.CurrentConversation ?? conv;
|
||||
conv = _currentConversation!;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user