Skip to content

refactor: what a double-click on a node opens is a contribution point - #140

Merged
endrix merged 3 commits into
mainfrom
refactor/node-opener-seam
Nov 9, 2026
Merged

endrix merged 3 commits into
mainfrom
refactor/node-opener-seam

Conversation

@endrix

@endrix endrix commented Nov 9, 2026 •

Copy link
Copy Markdown
Owner

P5e, the @viewer step: the next wfpy-only piece of the client, behind a fourth contribution point (after annotation editors, node sections and wizard templates). Nothing changes yet.

Why

The double-click listener (viewer-mouse-listener.ts) did two things:

  • for every product: open a node's definition (preview), unless the network navigation opens something for it;
  • for wfpy only: for a task with a @viewer annotation, open its last tokens instead (open, diff, open with an editor, run a command), or a path the annotation declares.

What

  • NodeOpener (src/node-opener.ts, exported). An opener has an id, and its open(node, { sourceUri }) answers a double-click with the actions to run, or undefined to leave the node to the platform. The listener multi-injects the openers and keeps one per id (the last binding wins, via lastPerKey). It asks them in the order their ids were first bound, before its own definition navigation, and the first that answers wins.
  • The @viewer half moves verbatim into src/viewer-node-opener.ts (ViewerNodeOpener, id viewer), with its helpers and wf:viewer* argument names. Git's moved-code detection marks 33 of its 372 lines as new: the opener's own guard, its context lines, and the imports. It emits the same NavigateToExternalTargetActions, which Dialogram's host still turns into an editor, a diff or a command.
  • The stock module binds it, so nothing changes. Next:
    1. wfpy-ide binds its own copy, as it did for the agent UI;
    2. the stock module stops binding this one.

Review (/code-review medium), addressed

The review confirmed the moved code is equivalent: the same order of checks, the same nodes reaching @viewer (it comes down to the type alone), the same messages and routes, and no change for actor or network nodes. Fixed:

  • An empty answer is no answer. [] used to stop the loop and block the definition.
  • An opener that throws is skipped, with a warning, instead of making the node open nothing.
  • The contract says:
    • an opener without an id is dropped;
    • an opener's answer, unlike the platform's definition, does not stand back for a node whose first view the network navigation opens.
  • One definition branch covers a task, or an actor not marked external. A test pins the marked-external actor.
  • Comments:
    • ABSOLUTE_URI's explanation moved with it.
    • The stock module's line for the listener now describes what it does for every product.

Tests

  • The three @viewer test files (viewer-declared-target, declared-target-resources, double-click-opens-one-thing) inject what the stock module binds and pass unchanged.
  • New test/node-opener-seam.test.ts (9 tests):
    • an opener answers first, with the node and the diagram's source;
    • when it answers nothing, the definition opens;
    • it is asked for actors too, and the first answer wins;
    • a later opener for an id replaces the earlier one in its place;
    • through a real container: the stock viewer opener, a product's after it, a product's viewer replacing it, and none when nothing is bound.
  • Container parity: the baseline accepts exactly the one NodeOpener constant binding.
  • Local results (after merging refactor: the platform no longer discovers agent skills and presets #139):
    • client suite: 419/419
    • typecheck: 5/5
    • neutrality: 5/5

https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

The double-click listener ("viewer listener") did two things: open a
node's definition, which every product uses, and, for a task with a
@viewer annotation, open its last tokens (open, diff, open with an editor,
run a command) or a path it declares, which only wfpy has.

`NodeOpener` (node-opener.ts, exported) is the contribution point: an
opener with an `id` answers a double-click on a node with the actions to
run, or leaves the node to the platform, which opens its definition as
before. The listener multi-injects them (one per id, the last binding
winning, asked in the order the ids were first bound; the first that
answers wins) and asks them before its own definition navigation.

The @viewer half moves verbatim into viewer-node-opener.ts
(ViewerNodeOpener, id `viewer`): git's moved-code detection marks 33 of
its 372 lines as new, the opener's guard and context lines and its
imports. The stock module binds it, so nothing changes; wfpy-ide binds
its own next, and then the stock module stops.

The three @viewer test files inject the opener the stock module binds
and pass unchanged; test/node-opener-seam.test.ts pins the contract and
the bindings, through a real container too. The parity baseline accepts
the one constant binding.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv
…seam

# Conflicts:
#	packages/diagram-client/README.md
- An empty answer is no answer (the contract named only undefined, but
  `[]` stopped the loop and blocked the definition), and an opener that
  throws is skipped with a warning instead of making the node open
  nothing at all.
- The contract says an opener without an id is dropped, and that an
  opener's answer, unlike the platform's definition, does not stand back
  for a node whose first view the network navigation opens.
- The two definition branches are one (a task, or an actor not marked
  external), with a test pinning the marked-external actor.
- Comments: ABSOLUTE_URI's explanation moved with it into the opener, and
  the stock module's line for the listener says what it does for every
  product now.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv
@endrix
endrix merged commit fda06d2 into main Nov 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant