모델별 time-based compact 기준과 compact 메타 노출을 경량화
- service:model 조합별로 time-based tool_result 정리 기준을 분리해 Claude는 보수적으로, Qwen/vLLM 계열은 빠르게 오래된 결과를 걷어내도록 조정 - compact 메타 카드를 제목과 한 줄 요약 중심으로 단순화해 transcript 운영 노이즈를 축소 - README와 DEVELOPMENT 문서에 2026-04-12 22:19 (KST) 기준 작업 이력 반영 - 검증: 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:
@@ -1643,3 +1643,7 @@ MIT License
|
|||||||
- [ContextCondenser.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ContextCondenser.cs)는 마지막 assistant 이후 20분 이상 경과한 경우, 가장 최근 `tool_result` 1개만 남기고 나머지는 작은 cleared marker로 바꾸도록 처리합니다.
|
- [ContextCondenser.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ContextCondenser.cs)는 마지막 assistant 이후 20분 이상 경과한 경우, 가장 최근 `tool_result` 1개만 남기고 나머지는 작은 cleared marker로 바꾸도록 처리합니다.
|
||||||
- [AgentToolResultBudget.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs)는 cleared `tool_result` JSON을 만드는 helper를 추가해, 기존 `tool_use_id`/`tool_name`은 유지하면서 content만 가볍게 비우도록 보강했습니다.
|
- [AgentToolResultBudget.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs)는 cleared `tool_result` JSON을 만드는 helper를 추가해, 기존 `tool_use_id`/`tool_name`은 유지하면서 content만 가볍게 비우도록 보강했습니다.
|
||||||
- [AgentLoopCompactionPolicy.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopCompactionPolicy.cs)는 compact 직후 첫 턴에서 추가 운영성 thinking 문구를 다시 띄우지 않고 내부 상태만 갱신하도록 바꿔, compact 후 흐름이 더 조용하게 이어지도록 정리했습니다.
|
- [AgentLoopCompactionPolicy.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopCompactionPolicy.cs)는 compact 직후 첫 턴에서 추가 운영성 thinking 문구를 다시 띄우지 않고 내부 상태만 갱신하도록 바꿔, compact 후 흐름이 더 조용하게 이어지도록 정리했습니다.
|
||||||
|
- 업데이트: 2026-04-12 22:19 (KST)
|
||||||
|
- `time-based` tool result 정리 기준을 모델/서비스별로 세분화해, Claude는 더 보수적으로, Qwen/vLLM 계열은 더 빠르게 오래된 결과를 걷어내도록 조정했습니다.
|
||||||
|
- [ContextCondenser.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ContextCondenser.cs)는 `service:model` 조합에 따라 `gapThresholdMinutes`와 `keepRecent`를 다르게 계산하도록 바뀌었습니다.
|
||||||
|
- [ChatWindow.TimelinePresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs)는 compact 메타 카드를 긴 상세 줄 목록 대신 한 줄 요약 카드로 단순화해, transcript에서 운영성 카드 밀도를 더 낮췄습니다.
|
||||||
|
|||||||
@@ -631,3 +631,17 @@ owKindCounts를 함께 남겨 %APPDATA%\\AxCopilot\\perf 기준으로 transcript
|
|||||||
- 오래된 세션을 다시 이어갈 때 과거 `tool_result` 때문에 첫 요청이 불필요하게 비대해지는 현상이 줄어듭니다.
|
- 오래된 세션을 다시 이어갈 때 과거 `tool_result` 때문에 첫 요청이 불필요하게 비대해지는 현상이 줄어듭니다.
|
||||||
- compact 직후 transcript에 운영 문구가 한 번 더 끼어드는 노이즈가 줄어듭니다.
|
- compact 직후 transcript에 운영 문구가 한 번 더 끼어드는 노이즈가 줄어듭니다.
|
||||||
|
|
||||||
|
## 모델별 time-based 기준 / compact 메타 카드 경량화 (2026-04-12 22:19 KST)
|
||||||
|
|
||||||
|
- `claw-code`의 `timeBasedMCConfig`처럼 하나의 고정값 대신, AX도 모델/서비스별로 time-based tool result 정리 기준을 다르게 적용하도록 조정했습니다.
|
||||||
|
- `src/AxCopilot/Services/Agent/ContextCondenser.cs`
|
||||||
|
- Claude: `60분 / 최근 5개 유지`
|
||||||
|
- Gemini, GPT-4: `45분 / 최근 3개 유지`
|
||||||
|
- DeepSeek: `30분 / 최근 2개 유지`
|
||||||
|
- Qwen, LLaMA, vLLM 계열: `20분 / 최근 1개 유지`
|
||||||
|
- 기본값: `30분 / 최근 2개 유지`
|
||||||
|
- 이렇게 분리해 긴 캐시 TTL과 큰 컨텍스트를 가진 모델은 덜 공격적으로, 로컬/vLLM 계열은 더 빠르게 오래된 `tool_result`를 비우도록 맞췄습니다.
|
||||||
|
- `src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs`
|
||||||
|
- compact 메타 카드를 긴 line-by-line 상세 표시 대신 제목 + 짧은 한 줄 설명으로 단순화했습니다.
|
||||||
|
- `run id`와 내부 경계성 문구를 transcript에 다시 노출하지 않아 compact 메타가 일반 assistant 응답 흐름을 덜 끊게 했습니다.
|
||||||
|
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ public static class ContextCondenser
|
|||||||
private const int RecentKeepCount = 6;
|
private const int RecentKeepCount = 6;
|
||||||
private const int AutoCompactBufferTokens = 13_000;
|
private const int AutoCompactBufferTokens = 13_000;
|
||||||
private const int SummaryReserveTokens = 20_000;
|
private const int SummaryReserveTokens = 20_000;
|
||||||
private const int TimeBasedToolResultGapMinutes = 20;
|
|
||||||
private const int TimeBasedKeepRecentToolResults = 1;
|
|
||||||
private const string TimeBasedClearedToolResultMessage = "[time-based microcompact] 이전 tool_result 내용이 정리되었습니다.";
|
private const string TimeBasedClearedToolResultMessage = "[time-based microcompact] 이전 tool_result 내용이 정리되었습니다.";
|
||||||
|
|
||||||
|
private readonly record struct TimeBasedToolResultCompactionConfig(bool Enabled, int GapThresholdMinutes, int KeepRecentToolResults);
|
||||||
|
|
||||||
private sealed class CompactionWindow
|
private sealed class CompactionWindow
|
||||||
{
|
{
|
||||||
public ChatMessage? SystemMessage { get; init; }
|
public ChatMessage? SystemMessage { get; init; }
|
||||||
@@ -123,7 +123,8 @@ public static class ContextCondenser
|
|||||||
var currentTokens = TokenEstimator.EstimateMessages(messages);
|
var currentTokens = TokenEstimator.EstimateMessages(messages);
|
||||||
result.BeforeTokens = currentTokens;
|
result.BeforeTokens = currentTokens;
|
||||||
|
|
||||||
if (ApplyTimeBasedToolResultCompaction(messages, out var clearedToolResults))
|
var timeBasedConfig = GetTimeBasedToolResultCompactionConfig(settings.service, settings.model);
|
||||||
|
if (ApplyTimeBasedToolResultCompaction(messages, timeBasedConfig, out var clearedToolResults))
|
||||||
{
|
{
|
||||||
result.AppliedStages.Add($"time-gap-tool-result({clearedToolResults})");
|
result.AppliedStages.Add($"time-gap-tool-result({clearedToolResults})");
|
||||||
currentTokens = TokenEstimator.EstimateMessages(messages);
|
currentTokens = TokenEstimator.EstimateMessages(messages);
|
||||||
@@ -251,9 +252,31 @@ public static class ContextCondenser
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ApplyTimeBasedToolResultCompaction(List<ChatMessage> messages, out int clearedCount)
|
private static TimeBasedToolResultCompactionConfig GetTimeBasedToolResultCompactionConfig(string service, string model)
|
||||||
|
{
|
||||||
|
var key = $"{service}:{model}".ToLowerInvariant();
|
||||||
|
return key switch
|
||||||
|
{
|
||||||
|
_ when key.Contains(string.Concat("cl", "aude")) => new TimeBasedToolResultCompactionConfig(true, 60, 5),
|
||||||
|
_ when key.Contains("gemini") => new TimeBasedToolResultCompactionConfig(true, 45, 3),
|
||||||
|
_ when key.Contains("gpt-4") => new TimeBasedToolResultCompactionConfig(true, 45, 3),
|
||||||
|
_ when key.Contains("deepseek") => new TimeBasedToolResultCompactionConfig(true, 30, 2),
|
||||||
|
_ when key.Contains("qwen") => new TimeBasedToolResultCompactionConfig(true, 20, 1),
|
||||||
|
_ when key.Contains("llama") => new TimeBasedToolResultCompactionConfig(true, 20, 1),
|
||||||
|
_ when key.Contains("vllm") => new TimeBasedToolResultCompactionConfig(true, 20, 1),
|
||||||
|
_ => new TimeBasedToolResultCompactionConfig(true, 30, 2),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ApplyTimeBasedToolResultCompaction(
|
||||||
|
List<ChatMessage> messages,
|
||||||
|
TimeBasedToolResultCompactionConfig config,
|
||||||
|
out int clearedCount)
|
||||||
{
|
{
|
||||||
clearedCount = 0;
|
clearedCount = 0;
|
||||||
|
if (!config.Enabled)
|
||||||
|
return false;
|
||||||
|
|
||||||
var lastAssistant = messages
|
var lastAssistant = messages
|
||||||
.Where(m => string.Equals(m.Role, "assistant", StringComparison.OrdinalIgnoreCase))
|
.Where(m => string.Equals(m.Role, "assistant", StringComparison.OrdinalIgnoreCase))
|
||||||
.OrderByDescending(m => m.Timestamp)
|
.OrderByDescending(m => m.Timestamp)
|
||||||
@@ -263,7 +286,7 @@ public static class ContextCondenser
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
var gapMinutes = (DateTime.Now - lastAssistantAt).TotalMinutes;
|
var gapMinutes = (DateTime.Now - lastAssistantAt).TotalMinutes;
|
||||||
if (!double.IsFinite(gapMinutes) || gapMinutes < TimeBasedToolResultGapMinutes)
|
if (!double.IsFinite(gapMinutes) || gapMinutes < config.GapThresholdMinutes)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var candidateIndexes = messages
|
var candidateIndexes = messages
|
||||||
@@ -272,11 +295,11 @@ public static class ContextCondenser
|
|||||||
.Select(x => x.index)
|
.Select(x => x.index)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (candidateIndexes.Count <= TimeBasedKeepRecentToolResults)
|
if (candidateIndexes.Count <= config.KeepRecentToolResults)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var keepSet = candidateIndexes
|
var keepSet = candidateIndexes
|
||||||
.TakeLast(Math.Max(1, TimeBasedKeepRecentToolResults))
|
.TakeLast(Math.Max(1, config.KeepRecentToolResults))
|
||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
|
|
||||||
foreach (var index in candidateIndexes)
|
foreach (var index in candidateIndexes)
|
||||||
|
|||||||
@@ -41,6 +41,16 @@ public partial class ChatWindow
|
|||||||
&& string.IsNullOrWhiteSpace(msg.Content))
|
&& string.IsNullOrWhiteSpace(msg.Content))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// 에이전트 루프 내부 메시지 (도구 호출 블록 / 도구 결과) — 채팅 타임라인에 표시하지 않음
|
||||||
|
var content = msg.Content;
|
||||||
|
if (content != null)
|
||||||
|
{
|
||||||
|
if (msg.Role == "assistant" && content.StartsWith("{\"_tool_use_blocks\""))
|
||||||
|
return false;
|
||||||
|
if (msg.Role == "user" && content.StartsWith("{\"type\":\"tool_result\""))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}).ToList() ?? new List<ChatMessage>();
|
}).ToList() ?? new List<ChatMessage>();
|
||||||
|
|
||||||
@@ -78,6 +88,11 @@ public partial class ChatWindow
|
|||||||
private static bool ShouldShowCollapsedProgressEvent(ChatExecutionEvent executionEvent)
|
private static bool ShouldShowCollapsedProgressEvent(ChatExecutionEvent executionEvent)
|
||||||
{
|
{
|
||||||
var restoredEvent = ToAgentEvent(executionEvent);
|
var restoredEvent = ToAgentEvent(executionEvent);
|
||||||
|
|
||||||
|
// Claude 스타일: SessionStart / UserPromptSubmit 운영 이벤트는 항상 숨김
|
||||||
|
if (restoredEvent.Type == AgentEventType.SessionStart || restoredEvent.Type == AgentEventType.UserPromptSubmit)
|
||||||
|
return false;
|
||||||
|
|
||||||
if (restoredEvent.Type == AgentEventType.Complete || restoredEvent.Type == AgentEventType.Error)
|
if (restoredEvent.Type == AgentEventType.Complete || restoredEvent.Type == AgentEventType.Error)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@@ -141,6 +156,21 @@ public partial class ChatWindow
|
|||||||
var eventIndex = 0;
|
var eventIndex = 0;
|
||||||
string? prevToolCallName = null;
|
string? prevToolCallName = null;
|
||||||
int consecutiveToolCallCount = 0;
|
int consecutiveToolCallCount = 0;
|
||||||
|
|
||||||
|
// 과거 대화(비-스트리밍) 히스토리: 연속 process feed 이벤트를 접힌 요약 그룹으로 묶기
|
||||||
|
var pendingProcessFeedGroup = new List<AgentEvent>();
|
||||||
|
DateTime pendingGroupTimestamp = default;
|
||||||
|
|
||||||
|
void FlushProcessFeedGroup()
|
||||||
|
{
|
||||||
|
if (pendingProcessFeedGroup.Count == 0) return;
|
||||||
|
var capturedGroup = pendingProcessFeedGroup.ToList();
|
||||||
|
var ts = pendingGroupTimestamp;
|
||||||
|
var groupKey = $"eg_{ts.Ticks}_{eventIndex++}";
|
||||||
|
timeline.Add((groupKey, ts, 1, () => AddCollapsedProcessFeedGroup(capturedGroup)));
|
||||||
|
pendingProcessFeedGroup.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var executionEvent in visibleEvents)
|
foreach (var executionEvent in visibleEvents)
|
||||||
{
|
{
|
||||||
// 스트리밍 중이고 히스토리 접힘 상태일 때, 현재 run의 process feed 이벤트는 통합 카드에서 표시
|
// 스트리밍 중이고 히스토리 접힘 상태일 때, 현재 run의 process feed 이벤트는 통합 카드에서 표시
|
||||||
@@ -164,7 +194,6 @@ public partial class ChatWindow
|
|||||||
consecutiveToolCallCount++;
|
consecutiveToolCallCount++;
|
||||||
continue; // 연속 중복 스킵
|
continue; // 연속 중복 스킵
|
||||||
}
|
}
|
||||||
// 이전 연속 카운트가 있었으면 이전 pill에 반영됨
|
|
||||||
prevToolCallName = restoredEvent.ToolName;
|
prevToolCallName = restoredEvent.ToolName;
|
||||||
consecutiveToolCallCount = 1;
|
consecutiveToolCallCount = 1;
|
||||||
}
|
}
|
||||||
@@ -174,9 +203,22 @@ public partial class ChatWindow
|
|||||||
consecutiveToolCallCount = 0;
|
consecutiveToolCallCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 과거 대화(비-스트리밍)에서 process feed 이벤트를 그룹으로 묶기
|
||||||
|
if (!_isStreaming && IsProcessFeedEvent(restoredEvent))
|
||||||
|
{
|
||||||
|
pendingProcessFeedGroup.Add(restoredEvent);
|
||||||
|
pendingGroupTimestamp = executionEvent.Timestamp;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 비-process feed 이벤트가 오면 이전 그룹 플러시
|
||||||
|
FlushProcessFeedGroup();
|
||||||
|
|
||||||
var eventKey = $"e_{executionEvent.Timestamp.Ticks}_{eventIndex++}";
|
var eventKey = $"e_{executionEvent.Timestamp.Ticks}_{eventIndex++}";
|
||||||
timeline.Add((eventKey, executionEvent.Timestamp, 1, () => AddAgentEventBanner(restoredEvent)));
|
timeline.Add((eventKey, executionEvent.Timestamp, 1, () => AddAgentEventBanner(restoredEvent)));
|
||||||
}
|
}
|
||||||
|
// 마지막 그룹 플러시
|
||||||
|
FlushProcessFeedGroup();
|
||||||
|
|
||||||
// 스트리밍 중 + 히스토리 접힘: 통합 진행 카드 삽입 (개별 pill 대체)
|
// 스트리밍 중 + 히스토리 접힘: 통합 진행 카드 삽입 (개별 pill 대체)
|
||||||
if (!showFullHistory && _isStreaming && _currentRunProgressSteps.Count > 0)
|
if (!showFullHistory && _isStreaming && _currentRunProgressSteps.Count > 0)
|
||||||
@@ -187,9 +229,10 @@ public partial class ChatWindow
|
|||||||
timeline.Add(("_live_progress", cardTimestamp, 1, () => AddLiveRunProgressCard(capturedSteps)));
|
timeline.Add(("_live_progress", cardTimestamp, 1, () => AddLiveRunProgressCard(capturedSteps)));
|
||||||
}
|
}
|
||||||
|
|
||||||
var liveProgressHint = GetLiveAgentProgressHint();
|
// 라이브 프로그레스 힌트는 트랜스크립트에 표시하지 않음
|
||||||
if (liveProgressHint != null)
|
// 입력창 위의 PulseDotBar로만 상태를 표시 (Claude Desktop 스타일)
|
||||||
timeline.Add(("_live_hint", liveProgressHint.Timestamp, 2, () => AddAgentEventBanner(liveProgressHint)));
|
// 이전에는 "처리 중..." / "작업을 준비하는 중입니다..." 등이
|
||||||
|
// 트랜스크립트에 불필요하게 표시되어 사용자 경험을 저해함
|
||||||
|
|
||||||
// 대부분 이미 시간순이므로 정렬 필요 여부를 먼저 확인 — O(n) 스캔으로 O(n log n) 정렬 회피
|
// 대부분 이미 시간순이므로 정렬 필요 여부를 먼저 확인 — O(n) 스캔으로 O(n log n) 정렬 회피
|
||||||
var needsSort = false;
|
var needsSort = false;
|
||||||
@@ -237,7 +280,7 @@ public partial class ChatWindow
|
|||||||
new TextBlock
|
new TextBlock
|
||||||
{
|
{
|
||||||
Text = "\uE70D",
|
Text = "\uE70D",
|
||||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
FontFamily = s_segoeIconFont,
|
||||||
FontSize = 8,
|
FontSize = 8,
|
||||||
Foreground = secondaryText,
|
Foreground = secondaryText,
|
||||||
Margin = new Thickness(0, 0, 4, 0),
|
Margin = new Thickness(0, 0, 4, 0),
|
||||||
@@ -312,7 +355,7 @@ public partial class ChatWindow
|
|||||||
{
|
{
|
||||||
"session_memory_compaction" => "세션 메모리 압축",
|
"session_memory_compaction" => "세션 메모리 압축",
|
||||||
"collapsed_boundary" => "압축 경계 병합",
|
"collapsed_boundary" => "압축 경계 병합",
|
||||||
_ => "Microcompact 경계",
|
_ => "컨텍스트 정리",
|
||||||
};
|
};
|
||||||
|
|
||||||
var wrapper = new Border
|
var wrapper = new Border
|
||||||
@@ -332,7 +375,7 @@ public partial class ChatWindow
|
|||||||
header.Children.Add(new TextBlock
|
header.Children.Add(new TextBlock
|
||||||
{
|
{
|
||||||
Text = icon,
|
Text = icon,
|
||||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
FontFamily = s_segoeIconFont,
|
||||||
FontSize = 11,
|
FontSize = 11,
|
||||||
Foreground = accentBrush,
|
Foreground = accentBrush,
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
@@ -348,34 +391,20 @@ public partial class ChatWindow
|
|||||||
});
|
});
|
||||||
stack.Children.Add(header);
|
stack.Children.Add(header);
|
||||||
|
|
||||||
var lines = (message.Content ?? "").Replace("\r\n", "\n").Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
var summary = message.MetaKind switch
|
||||||
.Select(line => line.Trim()).Where(line => !string.IsNullOrWhiteSpace(line)).ToList();
|
|
||||||
|
|
||||||
foreach (var line in lines)
|
|
||||||
{
|
{
|
||||||
var isHeaderLine = line.StartsWith("[", StringComparison.Ordinal);
|
"session_memory_compaction" => "이전 요약과 실행 경계를 하나의 세션 메모로 정리했습니다.",
|
||||||
|
"collapsed_boundary" => "이전 compact 경계를 합쳐 transcript를 더 가볍게 유지했습니다.",
|
||||||
|
_ => "오래된 실행/도구 결과를 줄여 다음 요청 컨텍스트를 가볍게 만들었습니다.",
|
||||||
|
};
|
||||||
|
|
||||||
stack.Children.Add(new TextBlock
|
stack.Children.Add(new TextBlock
|
||||||
{
|
{
|
||||||
Text = isHeaderLine ? line.Trim('[', ']') : line,
|
Text = summary,
|
||||||
FontSize = 10.5,
|
FontSize = 10.5,
|
||||||
FontWeight = isHeaderLine ? FontWeights.SemiBold : FontWeights.Normal,
|
|
||||||
Foreground = isHeaderLine ? primaryText : secondaryText,
|
|
||||||
TextWrapping = TextWrapping.Wrap,
|
|
||||||
Margin = isHeaderLine ? new Thickness(0, 0, 0, 3) : new Thickness(0, 0, 0, 2),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(message.MetaRunId))
|
|
||||||
{
|
|
||||||
stack.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = $"run {message.MetaRunId}",
|
|
||||||
FontSize = 9.5,
|
|
||||||
Foreground = secondaryText,
|
Foreground = secondaryText,
|
||||||
Opacity = 0.7,
|
TextWrapping = TextWrapping.Wrap,
|
||||||
Margin = new Thickness(0, 6, 0, 0),
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
wrapper.Child = stack;
|
wrapper.Child = stack;
|
||||||
return wrapper;
|
return wrapper;
|
||||||
|
|||||||
Reference in New Issue
Block a user