Live undo/redo: add/delete component and link, move, property edits

Undo and redo now work against a connected device; each step is sent
there through Sox. Deleting a component first reads its whole subtree
back (config, unloaded children, links, place among siblings) so undo
can recreate it; recreated components take new device ids, which the
DOM elements pick up so later steps still reach them. Binary Buf
config (a user's cred) is read with read_prop, since the cache decodes
every Buf as a Str. Moves, tidy layout, and Properties / page editor /
user edits record only what the device accepted.

A step that does not fully reach the device warns and clears the
history. History is also cleared on every view change in live mode,
as opening a sheet rebuilds the DOM from the device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 20:03:26 +03:00
co-authored by Claude Opus 5.5
parent fd22eb314d
commit b98fe828da
9 changed files with 534 additions and 67 deletions
+10 -3
View File
@@ -1170,7 +1170,9 @@ class PropertiesPanel:
# In live mode the device is written first and a value only lands in the document
# once the device took it, so the panel never shows what the device does not hold.
live_failures = []
for element, prop_name, before, after in changes:
applied = []
for change in changes:
element, prop_name, before, after = change
if app_state.live_mode and not live_hooks.live_set_prop_value(
element, prop_name, after):
live_failures.append(f"{prop_name}: {live_hooks.last_error}")
@@ -1182,9 +1184,14 @@ class PropertiesPanel:
continue
if prop_name != "meta":
set_prop_value(element, prop_name, after)
written = len(changes) - len(live_failures)
applied.append(change)
written = len(applied)
if not app_state.live_mode:
if app_state.live_mode:
# Only what the device took; undo writes it back there.
if applied:
undo.push(undo.LiveSlotValueCommand(applied))
else:
undo.push(undo.SlotValueCommand(list(changes)))
app_state.mark_dirty()