Skip to content

feat(sdk): pj.playback.v1 + pj.viewport.v1 host services - #184

Draft
Alvvalencia wants to merge 5 commits into
mainfrom
feat/playback-viewport-services
Draft

feat(sdk): pj.playback.v1 + pj.viewport.v1 host services#184
Alvvalencia wants to merge 5 commits into
mainfrom
feat/playback-viewport-services

Conversation

@Alvvalencia

Copy link
Copy Markdown
Contributor

Upstreams the two host services the Assistant Agent's transport/viewport tools were built on in July, re-applied onto current main. Tail-appended MINOR, additions only — no existing struct or vtable slot changes, so every already-built plugin keeps working without a recompile. Same pattern as the dataset-naming addition in #183.

What's in

  • pj.playback.v1 (PJ_playback_host_vtable_t, sdk::PlaybackHostView / PlaybackHostService): play / pause / seek / set_playback_rate / get_state (ABI-frozen PJ_playback_state_t snapshot) / to_display_time (absolute ns → display-axis seconds via the owning dataset's offset; current-frame semantics). Every time in the service is display-axis seconds — the numbers the plot X axes and the playback slider show.
  • pj.viewport.v1 (PJ_viewport_host_vtable_t, sdk::ViewportHostView / ViewportHostService): zoom_to_time_range (every open time plot's X window, per-plot Y preserved) and zoom_reset (fit all).
  • Codec fix: deserializePlotMarkers now decodes an empty buffer as an empty set (proto semantics: empty message = all defaults) instead of erroring — the "clear my markers" tombstone a producer publishes on a replace-only store. A null pointer with nonzero size is still an error.
  • Error-code convention: PJ_ERROR_CODE_REJECTED / PJ_ERROR_CODE_INTERNAL named in the ABI header — aggregators (e.g. a kind router probing multiple backends) rely on the distinction, and it was previously folklore.
  • pj_enable_when dialog-property docs; playback_viewport_api_test covering the ABI structs, view wrappers and traits.

Versioning

Claims 0.27.0 assuming #183 (0.26.0) merges first; renumber at release if they land in the other order.

Verification

  • Full SDK build + 83/84 tests locally (the one red is PluginCatalogTest.DependencySearchExcludesCwdAndPath, environmental in this sandbox — main is green in CI with the same code).
  • The services and their seven consumer tools were E2E-verified in the application in July (play/pause/seek/rate clamped and reported, zoom/framing pixel-verified); the host-side PJ4 wiring PR follows.

Two new optional host services so a plugin (first consumer: the Assistant
Agent toolbox) can drive the app like a user — transport and zoom:

- pj.playback.v1 (PJ_playback_host_vtable_t, sdk::PlaybackHostView):
  play/pause/seek/set_playback_rate/get_state (ABI-frozen
  PJ_playback_state_t) + to_display_time (absolute int64 ns -> display-axis
  seconds via the topic's dataset offset; current-frame semantics).
- pj.viewport.v1 (PJ_viewport_host_vtable_t, sdk::ViewportHostView):
  zoom_to_time_range (every open time plot's X window, per-plot Y kept) and
  zoom_reset (fit all).

All times are display-axis seconds — the numbers the plot X axes and the
playback slider show. Additive only: no existing struct or slot touched
(abidiff: additions only). Version provisional at 0.16.0 (0.15.0 taken by
the in-flight per-topic subscription work).
… playback state layout

An empty buffer IS the canonical proto encoding of an empty set — the
"clear my markers" tombstone a producer publishes to a replace-only
store — but deserializePlotMarkers rejected size==0 as an error, making
the tombstone unrepresentable on the wire. Now it round-trips; null-with-
size, truncated, and malformed payloads still error. (Logically part of
the plot-markers feature line; riding this branch until PRs are sorted.)

Also pin PJ_playback_state_t (ABI-FROZEN, bool + padding + 4 doubles)
in the layout-sentinels test.
- PJ_ERROR_CODE_REJECTED / PJ_ERROR_CODE_INTERNAL: the two error classes
  every PJ host service distinguishes, now with an authoritative home —
  aggregators (a kind router probing multiple backends) rely on REJECTED
  meaning "not mine, safe to try another backend" vs INTERNAL meaning "a
  real failure that must never be masked by a fallback".
- Document the host-honored "pj_enable_when" dynamic property (combo-
  driven field enabling, incl. inside modal sub-dialogs) in the dialog
  plugin guide + SDK reference.
Tail-appended MINOR, additions only. Numbered assuming #183 (0.26.0) merges
first; renumber at release if the two land in the other order.
@Alvvalencia
Alvvalencia force-pushed the feat/playback-viewport-services branch from 661bc9f to a437921 Compare September 1, 2026 12:49
A plugin can create a plotting tab, place and remove curves in it, read back
what it holds, and close it. The point is a boundary: a tab a plugin made is
its own to compose, and the user's tabs stay the user's.

Scoping is the HOST's duty, enforced where ownership is actually known. A tab
this plugin did not create is rejected exactly as an unknown id is, so the
service never discloses, mutates, or even confirms the existence of anyone
else's tab; ownership comes from the per-binding ctx and never crosses the
wire, the same unspoofable-identity discipline pj.data_processors.v1 already
uses for its per-plugin isolation.

Three choices worth their reasoning:

- Ids are plugin-chosen and host-namespaced, so no handle has to travel back
  as an out-string. The resemblance to pj.data_processors.v1 stops at
  namespacing, and the doc says so: a processor id survives a reload because
  the host replays it from a recipe, whereas a tab is a view and this service
  promises nothing of the kind.
- Curves are addressed by their parts — topic, field, dataset source — not by
  a joined path. Field paths legitimately contain '/' and dataset names contain
  ':', so splitting a joined form would be guesswork the host should not have
  to do. An empty dataset source means the pair must be unique, and an
  ambiguous one is refused with the candidates rather than picked.
- tab_config reads back what a tab actually holds, so a caller reports what was
  drawn instead of what it asked for — the same reason create_markers reads its
  own output back out of the store.

pj.viewport.v1 keeps both signatures and narrows only its documented scope, to
the tabs the calling plugin owns; it has never shipped, so nothing is broken.
That does couple the two services in practice even though the registry treats
them as independently optional, and the doc now says they are registered
together or not at all. The boundary is the view: pj.playback.v1 stays global,
because one time cursor is shared by every plot.

Additions only — no existing struct or vtable slot was touched. 85 tests
(10 new); the one red is the environmental plugin_catalog_test.
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