채팅 탭 SSE 스트리밍 응답 경로 복구 및 문서 반영
Some checks failed
Release Gate / gate (push) Has been cancelled

- Chat 탭 직접 대화 경로가 최종 응답만 한 번에 표시하던 문제를 수정하고 LlmService 스트리밍 경로를 실제 UI에 연결함\n- AxAgentExecutionEngine에서 비에이전트 채팅이 스트리밍 전송을 사용할 수 있도록 실행 모드를 조정함\n- ChatWindow에서 기존 스트리밍 컨테이너와 타이핑 타이머를 실제 전송 루프에 연결해 타자 치듯 점진적으로 응답이 보이게 함\n- README와 DEVELOPMENT 문서에 2026-04-07 02:23 (KST) 기준 변경 이력 반영\n- 검증: 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-07 08:03:37 +09:00
parent 8617f66496
commit 23b2352637
4 changed files with 52 additions and 8 deletions

View File

@@ -7,6 +7,10 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
개발 참고: Claw Code 동등성 작업 추적 문서 개발 참고: Claw Code 동등성 작업 추적 문서
`docs/claw-code-parity-plan.md` `docs/claw-code-parity-plan.md`
- 업데이트: 2026-04-07 02:23 (KST)
- AX Agent 직접 대화(Chat 탭) 경로에 실제 스트리밍 응답 연결을 복구했습니다. LLM 서비스는 원래 SSE/스트리밍을 지원하고 있었지만 UI 실행 경로가 최종 문자열만 받아 한 번에 붙이던 상태였고, 이제 설정상 스트리밍이 켜져 있으면 채팅 응답이 타자 치듯 점진적으로 표시됩니다.
- Cowork/Code는 기존처럼 agent loop 진행 메시지 중심을 유지하고, 직접 대화 재생성은 같은 스트리밍 경로를 공유하도록 정리했습니다.
- 업데이트: 2026-04-07 02:03 (KST) - 업데이트: 2026-04-07 02:03 (KST)
- Cowork 진행 표시가 오래 비거나 완료 후 실패처럼 깜박이던 흐름을 정리했습니다. 진행 힌트는 작업 시작 직후부터 즉시 보이게 하고, 중간 이벤트가 들어와도 불필요하게 사라지지 않도록 유지 로직을 조정했습니다. - Cowork 진행 표시가 오래 비거나 완료 후 실패처럼 깜박이던 흐름을 정리했습니다. 진행 힌트는 작업 시작 직후부터 즉시 보이게 하고, 중간 이벤트가 들어와도 불필요하게 사라지지 않도록 유지 로직을 조정했습니다.
- 프리셋/입력창/진행 카드에 남아 있던 깨진 한글을 복구했고, 내부 실행 게이트 문구도 정상 한국어로 정리해 Cowork 루프가 잘못된 안내 문구에 끌리지 않도록 보강했습니다. - 프리셋/입력창/진행 카드에 남아 있던 깨진 한글을 복구했고, 내부 실행 게이트 문구도 정상 한국어로 정리해 Cowork 루프가 잘못된 안내 문구에 끌리지 않도록 보강했습니다.

View File

@@ -5284,3 +5284,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
- 내부 중단/취소가 발생했을 때 사용자 취소로 단정하는 문구를 중립적으로 조정해 오탐성 `사용자가 작업을 취소했습니다` 표시를 줄였다. - 내부 중단/취소가 발생했을 때 사용자 취소로 단정하는 문구를 중립적으로 조정해 오탐성 `사용자가 작업을 취소했습니다` 표시를 줄였다.
- Document update: 2026-04-07 02:23 (KST) - Reconnected the AX Agent direct-chat execution path to the existing SSE/streaming transport in `LlmService`. Chat replies no longer wait for the final full string before rendering; when streaming is enabled they now advance through the existing streaming container and typing-timer path.
- Document update: 2026-04-07 02:23 (KST) - Updated `AxAgentExecutionEngine.ResolveExecutionMode()` so non-agent chat can opt into streaming transport, and wired `ChatWindow.ExecutePreparedTurnAsync()` to consume `LlmService.StreamAsync(...)` for direct conversations while keeping Cowork/Code on the agent-loop progress-feed path.

View File

@@ -49,7 +49,7 @@ public sealed class AxAgentExecutionEngine
if (string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase)) if (string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase))
return new ExecutionMode(true, false, codeSystemPrompt); return new ExecutionMode(true, false, codeSystemPrompt);
return new ExecutionMode(false, false, null); return new ExecutionMode(false, streamingEnabled, null);
} }
public PreparedExecution PrepareExecution( public PreparedExecution PrepareExecution(

View File

@@ -5593,14 +5593,45 @@ public partial class ChatWindow : Window
_elapsedTimer.Start(); _elapsedTimer.Start();
SetStatus(busyStatus, spinning: true); SetStatus(busyStatus, spinning: true);
StackPanel? streamingContainer = null;
TextBlock? streamingText = null;
try try
{ {
var response = await _chatEngine.ExecutePreparedAsync( if (!preparedExecution.Mode.UseAgentLoop && preparedExecution.Mode.UseStreamingTransport)
preparedExecution, {
(messages, token) => RunAgentLoopAsync(runTab, rememberTab, conversation, messages, token), streamingContainer = CreateStreamingContainer(out var createdStreamText);
(messages, token) => _llm.SendAsync(messages.ToList(), token), streamingText = createdStreamText;
_streamCts.Token); _activeStreamText = streamingText;
assistantContent = response; _cachedStreamContent = "";
_displayedLength = 0;
_cursorVisible = true;
MessagePanel.Children.Add(streamingContainer);
ForceScrollToEnd();
_cursorTimer.Start();
_typingTimer.Start();
await foreach (var chunk in _llm.StreamAsync(preparedExecution.Messages.ToList(), _streamCts.Token))
{
if (string.IsNullOrEmpty(chunk))
continue;
assistantContent += chunk;
_cachedStreamContent = assistantContent;
if (_activeStreamText != null && _displayedLength == 0)
_activeStreamText.Text = _cursorVisible ? "\u258c" : " ";
}
}
else
{
var response = await _chatEngine.ExecutePreparedAsync(
preparedExecution,
(messages, token) => RunAgentLoopAsync(runTab, rememberTab, conversation, messages, token),
(messages, token) => _llm.SendAsync(messages.ToList(), token),
_streamCts.Token);
assistantContent = response;
}
responseElapsedMs = Math.Max(0, (long)(DateTime.UtcNow - _streamStartTime).TotalMilliseconds); responseElapsedMs = Math.Max(0, (long)(DateTime.UtcNow - _streamStartTime).TotalMilliseconds);
assistantMetaRunId = _appState.AgentRun.RunId; assistantMetaRunId = _appState.AgentRun.RunId;
var usage = _llm.LastTokenUsage; var usage = _llm.LastTokenUsage;
@@ -5618,8 +5649,11 @@ public partial class ChatWindow : Window
} }
} }
StopAiIconPulse(); StopAiIconPulse();
_cachedStreamContent = response; _cachedStreamContent = assistantContent;
draftSucceeded = true; draftSucceeded = true;
if (streamingContainer != null && streamingText != null)
FinalizeStreamingContainer(streamingContainer, streamingText, assistantContent);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -5627,6 +5661,8 @@ public partial class ChatWindow : Window
assistantContent = finalized.Content; assistantContent = finalized.Content;
draftCancelled = finalized.Cancelled; draftCancelled = finalized.Cancelled;
draftFailure = finalized.FailureReason; draftFailure = finalized.FailureReason;
if (streamingContainer != null && streamingText != null)
FinalizeStreamingContainer(streamingContainer, streamingText, assistantContent);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -5634,6 +5670,8 @@ public partial class ChatWindow : Window
assistantContent = finalized.Content; assistantContent = finalized.Content;
draftFailure = finalized.FailureReason; draftFailure = finalized.FailureReason;
ShowToast("실패한 요청은 작업 요약에서 다시 시도할 수 있습니다.", "\uE783", 2600); ShowToast("실패한 요청은 작업 요약에서 다시 시도할 수 있습니다.", "\uE783", 2600);
if (streamingContainer != null && streamingText != null)
FinalizeStreamingContainer(streamingContainer, streamingText, assistantContent);
} }
finally finally
{ {