diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll
index 1ec002f9..642dccc7 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 f2fa736d..c1ebdd6c 100644
--- a/1.6/1.6/Defs/PrefabDefs/WULA_Prefabs.xml
+++ b/1.6/1.6/Defs/PrefabDefs/WULA_Prefabs.xml
@@ -144,132 +144,130 @@
-
-
- WULA_StorageBase
- (13,14)
-
-
- (5, 0, 6)
-
-
-
- (6,0,6,0)
- (6,12,6,12)
-
-
-
-
- (0,6,0,6)
- (12,6,12,6)
-
- Clockwise
-
-
-
- (6,1,6,11)
- (1,6,5,6)
- (7,6,11,6)
-
-
-
-
- (4,1,4,1)
- (8,1,8,1)
-
- Opposite
-
-
-
- (1,4,1,4)
- (1,8,1,8)
-
- Counterclockwise
-
-
-
- (11,4,11,4)
- (11,8,11,8)
-
- Clockwise
-
-
-
- (4,11,4,11)
- (8,11,8,11)
-
-
-
- (7, 0, 6)
-
-
-
- (0,0,5,0)
- (7,0,12,0)
- (0,1,0,5)
- (12,1,12,5)
- (0,7,0,12)
- (12,7,12,12)
- (1,12,5,12)
- (7,12,11,12)
-
-
-
-
- (3, 0, 2)
- (5, 0, 2)
- (8, 0, 2)
- (10, 0, 2)
- (3, 0, 4)
- (5, 0, 4)
- (8, 0, 4)
- (10, 0, 4)
- (3, 0, 7)
- (5, 0, 7)
- (8, 0, 7)
- (10, 0, 7)
- (3, 0, 9)
- (5, 0, 9)
- (8, 0, 9)
- (10, 0, 9)
-
- Opposite
- WULA_Alloy
-
-
-
- (2, 0, 3)
- (4, 0, 3)
- (7, 0, 3)
- (9, 0, 3)
- (2, 0, 5)
- (4, 0, 5)
- (7, 0, 5)
- (9, 0, 5)
- (2, 0, 8)
- (4, 0, 8)
- (7, 0, 8)
- (9, 0, 8)
- (2, 0, 10)
- (4, 0, 10)
- (7, 0, 10)
- (9, 0, 10)
-
- WULA_Alloy
-
-
- (6, 0, 6)
-
-
-
+
+ WULA_StorageBase
+ (13,14)
+
+
+ (5, 0, 6)
+
+
+
+ (6,0,6,0)
+ (6,12,6,12)
+
+
+
+
+ (0,6,0,6)
+ (12,6,12,6)
+
+ Clockwise
+
+
+
+ (6,1,6,11)
+ (1,6,5,6)
+ (7,6,11,6)
+
+
+
+
+ (4,1,4,1)
+ (8,1,8,1)
+
+ Opposite
+
+
+
+ (1,4,1,4)
+ (1,8,1,8)
+
+ Counterclockwise
+
+
+
+ (11,4,11,4)
+ (11,8,11,8)
+
+ Clockwise
+
+
+
+ (4,11,4,11)
+ (8,11,8,11)
+
+
+
+ (7, 0, 6)
+
+
+
+ (0,0,5,0)
+ (7,0,12,0)
+ (0,1,0,5)
+ (12,1,12,5)
+ (0,7,0,12)
+ (12,7,12,12)
+ (1,12,5,12)
+ (7,12,11,12)
+
+
+
+
+ (3, 0, 2)
+ (5, 0, 2)
+ (8, 0, 2)
+ (10, 0, 2)
+ (3, 0, 4)
+ (5, 0, 4)
+ (8, 0, 4)
+ (10, 0, 4)
+ (3, 0, 7)
+ (5, 0, 7)
+ (8, 0, 7)
+ (10, 0, 7)
+ (3, 0, 9)
+ (5, 0, 9)
+ (8, 0, 9)
+ (10, 0, 9)
+
+ Opposite
+ WULA_Alloy
+
+
+
+ (2, 0, 3)
+ (4, 0, 3)
+ (7, 0, 3)
+ (9, 0, 3)
+ (2, 0, 5)
+ (4, 0, 5)
+ (7, 0, 5)
+ (9, 0, 5)
+ (2, 0, 8)
+ (4, 0, 8)
+ (7, 0, 8)
+ (9, 0, 8)
+ (2, 0, 10)
+ (4, 0, 10)
+ (7, 0, 10)
+ (9, 0, 10)
+
+ WULA_Alloy
+
+
+ (6, 0, 6)
+
+
+
(0,1,12,12)
-
-
+
WULA_KitchenBase
@@ -417,160 +415,358 @@
-
+
+ WULA_HospitalBase
+ (13,14)
+
+
+
+ (1,2,1,2)
+ (1,10,1,10)
+
+ Clockwise
+
+
+ (11, 0, 2)
+ Counterclockwise
+ 93
+
+
+ (11, 0, 10)
+ Counterclockwise
+
+
+ (6, 0, 0)
+ 982
+
+
+ (6, 0, 12)
+ 977
+
+
+ (10, 0, 6)
+ Clockwise
+
+
+ (8, 0, 11)
+ 92
+
+
+ (2, 0, 6)
+
+
+ (8, 0, 6)
+ Clockwise
+ WULA_Alloy
+ Normal
+
+
+
+ (4,1,4,1)
+ (8,1,8,1)
+
+ Opposite
+
+
+
+ (1,4,1,4)
+ (1,8,1,8)
+
+ Counterclockwise
+
+
+
+ (11,4,11,4)
+ (11,8,11,8)
+
+ Clockwise
+
+
+ (4, 0, 11)
+
+
+ (8, 0, 11)
+ 17
+
+
+
+ (1, 0, 1)
+ (1, 0, 3)
+ (1, 0, 9)
+ (1, 0, 11)
+
+ Clockwise
+ WULA_Alloy
+ Normal
+
+
+ (11, 0, 1)
+ Counterclockwise
+ WULA_Alloy
+ Normal
+ 149
+
+
+
+ (11, 0, 3)
+ (11, 0, 9)
+ (11, 0, 11)
+
+ Counterclockwise
+ WULA_Alloy
+ Normal
+
+
+
+ (4,5,4,7)
+
+
+
+
+ (1, 0, 4)
+ (3, 0, 4)
+ (8, 0, 4)
+ (10, 0, 4)
+ (1, 0, 8)
+ (3, 0, 8)
+ (8, 0, 8)
+ (10, 0, 8)
+
+ Synthread
+
+
+
+ (0,0,5,0)
+ (7,0,12,0)
+ (0,1,0,12)
+ (12,2,12,2)
+ (12,4,12,12)
+ (1,12,5,12)
+ (8,12,11,12)
+
+
+
+
+ (12,1,12,1)
+ (12,3,12,3)
+ (7,12,7,12)
+
+ 1199
+
+
+
+ (4, 0, 1)
+ (7, 0, 1)
+
+ WULA_Alloy
+
+
+ (5, 0, 11)
+ Opposite
+ WULA_Alloy
+
+
+ (6, 0, 6)
+
+
+
+
+
+ (0,1,12,12)
+
+
+
+
+
+
+
+ WULA_PowerPlantBase
+ (13,14)
+
+
+
+ (5, 0, 3)
+ (6, 0, 3)
+ (7, 0, 3)
+
+
+
+
+ (3, 0, 5)
+ (3, 0, 6)
+ (3, 0, 7)
+
+ Clockwise
+
+
+
+ (9, 0, 5)
+ (9, 0, 6)
+ (9, 0, 7)
+
+ Counterclockwise
+
+
+
+ (5, 0, 9)
+ (6, 0, 9)
+ (7, 0, 9)
+
+ Opposite
+
+
+
+ (6,0,6,0)
+ (6,12,6,12)
+
+
+
+
+ (0,6,0,6)
+ (12,6,12,6)
+
+ Clockwise
+
+
+
+ (3, 0, 3)
+ (9, 0, 3)
+ (3, 0, 9)
+ (9, 0, 9)
+
+
+
+
+ (3,1,3,1)
+ (9,1,9,1)
+ (1,3,1,3)
+ (11,3,11,3)
+ (3,11,3,11)
+ (9,11,9,11)
+
+
+
+
+ (4,1,4,1)
+ (8,1,8,1)
+
+ Opposite
+
+
+
+ (1,4,1,4)
+ (1,8,1,8)
+
+ Counterclockwise
+
+
+
+ (11,4,11,4)
+ (11,8,11,8)
+
+ Clockwise
+
+
+
+ (4,11,4,11)
+ (8,11,8,11)
+
+
+
+
+ (5,5,5,5)
+ (7,7,7,7)
+
+
+
+ (6, 0, 2)
+ Opposite
+ WULA_Alloy
+
+
+ (2, 0, 6)
+ Counterclockwise
+ WULA_Alloy
+
+
+ (10, 0, 6)
+ Clockwise
+ WULA_Alloy
+
+
+ (6, 0, 10)
+ WULA_Alloy
+
+
+
+ (0,0,5,0)
+ (7,0,12,0)
+ (0,1,0,5)
+ (12,1,12,5)
+ (0,7,0,12)
+ (12,7,12,12)
+ (1,12,5,12)
+ (7,12,11,12)
+
+
+
+ (6, 0, 6)
+
+
+
+
+
+ (0,1,12,12)
+
+
+
+
+
+
- WULA_HospitalBase
- (13,14)
+ WULA_RavenShuttleBase
+ (13,13)
-
+
+
+ (1,6,1,6)
+ (11,6,11,6)
+
+ Clockwise
+
+
(1,2,1,2)
(1,10,1,10)
- Clockwise
-
-
- (11, 0, 2)
- Counterclockwise
- 93
-
-
- (11, 0, 10)
- Counterclockwise
-
-
- (6, 0, 0)
- 982
-
-
- (6, 0, 12)
- 977
-
-
- (10, 0, 6)
- Clockwise
-
-
- (8, 0, 11)
- 92
-
-
- (2, 0, 6)
-
-
- (8, 0, 6)
- Clockwise
- WULA_Alloy
- Normal
-
-
-
- (4,1,4,1)
- (8,1,8,1)
-
- Opposite
-
-
-
- (1,4,1,4)
- (1,8,1,8)
-
Counterclockwise
- (11,4,11,4)
- (11,8,11,8)
+ (11,2,11,2)
+ (11,10,11,10)
Clockwise
-
- (4, 0, 11)
-
-
- (8, 0, 11)
- 17
-
-
-
- (1, 0, 1)
- (1, 0, 3)
- (1, 0, 9)
- (1, 0, 11)
-
- Clockwise
- WULA_Alloy
- Normal
-
-
- (11, 0, 1)
- Counterclockwise
- WULA_Alloy
- Normal
- 149
-
-
-
- (11, 0, 3)
- (11, 0, 9)
- (11, 0, 11)
-
- Counterclockwise
- WULA_Alloy
- Normal
-
-
-
- (4,5,4,7)
-
-
-
-
- (1, 0, 4)
- (3, 0, 4)
- (8, 0, 4)
- (10, 0, 4)
- (1, 0, 8)
- (3, 0, 8)
- (8, 0, 8)
- (10, 0, 8)
-
- Synthread
-
- (0,0,5,0)
- (7,0,12,0)
- (0,1,0,12)
- (12,2,12,2)
- (12,4,12,12)
- (1,12,5,12)
- (8,12,11,12)
+ (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)
-
-
- (12,1,12,1)
- (12,3,12,3)
- (7,12,7,12)
-
- 1199
-
-
-
- (4, 0, 1)
- (7, 0, 1)
-
- WULA_Alloy
-
-
- (5, 0, 11)
- Opposite
- WULA_Alloy
-
-
+
(6, 0, 6)
-
+
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs
index 138b1676..535f6b59 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs
@@ -35,6 +35,7 @@ namespace WulaFallenEmpire.EventSystem.AI
private float _thinkingStartTime;
private int _thinkingPhaseIndex = 1;
private bool _thinkingPhaseRetry;
+ private float _lastThinkingDuration;
private bool _lastActionExecuted;
private bool _lastActionHadError;
@@ -192,6 +193,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
public int ThinkingPhaseIndex => _thinkingPhaseIndex;
public bool ThinkingPhaseRetry => _thinkingPhaseRetry;
public int ThinkingPhaseTotal => _thinkingPhaseTotal;
+ public float LastThinkingDuration => _lastThinkingDuration;
public void InitializeConversation(string eventDefName)
{
if (string.IsNullOrWhiteSpace(eventDefName))
@@ -439,6 +441,11 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
return;
}
+ if (_isThinking && !isThinking)
+ {
+ _lastThinkingDuration = Mathf.Max(0f, Time.realtimeSinceStartup - _thinkingStartTime);
+ }
+
_isThinking = isThinking;
OnThinkingStateChanged?.Invoke(_isThinking);
}
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs
index 83c5f889..40d9b2d4 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using RimWorld;
using UnityEngine;
using Verse;
+using WulaFallenEmpire;
using WulaFallenEmpire.EventSystem.AI;
using WulaFallenEmpire.EventSystem.AI.Utils;
using System.Text.RegularExpressions;
@@ -29,6 +30,13 @@ 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 class CachedMessage
{
@@ -40,6 +48,13 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
public GameFont font;
}
+ private class ReactTraceStep
+ {
+ public int Step;
+ public List Calls = new List();
+ public List Results = new List();
+ }
+
public static Dialog_AIConversation Instance { get; private set; }
public Dialog_AIConversation(EventDef def) : base(def)
@@ -353,6 +368,8 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
curY += height + 10f;
}
+
+ UpdateReactTraceCache(history, contentWidth, ref curY);
_cachedTotalHeight = curY;
}
@@ -409,6 +426,15 @@ 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;
@@ -445,6 +471,186 @@ 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)
+ {
+ _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;
+
+ int stepIndex = 0;
+ for (int i = lastUserIndex + 1; i < history.Count; i++)
+ {
+ var entry = history[i];
+ if (entry.role != "toolcall") continue;
+
+ stepIndex++;
+ var step = new ReactTraceStep { Step = stepIndex };
+
+ if (JsonToolCallParser.TryParseToolCallsFromText(entry.message, out var calls, out _))
+ {
+ foreach (var call in calls)
+ {
+ if (string.IsNullOrWhiteSpace(call?.Name)) continue;
+ string args = call.ArgumentsJson;
+ string callText = string.IsNullOrWhiteSpace(args) || args == "{}"
+ ? call.Name
+ : $"{call.Name} {TrimForDisplay(args, 160)}";
+ step.Calls.Add(callText);
+ }
+ }
+
+ if (i + 1 < history.Count && history[i + 1].role == "tool")
+ {
+ step.Results.AddRange(ExtractToolResultLines(history[i + 1].message, 4));
+ i++;
+ }
+
+ if (step.Calls.Count > 0 || step.Results.Count > 0)
+ {
+ steps.Add(step);
+ }
+ }
+
+ return steps;
+ }
+
+ private static List ExtractToolResultLines(string toolMessage, int maxLines)
+ {
+ var lines = new List();
+ if (string.IsNullOrWhiteSpace(toolMessage)) return lines;
+
+ string[] rawLines = toolMessage.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
+ foreach (string raw in rawLines)
+ {
+ string line = raw.Trim();
+ if (string.IsNullOrEmpty(line)) continue;
+ if (line.StartsWith("[Tool Results]", StringComparison.OrdinalIgnoreCase)) continue;
+ lines.Add(TrimForDisplay(line, 200));
+ if (lines.Count >= maxLines) break;
+ }
+
+ return lines;
+ }
+
+ private static string TrimForDisplay(string text, int maxChars)
+ {
+ if (string.IsNullOrEmpty(text) || text.Length <= maxChars) return text ?? "";
+ return text.Substring(0, maxChars) + "...";
+ }
+
+ private string BuildReactTraceHeader()
+ {
+ string state = _isThinking ? "思考中" : "已思考";
+ float elapsed = _isThinking
+ ? 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})";
+ }
+
+ private void DrawReactTracePanel(Rect rect)
+ {
+ 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);
+ 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);
+
+ if (Widgets.ButtonInvisible(headerRect))
+ {
+ _reactTraceExpanded = !_reactTraceExpanded;
+ _lastHistoryCount = -1;
+ _lastUsedWidth = -1f;
+ }
+
+ if (_reactTraceExpanded && _reactTraceLines.Count > 0)
+ {
+ float y = headerRect.yMax + 2f;
+ foreach (string line in _reactTraceLines)
+ {
+ float lineHeight = Text.CalcHeight(line, rect.width - padding * 2f) + 2f;
+ Rect lineRect = new Rect(rect.x + padding, y, rect.width - padding * 2f, lineHeight);
+ GUI.color = new Color(0.85f, 0.85f, 0.85f, 1f);
+ Widgets.Label(lineRect, line);
+ y += lineHeight;
+ }
+ }
+
+ GUI.color = originalColor;
+ Text.Font = originalFont;
+ Text.Anchor = originalAnchor;
+ }
+
private string BuildThinkingStatus()
{
if (_core == null) return "Thinking...";
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs
index 42be57b2..44d98a32 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink.cs
@@ -4,6 +4,7 @@ using System.Linq;
using UnityEngine;
using Verse;
using RimWorld;
+using WulaFallenEmpire;
using WulaFallenEmpire.EventSystem.AI;
using WulaFallenEmpire.EventSystem.AI.Utils;
@@ -24,6 +25,13 @@ 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 class CachedMessage
{
@@ -34,6 +42,13 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
public float yOffset;
}
+ private class ReactTraceStep
+ {
+ public int Step;
+ public List Calls = new List();
+ public List Results = new List();
+ }
+
// HUD / Minimized State
private bool _isMinimized = false;
@@ -410,6 +425,7 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
curY += h + reducedSpacing;
}
+ UpdateReactTraceCache(history, width, ref curY);
_cachedTotalHeight = curY;
}
@@ -464,6 +480,15 @@ 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;
@@ -490,6 +515,189 @@ 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)
+ {
+ _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;
+
+ int stepIndex = 0;
+ for (int i = lastUserIndex + 1; i < history.Count; i++)
+ {
+ var entry = history[i];
+ if (entry.role != "toolcall") continue;
+
+ stepIndex++;
+ var step = new ReactTraceStep { Step = stepIndex };
+
+ if (JsonToolCallParser.TryParseToolCallsFromText(entry.message, out var calls, out _))
+ {
+ foreach (var call in calls)
+ {
+ if (string.IsNullOrWhiteSpace(call?.Name)) continue;
+ string args = call.ArgumentsJson;
+ string callText = string.IsNullOrWhiteSpace(args) || args == "{}"
+ ? call.Name
+ : $"{call.Name} {TrimForDisplay(args, 160)}";
+ step.Calls.Add(callText);
+ }
+ }
+
+ if (i + 1 < history.Count && history[i + 1].role == "tool")
+ {
+ step.Results.AddRange(ExtractToolResultLines(history[i + 1].message, 4));
+ i++;
+ }
+
+ if (step.Calls.Count > 0 || step.Results.Count > 0)
+ {
+ steps.Add(step);
+ }
+ }
+
+ return steps;
+ }
+
+ private static List ExtractToolResultLines(string toolMessage, int maxLines)
+ {
+ var lines = new List();
+ if (string.IsNullOrWhiteSpace(toolMessage)) return lines;
+
+ string[] rawLines = toolMessage.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
+ foreach (string raw in rawLines)
+ {
+ string line = raw.Trim();
+ if (string.IsNullOrEmpty(line)) continue;
+ if (line.StartsWith("[Tool Results]", StringComparison.OrdinalIgnoreCase)) continue;
+ lines.Add(TrimForDisplay(line, 200));
+ if (lines.Count >= maxLines) break;
+ }
+
+ return lines;
+ }
+
+ private static string TrimForDisplay(string text, int maxChars)
+ {
+ if (string.IsNullOrEmpty(text) || text.Length <= maxChars) return text ?? "";
+ return text.Substring(0, maxChars) + "...";
+ }
+
+ private string BuildReactTraceHeader()
+ {
+ string state = _core != null && _core.IsThinking ? "思考中" : "已思考";
+ float startTime = _core?.ThinkingStartTime ?? 0f;
+ float elapsed = _core != null && _core.IsThinking
+ ? Mathf.Max(0f, Time.realtimeSinceStartup - startTime)
+ : _core?.LastThinkingDuration ?? 0f;
+ 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})";
+ }
+
+ private void DrawReactTracePanel(Rect rect)
+ {
+ 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);
+ 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);
+
+ if (Widgets.ButtonInvisible(headerRect))
+ {
+ _reactTraceExpanded = !_reactTraceExpanded;
+ _lastHistoryCount = -1;
+ _lastUsedWidth = -1f;
+ }
+
+ if (_reactTraceExpanded && _reactTraceLines.Count > 0)
+ {
+ float y = headerRect.yMax + 2f;
+ foreach (string line in _reactTraceLines)
+ {
+ float lineHeight = Text.CalcHeight(line, rect.width - padding * 2f) + 2f;
+ Rect lineRect = new Rect(rect.x + padding, y, rect.width - padding * 2f, lineHeight);
+ GUI.color = new Color(0.85f, 0.85f, 0.85f, 1f);
+ Widgets.Label(lineRect, line);
+ y += lineHeight;
+ }
+ }
+
+ GUI.color = originalColor;
+ Text.Font = originalFont;
+ Text.Anchor = originalAnchor;
+ }
+
private void DrawFooter(Rect rect)
{
Widgets.DrawBoxSolid(rect, WulaLinkStyles.InputBarColor);