summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRider Linden <rider@lindenlab.com>2026-08-06 14:25:45 -0700
committerRider Linden <rider@lindenlab.com>2026-08-06 14:25:45 -0700
commit309fc5b4c21151e3664f5bdab99f57bae24266a0 (patch)
treed7700857c86f8945d805b848494102cee299ac50
parentebf9cd31d789c0556d741987e95a961edd2955d0 (diff)
JSON-RPC command.execute instruction for arbitrary 1-off commands and extensions.
-rw-r--r--doc/external-editor-json-rpc.md140
-rw-r--r--indra/newview/llscripteditorws.cpp128
-rw-r--r--indra/newview/llscripteditorws.h24
3 files changed, 292 insertions, 0 deletions
diff --git a/doc/external-editor-json-rpc.md b/doc/external-editor-json-rpc.md
index a3404ce1eb..6c041cf70b 100644
--- a/doc/external-editor-json-rpc.md
+++ b/doc/external-editor-json-rpc.md
@@ -46,6 +46,9 @@ This document describes all the message interfaces defined for WebSocket communi
- [ObjectRequest](#objectrequest)
- [ObjectModify](#objectmodify)
- [ObjectItemModify](#objectitemmodify)
+- [Command Interfaces](#command-interfaces)
+ - [CommandExecute](#commandexecute)
+ - [CommandList](#commandlist)
## Usage Flow
@@ -196,6 +199,10 @@ WebSocket connects -> session.handshake -> session.ok
| `object.modify` (response) | Viewer -> Extension | Response | `ObjectModifyResponse` |
| `object.item.modify` | Extension -> Viewer | Call | `ObjectItemModifyParams` |
| `object.item.modify` (response) | Viewer -> Extension | Response | `ObjectItemModifyResponse` |
+| `command.execute` | Bidirectional | Call | `CommandExecuteParams` |
+| `command.execute` (response) | Bidirectional | Response | `CommandExecuteResponse` |
+| `command.list` | Bidirectional | Call | _(no params)_ |
+| `command.list` (response) | Bidirectional | Response | `CommandListResponse` |
## Session Management Interfaces
@@ -235,6 +242,7 @@ interface SessionHandshake {
- `live_sync`: Viewer supports live script synchronisation with the external editor
- `compilation`: Viewer will forward compilation results via `script.compiled`
- `syntax_cache`: Viewer supports `language.syntax.cache` and `language.syntax.get` for retrieving syntax definition files
+ - `commands`: Both sides support `command.execute` and `command.list`
### SessionHandshakeResponse
@@ -1218,3 +1226,135 @@ interface ObjectItemModifyResponse {
- An `object.update` notification will fire after successful modification.
- Owner permissions cannot be modified directly — only `next_owner` can be changed.
- If the item is renamed, the virtual filesystem path will change and the extension must handle the rename appropriately.
+
+---
+
+## Command Interfaces
+
+These interfaces provide a general-purpose, bidirectional command channel. Either side may invoke a named command on the other side and receive a structured result. The feature is optional and must be negotiated via the `commands` flag in the session handshake.
+
+### CommandExecute
+
+**JSON-RPC Method:** `command.execute` (call, bidirectional)
+
+Invokes a named command on the receiving side. Commands are identified by a namespaced string and carry an optional freeform parameter map.
+
+```typescript
+interface CommandExecuteParams {
+ command: string; // namespaced command id, e.g. "viewer.teleport"
+ params?: Record<string, unknown>; // command-specific arguments
+}
+
+interface CommandExecuteResponse {
+ success: boolean;
+ result?: unknown; // optional command-specific return value
+ error_code?: number;
+ message?: string;
+}
+```
+
+**Fields:**
+
+- `command`: Namespaced command identifier. The prefix before the first `.` identifies the side that owns and executes the command:
+ - `viewer.*` - commands executed by the viewer (e.g. `viewer.teleport`, `viewer.script.recompile_all`)
+ - `editor.*` - commands executed by the extension (e.g. `editor.open_file`, `editor.show_message`)
+- `params` (optional): Command-specific argument map. Structure varies by command.
+- `success`: Whether the command was found and executed without error.
+- `result` (optional): Command-specific return value. Only present when `success` is `true` and the command produces output.
+- `error_code` (optional): Numeric failure code. Only present when `success` is `false`:
+ - `1` - Unknown command
+ - `2` - Invalid or missing parameters
+ - `3` - Not permitted
+ - `4` - Execution error
+- `message` (optional): Human-readable error description. Only present when `success` is `false`.
+
+**Capability gate:** A side MUST NOT send `command.execute` unless the peer advertised `commands: true` in the handshake. A receiver that receives the call without having negotiated the feature MUST respond with `success: false, error_code: 1`.
+
+**Example - extension asks viewer to teleport:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "command.execute",
+ "id": 7,
+ "params": {
+ "command": "viewer.teleport",
+ "params": { "region": "Aditi", "position": [128, 128, 25] }
+ }
+}
+```
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 7,
+ "result": { "success": true }
+}
+```
+
+**Example - viewer asks extension to show a message:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "command.execute",
+ "id": 8,
+ "params": {
+ "command": "editor.show_message",
+ "params": { "message": "Script reset complete", "level": "info" }
+ }
+}
+```
+
+---
+
+### CommandList
+
+**JSON-RPC Method:** `command.list` (call, bidirectional)
+
+Requests the list of commands the receiving side supports. Intended for tooling and autocomplete; implementations may omit this method and return an error response if discovery is not needed.
+
+This method takes no parameters.
+
+**Response:**
+
+```typescript
+interface CommandListResponse {
+ commands: CommandInfo[];
+}
+
+interface CommandInfo {
+ command: string;
+ description?: string;
+ params?: Record<string, CommandParamInfo>;
+}
+
+interface CommandParamInfo {
+ type: "string" | "number" | "boolean" | "object" | "array";
+ required?: boolean;
+ description?: string;
+}
+```
+
+**Response Fields:**
+
+- `commands`: Array of commands the responder supports. Each entry describes one command.
+ - `command`: The namespaced command identifier.
+ - `description` (optional): Human-readable description of what the command does.
+ - `params` (optional): Map of parameter names to their type descriptors.
+
+**Known viewer commands:**
+
+| Command | Required params | Description |
+|---------|----------------|-------------|
+| `viewer.teleport` | `region: string` | Teleport agent to a region. Optional `position: [x, y, z]`. |
+| `viewer.script.recompile_all` | `object_id: string` | Recompile all scripts in an object. |
+| `viewer.script.reset_all` | `object_id: string` | Reset all scripts in an object. |
+| `viewer.camera.focus` | `object_id: string` | Move camera focus to an in-world object. |
+
+**Known extension commands:**
+
+| Command | Required params | Description |
+|---------|----------------|-------------|
+| `editor.open_file` | `path: string` | Open a file in the editor. Optional `line: number`. |
+| `editor.show_message` | `message: string` | Show a notification. Optional `level: "info" | "warn" | "error"`. |
diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp
index 8f45d14c36..3fbcd5c384 100644
--- a/indra/newview/llscripteditorws.cpp
+++ b/indra/newview/llscripteditorws.cpp
@@ -31,6 +31,7 @@
#include "llscripteditorws.h"
#include "llagent.h"
+#include "llagentcamera.h"
#include "llappviewer.h"
#include "llchat.h"
#include "lldate.h"
@@ -61,6 +62,7 @@
#include "llviewerobject.h"
#include "llviewerobjectlist.h"
#include "llviewerregion.h"
+#include "llviewermenu.h"
#include "llviewertexteditor.h"
#include "llvoinventorylistener.h"
#include "roles_constants.h"
@@ -166,6 +168,46 @@ LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port
{
LL_INFOS("ScriptEditorWS") << "Created JSON-RPC script editor server: " << name
<< " on port " << port << LL_ENDL;
+
+ registerCommand({ "viewer.teleport", "Teleport agent to an in-world object" },
+ [](U32, const LLSD& p) -> LLSD
+ {
+ LLUUID object_id = p["object_id"].asUUID();
+ if (object_id.isNull())
+ throw LLJSONRPCConnection::InvalidParams("object_id is required");
+
+ LLViewerObject* object = gObjectList.findObject(object_id);
+ if (!object)
+ throw LLJSONRPCConnection::InvalidParams("object_id not found");
+
+ LLVector3d global_pos = object->getPositionGlobal();
+ gAgent.teleportViaLocation(global_pos);
+
+ LLSD response;
+ response["success"] = true;
+ return response;
+ });
+
+ registerCommand({ "viewer.camera.focus", "Zoom camera to an in-world object (same behavior as context menu Zoom In)" },
+ [](U32, const LLSD& p) -> LLSD
+ {
+ LLUUID object_id = p["object_id"].asUUID();
+ if (object_id.isNull())
+ throw LLJSONRPCConnection::InvalidParams("object_id is required");
+
+ if (!handle_zoom_to_object(object_id))
+ {
+ LLSD response;
+ response["success"] = false;
+ response["error_code"] = WSCommandError::ExecutionError;
+ response["message"] = "Object not found or not reachable";
+ return response;
+ }
+
+ LLSD response;
+ response["success"] = true;
+ return response;
+ });
}
LLScriptEditorWSServer::ptr_t LLScriptEditorWSServer::getServer()
@@ -623,6 +665,18 @@ void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t c
{
return s.handleObjectItemModify(connection_id, params);
}));
+
+ script_connection->registerAsyncMethod("command.execute",
+ bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params)
+ {
+ return s.handleCommandExecute(connection_id, params);
+ }));
+
+ script_connection->registerMethod("command.list",
+ bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, auto&)
+ {
+ return s.handleCommandList();
+ }));
}
}
@@ -854,6 +908,79 @@ LLSD LLScriptEditorWSServer::handleObjectItemModify(U32 connection_id, const LLS
return response;
}
+void LLScriptEditorWSServer::registerCommand(const WSCommandInfo& info, WSCommandHandler handler)
+{
+ mCommandRegistry.emplace(info.command, std::make_pair(info, std::move(handler)));
+}
+
+bool LLScriptEditorWSConnection::hasFeature(const std::string& feature) const
+{
+ return mFeatures.count(feature) > 0;
+}
+
+LLSD LLScriptEditorWSServer::handleCommandExecute(U32 connection_id, const LLSD& params)
+{
+ const std::string command = params["command"].asString();
+ if (command.empty())
+ {
+ throw LLJSONRPCConnection::InvalidParams("command is required");
+ }
+
+ auto it = mCommandRegistry.find(command);
+ if (it == mCommandRegistry.end())
+ {
+ LLSD response;
+ response["success"] = false;
+ response["error_code"] = WSCommandError::UnknownCommand;
+ response["message"] = "Unknown command: " + command;
+ return response;
+ }
+
+ return it->second.second(connection_id, params["params"]);
+}
+
+LLSD LLScriptEditorWSServer::handleCommandList()
+{
+ LLSD commands(LLSD::emptyArray());
+ for (const auto& [name, entry] : mCommandRegistry)
+ {
+ LLSD info;
+ info["command"] = entry.first.command;
+ info["description"] = entry.first.description;
+ commands.append(info);
+ }
+ LLSD response;
+ response["commands"] = commands;
+ return response;
+}
+
+void LLScriptEditorWSServer::sendCommandExecute(
+ U32 connection_id, const std::string& command, const LLSD& params)
+{
+ auto it = mActiveConnections.find(connection_id);
+ if (it == mActiveConnections.end())
+ {
+ return;
+ }
+
+ auto connection = it->second.lock();
+ if (!connection || !connection->hasFeature("commands"))
+ {
+ return;
+ }
+
+ LLSD call_params;
+ call_params["command"] = command;
+ call_params["params"] = params;
+
+ connection->call("command.execute", call_params,
+ [command](const LLSD& result, const LLSD& error)
+ {
+ LL_WARNS_IF(!error.isUndefined() || !result["success"].asBoolean(), "WSCommand")
+ << "command.execute failed for " << command << LL_ENDL;
+ });
+}
+
void LLScriptEditorWSServer::broadcastLanguageChange()
{
LLUUID syntax_id = LLSyntaxDefCache::instance().getSyntaxID();
@@ -2300,6 +2427,7 @@ void LLScriptEditorWSConnection::onOpen()
features["live_sync"] = true;
features["compilation"] = true;
features["syntax_cache"] = true;
+ features["commands"] = true;
handshake["features"] = features;
wptr_t that = weak_from_this();
diff --git a/indra/newview/llscripteditorws.h b/indra/newview/llscripteditorws.h
index 475a8049ee..63b60335ff 100644
--- a/indra/newview/llscripteditorws.h
+++ b/indra/newview/llscripteditorws.h
@@ -39,6 +39,7 @@
#include <map>
#include <set>
#include <atomic>
+#include <functional>
// Forward declarations
class LLLiveLSLEditor;
@@ -87,6 +88,7 @@ public:
void onClose() override;
void sendDisconnect(DisconnectReason reason = DisconnectReason::NORMAL, const std::string& message = "Goodbye");
+ bool hasFeature(const std::string& feature) const;
private:
using string_set_t = std::set<std::string>;
@@ -205,6 +207,8 @@ public:
bool publishObject(const LLUUID& object_id);
void unpublishObject(const LLUUID& object_id, const std::string& reason = "");
bool isObjectPublished(const LLUUID& object_id) const;
+
+ // *TODO*: These should be moved to LLPublishedObjectMgr at some point.
void onPrimInventoryReady(const LLUUID& object_id, const LLUUID& prim_id);
void onPrimInventoryChanged(const LLUUID& object_id, const LLUUID& prim_id);
void onObjectPropertyChanged(const LLUUID& prim_id, const std::string& name, const std::string& desc, S16 inventory_serial = -1);
@@ -242,6 +246,9 @@ protected:
LLSD handleObjectScriptReset(U32 connection_id, const LLSD& params);
LLSD handleObjectModify(U32 connection_id, const LLSD& params);
LLSD handleObjectItemModify(U32 connection_id, const LLSD& params);
+ LLSD handleCommandExecute(U32 connection_id, const LLSD& params);
+ LLSD handleCommandList();
+ void sendCommandExecute(U32 connection_id, const std::string& command, const LLSD& params);
struct ValidatedItem
{
@@ -313,6 +320,23 @@ private:
mutable LLPublishedObjectMgr mPublishedObjectManager;
+ struct WSCommandInfo
+ {
+ std::string command;
+ std::string description;
+ };
+ enum WSCommandError
+ {
+ UnknownCommand = 1,
+ InvalidParams = 2,
+ NotPermitted = 3,
+ ExecutionError = 4,
+ };
+ using WSCommandHandler = std::function<LLSD(U32 connection_id, const LLSD& params)>;
+ std::unordered_map<std::string, std::pair<WSCommandInfo, WSCommandHandler>> mCommandRegistry;
+
+ void registerCommand(const WSCommandInfo& info, WSCommandHandler handler);
+
boost::signals2::connection mLanguageChangeSignal;
LLUUID mLastSyntaxId;