diff --git a/agent/catalogs/step-catalog-en.json b/agent/catalogs/step-catalog-en.json
index 7243fc07..bff7e167 100644
--- a/agent/catalogs/step-catalog-en.json
+++ b/agent/catalogs/step-catalog-en.json
@@ -2765,8 +2765,8 @@
"enumValues": [
"SelectedLayout",
"OriginalLayout",
- "LayoutNameByCalculation",
- "LayoutNumberByCalculation"
+ "LayoutNameByCalc",
+ "LayoutNumberByCalc"
]
},
{
diff --git a/agent/scripts/deploy.py b/agent/scripts/deploy.py
index 8f3ed16b..7df5c38b 100644
--- a/agent/scripts/deploy.py
+++ b/agent/scripts/deploy.py
@@ -242,6 +242,10 @@ def _esc(s: str) -> str:
f' keystroke "s" using {{command down}}\n'
)
+ # SAFETY: poll for the target tab up to ~3 seconds. If the tab never
+ # appears, abort with an error — DO NOT press Cmd+A blindly, or the
+ # focused script-list panel will receive the keystroke and the
+ # subsequent Delete will wipe the entire script tree.
return (
f'tell application "{_esc(fm_app_name)}"\n'
f' activate\n'
@@ -251,20 +255,33 @@ def _esc(s: str) -> str:
f'\n'
f'tell application "System Events"\n'
f' tell process "{_esc(fm_process)}"\n'
- # AXPress the script tab to move focus to step editor
- f' set wsWindows to windows whose title contains "Script Workspace"\n'
- f' if (count of wsWindows) > 0 then\n'
- f' tell item 1 of wsWindows\n'
- f' tell splitter group 1\n'
- f' set tabButtons to every button whose description is "{_esc(target_script)}"\n'
- f' if (count of tabButtons) > 0 then\n'
- f' perform action "AXPress" of item 1 of tabButtons\n'
- f' end if\n'
- f' end tell\n'
- f' end tell\n'
+ f' set tabFound to false\n'
+ f' set attemptCount to 0\n'
+ f' repeat while (tabFound is false) and (attemptCount < 15)\n'
+ f' set wsWindows to windows whose title contains "Script Workspace"\n'
+ f' if (count of wsWindows) > 0 then\n'
+ f' try\n'
+ f' tell item 1 of wsWindows\n'
+ f' tell splitter group 1\n'
+ f' set tabButtons to every button whose description is "{_esc(target_script)}"\n'
+ f' if (count of tabButtons) > 0 then\n'
+ f' perform action "AXPress" of item 1 of tabButtons\n'
+ f' set tabFound to true\n'
+ f' end if\n'
+ f' end tell\n'
+ f' end tell\n'
+ f' end try\n'
+ f' end if\n'
+ f' if tabFound is false then\n'
+ f' delay 0.2\n'
+ f' set attemptCount to attemptCount + 1\n'
+ f' end if\n'
+ f' end repeat\n'
+ f' if tabFound is false then\n'
+ f' error "AGENTIC_FM_ABORT: target script tab \'{_esc(target_script)}\' not found in Script Workspace — refusing to send keystrokes" number 5001\n'
f' end if\n'
f' delay 0.5\n'
- # Paste sequence
+ # Paste sequence — only reached when the tab is confirmed focused
f'{paste_block}'
f'{save_block}'
f' end tell\n'
diff --git a/agent/scripts/fm_xml_to_snippet.py b/agent/scripts/fm_xml_to_snippet.py
index b91d8327..7a82e0ab 100644
--- a/agent/scripts/fm_xml_to_snippet.py
+++ b/agent/scripts/fm_xml_to_snippet.py
@@ -520,17 +520,27 @@ def tx_go_to_layout(step) -> str:
layout_name = ''
has_layout = False
animation = ''
+ calc_text = '' # for LayoutNameByCalc / LayoutNumberByCalc
p = param_by_type(step, 'LayoutReferenceContainer')
if p is not None:
lrc = p.find('LayoutReferenceContainer')
if lrc is not None:
+ lrc_value = lrc.get('value', '')
lr = lrc.find('LayoutReference')
if lr is not None:
+ # value=2 — explicit selected layout by id/name
layout_dest = 'SelectedLayout'
layout_id = lr.get('id', '0')
layout_name = lr.get('name', '')
has_layout = True
+ elif lrc_value in ('3', '4'):
+ # value=3 → LayoutNameByCalc, value=4 → LayoutNumberByCalc.
+ # SaXML nests the formula at LayoutReferenceContainer/Calculation/Calculation/Text.
+ layout_dest = 'LayoutNameByCalc' if lrc_value == '3' else 'LayoutNumberByCalc'
+ text_el = lrc.find('Calculation/Calculation/Text')
+ if text_el is not None and text_el.text:
+ calc_text = text_el.text
else:
label_el = lrc.find('Label')
label_text = (label_el.text or '').strip() if label_el is not None else ''
@@ -548,6 +558,11 @@ def tx_go_to_layout(step) -> str:
]
if has_layout:
parts.append(f'{L1}')
+ elif calc_text:
+ # Calculated layout name/number — wrap in with inner
+ parts.append(f'{L1}')
+ parts.append(f'{L2}')
+ parts.append(f'{L1}')
if animation:
parts.append(f'{L1}')
parts.append(f'{S}')
@@ -1740,6 +1755,99 @@ def tx_unknown(step) -> str:
return _tx_unknown_inner(name, enable, sid)
+def tx_perform_find_natural_language(step) -> str:
+ """
+ Perform Find by Natural Language (id=221).
+
+ SaXML params (each ):
+ Target → $VARIABLE (variable; field-ref form not yet observed)
+ LLMAccountName → (inside LLMCreateFind)
+ LLMModel → (inside LLMCreateFind)
+ LLMMessage → (inside LLMCreateFind)
+ Parameters → (inside LLMCreateFind)
+ LLMAction → Query|Data API|None (inside LLMCreateFind)
+ TemplateName → (inside LLMCreateFind)
+
+ LLMAction list name → Action element value:
+ "Found Set" → "Query"
+ "Found Set as JSON" → "Data API"
+ "Find Request as JSON" → "None"
+
+ Option state is "True" when Parameters present, else "False". SelectAll always "True".
+ """
+ enable, sid = step_attrs(step)
+
+ target_var = ''
+ account_calc = ''
+ model_calc = ''
+ prompt_calc = ''
+ parameters_calc = ''
+ template_calc = ''
+ action_value = 'Query'
+
+ action_map = {
+ 'Found Set': 'Query',
+ 'Found Set as JSON': 'Data API',
+ 'Find Request as JSON': 'None',
+ }
+
+ for p in all_params(step):
+ ptype = p.get('type', '')
+ if ptype == 'Target':
+ kind, val = _extract_saxml_target(p)
+ if kind == 'variable':
+ target_var = val or ''
+ elif ptype == 'LLMAccountName':
+ account_calc = _extract_saxml_calc(p)
+ elif ptype == 'LLMModel':
+ model_calc = _extract_saxml_calc(p)
+ elif ptype == 'LLMMessage':
+ prompt_calc = _extract_saxml_calc(p)
+ elif ptype == 'Parameters':
+ parameters_calc = _extract_saxml_calc(p)
+ elif ptype == 'TemplateName':
+ template_calc = _extract_saxml_calc(p)
+ elif ptype == 'LLMAction':
+ li = _extract_saxml_list(p)
+ if li:
+ action_value = action_map.get(li.get('name', ''), 'Query')
+
+ option_state = 'True' if parameters_calc else 'False'
+
+ parts = [
+ f'{S}',
+ f'{L1}',
+ f'{L1}',
+ ]
+ if target_var:
+ parts.append(f'{L1}{escape_attr(target_var)}')
+ else:
+ parts.append(f'{L1}')
+
+ parts.append(f'{L1}')
+ parts.append(f'{L2}')
+ parts.append(f'{L3}')
+ parts.append(f'{L2}')
+ parts.append(f'{L2}')
+ parts.append(f'{L3}')
+ parts.append(f'{L2}')
+ parts.append(f'{L2}')
+ parts.append(f'{L3}')
+ parts.append(f'{L2}')
+ if parameters_calc:
+ parts.append(f'{L2}')
+ parts.append(f'{L3}')
+ parts.append(f'{L2}')
+ parts.append(f'{L2}{action_value}')
+ if template_calc:
+ parts.append(f'{L2}')
+ parts.append(f'{L3}')
+ parts.append(f'{L2}')
+ parts.append(f'{L1}')
+ parts.append(f'{S}')
+ return '\n'.join(parts)
+
+
# ---------------------------------------------------------------------------
# Dispatch table
# ---------------------------------------------------------------------------
@@ -1763,6 +1871,7 @@ def tx_unknown(step) -> str:
'Commit Records/Requests': tx_commit,
'Refresh Object': tx_refresh_object,
'Insert Calculated Result': tx_insert_calculated_result,
+ 'Perform Find by Natural Language': tx_perform_find_natural_language,
'Insert Text': tx_insert_text,
'Insert from URL': tx_insert_from_url,
'Open URL': tx_open_url,