Skip to content

feat(canvas): broadcast UI commands to all connected nodes#31828

Closed
carbaj03 wants to merge 6 commits intoopenclaw:mainfrom
carbaj03:feat/canvas-broadcast-all-nodes
Closed

feat(canvas): broadcast UI commands to all connected nodes#31828
carbaj03 wants to merge 6 commits intoopenclaw:mainfrom
carbaj03:feat/canvas-broadcast-all-nodes

Conversation

@carbaj03
Copy link
Copy Markdown

@carbaj03 carbaj03 commented Mar 2, 2026

Summary

  • Canvas UI commands (a2ui_push, a2ui_reset, present, hide, navigate) are now broadcast to all connected canvas-capable nodes instead of routing to a single node
  • Device-specific commands (eval, snapshot) remain single-node since they return a result
  • Uses Promise.allSettled with "at least one success" semantics — if one device is unreachable, others still get the update
  • Adds resolveCanvasNodeIds() helper that returns all connected canvas-capable node IDs

Motivation

Users with multiple devices (desktop + phone, or multiple browsers) only see canvas/a2ui updates on one device. The canvas tool resolved to a single node via resolveNodeId() with preferLocalMac: true, so the Mac desktop always won. The other device never received UI updates.

Canvas state is inherently device-independent — it's UI data, not system commands. Broadcasting it matches how chat events already work (all clients see them).

Test plan

  • Single node connected: behavior unchanged, commands go to that node
  • Two nodes connected (e.g. desktop + mobile): both receive a2ui_push updates
  • Explicit node parameter still targets a single node
  • snapshot and eval still go to single node
  • One node offline during broadcast: other node still gets the update, no error

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Mar 2, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 35f88071b4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/tools/nodes-utils.ts Outdated
return [resolveNodeIdFromList(nodes, q)];
}
const canvasConnected = nodes.filter(
(n) => n.connected && Array.isArray(n.caps) && n.caps.includes("canvas"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep broadcast node selection compatible with pair-list fallback

The new broadcast selector now requires connected === true and caps to include "canvas", which makes resolveCanvasNodeIds() return no targets for gateways that fall back to node.pair.list (those records do not include connected/caps, as shown by the existing listNodes fallback behavior). In that environment, present/hide/navigate/a2ui_* without an explicit node now fail with "no connected canvas-capable nodes", whereas the previous resolveNodeId(..., allowDefault=true) path still worked by choosing a default paired node.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. resolveCanvasNodeIds now applies the same graceful fallback as selectDefaultNodeFromList: when caps is absent (pair-list fallback), assume the node may support canvas; when connected is absent, fall back to all candidates instead of filtering to zero. Added a test covering the pair-list fallback path.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 2, 2026

Greptile Summary

This PR upgrades the canvas tool to broadcast UI state commands (present, hide, navigate, a2ui_push, a2ui_reset) to all connected canvas-capable nodes instead of routing to a single (Mac-preferred) node, while keeping device-specific commands (eval, snapshot) on single-node resolution.

Key changes:

  • resolveCanvasNodeIds() added to nodes-utils.ts — filters to canvas-capable connected nodes; falls back gracefully for pair-list gateways that don't populate caps/connected
  • broadcast() helper in canvas-tool.ts calls resolveCanvasNodeIds and fans out via Promise.allSettled; silently drops partial failures per design, surfaces error only when all nodes reject
  • eval and snapshot retain single-node resolution via resolveNodeId(..., allowDefault=true) since they return a result
  • Test coverage added for the main resolveCanvasNodeIds scenarios including the pair-list fallback path

The broadcast semantics are sound, the resolveCanvasNodeIds fallback logic correctly handles both node.list and legacy pair.list gateways, and eval/snapshot retain proper single-node resolution. Test coverage is thorough. Safe to merge.

Confidence Score: 5/5

  • PR is safe to merge — logic is correct, fallback handling is robust, and test coverage is thorough.
  • The broadcast implementation correctly fans out to all canvas-capable nodes using Promise.allSettled semantics. The resolveCanvasNodeIds() helper properly handles both modern node.list and legacy pair-list gateway versions with appropriate fallback logic. Device-specific commands retain single-node resolution. Tests cover key scenarios including the pair-list fallback path. No functional issues identified.
  • No files require special attention

Last reviewed commit: 41e0347

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment thread src/agents/tools/nodes-utils.ts Outdated
Comment on lines +181 to +187
const canvasConnected = nodes.filter(
(n) => n.connected && Array.isArray(n.caps) && n.caps.includes("canvas"),
);
if (canvasConnected.length === 0) {
throw new Error("no connected canvas-capable nodes");
}
return canvasConnected.map((n) => n.nodeId);
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.

Fallback nodes excluded from broadcast — breaks older gateway versions

When node.list is unavailable, loadNodes falls back to node.pair.list and maps each result to a NodeListNode without connected or caps fields (both undefined). The filter below requires both to be truthy, so every node from the fallback path is excluded, causing this function to always throw "no connected canvas-capable nodes" for users with older gateway versions.

Compare with selectDefaultNodeFromList, which explicitly handles this with:

  • Array.isArray(n.caps) ? n.caps.includes(capability) : true — treats nodes without caps as capable of everything
  • Falls back to withCapability (all nodes) when no node has connected === true

The new filter should apply the same permissiveness to stay consistent with the existing resolution logic:

  const canvasConnected = nodes.filter(
    (n) =>
      (n.connected === true || n.connected === undefined) &&
      (!Array.isArray(n.caps) || n.caps.includes("canvas")),
  );

This preserves the "treat unknown-caps nodes as canvas-capable" grace for legacy gateway clients while still preferring explicitly-connected canvas nodes on modern ones.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/tools/nodes-utils.ts
Line: 181-187

Comment:
**Fallback nodes excluded from broadcast — breaks older gateway versions**

When `node.list` is unavailable, `loadNodes` falls back to `node.pair.list` and maps each result to a `NodeListNode` without `connected` or `caps` fields (both `undefined`). The filter below requires both to be truthy, so every node from the fallback path is excluded, causing this function to always throw `"no connected canvas-capable nodes"` for users with older gateway versions.

Compare with `selectDefaultNodeFromList`, which explicitly handles this with:
- `Array.isArray(n.caps) ? n.caps.includes(capability) : true` — treats nodes without `caps` as capable of everything
- Falls back to `withCapability` (all nodes) when no node has `connected === true`

The new filter should apply the same permissiveness to stay consistent with the existing resolution logic:

```ts
  const canvasConnected = nodes.filter(
    (n) =>
      (n.connected === true || n.connected === undefined) &&
      (!Array.isArray(n.caps) || n.caps.includes("canvas")),
  );
```

This preserves the "treat unknown-caps nodes as canvas-capable" grace for legacy gateway clients while still preferring explicitly-connected canvas nodes on modern ones.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. Same approach — resolveCanvasNodeIds now mirrors the fallback logic in selectDefaultNodeFromList: nodes without caps pass through, and nodes without connected are included as candidates when no connected nodes exist. Test added for the pair-list path.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47028e2fa8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/tools/nodes-utils.ts Outdated
Comment on lines +188 to +189
const connected = withCanvas.filter((n) => n.connected);
const candidates = connected.length > 0 ? connected : withCanvas;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Exclude disconnected nodes from canvas broadcast candidates

resolveCanvasNodeIds falls back to withCanvas whenever connected.length === 0, which treats “connected missing” and “all nodes explicitly disconnected” as the same case. In normal node.list responses, connected is an explicit boolean (src/gateway/server-methods/nodes.ts sets connected: Boolean(live)), so when every canvas node is offline this function still returns their IDs instead of throwing no connected canvas-capable nodes. As a result, broadcast actions will invoke offline nodes and wait on avoidable timeouts before failing, contrary to the helper’s contract.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. The fallback now checks whether any node has a connected field (typeof n.connected === "boolean"). When the field exists (from node.list), only connected: true nodes are included — explicitly disconnected nodes are excluded. The fallback to all candidates only triggers when the field is entirely absent (pair-list path). Added a test for the all-explicitly-disconnected case.

@carbaj03
Copy link
Copy Markdown
Author

carbaj03 commented Mar 6, 2026

Still active. All bot review concerns addressed (P1 pair-list fallback + P2 explicit disconnect handling — see inline replies). CI failures are infra-only (GitHub API rate limit on label job, unrelated macOS test suite failures in Slack routing + ACP persistent-bindings). None related to this PR's changes — happy to rebase or retrigger if needed.

This PR unblocks canvas broadcast for multi-node setups (needed for Tenazitas). Would appreciate a review when possible.

@carbaj03
Copy link
Copy Markdown
Author

carbaj03 commented Mar 7, 2026

👋 Friendly ping — CI is mostly green (the macOS and labeler failures look like infra flakes). This one is ready for review.

We're preparing to launch our app and this feature is part of the release. Would really appreciate a review when you get a chance. Thanks!

@carbaj03 carbaj03 closed this Mar 7, 2026
@carbaj03 carbaj03 reopened this Mar 7, 2026
@carbaj03
Copy link
Copy Markdown
Author

carbaj03 commented Mar 9, 2026

Hi! Just rebased and retriggered CI — previous failures were infra-only (detect-secrets full-scan false positives and macOS runner hang, both passing on main).

Quick summary of what this enables: users with multiple devices (e.g. desktop + phone) only see canvas/a2ui updates on one device today. This PR broadcasts UI-state commands to all connected canvas-capable nodes, while keeping device-specific commands (eval, snapshot) single-node.

3 files changed, 6 tests added. Happy to address any feedback.

@carbaj03
Copy link
Copy Markdown
Author

carbaj03 commented Mar 9, 2026

@vincentkoc would you have a chance to take a look when you get a moment?

@carbaj03
Copy link
Copy Markdown
Author

@joshavant @altaywtf — friendly ping, this PR is ready for review (CI failures are infra-only — macOS runner hang and unrelated Windows flakes). We're preparing for our app launch and this feature is in the critical path. Would appreciate a look when you get a chance!

@carbaj03 carbaj03 force-pushed the feat/canvas-broadcast-all-nodes branch from 9af3c51 to ac2f2ef Compare March 13, 2026 05:31
@carbaj03
Copy link
Copy Markdown
Author

@gumadeiras @ngutman — this is a small feature (135 lines, 3 files) that broadcasts canvas UI commands (a2ui_push, a2ui_reset, present, hide, navigate) to all connected canvas-capable nodes instead of routing to a single one. Device-specific commands (eval, snapshot) remain single-node. Uses Promise.allSettled with at-least-one-success semantics.

CI is green, tests included. Would really appreciate a review!

@carbaj03
Copy link
Copy Markdown
Author

Hi @vincentkoc and @steipete — gentle ping on this one. This PR broadcasts canvas/a2ui commands to all connected nodes (multi-device support). All CI checks pass, Codex review is positive. Would love a human reviewer to take a look when you have a moment. Thanks! 🙏

@carbaj03
Copy link
Copy Markdown
Author

Bumping this for visibility — this PR and #29515 (plugin hook preservation across gateway restart) are a pair, both green with 23/23 CI checks passing.

supersantux expressed interest on Discord today and mentioned he'd follow up. Both are small and non-breaking — would really appreciate a maintainer pass when you get a chance. 🙏

carbaj03 and others added 6 commits March 16, 2026 17:35
…odes

Canvas commands like a2ui_push, a2ui_reset, present, hide, and navigate
are UI updates that should reach every connected device. Previously they
were routed to a single node (preferring local Mac), so users with
multiple devices (e.g. desktop + phone) would only see updates on one.

Now the canvas tool fans out these commands to all connected nodes with
the canvas capability via Promise.allSettled, succeeding if at least one
node receives the update. Device-specific commands (eval, snapshot) remain
single-node since they return a result from one device.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@carbaj03 carbaj03 force-pushed the feat/canvas-broadcast-all-nodes branch from 4c3e4b8 to 4428b4d Compare March 16, 2026 16:35
@carbaj03
Copy link
Copy Markdown
Author

carbaj03 commented Apr 1, 2026

Closing — stale with merge conflicts and no reviews. Will re-submit on a fresh branch if still needed.

@carbaj03 carbaj03 closed this Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant