owenkaplinsky
Add cast_as block; fix bugs
ae5e83b
raw
history blame
37.9 kB
import * as Blockly from 'blockly';
import { pythonGenerator } from 'blockly/python';
import { registerFieldMultilineInput } from '@blockly/field-multilineinput';
registerFieldMultilineInput();
// Utility to create a unique input reference block type
function createInputRefBlockType(inputName) {
const blockType = `input_reference_${inputName}`;
if (!Blockly.Blocks[blockType]) {
Blockly.Blocks[blockType] = {
init: function () {
this.jsonInit({
type: blockType,
message0: "%1",
args0: [
{
type: "field_label_serializable",
name: "VARNAME",
text: inputName
}
],
output: null,
colour: 255,
outputShape: 2
});
},
// Save the owner block ID for proper restoration after deserialization
saveExtraState: function () {
return {
ownerBlockId: this._ownerBlockId || null
};
},
// Restore the owner block ID
loadExtraState: function (state) {
if (state.ownerBlockId) {
this._ownerBlockId = state.ownerBlockId;
}
}
};
pythonGenerator.forBlock[blockType] = function () {
return [inputName, pythonGenerator.ORDER_ATOMIC];
};
}
return blockType;
}
// Global input reference tracking map
const inputRefs = new Map();
// Core mutator registration for dynamic tool and input creation
Blockly.Extensions.registerMutator(
'test_mutator',
{
initialize: function () {
if (!this.initialized_) {
this.inputCount_ = 0;
this.inputNames_ = [];
this.inputTypes_ = [];
this.inputRefBlocks_ = new Map();
this.outputCount_ = 0;
this.outputNames_ = [];
this.outputTypes_ = [];
this.initialized_ = true;
// Mark all reference blocks with their owner for later identification
this._ownerBlockId = this.id;
}
},
decompose: function (workspace) {
const containerBlock = workspace.newBlock('container');
containerBlock.initSvg();
let connection = containerBlock.getInput('STACK').connection;
this.inputCount_ = this.inputCount_ || 0;
this.inputNames_ = this.inputNames_ || [];
this.inputTypes_ = this.inputTypes_ || [];
this.outputCount_ = this.outputCount_ || 0;
this.outputNames_ = this.outputNames_ || [];
this.outputTypes_ = this.outputTypes_ || [];
// Restore dynamically added input items
for (let i = 0; i < this.inputCount_; i++) {
const itemBlock = workspace.newBlock('container_input');
itemBlock.initSvg();
const typeVal = this.inputTypes_[i] || 'string';
const nameVal = this.inputNames_[i] || typeVal;
itemBlock.setFieldValue(typeVal, 'TYPE');
itemBlock.setFieldValue(nameVal, 'NAME');
const input = this.getInput('X' + i);
if (input && input.connection && input.connection.targetConnection) {
itemBlock.valueConnection_ = input.connection.targetConnection;
}
connection.connect(itemBlock.previousConnection);
connection = itemBlock.nextConnection;
}
// Restore dynamically added output items
let connection2 = containerBlock.getInput('STACK2').connection;
for (let i = 0; i < this.outputCount_; i++) {
const itemBlock = workspace.newBlock('container_output');
itemBlock.initSvg();
const typeVal = this.outputTypes_[i] || 'string';
const nameVal = this.outputNames_[i] || typeVal;
itemBlock.setFieldValue(typeVal, 'TYPE');
itemBlock.setFieldValue(nameVal, 'NAME');
connection2.connect(itemBlock.previousConnection);
connection2 = itemBlock.nextConnection;
}
return containerBlock;
},
compose: function (containerBlock) {
Blockly.Events.disable();
try {
if (!this.initialized_) this.initialize();
// Ensure the inputRefBlocks_ map is properly initialized
if (!this.inputRefBlocks_) {
this.inputRefBlocks_ = new Map();
}
// Check if we need to find existing reference blocks in the workspace
// This happens after deserialization when the blocks exist but aren't tracked
if (this.inputRefBlocks_.size === 0 && this.inputNames_ && this.inputNames_.length > 0) {
for (let i = 0; i < this.inputNames_.length; i++) {
const name = this.inputNames_[i];
const input = this.getInput('X' + i);
// Check if there's already a connected block in this input
if (input && input.connection && input.connection.targetBlock()) {
const connectedBlock = input.connection.targetBlock();
const expectedType = `input_reference_${name}`;
// If this is the expected reference block, track it
if (connectedBlock.type === expectedType && connectedBlock._ownerBlockId === this.id) {
this.inputRefBlocks_.set(name, connectedBlock);
}
}
}
}
const oldNames = [...(this.inputNames_ || [])];
const oldOutputNames = [...(this.outputNames_ || [])];
const connections = [];
const returnConnections = [];
let itemBlock = containerBlock.getInputTargetBlock('STACK');
// Collect all child connections from mutator stack
while (itemBlock) {
connections.push(itemBlock.valueConnection_);
itemBlock = itemBlock.nextConnection && itemBlock.nextConnection.targetBlock();
}
// Save existing return connections before removing them
let rIdx = 0;
while (this.getInput('R' + rIdx)) {
const returnInput = this.getInput('R' + rIdx);
if (returnInput && returnInput.connection && returnInput.connection.targetConnection) {
returnConnections.push(returnInput.connection.targetConnection);
} else {
returnConnections.push(null);
}
rIdx++;
}
// Collect output specifications from STACK2
const outputSpecs = [];
let outputBlock = containerBlock.getInputTargetBlock('STACK2');
while (outputBlock) {
outputSpecs.push(outputBlock);
outputBlock = outputBlock.nextConnection && outputBlock.nextConnection.targetBlock();
}
const newCount = connections.length;
const newOutputCount = outputSpecs.length;
this.inputCount_ = newCount;
this.outputCount_ = newOutputCount;
this.inputNames_ = this.inputNames_ || [];
this.inputTypes_ = this.inputTypes_ || [];
this.outputNames_ = this.outputNames_ || [];
this.outputTypes_ = this.outputTypes_ || [];
// Rebuild the new list of input names and types
let idx = 0;
let it = containerBlock.getInputTargetBlock('STACK');
const newNames = [];
while (it) {
this.inputTypes_[idx] = it.getFieldValue('TYPE') || 'string';
this.inputNames_[idx] = it.getFieldValue('NAME') || 'arg' + idx;
newNames.push(this.inputNames_[idx]);
it = it.nextConnection && it.nextConnection.targetBlock();
idx++;
}
// Rebuild the new list of output names and types
let oidx = 0;
const newOutputNames = [];
for (const outBlock of outputSpecs) {
this.outputTypes_[oidx] = outBlock.getFieldValue('TYPE') || 'string';
this.outputNames_[oidx] = outBlock.getFieldValue('NAME') || 'output' + oidx;
newOutputNames.push(this.outputNames_[oidx]);
oidx++;
}
// Dispose of removed input reference blocks when inputs shrink
if (newCount < oldNames.length) {
for (let i = newCount; i < oldNames.length; i++) {
const oldName = oldNames[i];
const block = this.inputRefBlocks_.get(oldName);
if (block && !block.disposed) block.dispose(true);
this.inputRefBlocks_.delete(oldName);
}
}
// Rename reference blocks when variable names change
// Only update reference blocks that belong to THIS block
for (let i = 0; i < Math.min(oldNames.length, newNames.length); i++) {
const oldName = oldNames[i];
const newName = newNames[i];
if (oldName !== newName) {
const oldBlockType = `input_reference_${oldName}`;
const newBlockType = `input_reference_${newName}`;
if (this.inputRefBlocks_.has(oldName)) {
const refBlock = this.inputRefBlocks_.get(oldName);
if (refBlock && !refBlock.disposed) {
this.inputRefBlocks_.delete(oldName);
this.inputRefBlocks_.set(newName, refBlock);
refBlock.setFieldValue(newName, 'VARNAME');
// Properly update the block type in workspace tracking
if (refBlock.workspace && refBlock.workspace.removeTypedBlock) {
refBlock.workspace.removeTypedBlock(refBlock);
refBlock.type = newBlockType;
refBlock.workspace.addTypedBlock(refBlock);
} else {
refBlock.type = newBlockType;
}
}
}
// Update all clones of this reference block that share the same owner
// (i.e., all clones that were created from the same parent block)
const refBlock = this.inputRefBlocks_.get(newName);
const ownerBlockId = this.id;
if (refBlock && !refBlock.disposed) {
const allBlocks = this.workspace.getAllBlocks(false);
for (const block of allBlocks) {
if (block.type === oldBlockType) {
// Update if this block has the same owner as our reference block
// This includes both connected and cloned blocks
if (block._ownerBlockId === ownerBlockId) {
// Properly update the block type in workspace tracking
if (block.workspace && block.workspace.removeTypedBlock) {
block.workspace.removeTypedBlock(block);
block.type = newBlockType;
block.workspace.addTypedBlock(block);
} else {
block.type = newBlockType;
}
block.setFieldValue(newName, 'VARNAME');
}
}
}
}
pythonGenerator.forBlock[newBlockType] = function () {
return [newName, pythonGenerator.ORDER_ATOMIC];
};
}
}
// Remove all dynamic and temporary inputs before reconstruction
let i = 0;
while (this.getInput('X' + i)) this.removeInput('X' + i++);
let r = 0;
while (this.getInput('R' + r)) this.removeInput('R' + r++);
let t = 0;
while (this.getInput('T' + t)) this.removeInput('T' + t++);
['INPUTS_TEXT', 'RETURNS_TEXT', 'TOOLS_TEXT'].forEach(name => {
if (this.getInput(name)) this.removeInput(name);
});
if (newCount > 0) {
const inputsText = this.appendDummyInput('INPUTS_TEXT');
inputsText.appendField('with inputs:');
this.moveInputBefore('INPUTS_TEXT', 'BODY');
}
// Add each dynamic input, reconnecting to reference blocks
for (let j = 0; j < newCount; j++) {
const type = this.inputTypes_[j] || 'string';
const name = this.inputNames_[j] || type;
let check = null;
if (type === 'integer') check = 'Number';
if (type === 'float') check = 'Number';
if (type === 'string') check = 'String';
const existingRefBlock = this.inputRefBlocks_.get(name);
const input = this.appendValueInput('X' + j);
if (check) input.setCheck(check);
input.appendField(type);
this.moveInputBefore('X' + j, 'BODY');
const blockType = createInputRefBlockType(name);
// Check if there's already a block connected to this input
const currentlyConnected = input.connection ? input.connection.targetBlock() : null;
if (currentlyConnected && currentlyConnected.type === blockType) {
// There's already the correct reference block connected, just track it
currentlyConnected._ownerBlockId = this.id;
this.inputRefBlocks_.set(name, currentlyConnected);
} else if (!existingRefBlock) {
// Only create a new reference block if none exists and nothing is connected
const refBlock = this.workspace.newBlock(blockType);
refBlock.initSvg();
refBlock.setDeletable(false);
refBlock.render();
// Mark the reference block with its owner
refBlock._ownerBlockId = this.id;
this.inputRefBlocks_.set(name, refBlock);
if (input.connection && refBlock.outputConnection) {
input.connection.connect(refBlock.outputConnection);
}
} else {
// Reference block exists - only reconnect if not already connected to this input
existingRefBlock._ownerBlockId = this.id;
if (input.connection && !input.connection.targetBlock() && existingRefBlock.outputConnection) {
input.connection.connect(existingRefBlock.outputConnection);
}
}
pythonGenerator.forBlock[blockType] = function () {
return [name, pythonGenerator.ORDER_ATOMIC];
};
}
// Reconnect preserved connections to new structure
for (let k = 0; k < newCount; k++) {
const conn = connections[k];
if (conn) {
try {
conn.connect(this.getInput('X' + k).connection);
} catch { }
}
}
// Handle return inputs based on outputs
if (newOutputCount > 0) {
// Remove the default RETURN input if it exists
if (this.getInput('RETURN')) {
this.removeInput('RETURN');
}
// Add the "and return" label
const returnsText = this.appendDummyInput('RETURNS_TEXT');
returnsText.appendField('and return');
// Add each return value input slot
for (let j = 0; j < newOutputCount; j++) {
const type = this.outputTypes_[j] || 'string';
const name = this.outputNames_[j] || ('output' + j);
let check = null;
if (type === 'integer') check = 'Number';
if (type === 'float') check = 'Number';
if (type === 'string') check = 'String';
const returnInput = this.appendValueInput('R' + j);
if (check) returnInput.setCheck(check);
returnInput.appendField(type);
returnInput.appendField('"' + name + '":');
// Reconnect previous connection if it exists
if (returnConnections[j]) {
try {
returnInput.connection.connect(returnConnections[j]);
} catch { }
}
}
}
this.workspace.render();
} finally {
Blockly.Events.enable();
}
},
saveExtraState: function () {
return {
inputCount: this.inputCount_,
inputNames: this.inputNames_,
inputTypes: this.inputTypes_,
outputCount: this.outputCount_,
outputNames: this.outputNames_,
outputTypes: this.outputTypes_,
toolCount: this.toolCount_ || 0
};
},
loadExtraState: function (state) {
this.inputCount_ = state.inputCount || 0;
this.inputNames_ = state.inputNames || [];
this.inputTypes_ = state.inputTypes || [];
this.outputCount_ = state.outputCount || 0;
this.outputNames_ = state.outputNames || [];
this.outputTypes_ = state.outputTypes || [];
this.toolCount_ = state.toolCount || 0;
// Ensure the reference block map is initialized
if (!this.inputRefBlocks_) {
this.inputRefBlocks_ = new Map();
}
// Immediately rebuild the inputs structure so they exist when connections are loaded
// This must happen BEFORE Blockly tries to restore connections
if (this.inputCount_ > 0) {
const inputsText = this.appendDummyInput('INPUTS_TEXT');
inputsText.appendField('with inputs:');
this.moveInputBefore('INPUTS_TEXT', 'BODY');
for (let j = 0; j < this.inputCount_; j++) {
const type = this.inputTypes_[j] || 'string';
const name = this.inputNames_[j] || ('arg' + j);
let check = null;
if (type === 'integer') check = 'Number';
if (type === 'float') check = 'Number';
if (type === 'string') check = 'String';
const input = this.appendValueInput('X' + j);
if (check) input.setCheck(check);
input.appendField(type);
this.moveInputBefore('X' + j, 'BODY');
// Create the block type definition if it doesn't exist
const blockType = createInputRefBlockType(name);
pythonGenerator.forBlock[blockType] = function () {
return [name, pythonGenerator.ORDER_ATOMIC];
};
}
}
// Also rebuild return inputs if there are outputs
if (this.outputCount_ > 0) {
if (this.getInput('RETURN')) {
this.removeInput('RETURN');
}
const returnsText = this.appendDummyInput('RETURNS_TEXT');
returnsText.appendField('and return');
for (let j = 0; j < this.outputCount_; j++) {
const type = this.outputTypes_[j] || 'string';
const name = this.outputNames_[j] || ('output' + j);
let check = null;
if (type === 'integer') check = 'Number';
if (type === 'float') check = 'Number';
if (type === 'string') check = 'String';
const returnInput = this.appendValueInput('R' + j);
if (check) returnInput.setCheck(check);
returnInput.appendField(type);
returnInput.appendField('"' + name + '":');
}
}
}
},
null,
['container_input']
);
// Base block definitions
const container = {
type: "container",
message0: "inputs %1 %2 outputs %3 %4",
args0: [
{ type: "input_dummy", name: "title" },
{ type: "input_statement", name: "STACK" },
{ type: "input_dummy", name: "title2" },
{ type: "input_statement", name: "STACK2" },
],
colour: 210,
inputsInline: false
};
const container_input = {
type: "container_input",
message0: "%1 %2",
args0: [
{
type: "field_dropdown",
name: "TYPE",
options: [
["String", "string"],
["Integer", "integer"],
["Float", "float"],
["List", "list"],
["Boolean", "boolean"],
["Any", "any"],
]
},
{ type: "field_input", name: "NAME" },
],
previousStatement: null,
nextStatement: null,
colour: 210,
};
const container_output = {
type: "container_output",
message0: "%1 %2",
args0: [
{
type: "field_dropdown",
name: "TYPE",
options: [
["String", "string"],
["Integer", "integer"],
["Float", "float"],
["List", "list"],
["Boolean", "boolean"],
["Any", "any"],
]
},
{ type: "field_input", name: "NAME" },
],
previousStatement: null,
nextStatement: null,
colour: 210,
};
const llm_call = {
type: "llm_call",
message0: "call model %1 with prompt %2",
args0: [
{
type: "field_dropdown",
name: "MODEL",
options: [
["gpt-3.5-turbo", "gpt-3.5-turbo-0125"],
["gpt-4o", "gpt-4o-2024-08-06"],
["gpt-5-mini", "gpt-5-mini-2025-08-07"],
["gpt-5", "gpt-5-2025-08-07"],
["gpt-4o search", "gpt-4o-search-preview-2025-03-11"],
]
},
{ type: "input_value", name: "PROMPT", check: "String" },
],
inputsInline: true,
output: "String",
colour: 160,
tooltip: "Call the selected OpenAI model to get a response.",
helpUrl: "",
};
const call_api = {
"type": "call_api",
"message0": "call API with method %1 url %2 headers %3",
"args0": [
{
type: "field_dropdown",
name: "METHOD",
options: [
["GET", "GET"],
["POST", "POST"],
["PUT", "PUT"],
["DELETE", "DELETE"],
]
},
{
"type": "input_value",
"name": "URL",
},
{
"type": "input_value",
"name": "HEADERS",
},
],
"output": ["String", "Integer", "List"],
"colour": 165,
"inputsInline": true
}
const in_json = {
"type": "in_json",
"message0": "get %1 from JSON %2",
"args0": [
{
"type": "input_value",
"name": "NAME",
},
{
"type": "input_value",
"name": "JSON",
},
],
"output": ["String", "Integer", "List"],
"colour": 165,
"inputsInline": true
}
const json_field = {
type: "json_field",
message0: "field",
args0: [],
previousStatement: null,
nextStatement: null,
colour: 165,
};
const make_json_container = {
type: "make_json_container",
message0: "fields %1",
args0: [
{ type: "input_statement", name: "STACK" },
],
colour: 165,
inputsInline: false
};
const make_json = {
type: "make_json",
message0: "make JSON %1",
args0: [
{ type: "input_dummy" },
],
colour: 165,
inputsInline: false,
output: ["String", "Integer"],
mutator: "json_mutator",
fieldCount_: 1,
};
const lists_contains = {
type: "lists_contains",
message0: "item %1 in list %2",
args0: [
{ type: "input_value", name: "ITEM", check: null },
{ type: "input_value", name: "LIST", check: "Array" },
],
output: "Boolean",
colour: 260,
inputsInline: true,
tooltip: "Check if an item exists in a list",
helpUrl: "",
};
// Cast block for type conversion
const cast_as = {
type: "cast_as",
message0: "cast %1 as %2",
args0: [
{ type: "input_value", name: "VALUE", check: null },
{
type: "field_dropdown",
name: "TYPE",
options: [
["int", "int"],
["float", "float"],
["str", "str"],
["bool", "bool"],
],
},
],
output: null,
colour: 210,
inputsInline: true,
tooltip: "Convert a value to a different type",
helpUrl: "",
};
// Dynamic function call block
const func_call = {
type: "func_call",
message0: "func %1",
args0: [
{
type: "field_dropdown",
name: "FUNC_NAME",
options: function () {
// This will be populated dynamically
const options = [["<no functions>", "NONE"]];
if (this.sourceBlock_) {
const workspace = this.sourceBlock_.workspace;
const funcBlocks = workspace.getAllBlocks(false).filter(b => b.type === 'func_def');
if (funcBlocks.length > 0) {
options.length = 0;
funcBlocks.forEach(block => {
const name = block.getFieldValue('NAME');
if (name) {
options.push([name, name]);
}
});
}
}
return options;
}
}
],
inputsInline: true,
output: null,
colour: 210,
tooltip: "Call a function defined in the workspace",
helpUrl: "",
extensions: ["func_call_dynamic"]
};
const create_mcp = {
type: "create_mcp",
message0: "create MCP %1 %2",
args0: [
{ type: "input_dummy" },
{ type: "input_statement", name: "BODY" },
],
colour: 210,
inputsInline: true,
mutator: "test_mutator",
inputCount_: 0,
deletable: false,
extensions: ["test_cleanup_extension"]
};
const func_def = {
type: "func_def",
message0: "function %1 %2 %3",
args0: [
{ type: "field_input", name: "NAME", text: "newFunction" },
{ type: "input_dummy" },
{ type: "input_statement", name: "BODY" },
],
colour: 210,
inputsInline: true,
mutator: "test_mutator",
inputCount_: 0,
deletable: true,
extensions: ["test_cleanup_extension"]
};
// Cleanup extension ensures that dynamic reference blocks are deleted when parent is
Blockly.Extensions.register('test_cleanup_extension', function () {
const oldDispose = this.dispose;
this.dispose = function (healStack, recursive) {
if (this.inputRefBlocks_) {
for (const [, refBlock] of this.inputRefBlocks_) {
if (refBlock && !refBlock.disposed) refBlock.dispose(false);
}
this.inputRefBlocks_.clear();
}
if (oldDispose) oldDispose.call(this, healStack, recursive);
};
});
// JSON mutator for dynamic field creation
Blockly.Extensions.registerMutator(
'json_mutator',
{
decompose: function (workspace) {
const containerBlock = workspace.newBlock('make_json_container');
containerBlock.initSvg();
let connection = containerBlock.getInput('STACK').connection;
// Initialize defaults if not set
if (this.fieldCount_ === undefined) {
this.fieldCount_ = 0;
this.fieldKeys_ = [];
}
// Restore dynamically added field items
for (let i = 0; i < this.fieldCount_; i++) {
const itemBlock = workspace.newBlock('json_field');
itemBlock.initSvg();
// Store the connection for compose
const input = this.getInput('FIELD' + i);
if (input && input.connection && input.connection.targetConnection) {
itemBlock.valueConnection_ = input.connection.targetConnection;
}
connection.connect(itemBlock.previousConnection);
connection = itemBlock.nextConnection;
}
return containerBlock;
},
compose: function (containerBlock) {
Blockly.Events.disable();
try {
// Initialize defaults if not set
if (this.fieldCount_ === undefined) {
this.fieldCount_ = 0;
this.fieldKeys_ = [];
}
const connections = [];
let itemBlock = containerBlock.getInputTargetBlock('STACK');
// Collect all child connections from mutator stack
while (itemBlock) {
connections.push(itemBlock.valueConnection_);
itemBlock = itemBlock.nextConnection && itemBlock.nextConnection.targetBlock();
}
const newCount = connections.length;
const oldCount = this.fieldCount_;
this.fieldCount_ = newCount;
// Preserve old keys and extend array if needed
if (!this.fieldKeys_) {
this.fieldKeys_ = [];
}
// Remove all dynamic inputs before reconstruction
let i = 0;
while (this.getInput('FIELD' + i)) this.removeInput('FIELD' + i++);
// Add each dynamic field input with editable key name
for (let j = 0; j < newCount; j++) {
// Use existing key or create new one
if (!this.fieldKeys_[j]) {
this.fieldKeys_[j] = 'key' + j;
}
const key = this.fieldKeys_[j];
const input = this.appendValueInput('FIELD' + j);
const field = new Blockly.FieldTextInput(key);
field.setValidator((newValue) => {
// Update the stored key when user edits it
this.fieldKeys_[j] = newValue || 'key' + j;
return newValue;
});
input.appendField(field, 'KEY' + j);
input.appendField(':');
this.moveInputBefore('FIELD' + j, null);
}
// Trim fieldKeys array if fields were removed
if (newCount < oldCount) {
this.fieldKeys_.length = newCount;
}
// Reconnect preserved connections to new structure
for (let k = 0; k < newCount; k++) {
const conn = connections[k];
if (conn) {
try {
this.getInput('FIELD' + k).connection.connect(conn);
} catch { }
}
}
this.workspace.render();
} finally {
Blockly.Events.enable();
}
},
saveExtraState: function () {
return {
fieldCount: this.fieldCount_,
fieldKeys: this.fieldKeys_,
};
},
loadExtraState: function (state) {
this.fieldCount_ = state.fieldCount || 0;
this.fieldKeys_ = state.fieldKeys || [];
// Immediately rebuild the inputs structure so they exist when connections are loaded
if (this.fieldCount_ > 0) {
for (let j = 0; j < this.fieldCount_; j++) {
const key = this.fieldKeys_[j] || ('key' + j);
const input = this.appendValueInput('FIELD' + j);
const field = new Blockly.FieldTextInput(key);
field.setValidator((newValue) => {
// Update the stored key when user edits it
this.fieldKeys_[j] = newValue || 'key' + j;
return newValue;
});
input.appendField(field, 'KEY' + j);
input.appendField(':');
}
}
}
},
null,
['json_field']
);
// Extension for dynamic function call blocks
Blockly.Extensions.register('func_call_dynamic', function () {
const block = this;
// Store current function being called
block.currentFunction_ = null;
block.paramCount_ = 0;
// Function to update the block based on selected function
block.updateShape_ = function () {
const funcName = this.getFieldValue('FUNC_NAME');
// Temporarily disable events to prevent recursive updates
const eventsEnabled = Blockly.Events.isEnabled();
if (eventsEnabled) {
Blockly.Events.disable();
}
try {
// Remove all existing parameter inputs
let i = 0;
while (this.getInput('ARG' + i)) {
this.removeInput('ARG' + i);
i++;
}
if (funcName && funcName !== 'NONE') {
// Find the function definition block
const workspace = this.workspace;
const funcBlock = workspace.getAllBlocks(false).find(b =>
b.type === 'func_def' && b.getFieldValue('NAME') === funcName
);
if (funcBlock) {
this.currentFunction_ = funcName;
// Get the function's parameters
const inputCount = funcBlock.inputCount_ || 0;
const inputNames = funcBlock.inputNames_ || [];
const inputTypes = funcBlock.inputTypes_ || [];
this.paramCount_ = inputCount;
// Add parameter inputs matching the function definition
for (let j = 0; j < inputCount; j++) {
const paramName = inputNames[j] || ('arg' + j);
const paramType = inputTypes[j] || 'string';
let check = null;
if (paramType === 'integer') check = 'Number';
if (paramType === 'string') check = 'String';
const input = this.appendValueInput('ARG' + j);
if (check) input.setCheck(check);
input.appendField(`${paramType} "${paramName}"`);
}
// Set output type based on function's return type
if (funcBlock.outputCount_ && funcBlock.outputCount_ > 0) {
const outputType = funcBlock.outputTypes_[0] || 'string';
if (outputType === 'integer') {
this.setOutput(true, 'Number');
} else if (outputType === 'string') {
this.setOutput(true, 'String');
} else {
this.setOutput(true, null);
}
} else {
this.setOutput(true, null);
}
} else {
this.currentFunction_ = null;
this.paramCount_ = 0;
}
} else {
this.currentFunction_ = null;
this.paramCount_ = 0;
}
} finally {
// Re-enable events if they were enabled before
if (eventsEnabled) {
Blockly.Events.enable();
}
}
};
// Listen for dropdown changes
block.getField('FUNC_NAME').setValidator(function (newValue) {
const block = this.getSourceBlock();
// Ensure the update happens after the dropdown value is set
setTimeout(() => {
block.updateShape_();
}, 0);
return newValue;
});
// Listen for workspace changes to update when functions are modified
const workspaceListener = function (event) {
// Skip if block has been disposed
if (block.disposed) {
return;
}
if (event.type === Blockly.Events.BLOCK_CHANGE ||
event.type === Blockly.Events.BLOCK_DELETE ||
event.type === Blockly.Events.BLOCK_CREATE ||
event.type === Blockly.Events.BLOCK_MOVE) {
// Check if a func_def block was changed
const changedBlock = block.workspace.getBlockById(event.blockId);
if (event.type === Blockly.Events.BLOCK_DELETE) {
// Delay check to ensure workspace has been updated
setTimeout(() => {
if (!block.disposed && block.currentFunction_ &&
!block.workspace.getAllBlocks(false).some(b =>
b.type === 'func_def' && b.getFieldValue('NAME') === block.currentFunction_)) {
block.dispose(true);
}
}, 0);
} else if (changedBlock && changedBlock.type === 'func_def') {
// If the function being called was modified, update shape
const funcName = changedBlock.getFieldValue('NAME');
if (funcName === block.currentFunction_ ||
(event.oldValue && event.oldValue === block.currentFunction_)) {
// Handle renaming
if (event.type === Blockly.Events.BLOCK_CHANGE &&
event.name === 'NAME' && event.oldValue === block.currentFunction_) {
block.currentFunction_ = funcName;
block.setFieldValue(funcName, 'FUNC_NAME');
}
setTimeout(() => {
if (!block.disposed) {
block.updateShape_();
}
}, 0);
}
}
// Update dropdown options
const dropdown = block.getField('FUNC_NAME');
if (dropdown) {
const workspace = block.workspace;
const funcBlocks = workspace.getAllBlocks(false).filter(b => b.type === 'func_def');
const options = funcBlocks.length > 0
? funcBlocks.map(b => [b.getFieldValue('NAME'), b.getFieldValue('NAME')])
: [["<no functions>", "NONE"]];
dropdown.menuGenerator_ = options;
// If current function no longer exists, reset
if (block.currentFunction_ && !options.some(opt => opt[1] === block.currentFunction_)) {
block.setFieldValue('NONE', 'FUNC_NAME');
setTimeout(() => {
if (!block.disposed) {
block.updateShape_();
}
}, 0);
}
}
}
};
block.workspace.addChangeListener(workspaceListener);
// Clean up the listener when block is disposed
const oldDispose = block.dispose;
block.dispose = function (healStack) {
if (block.workspace) {
block.workspace.removeChangeListener(workspaceListener);
}
if (oldDispose) {
oldDispose.call(this, healStack);
}
};
});
// Function to generate a unique tool name
function generateUniqueToolName(workspace, excludeBlock) {
const existingNames = new Set();
const allBlocks = workspace.getAllBlocks(false);
// Collect all existing tool names, excluding the block being created
for (const block of allBlocks) {
if (block.type === 'func_def' && block !== excludeBlock && block.getFieldValue('NAME')) {
existingNames.add(block.getFieldValue('NAME'));
}
}
// Generate a unique name
let baseName = 'newTool';
let name = baseName;
let counter = 1;
while (existingNames.has(name)) {
counter++;
name = `${baseName}${counter}`;
}
return name;
}
// Register create_mcp block separately to include custom init logic
Blockly.Blocks['create_mcp'] = {
init: function () {
this.jsonInit(create_mcp);
// Apply extensions
Blockly.Extensions.apply('test_cleanup_extension', this, false);
// Initialize mutator state
if (this.initialize) {
this.initialize();
}
}
};
// Register func_def block separately to include custom init logic
Blockly.Blocks['func_def'] = {
init: function () {
this.jsonInit(func_def);
// Apply extensions
Blockly.Extensions.apply('test_cleanup_extension', this, false);
// Initialize mutator state
if (this.initialize) {
this.initialize();
}
}
};
// Register func_call block separately to include custom logic
Blockly.Blocks['func_call'] = {
init: function () {
this.jsonInit(func_call);
},
// Save the current function name and parameter count for serialization
saveExtraState: function () {
return {
currentFunction: this.currentFunction_ || null,
paramCount: this.paramCount_ || 0
};
},
loadExtraState(state) {
this.currentFunction_ = state.currentFunction;
this.paramCount_ = state.paramCount;
// Ensure ARG0..ARGN exist so the deserializer can reconnect children
for (let i = 0; i < this.paramCount_; i++) {
if (!this.getInput('ARG' + i)) {
this.appendValueInput('ARG' + i).appendField("");
}
}
}
};
// Register json_field block (internal mutator block, not user-visible)
Blockly.Blocks['json_field'] = {
init: function () {
this.jsonInit(json_field);
}
};
// Register make_json_container block (internal mutator block, not user-visible)
Blockly.Blocks['make_json_container'] = {
init: function () {
this.jsonInit(make_json_container);
}
};
// Register make_json block separately to include custom init logic
Blockly.Blocks['make_json'] = {
init: function () {
this.jsonInit(make_json);
// Initialize with no fields by default
this.fieldCount_ = 0;
this.fieldKeys_ = [];
}
};
export const blocks = Blockly.common.createBlockDefinitionsFromJsonArray([
container,
container_input,
container_output,
llm_call,
call_api,
in_json,
lists_contains,
cast_as,
]);