diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll
index 642dccc7..da3f51a3 100644
Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.dll and b/1.6/1.6/Assemblies/WulaFallenEmpire.dll differ
diff --git a/1.6/1.6/Defs/PrefabDefs/WULA_Prefabs.xml b/1.6/1.6/Defs/PrefabDefs/WULA_Prefabs.xml
index c1ebdd6c..c52b2923 100644
--- a/1.6/1.6/Defs/PrefabDefs/WULA_Prefabs.xml
+++ b/1.6/1.6/Defs/PrefabDefs/WULA_Prefabs.xml
@@ -723,59 +723,199 @@
-
- WULA_RavenShuttleBase
- (13,13)
-
-
-
- (1,6,1,6)
- (11,6,11,6)
-
- Clockwise
-
-
-
- (1,2,1,2)
- (1,10,1,10)
-
- Counterclockwise
-
-
-
- (11,2,11,2)
- (11,10,11,10)
-
- Clockwise
-
-
-
- (0,0,1,0)
- (11,0,12,0)
- (0,1,0,5)
- (12,1,12,5)
- (1,5,1,5)
- (11,5,11,5)
- (0,7,1,7)
- (11,7,12,7)
- (0,8,0,12)
- (12,8,12,12)
- (1,12,1,12)
- (11,12,11,12)
-
-
-
- (6, 0, 6)
-
-
-
+
+ WULA_PrisonBase
+ (13,13)
+
+
+
+ (1,4,1,4)
+ (4,4,4,4)
+ (8,4,8,4)
+ (11,4,11,4)
+ (1,8,1,8)
+ (4,8,4,8)
+ (8,8,8,8)
+ (11,8,11,8)
+
+
+
+
+ (0,6,0,6)
+ (12,6,12,6)
+
+ Clockwise
+
+
+
+ (4,5,4,5)
+ (8,5,8,5)
+ (4,7,4,7)
+ (8,7,8,7)
+
+
+
+
+ (2, 0, 1)
+ (5, 0, 1)
+ (7, 0, 1)
+ (10, 0, 1)
+
+ WULA_Alloy
+ Normal
+
+
+
+ (2, 0, 11)
+ (5, 0, 11)
+ (7, 0, 11)
+ (10, 0, 11)
+
+ Opposite
+ WULA_Alloy
+ Normal
+
+
+
+ (1,1,1,1)
+ (4,1,4,1)
+ (8,1,8,1)
+ (11,1,11,1)
+
+ Opposite
+
+
+ (3, 0, 6)
+ Clockwise
+
+
+ (9, 0, 6)
+ Counterclockwise
+
+
+
+ (1,11,1,11)
+ (4,11,4,11)
+ (8,11,8,11)
+ (11,11,11,11)
+
+
+
+
+ (1,1,1,1)
+ (4,1,4,1)
+ (8,1,8,1)
+ (11,1,11,1)
+
+ WULA_Alloy
+ Normal
+
+
+
+ (1,11,1,11)
+ (4,11,4,11)
+ (8,11,8,11)
+ (11,11,11,11)
+
+ Opposite
+ WULA_Alloy
+ Normal
+
+
+
+ (0,0,12,0)
+ (0,1,0,5)
+ (3,1,3,4)
+ (6,1,6,4)
+ (9,1,9,4)
+ (12,1,12,5)
+ (2,4,2,4)
+ (5,4,5,4)
+ (7,4,7,4)
+ (10,4,10,4)
+ (4,6,4,6)
+ (8,6,8,6)
+ (0,7,0,12)
+ (12,7,12,12)
+ (2,8,3,8)
+ (5,8,7,8)
+ (9,8,10,8)
+ (3,9,3,12)
+ (6,9,6,12)
+ (9,9,9,12)
+ (1,12,2,12)
+ (4,12,5,12)
+ (7,12,8,12)
+ (10,12,11,12)
+
+
+
+ (6, 0, 6)
+
+
+
(0,1,12,12)
-
+
+
+
+
+ WULA_RavenShuttleBase
+ (13,13)
+
+
+
+ (1,6,1,6)
+ (11,6,11,6)
+
+ Clockwise
+
+
+
+ (1,2,1,2)
+ (1,10,1,10)
+
+ Counterclockwise
+
+
+
+ (11,2,11,2)
+ (11,10,11,10)
+
+ Clockwise
+
+
+
+ (0,0,1,0)
+ (11,0,12,0)
+ (0,1,0,5)
+ (12,1,12,5)
+ (1,5,1,5)
+ (11,5,11,5)
+ (0,7,1,7)
+ (11,7,12,7)
+ (0,8,0,12)
+ (12,8,12,12)
+ (1,12,1,12)
+ (11,12,11,12)
+
+
+
+ (6, 0, 6)
+
+
+
+
+
+ (0,1,12,12)
+
+
+
+
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs
index 535f6b59..9a8cfce1 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs
@@ -941,6 +941,14 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
return cleaned.Trim();
}
+ private static bool LooksLikeNaturalReply(string response)
+ {
+ if (string.IsNullOrWhiteSpace(response)) return false;
+ string trimmed = response.Trim();
+ if (JsonToolCallParser.LooksLikeJson(trimmed)) return false;
+ return trimmed.Length >= 4;
+ }
+
private bool IsToolAvailable(string toolName)
{
if (string.IsNullOrWhiteSpace(toolName)) return false;
@@ -1758,6 +1766,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
maxSeconds = Mathf.Clamp(settings.reactMaxSeconds, 2f, 60f);
}
_thinkingPhaseTotal = maxSteps;
+ string toolPhaseReplyCandidate = null;
for (int step = 1; step <= maxSteps; step++)
{
if (Time.realtimeSinceStartup - startTime > maxSeconds)
@@ -1783,6 +1792,12 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
string normalizedResponse = NormalizeReactResponse(response);
if (!JsonToolCallParser.TryParseToolCallsFromText(normalizedResponse, out var toolCalls, out string jsonFragment))
{
+ if (LooksLikeNaturalReply(normalizedResponse))
+ {
+ toolPhaseReplyCandidate = normalizedResponse;
+ break;
+ }
+
if (Prefs.DevMode)
{
WulaLog.Debug("[WulaAI] ReAct step missing JSON envelope; attempting format fix.");
@@ -1793,6 +1808,10 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
if (string.IsNullOrEmpty(normalizedFixed) ||
!JsonToolCallParser.TryParseToolCallsFromText(normalizedFixed, out toolCalls, out jsonFragment))
{
+ if (LooksLikeNaturalReply(normalizedFixed))
+ {
+ toolPhaseReplyCandidate = normalizedFixed;
+ }
if (Prefs.DevMode)
{
WulaLog.Debug("[WulaAI] ReAct format fix failed.");
@@ -1893,10 +1912,21 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
return;
}
+ string fallbackReply = string.IsNullOrWhiteSpace(toolPhaseReplyCandidate)
+ ? ""
+ : StripToolCallJson(toolPhaseReplyCandidate)?.Trim() ?? "";
bool replyHadToolCalls = IsToolCallJson(reply);
string strippedReply = StripToolCallJson(reply)?.Trim() ?? "";
if (replyHadToolCalls || string.IsNullOrWhiteSpace(strippedReply))
{
+ if (!string.IsNullOrWhiteSpace(fallbackReply))
+ {
+ reply = fallbackReply;
+ replyHadToolCalls = false;
+ strippedReply = fallbackReply;
+ }
+ else
+ {
string retryReplyInstruction = replyInstruction +
"\n\n# RETRY (REPLY OUTPUT)\n" +
"Your last reply included tool call JSON or was empty. Tool calls are DISABLED.\n" +
@@ -1908,16 +1938,24 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
replyHadToolCalls = IsToolCallJson(reply);
strippedReply = StripToolCallJson(reply)?.Trim() ?? "";
}
+ }
}
if (replyHadToolCalls)
{
+ if (!string.IsNullOrWhiteSpace(fallbackReply))
+ {
+ reply = fallbackReply;
+ }
+ else
+ {
string cleaned = StripToolCallJson(reply)?.Trim() ?? "";
if (string.IsNullOrWhiteSpace(cleaned))
{
cleaned = "(system) AI reply returned tool call JSON only and was discarded. Please retry or send /clear to reset context.";
}
reply = cleaned;
+ }
}
AddAssistantMessage(reply);
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs
index 40d9b2d4..86f8fe80 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs
@@ -30,13 +30,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
private Dictionary _portraits = new Dictionary();
private int _currentPortraitId = 0;
private static readonly Regex ExpressionTagRegex = new Regex(@"\[EXPR\s*:\s*([1-6])\s*\]", RegexOptions.IgnoreCase);
- private bool _reactTraceExpanded = false;
- private bool _hasReactTrace = false;
- private float _reactTraceHeight = 0f;
- private float _reactTraceYOffset = 0f;
- private float _reactTraceHeaderHeight = 0f;
- private string _reactTraceHeader = "";
- private List _reactTraceLines = new List();
+ private readonly Dictionary _traceExpandedByAssistantIndex = new Dictionary();
private class CachedMessage
{
@@ -46,13 +40,12 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
public float height;
public float yOffset;
public GameFont font;
- }
-
- private class ReactTraceStep
- {
- public int Step;
- public List Calls = new List();
- public List Results = new List();
+ public bool isTrace;
+ public int traceKey;
+ public string traceHeader;
+ public List traceLines;
+ public bool traceExpanded;
+ public float traceHeaderHeight;
}
public static Dialog_AIConversation Instance { get; private set; }
@@ -338,15 +331,91 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
float curY = 0f;
float innerPadding = 5f;
float contentWidth = width - innerPadding * 2;
+ var toolcallBuffer = new List();
+ var toolResultBuffer = new List();
+ bool traceEnabled = WulaFallenEmpireMod.settings?.showReactTraceInUI == true;
for (int i = 0; i < history.Count; i++)
{
var entry = history[i];
- string messageText = entry.role == "assistant" ? ParseResponseForDisplay(entry.message) : AIIntelligenceCore.StripContextInfo(entry.message);
-
- if (entry.role == "tool" || entry.role == "system" || entry.role == "toolcall") continue;
+ if (entry.role == "user")
+ {
+ toolcallBuffer.Clear();
+ toolResultBuffer.Clear();
+ }
+
+ if (entry.role == "toolcall")
+ {
+ if (traceEnabled)
+ {
+ toolcallBuffer.Add(entry.message ?? "");
+ }
+ continue;
+ }
+
+ if (entry.role == "tool")
+ {
+ if (traceEnabled)
+ {
+ toolResultBuffer.Add(entry.message ?? "");
+ }
+ continue;
+ }
+
+ string messageText = entry.role == "assistant"
+ ? ParseResponseForDisplay(entry.message)
+ : AIIntelligenceCore.StripContextInfo(entry.message);
+
+ if (entry.role == "system") continue;
// Hide auto-commentary system messages (user-side) from display
if (entry.role == "user" && entry.message.Contains("[AUTO_COMMENTARY]")) continue;
+ if (entry.role == "assistant" && traceEnabled && toolcallBuffer.Count > 0)
+ {
+ var traceLines = BuildTraceLines(toolcallBuffer, toolResultBuffer);
+ if (traceLines.Count > 0)
+ {
+ int traceKey = i;
+ bool expanded = _traceExpandedByAssistantIndex.TryGetValue(traceKey, out bool saved) && saved;
+ string header = BuildReactTraceHeader();
+
+ Text.Font = GameFont.Tiny;
+ float tracePadding = 8f;
+ float headerWidth = Mathf.Max(10f, contentWidth - tracePadding * 2f);
+ string headerLine = $"{(expanded ? "v" : ">")} {header}";
+ float headerHeight = Text.CalcHeight(headerLine, headerWidth) + 10f;
+ float linesHeight = 0f;
+ if (expanded)
+ {
+ float lineWidth = Mathf.Max(10f, contentWidth - tracePadding * 2f);
+ foreach (string line in traceLines)
+ {
+ linesHeight += Text.CalcHeight(line, lineWidth) + 2f;
+ }
+ linesHeight += 8f;
+ }
+ float traceHeight = headerHeight + linesHeight;
+
+ _cachedMessages.Add(new CachedMessage
+ {
+ role = "trace",
+ message = "",
+ displayText = "",
+ height = traceHeight,
+ yOffset = curY,
+ font = GameFont.Tiny,
+ isTrace = true,
+ traceKey = traceKey,
+ traceHeader = header,
+ traceLines = traceLines,
+ traceExpanded = expanded,
+ traceHeaderHeight = headerHeight
+ });
+ curY += traceHeight + 10f;
+ }
+
+ toolcallBuffer.Clear();
+ toolResultBuffer.Clear();
+ }
if (string.IsNullOrEmpty(messageText) || (entry.role == "user" && messageText.StartsWith("[Tool Results]"))) continue;
bool isLastMessage = i == history.Count - 1;
@@ -368,8 +437,6 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
curY += height + 10f;
}
-
- UpdateReactTraceCache(history, contentWidth, ref curY);
_cachedTotalHeight = curY;
}
@@ -407,7 +474,11 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
Text.Font = entry.font;
Rect labelRect = new Rect(innerPadding, entry.yOffset, contentWidth, entry.height);
- if (entry.role == "user")
+ if (entry.isTrace)
+ {
+ DrawReactTracePanel(labelRect, entry);
+ }
+ else if (entry.role == "user")
{
Text.Anchor = TextAnchor.MiddleRight;
Widgets.Label(labelRect, $"{entry.displayText}");
@@ -426,15 +497,6 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
}
}
- if (_hasReactTrace)
- {
- Rect traceRect = new Rect(innerPadding, _reactTraceYOffset, contentWidth, _reactTraceHeight);
- if (traceRect.yMax >= viewTop && traceRect.y <= viewBottom)
- {
- DrawReactTracePanel(traceRect);
- }
- }
-
if (_isThinking)
{
float thinkingY = _cachedTotalHeight > 0 ? _cachedTotalHeight : 0f;
@@ -471,83 +533,20 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
return text.Split(new[] { "OPTIONS:" }, StringSplitOptions.None)[0].Trim();
}
- private void UpdateReactTraceCache(List<(string role, string message)> history, float contentWidth, ref float curY)
+ private List BuildTraceLines(List toolcallBuffer, List toolResultBuffer)
{
- _hasReactTrace = false;
- _reactTraceLines.Clear();
- _reactTraceHeader = "";
- _reactTraceHeight = 0f;
- _reactTraceYOffset = 0f;
- _reactTraceHeaderHeight = 0f;
-
- if (WulaFallenEmpireMod.settings?.showReactTraceInUI != true)
- {
- return;
- }
-
- List steps = BuildReactTraceSteps(history);
- if (steps.Count == 0)
- {
- return;
- }
-
- _hasReactTrace = true;
- _reactTraceHeader = BuildReactTraceHeader();
- foreach (var step in steps)
- {
- foreach (string call in step.Calls)
- {
- _reactTraceLines.Add($"Step {step.Step}: call {call}");
- }
- foreach (string result in step.Results)
- {
- _reactTraceLines.Add($"Step {step.Step}: result {result}");
- }
- }
-
- var originalFont = Text.Font;
- Text.Font = GameFont.Tiny;
- float panelPadding = 6f;
- float headerWidth = Mathf.Max(10f, contentWidth - panelPadding * 2f);
- string headerLine = $"{(_reactTraceExpanded ? "v" : ">")} {_reactTraceHeader}";
- _reactTraceHeaderHeight = Text.CalcHeight(headerLine, headerWidth) + 6f;
-
- float linesHeight = 0f;
- if (_reactTraceExpanded && _reactTraceLines.Count > 0)
- {
- float lineWidth = Mathf.Max(10f, contentWidth - panelPadding * 2f);
- foreach (string line in _reactTraceLines)
- {
- linesHeight += Text.CalcHeight(line, lineWidth) + 2f;
- }
- linesHeight += 4f;
- }
- _reactTraceHeight = _reactTraceHeaderHeight + linesHeight;
- Text.Font = originalFont;
-
- curY += 6f;
- _reactTraceYOffset = curY;
- curY += _reactTraceHeight + 8f;
- }
-
- private List BuildReactTraceSteps(List<(string role, string message)> history)
- {
- var steps = new List();
- if (history == null || history.Count == 0) return steps;
-
- int lastUserIndex = history.FindLastIndex(entry => entry.role == "user");
- if (lastUserIndex < 0) return steps;
+ var lines = new List();
+ if (toolcallBuffer == null || toolcallBuffer.Count == 0) return lines;
int stepIndex = 0;
- for (int i = lastUserIndex + 1; i < history.Count; i++)
+ int maxSteps = Math.Max(toolcallBuffer.Count, toolResultBuffer.Count);
+ for (int i = 0; i < maxSteps; i++)
{
- var entry = history[i];
- if (entry.role != "toolcall") continue;
-
+ bool anyStepContent = false;
stepIndex++;
- var step = new ReactTraceStep { Step = stepIndex };
- if (JsonToolCallParser.TryParseToolCallsFromText(entry.message, out var calls, out _))
+ if (i < toolcallBuffer.Count &&
+ JsonToolCallParser.TryParseToolCallsFromText(toolcallBuffer[i], out var calls, out _))
{
foreach (var call in calls)
{
@@ -556,23 +555,27 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
string callText = string.IsNullOrWhiteSpace(args) || args == "{}"
? call.Name
: $"{call.Name} {TrimForDisplay(args, 160)}";
- step.Calls.Add(callText);
+ lines.Add($"步骤 {stepIndex} · 调用 {callText}");
+ anyStepContent = true;
}
}
- if (i + 1 < history.Count && history[i + 1].role == "tool")
+ if (i < toolResultBuffer.Count)
{
- step.Results.AddRange(ExtractToolResultLines(history[i + 1].message, 4));
- i++;
+ foreach (string resultLine in ExtractToolResultLines(toolResultBuffer[i], 4))
+ {
+ lines.Add($"步骤 {stepIndex} · 结果 {resultLine}");
+ anyStepContent = true;
+ }
}
- if (step.Calls.Count > 0 || step.Results.Count > 0)
+ if (!anyStepContent)
{
- steps.Add(step);
+ stepIndex--;
}
}
- return steps;
+ return lines;
}
private static List ExtractToolResultLines(string toolMessage, int maxLines)
@@ -586,6 +589,13 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
string line = raw.Trim();
if (string.IsNullOrEmpty(line)) continue;
if (line.StartsWith("[Tool Results]", StringComparison.OrdinalIgnoreCase)) continue;
+ if (!line.StartsWith("Tool '", StringComparison.OrdinalIgnoreCase) &&
+ !line.StartsWith("ToolRunner", StringComparison.OrdinalIgnoreCase) &&
+ !line.StartsWith("Query Result:", StringComparison.OrdinalIgnoreCase) &&
+ !line.StartsWith("Error:", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
lines.Add(TrimForDisplay(line, 200));
if (lines.Count >= maxLines) break;
}
@@ -606,37 +616,43 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
? Mathf.Max(0f, Time.realtimeSinceStartup - (_core?.ThinkingStartTime ?? 0f))
: _core?.LastThinkingDuration ?? 0f;
string elapsedText = elapsed > 0f ? elapsed.ToString("0.0", CultureInfo.InvariantCulture) : "0.0";
- return $"{state} ({elapsedText}s · Loop {_core?.ThinkingPhaseIndex ?? 0}/{_core?.ThinkingPhaseTotal ?? 0})";
+ return $"{state} (用时 {elapsedText}s · Loop {_core?.ThinkingPhaseIndex ?? 0}/{_core?.ThinkingPhaseTotal ?? 0})";
}
- private void DrawReactTracePanel(Rect rect)
+ private void DrawReactTracePanel(Rect rect, CachedMessage traceEntry)
{
var originalColor = GUI.color;
var originalFont = Text.Font;
var originalAnchor = Text.Anchor;
- float padding = 6f;
- Rect headerRect = new Rect(rect.x, rect.y, rect.width, _reactTraceHeaderHeight);
+ float padding = 8f;
+ Rect headerRect = new Rect(rect.x, rect.y, rect.width, traceEntry.traceHeaderHeight);
GUI.color = new Color(0.15f, 0.15f, 0.15f, 0.8f);
Widgets.DrawBoxSolid(headerRect, GUI.color);
GUI.color = Color.white;
Text.Font = GameFont.Tiny;
Text.Anchor = TextAnchor.MiddleLeft;
- string headerLine = $"{(_reactTraceExpanded ? "v" : ">")} {_reactTraceHeader}";
- Widgets.Label(headerRect.ContractedBy(padding), headerLine);
+ string headerLine = $"{(traceEntry.traceExpanded ? "v" : ">")} {traceEntry.traceHeader}";
+ Widgets.Label(headerRect.ContractedBy(padding, 4f), headerLine);
if (Widgets.ButtonInvisible(headerRect))
{
- _reactTraceExpanded = !_reactTraceExpanded;
+ traceEntry.traceExpanded = !traceEntry.traceExpanded;
+ _traceExpandedByAssistantIndex[traceEntry.traceKey] = traceEntry.traceExpanded;
_lastHistoryCount = -1;
_lastUsedWidth = -1f;
}
- if (_reactTraceExpanded && _reactTraceLines.Count > 0)
+ if (traceEntry.traceExpanded && traceEntry.traceLines != null && traceEntry.traceLines.Count > 0)
{
- float y = headerRect.yMax + 2f;
- foreach (string line in _reactTraceLines)
+ Rect bodyRect = new Rect(rect.x, headerRect.yMax, rect.width, rect.height - traceEntry.traceHeaderHeight);
+ GUI.color = new Color(0.12f, 0.12f, 0.12f, 0.45f);
+ Widgets.DrawBoxSolid(bodyRect, GUI.color);
+ GUI.color = Color.white;
+
+ float y = headerRect.yMax + 6f;
+ foreach (string line in traceEntry.traceLines)
{
float lineHeight = Text.CalcHeight(line, rect.width - padding * 2f) + 2f;
Rect lineRect = new Rect(rect.x + padding, y, rect.width - padding * 2f, lineHeight);
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs
index 44d98a32..75f74016 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs
@@ -25,13 +25,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
private float _lastUsedWidth = -1f;
private List _cachedMessages = new List();
private float _cachedTotalHeight = 0f;
- private bool _reactTraceExpanded = false;
- private bool _hasReactTrace = false;
- private float _reactTraceHeight = 0f;
- private float _reactTraceYOffset = 0f;
- private float _reactTraceHeaderHeight = 0f;
- private string _reactTraceHeader = "";
- private List _reactTraceLines = new List();
+ private readonly Dictionary _traceExpandedByAssistantIndex = new Dictionary();
private class CachedMessage
{
@@ -40,13 +34,12 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
public string displayText;
public float height;
public float yOffset;
- }
-
- private class ReactTraceStep
- {
- public int Step;
- public List Calls = new List();
- public List Results = new List();
+ public bool isTrace;
+ public int traceKey;
+ public string traceHeader;
+ public List traceLines;
+ public bool traceExpanded;
+ public float traceHeaderHeight;
}
@@ -389,11 +382,38 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
_cachedTotalHeight = 0f;
float reducedSpacing = 8f;
float curY = 10f;
+ var toolcallBuffer = new List();
+ var toolResultBuffer = new List();
+ bool traceEnabled = WulaFallenEmpireMod.settings?.showReactTraceInUI == true;
- foreach (var msg in history)
+ for (int i = 0; i < history.Count; i++)
{
+ var msg = history[i];
// Filter logic
- if (msg.role == "tool" || msg.role == "toolcall") continue;
+ if (msg.role == "user")
+ {
+ toolcallBuffer.Clear();
+ toolResultBuffer.Clear();
+ }
+
+ if (msg.role == "toolcall")
+ {
+ if (traceEnabled)
+ {
+ toolcallBuffer.Add(msg.message ?? "");
+ }
+ continue;
+ }
+
+ if (msg.role == "tool")
+ {
+ if (traceEnabled)
+ {
+ toolResultBuffer.Add(msg.message ?? "");
+ }
+ continue;
+ }
+
if (msg.role == "system" && !Prefs.DevMode) continue;
// Hide auto-commentary system messages (user-side) from display
@@ -409,6 +429,53 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
displayText = AIIntelligenceCore.StripContextInfo(msg.message);
}
+ if (msg.role == "assistant" && traceEnabled && toolcallBuffer.Count > 0)
+ {
+ var traceLines = BuildTraceLines(toolcallBuffer, toolResultBuffer);
+ if (traceLines.Count > 0)
+ {
+ int traceKey = i;
+ bool expanded = _traceExpandedByAssistantIndex.TryGetValue(traceKey, out bool saved) && saved;
+ string header = BuildReactTraceHeader();
+
+ Text.Font = GameFont.Tiny;
+ float padding = 8f;
+ float headerWidth = Mathf.Max(10f, width - padding * 2f);
+ string headerLine = $"{(expanded ? "v" : ">")} {header}";
+ float headerHeight = Text.CalcHeight(headerLine, headerWidth) + 10f;
+ float linesHeight = 0f;
+ if (expanded)
+ {
+ float lineWidth = Mathf.Max(10f, width - padding * 2f);
+ foreach (string line in traceLines)
+ {
+ linesHeight += Text.CalcHeight(line, lineWidth) + 2f;
+ }
+ linesHeight += 8f;
+ }
+ float traceHeight = headerHeight + linesHeight;
+
+ _cachedMessages.Add(new CachedMessage
+ {
+ role = "trace",
+ message = "",
+ displayText = "",
+ height = traceHeight,
+ yOffset = curY,
+ isTrace = true,
+ traceKey = traceKey,
+ traceHeader = header,
+ traceLines = traceLines,
+ traceExpanded = expanded,
+ traceHeaderHeight = headerHeight
+ });
+ curY += traceHeight + reducedSpacing;
+ }
+
+ toolcallBuffer.Clear();
+ toolResultBuffer.Clear();
+ }
+
if (string.IsNullOrWhiteSpace(displayText)) continue;
float h = CalcMessageHeight(displayText, width);
@@ -425,7 +492,6 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
curY += h + reducedSpacing;
}
- UpdateReactTraceCache(history, width, ref curY);
_cachedTotalHeight = curY;
}
@@ -461,8 +527,12 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
if (entry.yOffset > viewBottom + 100f) break;
Rect msgRect = new Rect(0, entry.yOffset, width, entry.height);
-
- if (entry.role == "user")
+
+ if (entry.isTrace)
+ {
+ DrawReactTracePanel(msgRect, entry);
+ }
+ else if (entry.role == "user")
{
DrawSenseiMessage(msgRect, entry.displayText);
}
@@ -480,15 +550,6 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
}
}
- if (_hasReactTrace)
- {
- Rect traceRect = new Rect(0, _reactTraceYOffset, width, _reactTraceHeight);
- if (traceRect.yMax >= viewTop && traceRect.y <= viewBottom)
- {
- DrawReactTracePanel(traceRect);
- }
- }
-
if (_core != null && _core.IsThinking)
{
float thinkingY = _cachedTotalHeight > 0 ? _cachedTotalHeight : 10f;
@@ -515,83 +576,20 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
return text.Remove(index, fragment.Length).Trim();
}
- private void UpdateReactTraceCache(List<(string role, string message)> history, float contentWidth, ref float curY)
+ private List BuildTraceLines(List toolcallBuffer, List toolResultBuffer)
{
- _hasReactTrace = false;
- _reactTraceLines.Clear();
- _reactTraceHeader = "";
- _reactTraceHeight = 0f;
- _reactTraceYOffset = 0f;
- _reactTraceHeaderHeight = 0f;
-
- if (WulaFallenEmpireMod.settings?.showReactTraceInUI != true)
- {
- return;
- }
-
- List steps = BuildReactTraceSteps(history);
- if (steps.Count == 0)
- {
- return;
- }
-
- _hasReactTrace = true;
- _reactTraceHeader = BuildReactTraceHeader();
- foreach (var step in steps)
- {
- foreach (string call in step.Calls)
- {
- _reactTraceLines.Add($"Step {step.Step}: call {call}");
- }
- foreach (string result in step.Results)
- {
- _reactTraceLines.Add($"Step {step.Step}: result {result}");
- }
- }
-
- var originalFont = Text.Font;
- Text.Font = GameFont.Tiny;
- float panelPadding = 8f;
- float headerWidth = Mathf.Max(10f, contentWidth - panelPadding * 2f);
- string headerLine = $"{(_reactTraceExpanded ? "v" : ">")} {_reactTraceHeader}";
- _reactTraceHeaderHeight = Text.CalcHeight(headerLine, headerWidth) + 6f;
-
- float linesHeight = 0f;
- if (_reactTraceExpanded && _reactTraceLines.Count > 0)
- {
- float lineWidth = Mathf.Max(10f, contentWidth - panelPadding * 2f);
- foreach (string line in _reactTraceLines)
- {
- linesHeight += Text.CalcHeight(line, lineWidth) + 2f;
- }
- linesHeight += 6f;
- }
- _reactTraceHeight = _reactTraceHeaderHeight + linesHeight;
- Text.Font = originalFont;
-
- curY += 6f;
- _reactTraceYOffset = curY;
- curY += _reactTraceHeight + 6f;
- }
-
- private List BuildReactTraceSteps(List<(string role, string message)> history)
- {
- var steps = new List();
- if (history == null || history.Count == 0) return steps;
-
- int lastUserIndex = history.FindLastIndex(entry => entry.role == "user");
- if (lastUserIndex < 0) return steps;
+ var lines = new List();
+ if (toolcallBuffer == null || toolcallBuffer.Count == 0) return lines;
int stepIndex = 0;
- for (int i = lastUserIndex + 1; i < history.Count; i++)
+ int maxSteps = Math.Max(toolcallBuffer.Count, toolResultBuffer.Count);
+ for (int i = 0; i < maxSteps; i++)
{
- var entry = history[i];
- if (entry.role != "toolcall") continue;
-
+ bool anyStepContent = false;
stepIndex++;
- var step = new ReactTraceStep { Step = stepIndex };
- if (JsonToolCallParser.TryParseToolCallsFromText(entry.message, out var calls, out _))
+ if (i < toolcallBuffer.Count &&
+ JsonToolCallParser.TryParseToolCallsFromText(toolcallBuffer[i], out var calls, out _))
{
foreach (var call in calls)
{
@@ -600,23 +598,27 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
string callText = string.IsNullOrWhiteSpace(args) || args == "{}"
? call.Name
: $"{call.Name} {TrimForDisplay(args, 160)}";
- step.Calls.Add(callText);
+ lines.Add($"步骤 {stepIndex} · 调用 {callText}");
+ anyStepContent = true;
}
}
- if (i + 1 < history.Count && history[i + 1].role == "tool")
+ if (i < toolResultBuffer.Count)
{
- step.Results.AddRange(ExtractToolResultLines(history[i + 1].message, 4));
- i++;
+ foreach (string resultLine in ExtractToolResultLines(toolResultBuffer[i], 4))
+ {
+ lines.Add($"步骤 {stepIndex} · 结果 {resultLine}");
+ anyStepContent = true;
+ }
}
- if (step.Calls.Count > 0 || step.Results.Count > 0)
+ if (!anyStepContent)
{
- steps.Add(step);
+ stepIndex--;
}
}
- return steps;
+ return lines;
}
private static List ExtractToolResultLines(string toolMessage, int maxLines)
@@ -630,6 +632,13 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
string line = raw.Trim();
if (string.IsNullOrEmpty(line)) continue;
if (line.StartsWith("[Tool Results]", StringComparison.OrdinalIgnoreCase)) continue;
+ if (!line.StartsWith("Tool '", StringComparison.OrdinalIgnoreCase) &&
+ !line.StartsWith("ToolRunner", StringComparison.OrdinalIgnoreCase) &&
+ !line.StartsWith("Query Result:", StringComparison.OrdinalIgnoreCase) &&
+ !line.StartsWith("Error:", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
lines.Add(TrimForDisplay(line, 200));
if (lines.Count >= maxLines) break;
}
@@ -653,37 +662,43 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
string elapsedText = elapsed > 0f ? elapsed.ToString("0.0", System.Globalization.CultureInfo.InvariantCulture) : "0.0";
int phaseIndex = _core?.ThinkingPhaseIndex ?? 0;
int phaseTotal = _core?.ThinkingPhaseTotal ?? 0;
- return $"{state} ({elapsedText}s · Loop {phaseIndex}/{phaseTotal})";
+ return $"{state} (用时 {elapsedText}s · Loop {phaseIndex}/{phaseTotal})";
}
- private void DrawReactTracePanel(Rect rect)
+ private void DrawReactTracePanel(Rect rect, CachedMessage traceEntry)
{
var originalColor = GUI.color;
var originalFont = Text.Font;
var originalAnchor = Text.Anchor;
float padding = 8f;
- Rect headerRect = new Rect(rect.x, rect.y, rect.width, _reactTraceHeaderHeight);
+ Rect headerRect = new Rect(rect.x, rect.y, rect.width, traceEntry.traceHeaderHeight);
GUI.color = new Color(0.12f, 0.12f, 0.12f, 0.8f);
Widgets.DrawBoxSolid(headerRect, GUI.color);
GUI.color = Color.white;
Text.Font = GameFont.Tiny;
Text.Anchor = TextAnchor.MiddleLeft;
- string headerLine = $"{(_reactTraceExpanded ? "v" : ">")} {_reactTraceHeader}";
- Widgets.Label(headerRect.ContractedBy(padding), headerLine);
+ string headerLine = $"{(traceEntry.traceExpanded ? "v" : ">")} {traceEntry.traceHeader}";
+ Widgets.Label(headerRect.ContractedBy(padding, 4f), headerLine);
if (Widgets.ButtonInvisible(headerRect))
{
- _reactTraceExpanded = !_reactTraceExpanded;
+ traceEntry.traceExpanded = !traceEntry.traceExpanded;
+ _traceExpandedByAssistantIndex[traceEntry.traceKey] = traceEntry.traceExpanded;
_lastHistoryCount = -1;
_lastUsedWidth = -1f;
}
- if (_reactTraceExpanded && _reactTraceLines.Count > 0)
+ if (traceEntry.traceExpanded && traceEntry.traceLines != null && traceEntry.traceLines.Count > 0)
{
- float y = headerRect.yMax + 2f;
- foreach (string line in _reactTraceLines)
+ Rect bodyRect = new Rect(rect.x, headerRect.yMax, rect.width, rect.height - traceEntry.traceHeaderHeight);
+ GUI.color = new Color(0.1f, 0.1f, 0.1f, 0.45f);
+ Widgets.DrawBoxSolid(bodyRect, GUI.color);
+ GUI.color = Color.white;
+
+ float y = headerRect.yMax + 6f;
+ foreach (string line in traceEntry.traceLines)
{
float lineHeight = Text.CalcHeight(line, rect.width - padding * 2f) + 2f;
Rect lineRect = new Rect(rect.x + padding, y, rect.width - padding * 2f, lineHeight);