@el4cteo/rbx-studio-mcp 0.8.3 → 0.8.6
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/README.md +24 -6
- package/dist/bridge/rpc.js +1 -1
- package/dist/bridge/rpc.js.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/apidump.js +75 -0
- package/dist/lib/apidump.js.map +1 -1
- package/dist/lib/errors.js +5 -4
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/format.js +92 -25
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/notices.js +29 -0
- package/dist/lib/notices.js.map +1 -0
- package/dist/lib/protocol.js.map +1 -1
- package/dist/lib/sync.js +1141 -0
- package/dist/lib/sync.js.map +1 -0
- package/dist/lib/syncplan.js +338 -0
- package/dist/lib/syncplan.js.map +1 -0
- package/dist/lib/tool.js +9 -2
- package/dist/lib/tool.js.map +1 -1
- package/dist/tools/discover.js +6 -1
- package/dist/tools/discover.js.map +1 -1
- package/dist/tools/exec.js +5 -0
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/instances.js +2 -2
- package/dist/tools/instances.js.map +1 -1
- package/dist/tools/perf.js +28 -3
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/screenshot.js +7 -3
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/scripts.js +110 -28
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/sync.js +169 -0
- package/dist/tools/sync.js.map +1 -0
- package/package.json +4 -4
- package/plugin/src/Commands.luau +72 -5
- package/plugin/src/Config.luau +65 -65
- package/plugin/src/Console.luau +5 -0
- package/plugin/src/Dispatch.luau +131 -90
- package/plugin/src/ExecRuntime.luau +190 -168
- package/plugin/src/LogBuffer.luau +38 -9
- package/plugin/src/Paths.luau +42 -0
- package/plugin/src/Phrase.luau +41 -0
- package/plugin/src/Prompt.luau +23 -3
- package/plugin/src/ScriptEdit.luau +94 -8
- package/plugin/src/Serialize.luau +16 -1
- package/plugin/src/Transport.luau +5 -2
- package/plugin/src/Undo.luau +74 -10
- package/plugin/src/handlers/Capture.luau +818 -809
- package/plugin/src/handlers/Debug.luau +19 -16
- package/plugin/src/handlers/Discover.luau +31 -1
- package/plugin/src/handlers/Perf.luau +100 -21
- package/plugin/src/handlers/Scripts.luau +274 -90
- package/plugin/src/handlers/Sync.luau +968 -0
- package/plugin/src/init.server.luau +47 -2
- package/scripts/build.mjs +14 -0
- package/scripts/sync-fake.mjs +191 -0
- package/scripts/test-live-sync-scale.mjs +150 -0
- package/scripts/test-live-sync.mjs +232 -0
- package/scripts/test-live-tools.mjs +6 -1
- package/scripts/test-plugin.mjs +17 -0
- package/scripts/test-results.mjs +41 -0
- package/scripts/test-sync-more.mjs +228 -0
- package/scripts/test-sync.mjs +245 -0
- package/dist/tools/spatial.js +0 -135
- package/dist/tools/spatial.js.map +0 -1
- package/dist/tools/upload.js +0 -294
- package/dist/tools/upload.js.map +0 -1
|
@@ -1,168 +1,190 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
-- Shared bounded execution results in the VM that calls run.
|
|
3
|
-
local LogService = game:GetService("LogService")
|
|
4
|
-
local LogBuffer = require(script.Parent.LogBuffer)
|
|
5
|
-
local Serialize = require(script.Parent.Serialize)
|
|
6
|
-
-- A chunk that prints in a loop would otherwise return a response no context
|
|
7
|
-
-- window can hold.
|
|
8
|
-
local MAX_OUTPUT_LINES = 200
|
|
9
|
-
local MAX_RETURN_VALUES = 10
|
|
10
|
-
|
|
11
|
-
-- Returned tables are walked rather than counted, but not without limit: a
|
|
12
|
-
-- chunk that returns a whole config tree, or one with a cycle in it, must not be
|
|
13
|
-
-- able to produce a response nothing can read.
|
|
14
|
-
local MAX_TABLE_ENTRIES = 50
|
|
15
|
-
local MAX_TABLE_DEPTH = 4
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
local
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
local
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
1
|
+
--!strict
|
|
2
|
+
-- Shared bounded execution results in the VM that calls run.
|
|
3
|
+
local LogService = game:GetService("LogService")
|
|
4
|
+
local LogBuffer = require(script.Parent.LogBuffer)
|
|
5
|
+
local Serialize = require(script.Parent.Serialize)
|
|
6
|
+
-- A chunk that prints in a loop would otherwise return a response no context
|
|
7
|
+
-- window can hold.
|
|
8
|
+
local MAX_OUTPUT_LINES = 200
|
|
9
|
+
local MAX_RETURN_VALUES = 10
|
|
10
|
+
|
|
11
|
+
-- Returned tables are walked rather than counted, but not without limit: a
|
|
12
|
+
-- chunk that returns a whole config tree, or one with a cycle in it, must not be
|
|
13
|
+
-- able to produce a response nothing can read.
|
|
14
|
+
local MAX_TABLE_ENTRIES = 50
|
|
15
|
+
local MAX_TABLE_DEPTH = 4
|
|
16
|
+
type Budget = { nodes: number, chars: number }
|
|
17
|
+
|
|
18
|
+
local function describe(value: any, depth: number?, seen: { [any]: boolean }?, shared: Budget?): any
|
|
19
|
+
local budget = shared or {nodes = 300, chars = 10000}
|
|
20
|
+
budget.nodes -= 1
|
|
21
|
+
budget.chars -= 30
|
|
22
|
+
if budget.nodes < 0 or budget.chars < 100 then return "<budget exceeded>" end
|
|
23
|
+
local level = depth or 0
|
|
24
|
+
local visited = seen or {}
|
|
25
|
+
local kind = typeof(value)
|
|
26
|
+
|
|
27
|
+
if kind == "function" or kind == "thread" then
|
|
28
|
+
return string.format("<%s>", kind)
|
|
29
|
+
end
|
|
30
|
+
if kind ~= "table" then
|
|
31
|
+
local result = Serialize.value(value)
|
|
32
|
+
if typeof(result) == "string" then
|
|
33
|
+
local cap = math.min(2000, math.floor(budget.chars / 6))
|
|
34
|
+
budget.chars -= math.min(#result, cap) * 6
|
|
35
|
+
return if #result > cap then string.sub(result, 1, cap) .. "<truncated>" else result
|
|
36
|
+
end
|
|
37
|
+
return result
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
local source = value :: { [any]: any }
|
|
41
|
+
-- Named separately from the depth cap, because they mean opposite things to
|
|
42
|
+
-- a reader. The depth cap says "there is more below this"; a cycle says
|
|
43
|
+
-- "this is the table you are already inside". Reporting the second as the
|
|
44
|
+
-- first printed `t.self.self.self` as three distinct nested tables, which
|
|
45
|
+
-- reads as real structure that does not exist.
|
|
46
|
+
if visited[source] then
|
|
47
|
+
return "<circular reference>"
|
|
48
|
+
end
|
|
49
|
+
if level >= MAX_TABLE_DEPTH then
|
|
50
|
+
return "<nested table>"
|
|
51
|
+
end
|
|
52
|
+
visited[source] = true
|
|
53
|
+
|
|
54
|
+
local count = 0
|
|
55
|
+
local isArray = true
|
|
56
|
+
for key in source do
|
|
57
|
+
if typeof(key) ~= "number" or key < 1 or key > #source or key % 1 ~= 0 then isArray = false end
|
|
58
|
+
count += 1
|
|
59
|
+
if count > MAX_TABLE_ENTRIES then break end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
-- Arrays keep their shape so they read as lists on the other side; anything
|
|
63
|
+
-- with non-sequential keys becomes a map with stringified keys.
|
|
64
|
+
if isArray and (count == #source or count > MAX_TABLE_ENTRIES) then
|
|
65
|
+
local list: { any } = {}
|
|
66
|
+
for index, item in ipairs(source) do
|
|
67
|
+
if index > MAX_TABLE_ENTRIES or budget.nodes <= 0 or budget.chars < 100 then
|
|
68
|
+
table.insert(list, "<more entries>")
|
|
69
|
+
break
|
|
70
|
+
end
|
|
71
|
+
table.insert(list, describe(item, level + 1, visited, budget))
|
|
72
|
+
end
|
|
73
|
+
-- Cleared on the way out so the same table appearing twice side by side
|
|
74
|
+
-- still renders twice; only a table containing itself is a cycle.
|
|
75
|
+
visited[source] = nil
|
|
76
|
+
return list
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
local map: { [string]: any } = {}
|
|
80
|
+
local shown = 0
|
|
81
|
+
for key, item in source do
|
|
82
|
+
shown += 1
|
|
83
|
+
if shown > MAX_TABLE_ENTRIES or budget.nodes <= 0 or budget.chars < 100 then
|
|
84
|
+
map["..."] = "<more entries>"
|
|
85
|
+
break
|
|
86
|
+
end
|
|
87
|
+
local name = string.sub(tostring(key), 1, 100)
|
|
88
|
+
budget.chars -= #name * 6
|
|
89
|
+
map[name] = describe(item, level + 1, visited, budget)
|
|
90
|
+
end
|
|
91
|
+
visited[source] = nil
|
|
92
|
+
return map
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
local Runtime = {}
|
|
96
|
+
Runtime.describe = describe
|
|
97
|
+
function Runtime.run(chunk: () -> ...any, timeout: number?): { [string]: any }
|
|
98
|
+
-- Captured rather than inferred: a chunk's prints are usually the point, and
|
|
99
|
+
-- reading them back from the log afterwards would also pick up whatever else
|
|
100
|
+
-- the place logged in the meantime.
|
|
101
|
+
local output: { { [string]: any } } = {}
|
|
102
|
+
local outputChars, outputDropped = 0, 0
|
|
103
|
+
local connection = LogService.MessageOut:Connect(function(message, messageType, context)
|
|
104
|
+
if #output < MAX_OUTPUT_LINES and outputChars < 8000 then
|
|
105
|
+
-- A structured log's context rides on the line, as it does in `console`.
|
|
106
|
+
local encoded = LogBuffer.encodeContext(context)
|
|
107
|
+
if encoded then
|
|
108
|
+
message ..= " " .. encoded
|
|
109
|
+
end
|
|
110
|
+
if #message > math.min(2000, 8000 - outputChars) then message = string.sub(message, 1, math.min(2000, 8000 - outputChars)) .. "<truncated>" end
|
|
111
|
+
outputChars += #message
|
|
112
|
+
table.insert(output, {
|
|
113
|
+
message = message,
|
|
114
|
+
level = if messageType == Enum.MessageType.MessageError
|
|
115
|
+
then "error"
|
|
116
|
+
elseif messageType == Enum.MessageType.MessageWarning then "warning"
|
|
117
|
+
elseif messageType == Enum.MessageType.MessageInfo then "info"
|
|
118
|
+
else "print",
|
|
119
|
+
})
|
|
120
|
+
else outputDropped += 1 end
|
|
121
|
+
end)
|
|
122
|
+
|
|
123
|
+
local started = os.clock()
|
|
124
|
+
local results: any = nil
|
|
125
|
+
if timeout then
|
|
126
|
+
local worker = task.spawn(function() results = table.pack(pcall(chunk :: any)) end)
|
|
127
|
+
local deadline = started + timeout
|
|
128
|
+
while results == nil and os.clock() < deadline do task.wait() end
|
|
129
|
+
if results == nil then
|
|
130
|
+
task.cancel(worker)
|
|
131
|
+
results = table.pack(false, "CLIENT_TIMEOUT: execution exceeded " .. tostring(timeout) .. "s")
|
|
132
|
+
end
|
|
133
|
+
else
|
|
134
|
+
results = table.pack(pcall(chunk :: () -> ...any))
|
|
135
|
+
end
|
|
136
|
+
local elapsed = os.clock() - started
|
|
137
|
+
|
|
138
|
+
--[[
|
|
139
|
+
MessageOut is deferred: `print` returns before the event fires, so
|
|
140
|
+
disconnecting straight after the chunk finished captured nothing at all.
|
|
141
|
+
A short settle picks the lines up.
|
|
142
|
+
|
|
143
|
+
It is bounded and stops as soon as the flow stops, rather than waiting a
|
|
144
|
+
fixed period, so a chunk that logged nothing costs almost no time and one
|
|
145
|
+
that logged plenty is not truncated.
|
|
146
|
+
]]
|
|
147
|
+
local settleDeadline = os.clock() + 0.5
|
|
148
|
+
local seen = #output
|
|
149
|
+
local quietFrames = 0
|
|
150
|
+
while os.clock() < settleDeadline and quietFrames < 3 do
|
|
151
|
+
task.wait()
|
|
152
|
+
if #output == seen then
|
|
153
|
+
quietFrames += 1
|
|
154
|
+
else
|
|
155
|
+
seen = #output
|
|
156
|
+
quietFrames = 0
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
connection:Disconnect()
|
|
161
|
+
|
|
162
|
+
if not results[1] then
|
|
163
|
+
return {
|
|
164
|
+
ok = false,
|
|
165
|
+
error = string.sub(tostring(results[2]), 1, 2000),
|
|
166
|
+
output = output,
|
|
167
|
+
milliseconds = math.floor(elapsed * 1000 + 0.5),
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
local returned: { any } = {}
|
|
172
|
+
local budget: Budget = {nodes = 300, chars = 6000}
|
|
173
|
+
for index = 2, math.min(results.n, MAX_RETURN_VALUES + 1) do
|
|
174
|
+
-- A nil return is written out rather than dropped. `table.insert` of nil
|
|
175
|
+
-- does nothing, so `return a, nil, b` came back as two values and every
|
|
176
|
+
-- one after the nil shifted into the wrong position.
|
|
177
|
+
local value = results[index]
|
|
178
|
+
table.insert(returned, if value == nil then "nil" else describe(value, nil, nil, budget))
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
ok = true,
|
|
183
|
+
returned = returned,
|
|
184
|
+
outputDropped = if outputDropped > 0 then outputDropped else nil,
|
|
185
|
+
returnsDropped = if results.n > MAX_RETURN_VALUES + 1 then results.n - MAX_RETURN_VALUES - 1 else nil,
|
|
186
|
+
output = output,
|
|
187
|
+
milliseconds = math.floor(elapsed * 1000 + 0.5),
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
return Runtime
|
|
@@ -41,6 +41,8 @@ local MAX_CONTEXT = 1000
|
|
|
41
41
|
|
|
42
42
|
export type Entry = {
|
|
43
43
|
sequence: number?,
|
|
44
|
+
id: number?,
|
|
45
|
+
updated: boolean?,
|
|
44
46
|
level: string,
|
|
45
47
|
message: string,
|
|
46
48
|
timestamp: number,
|
|
@@ -101,6 +103,10 @@ local collecting: { string }? = nil
|
|
|
101
103
|
local function pushEntry(entry: Entry)
|
|
102
104
|
nextSequence += 1
|
|
103
105
|
entry.sequence = nextSequence
|
|
106
|
+
entry.id = nextSequence
|
|
107
|
+
entry.message = string.sub(entry.message, 1, 2000)
|
|
108
|
+
if entry.stack then entry.stack = string.sub(entry.stack, 1, 4000) end
|
|
109
|
+
if entry.source then entry.source = string.sub(entry.source, 1, 500) end
|
|
104
110
|
table.insert(entries, entry)
|
|
105
111
|
|
|
106
112
|
if #entries > CAPACITY + SLACK then
|
|
@@ -133,18 +139,24 @@ function LogBuffer.pushClient(player: Player, payload: any)
|
|
|
133
139
|
if payload.kind == "ready" then buffer.ready = true; return end
|
|
134
140
|
if payload.kind == "trace" then
|
|
135
141
|
if typeof(payload.message) ~= "string" or typeof(payload.stack) ~= "string" then return end
|
|
142
|
+
local traceMessage = string.sub(payload.message, 1, 2000)
|
|
136
143
|
for index = #buffer.entries, math.max(1, #buffer.entries - CORRELATION_WINDOW + 1), -1 do
|
|
137
144
|
local item = buffer.entries[index]
|
|
138
|
-
if item.level == "error" and item.message == payload.message and item.stack == nil then
|
|
145
|
+
if item.level == "error" and item.message == string.sub(payload.message, 1, 2000) and item.stack == nil then
|
|
139
146
|
item.stack = string.sub(payload.stack, 1, 4000)
|
|
140
147
|
item.source = if typeof(payload.source) == "string" then string.sub(payload.source, 1, 500) else nil
|
|
148
|
+
buffer.sequence += 1
|
|
149
|
+
item.sequence = buffer.sequence
|
|
150
|
+
item.updated = true
|
|
151
|
+
table.remove(buffer.entries, index)
|
|
152
|
+
table.insert(buffer.entries, item)
|
|
141
153
|
return
|
|
142
154
|
end
|
|
143
155
|
end
|
|
144
156
|
local orphanCount = 0
|
|
145
157
|
for _ in buffer.orphan do orphanCount += 1 end
|
|
146
|
-
if orphanCount < 20 or buffer.orphan[
|
|
147
|
-
buffer.orphan[
|
|
158
|
+
if orphanCount < 20 or buffer.orphan[traceMessage] ~= nil then
|
|
159
|
+
buffer.orphan[traceMessage] = { stack = string.sub(payload.stack, 1, 4000), source = if typeof(payload.source) == "string" then string.sub(payload.source, 1, 500) else nil }
|
|
148
160
|
end
|
|
149
161
|
return
|
|
150
162
|
end
|
|
@@ -154,6 +166,7 @@ function LogBuffer.pushClient(player: Player, payload: any)
|
|
|
154
166
|
buffer.sequence += 1
|
|
155
167
|
local entry: Entry = {
|
|
156
168
|
sequence = buffer.sequence,
|
|
169
|
+
id = buffer.sequence,
|
|
157
170
|
level = level,
|
|
158
171
|
message = string.sub(payload.message, 1, 2000),
|
|
159
172
|
timestamp = if typeof(payload.timestamp) == "number" and payload.timestamp >= 0 and payload.timestamp < 100000000000 then payload.timestamp else os.time(),
|
|
@@ -202,17 +215,20 @@ end
|
|
|
202
215
|
]]
|
|
203
216
|
-- JSONEncode writes a Vector3 or an Instance as `null` without complaint, so
|
|
204
217
|
-- anything JSON has no shape for is turned into its text first.
|
|
205
|
-
local function plain(value: any, depth: number): any
|
|
218
|
+
local function plain(value: any, depth: number, budget: { nodes: number }): any
|
|
219
|
+
budget.nodes -= 1
|
|
220
|
+
if budget.nodes <= 0 then return "<truncated>" end
|
|
206
221
|
local kind = typeof(value)
|
|
207
222
|
if kind == "string" or kind == "number" or kind == "boolean" then
|
|
208
|
-
return value
|
|
223
|
+
return if kind == "string" then string.sub(value, 1, MAX_CONTEXT) else value
|
|
209
224
|
elseif kind == "table" then
|
|
210
225
|
if depth >= 4 then
|
|
211
226
|
return "<table>"
|
|
212
227
|
end
|
|
213
228
|
local copy = {}
|
|
214
229
|
for key, item in value do
|
|
215
|
-
|
|
230
|
+
if budget.nodes <= 0 then copy["..."] = "<truncated>"; break end
|
|
231
|
+
copy[if typeof(key) == "number" then key else string.sub(tostring(key), 1, 100)] = plain(item, depth + 1, budget)
|
|
216
232
|
end
|
|
217
233
|
return copy
|
|
218
234
|
elseif kind == "Instance" then
|
|
@@ -229,15 +245,17 @@ function LogBuffer.encodeContext(context: any): string?
|
|
|
229
245
|
return nil
|
|
230
246
|
end
|
|
231
247
|
local ok, encoded = pcall(function()
|
|
232
|
-
return HttpService:JSONEncode(plain(context, 0))
|
|
248
|
+
return HttpService:JSONEncode(plain(context, 0, {nodes = 50}))
|
|
233
249
|
end)
|
|
234
250
|
if not ok then
|
|
235
251
|
return nil
|
|
236
252
|
end
|
|
237
|
-
return string.sub(encoded
|
|
253
|
+
if #encoded > MAX_CONTEXT then return HttpService:JSONEncode({truncated = string.sub(encoded, 1, 100)}) end
|
|
254
|
+
return encoded
|
|
238
255
|
end
|
|
239
256
|
|
|
240
257
|
local function push(message: string, messageType: Enum.MessageType, timestamp: number?, context: any?)
|
|
258
|
+
message = string.sub(message, 1, 2000)
|
|
241
259
|
local level = LEVELS[messageType.Value] or "print"
|
|
242
260
|
local entry: Entry = {
|
|
243
261
|
level = level,
|
|
@@ -270,12 +288,20 @@ end
|
|
|
270
288
|
two identical lines.
|
|
271
289
|
]]
|
|
272
290
|
local function attachStack(message: string, stack: string, source: string?)
|
|
291
|
+
message = string.sub(message, 1, 2000)
|
|
292
|
+
stack = string.sub(stack, 1, 4000)
|
|
293
|
+
if source then source = string.sub(source, 1, 500) end
|
|
273
294
|
local first = math.max(1, #entries - CORRELATION_WINDOW + 1)
|
|
274
295
|
for index = #entries, first, -1 do
|
|
275
296
|
local entry = entries[index]
|
|
276
297
|
if entry.level == "error" and entry.message == message and entry.stack == nil then
|
|
277
298
|
entry.stack = stack
|
|
278
299
|
entry.source = source
|
|
300
|
+
nextSequence += 1
|
|
301
|
+
entry.sequence = nextSequence
|
|
302
|
+
entry.updated = true
|
|
303
|
+
table.remove(entries, index)
|
|
304
|
+
table.insert(entries, entry)
|
|
279
305
|
return
|
|
280
306
|
end
|
|
281
307
|
end
|
|
@@ -336,7 +362,10 @@ function LogBuffer.start()
|
|
|
336
362
|
local existing = recent.stack
|
|
337
363
|
local existingFrames = if existing then #string.split(existing, "\n") else 0
|
|
338
364
|
if existing == nil or #buffered > existingFrames then
|
|
339
|
-
recent.stack = table.concat(buffered, "\n")
|
|
365
|
+
recent.stack = string.sub(table.concat(buffered, "\n"), 1, 4000)
|
|
366
|
+
nextSequence += 1
|
|
367
|
+
recent.sequence = nextSequence
|
|
368
|
+
recent.updated = true
|
|
340
369
|
end
|
|
341
370
|
end
|
|
342
371
|
end
|
package/plugin/src/Paths.luau
CHANGED
|
@@ -67,6 +67,40 @@ local function childrenByName(parent: Instance, memo: NameIndex?): { [string]: {
|
|
|
67
67
|
table.insert(list, child)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
--[[
|
|
71
|
+
Same-named siblings put into a fixed order.
|
|
72
|
+
|
|
73
|
+
`GetChildren` does not return a stable order (see `stableOrder` in
|
|
74
|
+
Discover), so `Part[2]` could name a different part on the call that
|
|
75
|
+
reads the path back than on the one that printed it. The engine's debug
|
|
76
|
+
id is fixed for an instance's lifetime, which makes it the tiebreak.
|
|
77
|
+
|
|
78
|
+
Guarded, because this module also runs in the client relay, where
|
|
79
|
+
`GetDebugId` is not callable -- there the order is simply the engine's,
|
|
80
|
+
as it always was, rather than an error on every path.
|
|
81
|
+
]]
|
|
82
|
+
for _, list in byName do
|
|
83
|
+
if #list > 1 then
|
|
84
|
+
local ids: { [Instance]: string } = {}
|
|
85
|
+
local stable = true
|
|
86
|
+
for _, child in list do
|
|
87
|
+
local ok, id = pcall(function()
|
|
88
|
+
return child:GetDebugId()
|
|
89
|
+
end)
|
|
90
|
+
if not ok then
|
|
91
|
+
stable = false
|
|
92
|
+
break
|
|
93
|
+
end
|
|
94
|
+
ids[child] = id
|
|
95
|
+
end
|
|
96
|
+
if stable then
|
|
97
|
+
table.sort(list, function(a: Instance, b: Instance): boolean
|
|
98
|
+
return ids[a] < ids[b]
|
|
99
|
+
end)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
70
104
|
if memo then
|
|
71
105
|
memo[parent] = byName
|
|
72
106
|
end
|
|
@@ -315,6 +349,14 @@ end
|
|
|
315
349
|
The index is positional, so it shifts if same-named siblings are inserted or
|
|
316
350
|
removed between calls. Read a fresh path after any structural change.
|
|
317
351
|
]]
|
|
352
|
+
--[[
|
|
353
|
+
The position among same-named siblings, or nil when the name is unique --
|
|
354
|
+
the `n` in `Name[n]`. Exposed for `sync`, which names files after it.
|
|
355
|
+
]]
|
|
356
|
+
function Paths.ordinal(instance: Instance, memo: NameIndex?): number?
|
|
357
|
+
return siblingIndex(instance, memo)
|
|
358
|
+
end
|
|
359
|
+
|
|
318
360
|
function Paths.of(instance: Instance, memo: NameIndex?): string
|
|
319
361
|
--[[
|
|
320
362
|
Refuses anything that is not an Instance, rather than duck-typing it.
|
package/plugin/src/Phrase.luau
CHANGED
|
@@ -91,6 +91,42 @@ type Describer = (params: { [string]: any }) -> string
|
|
|
91
91
|
playtest is about a mode, a find is about what was searched for.
|
|
92
92
|
]]
|
|
93
93
|
local DESCRIBERS: { [string]: Describer } = {
|
|
94
|
+
["sync.scan"] = function(params)
|
|
95
|
+
return if params.revisions == false then "Check script layout for sync" else "Scan scripts for sync"
|
|
96
|
+
end,
|
|
97
|
+
["sync.read"] = function(params)
|
|
98
|
+
return "Read " .. subjectOf(params.paths, "script") .. " for sync"
|
|
99
|
+
end,
|
|
100
|
+
["sync.apply"] = function(params)
|
|
101
|
+
local total = count(params.writes) + count(params.creates) + count(params.deletes) + count(params.moves)
|
|
102
|
+
return string.format("Sync %d change%s from files", total, if total == 1 then "" else "s")
|
|
103
|
+
end,
|
|
104
|
+
["sync.shape"] = function()
|
|
105
|
+
return "Check script layout for sync"
|
|
106
|
+
end,
|
|
107
|
+
["sync.revisions"] = function()
|
|
108
|
+
return "Check script revisions"
|
|
109
|
+
end,
|
|
110
|
+
["sync.changes"] = function()
|
|
111
|
+
return "Check for script changes"
|
|
112
|
+
end,
|
|
113
|
+
["sync.stop"] = function()
|
|
114
|
+
return "Stop sync tracking"
|
|
115
|
+
end,
|
|
116
|
+
["sync.log"] = function()
|
|
117
|
+
return "Sync note"
|
|
118
|
+
end,
|
|
119
|
+
["sync.classes"] = function(params)
|
|
120
|
+
return "List classes in " .. (leaf(params.path) or "tree")
|
|
121
|
+
end,
|
|
122
|
+
["sync.export"] = function(params)
|
|
123
|
+
return "Export " .. (leaf(params.path) or "tree") .. " to a build file"
|
|
124
|
+
end,
|
|
125
|
+
["sync.build"] = function(params)
|
|
126
|
+
local spec = params.spec
|
|
127
|
+
local name = if typeof(spec) == "table" then spec.name or spec.className else nil
|
|
128
|
+
return "Build " .. tostring(name or "tree") .. " from a build file"
|
|
129
|
+
end,
|
|
94
130
|
["script.read"] = function(params)
|
|
95
131
|
return "Read " .. subjectOf(params.paths, "script")
|
|
96
132
|
end,
|
|
@@ -756,6 +792,8 @@ local KINDS: { [string]: string } = {
|
|
|
756
792
|
terrain = "write",
|
|
757
793
|
generate = "write",
|
|
758
794
|
data = "write",
|
|
795
|
+
-- Applies files to the place; the reads inside the group are carved out below.
|
|
796
|
+
sync = "write",
|
|
759
797
|
}
|
|
760
798
|
|
|
761
799
|
function Phrase.kindOf(op: string): string
|
|
@@ -770,6 +808,9 @@ function Phrase.kindOf(op: string): string
|
|
|
770
808
|
if op == "audio.inspect" then
|
|
771
809
|
return "read"
|
|
772
810
|
end
|
|
811
|
+
if op == "sync.scan" or op == "sync.shape" or op == "sync.revisions" or op == "sync.read" or op == "sync.changes" or op == "sync.classes" or op == "sync.export" then
|
|
812
|
+
return "read"
|
|
813
|
+
end
|
|
773
814
|
return KINDS[group] or "read"
|
|
774
815
|
end
|
|
775
816
|
|
package/plugin/src/Prompt.luau
CHANGED
|
@@ -162,6 +162,10 @@ type Runtime = {
|
|
|
162
162
|
-- back returns it rather than an empty line.
|
|
163
163
|
draft: string,
|
|
164
164
|
busy: boolean,
|
|
165
|
+
-- Whether a line that is not a command goes to an agent. Off until the user
|
|
166
|
+
-- runs `chat on`; only used to word the placeholder, since `Commands` is what
|
|
167
|
+
-- actually refuses the line.
|
|
168
|
+
chat: boolean,
|
|
165
169
|
--[[
|
|
166
170
|
What is selected in Studio right now, already worded for display.
|
|
167
171
|
|
|
@@ -195,6 +199,7 @@ local runtime: Runtime = {
|
|
|
195
199
|
offset = 0,
|
|
196
200
|
draft = "",
|
|
197
201
|
busy = false,
|
|
202
|
+
chat = false,
|
|
198
203
|
selection = "",
|
|
199
204
|
complete = nil,
|
|
200
205
|
suggest = nil,
|
|
@@ -218,9 +223,12 @@ local function paintCaret()
|
|
|
218
223
|
end
|
|
219
224
|
|
|
220
225
|
local function placeholder(): string
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
if runtime.busy then
|
|
227
|
+
return "running -- type stop to cancel"
|
|
228
|
+
end
|
|
229
|
+
return if runtime.chat
|
|
230
|
+
then "type a command, or ask for something"
|
|
231
|
+
else "type a command -- `chat on` lets an agent answer"
|
|
224
232
|
end
|
|
225
233
|
|
|
226
234
|
--[[
|
|
@@ -903,6 +911,18 @@ function Prompt.setBusy(busy: boolean)
|
|
|
903
911
|
end
|
|
904
912
|
end
|
|
905
913
|
|
|
914
|
+
--[[
|
|
915
|
+
Reports whether sentences are answered, so the placeholder does not invite
|
|
916
|
+
a request the console is about to decline.
|
|
917
|
+
]]
|
|
918
|
+
function Prompt.setChat(enabled: boolean)
|
|
919
|
+
runtime.chat = enabled
|
|
920
|
+
local field = runtime.field
|
|
921
|
+
if field ~= nil then
|
|
922
|
+
field.PlaceholderText = placeholder()
|
|
923
|
+
end
|
|
924
|
+
end
|
|
925
|
+
|
|
906
926
|
--[[
|
|
907
927
|
Reports what Studio has selected, as a phrase rather than a list.
|
|
908
928
|
|