feat(sdk): pj.playback.v1 + pj.viewport.v1 host services - #184
Draft
Alvvalencia wants to merge 5 commits into
Draft
feat(sdk): pj.playback.v1 + pj.viewport.v1 host services#184Alvvalencia wants to merge 5 commits into
Alvvalencia wants to merge 5 commits into
Conversation
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
force-pushed
the
feat/playback-viewport-services
branch
from
September 1, 2026 12:49
661bc9f to
a437921
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-frozenPJ_playback_state_tsnapshot) /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) andzoom_reset(fit all).deserializePlotMarkersnow 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.PJ_ERROR_CODE_REJECTED/PJ_ERROR_CODE_INTERNALnamed in the ABI header — aggregators (e.g. a kind router probing multiple backends) rely on the distinction, and it was previously folklore.pj_enable_whendialog-property docs;playback_viewport_api_testcovering 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
PluginCatalogTest.DependencySearchExcludesCwdAndPath, environmental in this sandbox — main is green in CI with the same code).