Skip to content

feat: add planning group Viser panel#2563

Open
TomCC7 wants to merge 1 commit into
cc/spec/movegroupfrom
cc/spec/movegroup-viser
Open

feat: add planning group Viser panel#2563
TomCC7 wants to merge 1 commit into
cc/spec/movegroupfrom
cc/spec/movegroup-viser

Conversation

@TomCC7

@TomCC7 TomCC7 commented Jun 22, 2026

Copy link
Copy Markdown
Member

Summary

  • Move the bulky Viser planning-group panel/UI changes out of the core planning-groups PR.
  • Add group-aware Viser panel state, target evaluation, pose/group selectors, target ghosts, and preview animation support.
  • Replace the old in-process Viser adapter path with direct group-aware panel/backend helpers.

Stack

Verification

  • uv run pytest dimos/manipulation/visualization/viser/test_viser_visualization.py dimos/manipulation/visualization/viser/test_visualizer_lifecycle.py dimos/manipulation/visualization/viser/test_operation_worker.py dimos/manipulation/visualization/viser/test_gui_status.py dimos/manipulation/visualization/viser/test_state.py -q
  • uv run --group lint mypy
  • uv run ruff check dimos/manipulation/visualization/viser
  • uv run ruff format --check dimos/manipulation/visualization/viser
  • rtk git diff --check

@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR layers the full Viser planning-group UI on top of the core planning-group API from #2489, replacing the deleted InProcessViserAdapter with a new panel_backend.py of stateless helpers and a substantially rewritten ViserPanelGui that is aware of multi-group selection, per-group pose/joint targets, target ghosts, and preview animations.

  • panel_backend.py extracts IK/FK/collision evaluation and joint-value utilities from the old adapter into clean, testable functions.
  • gui.py and state.py add group-selector buttons, per-group pose transform controls, joint sliders, target ghost synchronization, and a group-native preset dropdown.
  • scene.py gains multi-robot ghost management (per-group ee_control handles, shared-clock animate_preview for multi-track plans) and base-pose frame support.

Confidence Score: 3/5

Safe for single-robot setups; multi-robot plans bypass the joint-staleness gate before execution, which could allow executing a stale plan after a robot has moved.

The new panel_backend.py, scene.py additions, and 73-test suite are well-structured. Two issues in state.py and gui.py affect multi-robot correctness and thread safety respectively.

state.py (can_execute multi-group branch) and gui.py (_set_operation_error / _build_joint_slider_handles) need attention before use in a multi-robot context.

Important Files Changed

Filename Overview
dimos/manipulation/visualization/viser/state.py Adds group-aware panel state. can_execute skips the current-joints staleness check for all multi-group plans, unconditionally returning True after the single-group branch.
dimos/manipulation/visualization/viser/gui.py Large rewrite introducing group-aware panel controls. _set_operation_error unsafely mutates _operation_sequence_id from a background thread; joint-slider limit indexing assumes group and robot joint arrays are co-indexed.
dimos/manipulation/visualization/viser/panel_backend.py New module extracting stateless helpers from the deleted adapter. Logic is clean and well-isolated.
dimos/manipulation/visualization/viser/scene.py Adds multi-robot ghost management. Uses group_id as the key for ee_control handles; callers are consistent with this convention.
dimos/manipulation/visualization/viser/animation.py Adds GroupPreviewAnimation / PreviewTrack dataclasses and sampled_joint_path_frames helper for multi-track preview playback.
dimos/manipulation/visualization/viser/visualizer.py Replaces the InProcessViserAdapter path with direct scene/GUI helpers. No issues found.
dimos/manipulation/visualization/viser/adapter.py Deleted — InProcessViserAdapter removed; responsibilities split between panel_backend.py and gui.py.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant U as User (GUI thread)
    participant GUI as ViserPanelGui
    participant TEW as TargetEvaluationWorker
    participant OW as OperationWorker
    participant BE as panel_backend

    U->>GUI: drag transform control
    GUI->>GUI: _on_transform_update(group_id, handle)
    GUI->>TEW: submit(TargetEvaluationRequest)
    TEW->>BE: evaluate_pose_target_set(pose_targets, seed)
    BE-->>TEW: TargetSetEvaluation
    TEW->>GUI: _apply_target_evaluation_result(request, result)
    GUI->>GUI: update feasibility / target_status / refresh()

    U->>GUI: click Plan
    GUI->>GUI: _submit_plan() / _next_operation_id()
    GUI->>OW: submit(operation)
    OW->>BE: plan_to_joint_targets(targets)
    BE-->>OW: ok
    OW->>GUI: "_finish_operation(plan=True)"
    GUI->>GUI: "plan_state.status = FRESH"

    U->>GUI: click Execute
    GUI->>GUI: can_execute() check
    GUI->>OW: submit(execute_operation)
    OW->>BE: manipulation_module.execute()
    BE-->>OW: ok
    OW->>GUI: "_finish_operation(execute=True)"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant U as User (GUI thread)
    participant GUI as ViserPanelGui
    participant TEW as TargetEvaluationWorker
    participant OW as OperationWorker
    participant BE as panel_backend

    U->>GUI: drag transform control
    GUI->>GUI: _on_transform_update(group_id, handle)
    GUI->>TEW: submit(TargetEvaluationRequest)
    TEW->>BE: evaluate_pose_target_set(pose_targets, seed)
    BE-->>TEW: TargetSetEvaluation
    TEW->>GUI: _apply_target_evaluation_result(request, result)
    GUI->>GUI: update feasibility / target_status / refresh()

    U->>GUI: click Plan
    GUI->>GUI: _submit_plan() / _next_operation_id()
    GUI->>OW: submit(operation)
    OW->>BE: plan_to_joint_targets(targets)
    BE-->>OW: ok
    OW->>GUI: "_finish_operation(plan=True)"
    GUI->>GUI: "plan_state.status = FRESH"

    U->>GUI: click Execute
    GUI->>GUI: can_execute() check
    GUI->>OW: submit(execute_operation)
    OW->>BE: manipulation_module.execute()
    BE-->>OW: ok
    OW->>GUI: "_finish_operation(execute=True)"
Loading

Comments Outside Diff (1)

  1. dimos/manipulation/visualization/viser/gui.py, line 1189-1192 (link)

    P1 Unsynchronized write to _operation_sequence_id from worker thread

    _set_operation_error is invoked as an on_error callback from the OperationWorker background thread (see OperationWorker._run and _run_operation). It performs a non-atomic read-modify-write (+= 1) on _operation_sequence_id without a lock, while the main GUI thread performs the same operation via _next_operation_id(). Under CPython's GIL the interleaving of LOAD_ATTR / INPLACE_ADD / STORE_ATTR across two threads can produce a lost-update: both threads read N, both write N+1, and one increment is silently dropped. The result is that the sequence counter can end up one step behind, making a freshly submitted operation immediately appear "not current" to _operation_is_current, which would suppress its result callbacks and leave the panel stuck in ActionStatus.FAILED until the next user interaction.

Reviews (1): Last reviewed commit: "feat: move planning group viser panel to..." | Re-trigger Greptile

Comment on lines +190 to +200
# Multi-group freshness is checked by group snapshot when available. The
# robot-level current-joint fallback preserves one-group legacy tests.
if len(plan.start_joints_snapshot) == 1:
snapshot = next(iter(plan.start_joints_snapshot.values()))
if len(snapshot) != len(self.current_joints):
return False
return all(
abs(expected - current) <= current_tolerance
for expected, current in zip(snapshot, self.current_joints, strict=False)
)
)
return True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Multi-group execute freshness check is absent

When plan.start_joints_snapshot has more than one entry (any multi-robot plan), the method skips the staleness guard entirely and returns True. A user can plan, wait for a robot to drift outside current_match_tolerance, and still have the Execute button enabled. The comment claims "Multi-group freshness is checked by group snapshot when available," but no such check exists in the else branch — it unconditionally returns True. For a single-group plan the tolerance gate works correctly; only multi-group plans bypass it.

Comment on lines +406 to +425
joint_limits_lower = config.joint_limits_lower if config is not None else None
joint_limits_upper = config.joint_limits_upper if config is not None else None
for index, (global_name, local_name) in enumerate(
zip(group.joint_names, group.local_joint_names, strict=False)
):
joint_name = str(global_name)
local = str(local_name)
value = float(
target_by_name.get(
joint_name,
target_by_name.get(
local, current_by_name.get(joint_name, current_by_name.get(local, 0.0))
),
)
)
lower, upper = DEFAULT_JOINT_LIMITS
if joint_limits_lower is not None and index < len(joint_limits_lower):
lower = joint_limits_lower[index]
if joint_limits_upper is not None and index < len(joint_limits_upper):
upper = joint_limits_upper[index]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Joint-limit index assumes group joints align with robot-config joint array

index here is the ordinal position of a joint within group.joint_names, not within the robot's full joint list. If a planning group covers only a subset of the robot's DOF (e.g., a wrist-only group whose joints are indices 4–6 of a 7-DOF arm), joint_limits_lower[index] will retrieve the limit for joints 0–2 rather than 4–6. The slider limits would then be silently wrong for such groups. Is it guaranteed that config.joint_limits_lower/upper is always indexed identically to group.joint_names? Is config.joint_limits_lower/upper always indexed identically to group.joint_names (i.e. the group always covers a prefix or the full robot joint list in the same order), or can a group reference an arbitrary subset of robot joints?

@codecov

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.30849% with 158 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
dimos/manipulation/visualization/viser/gui.py 82.40% 45 Missing and 34 partials ⚠️
.../manipulation/visualization/viser/panel_backend.py 81.67% 11 Missing and 13 partials ⚠️
...on/visualization/viser/test_viser_visualization.py 94.23% 16 Missing and 5 partials ⚠️
dimos/manipulation/visualization/viser/scene.py 82.05% 7 Missing and 7 partials ⚠️
...mos/manipulation/visualization/viser/visualizer.py 70.00% 3 Missing and 3 partials ⚠️
dimos/manipulation/visualization/viser/state.py 80.00% 2 Missing and 3 partials ⚠️
...ation/visualization/viser/test_operation_worker.py 77.77% 4 Missing ⚠️
...imos/manipulation/visualization/viser/animation.py 80.00% 3 Missing ⚠️
...n/visualization/viser/test_visualizer_lifecycle.py 96.00% 2 Missing ⚠️
@@                  Coverage Diff                  @@
##           cc/spec/movegroup    #2563      +/-   ##
=====================================================
- Coverage              70.93%   69.84%   -1.10%     
=====================================================
  Files                    883      894      +11     
  Lines                  79958    82622    +2664     
  Branches                7242     7566     +324     
=====================================================
+ Hits                   56722    57710     +988     
- Misses                 21361    22951    +1590     
- Partials                1875     1961      +86     
Flag Coverage Δ
OS-ubuntu-24.04-arm 62.67% <4.13%> (-0.20%) ⬇️
OS-ubuntu-latest 65.74% <86.30%> (+0.26%) ⬆️
Py-3.10 65.74% <86.30%> (+0.25%) ⬆️
Py-3.11 65.74% <86.30%> (+0.25%) ⬆️
Py-3.12 65.74% <86.30%> (+0.26%) ⬆️
Py-3.13 65.74% <86.30%> (+0.26%) ⬆️
Py-3.14 65.75% <86.30%> (+0.26%) ⬆️
Py-3.14t 65.74% <86.30%> (+0.26%) ⬆️
SelfHosted-Linux 37.21% <14.38%> (-0.19%) ⬇️
SelfHosted-macOS 36.05% <14.38%> (-0.17%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...anipulation/visualization/viser/test_gui_status.py 100.00% <100.00%> (ø)
...mos/manipulation/visualization/viser/test_state.py 100.00% <100.00%> (ø)
dimos/manipulation/visualization/viser/theme.py 76.59% <100.00%> (ø)
...n/visualization/viser/test_visualizer_lifecycle.py 97.70% <96.00%> (-0.49%) ⬇️
...imos/manipulation/visualization/viser/animation.py 88.33% <80.00%> (-11.67%) ⬇️
...ation/visualization/viser/test_operation_worker.py 95.18% <77.77%> (-3.08%) ⬇️
dimos/manipulation/visualization/viser/state.py 90.47% <80.00%> (-3.06%) ⬇️
...mos/manipulation/visualization/viser/visualizer.py 70.29% <70.00%> (+2.35%) ⬆️
dimos/manipulation/visualization/viser/scene.py 83.16% <82.05%> (-5.83%) ⬇️
...on/visualization/viser/test_viser_visualization.py 96.81% <94.23%> (-1.61%) ⬇️
... and 2 more

... and 48 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-merge Required CI checks have passed on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant