AX Agent 상태 메시지 내러티브 고도화 및 코워크/코드 진행 이력 개선
- AgentStatusNarrativeCatalog를 추가해 agent event를 탭(Cowork/Code), 도구 카테고리, 대상 힌트 기준으로 해석하고 상태 메시지/상세 설명/phase label/meta를 한 곳에서 생성하도록 정리함 - ChatWindow의 live pulse 상태, idle 진행 힌트, readable process feed 요약이 동일 narrative 카탈로그를 재사용하도록 변경해 단조로운 도구명 중심 문구를 작업 의도 중심 문구로 치환함 - README, DEVELOPMENT, NEXT_ROADMAP에 2026-04-15 12:14 (KST) 기준 이력과 남은 UX 마감 메모를 반영함 검증 결과 - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_status_narrative\\ -p:IntermediateOutputPath=obj\\verify_status_narrative\\ : 경고 0 / 오류 0 - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentStatusNarrativeCatalogTests|AgentLoopIterationPreparationServiceTests|AgentToolResultBudgetTests|ChatStorageServiceTests|AgentMessageInvariantHelperTests" -p:OutputPath=bin\\verify_status_narrative_tests\\ -p:IntermediateOutputPath=obj\\verify_status_narrative_tests\\ : 통과 15
This commit is contained in:
@@ -7,6 +7,14 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
|
||||
개발 참고: Claw Code 동등성 작업 추적 문서
|
||||
`docs/claw-code-parity-plan.md`
|
||||
|
||||
- 업데이트: 2026-04-15 10:57 (KST)
|
||||
- 업데이트: 2026-04-15 12:14 (KST)
|
||||
- 코워크/코드에서 보이는 AX Agent 현재 상태 문구를 더 풍부한 narrative 기준으로 정리했습니다. 새 [AgentStatusNarrativeCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentStatusNarrativeCatalog.cs)가 이벤트 타입, 도구 성격, 탭(Cowork/Code), 대상 힌트를 함께 해석해 `작업 분석 중 → 관련 파일 확인 중 → 변경 적용 중 → 결과 검증 중`처럼 더 이해하기 쉬운 현재 상태 문구와 상세 설명을 만듭니다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs), [ChatWindow.AgentStatusPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentStatusPresentation.cs), [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs)는 이제 같은 narrative 카탈로그를 사용해 초기 상태, live pulse 상태, idle 진행 힌트, readable process feed 요약을 일관된 문장으로 보여줍니다. 도구명 한 줄 표시보다 `왜 기다리는지`, `무엇을 정리 중인지`, `다음 단계가 무엇인지`가 더 잘 드러납니다.
|
||||
- 테스트: [AgentStatusNarrativeCatalogTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentStatusNarrativeCatalogTests.cs) 추가
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_status_narrative\\ -p:IntermediateOutputPath=obj\\verify_status_narrative\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentStatusNarrativeCatalogTests|AgentLoopIterationPreparationServiceTests|AgentToolResultBudgetTests|ChatStorageServiceTests|AgentMessageInvariantHelperTests" -p:OutputPath=bin\\verify_status_narrative_tests\\ -p:IntermediateOutputPath=obj\\verify_status_narrative_tests\\` 통과 15
|
||||
|
||||
- 업데이트: 2026-04-15 10:57 (KST)
|
||||
- AX Agent 루프의 도구 미호출 복구 규칙을 [AgentLoopNoToolResponseRecoveryService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopNoToolResponseRecoveryService.cs)로 분리했습니다. `도구 미호출 루프`와 `계획만 세우고 실행하지 않는 경우`의 경고 문구, 재시도 횟수, 이벤트 요약을 별도 서비스에서 생성해 [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)의 반복 분기를 더 읽기 쉽게 정리했습니다.
|
||||
- 새 테스트 [AgentLoopNoToolResponseRecoveryServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentLoopNoToolResponseRecoveryServiceTests.cs)로 probe-only 즉시 복구, 최종 경고 전환, 계획 미실행 재시도 규칙을 회귀로 고정했습니다.
|
||||
|
||||
@@ -32,6 +32,14 @@
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_closeout\\ -p:IntermediateOutputPath=obj\\verify_closeout\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopNoToolResponseRecoveryServiceTests|AgentLoopIterationPreparationServiceTests|AgentLoopLlmRequestPreparationServiceTests|AgentQueuedCommandProjectorTests|AgentMessageInvariantHelperTests|AgentToolResultBudgetTests|AgentQueryContextBuilderTests|ChatStorageServiceTests|HtmlSkillGoldenReportTests|PptxSkillGoldenDeckTests|DocxSkillGoldenDocumentTests|ExcelSkillGoldenWorkbookTests" -p:OutputPath=bin\\verify_closeout_tests\\ -p:IntermediateOutputPath=obj\\verify_closeout_tests\\` 통과 27
|
||||
|
||||
업데이트: 2026-04-15 12:14 (KST)
|
||||
- 사용자에게 보이는 AX Agent 진행 상태 문구를 richer narrative로 고도화했습니다. `src/AxCopilot/Services/Agent/AgentStatusNarrativeCatalog.cs`를 추가해 agent event를 `탭(Cowork/Code) + 도구 카테고리 + 대상 힌트 + transcript row presentation` 기준으로 해석하고, 현재 상태 메시지/상세 설명/phase label/meta를 한곳에서 생성합니다.
|
||||
- `src/AxCopilot/Views/ChatWindow.xaml.cs`는 live pulse 상태 업데이트에 narrative 카탈로그를 적용하고, `src/AxCopilot/Views/ChatWindow.AgentStatusPresentation.cs`는 초기 준비 상태와 idle 상태를 같은 narrative 소스로 렌더링하도록 변경했습니다. `src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs`도 readable process feed summary와 phase label/meta를 동일 카탈로그에 맞춰 도구명 중심 문구 대신 의도 중심 문구를 재사용합니다.
|
||||
- 이로써 Cowork/Code에서 보이는 현재 상태와 이력 요약이 `생각하는 중`, `작업 실행 중` 같은 일반 문구에서 `관련 코드 범위 확인`, `변경 적용`, `실행 결과 분석`, `문서 산출물 구성`, `권한 확인 대기`, `컨텍스트 정리`처럼 더 구체적인 문장으로 바뀝니다.
|
||||
- 테스트: `src/AxCopilot.Tests/Services/AgentStatusNarrativeCatalogTests.cs`
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_status_narrative\\ -p:IntermediateOutputPath=obj\\verify_status_narrative\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentStatusNarrativeCatalogTests|AgentLoopIterationPreparationServiceTests|AgentToolResultBudgetTests|ChatStorageServiceTests|AgentMessageInvariantHelperTests" -p:OutputPath=bin\\verify_status_narrative_tests\\ -p:IntermediateOutputPath=obj\\verify_status_narrative_tests\\` 통과 15
|
||||
|
||||
업데이트: 2026-04-14 19:50 (KST)
|
||||
- Agent loop/queue/context 품질을 보강했습니다. `src/AxCopilot/Services/Agent/AgentCommandQueue.cs`로 실행 중 추가 입력을 우선순위와 interrupt 여부까지 포함해 관리하고, `AgentLoopService`는 이를 안전하게 반영합니다.
|
||||
- `AgentToolResultBudget`, `AgentQueryContextBuilder`, `ChatModels`는 tool result preview를 메시지에 캐시해 긴 세션과 재질문에서도 같은 축약 결과를 재사용하도록 정리했습니다.
|
||||
|
||||
@@ -217,3 +217,10 @@
|
||||
1. SQL은 이제 fallback 요약에 별도 `review severity / findings / checklist`가 붙는 수준까지 올라왔습니다. 남은 고도화는 dialect별 migration lint, schema dependency graph, rollback simulation 같은 더 깊은 검증 계층입니다.
|
||||
2. Agent loop는 응답 분해 helper가 추가되어 `RunAsync` 본체가 한 단계 더 얇아졌습니다. 이후 분리 후보는 tool dispatch와 finalize 쪽이라, 구조적 마감은 거의 끝나가고 있습니다.
|
||||
3. 문서 쪽은 기능 확장보다 `golden fixture 확대`, `목적형 bundled skill`, `print/export polish`처럼 완성도 마감 항목이 중심으로 남았습니다.
|
||||
|
||||
업데이트: 2026-04-15 12:14 (KST)
|
||||
|
||||
### 추가 진행 메모
|
||||
1. Cowork/Code의 사용자 체감 진행 문구는 `AgentStatusNarrativeCatalog` 기준으로 정리됐습니다. 같은 agent event를 status bar, idle 힌트, readable process feed가 함께 재사용하게 되어 `작업 분석 -> 읽기 -> 수정 -> 검증 -> 정리` 흐름이 더 선명하게 드러납니다.
|
||||
2. 남은 AX Agent UX 마감 항목은 상태 문구 자체보다 `카드 밀도`, `표현 수준(풍부하게/적절하게/간단하게)`별 정보량 차등, 목적형 이벤트 배지 polish 쪽입니다.
|
||||
3. 구조적 우선순위는 여전히 `tool dispatch/finalize 추가 분리`, `장기 세션 replacement state 고정`, `PPT/HTML golden 확대`가 가장 높습니다.
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class AgentStatusNarrativeCatalogTests
|
||||
{
|
||||
[Fact]
|
||||
public void BuildFromEvent_ShouldDescribeCodeReadWithTargetDetail()
|
||||
{
|
||||
var evt = new AgentEvent
|
||||
{
|
||||
Type = AgentEventType.ToolCall,
|
||||
ToolName = "file_read",
|
||||
Summary = "관련 파일 읽기",
|
||||
ToolInput = "{\"path\":\"src/AxCopilot/Views/ChatWindow.xaml.cs\"}"
|
||||
};
|
||||
|
||||
var narrative = AgentStatusNarrativeCatalog.BuildFromEvent(evt, "Code");
|
||||
|
||||
narrative.Message.Should().Contain("코드");
|
||||
narrative.Detail.Should().Contain("ChatWindow.xaml.cs");
|
||||
narrative.Category.Should().Be("read");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildFromEvent_ShouldExplainPermissionWaitClearly()
|
||||
{
|
||||
var evt = new AgentEvent
|
||||
{
|
||||
Type = AgentEventType.PermissionRequest,
|
||||
ToolName = "file_edit",
|
||||
Summary = "권한 확인 필요(Deny) · 대상: src/AxCopilot/Views/ChatWindow.xaml.cs"
|
||||
};
|
||||
|
||||
var narrative = AgentStatusNarrativeCatalog.BuildFromEvent(evt, "Code");
|
||||
|
||||
narrative.Message.Should().Contain("권한");
|
||||
narrative.Detail.Should().NotBeNullOrWhiteSpace();
|
||||
narrative.Category.Should().Be("permission");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildIdle_ShouldReflectLastExecutionCategory()
|
||||
{
|
||||
var evt = new AgentEvent
|
||||
{
|
||||
Type = AgentEventType.ToolResult,
|
||||
ToolName = "build_run",
|
||||
Summary = "빌드 결과 확인"
|
||||
};
|
||||
|
||||
var narrative = AgentStatusNarrativeCatalog.BuildIdle(
|
||||
evt,
|
||||
"Code",
|
||||
TimeSpan.FromSeconds(16),
|
||||
TimeSpan.FromSeconds(24),
|
||||
pendingPostCompaction: false);
|
||||
|
||||
narrative.Message.Should().Contain("실행");
|
||||
narrative.Detail.Should().Contain("로그");
|
||||
narrative.Category.Should().Be("execute");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildProgressStepLabel_ShouldPromoteDocumentWorkToReadableSummary()
|
||||
{
|
||||
var evt = new AgentEvent
|
||||
{
|
||||
Type = AgentEventType.ToolCall,
|
||||
ToolName = "html_create",
|
||||
Summary = "html_create 실행"
|
||||
};
|
||||
|
||||
var label = AgentStatusNarrativeCatalog.BuildProgressStepLabel(evt, "문서", "HTML 생성");
|
||||
|
||||
label.Should().Contain("문서");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildProgressPhaseMeta_ShouldExposeReadableEditCompletionLabel()
|
||||
{
|
||||
var evt = new AgentEvent
|
||||
{
|
||||
Type = AgentEventType.ToolResult,
|
||||
ToolName = "file_edit",
|
||||
Summary = "파일 수정 완료"
|
||||
};
|
||||
|
||||
AgentStatusNarrativeCatalog.BuildProgressPhaseMeta(evt).Should().Be("수정 완료");
|
||||
}
|
||||
}
|
||||
513
src/AxCopilot/Services/Agent/AgentStatusNarrativeCatalog.cs
Normal file
513
src/AxCopilot/Services/Agent/AgentStatusNarrativeCatalog.cs
Normal file
@@ -0,0 +1,513 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
internal sealed record AgentStatusNarrative(
|
||||
string Message,
|
||||
string? Detail,
|
||||
string Category,
|
||||
string? Meta = null);
|
||||
|
||||
internal static class AgentStatusNarrativeCatalog
|
||||
{
|
||||
public static AgentStatusNarrative BuildInitial(string? runTab)
|
||||
=> IsCodeTab(runTab)
|
||||
? new AgentStatusNarrative(
|
||||
"관련 코드와 수정 범위를 파악하고 있습니다...",
|
||||
"필요한 파일과 테스트 범위를 좁혀서 읽기 시작합니다.",
|
||||
"initialize")
|
||||
: new AgentStatusNarrative(
|
||||
"요청 목적과 필요한 자료를 정리하고 있습니다...",
|
||||
"답변 구조와 필요한 문서·근거를 먼저 가볍게 확인합니다.",
|
||||
"initialize");
|
||||
|
||||
public static AgentStatusNarrative BuildFromEvent(AgentEvent evt, string? runTab)
|
||||
{
|
||||
var itemDisplayName = evt.Type == AgentEventType.SkillCall
|
||||
? AgentTranscriptDisplayCatalog.GetDisplayName(evt.ToolName, slashPrefix: true)
|
||||
: AgentTranscriptDisplayCatalog.GetDisplayName(evt.ToolName);
|
||||
var transcriptBadgeLabel = AgentTranscriptDisplayCatalog.GetEventBadgeLabel(evt);
|
||||
var row = AgentTranscriptDisplayCatalog.ResolveRowPresentation(evt, itemDisplayName, transcriptBadgeLabel);
|
||||
var category = ResolveCategory(evt, row);
|
||||
var message = BuildEventMessage(evt, runTab, row, category, itemDisplayName);
|
||||
var detail = BuildEventDetail(evt, row, category, message);
|
||||
|
||||
return new AgentStatusNarrative(
|
||||
SanitizeSingleLine(message),
|
||||
SanitizeDetail(detail),
|
||||
category,
|
||||
BuildProgressPhaseMeta(evt));
|
||||
}
|
||||
|
||||
public static AgentStatusNarrative BuildIdle(
|
||||
AgentEvent? lastProgressEvent,
|
||||
string? runTab,
|
||||
TimeSpan idle,
|
||||
TimeSpan elapsed,
|
||||
bool pendingPostCompaction)
|
||||
{
|
||||
if (pendingPostCompaction)
|
||||
{
|
||||
return new AgentStatusNarrative(
|
||||
"긴 대화를 이어가기 위해 컨텍스트를 정리하고 있습니다...",
|
||||
"최근 대화와 작업 결과를 압축한 뒤 이어서 진행합니다.",
|
||||
"compact");
|
||||
}
|
||||
|
||||
var category = lastProgressEvent == null
|
||||
? (IsCodeTab(runTab) ? "read" : "plan")
|
||||
: ResolveCategory(
|
||||
lastProgressEvent,
|
||||
AgentTranscriptDisplayCatalog.ResolveRowPresentation(
|
||||
lastProgressEvent,
|
||||
lastProgressEvent.Type == AgentEventType.SkillCall
|
||||
? AgentTranscriptDisplayCatalog.GetDisplayName(lastProgressEvent.ToolName, slashPrefix: true)
|
||||
: AgentTranscriptDisplayCatalog.GetDisplayName(lastProgressEvent.ToolName),
|
||||
AgentTranscriptDisplayCatalog.GetEventBadgeLabel(lastProgressEvent)));
|
||||
|
||||
if (idle >= TimeSpan.FromSeconds(90))
|
||||
{
|
||||
return new AgentStatusNarrative(
|
||||
"작업 범위가 커서 현재까지 수집한 내용을 종합하고 있습니다...",
|
||||
BuildIdleDetail(category),
|
||||
category);
|
||||
}
|
||||
|
||||
if (idle >= TimeSpan.FromSeconds(30))
|
||||
{
|
||||
return new AgentStatusNarrative(
|
||||
BuildIdleMessage(category, runTab, longWait: true),
|
||||
BuildIdleDetail(category),
|
||||
category);
|
||||
}
|
||||
|
||||
if (idle >= TimeSpan.FromSeconds(12))
|
||||
{
|
||||
return new AgentStatusNarrative(
|
||||
BuildIdleMessage(category, runTab, longWait: false),
|
||||
BuildIdleDetail(category),
|
||||
category);
|
||||
}
|
||||
|
||||
if (idle >= TimeSpan.FromSeconds(5))
|
||||
{
|
||||
return new AgentStatusNarrative(
|
||||
"다음 단계를 정리하고 있습니다...",
|
||||
BuildIdleDetail(category),
|
||||
category);
|
||||
}
|
||||
|
||||
if (elapsed >= TimeSpan.FromSeconds(4))
|
||||
{
|
||||
return new AgentStatusNarrative(
|
||||
"작업을 이어가고 있습니다...",
|
||||
BuildIdleDetail(category),
|
||||
category);
|
||||
}
|
||||
|
||||
return BuildInitial(runTab);
|
||||
}
|
||||
|
||||
public static string BuildProgressStepLabel(AgentEvent evt, string transcriptBadgeLabel, string itemDisplayName)
|
||||
{
|
||||
var row = AgentTranscriptDisplayCatalog.ResolveRowPresentation(evt, itemDisplayName, transcriptBadgeLabel);
|
||||
var category = ResolveCategory(evt, row);
|
||||
|
||||
if (evt.Type == AgentEventType.ToolCall)
|
||||
{
|
||||
return category switch
|
||||
{
|
||||
"read" => "관련 범위를 읽고 확인하는 중",
|
||||
"edit" => "변경 내용을 적용하는 중",
|
||||
"execute" => "실행 결과를 확인하는 중",
|
||||
"document" => "문서 산출물을 구성하는 중",
|
||||
"git" => "변경 범위를 확인하는 중",
|
||||
"web" => "외부 정보를 정리하는 중",
|
||||
_ => row.Title,
|
||||
};
|
||||
}
|
||||
|
||||
if (evt.Type == AgentEventType.ToolResult)
|
||||
{
|
||||
return category switch
|
||||
{
|
||||
"read" => "읽은 내용을 정리하는 중",
|
||||
"edit" => "적용한 변경을 정리하는 중",
|
||||
"execute" => "실행 결과를 분석하는 중",
|
||||
"document" => "생성 결과를 검토하는 중",
|
||||
_ => row.Title,
|
||||
};
|
||||
}
|
||||
|
||||
return row.Title;
|
||||
}
|
||||
|
||||
public static string? BuildProgressPhaseLabel(AgentEvent evt)
|
||||
{
|
||||
var category = ResolveCategory(
|
||||
evt,
|
||||
AgentTranscriptDisplayCatalog.ResolveRowPresentation(
|
||||
evt,
|
||||
evt.Type == AgentEventType.SkillCall
|
||||
? AgentTranscriptDisplayCatalog.GetDisplayName(evt.ToolName, slashPrefix: true)
|
||||
: AgentTranscriptDisplayCatalog.GetDisplayName(evt.ToolName),
|
||||
AgentTranscriptDisplayCatalog.GetEventBadgeLabel(evt)));
|
||||
|
||||
if (category == "compact")
|
||||
return "컨텍스트 압축 중...";
|
||||
|
||||
if (evt.Type == AgentEventType.Thinking && string.Equals(evt.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase))
|
||||
return "응답을 정리하는 중...";
|
||||
|
||||
return evt.Type switch
|
||||
{
|
||||
AgentEventType.Planning => "작업 순서를 정리하는 중...",
|
||||
AgentEventType.StepStart when evt.StepTotal > 0 => $"{evt.StepCurrent}/{evt.StepTotal} 단계 진행 중...",
|
||||
AgentEventType.StepDone when evt.StepTotal > 0 => $"{evt.StepCurrent}/{evt.StepTotal} 단계 정리 중...",
|
||||
AgentEventType.PermissionRequest => "권한 확인 대기 중...",
|
||||
AgentEventType.PermissionDenied => "대체 경로를 검토하는 중...",
|
||||
AgentEventType.ToolCall => category switch
|
||||
{
|
||||
"read" => "관련 파일을 확인하는 중...",
|
||||
"edit" => "변경 내용을 적용하는 중...",
|
||||
"execute" => "실행 결과를 확인하는 중...",
|
||||
"document" => "산출물을 구성하는 중...",
|
||||
"git" => "변경 범위를 정리하는 중...",
|
||||
"web" => "필요한 정보를 찾는 중...",
|
||||
_ => null,
|
||||
},
|
||||
AgentEventType.ToolResult => category switch
|
||||
{
|
||||
"read" => "읽은 내용을 정리하는 중...",
|
||||
"edit" => "적용한 변경을 점검하는 중...",
|
||||
"execute" => "실행 결과를 분석하는 중...",
|
||||
"document" => "생성 결과를 검토하는 중...",
|
||||
_ => null,
|
||||
},
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
public static string? BuildProgressPhaseMeta(AgentEvent evt)
|
||||
{
|
||||
var itemDisplayName = evt.Type == AgentEventType.SkillCall
|
||||
? AgentTranscriptDisplayCatalog.GetDisplayName(evt.ToolName, slashPrefix: true)
|
||||
: AgentTranscriptDisplayCatalog.GetDisplayName(evt.ToolName);
|
||||
var row = AgentTranscriptDisplayCatalog.ResolveRowPresentation(
|
||||
evt,
|
||||
itemDisplayName,
|
||||
AgentTranscriptDisplayCatalog.GetEventBadgeLabel(evt));
|
||||
var category = ResolveCategory(evt, row);
|
||||
|
||||
return evt.Type switch
|
||||
{
|
||||
AgentEventType.Planning => "계획",
|
||||
AgentEventType.StepStart or AgentEventType.StepDone => "단계",
|
||||
AgentEventType.PermissionRequest => "권한",
|
||||
AgentEventType.PermissionGranted => "권한 승인",
|
||||
AgentEventType.PermissionDenied => "권한 거부",
|
||||
AgentEventType.ToolCall => category switch
|
||||
{
|
||||
"read" => "탐색",
|
||||
"edit" => "수정",
|
||||
"execute" => "실행",
|
||||
"document" => "산출물",
|
||||
"git" => "Git",
|
||||
"web" => "웹",
|
||||
_ => "도구",
|
||||
},
|
||||
AgentEventType.ToolResult => category switch
|
||||
{
|
||||
"read" => "읽기 완료",
|
||||
"edit" => "수정 완료",
|
||||
"execute" => "실행 결과",
|
||||
"document" => "생성 결과",
|
||||
_ => "결과",
|
||||
},
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildEventMessage(
|
||||
AgentEvent evt,
|
||||
string? runTab,
|
||||
AgentTranscriptRowPresentation row,
|
||||
string category,
|
||||
string itemDisplayName)
|
||||
{
|
||||
return evt.Type switch
|
||||
{
|
||||
AgentEventType.Planning => IsCodeTab(runTab)
|
||||
? "수정 순서와 검증 단계를 정리하고 있습니다..."
|
||||
: "작업 순서와 산출물 구조를 정리하고 있습니다...",
|
||||
AgentEventType.StepStart when evt.StepTotal > 0
|
||||
=> $"{evt.StepCurrent}/{evt.StepTotal} 단계 작업을 진행하고 있습니다...",
|
||||
AgentEventType.StepDone when evt.StepTotal > 0
|
||||
=> $"{evt.StepCurrent}/{evt.StepTotal} 단계를 마무리하고 다음 단계로 넘어가고 있습니다...",
|
||||
AgentEventType.PermissionRequest => "실행 전에 필요한 권한 확인을 기다리고 있습니다...",
|
||||
AgentEventType.PermissionGranted => "권한이 확인되어 작업을 이어가고 있습니다...",
|
||||
AgentEventType.PermissionDenied => "권한이 거부되어 다른 진행 경로를 검토하고 있습니다...",
|
||||
AgentEventType.SkillCall => "전용 스킬 절차를 적용하고 있습니다...",
|
||||
AgentEventType.ToolCall => category switch
|
||||
{
|
||||
"read" => IsCodeTab(runTab)
|
||||
? "관련 코드와 파일을 확인하고 있습니다..."
|
||||
: "관련 자료와 문서를 확인하고 있습니다...",
|
||||
"edit" => IsCodeTab(runTab)
|
||||
? "코드 변경을 적용하고 있습니다..."
|
||||
: "초안 내용을 다듬고 있습니다...",
|
||||
"execute" => "실행 결과와 로그를 확인하고 있습니다...",
|
||||
"document" => "문서 산출물을 구성하고 있습니다...",
|
||||
"git" => "변경 범위와 저장소 상태를 확인하고 있습니다...",
|
||||
"web" => "필요한 외부 정보를 찾아 정리하고 있습니다...",
|
||||
_ => string.IsNullOrWhiteSpace(itemDisplayName)
|
||||
? "필요한 작업을 실행하고 있습니다..."
|
||||
: $"{itemDisplayName} 작업을 진행하고 있습니다...",
|
||||
},
|
||||
AgentEventType.ToolResult => category switch
|
||||
{
|
||||
"read" => "확인한 내용을 정리하고 다음 단계를 준비하고 있습니다...",
|
||||
"edit" => "적용한 변경을 정리하고 다음 검증으로 넘어가고 있습니다...",
|
||||
"execute" => "실행 결과를 분석하고 후속 조치를 판단하고 있습니다...",
|
||||
"document" => "생성한 결과를 검토하고 다듬고 있습니다...",
|
||||
"git" => "변경 내용을 정리하고 다음 작업을 준비하고 있습니다...",
|
||||
"web" => "수집한 정보를 정리하고 답변에 연결하고 있습니다...",
|
||||
_ => string.IsNullOrWhiteSpace(row.Title)
|
||||
? "결과를 정리하고 있습니다..."
|
||||
: EnsureSentence(row.Title),
|
||||
},
|
||||
AgentEventType.Thinking when string.Equals(evt.ToolName, "context_compaction", StringComparison.OrdinalIgnoreCase)
|
||||
=> "긴 대화를 이어가기 위해 컨텍스트를 압축하고 있습니다...",
|
||||
AgentEventType.Thinking when string.Equals(evt.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase)
|
||||
=> "현재까지 진행한 내용을 정리하고 있습니다...",
|
||||
AgentEventType.Thinking when category == "document"
|
||||
=> "문서 흐름과 누락된 내용을 정리하고 있습니다...",
|
||||
AgentEventType.Thinking when (evt.Summary ?? string.Empty).Contains("검증", StringComparison.OrdinalIgnoreCase)
|
||||
=> "결과를 검토하고 누락된 부분이 없는지 확인하고 있습니다...",
|
||||
AgentEventType.Thinking when (evt.Summary ?? string.Empty).Contains("재시도", StringComparison.OrdinalIgnoreCase)
|
||||
=> "다시 시도할 경로를 정리하고 있습니다...",
|
||||
AgentEventType.Complete => "작업이 완료되었습니다.",
|
||||
AgentEventType.Error => "오류를 정리하고 복구 경로를 판단하고 있습니다...",
|
||||
AgentEventType.Decision => "다음 진행을 위해 사용자 확인을 기다리고 있습니다...",
|
||||
_ => string.IsNullOrWhiteSpace(row.Title)
|
||||
? "작업을 진행하고 있습니다..."
|
||||
: EnsureSentence(row.Title),
|
||||
};
|
||||
}
|
||||
|
||||
private static string? BuildEventDetail(
|
||||
AgentEvent evt,
|
||||
AgentTranscriptRowPresentation row,
|
||||
string category,
|
||||
string message)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
var baseDescription = SanitizeDetail(row.Description);
|
||||
if (!string.IsNullOrWhiteSpace(baseDescription) &&
|
||||
!string.Equals(baseDescription, message, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
parts.Add(baseDescription);
|
||||
}
|
||||
|
||||
if (evt.Type == AgentEventType.Planning && evt.Steps is { Count: > 0 })
|
||||
{
|
||||
parts.Add($"총 {evt.Steps.Count}단계 계획을 기준으로 진행합니다.");
|
||||
}
|
||||
|
||||
var targetHint = ExtractTargetHint(evt);
|
||||
if (!string.IsNullOrWhiteSpace(targetHint))
|
||||
{
|
||||
parts.Add(evt.Type == AgentEventType.ToolResult
|
||||
? $"결과 대상: {targetHint}"
|
||||
: $"대상: {targetHint}");
|
||||
}
|
||||
|
||||
if (category == "compact" && evt.ElapsedMs > 0)
|
||||
parts.Add("이전 대화와 최근 작업 결과를 함께 압축하고 있습니다.");
|
||||
|
||||
return parts.Count == 0 ? null : string.Join(" · ", parts);
|
||||
}
|
||||
|
||||
private static string BuildIdleMessage(string category, string? runTab, bool longWait)
|
||||
=> category switch
|
||||
{
|
||||
"read" => IsCodeTab(runTab)
|
||||
? longWait
|
||||
? "읽은 코드 맥락을 종합하고 다음 수정 지점을 정리하고 있습니다..."
|
||||
: "읽은 코드와 검색 결과를 정리하고 있습니다..."
|
||||
: longWait
|
||||
? "확인한 자료를 종합해 답변 구조를 정리하고 있습니다..."
|
||||
: "확인한 자료와 문서를 정리하고 있습니다...",
|
||||
"edit" => longWait
|
||||
? "적용한 변경 내용을 다시 검토하고 검증 포인트를 정리하고 있습니다..."
|
||||
: "적용한 변경 내용을 다시 점검하고 있습니다...",
|
||||
"execute" => longWait
|
||||
? "실행 로그를 종합해 원인과 다음 조치를 정리하고 있습니다..."
|
||||
: "실행 결과와 로그를 정리하고 있습니다...",
|
||||
"document" => longWait
|
||||
? "초안 구조와 누락된 근거를 다시 맞추고 있습니다..."
|
||||
: "초안과 산출물 구성을 다듬고 있습니다...",
|
||||
"git" => "변경 범위와 저장소 상태를 다시 정리하고 있습니다...",
|
||||
"web" => "수집한 정보를 정리해 답변에 연결하고 있습니다...",
|
||||
"permission" => "권한 확인 결과를 기다리며 다음 단계를 준비하고 있습니다...",
|
||||
_ => longWait
|
||||
? "현재까지의 진행 내용을 종합하고 다음 단계를 정리하고 있습니다..."
|
||||
: "다음 단계를 정리하고 있습니다...",
|
||||
};
|
||||
|
||||
private static string BuildIdleDetail(string category)
|
||||
=> category switch
|
||||
{
|
||||
"read" => "읽은 파일과 검색 결과를 묶어 다음 단계로 연결합니다.",
|
||||
"edit" => "수정 범위와 영향 파일을 다시 확인합니다.",
|
||||
"execute" => "빌드·테스트 로그에서 실패 원인과 후속 조치를 추립니다.",
|
||||
"document" => "초안 구조와 빠진 근거, 결론 흐름을 다시 맞춥니다.",
|
||||
"git" => "변경 범위와 저장소 상태를 함께 확인합니다.",
|
||||
"web" => "수집한 정보를 질문 흐름에 맞게 추려냅니다.",
|
||||
"permission" => "권한 결과가 정리되면 같은 작업 흐름으로 이어집니다.",
|
||||
_ => "현재까지의 진행 내용을 정리해 다음 작업으로 연결합니다.",
|
||||
};
|
||||
|
||||
private static string ResolveCategory(AgentEvent evt, AgentTranscriptRowPresentation row)
|
||||
{
|
||||
var groupKey = row.GroupKey ?? string.Empty;
|
||||
if (groupKey.StartsWith("activity:", StringComparison.OrdinalIgnoreCase))
|
||||
return groupKey["activity:".Length..];
|
||||
if (groupKey.StartsWith("permission:", StringComparison.OrdinalIgnoreCase))
|
||||
return "permission";
|
||||
if (groupKey.StartsWith("compact:", StringComparison.OrdinalIgnoreCase))
|
||||
return "compact";
|
||||
if (groupKey.StartsWith("waiting:", StringComparison.OrdinalIgnoreCase))
|
||||
return "wait";
|
||||
if (groupKey.StartsWith("planning", StringComparison.OrdinalIgnoreCase))
|
||||
return "plan";
|
||||
if (groupKey.StartsWith("thinking:", StringComparison.OrdinalIgnoreCase))
|
||||
return groupKey["thinking:".Length..];
|
||||
if (groupKey.StartsWith("step:", StringComparison.OrdinalIgnoreCase))
|
||||
return "step";
|
||||
|
||||
return evt.Type switch
|
||||
{
|
||||
AgentEventType.PermissionRequest or AgentEventType.PermissionGranted or AgentEventType.PermissionDenied => "permission",
|
||||
AgentEventType.Planning => "plan",
|
||||
AgentEventType.StepStart or AgentEventType.StepDone => "step",
|
||||
AgentEventType.Complete => "complete",
|
||||
AgentEventType.Error => "error",
|
||||
_ => "general",
|
||||
};
|
||||
}
|
||||
|
||||
private static string? ExtractTargetHint(AgentEvent evt)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(evt.FilePath))
|
||||
return Path.GetFileName(evt.FilePath);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(evt.ToolInput))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(evt.ToolInput);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object)
|
||||
return null;
|
||||
|
||||
foreach (var key in new[]
|
||||
{
|
||||
"path", "file_path", "file", "file_name", "title", "query",
|
||||
"pattern", "url", "command", "sheet_name", "dashboard_sheet_name"
|
||||
})
|
||||
{
|
||||
if (!doc.RootElement.TryGetProperty(key, out var value))
|
||||
continue;
|
||||
|
||||
var normalized = NormalizeTargetValue(key, value);
|
||||
if (!string.IsNullOrWhiteSpace(normalized))
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore malformed tool_input
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? NormalizeTargetValue(string key, JsonElement value)
|
||||
{
|
||||
if (value.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var text = value.GetString()?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return null;
|
||||
|
||||
if (key is "path" or "file_path" or "file" or "file_name")
|
||||
return Path.GetFileName(text);
|
||||
|
||||
if (key == "command")
|
||||
return text.Length > 60 ? text[..60] + "…" : text;
|
||||
|
||||
return text.Length > 70 ? text[..70] + "…" : text;
|
||||
}
|
||||
|
||||
if (value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var item in value.EnumerateArray())
|
||||
{
|
||||
var normalized = NormalizeTargetValue(key, item);
|
||||
if (!string.IsNullOrWhiteSpace(normalized))
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsCodeTab(string? runTab)
|
||||
=> string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string EnsureSentence(string text)
|
||||
{
|
||||
var normalized = SanitizeSingleLine(text);
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
return "작업을 진행하고 있습니다...";
|
||||
if (normalized.EndsWith("...", StringComparison.Ordinal))
|
||||
return normalized;
|
||||
if (normalized.EndsWith("중", StringComparison.Ordinal))
|
||||
return normalized + "...";
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static string SanitizeSingleLine(string text)
|
||||
{
|
||||
var normalized = AgentTranscriptDisplayCatalog.StripNonBmpCharacters(text ?? string.Empty)
|
||||
.Replace("\r", " ")
|
||||
.Replace("\n", " ")
|
||||
.Trim();
|
||||
while (normalized.Contains(" ", StringComparison.Ordinal))
|
||||
normalized = normalized.Replace(" ", " ", StringComparison.Ordinal);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static string? SanitizeDetail(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return null;
|
||||
|
||||
var normalized = AgentTranscriptDisplayCatalog.StripNonBmpCharacters(text)
|
||||
.Replace("\r", " ")
|
||||
.Replace("\n", " ")
|
||||
.Trim();
|
||||
|
||||
while (normalized.Contains(" ", StringComparison.Ordinal))
|
||||
normalized = normalized.Replace(" ", " ", StringComparison.Ordinal);
|
||||
|
||||
if (normalized.Length > 160)
|
||||
normalized = normalized[..160].TrimEnd() + "…";
|
||||
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -661,36 +661,7 @@ public partial class ChatWindow
|
||||
}
|
||||
|
||||
private static string BuildReadableProcessFeedSummary(AgentEvent evt, string transcriptBadgeLabel, string itemDisplayName)
|
||||
{
|
||||
var phaseLabel = ResolveProgressPhaseLabel(evt);
|
||||
if (!string.IsNullOrWhiteSpace(phaseLabel))
|
||||
return phaseLabel;
|
||||
|
||||
return evt.Type switch
|
||||
{
|
||||
AgentEventType.Thinking when string.Equals(evt.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase)
|
||||
=> "처리 중...",
|
||||
AgentEventType.Thinking when string.Equals(evt.ToolName, "context_compaction", StringComparison.OrdinalIgnoreCase)
|
||||
=> "컨텍스트 압축 중...",
|
||||
AgentEventType.Planning when evt.Steps is { Count: > 0 }
|
||||
=> $"계획 {evt.Steps.Count}단계 정리",
|
||||
AgentEventType.StepStart when evt.StepTotal > 0
|
||||
=> $"{evt.StepCurrent}/{evt.StepTotal} 단계 진행",
|
||||
AgentEventType.StepDone when evt.StepTotal > 0
|
||||
=> $"{evt.StepCurrent}/{evt.StepTotal} 단계 완료",
|
||||
AgentEventType.Thinking when !string.IsNullOrWhiteSpace(evt.Summary)
|
||||
=> evt.Summary,
|
||||
AgentEventType.ToolCall
|
||||
=> string.IsNullOrWhiteSpace(itemDisplayName)
|
||||
? $"{transcriptBadgeLabel} 실행"
|
||||
: $"{itemDisplayName} 실행",
|
||||
AgentEventType.SkillCall
|
||||
=> string.IsNullOrWhiteSpace(itemDisplayName)
|
||||
? "스킬 실행"
|
||||
: $"{itemDisplayName} 실행",
|
||||
_ => string.IsNullOrWhiteSpace(evt.Summary) ? transcriptBadgeLabel : evt.Summary,
|
||||
};
|
||||
}
|
||||
=> AgentStatusNarrativeCatalog.BuildProgressStepLabel(evt, transcriptBadgeLabel, itemDisplayName);
|
||||
|
||||
private Border CreateReadableProcessFeedCard(
|
||||
string summary,
|
||||
@@ -793,59 +764,10 @@ public partial class ChatWindow
|
||||
}
|
||||
|
||||
private static string? ResolveProgressPhaseLabel(AgentEvent evt)
|
||||
{
|
||||
var summary = (evt.Summary ?? string.Empty).Trim();
|
||||
var toolName = (evt.ToolName ?? string.Empty).Trim();
|
||||
|
||||
if (string.Equals(toolName, "context_compaction", StringComparison.OrdinalIgnoreCase))
|
||||
return "컨텍스트 압축 중...";
|
||||
if (string.Equals(toolName, "agent_wait", StringComparison.OrdinalIgnoreCase))
|
||||
return "처리 중...";
|
||||
if (summary.Contains("html_create", StringComparison.OrdinalIgnoreCase)
|
||||
|| summary.Contains("document_assemble", StringComparison.OrdinalIgnoreCase)
|
||||
|| summary.Contains("docx_create", StringComparison.OrdinalIgnoreCase))
|
||||
return "문서 결과 생성 중...";
|
||||
if (summary.Contains("검증", StringComparison.OrdinalIgnoreCase)
|
||||
|| summary.Contains("verification", StringComparison.OrdinalIgnoreCase))
|
||||
return "결과 검증 중...";
|
||||
if (summary.Contains("diff", StringComparison.OrdinalIgnoreCase))
|
||||
return "변경 내용 확인 중...";
|
||||
if (evt.Type == AgentEventType.ToolCall && !string.IsNullOrWhiteSpace(toolName))
|
||||
{
|
||||
return toolName switch
|
||||
{
|
||||
"file_read" or "directory_list" or "glob" or "grep" or "folder_map" or "multi_read" => "파일 탐색 중...",
|
||||
"file_edit" or "file_write" or "html_create" or "docx_create" or "markdown_create" => "산출물 생성 중...",
|
||||
"build_run" or "test_loop" => "실행 결과 확인 중...",
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
=> AgentStatusNarrativeCatalog.BuildProgressPhaseLabel(evt);
|
||||
|
||||
private static string? ResolveProgressPhaseMeta(AgentEvent evt)
|
||||
{
|
||||
var summary = evt.Summary ?? string.Empty;
|
||||
var toolName = evt.ToolName ?? string.Empty;
|
||||
|
||||
if (evt.Type == AgentEventType.Planning)
|
||||
return "계획";
|
||||
if (evt.Type == AgentEventType.StepStart || evt.Type == AgentEventType.StepDone)
|
||||
return "단계";
|
||||
if (string.Equals(toolName, "context_compaction", StringComparison.OrdinalIgnoreCase))
|
||||
return "압축";
|
||||
if (summary.Contains("검증", StringComparison.OrdinalIgnoreCase) || summary.Contains("verification", StringComparison.OrdinalIgnoreCase))
|
||||
return "검증";
|
||||
if (summary.Contains("fallback", StringComparison.OrdinalIgnoreCase) || summary.Contains("자동 생성", StringComparison.OrdinalIgnoreCase))
|
||||
return "폴백";
|
||||
if (summary.Contains("재시도", StringComparison.OrdinalIgnoreCase) || summary.Contains("retry", StringComparison.OrdinalIgnoreCase))
|
||||
return "재시도";
|
||||
if (evt.Type == AgentEventType.ToolCall)
|
||||
return "도구";
|
||||
|
||||
return null;
|
||||
}
|
||||
=> AgentStatusNarrativeCatalog.BuildProgressPhaseMeta(evt);
|
||||
|
||||
private Border CreateReadableProgressFeedCard(
|
||||
string summary,
|
||||
|
||||
@@ -24,14 +24,19 @@ public partial class ChatWindow
|
||||
private const int MaxStatusSubItems = 6;
|
||||
|
||||
// ShowStreamingStatusBar → 펄스 닷 바로 위임 (플로팅 상태 바 표시 안 함)
|
||||
private void ShowStreamingStatusBar(string message, string? iconCode = null)
|
||||
=> ShowPulseDots(message, iconCode);
|
||||
private void ShowStreamingStatusBar(string message, string? iconCode = null, string? detail = null)
|
||||
=> ShowPulseDots(message, iconCode, detail);
|
||||
|
||||
private void HideStreamingStatusBar()
|
||||
=> HidePulseDots();
|
||||
|
||||
private void UpdateStreamingStatusBar(string message, string? iconCode = null)
|
||||
=> UpdatePulseDotsText(message, iconCode);
|
||||
private void UpdateStreamingStatusBar(
|
||||
string message,
|
||||
string? iconCode = null,
|
||||
string? detail = null,
|
||||
bool clearSubItems = false,
|
||||
string? subItemCategory = null)
|
||||
=> UpdatePulseDotsText(message, iconCode, detail, clearSubItems, subItemCategory);
|
||||
|
||||
// ─── 입력창 위 펄스 닷 애니메이션 ──────────────────────────────────────────
|
||||
|
||||
@@ -41,6 +46,8 @@ public partial class ChatWindow
|
||||
if (PulseDotStatusText != null)
|
||||
PulseDotStatusText.Text = message ?? "생각하는 중...";
|
||||
ClearStatusSubItems();
|
||||
if (!string.IsNullOrWhiteSpace(detail))
|
||||
AddStatusSubItem(detail);
|
||||
PulseDotBar.Visibility = Visibility.Visible;
|
||||
StartStatusDiamondAnimation();
|
||||
if (_pulseDotStoryboard != null) return; // 이미 실행 중
|
||||
@@ -325,8 +332,18 @@ public partial class ChatWindow
|
||||
|
||||
var idle = DateTime.UtcNow - _lastAgentProgressEventAt;
|
||||
TryGetStreamingElapsed(out var elapsed);
|
||||
string? summary = null;
|
||||
var toolName = "agent_wait";
|
||||
var lastProgressEvent = _currentRunProgressSteps.Count > 0
|
||||
? _currentRunProgressSteps[^1]
|
||||
: null;
|
||||
var idleNarrative = AgentStatusNarrativeCatalog.BuildIdle(
|
||||
lastProgressEvent,
|
||||
runTab,
|
||||
idle,
|
||||
elapsed,
|
||||
_pendingPostCompaction);
|
||||
string? summary = idleNarrative.Message;
|
||||
var toolName = _pendingPostCompaction ? "context_compaction" : "agent_wait";
|
||||
UpdateStreamingStatusBar(idleNarrative.Message, detail: idleNarrative.Detail);
|
||||
|
||||
if (_pendingPostCompaction && idle >= TimeSpan.FromSeconds(2))
|
||||
{
|
||||
@@ -359,6 +376,17 @@ public partial class ChatWindow
|
||||
summary = "작업을 진행하는 중입니다...";
|
||||
}
|
||||
|
||||
summary = idleNarrative.Message;
|
||||
if (_pendingPostCompaction || idle >= TimeSpan.FromSeconds(30))
|
||||
{
|
||||
UpdateStreamingStatusBar(
|
||||
summary,
|
||||
_pendingPostCompaction ? "\uE72C" : "\uE895",
|
||||
idleNarrative.Detail,
|
||||
clearSubItems: true,
|
||||
subItemCategory: idleNarrative.Category);
|
||||
}
|
||||
|
||||
UpdateLiveAgentProgressHint(summary, toolName);
|
||||
}
|
||||
|
||||
|
||||
@@ -6632,21 +6632,18 @@ public partial class ChatWindow : Window
|
||||
// ── 1단계: 경량 UI 피드백 (PulseDotBar 상태 텍스트만 갱신) ───────────
|
||||
if (string.Equals(runTab, _activeTab, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
switch (evt.Type)
|
||||
if (evt.Type is AgentEventType.Complete or AgentEventType.Error)
|
||||
{
|
||||
case AgentEventType.ToolCall when !string.IsNullOrWhiteSpace(evt.ToolName):
|
||||
if (PulseDotStatusText != null && PulseDotBar?.Visibility == Visibility.Visible)
|
||||
PulseDotStatusText.Text = GetStatusInfoForTool(evt.ToolName).message + "...";
|
||||
break;
|
||||
case AgentEventType.ToolResult when !string.IsNullOrWhiteSpace(evt.ToolName):
|
||||
if (PulseDotStatusText != null && PulseDotBar?.Visibility == Visibility.Visible)
|
||||
PulseDotStatusText.Text = GetToolResultMessage(evt.ToolName) + "...";
|
||||
break;
|
||||
case AgentEventType.Complete:
|
||||
case AgentEventType.Error:
|
||||
HideStreamingStatusBar();
|
||||
FlushPendingAgentUiEvent();
|
||||
break;
|
||||
}
|
||||
else if (PulseDotBar?.Visibility == Visibility.Visible)
|
||||
{
|
||||
var liveStatus = AgentStatusNarrativeCatalog.BuildFromEvent(evt, runTab);
|
||||
UpdateStreamingStatusBar(
|
||||
liveStatus.Message,
|
||||
detail: liveStatus.Detail,
|
||||
subItemCategory: liveStatus.Category);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6706,15 +6703,17 @@ public partial class ChatWindow : Window
|
||||
if (string.Equals(runTab, "Cowork", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
UpdateLiveAgentProgressHint("작업을 준비하는 중입니다...", "agent_wait");
|
||||
var initialStatus = AgentStatusNarrativeCatalog.BuildInitial(runTab);
|
||||
UpdateLiveAgentProgressHint(initialStatus.Message, "agent_wait");
|
||||
ShowStreamingStatusBar(initialStatus.Message, detail: initialStatus.Detail);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateLiveAgentProgressHint(null);
|
||||
ShowStreamingStatusBar("생각하는 중...");
|
||||
}
|
||||
_agentProgressHintTimer.Stop();
|
||||
_agentProgressHintTimer.Start();
|
||||
ShowStreamingStatusBar("생각하는 중...");
|
||||
}
|
||||
|
||||
private void StopLiveAgentProgressHints()
|
||||
|
||||
Reference in New Issue
Block a user