From 0cc04d3618b1f30b68430b3c5d4827285cd85865 Mon Sep 17 00:00:00 2001 From: Kirk Bowman Date: Mon, 18 May 2026 14:29:09 -0500 Subject: [PATCH] Converter fixes + safer Tier 2 deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fm_xml_to_snippet: handle calculated Go to Layout (LayoutNameByCalc / LayoutNumberByCalc) — previously dropped the calc, producing in Script Workspace. - fm_xml_to_snippet: add Perform Find by Natural Language handler (LLMCreateFind wrapper with AccountName, Model, PromptMessage, Action, PromptTemplateName) — previously emitted only Field/Action and FM showed all params blank. - step-catalog: correct Go to Layout enum values from LayoutNameByCalculation / LayoutNumberByCalculation to FM's actual LayoutNameByCalc / LayoutNumberByCalc. - deploy.py: Tier 2 Phase 2 now polls up to 3s for the target tab and aborts cleanly if it never appears, instead of blind-pressing Cmd+A on whatever has focus. Prevents catastrophic mis-paste when the script list (not the editor tab) is focused. Co-Authored-By: Claude Opus 4.7 --- agent/catalogs/step-catalog-en.json | 4 +- agent/scripts/deploy.py | 41 ++++++++--- agent/scripts/fm_xml_to_snippet.py | 109 ++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 14 deletions(-) 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}') + 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,