Skip to content

Conversation

@improdead
Copy link
Owner

@improdead improdead commented Oct 3, 2025

PR Type

Enhancement


Description

  • Add run_command tool for unified object operations

  • Implement manual mode with asset filtering and fallback

  • Simplify system prompt and tool guidance

  • Enhance asset search with limit 60 and filtering


Diagram Walkthrough

flowchart LR
  A["User Request"] --> B["run_command Tool"]
  B --> C["Parse Command Verbs"]
  C --> D["create_model/create_part/set_props"]
  A --> E["Asset Search"]
  E --> F{"Manual Mode?"}
  F -->|Yes| G["Block Assets"]
  F -->|No| H["Enhanced Filtering"]
  H --> I["Fallback to Manual"]
  I --> G
  G --> J["Manual Geometry"]
Loading

File Walkthrough

Relevant files
Enhancement
9 files
index.ts
Add run_command tool and manual mode logic                             
+457/-135
search.ts
Increase search limit to 60 and add metadata                         
+36/-6   
route.ts
Add manual mode state and asset logging                                   
+24/-0   
taskState.ts
Add manual mode and asset counters to state                           
+8/-2     
route.ts
Update search limit to 60                                                               
+1/-2     
schemas.ts
Add run_command tool schema                                                           
+3/-0     
main.server.lua
Add asset filtering and fallback mechanisms                           
+416/-54
create_instance.lua
Add idempotency check for existing instances                         
+8/-0     
Vector.rbxmx
Update plugin with asset filtering and fallback                   
+425/-55
Documentation
2 files
examples.ts
Simplify system prompt and tool guidance                                 
+14/-54 
README.md
Document manual mode and plan behavior                                     
+4/-0     
Tests
1 files
test-workflow-hospital.ts
Add hospital workflow end-to-end test                                       
+61/-0   
Configuration changes
1 files
package.json
Add hospital workflow test script                                               
+2/-1     

@improdead improdead requested a review from Copilot October 3, 2025 02:35
@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Oct 3, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Untrusted asset insertion

Description: The InsertService:GetFreeModels/LoadAsset fallback path inserts models from the
marketplace without explicit sandboxing or permission prompts beyond ensurePermission;
inserting arbitrary free models can execute malicious scripts if not vetted, so results
should be restricted or validated more strictly.
main.server.lua [1053-1137]

Referred Code
if #candidates == 0 then
    local trimmed = tostring(query or ""):gsub("^%s+", ""):gsub("%s+$", "")
    local first = string.match(trimmed, "^([^%s]+)")
    if first and string.lower(first) ~= string.lower(trimmed) then
        -- Broaden by retrying with the first word (e.g., "hospital" from "hospital building")
        for pageIndex = 0, maxPages - 1 do
            local okPage2, pages2 = pcall(function()
                return InsertService:GetFreeModels(first, pageIndex)
            end)
            if okPage2 then
                local page2 = (pages2 or {})[1]
                if page2 and typeof(page2) == "table" and typeof(page2.Results) == "table" then
                    for resultIndex, entry in ipairs(page2.Results) do
                        local assetId = entry and assetIdKey(entry.AssetId)
                        if assetId and not seenAsset[assetId] then
                            if ROBLOX_ONLY and not isRobloxEntry(entry) then
                                -- skip
                            else
                                seenAsset[assetId] = true
                                entry.__score = computeCatalogScore(entry, pageIndex * 100 + resultIndex)


 ... (clipped 64 lines)
Expanded catalog exposure

Description: Increasing search limits to 60 and broadening results may increase exposure to malicious
or low-quality assets unless paired with strict filtering; verify server-side rate
limiting and creator trust checks to mitigate abuse.
search.ts [118-131]

Referred Code
  SortType: '3',
})
// Optional: restrict to free assets only when explicitly requested
// Default free-only OFF to broaden results; env can enable explicitly
const freeOnly = String(process.env.CATALOG_FREE_ONLY || '0') === '1'
if (freeOnly) {
  // Prefer Roblox sales filter for free items; explicit price filters often 0-out results
  params.set('SalesTypeFilter', '1')
}
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), Number(process.env.CATALOG_TIMEOUT_MS || 15000))
try {
  const res = await fetch(`${ROBLOX_CATALOG_URL}?${params.toString()}`, {
    signal: controller.signal,
Policy enforcement via regex

Description: Manual mode blocks asset tools by regex on user/system text, which is bypassable; relying
on pattern matching to enforce security policy is brittle and may allow unintended asset
usage if strings are crafted or missed.
index.ts [1851-1873]

Referred Code
  toolName === 'search_files'
)
const isActionTool = !isContextOrNonActionTool

if (manualWanted && (toolName === 'search_assets' || toolName === 'insert_asset' || toolName === 'generate_asset_3d')) {
  consecutiveValidationErrors++
  const errMsg = 'MANUAL_MODE Active: use create_instance/set_properties or Luau; catalog tools are disabled until manual build is complete.'
  pushChunk(streamKey, `error.validation ${toolName} manual_mode`)
  console.warn(`[orch] manual_mode tool=${toolName}`)
  convo.push({ role: 'assistant', content: toolXml })
  convo.push({ role: 'user', content: errMsg })
  appendHistory('system', errMsg)
  continue
}

// Allow duplicate start_plan: if steps are unchanged, no-op; otherwise replace (handled by recordPlanStart)
if (toolName === 'start_plan' && planReady) {
  const incomingSteps = Array.isArray((a as any).steps)
    ? (a as any).steps.map((s: any) => String(s || '').trim())
    : []
  const currentSteps = Array.isArray(taskState.plan?.steps)


 ... (clipped 2 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
No custom compliance provided

Follow the guide to enable custom compliance check.

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds command mode experiment support, introducing a unified command interface for interacting with Roblox Studio through a text-based command system. The changes implement a new run_command tool that provides simplified syntax for common operations like creating models, parts, setting properties, and asset manipulation.

Key changes:

  • Added run_command tool with text-based command parsing (create_model, create_part, set_props, rename, delete, insert_asset)
  • Implemented manual mode for asset-restricted builds with improved fallback mechanisms
  • Enhanced asset search and insertion with scoring, sorting, and failure tracking systems

Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
vector/plugin/src/main.server.lua Core plugin enhancements with asset search improvements, scoring system, and manual mode support
vector/plugin/src/tools/create_instance.lua Added idempotency check to reuse existing instances with matching names and classes
vector/apps/web/lib/orchestrator/index.ts Major orchestrator updates with run_command tool, manual mode logic, and asset-first enforcement
vector/apps/web/lib/tools/schemas.ts Added run_command tool schema definition
vector/apps/web/lib/orchestrator/taskState.ts Extended task state with manual mode and asset operation counters
vector/apps/web/scripts/test-workflow-hospital.ts New workflow test script for hospital building scenarios
vector/apps/web/package.json Added test script for hospital workflow
vector/apps/web/lib/orchestrator/prompts/examples.ts Simplified prompt examples focusing on command-based approach
vector/apps/web/lib/orchestrator/README.md Updated documentation explaining manual mode behavior
vector/apps/web/lib/catalog/search.ts Enhanced catalog search with creator metadata and higher limits
vector/apps/web/app/api/proposals/[id]/apply/route.ts Added manual mode state management and verbose asset operation logging
vector/apps/web/app/api/assets/search/route.ts Increased search limit from 50 to 60 results
vector/plugin/Vector.rbxmx Duplicate of main.server.lua changes in the plugin bundle

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +1037 to +1045
if ROBLOX_ONLY and not isRobloxEntry(entry) then
-- skip non-Roblox entries when filter is active
else
seenAsset[assetId] = true
entry.__score = computeCatalogScore(entry, pageIndex * 100 + resultIndex)
entry.__pageIndex = pageIndex
entry.__resultIndex = resultIndex
table.insert(candidates, entry)
end
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The if-else structure for ROBLOX_ONLY filtering has inconsistent indentation and could be simplified for better readability. Consider extracting the filtering logic or restructuring the conditional flow.

Copilot uses AI. Check for mistakes.
Comment on lines +1069 to +1077
if ROBLOX_ONLY and not isRobloxEntry(entry) then
-- skip
else
seenAsset[assetId] = true
entry.__score = computeCatalogScore(entry, pageIndex * 100 + resultIndex)
entry.__pageIndex = pageIndex
entry.__resultIndex = resultIndex
table.insert(candidates, entry)
end
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is duplicate logic from lines 1036-1044. Consider extracting this filtering and scoring logic into a helper function to reduce code duplication.

Copilot uses AI. Check for mistakes.
}

if (!fallbacksDisabled) {
if (fallbacksDisabled) {
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic error: the condition should be !fallbacksDisabled to match the comment and intended behavior. The current condition will trigger the error message when fallbacks are disabled, but the surrounding logic expects this block to run when fallbacks are enabled.

Suggested change
if (fallbacksDisabled) {
if (!fallbacksDisabled) {

Copilot uses AI. Check for mistakes.
}
if (!state.policy || typeof state.policy !== 'object') {
state.policy = { geometryOps: 0, luauEdits: 0 }
state.policy = { geometryOps: 0, luauEdits: 0, assetSearches: 0, assetInserts: 0, manualMode: false }
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The policy object initialization is duplicated on line 138 with the same structure. Consider extracting this to a helper function createDefaultPolicy() to ensure consistency and reduce duplication.

Copilot uses AI. Check for mistakes.
url.searchParams.set('includeOnlyVerifiedCreators', 'false')
// Free-only via price caps in cents
if (String(process.env.CATALOG_FREE_ONLY || '0') === '1') {
if (String(process.env.CATALOG_FREE_ONLY || '1') === '1') {
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value for CATALOG_FREE_ONLY has changed from '0' to '1', but this differs from the Roblox catalog implementation on line 122 which still uses '0'. This inconsistency could lead to different filtering behavior between catalog sources.

Suggested change
if (String(process.env.CATALOG_FREE_ONLY || '1') === '1') {
if (String(process.env.CATALOG_FREE_ONLY || '0') === '1') {

Copilot uses AI. Check for mistakes.
@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Oct 3, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Consolidate asset search and insertion logic

The PR splits asset search logic between the TypeScript backend and the Luau
plugin. To improve maintainability, this logic, including filtering and scoring,
should be consolidated into the backend service.

Examples:

vector/plugin/src/main.server.lua [901-951]
local function computeCatalogScore(entry, index)
    local score = (index or 0) * 10
    local creatorName = (typeof(entry.creator) == "string" and entry.creator) or (typeof(entry.CreatorName) == "string" and entry.CreatorName) or ""
    local creatorId = assetIdKey(entry.creatorId or entry.CreatorId)
    local creatorType
    if typeof(entry.creatorType) == "string" then
        creatorType = string.lower(entry.creatorType)
    elseif typeof(entry.CreatorType) == "string" then
        creatorType = string.lower(entry.CreatorType)
    elseif typeof(entry.CreatorType) == "EnumItem" then

 ... (clipped 41 lines)
vector/plugin/src/main.server.lua [2633-2643]
                        local orderedResults = sortCatalogResults(resultsOrErr)
						local desiredParentPath = nil
						local sel = Selection:Get()
						if type(sel) == "table" and #sel == 1 and sel[1] and sel[1].GetFullName then
							local okPath, pathOrErr = pcall(function() return sel[1]:GetFullName() end)
							if okPath and type(pathOrErr) == "string" then desiredParentPath = pathOrErr end
						end
						if not desiredParentPath then
							local hosp = workspace:FindFirstChild("Hospital")
							if hosp and hosp.GetFullName then

 ... (clipped 1 lines)

Solution Walkthrough:

Before:

// Backend (TypeScript)
async function searchAssets(query, limit) {
  // Fetches raw, unsorted asset data from Roblox APIs
  const results = await fetchFromRoblox(query, limit);
  return Response.json({ results });
}

// Plugin (Luau)
function handleAssetSearch() {
  local raw_results = fetchAssets_from_backend();
  // Complex scoring, filtering, and sorting happens in the plugin
  local ordered_results = sortCatalogResults(raw_results);
  // Fallback logic using Studio-only APIs also in plugin
  if #ordered_results == 0 then
    insertAssetFromFreeModels();
  end
  // Display/insert from `ordered_results`
}

After:

// Backend (TypeScript)
async function searchAssets(query, limit) {
  // Fetches asset data from Roblox APIs
  const raw_results = await fetchFromRoblox(query, limit);
  // All scoring, filtering, and sorting logic is centralized here
  const ordered_results = sortAndScoreResults(raw_results);
  return Response.json({ results: ordered_results });
}

// Plugin (Luau)
function handleAssetSearch() {
  // Plugin receives a ready-to-use, sorted list
  local ordered_results = fetchAssets_from_backend();
  // If results are empty, trigger a specific fallback
  if #ordered_results == 0 then
    insertAssetFromFreeModels(); // Studio-only fallback remains in plugin
  end
  // Display/insert from `ordered_results`
}
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a significant architectural issue where complex asset scoring and filtering logic is added to the Luau plugin, creating a split that is hard to maintain, but it overlooks that some fallback logic relies on a Studio-only API.

Medium
Possible issue
Fix fragile key-value pair parsing

The regular expression in parseKV is too simple and fails to parse unquoted
values containing spaces, such as CFrame coordinates. Update the regex and
parsing logic to correctly handle these cases.

vector/apps/web/lib/orchestrator/index.ts [675-688]

 const parseKV = (s: string): Record<string, string> => {
   const out: Record<string, string> = {}
-  const re = /(\w+)=(("[^"]*")|('[^']*')|([^\s]+))/g
-  let m: RegExpExecArray | null
-  while ((m = re.exec(s))) {
-    const key = String(m[1])
-    let val = String(m[2])
+  // This regex handles:
+  // - word=value (no spaces)
+  // - word="quoted value"
+  // - word='quoted value'
+  // - word=unquoted value with spaces (must be last or followed by another key=)
+  const re = /(\w+)=((?:"[^"]*")|(?:'[^']*')|(?:[^\s'"]+))/g
+  const args = s.match(/(\w+)=((?:"[^"]*")|(?:'[^']*')|(?:[^\s=]+(?:\s+[^\s=]+)*))/g) || []
+  for (const arg of args) {
+    const eqIndex = arg.indexOf('=')
+    const key = arg.slice(0, eqIndex)
+    let val = arg.slice(eqIndex + 1)
     if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
       val = val.slice(1, -1)
     }
     out[key] = val
   }
   return out
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a bug in the new parseKV function where unquoted values with spaces are not parsed correctly, and provides a more robust implementation.

Medium
Report correct asset ID on fallback

When an asset insertion fails and a fallback is used, the reportApply function
incorrectly sends the assetId of the original failed asset. The code should be
modified to track and report the assetId of the asset that was successfully
inserted.

vector/plugin/src/main.server.lua [2039-2083]

 insertBtn.MouseButton1Click:Connect(function()
-    -- Try this result first, then fall back to trying others in order until one inserts
+    -- Try this result first, then fall back to trying others in order to find one that can be inserted
     local function tryInsert(id)
         local ok, modelOrErr = insertAsset(id, p.insert and p.insert.parentPath or nil)
         if ok then
             local insertedPath = nil
             if modelOrErr and typeof(modelOrErr) == "Instance" and modelOrErr.GetFullName then
                 local success, value = pcall(function() return modelOrErr:GetFullName() end)
                 insertedPath = success and value or nil
             end
             return true, insertedPath
         end
         return false, tostring(modelOrErr)
     end
 
-    local ok, insertedPath = tryInsert(r.id)
+    local ok, insertedPath
+    local finalAssetId = r.id
+    ok, insertedPath = tryInsert(r.id)
+
     if not ok then
         for _, alt in ipairs(ordered) do
             if alt.id ~= r.id then
                 ok, insertedPath = tryInsert(alt.id)
-                if ok then break end
+                if ok then
+                    finalAssetId = alt.id
+                    break
+                end
             end
         end
     end
 
     if ok then
         row.BackgroundColor3 = Color3.fromRGB(32, 64, 32)
         row.BackgroundTransparency = 0.2
-        reportApply(p.id, { ok = true, type = p.type, op = "insert_asset", assetId = r.id, insertedPath = insertedPath })
+        reportApply(p.id, { ok = true, type = p.type, op = "insert_asset", assetId = finalAssetId, insertedPath = insertedPath })
     else
         row.BackgroundColor3 = Color3.fromRGB(64, 32, 32)
         row.BackgroundTransparency = 0.2
         reportApply(p.id, { ok = false, type = p.type, op = "insert_asset", assetId = r.id, error = insertedPath })
     end
 end)

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a bug where the wrong assetId is reported when a fallback asset is inserted, and the proposed fix correctly tracks and reports the ID of the asset that was actually used.

Medium
Possible issue
Apply props when reusing instance

When reusing an existing instance in create_instance, ensure that the provided
props are still applied to the existing instance to avoid silently ignoring
property updates.

vector/plugin/src/tools/create_instance.lua [116-144]

 local ok, res = pcall(function()
-    -- Idempotency: if a child with the target Name and class already exists, reuse it
+    -- Idempotency: if a child with the target Name and class already exists, reuse it but still apply props
     local desiredName = type(props) == "table" and props.Name
     if type(desiredName) == "string" and #desiredName > 0 then
         local existing = parent:FindFirstChild(desiredName)
         if existing and existing.ClassName == className then
+            if type(props) == "table" then
+                for k, v in pairs(props) do
+                    if type(k) == "string" and string.sub(k,1,1) == "@" then
+                        pcall(function() existing:SetAttribute(string.sub(k,2), v) end)
+                    else
+                        pcall(function() existing[k] = v end)
+                    end
+                end
+            end
             return existing:GetFullName()
         end
     end
     local inst = Instance.new(className)
     if type(props) == "table" then
         for k, v in pairs(props) do
             if type(k) == "string" and string.sub(k,1,1) == "@" then
-                -- attribute
                 pcall(function() inst:SetAttribute(string.sub(k,2), v) end)
             else
                 pcall(function() inst[k] = v end)
             end
         end
     end
     inst.Parent = parent
     return inst:GetFullName()
 end)
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a flaw in the new idempotency logic where properties are not applied to a reused instance, which could lead to incorrect scene state. Applying properties to the existing instance is a critical fix for correctness.

Medium
Validate and enrich CFrame parsing

Improve the parseCFrame function by validating the 3x3 rotation matrix for
orthonormality and adding support for parsing 6-number Euler angle inputs
(x,y,z,rx,ry,rz).

vector/apps/web/lib/orchestrator/index.ts [712-719]

 const parseCFrame = (v?: string) => {
   if (!v) return undefined
-  const parts = v.split(/[,\s]+/).filter(Boolean).map((p) => Number(p))
-  if (parts.some((n) => !Number.isFinite(n))) return undefined
-  if (parts.length >= 12) return { __t: 'CFrame', comps: parts.slice(0, 12) }
-  if (parts.length >= 3) return { __t: 'CFrame', comps: [parts[0], parts[1], parts[2], 1,0,0, 0,1,0, 0,0,1] }
+  const nums = v.split(/[,\s]+/).filter(Boolean).map((p) => Number(p))
+  if (nums.some((n) => !Number.isFinite(n))) return undefined
+  const make = (t: number[], r?: number[]) => ({ __t: 'CFrame', comps: [...t, ...(r ?? [1,0,0, 0,1,0, 0,0,1])] })
+  // 12 numbers: x y z and 3x3 matrix
+  if (nums.length >= 12) {
+    const t = nums.slice(0, 3)
+    const m = nums.slice(3, 12)
+    // Validate orthonormal rotation matrix (columns unit-length and orthogonal)
+    const dot = (a: number[], b: number[]) => a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
+    const col0 = [m[0], m[3], m[6]]
+    const col1 = [m[1], m[4], m[7]]
+    const col2 = [m[2], m[5], m[8]]
+    const len = (c: number[]) => Math.hypot(c[0], c[1], c[2])
+    const eps = 1e-3
+    const ok =
+      Math.abs(len(col0) - 1) < eps &&
+      Math.abs(len(col1) - 1) < eps &&
+      Math.abs(len(col2) - 1) < eps &&
+      Math.abs(dot(col0, col1)) < 2*eps &&
+      Math.abs(dot(col0, col2)) < 2*eps &&
+      Math.abs(dot(col1, col2)) < 2*eps
+    return ok ? { __t: 'CFrame', comps: nums.slice(0, 12) } : make(t)
+  }
+  // 6 numbers: x,y,z, rx,ry,rz (degrees) → convert to rotation matrix
+  if (nums.length >= 6) {
+    const [x, y, z, rx, ry, rz] = nums
+    const toRad = (d: number) => (d * Math.PI) / 180
+    const sx = Math.sin(toRad(rx)), cx = Math.cos(toRad(rx))
+    const sy = Math.sin(toRad(ry)), cy = Math.cos(toRad(ry))
+    const sz = Math.sin(toRad(rz)), cz = Math.cos(toRad(rz))
+    // R = Rz * Ry * Rx (XYZ intrinsic)
+    const r00 = cz*cy
+    const r01 = cz*sy*sx - sz*cx
+    const r02 = cz*sy*cx + sz*sx
+    const r10 = sz*cy
+    const r11 = sz*sy*sx + cz*cx
+    const r12 = sz*sy*cx - cz*sx
+    const r20 = -sy
+    const r21 = cy*sx
+    const r22 = cy*cx
+    return { __t: 'CFrame', comps: [x, y, z, r00,r01,r02, r10,r11,r12, r20,r21,r22] }
+  }
+  // 3 numbers: position only
+  if (nums.length >= 3) return make(nums.slice(0, 3))
   return undefined
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the new parseCFrame function lacks validation. Adding an orthonormality check and support for Euler angles significantly improves the robustness and usability of the new run_command feature.

Medium
General
Allow search pathway in insert command

Modify the insert_asset command to handle cases where assetId is missing but a
query is provided. Instead of erroring, it should generate an asset search
proposal.

vector/apps/web/lib/orchestrator/index.ts [787-796]

 if (verb === 'insert_asset' || verb === 'insert') {
-  const assetId = Number(kv.assetId || kv.id)
-  if (!Number.isFinite(assetId) || assetId <= 0) return { proposals, missingContext: 'insert_asset requires assetId=' }
   const parentPath = kv.parent || kv.parentPath
+  const idStr = kv.assetId || kv.id
+  const assetId = idStr ? Number(idStr) : NaN
   if (extras?.manualMode) {
     return { proposals, missingContext: 'Manual mode active: asset commands are disabled. Use create_part/set_props or Luau.' }
   }
-  addAssetOp({ assetId, parentPath })
-  return { proposals }
+  if (Number.isFinite(assetId) && assetId > 0) {
+    addAssetOp({ assetId, parentPath })
+    return { proposals }
+  }
+  const query = kv.query
+  const limit = kv.limit ? Number(kv.limit) : undefined
+  const tags = kv.tags ? String(kv.tags).split(',').map((t) => t.trim()).filter(Boolean) : undefined
+  if (typeof query === 'string' && query.trim()) {
+    proposals.push({
+      id: id('asset'),
+      type: 'asset_op',
+      search: { query: query.trim(), tags, limit: Number.isFinite(limit || NaN) ? limit : undefined },
+      meta: {} as any
+    })
+    return { proposals }
+  }
+  return { proposals, missingContext: 'insert_asset requires assetId= or query=' }
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: This suggestion correctly points out a limitation in the new insert_asset command. Implementing a search pathway when assetId is missing but query is present makes the command more intuitive and powerful, improving the user experience.

Medium
Align fetch size with final limit

In fetchFromCreatorStoreToolbox, align the maxPageSize parameter with the final
limit to avoid over-fetching data that is later discarded by slice.

vector/apps/web/lib/catalog/search.ts [217-231]

 async function fetchFromCreatorStoreToolbox(query: string, limit: number, opts?: CatalogSearchOptions): Promise<CatalogItem[]> {
   const apiKey = normalizeString(process.env.ROBLOX_OPEN_CLOUD_API_KEY)
   if (!apiKey) throw new Error('Missing ROBLOX_OPEN_CLOUD_API_KEY')
+  const finalLimit = Math.max(1, Math.min(60, Number.isFinite(limit) ? limit : 8))
   const url = new URL('https://apis.roblox.com/toolbox-service/v2/assets:search')
   url.searchParams.set('searchCategoryType', 'Model')
   url.searchParams.set('query', query || '')
-  url.searchParams.set('maxPageSize', String(Math.max(1, Math.min(100, limit || 8))))
-  // Broaden results by allowing all creators (verified and non-verified)
+  url.searchParams.set('maxPageSize', String(finalLimit))
   url.searchParams.set('includeOnlyVerifiedCreators', 'false')
-  // Free-only via price caps in cents
   if (String(process.env.CATALOG_FREE_ONLY || '1') === '1') {
     url.searchParams.set('minPriceCents', '0')
     url.searchParams.set('maxPriceCents', '0')
   }
   const controller = new AbortController()
+  const timeout = setTimeout(() => controller.abort(), 8000)
+  try {
+    const resp = await fetch(url.toString(), { headers: { 'x-api-key': apiKey }, signal: controller.signal })
+    if (!resp.ok) throw new Error(`creator-store ${resp.status}`)
+    const json = await resp.json().catch(() => ({}))
+    const arr = Array.isArray(json?.assets ?? json?.results) ? (json.assets ?? json.results) : []
+    if (!Array.isArray(arr)) throw new Error('Invalid creator-store response')
+    const items: CatalogItem[] = []
+    for (const entry of arr) {
+      const asset = entry?.asset ?? entry
+      const id = Number(asset?.id ?? entry?.assetId)
+      if (!Number.isFinite(id)) continue
+      const name = String(asset?.name ?? asset?.displayName ?? `Asset ${id}`)
+      const creator = String(entry?.creator?.name ?? entry?.creatorName ?? 'Unknown')
+      const type = String(asset?.assetTypeId ?? 'Model')
+      let thumbnailUrl: string | undefined
+      const thumbs = asset?.previewAssets?.imagePreviewAssets || entry?.thumbnails || entry?.previews
+      if (Array.isArray(thumbs) && thumbs.length > 0) {
+        // leave undefined; downstream can fetch by id
+      }
+      const creatorIdRaw = entry?.creator?.id ?? entry?.creatorId
+      const creatorId = typeof creatorIdRaw === 'number' ? creatorIdRaw : Number(creatorIdRaw)
+      const creatorType = typeof entry?.creator?.type === 'string' ? entry.creator.type : undefined
+      const isVerified = Boolean(entry?.creator?.hasVerifiedBadge ?? entry?.creatorHasVerifiedBadge)
+      items.push({
+        id, name, creator, type, thumbnailUrl,
+        creatorId: Number.isFinite(creatorId) ? Number(creatorId) : undefined,
+        creatorType, isVerified,
+      })
+    }
+    return items.slice(0, finalLimit)
+  } finally {
+    clearTimeout(timeout)
+  }
+}

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies an inefficiency where more items are fetched than used. Aligning the maxPageSize with the final desired limit is a good optimization to reduce data transfer and processing.

Low
  • Update

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants