thrust-cli 1.0.11 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mcps/ThrustMCPBridge.cs +226 -0
- package/package.json +1 -1
- package/utils/daemon.js +82 -35
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
using UnityEngine;
|
|
2
|
+
using UnityEditor;
|
|
3
|
+
using System.Net;
|
|
4
|
+
using System.Threading;
|
|
5
|
+
using System.IO;
|
|
6
|
+
using System.Text;
|
|
7
|
+
using System.Collections.Generic;
|
|
8
|
+
using UnityEngine.SceneManagement;
|
|
9
|
+
|
|
10
|
+
[InitializeOnLoad]
|
|
11
|
+
public static class ThrustMCPBridge
|
|
12
|
+
{
|
|
13
|
+
private static HttpListener listener;
|
|
14
|
+
private static Thread listenerThread;
|
|
15
|
+
|
|
16
|
+
// Buffers
|
|
17
|
+
private static List<string> activityBuffer = new List<string>();
|
|
18
|
+
private static List<string> consoleBuffer = new List<string>();
|
|
19
|
+
private static readonly object bufferLock = new object();
|
|
20
|
+
|
|
21
|
+
// Main Thread Dispatcher variables
|
|
22
|
+
private static string pendingAction = null;
|
|
23
|
+
private static string pendingArgument = null;
|
|
24
|
+
private static string actionResult = null;
|
|
25
|
+
private static volatile bool isProcessingOnMainThread = false;
|
|
26
|
+
|
|
27
|
+
private const int PORT = 8081;
|
|
28
|
+
|
|
29
|
+
static ThrustMCPBridge()
|
|
30
|
+
{
|
|
31
|
+
// 1. Hook into Unity Editor Events
|
|
32
|
+
EditorApplication.hierarchyChanged += OnHierarchyChanged;
|
|
33
|
+
EditorApplication.playModeStateChanged += OnPlayModeChanged;
|
|
34
|
+
UnityEditor.SceneManagement.EditorSceneManager.sceneSaved += OnSceneSaved;
|
|
35
|
+
Application.logMessageReceived += OnLogMessage;
|
|
36
|
+
|
|
37
|
+
// Hook into the main thread loop to process read requests
|
|
38
|
+
EditorApplication.update += OnEditorUpdate;
|
|
39
|
+
|
|
40
|
+
// 2. Start the local MCP Server
|
|
41
|
+
StartServer();
|
|
42
|
+
EditorApplication.quitting += StopServer;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private static void AddActivity(string activity)
|
|
46
|
+
{
|
|
47
|
+
lock (bufferLock)
|
|
48
|
+
{
|
|
49
|
+
activityBuffer.Add($"[{System.DateTime.Now.ToShortTimeString()}] {activity}");
|
|
50
|
+
if (activityBuffer.Count > 50) activityBuffer.RemoveAt(0);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private static void OnLogMessage(string logString, string stackTrace, LogType type)
|
|
55
|
+
{
|
|
56
|
+
lock (bufferLock)
|
|
57
|
+
{
|
|
58
|
+
if (type == LogType.Error || type == LogType.Exception || type == LogType.Warning)
|
|
59
|
+
{
|
|
60
|
+
consoleBuffer.Add($"[{type}] {logString}");
|
|
61
|
+
if (consoleBuffer.Count > 30) consoleBuffer.RemoveAt(0);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private static void OnHierarchyChanged() => AddActivity("Hierarchy changed.");
|
|
67
|
+
private static void OnSceneSaved(Scene scene) => AddActivity($"Saved scene: {scene.name}");
|
|
68
|
+
private static void OnPlayModeChanged(PlayModeStateChange state) => AddActivity($"Play mode state: {state}");
|
|
69
|
+
|
|
70
|
+
// --- HTTP SERVER LOOP (Background Thread) ---
|
|
71
|
+
private static void StartServer()
|
|
72
|
+
{
|
|
73
|
+
if (listener != null && listener.IsListening) return;
|
|
74
|
+
listener = new HttpListener();
|
|
75
|
+
listener.Prefixes.Add($"http://localhost:{PORT}/");
|
|
76
|
+
listener.Start();
|
|
77
|
+
|
|
78
|
+
listenerThread = new Thread(ListenForRequests) { IsBackground = true };
|
|
79
|
+
listenerThread.Start();
|
|
80
|
+
Debug.Log($"[Thrust MCP] Two-Way Unity Bridge active on port {PORT}...");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private static void ListenForRequests()
|
|
84
|
+
{
|
|
85
|
+
while (listener != null && listener.IsListening)
|
|
86
|
+
{
|
|
87
|
+
try { ProcessRequest(listener.GetContext()); }
|
|
88
|
+
catch (HttpListenerException) { /* Ignored on shutdown */ }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private static void ProcessRequest(HttpListenerContext context)
|
|
93
|
+
{
|
|
94
|
+
var request = context.Request;
|
|
95
|
+
var response = context.Response;
|
|
96
|
+
|
|
97
|
+
response.AddHeader("Access-Control-Allow-Origin", "*");
|
|
98
|
+
response.AddHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
99
|
+
|
|
100
|
+
if (request.HttpMethod == "OPTIONS") { SendResponse(response, ""); return; }
|
|
101
|
+
|
|
102
|
+
string requestBody;
|
|
103
|
+
using (var reader = new StreamReader(request.InputStream, request.ContentEncoding))
|
|
104
|
+
{
|
|
105
|
+
requestBody = reader.ReadToEnd();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
string resultText = "Unknown command";
|
|
109
|
+
|
|
110
|
+
// 1. Parse requested tool name (Crude JSON parsing to avoid dependencies)
|
|
111
|
+
if (requestBody.Contains("\"name\":\"get_recent_activity\"") || requestBody.Contains("\"name\": \"get_recent_activity\""))
|
|
112
|
+
{
|
|
113
|
+
lock (bufferLock)
|
|
114
|
+
{
|
|
115
|
+
resultText = activityBuffer.Count == 0 ? "No new activity" : string.Join("\\n", activityBuffer);
|
|
116
|
+
activityBuffer.Clear();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (requestBody.Contains("\"name\":\"get_console_logs\"") || requestBody.Contains("\"name\": \"get_console_logs\""))
|
|
120
|
+
{
|
|
121
|
+
lock (bufferLock)
|
|
122
|
+
{
|
|
123
|
+
resultText = consoleBuffer.Count == 0 ? "No recent errors/warnings." : string.Join("\\n", consoleBuffer).Replace("\"", "'");
|
|
124
|
+
consoleBuffer.Clear();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else if (requestBody.Contains("\"name\":\"get_hierarchy\"") || requestBody.Contains("\"name\": \"get_hierarchy\""))
|
|
128
|
+
{
|
|
129
|
+
resultText = DispatchToMainThread("get_hierarchy", "");
|
|
130
|
+
}
|
|
131
|
+
else if (requestBody.Contains("\"name\":\"get_properties\"") || requestBody.Contains("\"name\": \"get_properties\""))
|
|
132
|
+
{
|
|
133
|
+
// Extract the target parameter safely (e.g., "target": "Player")
|
|
134
|
+
string target = ExtractJsonValue(requestBody, "target");
|
|
135
|
+
resultText = DispatchToMainThread("get_properties", target);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Format as JSON-RPC response
|
|
139
|
+
string responseJson = $@"{{
|
|
140
|
+
""jsonrpc"": ""2.0"",
|
|
141
|
+
""result"": {{ ""content"": [{{ ""type"": ""text"", ""text"": ""{resultText}"" }}] }}
|
|
142
|
+
}}";
|
|
143
|
+
|
|
144
|
+
SendResponse(response, responseJson);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- MAIN THREAD DISPATCHER ---
|
|
148
|
+
// Passes the request to the main thread and waits for the result
|
|
149
|
+
private static string DispatchToMainThread(string action, string arg)
|
|
150
|
+
{
|
|
151
|
+
pendingAction = action;
|
|
152
|
+
pendingArgument = arg;
|
|
153
|
+
actionResult = null;
|
|
154
|
+
isProcessingOnMainThread = true;
|
|
155
|
+
|
|
156
|
+
// Block background thread until main thread finishes
|
|
157
|
+
while (isProcessingOnMainThread) { Thread.Sleep(10); }
|
|
158
|
+
|
|
159
|
+
return actionResult;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Executes on Unity's Main Thread
|
|
163
|
+
private static void OnEditorUpdate()
|
|
164
|
+
{
|
|
165
|
+
if (!isProcessingOnMainThread) return;
|
|
166
|
+
|
|
167
|
+
try
|
|
168
|
+
{
|
|
169
|
+
if (pendingAction == "get_hierarchy")
|
|
170
|
+
{
|
|
171
|
+
var rootObjects = SceneManager.GetActiveScene().GetRootGameObjects();
|
|
172
|
+
string hierarchy = "Active Scene Hierarchy:\\n";
|
|
173
|
+
foreach (var go in rootObjects) hierarchy += $"- {go.name}\\n";
|
|
174
|
+
actionResult = hierarchy;
|
|
175
|
+
}
|
|
176
|
+
else if (pendingAction == "get_properties")
|
|
177
|
+
{
|
|
178
|
+
if (string.IsNullOrEmpty(pendingArgument)) {
|
|
179
|
+
actionResult = "Error: No target specified.";
|
|
180
|
+
} else {
|
|
181
|
+
GameObject go = GameObject.Find(pendingArgument);
|
|
182
|
+
if (go == null) {
|
|
183
|
+
actionResult = $"GameObject '{pendingArgument}' not found in active scene.";
|
|
184
|
+
} else {
|
|
185
|
+
actionResult = $"Properties for '{go.name}':\\nPosition: {go.transform.position}\\nRotation: {go.transform.eulerAngles}\\nComponents:\\n";
|
|
186
|
+
foreach (var comp in go.GetComponents<Component>())
|
|
187
|
+
{
|
|
188
|
+
if(comp != null) actionResult += $"- {comp.GetType().Name}\\n";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (System.Exception e) { actionResult = $"Error reading Unity state: {e.Message}"; }
|
|
195
|
+
|
|
196
|
+
isProcessingOnMainThread = false; // Release the background thread
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Utility
|
|
200
|
+
private static string ExtractJsonValue(string json, string key)
|
|
201
|
+
{
|
|
202
|
+
string search = $"\"{key}\":";
|
|
203
|
+
int index = json.IndexOf(search);
|
|
204
|
+
if (index == -1) search = $"\"{key}\" :"; index = json.IndexOf(search);
|
|
205
|
+
if (index == -1) return "";
|
|
206
|
+
|
|
207
|
+
int start = json.IndexOf("\"", index + search.Length) + 1;
|
|
208
|
+
int end = json.IndexOf("\"", start);
|
|
209
|
+
return json.Substring(start, end - start);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private static void SendResponse(HttpListenerResponse response, string json)
|
|
213
|
+
{
|
|
214
|
+
byte[] buffer = Encoding.UTF8.GetBytes(json);
|
|
215
|
+
response.ContentType = "application/json";
|
|
216
|
+
response.ContentLength64 = buffer.Length;
|
|
217
|
+
response.OutputStream.Write(buffer, 0, buffer.Length);
|
|
218
|
+
response.OutputStream.Close();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private static void StopServer()
|
|
222
|
+
{
|
|
223
|
+
if (listener != null) { listener.Stop(); listener.Close(); listener = null; }
|
|
224
|
+
if (listenerThread != null) { listenerThread.Abort(); listenerThread = null; }
|
|
225
|
+
}
|
|
226
|
+
}
|
package/package.json
CHANGED
package/utils/daemon.js
CHANGED
|
@@ -20,8 +20,8 @@ const API_URL = GATEWAY_URL.replace('ws://', 'http://').replace('wss://', 'https
|
|
|
20
20
|
const AUTH_PROXY_URL = "https://everydaycats-thrust-auth-server.hf.space";
|
|
21
21
|
|
|
22
22
|
// --- DEBOUNCE & POLLING TIMERS ---
|
|
23
|
-
const INACTIVITY_DELAY_MS = 15 * 1000; //
|
|
24
|
-
const MCP_POLL_INTERVAL_MS = 18 * 1000; //
|
|
23
|
+
const INACTIVITY_DELAY_MS = 15 * 1000; // Wait for 15 seconds of silence across ALL inputs before sending
|
|
24
|
+
const MCP_POLL_INTERVAL_MS = 18 * 1000; // Poll external tools every 18 seconds
|
|
25
25
|
|
|
26
26
|
let currentWatcher = null;
|
|
27
27
|
let inactivityTimer = null;
|
|
@@ -59,6 +59,18 @@ function isBinaryData(buffer) {
|
|
|
59
59
|
return false;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// --- CENTRAL DEBOUNCER ---
|
|
63
|
+
// Called by local file changes AND MCP events. It resets the 15s timer.
|
|
64
|
+
function triggerDebouncedSync() {
|
|
65
|
+
const activeProject = getActiveProject();
|
|
66
|
+
if (!activeProject || !activeProject.path) return;
|
|
67
|
+
|
|
68
|
+
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
69
|
+
inactivityTimer = setTimeout(() => {
|
|
70
|
+
syncContext(activeProject.path);
|
|
71
|
+
}, INACTIVITY_DELAY_MS);
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
export async function startDaemon(preferredPort) {
|
|
63
75
|
const actualPort = await findAvailablePort(preferredPort);
|
|
64
76
|
const app = express();
|
|
@@ -67,7 +79,7 @@ export async function startDaemon(preferredPort) {
|
|
|
67
79
|
const origin = req.header('Origin');
|
|
68
80
|
const allowedWebApps = ['https://thrust.web.app', 'http://localhost:3000'];
|
|
69
81
|
if (req.path.startsWith('/api/mcp')) {
|
|
70
|
-
callback(null, { origin: true });
|
|
82
|
+
callback(null, { origin: true });
|
|
71
83
|
} else if (req.path === '/api/auth/callback') {
|
|
72
84
|
if (!origin || allowedWebApps.includes(origin) || origin === `http://localhost:${actualPort}`) {
|
|
73
85
|
callback(null, { origin: true, credentials: true });
|
|
@@ -102,7 +114,7 @@ export async function startDaemon(preferredPort) {
|
|
|
102
114
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
103
115
|
});
|
|
104
116
|
const data = await response.json();
|
|
105
|
-
|
|
117
|
+
|
|
106
118
|
res.json({
|
|
107
119
|
projectId,
|
|
108
120
|
projectPath: config.leads[projectId].path,
|
|
@@ -114,30 +126,26 @@ export async function startDaemon(preferredPort) {
|
|
|
114
126
|
}
|
|
115
127
|
});
|
|
116
128
|
|
|
117
|
-
app.post('/api/mcp/timeline',
|
|
129
|
+
app.post('/api/mcp/timeline', (req, res) => {
|
|
118
130
|
const { source, action_type, description, requires_code_sync } = req.body;
|
|
119
|
-
|
|
131
|
+
|
|
120
132
|
if (!source || !description) {
|
|
121
133
|
return res.status(400).json({ error: "Malformed MCP payload." });
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
broadcastLocalLog('mcp', `🔗 [${source} Pushed Event] ${action_type}: ${description}`);
|
|
125
137
|
|
|
138
|
+
// Add to buffer and reset the 15-second countdown
|
|
126
139
|
fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP EXT EVENT] Source: ${source} | Action: ${action_type} | Desc: ${description} | Needs Code Sync: ${requires_code_sync ? 'Yes' : 'No'}\n`;
|
|
140
|
+
triggerDebouncedSync();
|
|
127
141
|
|
|
128
|
-
|
|
129
|
-
if (activeProject && activeProject.path) {
|
|
130
|
-
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
131
|
-
await syncContext(activeProject.path);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
res.json({ success: true, message: "Timeline event ingested and sync triggered." });
|
|
142
|
+
res.json({ success: true, message: "Timeline event ingested to buffer." });
|
|
135
143
|
});
|
|
136
144
|
|
|
137
145
|
// ==========================================
|
|
138
146
|
// MCP CLIENT CONFIGURATION (FEED-IN PULL)
|
|
139
147
|
// ==========================================
|
|
140
|
-
|
|
148
|
+
|
|
141
149
|
app.get('/api/mcp/servers', (req, res) => {
|
|
142
150
|
res.json(getConfig().mcpServers || []);
|
|
143
151
|
});
|
|
@@ -145,13 +153,12 @@ export async function startDaemon(preferredPort) {
|
|
|
145
153
|
app.post('/api/mcp/servers', (req, res) => {
|
|
146
154
|
const { name, url } = req.body;
|
|
147
155
|
if (!name || !url) return res.status(400).json({ error: "Missing name or url" });
|
|
148
|
-
|
|
156
|
+
|
|
149
157
|
const config = getConfig();
|
|
150
158
|
if (!config.mcpServers) config.mcpServers = [];
|
|
151
159
|
config.mcpServers.push({ name, url, type: 'http' });
|
|
152
160
|
saveConfig(config);
|
|
153
|
-
|
|
154
|
-
// Trigger an immediate poll when a new service is added
|
|
161
|
+
|
|
155
162
|
pollExternalMCPServers();
|
|
156
163
|
res.json({ success: true });
|
|
157
164
|
});
|
|
@@ -166,6 +173,53 @@ export async function startDaemon(preferredPort) {
|
|
|
166
173
|
res.json({ success: true });
|
|
167
174
|
});
|
|
168
175
|
|
|
176
|
+
// ==========================================
|
|
177
|
+
// EXTERNAL MCP DIRECT QUERY (AI OR MANUAL)
|
|
178
|
+
// ==========================================
|
|
179
|
+
app.post('/api/mcp/query', async (req, res) => {
|
|
180
|
+
const { serverName, toolName, targetArg } = req.body;
|
|
181
|
+
const config = getConfig();
|
|
182
|
+
|
|
183
|
+
const server = config.mcpServers?.find(s => s.name.toLowerCase() === serverName.toLowerCase());
|
|
184
|
+
|
|
185
|
+
if (!server) return res.status(404).json({ error: `MCP Server '${serverName}' not found.` });
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const payload = {
|
|
189
|
+
jsonrpc: "2.0",
|
|
190
|
+
method: "tools/call",
|
|
191
|
+
params: {
|
|
192
|
+
name: toolName,
|
|
193
|
+
arguments: targetArg ? { target: targetArg } : {}
|
|
194
|
+
},
|
|
195
|
+
id: Date.now()
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const response = await fetch(server.url, {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
headers: { 'Content-Type': 'application/json' },
|
|
201
|
+
body: JSON.stringify(payload)
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (!response.ok) throw new Error("Server responded with error");
|
|
205
|
+
|
|
206
|
+
const data = await response.json();
|
|
207
|
+
const resultText = data.result?.content?.[0]?.text || "No data returned.";
|
|
208
|
+
|
|
209
|
+
broadcastLocalLog('mcp', `⚡ Queried ${serverName} for ${toolName}.`);
|
|
210
|
+
|
|
211
|
+
// Add the query result to the buffer and reset the 15-second countdown
|
|
212
|
+
fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP DIRECT QUERY RESULT] Source: ${serverName} | Tool: ${toolName} | Target: ${targetArg || 'none'} \nResult:\n${resultText}\n`;
|
|
213
|
+
triggerDebouncedSync();
|
|
214
|
+
|
|
215
|
+
res.json({ success: true, data: resultText });
|
|
216
|
+
|
|
217
|
+
} catch (e) {
|
|
218
|
+
broadcastLocalLog('error', `⚠️ Failed to query MCP server: ${serverName}`);
|
|
219
|
+
res.status(500).json({ error: e.message });
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
169
223
|
// ==========================================
|
|
170
224
|
// CORE DASHBOARD API
|
|
171
225
|
// ==========================================
|
|
@@ -234,6 +288,7 @@ export async function startDaemon(preferredPort) {
|
|
|
234
288
|
if (!response.ok) throw new Error(data.error);
|
|
235
289
|
|
|
236
290
|
if (config.leads[projectId]?.path) {
|
|
291
|
+
// Instantly sync when a task is manually completed to update cloud state fast
|
|
237
292
|
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
238
293
|
syncContext(config.leads[projectId].path);
|
|
239
294
|
}
|
|
@@ -320,10 +375,10 @@ export async function startDaemon(preferredPort) {
|
|
|
320
375
|
saveConfig(config);
|
|
321
376
|
|
|
322
377
|
await startWatching(folderPath);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
378
|
+
|
|
379
|
+
// Initial sync on link
|
|
380
|
+
triggerDebouncedSync();
|
|
381
|
+
|
|
327
382
|
res.json({ success: true });
|
|
328
383
|
});
|
|
329
384
|
|
|
@@ -402,7 +457,6 @@ async function pollExternalMCPServers() {
|
|
|
402
457
|
|
|
403
458
|
for (const server of config.mcpServers) {
|
|
404
459
|
try {
|
|
405
|
-
// Send standard MCP JSON-RPC payload asking for recent activity
|
|
406
460
|
const res = await fetch(server.url, {
|
|
407
461
|
method: 'POST',
|
|
408
462
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -416,11 +470,9 @@ async function pollExternalMCPServers() {
|
|
|
416
470
|
|
|
417
471
|
if (res.ok) {
|
|
418
472
|
const data = await res.json();
|
|
419
|
-
|
|
420
|
-
// Parse standard MCP response { result: { content: [{ text: "..." }] } }
|
|
421
473
|
if (data.result && data.result.content && data.result.content.length > 0) {
|
|
422
474
|
const updateText = data.result.content[0].text;
|
|
423
|
-
|
|
475
|
+
|
|
424
476
|
if (updateText && updateText.trim() !== "" && updateText.trim() !== "No new activity") {
|
|
425
477
|
fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP POLL] Source: ${server.name} | Update: ${updateText}\n`;
|
|
426
478
|
broadcastLocalLog('mcp', `🔗 Pulled new data from ${server.name}`);
|
|
@@ -429,16 +481,13 @@ async function pollExternalMCPServers() {
|
|
|
429
481
|
}
|
|
430
482
|
}
|
|
431
483
|
} catch (e) {
|
|
432
|
-
|
|
484
|
+
// Silently fail to avoid spamming the logs if Unity is temporarily closed
|
|
433
485
|
}
|
|
434
486
|
}
|
|
435
487
|
|
|
436
488
|
if (hasNewData) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
440
|
-
await syncContext(activeProject.path);
|
|
441
|
-
}
|
|
489
|
+
// Add to buffer and reset the 15-second countdown
|
|
490
|
+
triggerDebouncedSync();
|
|
442
491
|
}
|
|
443
492
|
}
|
|
444
493
|
|
|
@@ -465,10 +514,8 @@ async function startWatching(projectPath) {
|
|
|
465
514
|
fileActivityBuffer += `[${new Date().toLocaleTimeString()}] ${event.toUpperCase()}: ${relativePath}\n`;
|
|
466
515
|
broadcastLocalLog('watch', `[${event.toUpperCase()}] ${relativePath}`);
|
|
467
516
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
syncContext(projectPath);
|
|
471
|
-
}, INACTIVITY_DELAY_MS);
|
|
517
|
+
// Reset the 15-second countdown
|
|
518
|
+
triggerDebouncedSync();
|
|
472
519
|
});
|
|
473
520
|
} catch (err) {}
|
|
474
521
|
}
|