동시 Cowork·Code 실행 시 메인 루프 상태 혼선을 탭별로 분리
Cowork와 Code를 동시에 실행할 때 메인 루프 번호와 라이브 진행 힌트가 서로 섞이던 문제를 수정했다. ChatWindow가 전역 단일 진행 상태를 공유하던 구조를 탭별 현재 run 상태, 진행 스텝, 라이브 힌트, 대기 UI 이벤트로 분리해 현재 탭 기준으로만 렌더링하도록 정리했다. AppStateService에 탭별 최신 run 상태 추적을 추가하고 ConversationList, TaskSummary, Timeline, V2 라이브 카드가 활성 탭의 run 메타를 읽도록 변경했다. AppStateServiceTests에 탭별 run iteration 분리 회귀 테스트를 추가했고 README와 DEVELOPMENT 문서에도 2026-04-15 22:25 (KST) 기준 이력을 반영했다. 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_tab_loop_isolation\\ -p:IntermediateOutputPath=obj\\verify_tab_loop_isolation\\ (경고 0 / 오류 0) 검증: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AppStateServiceTests" -p:OutputPath=bin\\verify_tab_loop_isolation_tests\\ -p:IntermediateOutputPath=obj\\verify_tab_loop_isolation_tests\\ (통과 45)
This commit is contained in:
@@ -2296,3 +2296,9 @@ MIT License
|
||||
- idle 상태는 중립색 구체와 하이라이트로, 실행 중 상태는 `AccentColor` 기반 구체로 표시되게 바꿨고, unread completion 점도 같은 시각 언어에 맞춰 외곽 스트로크와 하이라이트를 맞췄습니다.
|
||||
- 검증:
|
||||
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_conversation_symbol_sphere\\ -p:IntermediateOutputPath=obj\\verify_conversation_symbol_sphere\\` 경고 0 / 오류 0
|
||||
- 업데이트: 2026-04-15 22:25 (KST)
|
||||
- AX Agent에서 Cowork/Code를 동시에 실행할 때 메인 루프 번호와 라이브 진행 힌트가 서로 섞이던 문제를 정리했습니다. `src/AxCopilot/Views/ChatWindow.xaml.cs`, `src/AxCopilot/Views/ChatWindow.AgentStatusPresentation.cs`, `src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs`가 전역 단일 상태 대신 탭별 현재 run 상태, 진행 스텝, 라이브 힌트, 대기 UI 이벤트를 분리해 관리합니다.
|
||||
- `src/AxCopilot/Services/AppStateService.cs`는 탭별 최신 run 상태 조회를 지원하도록 확장했고, 대화목록/작업요약/실행 메타 표시가 `Cowork`와 `Code` 각각의 run 정보를 읽도록 맞췄습니다. 동시에 실행해도 한 탭의 `메인 루프 1`이 다른 탭의 `메인 루프 14/15/16` 사이에 끼어드는 식의 혼선이 줄어듭니다.
|
||||
- 테스트:
|
||||
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_tab_loop_isolation\\ -p:IntermediateOutputPath=obj\\verify_tab_loop_isolation\\` 경고 0 / 오류 0
|
||||
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AppStateServiceTests" -p:OutputPath=bin\\verify_tab_loop_isolation_tests\\ -p:IntermediateOutputPath=obj\\verify_tab_loop_isolation_tests\\` 통과 45
|
||||
|
||||
@@ -1620,3 +1620,8 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫
|
||||
- AX Agent 좌측 대화 목록 상태 심볼을 열린 링 대신 꽉 찬 원형 구체 스타일로 정리했습니다. `src/AxCopilot/Views/ChatWindow.xaml`의 `ConversationItemTemplate`에서 idle/running 표시를 모두 레이어드 `Ellipse` 배지로 교체해, 한쪽이 비어 보이던 시각 문제를 없앴습니다.
|
||||
- idle 상태는 중립색 구체와 하이라이트 조합으로, 실행 중 상태는 `AccentColor` 기반 구체로 보이게 바꿨고, unread completion 점도 같은 외곽/하이라이트 패턴으로 맞췄습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_conversation_symbol_sphere\\ -p:IntermediateOutputPath=obj\\verify_conversation_symbol_sphere\\` 경고 0 / 오류 0
|
||||
업데이트: 2026-04-15 22:25 (KST)
|
||||
- AX Agent 동시 실행 회귀를 수정했습니다. `src/AxCopilot/Views/ChatWindow.xaml.cs`, `src/AxCopilot/Views/ChatWindow.AgentStatusPresentation.cs`, `src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs`, `src/AxCopilot/Views/ChatWindow.V2LiveProgressPresentation.cs`가 전역 단일 진행 상태 대신 탭별 현재 run 상태, 라이브 힌트, 진행 스텝, 대기 UI 이벤트를 분리해 Cowork/Code 동시 실행 시 메인 루프 번호가 서로 섞이지 않도록 보정합니다.
|
||||
- `src/AxCopilot/Services/AppStateService.cs`는 `GetAgentRunForTab(...)`과 탭 지정 `ApplyAgentEvent(...)`를 지원하도록 확장했고, `src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs`, `src/AxCopilot/Views/ChatWindow.TaskSummary.cs`가 현재 활성 탭의 run 메타를 읽도록 변경했습니다.
|
||||
- 테스트: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_tab_loop_isolation\\ -p:IntermediateOutputPath=obj\\verify_tab_loop_isolation\\` 경고 0 / 오류 0
|
||||
- 테스트: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AppStateServiceTests" -p:OutputPath=bin\\verify_tab_loop_isolation_tests\\ -p:IntermediateOutputPath=obj\\verify_tab_loop_isolation_tests\\` 통과 45
|
||||
|
||||
@@ -267,6 +267,37 @@ public class AppStateServiceTests
|
||||
state.FormatHookEventLine(state.GetRecentHookEvents()[0]).Should().Contain("hook");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyAgentEvent_TracksLatestRunPerTabWithoutMixingIterations()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
|
||||
state.ApplyAgentEvent(new AgentEvent
|
||||
{
|
||||
RunId = "cowork-run",
|
||||
Type = AgentEventType.Thinking,
|
||||
Summary = "코워크 진행",
|
||||
Iteration = 14,
|
||||
Timestamp = DateTime.Now.AddSeconds(-2),
|
||||
}, "Cowork");
|
||||
|
||||
state.ApplyAgentEvent(new AgentEvent
|
||||
{
|
||||
RunId = "code-run",
|
||||
Type = AgentEventType.Thinking,
|
||||
Summary = "코드 진행",
|
||||
Iteration = 1,
|
||||
Timestamp = DateTime.Now,
|
||||
}, "Code");
|
||||
|
||||
state.GetAgentRunForTab("Cowork").Should().NotBeNull();
|
||||
state.GetAgentRunForTab("Cowork")!.RunId.Should().Be("cowork-run");
|
||||
state.GetAgentRunForTab("Cowork")!.LastIteration.Should().Be(14);
|
||||
state.GetAgentRunForTab("Code").Should().NotBeNull();
|
||||
state.GetAgentRunForTab("Code")!.RunId.Should().Be("code-run");
|
||||
state.GetAgentRunForTab("Code")!.LastIteration.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySubAgentStatus_StoresBackgroundJobHistory()
|
||||
{
|
||||
|
||||
@@ -194,6 +194,7 @@ public sealed class AppStateService : IAppStateService
|
||||
public IReadOnlyList<TaskRunStore.TaskRun> ActiveTasks => TaskRuns.ActiveTasks;
|
||||
public IReadOnlyList<TaskRunStore.TaskRun> RecentTasks => TaskRuns.RecentTasks;
|
||||
public AgentRunState AgentRun { get; } = new();
|
||||
private readonly Dictionary<string, AgentRunState> _agentRunsByTab = new(StringComparer.OrdinalIgnoreCase);
|
||||
public List<AgentRunState> AgentRunHistory { get; } = new();
|
||||
public List<PermissionEventState> PermissionHistory { get; } = new();
|
||||
public List<HookEventState> HookHistory { get; } = new();
|
||||
@@ -278,30 +279,59 @@ public sealed class AppStateService : IAppStateService
|
||||
TaskRuns.CompleteByPrefix(prefix, summary, status);
|
||||
}
|
||||
|
||||
public void UpsertAgentRun(string runId, string status, string summary, int iteration)
|
||||
private static string NormalizeTabKey(string? tab)
|
||||
=> string.IsNullOrWhiteSpace(tab) ? "" : tab.Trim();
|
||||
|
||||
private static void ApplyRunSnapshot(AgentRunState target, string runId, string status, string summary, int iteration)
|
||||
{
|
||||
if (!string.Equals(AgentRun.RunId, runId, StringComparison.OrdinalIgnoreCase))
|
||||
if (!string.Equals(target.RunId, runId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
AgentRun.RunId = runId;
|
||||
AgentRun.StartedAt = DateTime.Now;
|
||||
target.RunId = runId;
|
||||
target.StartedAt = DateTime.Now;
|
||||
}
|
||||
|
||||
AgentRun.Status = status;
|
||||
AgentRun.Summary = summary;
|
||||
AgentRun.LastIteration = iteration;
|
||||
AgentRun.UpdatedAt = DateTime.Now;
|
||||
target.Status = status;
|
||||
target.Summary = summary;
|
||||
target.LastIteration = iteration;
|
||||
target.UpdatedAt = DateTime.Now;
|
||||
}
|
||||
|
||||
private AgentRunState GetOrCreateAgentRunForTab(string? tab)
|
||||
{
|
||||
var key = NormalizeTabKey(tab);
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return AgentRun;
|
||||
|
||||
if (_agentRunsByTab.TryGetValue(key, out var existing))
|
||||
return existing;
|
||||
|
||||
var created = new AgentRunState();
|
||||
_agentRunsByTab[key] = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
public AgentRunState? GetAgentRunForTab(string? tab)
|
||||
{
|
||||
var key = NormalizeTabKey(tab);
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return AgentRun;
|
||||
|
||||
return _agentRunsByTab.TryGetValue(key, out var state) ? state : null;
|
||||
}
|
||||
|
||||
public void UpsertAgentRun(string runId, string status, string summary, int iteration, string? tab = null)
|
||||
{
|
||||
ApplyRunSnapshot(AgentRun, runId, status, summary, iteration);
|
||||
if (!string.IsNullOrWhiteSpace(tab))
|
||||
ApplyRunSnapshot(GetOrCreateAgentRunForTab(tab), runId, status, summary, iteration);
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
public void CompleteAgentRun(string runId, string status, string summary, int iteration)
|
||||
public void CompleteAgentRun(string runId, string status, string summary, int iteration, string? tab = null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(runId))
|
||||
AgentRun.RunId = runId;
|
||||
|
||||
AgentRun.Status = status;
|
||||
AgentRun.Summary = summary;
|
||||
AgentRun.LastIteration = iteration;
|
||||
AgentRun.UpdatedAt = DateTime.Now;
|
||||
ApplyRunSnapshot(AgentRun, runId, status, summary, iteration);
|
||||
if (!string.IsNullOrWhiteSpace(tab))
|
||||
ApplyRunSnapshot(GetOrCreateAgentRunForTab(tab), runId, status, summary, iteration);
|
||||
AgentRunHistory.Insert(0, new AgentRunState
|
||||
{
|
||||
RunId = AgentRun.RunId,
|
||||
@@ -319,6 +349,7 @@ public sealed class AppStateService : IAppStateService
|
||||
public void RestoreAgentRunHistory(IEnumerable<ChatAgentRunRecord>? history)
|
||||
{
|
||||
AgentRunHistory.Clear();
|
||||
_agentRunsByTab.Clear();
|
||||
if (history != null)
|
||||
{
|
||||
foreach (var item in history
|
||||
@@ -398,6 +429,9 @@ public sealed class AppStateService : IAppStateService
|
||||
}
|
||||
|
||||
public void ApplyAgentEvent(AgentEvent evt)
|
||||
=> ApplyAgentEvent(evt, null);
|
||||
|
||||
public void ApplyAgentEvent(AgentEvent evt, string? tab)
|
||||
{
|
||||
TaskRuns.ApplyAgentEvent(evt);
|
||||
if (evt.Type is AgentEventType.PermissionRequest or AgentEventType.PermissionGranted or AgentEventType.PermissionDenied)
|
||||
@@ -412,21 +446,24 @@ public sealed class AppStateService : IAppStateService
|
||||
evt.RunId,
|
||||
"running",
|
||||
string.IsNullOrWhiteSpace(evt.Summary) ? "응답을 준비하는 중" : evt.Summary,
|
||||
evt.Iteration);
|
||||
evt.Iteration,
|
||||
tab);
|
||||
break;
|
||||
case AgentEventType.Planning:
|
||||
UpsertAgentRun(
|
||||
evt.RunId,
|
||||
"running",
|
||||
string.IsNullOrWhiteSpace(evt.Summary) ? "작업 계획을 세우는 중" : evt.Summary,
|
||||
evt.Iteration);
|
||||
evt.Iteration,
|
||||
tab);
|
||||
break;
|
||||
case AgentEventType.Complete:
|
||||
CompleteAgentRun(
|
||||
evt.RunId,
|
||||
"completed",
|
||||
string.IsNullOrWhiteSpace(evt.Summary) ? "작업 완료" : evt.Summary,
|
||||
evt.Iteration);
|
||||
evt.Iteration,
|
||||
tab);
|
||||
break;
|
||||
case AgentEventType.Error:
|
||||
if (string.IsNullOrWhiteSpace(evt.ToolName))
|
||||
@@ -435,7 +472,8 @@ public sealed class AppStateService : IAppStateService
|
||||
evt.RunId,
|
||||
"failed",
|
||||
string.IsNullOrWhiteSpace(evt.Summary) ? "에이전트 실행 실패" : evt.Summary,
|
||||
evt.Iteration);
|
||||
evt.Iteration,
|
||||
tab);
|
||||
}
|
||||
break;
|
||||
case AgentEventType.Paused:
|
||||
@@ -443,14 +481,16 @@ public sealed class AppStateService : IAppStateService
|
||||
evt.RunId,
|
||||
"paused",
|
||||
string.IsNullOrWhiteSpace(evt.Summary) ? "에이전트 일시중지" : evt.Summary,
|
||||
evt.Iteration);
|
||||
evt.Iteration,
|
||||
tab);
|
||||
break;
|
||||
case AgentEventType.Resumed:
|
||||
UpsertAgentRun(
|
||||
evt.RunId,
|
||||
"running",
|
||||
string.IsNullOrWhiteSpace(evt.Summary) ? "에이전트 재개" : evt.Summary,
|
||||
evt.Iteration);
|
||||
evt.Iteration,
|
||||
tab);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,10 +308,8 @@ public partial class ChatWindow
|
||||
private static string GetStatusMessageForTool(string toolName)
|
||||
=> GetStatusInfoForTool(toolName).message + "...";
|
||||
|
||||
private void TouchLiveAgentProgressHints()
|
||||
{
|
||||
_lastAgentProgressEventAt = DateTime.UtcNow;
|
||||
}
|
||||
private void TouchLiveAgentProgressHints(string? runTab = null)
|
||||
=> TouchLastAgentProgressEventAt(runTab);
|
||||
|
||||
private void AgentProgressHintTimer_Tick(object? sender, EventArgs e)
|
||||
{
|
||||
@@ -326,14 +324,15 @@ public partial class ChatWindow
|
||||
if (!string.Equals(runTab, "Cowork", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
UpdateLiveAgentProgressHint(null);
|
||||
UpdateLiveAgentProgressHint(null, runTab: runTab);
|
||||
return;
|
||||
}
|
||||
|
||||
var idle = DateTime.UtcNow - _lastAgentProgressEventAt;
|
||||
var idle = DateTime.UtcNow - GetLastAgentProgressEventAt(runTab);
|
||||
TryGetStreamingElapsed(out var elapsed);
|
||||
var lastProgressEvent = _currentRunProgressSteps.Count > 0
|
||||
? _currentRunProgressSteps[^1]
|
||||
var progressSteps = GetRunProgressSteps(runTab);
|
||||
var lastProgressEvent = progressSteps.Count > 0
|
||||
? progressSteps[^1]
|
||||
: null;
|
||||
var idleNarrative = AgentStatusNarrativeCatalog.BuildIdle(
|
||||
lastProgressEvent,
|
||||
@@ -387,20 +386,22 @@ public partial class ChatWindow
|
||||
subItemCategory: idleNarrative.Category);
|
||||
}
|
||||
|
||||
UpdateLiveAgentProgressHint(summary, toolName);
|
||||
UpdateLiveAgentProgressHint(summary, toolName, runTab);
|
||||
}
|
||||
|
||||
private void UpdateLiveAgentProgressHint(string? summary, string toolName = "")
|
||||
private void UpdateLiveAgentProgressHint(string? summary, string toolName = "", string? runTab = null)
|
||||
{
|
||||
var tabKey = NormalizeRunTabKey(runTab);
|
||||
var normalizedSummary = string.IsNullOrWhiteSpace(summary) ? null : summary.Trim();
|
||||
var currentSummary = _liveAgentProgressHint?.Summary;
|
||||
var currentToolName = _liveAgentProgressHint?.ToolName ?? "";
|
||||
var currentHint = GetLiveAgentProgressHintState(tabKey);
|
||||
var currentSummary = currentHint?.Summary;
|
||||
var currentToolName = currentHint?.ToolName ?? "";
|
||||
var elapsedMs = _isStreaming ? GetStreamingElapsedMsOrZero() : 0L;
|
||||
var inputTokens = (long)Math.Max(0, _tabCumulativeInputTokens.GetValueOrDefault(_activeTab));
|
||||
var outputTokens = (long)Math.Max(0, _tabCumulativeOutputTokens.GetValueOrDefault(_activeTab));
|
||||
var currentElapsedBucket = (_liveAgentProgressHint?.ElapsedMs ?? 0) / 3000;
|
||||
var inputTokens = (long)Math.Max(0, _tabCumulativeInputTokens.GetValueOrDefault(tabKey));
|
||||
var outputTokens = (long)Math.Max(0, _tabCumulativeOutputTokens.GetValueOrDefault(tabKey));
|
||||
var currentElapsedBucket = (currentHint?.ElapsedMs ?? 0) / 3000;
|
||||
var nextElapsedBucket = elapsedMs / 3000;
|
||||
var currentTokenBucket = ((_liveAgentProgressHint?.InputTokens ?? 0) + (_liveAgentProgressHint?.OutputTokens ?? 0)) / 500;
|
||||
var currentTokenBucket = ((currentHint?.InputTokens ?? 0) + (currentHint?.OutputTokens ?? 0)) / 500;
|
||||
var nextTokenBucket = (inputTokens + outputTokens) / 500;
|
||||
if (string.Equals(currentSummary, normalizedSummary, StringComparison.Ordinal)
|
||||
&& string.Equals(currentToolName, toolName, StringComparison.Ordinal)
|
||||
@@ -408,51 +409,56 @@ public partial class ChatWindow
|
||||
&& currentTokenBucket == nextTokenBucket)
|
||||
return;
|
||||
|
||||
_liveAgentProgressHint = normalizedSummary == null
|
||||
? null
|
||||
: new AgentEvent
|
||||
{
|
||||
Timestamp = DateTime.Now,
|
||||
RunId = _appState.AgentRun.RunId,
|
||||
Type = AgentEventType.Thinking,
|
||||
ToolName = toolName,
|
||||
Summary = normalizedSummary,
|
||||
ElapsedMs = elapsedMs,
|
||||
InputTokens = (int)inputTokens,
|
||||
OutputTokens = (int)outputTokens,
|
||||
};
|
||||
var currentRun = GetCurrentAgentRunState(tabKey);
|
||||
SetLiveAgentProgressHintState(
|
||||
tabKey,
|
||||
normalizedSummary == null
|
||||
? null
|
||||
: new AgentEvent
|
||||
{
|
||||
Timestamp = DateTime.Now,
|
||||
RunId = currentRun?.RunId ?? "",
|
||||
Type = AgentEventType.Thinking,
|
||||
ToolName = toolName,
|
||||
Summary = normalizedSummary,
|
||||
ElapsedMs = elapsedMs,
|
||||
InputTokens = (int)inputTokens,
|
||||
OutputTokens = (int)outputTokens,
|
||||
Iteration = currentRun?.LastIteration ?? 0,
|
||||
});
|
||||
|
||||
// 스트리밍 중 프로그레스 힌트 변경으로 인한 ScheduleExecutionHistoryRender 호출 차단
|
||||
// 힌트 업데이트만으로 전체 트랜스크립트를 재렌더하면 UI 스레드 멈춤 발생
|
||||
// 타이머가 자연스럽게 다음 렌더 사이클에서 힌트를 반영함
|
||||
}
|
||||
|
||||
private AgentEvent? GetLiveAgentProgressHint()
|
||||
private AgentEvent? GetLiveAgentProgressHint(string? runTab = null)
|
||||
{
|
||||
if (_liveAgentProgressHint == null)
|
||||
var tabKey = NormalizeRunTabKey(runTab);
|
||||
var hint = GetLiveAgentProgressHintState(tabKey);
|
||||
if (hint == null)
|
||||
return null;
|
||||
|
||||
var runTab = string.IsNullOrWhiteSpace(_streamRunTab) ? _activeTab : _streamRunTab!;
|
||||
if (!string.Equals(runTab, _activeTab, StringComparison.OrdinalIgnoreCase))
|
||||
if (!string.Equals(tabKey, _activeTab, StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
return new AgentEvent
|
||||
{
|
||||
Timestamp = _liveAgentProgressHint.Timestamp,
|
||||
RunId = _liveAgentProgressHint.RunId,
|
||||
Type = _liveAgentProgressHint.Type,
|
||||
ToolName = _liveAgentProgressHint.ToolName,
|
||||
Summary = _liveAgentProgressHint.Summary,
|
||||
FilePath = _liveAgentProgressHint.FilePath,
|
||||
Success = _liveAgentProgressHint.Success,
|
||||
StepCurrent = _liveAgentProgressHint.StepCurrent,
|
||||
StepTotal = _liveAgentProgressHint.StepTotal,
|
||||
Steps = _liveAgentProgressHint.Steps,
|
||||
ElapsedMs = _liveAgentProgressHint.ElapsedMs,
|
||||
InputTokens = _liveAgentProgressHint.InputTokens,
|
||||
OutputTokens = _liveAgentProgressHint.OutputTokens,
|
||||
ToolInput = _liveAgentProgressHint.ToolInput,
|
||||
Iteration = _liveAgentProgressHint.Iteration,
|
||||
Timestamp = hint.Timestamp,
|
||||
RunId = hint.RunId,
|
||||
Type = hint.Type,
|
||||
ToolName = hint.ToolName,
|
||||
Summary = hint.Summary,
|
||||
FilePath = hint.FilePath,
|
||||
Success = hint.Success,
|
||||
StepCurrent = hint.StepCurrent,
|
||||
StepTotal = hint.StepTotal,
|
||||
Steps = hint.Steps,
|
||||
ElapsedMs = hint.ElapsedMs,
|
||||
InputTokens = hint.InputTokens,
|
||||
OutputTokens = hint.OutputTokens,
|
||||
ToolInput = hint.ToolInput,
|
||||
Iteration = hint.Iteration,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -361,6 +361,7 @@ public partial class ChatWindow
|
||||
lock (_convLock)
|
||||
currentConversationId = _currentConversation?.Id ?? "";
|
||||
var streamingConversationId = GetStreamingConversation(_activeTab)?.Id;
|
||||
var activeRun = GetCurrentAgentRunState(_activeTab);
|
||||
|
||||
var items = metas.Select(c =>
|
||||
{
|
||||
@@ -384,8 +385,8 @@ public partial class ChatWindow
|
||||
var isRunning = ShouldShowConversationRunningIndicator(
|
||||
c.Id,
|
||||
streamingConversationId,
|
||||
_appState.AgentRun.RunId,
|
||||
_appState.AgentRun.Status);
|
||||
activeRun?.RunId,
|
||||
activeRun?.Status);
|
||||
|
||||
return new ConversationMeta
|
||||
{
|
||||
|
||||
@@ -49,7 +49,8 @@ public partial class ChatWindow
|
||||
lock (_convLock) currentConversation = _currentConversation;
|
||||
AddTaskSummaryObservabilitySections(panel, currentConversation);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_appState.AgentRun.RunId))
|
||||
var activeRun = GetCurrentAgentRunState(_activeTab);
|
||||
if (!string.IsNullOrWhiteSpace(activeRun?.RunId))
|
||||
{
|
||||
var currentRun = new Border
|
||||
{
|
||||
@@ -65,21 +66,21 @@ public partial class ChatWindow
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = $"실행 run {ShortRunId(_appState.AgentRun.RunId)}",
|
||||
Text = $"실행 run {ShortRunId(activeRun!.RunId)}",
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
FontSize = 9.75,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = $"{GetRunStatusLabel(_appState.AgentRun.Status)} · step {_appState.AgentRun.LastIteration}",
|
||||
Text = $"{GetRunStatusLabel(activeRun!.Status)} · step {activeRun.LastIteration}",
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
Foreground = GetRunStatusBrush(_appState.AgentRun.Status),
|
||||
Foreground = GetRunStatusBrush(activeRun.Status),
|
||||
FontSize = 9,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = string.IsNullOrWhiteSpace(_appState.AgentRun.Summary) ? "요약 없음" : _appState.AgentRun.Summary,
|
||||
Text = string.IsNullOrWhiteSpace(activeRun!.Summary) ? "요약 없음" : activeRun.Summary,
|
||||
Margin = new Thickness(0, 3, 0, 0),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Foreground = Brushes.DimGray,
|
||||
|
||||
@@ -154,7 +154,9 @@ public partial class ChatWindow
|
||||
|
||||
// 현재 실행 중인 run의 process feed 이벤트를 통합 카드로 대체 (히스토리 접힘 모드)
|
||||
var showFullHistory = _currentConversation?.ShowExecutionHistory ?? true;
|
||||
var activeRunId = _isStreaming ? (_appState.AgentRun.RunId ?? "") : "";
|
||||
var activeRunId = _isStreaming
|
||||
? (GetCurrentAgentRunState(_activeTab)?.RunId ?? "")
|
||||
: "";
|
||||
|
||||
var eventIndex = 0;
|
||||
string? prevToolCallName = null;
|
||||
@@ -224,9 +226,10 @@ public partial class ChatWindow
|
||||
FlushProcessFeedGroup();
|
||||
|
||||
// 스트리밍 중 + 히스토리 접힘: 통합 진행 카드 삽입 (개별 pill 대체)
|
||||
if (!showFullHistory && _isStreaming && _currentRunProgressSteps.Count > 0)
|
||||
var currentRunProgressSteps = GetRunProgressSteps(_activeTab);
|
||||
if (!showFullHistory && _isStreaming && currentRunProgressSteps.Count > 0)
|
||||
{
|
||||
var capturedSteps = _currentRunProgressSteps.ToList();
|
||||
var capturedSteps = currentRunProgressSteps.ToList();
|
||||
var cardTimestamp = capturedSteps[^1].Timestamp;
|
||||
// 통합 진행 카드는 매번 내용이 바뀌므로 volatile 키 사용
|
||||
timeline.Add(("_live_progress", cardTimestamp, 1, () => AddLiveRunProgressCard(capturedSteps)));
|
||||
|
||||
@@ -411,7 +411,7 @@ public partial class ChatWindow
|
||||
if (_v2LiveStatusText == null)
|
||||
return;
|
||||
|
||||
var hint = GetLiveAgentProgressHint();
|
||||
var hint = GetLiveAgentProgressHint(runTab);
|
||||
if (hint != null)
|
||||
{
|
||||
var narrative = AgentStatusNarrativeCatalog.BuildFromEvent(hint, runTab);
|
||||
|
||||
@@ -87,8 +87,8 @@ public partial class ChatWindow : Window
|
||||
private Border? _userAskCard; // transcript 내 질문 카드
|
||||
private string? _pendingPlanSummary;
|
||||
private List<string> _pendingPlanSteps = new();
|
||||
// 현재 실행 중인 에이전트 스텝 누적 (통합 진행 카드용)
|
||||
private readonly List<AgentEvent> _currentRunProgressSteps = new();
|
||||
// 탭별 현재 실행 중인 에이전트 스텝 누적 (통합 진행 카드용)
|
||||
private readonly Dictionary<string, List<AgentEvent>> _tabRunProgressSteps = new(StringComparer.OrdinalIgnoreCase);
|
||||
private bool _userScrolled; // 사용자가 위로 스크롤했는지
|
||||
private long _lastScrollTick; // 스크롤 스로틀링용 (Environment.TickCount64)
|
||||
// 메시지 버블 캐시: messageId → 렌더링된 UIElement (재생성 방지)
|
||||
@@ -171,11 +171,157 @@ public partial class ChatWindow : Window
|
||||
private bool _pendingExecutionHistoryAutoScroll;
|
||||
private readonly Dictionary<string, ChatConversation> _pendingConversationPersists = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _expandedDraftQueueTabs = new(StringComparer.OrdinalIgnoreCase);
|
||||
private AgentEvent? _pendingAgentUiEvent;
|
||||
private AgentEvent? _liveAgentProgressHint;
|
||||
private DateTime _lastAgentProgressEventAt = DateTime.UtcNow;
|
||||
private readonly Dictionary<string, AgentEvent> _pendingAgentUiEvents = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, AgentEvent> _tabLiveAgentProgressHints = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, DateTime> _tabLastAgentProgressEventAt = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, AppStateService.AgentRunState> _tabAgentRunStates = new(StringComparer.OrdinalIgnoreCase);
|
||||
private double _lastResponsiveComposerWidth;
|
||||
private double _lastResponsiveMessageWidth;
|
||||
|
||||
private string NormalizeRunTabKey(string? tab)
|
||||
=> NormalizeTabName(string.IsNullOrWhiteSpace(tab) ? _activeTab : tab!);
|
||||
|
||||
private List<AgentEvent> GetRunProgressSteps(string? tab, bool ensure = false)
|
||||
{
|
||||
var key = NormalizeRunTabKey(tab);
|
||||
if (_tabRunProgressSteps.TryGetValue(key, out var existing))
|
||||
return existing;
|
||||
|
||||
if (!ensure)
|
||||
return [];
|
||||
|
||||
var created = new List<AgentEvent>();
|
||||
_tabRunProgressSteps[key] = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
private void ClearRunProgressSteps(string? tab)
|
||||
{
|
||||
var key = NormalizeRunTabKey(tab);
|
||||
_tabRunProgressSteps.Remove(key);
|
||||
}
|
||||
|
||||
private AgentEvent? GetPendingAgentUiEvent(string? tab)
|
||||
{
|
||||
var key = NormalizeRunTabKey(tab);
|
||||
return _pendingAgentUiEvents.TryGetValue(key, out var evt) ? evt : null;
|
||||
}
|
||||
|
||||
private AgentEvent? ConsumePendingAgentUiEvent(string? tab)
|
||||
{
|
||||
var key = NormalizeRunTabKey(tab);
|
||||
if (!_pendingAgentUiEvents.TryGetValue(key, out var evt))
|
||||
return null;
|
||||
|
||||
_pendingAgentUiEvents.Remove(key);
|
||||
return evt;
|
||||
}
|
||||
|
||||
private AgentEvent? GetLiveAgentProgressHintState(string? tab)
|
||||
{
|
||||
var key = NormalizeRunTabKey(tab);
|
||||
return _tabLiveAgentProgressHints.TryGetValue(key, out var evt) ? evt : null;
|
||||
}
|
||||
|
||||
private void SetLiveAgentProgressHintState(string? tab, AgentEvent? evt)
|
||||
{
|
||||
var key = NormalizeRunTabKey(tab);
|
||||
if (evt == null)
|
||||
{
|
||||
_tabLiveAgentProgressHints.Remove(key);
|
||||
return;
|
||||
}
|
||||
|
||||
_tabLiveAgentProgressHints[key] = evt;
|
||||
}
|
||||
|
||||
private DateTime GetLastAgentProgressEventAt(string? tab)
|
||||
{
|
||||
var key = NormalizeRunTabKey(tab);
|
||||
return _tabLastAgentProgressEventAt.TryGetValue(key, out var timestamp)
|
||||
? timestamp
|
||||
: DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private void TouchLastAgentProgressEventAt(string? tab)
|
||||
{
|
||||
var key = NormalizeRunTabKey(tab);
|
||||
_tabLastAgentProgressEventAt[key] = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private void ClearLiveAgentProgressState(string? tab)
|
||||
{
|
||||
var key = NormalizeRunTabKey(tab);
|
||||
_tabLastAgentProgressEventAt.Remove(key);
|
||||
_tabLiveAgentProgressHints.Remove(key);
|
||||
_pendingAgentUiEvents.Remove(key);
|
||||
_tabRunProgressSteps.Remove(key);
|
||||
_tabAgentRunStates.Remove(key);
|
||||
}
|
||||
|
||||
private AppStateService.AgentRunState? GetCurrentAgentRunState(string? tab)
|
||||
{
|
||||
var key = NormalizeRunTabKey(tab);
|
||||
if (_tabAgentRunStates.TryGetValue(key, out var local))
|
||||
return local;
|
||||
|
||||
return _appState.GetAgentRunForTab(key);
|
||||
}
|
||||
|
||||
private static bool IsTerminalAgentRunStatus(string? status)
|
||||
=> string.Equals(status, "completed", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(status, "failed", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(status, "paused", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(status, "canceled", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(status, "cancelled", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private void ApplyCurrentAgentRunState(string runTab, AgentEvent evt)
|
||||
{
|
||||
var key = NormalizeRunTabKey(runTab);
|
||||
if (!_tabAgentRunStates.TryGetValue(key, out var state))
|
||||
{
|
||||
state = new AppStateService.AgentRunState();
|
||||
_tabAgentRunStates[key] = state;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(evt.RunId)
|
||||
&& !string.Equals(state.RunId, evt.RunId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
state.RunId = evt.RunId;
|
||||
state.StartedAt = evt.Timestamp == default ? DateTime.Now : evt.Timestamp;
|
||||
}
|
||||
|
||||
switch (evt.Type)
|
||||
{
|
||||
case AgentEventType.Thinking:
|
||||
case AgentEventType.Planning:
|
||||
case AgentEventType.Resumed:
|
||||
state.Status = "running";
|
||||
state.Summary = string.IsNullOrWhiteSpace(evt.Summary) ? "응답을 준비하는 중" : evt.Summary;
|
||||
state.LastIteration = evt.Iteration;
|
||||
state.UpdatedAt = evt.Timestamp == default ? DateTime.Now : evt.Timestamp;
|
||||
break;
|
||||
case AgentEventType.Paused:
|
||||
state.Status = "paused";
|
||||
state.Summary = string.IsNullOrWhiteSpace(evt.Summary) ? "에이전트 일시중지" : evt.Summary;
|
||||
state.LastIteration = evt.Iteration;
|
||||
state.UpdatedAt = evt.Timestamp == default ? DateTime.Now : evt.Timestamp;
|
||||
break;
|
||||
case AgentEventType.Complete:
|
||||
state.Status = "completed";
|
||||
state.Summary = string.IsNullOrWhiteSpace(evt.Summary) ? "작업 완료" : evt.Summary;
|
||||
state.LastIteration = evt.Iteration;
|
||||
state.UpdatedAt = evt.Timestamp == default ? DateTime.Now : evt.Timestamp;
|
||||
break;
|
||||
case AgentEventType.Error when string.IsNullOrWhiteSpace(evt.ToolName):
|
||||
state.Status = "failed";
|
||||
state.Summary = string.IsNullOrWhiteSpace(evt.Summary) ? "에이전트 실행 실패" : evt.Summary;
|
||||
state.LastIteration = evt.Iteration;
|
||||
state.UpdatedAt = evt.Timestamp == default ? DateTime.Now : evt.Timestamp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsLightweightLiveProgressMode(string? runTab = null)
|
||||
{
|
||||
var tab = string.IsNullOrWhiteSpace(runTab) ? (_streamRunTab ?? _activeTab) : runTab!;
|
||||
@@ -1564,15 +1710,19 @@ public partial class ChatWindow : Window
|
||||
|
||||
if (!shouldShowTopLevelGuide)
|
||||
{
|
||||
_agentProgressHintTimer.Stop();
|
||||
RemoveAgentLiveCard(animated: false);
|
||||
HideStreamingStatusBar();
|
||||
HideStickyProgress();
|
||||
}
|
||||
else if (IsAgentLiveCardEligibleTab(_activeTab))
|
||||
{
|
||||
if (!_agentProgressHintTimer.IsEnabled)
|
||||
_agentProgressHintTimer.Start();
|
||||
EnsureAgentLiveCardVisible(_activeTab);
|
||||
if (_pendingAgentUiEvent != null)
|
||||
UpdateAgentLiveCardV2(_pendingAgentUiEvent);
|
||||
var pendingUiEvent = GetPendingAgentUiEvent(_activeTab);
|
||||
if (pendingUiEvent != null)
|
||||
UpdateAgentLiveCardV2(pendingUiEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6127,6 +6277,7 @@ public partial class ChatWindow : Window
|
||||
doneCts.Dispose();
|
||||
_streamRunTab = _streamingTabs.Count > 0 ? _streamingTabs.First() : null;
|
||||
ClearStreamingConversation(tab, finishedConversation);
|
||||
ClearLiveAgentProgressState(tab);
|
||||
|
||||
// Cowork/Code 탭 작업 완료 시, 앱이 포커스 없으면 Windows 알림
|
||||
NotifyTaskCompletionIfBackground(tab, finishedConversation, previewContent);
|
||||
@@ -6136,7 +6287,7 @@ public partial class ChatWindow : Window
|
||||
{
|
||||
ViewModel.IsStreaming = false; // ViewModel 상태 동기화 (CanSend 등 파생 프로퍼티 갱신)
|
||||
FlushPendingConversationPersists();
|
||||
FlushPendingAgentUiEvent();
|
||||
FlushPendingAgentUiEvent(_activeTab);
|
||||
StopLiveAgentProgressHints();
|
||||
_cursorTimer.Stop();
|
||||
_elapsedTimer.Stop();
|
||||
@@ -6168,6 +6319,11 @@ public partial class ChatWindow : Window
|
||||
UpdateResponsiveChatLayout();
|
||||
}
|
||||
}
|
||||
else if (string.Equals(tab, _activeTab, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
FlushPendingAgentUiEvent(_activeTab);
|
||||
StopLiveAgentProgressHints(tab);
|
||||
}
|
||||
|
||||
RefreshStreamingControlsForActiveTab();
|
||||
|
||||
@@ -6313,7 +6469,7 @@ public partial class ChatWindow : Window
|
||||
var streamToken = streamCts.Token;
|
||||
Services.LogService.Info($"[Exec] ExecutePreparedTurnAsync 진입: tab={runTab}, mode={preparedExecution.Mode}");
|
||||
RefreshStreamingControlsForActiveTab();
|
||||
StartLiveAgentProgressHints();
|
||||
StartLiveAgentProgressHints(runTab);
|
||||
UpdateStreamMetricsLabel();
|
||||
ForceScrollToEnd();
|
||||
|
||||
@@ -6398,7 +6554,7 @@ public partial class ChatWindow : Window
|
||||
}
|
||||
|
||||
responseElapsedMs = GetStreamingElapsedMsOrZero();
|
||||
assistantMetaRunId = _appState.AgentRun.RunId;
|
||||
assistantMetaRunId = GetCurrentAgentRunState(runTab)?.RunId;
|
||||
var usage = _llm.LastTokenUsage;
|
||||
if (usage != null)
|
||||
{
|
||||
@@ -6610,9 +6766,9 @@ public partial class ChatWindow : Window
|
||||
_conversationPersistTimer.Start();
|
||||
}
|
||||
|
||||
private void ScheduleAgentUiEvent(AgentEvent evt)
|
||||
private void ScheduleAgentUiEvent(string runTab, AgentEvent evt)
|
||||
{
|
||||
_pendingAgentUiEvent = evt;
|
||||
_pendingAgentUiEvents[NormalizeRunTabKey(runTab)] = evt;
|
||||
if (IsBackgroundUiThrottleActive())
|
||||
{
|
||||
_pendingBackgroundAgentUiEventFlush = true;
|
||||
@@ -6637,10 +6793,9 @@ public partial class ChatWindow : Window
|
||||
return !IsActive;
|
||||
}
|
||||
|
||||
private void FlushPendingAgentUiEvent()
|
||||
private void FlushPendingAgentUiEvent(string? runTab = null)
|
||||
{
|
||||
var evt = _pendingAgentUiEvent;
|
||||
_pendingAgentUiEvent = null;
|
||||
var evt = ConsumePendingAgentUiEvent(runTab);
|
||||
if (evt == null)
|
||||
return;
|
||||
|
||||
@@ -6816,9 +6971,10 @@ public partial class ChatWindow : Window
|
||||
|
||||
private void OnAgentEvent(AgentEvent evt, string runTab)
|
||||
{
|
||||
TouchLiveAgentProgressHints();
|
||||
TouchLiveAgentProgressHints(runTab);
|
||||
var eventTab = runTab;
|
||||
var normalizedRunTab = NormalizeTabName(runTab);
|
||||
ApplyCurrentAgentRunState(normalizedRunTab, evt);
|
||||
var isOwningActiveTab = string.Equals(normalizedRunTab, _activeTab, StringComparison.OrdinalIgnoreCase)
|
||||
&& _streamingTabs.Contains(normalizedRunTab);
|
||||
var guideVisibility = ChatStreamingUiPolicy.ResolveGuideVisibility(
|
||||
@@ -6854,11 +7010,12 @@ public partial class ChatWindow : Window
|
||||
}
|
||||
|
||||
// 현재 실행 중인 스텝 추적 (통합 진행 카드용)
|
||||
if (IsProcessFeedEvent(evt) && shouldShowTopLevelGuide)
|
||||
if (IsProcessFeedEvent(evt) && _streamingTabs.Contains(normalizedRunTab))
|
||||
{
|
||||
_currentRunProgressSteps.Add(evt);
|
||||
if (_currentRunProgressSteps.Count > 8)
|
||||
_currentRunProgressSteps.RemoveAt(0);
|
||||
var progressSteps = GetRunProgressSteps(normalizedRunTab, ensure: true);
|
||||
progressSteps.Add(evt);
|
||||
if (progressSteps.Count > 8)
|
||||
progressSteps.RemoveAt(0);
|
||||
}
|
||||
|
||||
// ── 2단계: 무거운 작업을 백그라운드 큐로 위임 ──────────────────────────
|
||||
@@ -6877,7 +7034,7 @@ public partial class ChatWindow : Window
|
||||
EnqueueAgentEventWork(evt, eventTab, shouldRender);
|
||||
|
||||
// ── 3단계: 경량 상태 추적 (UI 스레드) ───────────────────────────────
|
||||
_appState.ApplyAgentEvent(evt);
|
||||
_appState.ApplyAgentEvent(evt, normalizedRunTab);
|
||||
|
||||
// 탭별 토큰 누적 — 활성 탭 것만 하단 바에 표시
|
||||
if (evt.InputTokens > 0 || evt.OutputTokens > 0)
|
||||
@@ -6889,7 +7046,7 @@ public partial class ChatWindow : Window
|
||||
}
|
||||
|
||||
if (shouldShowTopLevelGuide)
|
||||
ScheduleAgentUiEvent(evt);
|
||||
ScheduleAgentUiEvent(normalizedRunTab, evt);
|
||||
if (!lightweightLiveMode
|
||||
|| evt.Type is AgentEventType.Complete
|
||||
or AgentEventType.Error
|
||||
@@ -6901,33 +7058,43 @@ public partial class ChatWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
private void StartLiveAgentProgressHints()
|
||||
private void StartLiveAgentProgressHints(string? runTab = null)
|
||||
{
|
||||
_lastAgentProgressEventAt = DateTime.UtcNow;
|
||||
_currentRunProgressSteps.Clear();
|
||||
var runTab = string.IsNullOrWhiteSpace(_streamRunTab) ? _activeTab : _streamRunTab!;
|
||||
if (IsAgentLiveCardEligibleTab(runTab))
|
||||
var normalizedRunTab = NormalizeRunTabKey(runTab ?? _streamRunTab ?? _activeTab);
|
||||
TouchLastAgentProgressEventAt(normalizedRunTab);
|
||||
ClearRunProgressSteps(normalizedRunTab);
|
||||
SetLiveAgentProgressHintState(normalizedRunTab, null);
|
||||
if (IsAgentLiveCardEligibleTab(normalizedRunTab))
|
||||
{
|
||||
EnsureAgentLiveCardVisible(runTab);
|
||||
var initialStatus = AgentStatusNarrativeCatalog.BuildInitial(runTab);
|
||||
UpdateLiveAgentProgressHint(initialStatus.Message, "agent_wait");
|
||||
EnsureAgentLiveCardVisible(normalizedRunTab);
|
||||
var initialStatus = AgentStatusNarrativeCatalog.BuildInitial(normalizedRunTab);
|
||||
UpdateLiveAgentProgressHint(initialStatus.Message, "agent_wait", normalizedRunTab);
|
||||
ShowStreamingStatusBar(initialStatus.Message, detail: initialStatus.Detail);
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveAgentLiveCard(animated: false);
|
||||
UpdateLiveAgentProgressHint(null);
|
||||
UpdateLiveAgentProgressHint(null, runTab: normalizedRunTab);
|
||||
ShowStreamingStatusBar("생각하는 중...");
|
||||
}
|
||||
_agentProgressHintTimer.Stop();
|
||||
_agentProgressHintTimer.Start();
|
||||
}
|
||||
|
||||
private void StopLiveAgentProgressHints()
|
||||
private void StopLiveAgentProgressHints(string? runTab = null)
|
||||
{
|
||||
_agentProgressHintTimer.Stop();
|
||||
UpdateLiveAgentProgressHint(null);
|
||||
HideStreamingStatusBar();
|
||||
if (string.IsNullOrWhiteSpace(runTab))
|
||||
{
|
||||
foreach (var key in _tabLiveAgentProgressHints.Keys.ToList())
|
||||
SetLiveAgentProgressHintState(key, null);
|
||||
HideStreamingStatusBar();
|
||||
return;
|
||||
}
|
||||
|
||||
SetLiveAgentProgressHintState(runTab, null);
|
||||
if (string.Equals(NormalizeRunTabKey(runTab), _activeTab, StringComparison.OrdinalIgnoreCase))
|
||||
HideStreamingStatusBar();
|
||||
}
|
||||
|
||||
// ─── 프롬프트 템플릿 팝업 ────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user