git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commit3083349a4ab9d26743a8ac4638a9f33cc82f9e91
parenta372953278
authorLucas Galante <[email protected]>
date2026-08-24 15:33
fix: give the remaining MCP actions the syncs the interactive tail provides

Follow-up to a372953 (MCP select vs spreadsheet): every interactive event
ends in process_window_event's changed-tail, which resyncs sync_nodes and
the parameter pane; MCP actions bypass it, so each arm must carry its own
syncs. Audit of all 21 actions found six more gaps:

- up/enter: on_path_changed clears the selection but left the previous
  node's params displayed — sync_parameters_pane after the path change.
- set_param: a selected node's param pane kept the old value (the
  interactive edit originates in the pane itself) — push the value back.
- delete_node: deleting the selected node left its params displayed.
- add_param/delete_param: param pane stale when the slot is selected, and
  no scene rebuild though params drive kernel evaluation (set_param's own
  rationale).
- rename_node: connections reference nodes by name, so a rename changes
  downstream evaluation — rebuild the scene.
- menu_action: execute_menu_action's pane-show arms don't sync_nodes (the
  interactive menu dispatch has its own copies that do), so a spreadsheet
  shown via MCP kept stale contents — sync_nodes after any successful
  label; the name/params cache makes it a no-op when nothing changed.

Clean by inspection: select (fixed prior), reset_camera, load, save,
toggle_geometry, add_node, move_node, set_pane_collapsed/detached,
toggle_circular_pane, menu_click (full process_window_event ritual),
menu_closed, set_frame (tick_frame polls sim_frame every frame).

Verified live in a shadow session: select-then-show populates the
spreadsheet, set_param moves the Radius slider/value and re-evaluates
viewport+spreadsheet, enter clears the params of the exited dir's node.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/window.rs | 25 +++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/src/window.rs b/src/window.rs
index 81913d2..e89c93c 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -664,6 +664,9 @@ impl State {
         let res = match action {
             McpAction::Up => {
                 if state.move_up() {
+                    // on_path_changed clears the selection but leaves the param
+                    // pane to process_window_event's tail, which MCP bypasses.
+                    state.sync_parameters_pane();
                     needs_redraw = true;
                     Ok("Moved up".to_string())
                 } else {
@@ -675,6 +678,7 @@ impl State {
                 if slot < dir.children.len() && (dir.children[slot].node_type == "node" || dir.children[slot].node_type == "utility" || !dir.children[slot].children.is_empty()) {
                     state.current_path.push(slot);
                     state.on_path_changed();
+                    state.sync_parameters_pane();
                     needs_redraw = true;
                     Ok("Entered subnet".to_string())
                 } else {
@@ -708,6 +712,10 @@ impl State {
                         state.sync_grid_settings();
                         state.sync_nodes();
                         state.rebuild_scene_geometry();
+                        // Interactively the edit originates IN the param pane;
+                        // here it must be pushed back or a selected node's pane
+                        // keeps showing the old value.
+                        state.sync_parameters_pane();
                         needs_redraw = true;
                         Ok("Parameter updated".to_string())
                     } else {
@@ -809,6 +817,9 @@ impl State {
                     return Err("The Session node is permanent and cannot be deleted".to_string());
                 }
                 if state.delete_node(slot) {
+                    // delete_node clears/shifts the selection; the param pane
+                    // resync normally comes from process_window_event's tail.
+                    state.sync_parameters_pane();
                     needs_redraw = true;
                     Ok("Node deleted".to_string())
                 } else {
@@ -820,6 +831,9 @@ impl State {
                 if slot < len {
                     state.current_dir_mut().children[slot].name = new_name;
                     state.sync_nodes();
+                    // Connections reference nodes by name (Input params), so a
+                    // rename changes downstream evaluation.
+                    state.rebuild_scene_geometry();
                     needs_redraw = true;
                     Ok("Node renamed".to_string())
                 } else {
@@ -856,6 +870,10 @@ impl State {
                     };
                     state.current_dir_mut().children[slot].params.push(param);
                     state.sync_nodes();
+                    // Params feed kernel evaluation and the param pane shows
+                    // the selected node's list — same rationale as SetParam.
+                    state.rebuild_scene_geometry();
+                    state.sync_parameters_pane();
                     needs_redraw = true;
                     Ok("Parameter added".to_string())
                 } else {
@@ -869,6 +887,8 @@ impl State {
                     if let Some(pos) = params.iter().position(|p| p.name == name) {
                         params.remove(pos);
                         state.sync_nodes();
+                        state.rebuild_scene_geometry();
+                        state.sync_parameters_pane();
                         needs_redraw = true;
                         Ok("Parameter deleted".to_string())
                     } else {
@@ -960,6 +980,11 @@ impl State {
                 if state.execute_menu_action(&label) {
                     // The arms relayout themselves but render() draws the last
                     // uploaded buffer (same ritual as ToggleCircularPane).
+                    // The interactive menu dispatch follows its pane-show arms
+                    // with sync_nodes; execute_menu_action's copies don't, so a
+                    // spreadsheet shown here would keep stale contents without
+                    // this (the cache makes it a no-op when nothing changed).
+                    state.sync_nodes();
                     needs_redraw = true;
                     Ok(format!("Menu action executed: {}", label.replace(['"', '\\'], "'")))
                 } else {