Make every live edit reach the device, and warn when it doesn't

In live mode the device is now written first and the document only takes
what the device accepted. Every live_hooks failure records a reason, and
warn_failure() shows it in a dialog and the status bar instead of failing
silently.

- Move, properties, OX page editor: rejected values are rolled back and listed
- Add/remove link: no longer edits the DOM alone when an end can't be resolved
- Add/delete component: report why the device refused
- Tidy Layout: sends each new position to the device, reverts refusals,
  marks the document dirty, and pushes no undo step in live mode
- User Manager: add (one Sox add carrying cred/perm/prov), edit and delete
  now go to the device
- Binary Buf slots (e.g. User.cred) are sent as Buf bytes, not as Str text
- Bump version to 0.0.0.136

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 20:16:00 +03:00
co-authored by Claude Opus 5.5
parent f15fc42363
commit a2d4be6ea8
8 changed files with 312 additions and 114 deletions
+41 -21
View File
@@ -760,10 +760,13 @@ def reconcile_schema_kits_before_save():
return True
def _build_config_values(full_type):
"""Builds SOX config_values list from manifest defaults for an add command."""
from sox import type_id_for_slot
from live_hooks import _coerce_value
def build_config_values(full_type, overrides=None):
"""Builds SOX config_values list from manifest defaults for an add command.
`overrides` maps slot name to the document-form value to send instead of the default.
Raises ValueError when a value does not fit its slot's type.
"""
from live_hooks import _coerce_value, _wire_type
slots = app_state.final_components_registry.get(full_type, [])
config_values = []
@@ -772,12 +775,12 @@ def _build_config_values(full_type):
continue
if slot["type"] == "void":
continue
val = slot["default"]
val = (overrides or {}).get(slot["name"], slot["default"])
if val is None:
val = properties.type_zero(slot["type"])
if val is None:
continue
tid = type_id_for_slot(slot["type"])
tid = _wire_type(slot["type"], slot["facets"].get("asStr") is True)
config_values.append((tid, _coerce_value(tid, val)))
return config_values
@@ -811,12 +814,18 @@ def add_component(kit_name, type_name, grid_x, grid_y):
comp_name = unique_child_name(parent, type_name)
if live_hooks.is_live():
if app_state.live_mode:
full_type = f"{kit_name}::{type_name}"
config_values = _build_config_values(full_type)
try:
config_values = build_config_values(full_type)
except (ValueError, TypeError) as e:
live_hooks.warn_failure("Add Component", f"{comp_name} was not added to the device",
f"A default value in the kit manifest does not fit its slot: {e}")
return None
new_id = live_hooks.live_add_component(
parent, kit_name, type_name, comp_name, config_values or None)
if new_id is None:
live_hooks.warn_failure("Add Component", f"{comp_name} was not added to the device")
return None
comp_id = new_id
else:
@@ -847,17 +856,19 @@ def add_component(kit_name, type_name, grid_x, grid_y):
index = len(list(parent))
parent.append(element)
if live_hooks.is_live():
if app_state.live_mode:
ds = app_state.device_session
if ds is not None:
ds.update_element_from_cache(comp_id, app_state.xml_root_element)
update_meta_property(element, None, grid_x, grid_y)
if live_hooks.is_live():
position_failed = False
if app_state.live_mode:
meta_prop = element.find("prop[@name='meta']")
if meta_prop is not None:
live_hooks.live_set_prop_value(element, "meta", meta_prop.get("val", "0"))
position_failed = not live_hooks.live_set_prop_value(
element, "meta", meta_prop.get("val", "0"))
if not app_state.live_mode:
undo.push(undo.AddComponentCommand(
@@ -869,7 +880,13 @@ def add_component(kit_name, type_name, grid_x, grid_y):
refresh_treeview_from_dom(app_state.xml_root_element)
wiresheet.render_current_sheet()
if app_state.live_mode:
if position_failed:
# The component exists on the device, so it stays; only where it sits is unsaved.
live_hooks.warn_failure(
"Add Component",
f"{comp_name} was added to the device, but its position on the sheet was not "
"written, so the device still has it at its default position")
elif app_state.live_mode:
app_state.set_status(f"{comp_name} ({kit_name}::{type_name}) added to device.")
elif kit_name in app_state.pending_schema_kits:
app_state.set_status(
@@ -895,14 +912,15 @@ def remove_link(link_element):
from_ref = link_element.get("from", "")
to_ref = link_element.get("to", "")
if live_hooks.is_live():
if app_state.live_mode:
from_comp_path, from_slot_name = from_ref.rsplit(".", 1) if "." in from_ref else ("", from_ref)
to_comp_path, to_slot_name = to_ref.rsplit(".", 1) if "." in to_ref else ("", to_ref)
from_elem = find_component_by_path(app_state.xml_root_element, from_comp_path)
to_elem = find_component_by_path(app_state.xml_root_element, to_comp_path)
if from_elem is not None and to_elem is not None:
if not live_hooks.live_delete_link(from_elem, from_slot_name, to_elem, to_slot_name):
return
if not live_hooks.live_delete_link(from_elem, from_slot_name, to_elem, to_slot_name):
live_hooks.warn_failure("Delete Link",
f"The link {from_ref} → {to_ref} was not removed from the device")
return
try:
index = list(links_elem).index(link_element)
@@ -932,14 +950,15 @@ def add_link(from_path_slot, to_path_slot):
if links_elem is None:
links_elem = ET.SubElement(app_state.xml_root_element, "links")
if live_hooks.is_live():
if app_state.live_mode:
from_comp_path, from_slot_name = from_path_slot.rsplit(".", 1) if "." in from_path_slot else ("", from_path_slot)
to_comp_path, to_slot_name = to_path_slot.rsplit(".", 1) if "." in to_path_slot else ("", to_path_slot)
from_elem = find_component_by_path(app_state.xml_root_element, from_comp_path)
to_elem = find_component_by_path(app_state.xml_root_element, to_comp_path)
if from_elem is not None and to_elem is not None:
if not live_hooks.live_add_link(from_elem, from_slot_name, to_elem, to_slot_name):
return
if not live_hooks.live_add_link(from_elem, from_slot_name, to_elem, to_slot_name):
live_hooks.warn_failure("Add Link",
f"The link {from_path_slot} → {to_path_slot} was not made on the device")
return
link_elem = ET.SubElement(links_elem, "link")
link_elem.set("from", from_path_slot)
@@ -973,8 +992,9 @@ def execute_cascading_deletion(comp_element):
if not confirm:
return False
if live_hooks.is_live():
if app_state.live_mode:
if not live_hooks.live_delete_component(comp_element):
live_hooks.warn_failure("Delete Component", f"{comp_name} was not deleted from the device")
return False
comp_path = get_component_path(comp_element, app_state.xml_root_element)