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
|
- 검증: `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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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에서 편집된 버블 이후 모두 제거
|
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!;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user