owenkaplinsky commited on
Commit
0daf9fe
·
1 Parent(s): 7c1461a

Add replace block tool

Browse files
Files changed (2) hide show
  1. project/chat.py +105 -8
  2. project/src/index.js +647 -552
project/chat.py CHANGED
@@ -43,6 +43,10 @@ variable_results = {}
43
  edit_mcp_queue = queue.Queue()
44
  edit_mcp_results = {}
45
 
 
 
 
 
46
  # Global variable to store the deployed HF MCP server URL
47
  current_mcp_server_url = None
48
 
@@ -269,6 +273,48 @@ def edit_mcp(inputs=None, outputs=None):
269
  traceback.print_exc()
270
  return f"Error editing MCP block: {str(e)}"
271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  # Unified Server-Sent Events endpoint for all workspace operations
273
  @app.get("/unified_stream")
274
  async def unified_stream():
@@ -352,6 +398,23 @@ async def unified_stream():
352
  else:
353
  print(f"[SSE SKIP] Skipping duplicate request for ID: {request_id}")
354
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  else:
356
  # Send a heartbeat every 30 seconds to keep connection alive
357
  heartbeat_counter += 1
@@ -437,6 +500,22 @@ async def edit_mcp_result(request: Request):
437
 
438
  return {"received": True}
439
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  def deploy_to_huggingface(space_name):
441
  global stored_hf_key
442
 
@@ -888,6 +967,25 @@ def create_gradio_interface():
888
  }
889
  }
890
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
891
  {
892
  "type": "function",
893
  "name": "deploy_to_huggingface",
@@ -1081,14 +1179,6 @@ def create_gradio_interface():
1081
  tool_result = None
1082
  result_label = ""
1083
 
1084
- # Log MCP tool calls
1085
- if function_name not in ("delete_block", "create_block", "create_variable", "edit_mcp", "deploy_to_huggingface"):
1086
- # This is an MCP tool call
1087
- print(Fore.GREEN + f"[MCP TOOL CALL] Running MCP with inputs:" + Style.RESET_ALL)
1088
- print(Fore.GREEN + f" Tool name: {function_name}" + Style.RESET_ALL)
1089
- for key, value in function_args.items():
1090
- print(Fore.GREEN + f" {key}: {value}" + Style.RESET_ALL)
1091
-
1092
  if function_name == "delete_block":
1093
  block_id = function_args.get("id", "")
1094
  print(Fore.YELLOW + f"Agent deleted block with ID `{block_id}`." + Style.RESET_ALL)
@@ -1193,6 +1283,13 @@ def create_gradio_interface():
1193
  tool_result = edit_mcp(inputs, outputs)
1194
  result_label = "Edit MCP Operation"
1195
 
 
 
 
 
 
 
 
1196
  elif function_name == "deploy_to_huggingface":
1197
  space_name = function_args.get("space_name", "")
1198
  print(Fore.YELLOW + f"Agent deploying to Hugging Face Space `{space_name}`." + Style.RESET_ALL)
 
43
  edit_mcp_queue = queue.Queue()
44
  edit_mcp_results = {}
45
 
46
+ # Queue for replace block requests and results storage
47
+ replace_block_queue = queue.Queue()
48
+ replace_block_results = {}
49
+
50
  # Global variable to store the deployed HF MCP server URL
51
  current_mcp_server_url = None
52
 
 
273
  traceback.print_exc()
274
  return f"Error editing MCP block: {str(e)}"
275
 
276
+ def replace_block(block_id, command):
277
+ try:
278
+ print(f"[REPLACE REQUEST] Attempting to replace block {block_id} with: {command}")
279
+
280
+ # Generate a unique request ID
281
+ request_id = str(uuid.uuid4())
282
+
283
+ # Clear any old results for this request ID first
284
+ if request_id in replace_block_results:
285
+ replace_block_results.pop(request_id)
286
+
287
+ # Build the replace data
288
+ replace_data = {"request_id": request_id, "block_id": block_id, "block_spec": command}
289
+
290
+ # Add to replace block queue
291
+ replace_block_queue.put(replace_data)
292
+ print(f"[REPLACE REQUEST] Added to queue with ID: {request_id}")
293
+
294
+ # Wait for result with timeout
295
+ timeout = 8 # 8 seconds timeout
296
+ start_time = time.time()
297
+ check_interval = 0.05 # Check more frequently
298
+
299
+ while time.time() - start_time < timeout:
300
+ if request_id in replace_block_results:
301
+ result = replace_block_results.pop(request_id)
302
+ print(f"[REPLACE RESULT] Received result for {request_id}: success={result.get('success')}, error={result.get('error')}")
303
+ if result["success"]:
304
+ return f"[TOOL] Successfully replaced block {block_id}"
305
+ else:
306
+ return f"[TOOL] Failed to replace block: {result.get('error', 'Unknown error')}"
307
+ time.sleep(check_interval)
308
+
309
+ print(f"[REPLACE TIMEOUT] No response received for request {request_id} after {timeout} seconds")
310
+ return f"Timeout waiting for block replacement confirmation"
311
+
312
+ except Exception as e:
313
+ print(f"[REPLACE ERROR] {e}")
314
+ import traceback
315
+ traceback.print_exc()
316
+ return f"Error replacing block: {str(e)}"
317
+
318
  # Unified Server-Sent Events endpoint for all workspace operations
319
  @app.get("/unified_stream")
320
  async def unified_stream():
 
398
  else:
399
  print(f"[SSE SKIP] Skipping duplicate request for ID: {request_id}")
400
 
401
+ # Check replace block queue
402
+ elif not replace_block_queue.empty():
403
+ replace_request = replace_block_queue.get_nowait()
404
+ request_id = replace_request.get("request_id")
405
+ request_key = f"replace_{request_id}"
406
+
407
+ # Avoid sending duplicate requests too quickly
408
+ if request_key not in sent_requests:
409
+ sent_requests.add(request_key)
410
+ replace_request["type"] = "replace" # Add type identifier
411
+ yield f"data: {json.dumps(replace_request)}\n\n"
412
+
413
+ # Clear from sent_requests after 10 seconds
414
+ asyncio.create_task(clear_sent_request(sent_requests, request_key, 10))
415
+ else:
416
+ print(f"[SSE SKIP] Skipping duplicate request for ID: {request_id}")
417
+
418
  else:
419
  # Send a heartbeat every 30 seconds to keep connection alive
420
  heartbeat_counter += 1
 
500
 
501
  return {"received": True}
502
 
503
+ # Endpoint to receive replace block results from frontend
504
+ @app.post("/replace_block_result")
505
+ async def replace_block_result(request: Request):
506
+ data = await request.json()
507
+ request_id = data.get("request_id")
508
+ success = data.get("success")
509
+ error = data.get("error")
510
+
511
+ print(f"[REPLACE RESULT RECEIVED] request_id={request_id}, success={success}, error={error}")
512
+
513
+ if request_id:
514
+ # Store the result for the replace_block function to retrieve
515
+ replace_block_results[request_id] = data
516
+
517
+ return {"received": True}
518
+
519
  def deploy_to_huggingface(space_name):
520
  global stored_hf_key
521
 
 
967
  }
968
  }
969
  },
970
+ {
971
+ "type": "function",
972
+ "name": "replace_block",
973
+ "description": "Replace a block with a new block in the exact same location. The new block will take the place of the old one.",
974
+ "parameters": {
975
+ "type": "object",
976
+ "properties": {
977
+ "block_id": {
978
+ "type": "string",
979
+ "description": "The ID of the block you want to replace.",
980
+ },
981
+ "command": {
982
+ "type": "string",
983
+ "description": "The create block command using the custom DSL format for the new block. You must rewrite it ENTIRELY from scratch.",
984
+ },
985
+ },
986
+ "required": ["block_id", "command"],
987
+ }
988
+ },
989
  {
990
  "type": "function",
991
  "name": "deploy_to_huggingface",
 
1179
  tool_result = None
1180
  result_label = ""
1181
 
 
 
 
 
 
 
 
 
1182
  if function_name == "delete_block":
1183
  block_id = function_args.get("id", "")
1184
  print(Fore.YELLOW + f"Agent deleted block with ID `{block_id}`." + Style.RESET_ALL)
 
1283
  tool_result = edit_mcp(inputs, outputs)
1284
  result_label = "Edit MCP Operation"
1285
 
1286
+ elif function_name == "replace_block":
1287
+ block_id = function_args.get("block_id", "")
1288
+ command = function_args.get("command", "")
1289
+ print(Fore.YELLOW + f"Agent replacing block with ID `{block_id}` with command `{command}`." + Style.RESET_ALL)
1290
+ tool_result = replace_block(block_id, command)
1291
+ result_label = "Replace Block Operation"
1292
+
1293
  elif function_name == "deploy_to_huggingface":
1294
  space_name = function_args.get("space_name", "")
1295
  print(Fore.YELLOW + f"Agent deploying to Hugging Face Space `{space_name}`." + Style.RESET_ALL)
project/src/index.js CHANGED
@@ -223,6 +223,557 @@ cleanWorkspace.addEventListener("click", () => {
223
  ws.cleanUp();
224
  });
225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  // Set up unified SSE connection for all workspace operations
227
  const setupUnifiedStream = () => {
228
  const eventSource = new EventSource('/unified_stream');
@@ -245,6 +796,8 @@ const setupUnifiedStream = () => {
245
  requestKey = `variable_${data.request_id}`;
246
  } else if (data.type === 'edit_mcp') {
247
  requestKey = `edit_mcp_${data.request_id}`;
 
 
248
  }
249
 
250
  // Skip if we've already processed this request
@@ -341,6 +894,100 @@ const setupUnifiedStream = () => {
341
  console.error('[SSE] Error sending edit MCP result:', err);
342
  });
343
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  // Handle deletion requests
345
  else if (data.type === 'delete' && data.block_id) {
346
  console.log('[SSE] Received deletion request for block:', data.block_id);
@@ -396,558 +1043,6 @@ const setupUnifiedStream = () => {
396
  let blockId = null;
397
 
398
  try {
399
- // Parse and create blocks recursively
400
- function parseAndCreateBlock(spec, shouldPosition = false, placementType = null, placementBlockID = null) {
401
- // Match block_name(inputs(...)) with proper parenthesis matching
402
- const blockMatch = spec.match(/^(\w+)\s*\((.*)$/s);
403
-
404
- if (!blockMatch) {
405
- throw new Error(`Invalid block specification format: ${spec}`);
406
- }
407
-
408
- const blockType = blockMatch[1];
409
- let content = blockMatch[2].trim();
410
-
411
- // We need to find the matching closing parenthesis for blockType(
412
- // Count from the beginning and find where the matching ) is
413
- let parenCount = 1; // We already have the opening (
414
- let matchIndex = -1;
415
- let inQuotes = false;
416
- let quoteChar = '';
417
-
418
- for (let i = 0; i < content.length; i++) {
419
- const char = content[i];
420
-
421
- // Handle quotes
422
- if ((char === '"' || char === "'") && (i === 0 || content[i - 1] !== '\\')) {
423
- if (!inQuotes) {
424
- inQuotes = true;
425
- quoteChar = char;
426
- } else if (char === quoteChar) {
427
- inQuotes = false;
428
- }
429
- }
430
-
431
- // Only count parens outside quotes
432
- if (!inQuotes) {
433
- if (char === '(') parenCount++;
434
- else if (char === ')') {
435
- parenCount--;
436
- if (parenCount === 0) {
437
- matchIndex = i;
438
- break;
439
- }
440
- }
441
- }
442
- }
443
-
444
- // Extract content up to the matching closing paren
445
- if (matchIndex >= 0) {
446
- content = content.slice(0, matchIndex).trim();
447
- } else {
448
- // Fallback: remove last paren if present
449
- if (content.endsWith(')')) {
450
- content = content.slice(0, -1).trim();
451
- }
452
- }
453
-
454
- console.log('[SSE CREATE] Parsing block:', blockType, 'with content:', content);
455
-
456
- // Check if this has inputs() wrapper
457
- let inputsContent = content;
458
- if (content.startsWith('inputs(')) {
459
- // Find the matching closing parenthesis for inputs(
460
- parenCount = 1;
461
- matchIndex = -1;
462
- inQuotes = false;
463
- quoteChar = '';
464
-
465
- for (let i = 7; i < content.length; i++) { // Start after 'inputs('
466
- const char = content[i];
467
-
468
- // Handle quotes
469
- if ((char === '"' || char === "'") && (i === 0 || content[i - 1] !== '\\')) {
470
- if (!inQuotes) {
471
- inQuotes = true;
472
- quoteChar = char;
473
- } else if (char === quoteChar) {
474
- inQuotes = false;
475
- }
476
- }
477
-
478
- // Only count parens outside quotes
479
- if (!inQuotes) {
480
- if (char === '(') parenCount++;
481
- else if (char === ')') {
482
- parenCount--;
483
- if (parenCount === 0) {
484
- matchIndex = i;
485
- break;
486
- }
487
- }
488
- }
489
- }
490
-
491
- // Extract content between inputs( and its matching )
492
- if (matchIndex >= 0) {
493
- inputsContent = content.slice(7, matchIndex);
494
- } else {
495
- // Fallback: remove inputs( and last )
496
- if (content.endsWith(')')) {
497
- inputsContent = content.slice(7, -1);
498
- } else {
499
- inputsContent = content.slice(7);
500
- }
501
- }
502
- }
503
-
504
- console.log('[SSE CREATE] inputsContent to parse:', inputsContent);
505
-
506
- // VALIDATION: Check if trying to place a value block under a statement block
507
- // Value blocks have an output connection but no previous connection
508
- if (placementType === 'under') {
509
- // Check if this block type is a value block by temporarily creating it
510
- const testBlock = ws.newBlock(blockType);
511
- const isValueBlock = testBlock.outputConnection && !testBlock.previousConnection;
512
- testBlock.dispose(true); // Remove the test block
513
-
514
- if (isValueBlock) {
515
- throw new Error(`Cannot place value block '${blockType}' under a statement block. Value blocks must be nested inside inputs of other blocks or placed in MCP outputs using type: "input". Try creating a variable and assigning the value to that.`);
516
- }
517
- }
518
-
519
- // Create the block
520
- const newBlock = ws.newBlock(blockType);
521
-
522
- if (inputsContent) {
523
- // Parse the inputs content
524
- console.log('[SSE CREATE] About to call parseInputs with:', inputsContent);
525
- const inputs = parseInputs(inputsContent);
526
- console.log('[SSE CREATE] Parsed inputs:', inputs);
527
-
528
- // Special handling for make_json block
529
- if (blockType === 'make_json') {
530
- // Count FIELD entries to determine how many fields we need
531
- let fieldCount = 0;
532
- const fieldValues = {};
533
- const keyValues = {};
534
-
535
- for (const [key, value] of Object.entries(inputs)) {
536
- const fieldMatch = key.match(/^FIELD(\d+)$/);
537
- const keyMatch = key.match(/^KEY(\d+)$/);
538
-
539
- if (fieldMatch) {
540
- const index = parseInt(fieldMatch[1]);
541
- fieldCount = Math.max(fieldCount, index + 1);
542
- fieldValues[index] = value;
543
- } else if (keyMatch) {
544
- const index = parseInt(keyMatch[1]);
545
- keyValues[index] = value;
546
- }
547
- }
548
-
549
- // Set up the mutator state
550
- if (fieldCount > 0) {
551
- newBlock.fieldCount_ = fieldCount;
552
- newBlock.fieldKeys_ = [];
553
-
554
- // Create the inputs through the mutator
555
- for (let i = 0; i < fieldCount; i++) {
556
- const keyValue = keyValues[i];
557
- const key = (typeof keyValue === 'string' && !keyValue.match(/^\w+\s*\(inputs\(/))
558
- ? keyValue.replace(/^["']|["']$/g, '')
559
- : `key${i}`;
560
-
561
- newBlock.fieldKeys_[i] = key;
562
-
563
- // Create the input
564
- const input = newBlock.appendValueInput('FIELD' + i);
565
- const field = new Blockly.FieldTextInput(key);
566
- field.setValidator((newValue) => {
567
- newBlock.fieldKeys_[i] = newValue || `key${i}`;
568
- return newValue;
569
- });
570
- input.appendField(field, 'KEY' + i);
571
- input.appendField(':');
572
- }
573
-
574
- // Now connect the field values
575
- for (let i = 0; i < fieldCount; i++) {
576
- const value = fieldValues[i];
577
- if (value && typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
578
- // This is a nested block, create it recursively
579
- const childBlock = parseAndCreateBlock(value);
580
-
581
- // Connect the child block to the FIELD input
582
- const input = newBlock.getInput('FIELD' + i);
583
- if (input && input.connection && childBlock.outputConnection) {
584
- childBlock.outputConnection.connect(input.connection);
585
- }
586
- }
587
- }
588
- }
589
- } else if (blockType === 'text_join') {
590
- // Special handling for text_join block (and similar blocks with ADD0, ADD1, ADD2...)
591
- // Count ADD entries to determine how many items we need
592
- let addCount = 0;
593
- const addValues = {};
594
-
595
- for (const [key, value] of Object.entries(inputs)) {
596
- const addMatch = key.match(/^ADD(\d+)$/);
597
- if (addMatch) {
598
- const index = parseInt(addMatch[1]);
599
- addCount = Math.max(addCount, index + 1);
600
- addValues[index] = value;
601
- }
602
- }
603
-
604
- console.log('[SSE CREATE] text_join detected with', addCount, 'items');
605
-
606
- // Store pending text_join state to apply after initSvg()
607
- if (addCount > 0) {
608
- newBlock.pendingAddCount_ = addCount;
609
- newBlock.pendingAddValues_ = addValues;
610
- }
611
- } else if (blockType === 'controls_if') {
612
- // Special handling for if/else blocks - create condition blocks now and store references
613
- const conditionBlocks = {};
614
- const conditionBlockObjects = {};
615
- let hasElse = false;
616
-
617
- console.log('[SSE CREATE] controls_if inputs:', inputs);
618
-
619
- // Process condition inputs and store block objects
620
- // Blockly uses IF0, IF1, IF2... not IF, IFELSEN0, IFELSEN1
621
- for (const [key, value] of Object.entries(inputs)) {
622
- if (key.match(/^IF\d+$/)) {
623
- // This is a condition block specification (IF0, IF1, IF2, ...)
624
- conditionBlocks[key] = value;
625
-
626
- if (typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
627
- // Create the condition block now
628
- const conditionBlock = parseAndCreateBlock(value);
629
- conditionBlockObjects[key] = conditionBlock;
630
- console.log('[SSE CREATE] Created condition block for', key);
631
- }
632
- } else if (key === 'ELSE' && value === true) {
633
- // ELSE is a marker with no value (set to true by parseInputs)
634
- console.log('[SSE CREATE] Detected ELSE marker');
635
- hasElse = true;
636
- }
637
- }
638
-
639
- // Count IFELSE (else-if) blocks: IF1, IF2, IF3... (IF0 is the main if, not an else-if)
640
- let elseIfCount = 0;
641
- for (const key of Object.keys(conditionBlocks)) {
642
- if (key.match(/^IF\d+$/) && key !== 'IF0') {
643
- elseIfCount++;
644
- }
645
- }
646
-
647
- console.log('[SSE CREATE] controls_if parsed: elseIfCount =', elseIfCount, 'hasElse =', hasElse);
648
-
649
- // Store condition block OBJECTS for later - we'll connect them after mutator creates inputs
650
- newBlock.pendingConditionBlockObjects_ = conditionBlockObjects;
651
- newBlock.pendingElseifCount_ = elseIfCount;
652
- newBlock.pendingElseCount_ = hasElse ? 1 : 0;
653
- console.log('[SSE CREATE] Stored pending condition block objects:', Object.keys(conditionBlockObjects));
654
- // Skip normal input processing for controls_if - we handle conditions after mutator
655
- } else if (blockType !== 'controls_if') {
656
- // Normal block handling (skip for controls_if which is handled specially)
657
- for (const [key, value] of Object.entries(inputs)) {
658
- if (typeof value === 'string') {
659
- // Check if this is a nested block specification
660
- if (value.match(/^\w+\s*\(inputs\(/)) {
661
- // This is a nested block, create it recursively
662
- const childBlock = parseAndCreateBlock(value);
663
-
664
- // Connect the child block to the appropriate input
665
- const input = newBlock.getInput(key);
666
- if (input && input.connection && childBlock.outputConnection) {
667
- childBlock.outputConnection.connect(input.connection);
668
- }
669
- } else {
670
- // This is a simple value, set it as a field
671
- // Remove quotes if present
672
- const cleanValue = value.replace(/^["']|["']$/g, '');
673
-
674
- // Try to set as a field value
675
- try {
676
- newBlock.setFieldValue(cleanValue, key);
677
- } catch (e) {
678
- console.log(`[SSE CREATE] Could not set field ${key} to ${cleanValue}:`, e);
679
- }
680
- }
681
- } else if (typeof value === 'number') {
682
- // Set numeric field value
683
- try {
684
- newBlock.setFieldValue(value, key);
685
- } catch (e) {
686
- console.log(`[SSE CREATE] Could not set field ${key} to ${value}:`, e);
687
- }
688
- } else if (typeof value === 'boolean') {
689
- // Set boolean field value
690
- try {
691
- newBlock.setFieldValue(value ? 'TRUE' : 'FALSE', key);
692
- } catch (e) {
693
- console.log(`[SSE CREATE] Could not set field ${key} to ${value}:`, e);
694
- }
695
- }
696
- }
697
- }
698
- }
699
-
700
- // Initialize the block (renders it)
701
- newBlock.initSvg();
702
-
703
- // Apply pending controls_if mutations (must be after initSvg)
704
- try {
705
- console.log('[SSE CREATE] Checking for controls_if mutations: type =', newBlock.type, 'pendingElseifCount_ =', newBlock.pendingElseifCount_, 'pendingConditionBlockObjects_ =', !!newBlock.pendingConditionBlockObjects_);
706
- if (newBlock.type === 'controls_if' && (newBlock.pendingElseifCount_ > 0 || newBlock.pendingElseCount_ > 0 || newBlock.pendingConditionBlockObjects_)) {
707
- console.log('[SSE CREATE] ENTERING controls_if mutation block');
708
- console.log('[SSE CREATE] Applying controls_if mutation:', {
709
- elseifCount: newBlock.pendingElseifCount_,
710
- elseCount: newBlock.pendingElseCount_
711
- });
712
-
713
- // Use the loadExtraState method if available (Blockly's preferred way)
714
- if (typeof newBlock.loadExtraState === 'function') {
715
- const state = {};
716
- if (newBlock.pendingElseifCount_ > 0) {
717
- state.elseIfCount = newBlock.pendingElseifCount_;
718
- }
719
- if (newBlock.pendingElseCount_ > 0) {
720
- state.hasElse = true;
721
- }
722
- console.log('[SSE CREATE] Using loadExtraState with:', state);
723
- newBlock.loadExtraState(state);
724
- console.log('[SSE CREATE] After loadExtraState');
725
- } else {
726
- // Fallback: Set the internal state variables and call updateShape_
727
- newBlock.elseifCount_ = newBlock.pendingElseifCount_;
728
- newBlock.elseCount_ = newBlock.pendingElseCount_;
729
-
730
- if (typeof newBlock.updateShape_ === 'function') {
731
- console.log('[SSE CREATE] Calling updateShape_ on controls_if');
732
- newBlock.updateShape_();
733
- }
734
- }
735
-
736
- // Now that the mutator has created all the inputs, connect the stored condition block objects
737
- console.log('[SSE CREATE] pendingConditionBlockObjects_ exists?', !!newBlock.pendingConditionBlockObjects_);
738
- if (newBlock.pendingConditionBlockObjects_) {
739
- const conditionBlockObjects = newBlock.pendingConditionBlockObjects_;
740
- console.log('[SSE CREATE] Connecting condition blocks:', Object.keys(conditionBlockObjects));
741
-
742
- // Connect the IF0 condition
743
- if (conditionBlockObjects['IF0']) {
744
- const ifBlock = conditionBlockObjects['IF0'];
745
- const input = newBlock.getInput('IF0');
746
- console.log('[SSE CREATE] IF0 input exists?', !!input);
747
- if (input && input.connection && ifBlock.outputConnection) {
748
- ifBlock.outputConnection.connect(input.connection);
749
- console.log('[SSE CREATE] Connected IF0 condition');
750
- } else {
751
- console.warn('[SSE CREATE] Could not connect IF0 - input:', !!input, 'childConnection:', !!ifBlock.outputConnection);
752
- }
753
- }
754
-
755
- // Connect IF1, IF2, IF3... (else-if conditions)
756
- console.log('[SSE CREATE] Processing', newBlock.pendingElseifCount_, 'else-if conditions');
757
- for (let i = 1; i <= newBlock.pendingElseifCount_; i++) {
758
- const key = 'IF' + i;
759
- console.log('[SSE CREATE] Looking for key:', key, 'exists?', !!conditionBlockObjects[key]);
760
- if (conditionBlockObjects[key]) {
761
- const ifElseBlock = conditionBlockObjects[key];
762
- const input = newBlock.getInput(key);
763
- console.log('[SSE CREATE] Input', key, 'exists?', !!input);
764
- if (input && input.connection && ifElseBlock.outputConnection) {
765
- ifElseBlock.outputConnection.connect(input.connection);
766
- console.log('[SSE CREATE] Connected', key, 'condition');
767
- } else {
768
- console.warn('[SSE CREATE] Could not connect', key, '- input exists:', !!input, 'has connection:', input ? !!input.connection : false, 'childHasOutput:', !!ifElseBlock.outputConnection);
769
- }
770
- }
771
- }
772
- } else {
773
- console.warn('[SSE CREATE] No pendingConditionBlockObjects_ found');
774
- }
775
-
776
- // Verify the ELSE input was created
777
- if (newBlock.pendingElseCount_ > 0) {
778
- const elseInput = newBlock.getInput('ELSE');
779
- console.log('[SSE CREATE] ELSE input after mutation:', elseInput);
780
- if (!elseInput) {
781
- console.error('[SSE CREATE] ELSE input was NOT created!');
782
- }
783
- }
784
-
785
- // Re-render after connecting condition blocks
786
- newBlock.render();
787
- }
788
- } catch (err) {
789
- console.error('[SSE CREATE] Error in controls_if mutations:', err);
790
- }
791
-
792
- // Apply pending text_join mutations (must be after initSvg)
793
- if (newBlock.type === 'text_join' && newBlock.pendingAddCount_ && newBlock.pendingAddCount_ > 0) {
794
- console.log('[SSE CREATE] Applying text_join mutation with', newBlock.pendingAddCount_, 'items');
795
-
796
- const addCount = newBlock.pendingAddCount_;
797
- const addValues = newBlock.pendingAddValues_;
798
-
799
- // Use loadExtraState if available to set the item count
800
- if (typeof newBlock.loadExtraState === 'function') {
801
- newBlock.loadExtraState({ itemCount: addCount });
802
- } else {
803
- // Fallback: set internal state
804
- newBlock.itemCount_ = addCount;
805
- }
806
-
807
- // Now connect the ADD values
808
- for (let i = 0; i < addCount; i++) {
809
- const value = addValues[i];
810
- if (value && typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
811
- // This is a nested block, create it recursively
812
- const childBlock = parseAndCreateBlock(value);
813
-
814
- // Connect the child block to the ADD input
815
- const input = newBlock.getInput('ADD' + i);
816
- if (input && input.connection && childBlock.outputConnection) {
817
- childBlock.outputConnection.connect(input.connection);
818
- console.log('[SSE CREATE] Connected ADD' + i + ' input');
819
- } else {
820
- console.warn('[SSE CREATE] Could not connect ADD' + i + ' input');
821
- }
822
- }
823
- }
824
- }
825
-
826
- // Only position the top-level block
827
- if (shouldPosition) {
828
- // Find a good position that doesn't overlap existing blocks
829
- const existingBlocks = ws.getAllBlocks();
830
- let x = 50;
831
- let y = 50;
832
-
833
- // Simple positioning: stack new blocks vertically
834
- if (existingBlocks.length > 0) {
835
- const lastBlock = existingBlocks[existingBlocks.length - 1];
836
- const lastPos = lastBlock.getRelativeToSurfaceXY();
837
- y = lastPos.y + lastBlock.height + 20;
838
- }
839
-
840
- newBlock.moveBy(x, y);
841
- }
842
-
843
- // Render the block
844
- newBlock.render();
845
-
846
- return newBlock;
847
- }
848
-
849
- // Helper function to parse inputs(key: value, key2: value2, ...)
850
- function parseInputs(inputStr) {
851
- const result = {};
852
- let currentKey = '';
853
- let currentValue = '';
854
- let depth = 0;
855
- let inQuotes = false;
856
- let quoteChar = '';
857
- let readingKey = true;
858
-
859
- for (let i = 0; i < inputStr.length; i++) {
860
- const char = inputStr[i];
861
-
862
- // Handle quotes
863
- if ((char === '"' || char === "'") && (i === 0 || inputStr[i - 1] !== '\\')) {
864
- if (!inQuotes) {
865
- inQuotes = true;
866
- quoteChar = char;
867
- } else if (char === quoteChar) {
868
- inQuotes = false;
869
- quoteChar = '';
870
- }
871
- }
872
-
873
- // Handle parentheses depth (for nested blocks)
874
- if (!inQuotes) {
875
- if (char === '(') depth++;
876
- else if (char === ')') depth--;
877
- }
878
-
879
- // Handle key-value separation
880
- if (char === ':' && depth === 0 && !inQuotes && readingKey) {
881
- readingKey = false;
882
- currentKey = currentKey.trim();
883
- continue;
884
- }
885
-
886
- // Handle comma separation
887
- if (char === ',' && depth === 0 && !inQuotes && !readingKey) {
888
- // Store the key-value pair
889
- currentValue = currentValue.trim();
890
-
891
- // Parse the value
892
- if (currentValue.match(/^\w+\s*\(inputs\(/)) {
893
- // This is a nested block
894
- result[currentKey] = currentValue;
895
- } else if (currentValue.match(/^-?\d+(\.\d+)?$/)) {
896
- // This is a number
897
- result[currentKey] = parseFloat(currentValue);
898
- } else if (currentValue === 'true' || currentValue === 'false') {
899
- // This is a boolean
900
- result[currentKey] = currentValue === 'true';
901
- } else {
902
- // This is a string (remove quotes if present)
903
- result[currentKey] = currentValue.replace(/^["']|["']$/g, '');
904
- }
905
-
906
- // Reset for next key-value pair
907
- currentKey = '';
908
- currentValue = '';
909
- readingKey = true;
910
- continue;
911
- }
912
-
913
- // Accumulate characters
914
- if (readingKey) {
915
- currentKey += char;
916
- } else {
917
- currentValue += char;
918
- }
919
- }
920
-
921
- // Handle the last key-value pair
922
- if (currentKey) {
923
- currentKey = currentKey.trim();
924
-
925
- // If there's no value, this is a flag/marker (like ELSE)
926
- if (!currentValue) {
927
- result[currentKey] = true; // Mark it as present
928
- } else {
929
- currentValue = currentValue.trim();
930
-
931
- // Parse the value
932
- if (currentValue.match(/^\w+\s*\(inputs\(/)) {
933
- // This is a nested block
934
- result[currentKey] = currentValue;
935
- } else if (currentValue.match(/^-?\d+(\.\d+)?$/)) {
936
- // This is a number
937
- result[currentKey] = parseFloat(currentValue);
938
- } else if (currentValue === 'true' || currentValue === 'false') {
939
- // This is a boolean
940
- result[currentKey] = currentValue === 'true';
941
- } else {
942
- // This is a string (remove quotes if present)
943
- result[currentKey] = currentValue.replace(/^["']|["']$/g, '');
944
- }
945
- }
946
- }
947
-
948
- return result;
949
- }
950
-
951
  // Create the block and all its nested children
952
  const newBlock = parseAndCreateBlock(data.block_spec, true, data.placement_type, data.blockID);
953
 
 
223
  ws.cleanUp();
224
  });
225
 
226
+ function parseAndCreateBlock(spec, shouldPosition = false, placementType = null, placementBlockID = null) {
227
+ // Match block_name(inputs(...)) with proper parenthesis matching
228
+ const blockMatch = spec.match(/^(\w+)\s*\((.*)$/s);
229
+
230
+ if (!blockMatch) {
231
+ throw new Error(`Invalid block specification format: ${spec}`);
232
+ }
233
+
234
+ const blockType = blockMatch[1];
235
+ let content = blockMatch[2].trim();
236
+
237
+ // We need to find the matching closing parenthesis for blockType(
238
+ // Count from the beginning and find where the matching ) is
239
+ let parenCount = 1; // We already have the opening (
240
+ let matchIndex = -1;
241
+ let inQuotes = false;
242
+ let quoteChar = '';
243
+
244
+ for (let i = 0; i < content.length; i++) {
245
+ const char = content[i];
246
+
247
+ // Handle quotes
248
+ if ((char === '"' || char === "'") && (i === 0 || content[i - 1] !== '\\')) {
249
+ if (!inQuotes) {
250
+ inQuotes = true;
251
+ quoteChar = char;
252
+ } else if (char === quoteChar) {
253
+ inQuotes = false;
254
+ }
255
+ }
256
+
257
+ // Only count parens outside quotes
258
+ if (!inQuotes) {
259
+ if (char === '(') parenCount++;
260
+ else if (char === ')') {
261
+ parenCount--;
262
+ if (parenCount === 0) {
263
+ matchIndex = i;
264
+ break;
265
+ }
266
+ }
267
+ }
268
+ }
269
+
270
+ // Extract content up to the matching closing paren
271
+ if (matchIndex >= 0) {
272
+ content = content.slice(0, matchIndex).trim();
273
+ } else {
274
+ // Fallback: remove last paren if present
275
+ if (content.endsWith(')')) {
276
+ content = content.slice(0, -1).trim();
277
+ }
278
+ }
279
+
280
+ console.log('[SSE CREATE] Parsing block:', blockType, 'with content:', content);
281
+
282
+ // Check if this has inputs() wrapper
283
+ let inputsContent = content;
284
+ if (content.startsWith('inputs(')) {
285
+ // Find the matching closing parenthesis for inputs(
286
+ parenCount = 1;
287
+ matchIndex = -1;
288
+ inQuotes = false;
289
+ quoteChar = '';
290
+
291
+ for (let i = 7; i < content.length; i++) { // Start after 'inputs('
292
+ const char = content[i];
293
+
294
+ // Handle quotes
295
+ if ((char === '"' || char === "'") && (i === 0 || content[i - 1] !== '\\')) {
296
+ if (!inQuotes) {
297
+ inQuotes = true;
298
+ quoteChar = char;
299
+ } else if (char === quoteChar) {
300
+ inQuotes = false;
301
+ }
302
+ }
303
+
304
+ // Only count parens outside quotes
305
+ if (!inQuotes) {
306
+ if (char === '(') parenCount++;
307
+ else if (char === ')') {
308
+ parenCount--;
309
+ if (parenCount === 0) {
310
+ matchIndex = i;
311
+ break;
312
+ }
313
+ }
314
+ }
315
+ }
316
+
317
+ // Extract content between inputs( and its matching )
318
+ if (matchIndex >= 0) {
319
+ inputsContent = content.slice(7, matchIndex);
320
+ } else {
321
+ // Fallback: remove inputs( and last )
322
+ if (content.endsWith(')')) {
323
+ inputsContent = content.slice(7, -1);
324
+ } else {
325
+ inputsContent = content.slice(7);
326
+ }
327
+ }
328
+ }
329
+
330
+ console.log('[SSE CREATE] inputsContent to parse:', inputsContent);
331
+
332
+ // VALIDATION: Check if trying to place a value block under a statement block
333
+ // Value blocks have an output connection but no previous connection
334
+ if (placementType === 'under') {
335
+ // Check if this block type is a value block by temporarily creating it
336
+ const testBlock = ws.newBlock(blockType);
337
+ const isValueBlock = testBlock.outputConnection && !testBlock.previousConnection;
338
+ testBlock.dispose(true); // Remove the test block
339
+
340
+ if (isValueBlock) {
341
+ throw new Error(`Cannot place value block '${blockType}' under a statement block. Value blocks must be nested inside inputs of other blocks or placed in MCP outputs using type: "input". Try creating a variable and assigning the value to that.`);
342
+ }
343
+ }
344
+
345
+ // Create the block
346
+ const newBlock = ws.newBlock(blockType);
347
+
348
+ if (inputsContent) {
349
+ // Parse the inputs content
350
+ console.log('[SSE CREATE] About to call parseInputs with:', inputsContent);
351
+ const inputs = parseInputs(inputsContent);
352
+ console.log('[SSE CREATE] Parsed inputs:', inputs);
353
+
354
+ // Special handling for make_json block
355
+ if (blockType === 'make_json') {
356
+ // Count FIELD entries to determine how many fields we need
357
+ let fieldCount = 0;
358
+ const fieldValues = {};
359
+ const keyValues = {};
360
+
361
+ for (const [key, value] of Object.entries(inputs)) {
362
+ const fieldMatch = key.match(/^FIELD(\d+)$/);
363
+ const keyMatch = key.match(/^KEY(\d+)$/);
364
+
365
+ if (fieldMatch) {
366
+ const index = parseInt(fieldMatch[1]);
367
+ fieldCount = Math.max(fieldCount, index + 1);
368
+ fieldValues[index] = value;
369
+ } else if (keyMatch) {
370
+ const index = parseInt(keyMatch[1]);
371
+ keyValues[index] = value;
372
+ }
373
+ }
374
+
375
+ // Set up the mutator state
376
+ if (fieldCount > 0) {
377
+ newBlock.fieldCount_ = fieldCount;
378
+ newBlock.fieldKeys_ = [];
379
+
380
+ // Create the inputs through the mutator
381
+ for (let i = 0; i < fieldCount; i++) {
382
+ const keyValue = keyValues[i];
383
+ const key = (typeof keyValue === 'string' && !keyValue.match(/^\w+\s*\(inputs\(/))
384
+ ? keyValue.replace(/^["']|["']$/g, '')
385
+ : `key${i}`;
386
+
387
+ newBlock.fieldKeys_[i] = key;
388
+
389
+ // Create the input
390
+ const input = newBlock.appendValueInput('FIELD' + i);
391
+ const field = new Blockly.FieldTextInput(key);
392
+ field.setValidator((newValue) => {
393
+ newBlock.fieldKeys_[i] = newValue || `key${i}`;
394
+ return newValue;
395
+ });
396
+ input.appendField(field, 'KEY' + i);
397
+ input.appendField(':');
398
+ }
399
+
400
+ // Now connect the field values
401
+ for (let i = 0; i < fieldCount; i++) {
402
+ const value = fieldValues[i];
403
+ if (value && typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
404
+ // This is a nested block, create it recursively
405
+ const childBlock = parseAndCreateBlock(value);
406
+
407
+ // Connect the child block to the FIELD input
408
+ const input = newBlock.getInput('FIELD' + i);
409
+ if (input && input.connection && childBlock.outputConnection) {
410
+ childBlock.outputConnection.connect(input.connection);
411
+ }
412
+ }
413
+ }
414
+ }
415
+ } else if (blockType === 'text_join') {
416
+ // Special handling for text_join block (and similar blocks with ADD0, ADD1, ADD2...)
417
+ // Count ADD entries to determine how many items we need
418
+ let addCount = 0;
419
+ const addValues = {};
420
+
421
+ for (const [key, value] of Object.entries(inputs)) {
422
+ const addMatch = key.match(/^ADD(\d+)$/);
423
+ if (addMatch) {
424
+ const index = parseInt(addMatch[1]);
425
+ addCount = Math.max(addCount, index + 1);
426
+ addValues[index] = value;
427
+ }
428
+ }
429
+
430
+ console.log('[SSE CREATE] text_join detected with', addCount, 'items');
431
+
432
+ // Store pending text_join state to apply after initSvg()
433
+ if (addCount > 0) {
434
+ newBlock.pendingAddCount_ = addCount;
435
+ newBlock.pendingAddValues_ = addValues;
436
+ }
437
+ } else if (blockType === 'controls_if') {
438
+ // Special handling for if/else blocks - create condition blocks now and store references
439
+ const conditionBlocks = {};
440
+ const conditionBlockObjects = {};
441
+ let hasElse = false;
442
+
443
+ console.log('[SSE CREATE] controls_if inputs:', inputs);
444
+
445
+ // Process condition inputs and store block objects
446
+ // Blockly uses IF0, IF1, IF2... not IF, IFELSEN0, IFELSEN1
447
+ for (const [key, value] of Object.entries(inputs)) {
448
+ if (key.match(/^IF\d+$/)) {
449
+ // This is a condition block specification (IF0, IF1, IF2, ...)
450
+ conditionBlocks[key] = value;
451
+
452
+ if (typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
453
+ // Create the condition block now
454
+ const conditionBlock = parseAndCreateBlock(value);
455
+ conditionBlockObjects[key] = conditionBlock;
456
+ console.log('[SSE CREATE] Created condition block for', key);
457
+ }
458
+ } else if (key === 'ELSE' && value === true) {
459
+ // ELSE is a marker with no value (set to true by parseInputs)
460
+ console.log('[SSE CREATE] Detected ELSE marker');
461
+ hasElse = true;
462
+ }
463
+ }
464
+
465
+ // Count IFELSE (else-if) blocks: IF1, IF2, IF3... (IF0 is the main if, not an else-if)
466
+ let elseIfCount = 0;
467
+ for (const key of Object.keys(conditionBlocks)) {
468
+ if (key.match(/^IF\d+$/) && key !== 'IF0') {
469
+ elseIfCount++;
470
+ }
471
+ }
472
+
473
+ console.log('[SSE CREATE] controls_if parsed: elseIfCount =', elseIfCount, 'hasElse =', hasElse);
474
+
475
+ // Store condition block OBJECTS for later - we'll connect them after mutator creates inputs
476
+ newBlock.pendingConditionBlockObjects_ = conditionBlockObjects;
477
+ newBlock.pendingElseifCount_ = elseIfCount;
478
+ newBlock.pendingElseCount_ = hasElse ? 1 : 0;
479
+ console.log('[SSE CREATE] Stored pending condition block objects:', Object.keys(conditionBlockObjects));
480
+ // Skip normal input processing for controls_if - we handle conditions after mutator
481
+ } else if (blockType !== 'controls_if') {
482
+ // Normal block handling (skip for controls_if which is handled specially)
483
+ for (const [key, value] of Object.entries(inputs)) {
484
+ if (typeof value === 'string') {
485
+ // Check if this is a nested block specification
486
+ if (value.match(/^\w+\s*\(inputs\(/)) {
487
+ // This is a nested block, create it recursively
488
+ const childBlock = parseAndCreateBlock(value);
489
+
490
+ // Connect the child block to the appropriate input
491
+ const input = newBlock.getInput(key);
492
+ if (input && input.connection && childBlock.outputConnection) {
493
+ childBlock.outputConnection.connect(input.connection);
494
+ }
495
+ } else {
496
+ // This is a simple value, set it as a field
497
+ // Remove quotes if present
498
+ const cleanValue = value.replace(/^["']|["']$/g, '');
499
+
500
+ // Try to set as a field value
501
+ try {
502
+ newBlock.setFieldValue(cleanValue, key);
503
+ } catch (e) {
504
+ console.log(`[SSE CREATE] Could not set field ${key} to ${cleanValue}:`, e);
505
+ }
506
+ }
507
+ } else if (typeof value === 'number') {
508
+ // Set numeric field value
509
+ try {
510
+ newBlock.setFieldValue(value, key);
511
+ } catch (e) {
512
+ console.log(`[SSE CREATE] Could not set field ${key} to ${value}:`, e);
513
+ }
514
+ } else if (typeof value === 'boolean') {
515
+ // Set boolean field value
516
+ try {
517
+ newBlock.setFieldValue(value ? 'TRUE' : 'FALSE', key);
518
+ } catch (e) {
519
+ console.log(`[SSE CREATE] Could not set field ${key} to ${value}:`, e);
520
+ }
521
+ }
522
+ }
523
+ }
524
+ }
525
+
526
+ // Initialize the block (renders it)
527
+ newBlock.initSvg();
528
+
529
+ // Apply pending controls_if mutations (must be after initSvg)
530
+ try {
531
+ console.log('[SSE CREATE] Checking for controls_if mutations: type =', newBlock.type, 'pendingElseifCount_ =', newBlock.pendingElseifCount_, 'pendingConditionBlockObjects_ =', !!newBlock.pendingConditionBlockObjects_);
532
+ if (newBlock.type === 'controls_if' && (newBlock.pendingElseifCount_ > 0 || newBlock.pendingElseCount_ > 0 || newBlock.pendingConditionBlockObjects_)) {
533
+ console.log('[SSE CREATE] ENTERING controls_if mutation block');
534
+ console.log('[SSE CREATE] Applying controls_if mutation:', {
535
+ elseifCount: newBlock.pendingElseifCount_,
536
+ elseCount: newBlock.pendingElseCount_
537
+ });
538
+
539
+ // Use the loadExtraState method if available (Blockly's preferred way)
540
+ if (typeof newBlock.loadExtraState === 'function') {
541
+ const state = {};
542
+ if (newBlock.pendingElseifCount_ > 0) {
543
+ state.elseIfCount = newBlock.pendingElseifCount_;
544
+ }
545
+ if (newBlock.pendingElseCount_ > 0) {
546
+ state.hasElse = true;
547
+ }
548
+ console.log('[SSE CREATE] Using loadExtraState with:', state);
549
+ newBlock.loadExtraState(state);
550
+ console.log('[SSE CREATE] After loadExtraState');
551
+ } else {
552
+ // Fallback: Set the internal state variables and call updateShape_
553
+ newBlock.elseifCount_ = newBlock.pendingElseifCount_;
554
+ newBlock.elseCount_ = newBlock.pendingElseCount_;
555
+
556
+ if (typeof newBlock.updateShape_ === 'function') {
557
+ console.log('[SSE CREATE] Calling updateShape_ on controls_if');
558
+ newBlock.updateShape_();
559
+ }
560
+ }
561
+
562
+ // Now that the mutator has created all the inputs, connect the stored condition block objects
563
+ console.log('[SSE CREATE] pendingConditionBlockObjects_ exists?', !!newBlock.pendingConditionBlockObjects_);
564
+ if (newBlock.pendingConditionBlockObjects_) {
565
+ const conditionBlockObjects = newBlock.pendingConditionBlockObjects_;
566
+ console.log('[SSE CREATE] Connecting condition blocks:', Object.keys(conditionBlockObjects));
567
+
568
+ // Connect the IF0 condition
569
+ if (conditionBlockObjects['IF0']) {
570
+ const ifBlock = conditionBlockObjects['IF0'];
571
+ const input = newBlock.getInput('IF0');
572
+ console.log('[SSE CREATE] IF0 input exists?', !!input);
573
+ if (input && input.connection && ifBlock.outputConnection) {
574
+ ifBlock.outputConnection.connect(input.connection);
575
+ console.log('[SSE CREATE] Connected IF0 condition');
576
+ } else {
577
+ console.warn('[SSE CREATE] Could not connect IF0 - input:', !!input, 'childConnection:', !!ifBlock.outputConnection);
578
+ }
579
+ }
580
+
581
+ // Connect IF1, IF2, IF3... (else-if conditions)
582
+ console.log('[SSE CREATE] Processing', newBlock.pendingElseifCount_, 'else-if conditions');
583
+ for (let i = 1; i <= newBlock.pendingElseifCount_; i++) {
584
+ const key = 'IF' + i;
585
+ console.log('[SSE CREATE] Looking for key:', key, 'exists?', !!conditionBlockObjects[key]);
586
+ if (conditionBlockObjects[key]) {
587
+ const ifElseBlock = conditionBlockObjects[key];
588
+ const input = newBlock.getInput(key);
589
+ console.log('[SSE CREATE] Input', key, 'exists?', !!input);
590
+ if (input && input.connection && ifElseBlock.outputConnection) {
591
+ ifElseBlock.outputConnection.connect(input.connection);
592
+ console.log('[SSE CREATE] Connected', key, 'condition');
593
+ } else {
594
+ console.warn('[SSE CREATE] Could not connect', key, '- input exists:', !!input, 'has connection:', input ? !!input.connection : false, 'childHasOutput:', !!ifElseBlock.outputConnection);
595
+ }
596
+ }
597
+ }
598
+ } else {
599
+ console.warn('[SSE CREATE] No pendingConditionBlockObjects_ found');
600
+ }
601
+
602
+ // Verify the ELSE input was created
603
+ if (newBlock.pendingElseCount_ > 0) {
604
+ const elseInput = newBlock.getInput('ELSE');
605
+ console.log('[SSE CREATE] ELSE input after mutation:', elseInput);
606
+ if (!elseInput) {
607
+ console.error('[SSE CREATE] ELSE input was NOT created!');
608
+ }
609
+ }
610
+
611
+ // Re-render after connecting condition blocks
612
+ newBlock.render();
613
+ }
614
+ } catch (err) {
615
+ console.error('[SSE CREATE] Error in controls_if mutations:', err);
616
+ }
617
+
618
+ // Apply pending text_join mutations (must be after initSvg)
619
+ if (newBlock.type === 'text_join' && newBlock.pendingAddCount_ && newBlock.pendingAddCount_ > 0) {
620
+ console.log('[SSE CREATE] Applying text_join mutation with', newBlock.pendingAddCount_, 'items');
621
+
622
+ const addCount = newBlock.pendingAddCount_;
623
+ const addValues = newBlock.pendingAddValues_;
624
+
625
+ // Use loadExtraState if available to set the item count
626
+ if (typeof newBlock.loadExtraState === 'function') {
627
+ newBlock.loadExtraState({ itemCount: addCount });
628
+ } else {
629
+ // Fallback: set internal state
630
+ newBlock.itemCount_ = addCount;
631
+ }
632
+
633
+ // Now connect the ADD values
634
+ for (let i = 0; i < addCount; i++) {
635
+ const value = addValues[i];
636
+ if (value && typeof value === 'string' && value.match(/^\w+\s*\(inputs\(/)) {
637
+ // This is a nested block, create it recursively
638
+ const childBlock = parseAndCreateBlock(value);
639
+
640
+ // Connect the child block to the ADD input
641
+ const input = newBlock.getInput('ADD' + i);
642
+ if (input && input.connection && childBlock.outputConnection) {
643
+ childBlock.outputConnection.connect(input.connection);
644
+ console.log('[SSE CREATE] Connected ADD' + i + ' input');
645
+ } else {
646
+ console.warn('[SSE CREATE] Could not connect ADD' + i + ' input');
647
+ }
648
+ }
649
+ }
650
+ }
651
+
652
+ // Only position the top-level block
653
+ if (shouldPosition) {
654
+ // Find a good position that doesn't overlap existing blocks
655
+ const existingBlocks = ws.getAllBlocks();
656
+ let x = 50;
657
+ let y = 50;
658
+
659
+ // Simple positioning: stack new blocks vertically
660
+ if (existingBlocks.length > 0) {
661
+ const lastBlock = existingBlocks[existingBlocks.length - 1];
662
+ const lastPos = lastBlock.getRelativeToSurfaceXY();
663
+ y = lastPos.y + lastBlock.height + 20;
664
+ }
665
+
666
+ newBlock.moveBy(x, y);
667
+ }
668
+
669
+ // Render the block
670
+ newBlock.render();
671
+
672
+ return newBlock;
673
+ }
674
+
675
+ // Helper function to parse inputs(key: value, key2: value2, ...)
676
+ function parseInputs(inputStr) {
677
+ const result = {};
678
+ let currentKey = '';
679
+ let currentValue = '';
680
+ let depth = 0;
681
+ let inQuotes = false;
682
+ let quoteChar = '';
683
+ let readingKey = true;
684
+
685
+ for (let i = 0; i < inputStr.length; i++) {
686
+ const char = inputStr[i];
687
+
688
+ // Handle quotes
689
+ if ((char === '"' || char === "'") && (i === 0 || inputStr[i - 1] !== '\\')) {
690
+ if (!inQuotes) {
691
+ inQuotes = true;
692
+ quoteChar = char;
693
+ } else if (char === quoteChar) {
694
+ inQuotes = false;
695
+ quoteChar = '';
696
+ }
697
+ }
698
+
699
+ // Handle parentheses depth (for nested blocks)
700
+ if (!inQuotes) {
701
+ if (char === '(') depth++;
702
+ else if (char === ')') depth--;
703
+ }
704
+
705
+ // Handle key-value separation
706
+ if (char === ':' && depth === 0 && !inQuotes && readingKey) {
707
+ readingKey = false;
708
+ currentKey = currentKey.trim();
709
+ continue;
710
+ }
711
+
712
+ // Handle comma separation
713
+ if (char === ',' && depth === 0 && !inQuotes && !readingKey) {
714
+ // Store the key-value pair
715
+ currentValue = currentValue.trim();
716
+
717
+ // Parse the value
718
+ if (currentValue.match(/^\w+\s*\(inputs\(/)) {
719
+ // This is a nested block
720
+ result[currentKey] = currentValue;
721
+ } else if (currentValue.match(/^-?\d+(\.\d+)?$/)) {
722
+ // This is a number
723
+ result[currentKey] = parseFloat(currentValue);
724
+ } else if (currentValue === 'true' || currentValue === 'false') {
725
+ // This is a boolean
726
+ result[currentKey] = currentValue === 'true';
727
+ } else {
728
+ // This is a string (remove quotes if present)
729
+ result[currentKey] = currentValue.replace(/^["']|["']$/g, '');
730
+ }
731
+
732
+ // Reset for next key-value pair
733
+ currentKey = '';
734
+ currentValue = '';
735
+ readingKey = true;
736
+ continue;
737
+ }
738
+
739
+ // Accumulate characters
740
+ if (readingKey) {
741
+ currentKey += char;
742
+ } else {
743
+ currentValue += char;
744
+ }
745
+ }
746
+
747
+ // Handle the last key-value pair
748
+ if (currentKey) {
749
+ currentKey = currentKey.trim();
750
+
751
+ // If there's no value, this is a flag/marker (like ELSE)
752
+ if (!currentValue) {
753
+ result[currentKey] = true; // Mark it as present
754
+ } else {
755
+ currentValue = currentValue.trim();
756
+
757
+ // Parse the value
758
+ if (currentValue.match(/^\w+\s*\(inputs\(/)) {
759
+ // This is a nested block
760
+ result[currentKey] = currentValue;
761
+ } else if (currentValue.match(/^-?\d+(\.\d+)?$/)) {
762
+ // This is a number
763
+ result[currentKey] = parseFloat(currentValue);
764
+ } else if (currentValue === 'true' || currentValue === 'false') {
765
+ // This is a boolean
766
+ result[currentKey] = currentValue === 'true';
767
+ } else {
768
+ // This is a string (remove quotes if present)
769
+ result[currentKey] = currentValue.replace(/^["']|["']$/g, '');
770
+ }
771
+ }
772
+ }
773
+
774
+ return result;
775
+ }
776
+
777
  // Set up unified SSE connection for all workspace operations
778
  const setupUnifiedStream = () => {
779
  const eventSource = new EventSource('/unified_stream');
 
796
  requestKey = `variable_${data.request_id}`;
797
  } else if (data.type === 'edit_mcp') {
798
  requestKey = `edit_mcp_${data.request_id}`;
799
+ } else if (data.type === 'replace') {
800
+ requestKey = `replace_${data.request_id}`;
801
  }
802
 
803
  // Skip if we've already processed this request
 
894
  console.error('[SSE] Error sending edit MCP result:', err);
895
  });
896
  }
897
+ // Handle replace block requests
898
+ else if (data.type === 'replace' && data.block_id && data.block_spec && data.request_id) {
899
+ console.log('[SSE] Received replace request for block:', data.block_id, data.block_spec);
900
+
901
+ let success = false;
902
+ let error = null;
903
+ let blockId = null;
904
+
905
+ try {
906
+ // Get the block to be replaced
907
+ const blockToReplace = ws.getBlockById(data.block_id);
908
+
909
+ if (!blockToReplace) {
910
+ throw new Error(`Block ${data.block_id} not found`);
911
+ }
912
+
913
+ // Store the parent and connection info before deletion
914
+ const parentBlock = blockToReplace.getParent();
915
+ const previousBlock = blockToReplace.getPreviousBlock();
916
+ const nextBlock = blockToReplace.getNextBlock();
917
+ let parentConnection = null;
918
+ let connectionType = null;
919
+ let inputName = null;
920
+
921
+ // Check if this block is connected to a parent's input
922
+ if (blockToReplace.outputConnection && blockToReplace.outputConnection.targetConnection) {
923
+ parentConnection = blockToReplace.outputConnection.targetConnection;
924
+ } else if (blockToReplace.previousConnection && blockToReplace.previousConnection.targetConnection) {
925
+ parentConnection = blockToReplace.previousConnection.targetConnection;
926
+ }
927
+
928
+ // If the block is in an input socket, get that info
929
+ if (parentBlock) {
930
+ const inputs = parentBlock.inputList;
931
+ for (const input of inputs) {
932
+ if (input.connection && input.connection.targetBlock() === blockToReplace) {
933
+ inputName = input.name;
934
+ break;
935
+ }
936
+ }
937
+ }
938
+
939
+ // Create the new block using the shared function (no positioning, no placement type for replace)
940
+ const newBlock = parseAndCreateBlock(data.block_spec, false, null, null);
941
+
942
+ if (!newBlock) {
943
+ throw new Error('Failed to create replacement block');
944
+ }
945
+
946
+ // Reattach the new block to the parent connection
947
+ if (parentConnection) {
948
+ if (newBlock.outputConnection) {
949
+ parentConnection.connect(newBlock.outputConnection);
950
+ } else if (newBlock.previousConnection) {
951
+ parentConnection.connect(newBlock.previousConnection);
952
+ }
953
+ }
954
+
955
+ // Reattach next block if it was connected
956
+ if (nextBlock && newBlock.nextConnection) {
957
+ newBlock.nextConnection.connect(nextBlock.previousConnection);
958
+ }
959
+
960
+ // Dispose the old block
961
+ blockToReplace.dispose(true);
962
+
963
+ // Render the workspace
964
+ ws.render();
965
+
966
+ success = true;
967
+ blockId = newBlock.id;
968
+ console.log('[SSE] Successfully replaced block:', data.block_id, 'with:', newBlock.id);
969
+ } catch (e) {
970
+ error = e.toString();
971
+ console.error('[SSE] Error replacing block:', e);
972
+ }
973
+
974
+ // Send result back to backend
975
+ console.log('[SSE] Sending replace block result:', { request_id: data.request_id, success, error, block_id: blockId });
976
+ fetch('/replace_block_result', {
977
+ method: 'POST',
978
+ headers: { 'Content-Type': 'application/json' },
979
+ body: JSON.stringify({
980
+ request_id: data.request_id,
981
+ success: success,
982
+ error: error,
983
+ block_id: blockId
984
+ })
985
+ }).then(response => {
986
+ console.log('[SSE] Replace block result sent successfully');
987
+ }).catch(err => {
988
+ console.error('[SSE] Error sending replace block result:', err);
989
+ });
990
+ }
991
  // Handle deletion requests
992
  else if (data.type === 'delete' && data.block_id) {
993
  console.log('[SSE] Received deletion request for block:', data.block_id);
 
1043
  let blockId = null;
1044
 
1045
  try {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1046
  // Create the block and all its nested children
1047
  const newBlock = parseAndCreateBlock(data.block_spec, true, data.placement_type, data.blockID);
1048