@vestra-player/sdk 1.5.48 → 1.5.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.49
4
+
5
+ **Who must change: nobody.** No changes to the plugin API (`@vestra-player/sdk/runtime` gains the additive `RuntimeOptions` and an optional sixth `createPlugin` argument), `apiVersion` stays `vestra/1`, no new capability, no player version needed, and the player's runner needs no change. **Behaviour change, approved by the maintainer (2026-11-01):** in the player, a command declaration it refuses during activation no longer fails the whole plugin. What `commands.add` checks itself (an unknown `@` menu id, more than 8 path segments, an `order` out of range, a malformed or repeated binding, a bad `button`, a reason text of the wrong shape) still throws from `add`. A plugin whose `activate` relied on that failure (none is known) now runs without the refused command.
6
+
7
+ - **A refused command declaration is skipped, not fatal.** When the player answers a `commands.add` made during activation with `invalid`, `conflict` or `limit` (a text over its limit, a path another command has, too many commands), the SDK logs it with the command id and the player's message (`[<plugin>] command <id> skipped, the player refused it (conflict): …`), skips that command and lets the rest of the plugin activate: its other commands, panels, settings pages and resolvers register and the player gets `runtime.ready`. Before, the first refusal made activation fail (`runtime.failed`) and the plugin's menus and panels were withdrawn. The command's `ready` still rejects, and the error reaches the scope's `onError` once activation completes, so a listener added anywhere in `activate` hears it. This is how a slot, sequence or resolver another plugin holds was already treated, and how VS Code treats a bad contribution (logged, the extension activates). Other failures during activation (`capability`, a timeout, an error of the player) still fail it, and a refusal after activation goes to `onError` as before.
8
+ - **A refused command sends no updates.** After a definite refusal of its registration, later state changes (`set`, a binding) send nothing, instead of updates the player answers with `command missing`; dispose it to declare the id again.
9
+ - **`TestHost` refuses a binding listed twice as player 0.6.1 does.** The player now checks a command's bindings with its binding table (`Table::canonical`) inside the registration: each binding parsed and each input once, compared as parsed inputs, before the reason, the id and path conflicts and the limits, with the `invalid command: ` prefix (`invalid command: binding F3 is listed twice`). `TestHost` answered it without the prefix and only after the conflict and limit checks; it now gives the same answer in the same order.
10
+ - **`TestHost` stays strict.** `startPlugin` still rejects when a command declaration is refused during activation, so a plugin's tests show the bad declaration. Fix the declaration, not the test. (`createPlugin` in `@vestra-player/sdk/runtime` takes `{ strictDeclarations }`; `startPlugin` sets it, the player's runner does not.)
11
+ - _Docs_: [Example: Panel menu](docs/examples/panel-menu.md) draws its list in a `ListView` instead of a `for` in a `VerticalLayout`; the [interface guidelines](docs/guidelines/interface.md#panels) gain **Long lists use `ListView`**. `Command.ready`, `Commands`, `Plugin`, `startPlugin`, [The plugin model](docs/explanation/plugins.md), [Host and Node](docs/explanation/host-and-node.md) and [Limits](docs/reference/limits.md) describe the new activation rule.
12
+
3
13
  ## 1.5.48
4
14
 
5
15
  **Who must change: nobody, but plugin tests may now fail.** No changes to the TypeScript API beyond new fields of `COMMAND_LIMITS`, `apiVersion` stays `vestra/1`, no new capability, no player version needed. `TestHost` now refuses the command declarations the player has always refused: a plugin test that starts failing shows a menu item, key or reason the real player already rejected (the command never appeared, or the plugin failed to activate). Fix the declaration, not the test.
@@ -8,7 +8,7 @@
8
8
  * @packageDocumentation
9
9
  */
10
10
  export { createPlugin } from "./plugin/index.ts";
11
- export type { PluginInstance, PluginManifest } from "./plugin/index.ts";
11
+ export type { PluginInstance, PluginManifest, RuntimeOptions, } from "./plugin/index.ts";
12
12
  export { HostError } from "./wire.ts";
13
13
  export type { Json, Transport } from "./wire.ts";
14
14
  export { API_VERSION, CAPABILITIES, CAPABILITY_INFO, MESSAGE_LIMIT, } from "./capabilities.ts";
@@ -1,5 +1,5 @@
1
1
  /** Menu commands: declaration, state sync (revisions hidden) and dispatch. */
2
- import { failure, Latest, quiet } from "./runtime.js";
2
+ import { failure, Latest, quiet, uncertain } from "./runtime.js";
3
3
  import { checkBindings } from "./bindings.js";
4
4
  import { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, MENU_IDS } from "../commands.js";
5
5
  import { ImageHandle } from "./ui.js";
@@ -100,6 +100,10 @@ class CommandHandle {
100
100
  #last = "";
101
101
  #disposed = false;
102
102
  #registered = false;
103
+ /** Its entry in `runtime.commands` (a later handle may own the id). */
104
+ #run = () => { };
105
+ /** The player refused the declaration: no state updates follow. */
106
+ #refused = false;
103
107
  #pump;
104
108
  #label;
105
109
  /** Where the plugin added it, for the player's log. */
@@ -124,10 +128,20 @@ class CommandHandle {
124
128
  // registration itself is revision 1, so every later state follows it.
125
129
  const previous = removing(runtime).get(this.id);
126
130
  this.#pump = new Latest(async ({ state, when }) => {
131
+ if (this.#refused)
132
+ return;
127
133
  const revision = ++this.#revision;
128
134
  if (revision === 1) {
129
135
  await previous;
130
- await runtime.host.commands.register(this.id, 1, declaration, state, when);
136
+ try {
137
+ await runtime.host.commands.register(this.id, 1, declaration, state, when);
138
+ }
139
+ catch (error) {
140
+ // A definite refusal: the player has no such command to update.
141
+ if (!uncertain(error))
142
+ this.#refused = true;
143
+ throw error;
144
+ }
131
145
  this.#registered = true;
132
146
  }
133
147
  else
@@ -136,12 +150,13 @@ class CommandHandle {
136
150
  const initial = this.#state();
137
151
  this.#last = JSON.stringify(initial);
138
152
  this.ready = quiet(runtime
139
- .starting(this.#pump.push(initial), (error) => scope.report(error))
153
+ .declaring(this.#pump.push(initial), `command ${this.id}`, (error) => scope.report(error))
140
154
  .then(() => { }));
141
- runtime.commands.set(this.id, () => {
155
+ this.#run = () => {
142
156
  if (!this.#disposed)
143
157
  scope.run(options.run);
144
- });
158
+ };
159
+ runtime.commands.set(this.id, this.#run);
145
160
  this.#binding = runtime.bind(() => this.#sync());
146
161
  scope.add(this);
147
162
  }
@@ -177,7 +192,7 @@ class CommandHandle {
177
192
  return Object.keys(when).length ? { state, when } : { state };
178
193
  }
179
194
  #sync() {
180
- if (this.#disposed)
195
+ if (this.#disposed || this.#refused)
181
196
  return;
182
197
  const state = this.#state();
183
198
  const encoded = JSON.stringify(state);
@@ -196,7 +211,8 @@ class CommandHandle {
196
211
  this.#disposed = true;
197
212
  this.#scope.forget(this);
198
213
  const runtime = this.#scope.runtime;
199
- runtime.commands.delete(this.id);
214
+ if (runtime.commands.get(this.id) === this.#run)
215
+ runtime.commands.delete(this.id);
200
216
  this.#pump.stop();
201
217
  // Removed from the menu once registered. Recorded before any await, so an
202
218
  // immediate re-add of the same id registers after this removal.
@@ -6,16 +6,23 @@
6
6
  * @internal
7
7
  */
8
8
  import type { Transport } from "../wire.ts";
9
- import type { PluginManifest } from "./runtime.ts";
9
+ import type { PluginManifest, RuntimeOptions } from "./runtime.ts";
10
10
  import type { Plugin } from "./types.ts";
11
11
  export type * from "./types.ts";
12
- export type { PluginManifest };
12
+ export type { PluginManifest, RuntimeOptions };
13
13
  export interface PluginInstance {
14
14
  plugin: Plugin;
15
- /** Wait for registrations started during activation; rejects if one failed. */
15
+ /**
16
+ * Wait for registrations started during activation; rejects if one failed.
17
+ * A command declaration the player refused is not a failure (it is
18
+ * logged, skipped and reported to `onError`) unless
19
+ * `RuntimeOptions.strictDeclarations` is set.
20
+ */
16
21
  activated(): Promise<void>;
17
22
  dispose(): Promise<void>;
18
23
  }
19
24
  export declare function createPlugin(transport: Transport, manifest: PluginManifest, directory: string, dataDirectory?: string,
20
25
  /** `plugin.app` (the runner passes the player's `VESTRA_APP_INFO`). */
21
- app?: unknown): Promise<PluginInstance>;
26
+ app?: unknown,
27
+ /** `TestHost` sets `strictDeclarations`; the player's runner does not. */
28
+ options?: RuntimeOptions): Promise<PluginInstance>;
@@ -5,8 +5,10 @@ import { windowOf } from "./window.js";
5
5
  import { timelineOf } from "./timeline.js";
6
6
  export async function createPlugin(transport, manifest, directory, dataDirectory,
7
7
  /** `plugin.app` (the runner passes the player's `VESTRA_APP_INFO`). */
8
- app) {
9
- const runtime = new Runtime(transport, manifest, directory, dataDirectory, app);
8
+ app,
9
+ /** `TestHost` sets `strictDeclarations`; the player's runner does not. */
10
+ options) {
11
+ const runtime = new Runtime(transport, manifest, directory, dataDirectory, app, options);
10
12
  // Player, window and timeline state are synchronous for plugins: read them first.
11
13
  await Promise.all([
12
14
  playerOf(runtime).init(),
@@ -37,6 +37,15 @@ export type Change = ReadonlyMap<string, ReadonlySet<string>>;
37
37
  * without `update()`) still shows within about this long.
38
38
  */
39
39
  export declare const SAFETY_PASS_MS = 1000;
40
+ /** How the plugin is run: the player's runner, or `TestHost`. */
41
+ export interface RuntimeOptions {
42
+ /**
43
+ * A declaration the player refuses during activation fails activation
44
+ * (`startPlugin`, so a plugin's tests show it) instead of being logged,
45
+ * reported to `onError` and skipped (the player's runner).
46
+ */
47
+ strictDeclarations?: boolean;
48
+ }
40
49
  export declare const same: (a: Json | undefined, b: Json | undefined) => boolean;
41
50
  /** One value in flight, only the newest waiting. Each push learns its fate. */
42
51
  export declare class Latest<T> {
@@ -74,7 +83,7 @@ export declare class Runtime {
74
83
  * property writes). Not part of the plugin API; empty for older players.
75
84
  */
76
85
  readonly features: ReadonlySet<string>;
77
- constructor(transport: Transport, manifest: PluginManifest, directory: string, dataDirectory?: string, app?: unknown);
86
+ constructor(transport: Transport, manifest: PluginManifest, directory: string, dataDirectory?: string, app?: unknown, options?: RuntimeOptions);
78
87
  can(capability: string): boolean;
79
88
  /** Lazily created singletons shared by every scope of this plugin. */
80
89
  service<T>(name: string, create: () => T): T;
@@ -83,6 +92,16 @@ export declare class Runtime {
83
92
  * plugin reports ready; later ones report failures to their scope.
84
93
  */
85
94
  starting<T>(work: Promise<T>, report: (error: unknown) => void): Promise<T>;
95
+ /**
96
+ * A declaration the player checks on its own (`what`: "command x"),
97
+ * started like `starting`. In the player's runner a refusal of it during
98
+ * activation (`invalid`, `conflict`, `limit`) does not fail the plugin:
99
+ * it is logged, skipped and reported to the scope once activation
100
+ * completes, so an `onError` added anywhere in `activate` hears it (SDK
101
+ * 1.5.49; VS Code logs a bad contribution and activates the extension).
102
+ * Under `strictDeclarations` (TestHost) it fails activation as before.
103
+ */
104
+ declaring<T>(work: Promise<T>, what: string, report: (error: unknown) => void): Promise<T>;
86
105
  activated(): Promise<void>;
87
106
  bind(evaluate: () => void): Disposable;
88
107
  /**
@@ -86,6 +86,16 @@ function merge(into, from) {
86
86
  * without `update()`) still shows within about this long.
87
87
  */
88
88
  export const SAFETY_PASS_MS = 1000;
89
+ /**
90
+ * The player's answers to a declaration that refuse that one declaration
91
+ * (a command's path, texts or count): during activation they skip it
92
+ * instead of failing the plugin (`Runtime.declaring`, SDK 1.5.49).
93
+ */
94
+ const DECLARATION_REFUSALS = new Set([
95
+ "invalid",
96
+ "conflict",
97
+ "limit",
98
+ ]);
89
99
  export const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
90
100
  /** One value in flight, only the newest waiting. Each push learns its fate. */
91
101
  export class Latest {
@@ -149,6 +159,10 @@ export class Runtime {
149
159
  /** Registrations that activation must wait for before reporting ready. */
150
160
  startup = new Set();
151
161
  #activating = true;
162
+ /** `RuntimeOptions.strictDeclarations`. */
163
+ #strict;
164
+ /** Declarations skipped during activation: reported once it completes. */
165
+ #refusals = [];
152
166
  #transport;
153
167
  /** Each binding with the events whose state it read last time (held). */
154
168
  #bindings = new Map();
@@ -187,8 +201,9 @@ export class Runtime {
187
201
  * property writes). Not part of the plugin API; empty for older players.
188
202
  */
189
203
  features;
190
- constructor(transport, manifest, directory, dataDirectory, app) {
204
+ constructor(transport, manifest, directory, dataDirectory, app, options = {}) {
191
205
  this.#transport = transport;
206
+ this.#strict = options.strictDeclarations ?? false;
192
207
  this.dataDirectory = dataDirectory;
193
208
  this.local = transport.local;
194
209
  this.host = createWire(transport);
@@ -233,6 +248,32 @@ export class Runtime {
233
248
  work.catch(() => { });
234
249
  return work;
235
250
  }
251
+ /**
252
+ * A declaration the player checks on its own (`what`: "command x"),
253
+ * started like `starting`. In the player's runner a refusal of it during
254
+ * activation (`invalid`, `conflict`, `limit`) does not fail the plugin:
255
+ * it is logged, skipped and reported to the scope once activation
256
+ * completes, so an `onError` added anywhere in `activate` hears it (SDK
257
+ * 1.5.49; VS Code logs a bad contribution and activates the extension).
258
+ * Under `strictDeclarations` (TestHost) it fails activation as before.
259
+ */
260
+ declaring(work, what, report) {
261
+ if (!this.#activating || this.#strict)
262
+ return this.starting(work, report);
263
+ const settled = work.then(() => { }, (error) => {
264
+ if (!(error instanceof HostError) ||
265
+ !DECLARATION_REFUSALS.has(error.code))
266
+ throw error;
267
+ console.error(`[${this.manifest.id}] ${what} skipped, the player refused it (${error.code}): ${error.message}`);
268
+ this.#refusals.push(() => report(error));
269
+ });
270
+ // Observed now: `activated()` may come after `activate` awaits, and
271
+ // its `allSettled` still sees the rejection.
272
+ settled.catch(() => { });
273
+ this.startup.add(settled);
274
+ work.catch(() => { });
275
+ return work;
276
+ }
236
277
  async activated() {
237
278
  while (this.startup.size) {
238
279
  const pending = [...this.startup];
@@ -242,6 +283,8 @@ export class Runtime {
242
283
  throw result.reason;
243
284
  }
244
285
  this.#activating = false;
286
+ for (const report of this.#refusals.splice(0))
287
+ report();
245
288
  }
246
289
  bind(evaluate) {
247
290
  const held = new Map();
@@ -2728,8 +2728,11 @@ export declare class TestHost implements Transport {
2728
2728
  *
2729
2729
  * Rejects when `manifest.json` cannot be read or parsed, when `activate`
2730
2730
  * throws, or when a registration made during activation fails (for example
2731
- * `limit`, `conflict`, `invalid`), which is when the player would fail the
2732
- * activation.
2731
+ * `limit`, `conflict`, `invalid`). Stricter than the player's runner for
2732
+ * commands (SDK 1.5.49): there the SDK logs a command declaration the
2733
+ * player refuses during activation, skips it and activates the rest of the
2734
+ * plugin (`onError` hears it); `startPlugin` fails, so a test shows the
2735
+ * bad declaration.
2733
2736
  *
2734
2737
  * @param activate - The plugin's default-exported `activate` function.
2735
2738
  * @param options - Where the plugin lives and how to start it.
@@ -3956,14 +3956,23 @@ export class TestHost {
3956
3956
  !BAR_BUTTON_ICONS.includes(icon))
3957
3957
  throw commandInvalid(`unknown button icon ${JSON.stringify(icon)}: a name of @vestra/icons.slint (${BAR_BUTTON_ICONS.join(", ")}) or an image from plugin.ui.image`);
3958
3958
  }
3959
+ // The binding table's `Table::canonical` (player 0.6.1): each binding
3960
+ // parsed, and each input once, compared parsed, before the reason,
3961
+ // conflicts and limits.
3959
3962
  const bindings = (declaration.bindings ?? []);
3960
- for (const binding of bindings)
3963
+ const seen = new Set();
3964
+ for (const binding of bindings.map(String)) {
3965
+ let input;
3961
3966
  try {
3962
- canonical(String(binding));
3967
+ input = canonical(binding);
3963
3968
  }
3964
3969
  catch (error) {
3965
3970
  throw commandInvalid(error.message);
3966
3971
  }
3972
+ if (seen.has(input))
3973
+ throw commandInvalid(`binding ${binding} is listed twice`);
3974
+ seen.add(input);
3975
+ }
3967
3976
  if (reason !== null)
3968
3977
  commandText(reason, "reason", COMMAND_LIMITS.reasonBytes);
3969
3978
  if (this.commands.has(id))
@@ -3991,13 +4000,6 @@ export class TestHost {
3991
4000
  this.commands.size + this.coreCommands.size + hostOnly.length >=
3992
4001
  COMMAND_LIMITS.total)
3993
4002
  throw new HostError("limit", "command registry limit reached");
3994
- // The binding table: each input once per command.
3995
- const seen = new Set();
3996
- for (const binding of bindings.map(String)) {
3997
- if (seen.has(binding))
3998
- throw new HostError("invalid", `binding ${binding} is listed twice`);
3999
- seen.add(binding);
4000
- }
4001
4003
  const host = this;
4002
4004
  let sent = p.state;
4003
4005
  this.commands.set(id, {
@@ -5426,8 +5428,11 @@ function tellResidency(host) {
5426
5428
  *
5427
5429
  * Rejects when `manifest.json` cannot be read or parsed, when `activate`
5428
5430
  * throws, or when a registration made during activation fails (for example
5429
- * `limit`, `conflict`, `invalid`), which is when the player would fail the
5430
- * activation.
5431
+ * `limit`, `conflict`, `invalid`). Stricter than the player's runner for
5432
+ * commands (SDK 1.5.49): there the SDK logs a command declaration the
5433
+ * player refuses during activation, skips it and activates the rest of the
5434
+ * plugin (`onError` hears it); `startPlugin` fails, so a test shows the
5435
+ * bad declaration.
5431
5436
  *
5432
5437
  * @param activate - The plugin's default-exported `activate` function.
5433
5438
  * @param options - Where the plugin lives and how to start it.
@@ -5512,7 +5517,10 @@ export async function startPlugin(activate, options = {}) {
5512
5517
  const instance = await createPlugin(host, manifest, directory, data,
5513
5518
  // Like the player: protocol additions it takes (`app.features` set on
5514
5519
  // the host replaces them, e.g. `[]` for an older player's behaviour).
5515
- { features: [PATCH_FEATURE], ...host.app });
5520
+ { features: [PATCH_FEATURE], ...host.app },
5521
+ // Unlike the player's runner, which logs a refused command declaration
5522
+ // and activates the rest: a plugin's tests show the refusal.
5523
+ { strictDeclarations: true });
5516
5524
  await activate(instance.plugin);
5517
5525
  // Like the player: a position or resolver another plugin holds is not an
5518
5526
  // activation failure.
@@ -487,7 +487,11 @@ export interface Scope extends Disposable {
487
487
  * Registrations started during activation (commands, panels, resolvers,
488
488
  * subscriptions) do not need to be awaited: the plugin is reported ready
489
489
  * once they all succeed, and activation fails if `activate` throws or any
490
- * of them fails. The entry module must **default-export** `activate`.
490
+ * of them fails. A command declaration the player refuses is the exception
491
+ * (SDK 1.5.49): the SDK logs and skips it, its `ready` rejects and
492
+ * {@link Scope.onError} hears it, the rest of the plugin activates
493
+ * (`startPlugin` still fails on it). The entry module must
494
+ * **default-export** `activate`.
491
495
  *
492
496
  * Stopping: the player first revokes everything the plugin owns on its side
493
497
  * (menus, windows, panels, filters, subtitle layers, snapshots, jobs,
@@ -1138,8 +1142,21 @@ export interface Command extends Disposable {
1138
1142
  * Resolves when the host has registered the command. Rejects with
1139
1143
  * `conflict` (path already taken), `invalid` or `limit` (more than 160
1140
1144
  * commands in this plugin, or 1024 in the whole player). You rarely need
1141
- * to await it: during activation a failure fails activation, later it
1142
- * goes to {@link Scope.onError}.
1145
+ * to await it: the refusal goes to {@link Scope.onError}. During
1146
+ * activation the SDK, in the plugin's process, logs the refusal with the
1147
+ * command id and the player's message, skips the command and lets the
1148
+ * rest of the plugin activate (SDK 1.5.49; before, the refusal failed
1149
+ * activation); the error reaches `onError` once activation completes,
1150
+ * so a listener added anywhere in `activate` hears it. `startPlugin`
1151
+ * stays strict: there a refusal during activation fails it, so tests
1152
+ * show the bad declaration. Any other failure during activation
1153
+ * (`capability`, no answer) fails activation.
1154
+ *
1155
+ * Only the player's answer is skipped. What {@link Commands.add} checks
1156
+ * itself still throws from `add` (an unknown `@` menu id, more than 8
1157
+ * path segments, an `order` out of range, a malformed binding or one
1158
+ * listed twice, a bad `button`, a reason text of the wrong shape, an id
1159
+ * already declared), and fails activation unless the plugin catches it.
1143
1160
  *
1144
1161
  * @throws `conflict` (rejects) Another command already has this menu path,
1145
1162
  * or the path would be both a menu item and a submenu.
@@ -1161,8 +1178,9 @@ export interface Command extends Disposable {
1161
1178
  * when the user picks the item, presses a key or clicks the button.
1162
1179
  * Commands of all plugins merge into the player's menus by their English
1163
1180
  * path keys. Registration is asynchronous: `add` returns at once, and
1164
- * {@link Command.ready} reports the player's answer (during activation a
1165
- * refusal fails activation).
1181
+ * {@link Command.ready} reports the player's answer (during activation
1182
+ * the SDK logs a refusal and skips that command, SDK 1.5.49;
1183
+ * `startPlugin` fails on it). What `add` checks itself throws from `add`.
1166
1184
  *
1167
1185
  * @capability commands
1168
1186
  *
package/llms-full.txt CHANGED
@@ -1,4 +1,4 @@
1
- # @vestra-player/sdk 1.5.48: full reference
1
+ # @vestra-player/sdk 1.5.49: full reference
2
2
 
3
3
  > Everything needed to write a Vestra Player plugin (plugin API `vestra/1`): the guides, every public declaration of `@vestra-player/sdk` and `@vestra-player/sdk/testing` with its documentation, and the complete source of the example plugins. Generated from the SDK's sources by scripts/llms.mjs; the short index is llms.txt.
4
4
 
@@ -2357,7 +2357,7 @@ The player bounds what one request may cost it. The limits are guards against ru
2357
2357
 
2358
2358
  #### Commands
2359
2359
 
2360
- A command added past `perPlugin` or `total` fails with code `limit` (its `ready` rejects; during activation, activation fails). Commands already registered are unaffected, and disposing one frees its slot at once. A declaration past the other limits fails with `invalid` the same way (SDK 1.5.48: `TestHost` refuses it too).
2360
+ A command added past `perPlugin` or `total` fails with code `limit` (its `ready` rejects and `onError` hears it; during activation the SDK logs and skips that command and the rest of the plugin activates, SDK 1.5.49, while `startPlugin` fails). Commands already registered are unaffected, and disposing one frees its slot at once. A declaration past the other limits fails with `invalid` the same way (SDK 1.5.48: `TestHost` refuses it too).
2361
2361
 
2362
2362
  | Limit | Value | Meaning |
2363
2363
  | --- | --- | --- |
@@ -3841,6 +3841,10 @@ When the player starts the plugin:
3841
3841
 
3842
3842
  You do not need to `await` those registrations. `commands.add`, `panels.mount` and `resolvers.add` return their handle at once and register in the background; their `ready` promise is there for the rare case that you want to wait. **Activation fails** when `activate` throws or rejects, or when one of those registrations fails. A registration made later, after activation, cannot fail activation any more: its failure goes to `onError`.
3843
3843
 
3844
+ A command declaration the player refuses (`invalid`, `conflict` or `limit`: a bad text, a path another command has, too many commands) does not fail activation either (SDK 1.5.49). The SDK, in the plugin's process, logs it with the command id and the player's message, skips that command and lets the rest of the plugin activate, like VS Code with a bad contribution; the command's `ready` rejects and `onError` hears the refusal once activation completes. `startPlugin` stays strict: there the refusal fails the start, so your tests show the bad declaration.
3845
+
3846
+ This covers only the player's answer. What `commands.add` checks itself still throws from `add`: an unknown `@` menu id, more than 8 path segments, an `order` out of range, a malformed binding or one listed twice, a bad `button`, a reason text of the wrong shape, an id already declared. Uncaught, such a throw fails activation like any other exception in `activate`.
3847
+
3844
3848
  Some refusals are deliberately not failures, because they depend on which other plugins are installed: a bottom bar or backdrop that another plugin's panel already holds, the next/previous sequence that another plugin provides, a file type or web host that another plugin already resolves. The plugin keeps running without that part, the player tells the user, and the plugin manager shows it as a warning (PluginInfo.warnings). The player's built-in plugins register first, so they win these contests at start-up.
3845
3849
 
3846
3850
  #### Handles and scopes
@@ -4579,6 +4583,7 @@ A tool window (Window), a panel (Panel) and a settings page (SettingsPage) all s
4579
4583
  - **A panel or settings page file exports one component and one global.** The player composes your component into a window it owns (the main window, the floating sidebar window, the Options window), so the component instance is not a stable address: a new instance is made whenever the main window is rebuilt or the sidebar moves to or from its floating window. The **global** is the bridge: properties and callbacks are declared on it, the component reads and calls them (`ListBridge.rows`, `ListBridge.select(index)`), and the plugin talks to the global. The SDK finds the two names from the file's `export component` and `export global` lines.
4580
4584
 
4581
4585
  ```slint
4586
+ import { ListView } from "std-widgets.slint";
4582
4587
  import { Theme } from "@vestra/theme.slint";
4583
4588
 
4584
4589
  export global ListBridge {
@@ -4591,7 +4596,9 @@ export global ListBridge {
4591
4596
  export component ListPanel inherits Rectangle {
4592
4597
  background: Theme.bg-list;
4593
4598
  min-width: 200px;
4594
- VerticalLayout {
4599
+ // A ListView builds only the rows in view and scrolls the rest, so the
4600
+ // list costs the same with three rows or thirty thousand.
4601
+ ListView {
4595
4602
  for row[index] in ListBridge.rows: Rectangle {
4596
4603
  height: 20px;
4597
4604
  background: index == ListBridge.selected ? Theme.selected-bg : transparent;
@@ -4614,9 +4621,6 @@ export component ListPanel inherits Rectangle {
4614
4621
  }
4615
4622
  }
4616
4623
  }
4617
- // Takes the space below the rows. Without it the layout's maximum
4618
- // height (three 20px rows) would cap the player's main window.
4619
- Rectangle { vertical-stretch: 1; }
4620
4624
  }
4621
4625
  }
4622
4626
  ```
@@ -5183,7 +5187,7 @@ self.activations.begin(owner, self.now_ms() + 15_000)?;
5183
5187
 
5184
5188
  Source: src/host/src/packages/package_manager.rs
5185
5189
 
5186
- `boot.ts` sends `runtime.ready` only after `activate` returned and every registration started during activation succeeded (a slot, sequence or resolver another plugin holds does not count as a failure). Otherwise it sends `runtime.failed` with a `code`. A failure or a missed deadline stops the plugin, and the plugin manager lists it as failed with the message. At start-up the installed plugins are started from a queue, about 25 ms of process spawning per UI tick, so the window stays responsive while they come up.
5190
+ `boot.ts` sends `runtime.ready` only after `activate` returned and every registration started during activation succeeded (a slot, sequence or resolver another plugin holds does not count as a failure, and the SDK skips a command declaration the player refuses with `invalid`, `conflict` or `limit`: it logs it and reports it to `onError`, SDK 1.5.49). Otherwise it sends `runtime.failed` with a `code`. A failure or a missed deadline stops the plugin, and the plugin manager lists it as failed with the message. At start-up the installed plugins are started from a queue, about 25 ms of process spawning per UI tick, so the window stays responsive while they come up.
5187
5191
 
5188
5192
  #### The wire protocol
5189
5193
 
@@ -5491,6 +5495,7 @@ Use the layout of Theme: small adjustment windows: the same width, row height, l
5491
5495
  - **No status or path lines in a panel.** Messages go to the OSD; a full path is a tooltip on its row.
5492
5496
  - **Show state with colour, not words.** As in the player's playlist: what is playing in `Theme.playing` (with a thin bar at the left, Theme: layouts), what was played in `Theme.text-played`, the selection on `Theme.selected-bg`. A failed item gets the `failed` icon at the end of its row and an OSD message.
5493
5497
  - **Lists are plain**: single-line rows, background `Theme.bg-list`, no cards, no stripes, no rounded rows; scroll bars come from `std-widgets.slint` so they follow the palette.
5498
+ - **Long lists use `ListView`.** A list whose length the user decides (a playlist, search results, files, messages) goes in a `ListView` from `std-widgets.slint`, with the `for` inside it: it creates only the rows in view, so a list of thousands of rows scrolls as smoothly as one of ten. A `for` in a layout builds every row up front. Keep a bare `for` for a short list of a fixed size, such as a row of buttons. Example: Panel menu is a panel list drawn this way.
5494
5499
  - **Commands of a panel go into its right-click menu**, made of the plugin's commands (panel.showMenu); see Menus.
5495
5500
  - **The bottom bar is icons only**: no text buttons, no source path, no status text (the source name is in the window title, status is an OSD message).
5496
5501
 
@@ -5827,6 +5832,7 @@ Every SDK release's CHANGELOG entry opens with who must change and what happens
5827
5832
 
5828
5833
  | Release | Who must change | Guide |
5829
5834
  | --- | --- | --- |
5835
+ | [1.5.49](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1549) | **Nobody.** No changes to the plugin API (`@vestra-player/sdk/runtime` gains the additive `RuntimeOptions` and an optional sixth `createPlugin` argument), `apiVersion` stays `vestra/1`, no new capability, no player version needed, and the player's runner needs no change. | — |
5830
5836
  | [1.5.48](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1548) | **Nobody, but plugin tests may now fail.** No changes to the TypeScript API beyond new fields of `COMMAND_LIMITS`, `apiVersion` stays `vestra/1`, no new capability, no player version needed. | — |
5831
5837
  | [1.5.47](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1547) | **Nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability, no player version needed. | — |
5832
5838
  | [1.5.46](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1546) | **Nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability. | — |
@@ -6312,7 +6318,11 @@ initial player, window and timeline state, then calls `activate(plugin)`.
6312
6318
  Registrations started during activation (commands, panels, resolvers,
6313
6319
  subscriptions) do not need to be awaited: the plugin is reported ready
6314
6320
  once they all succeed, and activation fails if `activate` throws or any
6315
- of them fails. The entry module must **default-export** `activate`.
6321
+ of them fails. A command declaration the player refuses is the exception
6322
+ (SDK 1.5.49): the SDK logs and skips it, its `ready` rejects and
6323
+ Scope.onError hears it, the rest of the plugin activates
6324
+ (`startPlugin` still fails on it). The entry module must
6325
+ **default-export** `activate`.
6316
6326
 
6317
6327
  Stopping: the player first revokes everything the plugin owns on its side
6318
6328
  (menus, windows, panels, filters, subtitle layers, snapshots, jobs,
@@ -6871,7 +6881,24 @@ interface Command extends Disposable {
6871
6881
  checked: Bindable<boolean | null> | Condition;
6872
6882
  }>): void;
6873
6883
  /**
6874
- * Resolves when the host has registered the command. Rejects with `conflict` (path already taken), `invalid` or `limit` (more than 160 commands in this plugin, or 1024 in the whole player). You rarely need to await it: during activation a failure fails activation, later it goes to Scope.onError.
6884
+ * Resolves when the host has registered the command. Rejects with
6885
+ * `conflict` (path already taken), `invalid` or `limit` (more than 160
6886
+ * commands in this plugin, or 1024 in the whole player). You rarely need
6887
+ * to await it: the refusal goes to Scope.onError. During
6888
+ * activation the SDK, in the plugin's process, logs the refusal with the
6889
+ * command id and the player's message, skips the command and lets the
6890
+ * rest of the plugin activate (SDK 1.5.49; before, the refusal failed
6891
+ * activation); the error reaches `onError` once activation completes,
6892
+ * so a listener added anywhere in `activate` hears it. `startPlugin`
6893
+ * stays strict: there a refusal during activation fails it, so tests
6894
+ * show the bad declaration. Any other failure during activation
6895
+ * (`capability`, no answer) fails activation.
6896
+ *
6897
+ * Only the player's answer is skipped. What Commands.add checks
6898
+ * itself still throws from `add` (an unknown `@` menu id, more than 8
6899
+ * path segments, an `order` out of range, a malformed binding or one
6900
+ * listed twice, a bad `button`, a reason text of the wrong shape, an id
6901
+ * already declared), and fails activation unless the plugin catches it.
6875
6902
  * @throws conflict (rejects) Another command already has this menu path, or the path would be both a menu item and a submenu.
6876
6903
  * @throws capability (rejects) The manifest does not declare `commands`.
6877
6904
  * @throws invalid (rejects) The player refused the declaration.
@@ -6908,8 +6935,9 @@ the menu, the binding table and the user's key overrides: it calls `run`
6908
6935
  when the user picks the item, presses a key or clicks the button.
6909
6936
  Commands of all plugins merge into the player's menus by their English
6910
6937
  path keys. Registration is asynchronous: `add` returns at once, and
6911
- Command.ready reports the player's answer (during activation a
6912
- refusal fails activation).
6938
+ Command.ready reports the player's answer (during activation
6939
+ the SDK logs a refusal and skips that command, SDK 1.5.49;
6940
+ `startPlugin` fails on it). What `add` checks itself throws from `add`.
6913
6941
 
6914
6942
  - @capability commands
6915
6943
 
@@ -8464,6 +8492,7 @@ panel.on("contextMenu", (x: number, y: number) =>
8464
8492
  `ui/panel.slint`:
8465
8493
 
8466
8494
  ```slint
8495
+ import { ListView } from "std-widgets.slint";
8467
8496
  import { Theme } from "@vestra/theme.slint";
8468
8497
 
8469
8498
  export global ListBridge {
@@ -8476,7 +8505,9 @@ export global ListBridge {
8476
8505
  export component ListPanel inherits Rectangle {
8477
8506
  background: Theme.bg-list;
8478
8507
  min-width: 200px;
8479
- VerticalLayout {
8508
+ // A ListView builds only the rows in view and scrolls the rest, so the
8509
+ // list costs the same with three rows or thirty thousand.
8510
+ ListView {
8480
8511
  for row[index] in ListBridge.rows: Rectangle {
8481
8512
  height: 20px;
8482
8513
  background: index == ListBridge.selected ? Theme.selected-bg : transparent;
@@ -8499,9 +8530,6 @@ export component ListPanel inherits Rectangle {
8499
8530
  }
8500
8531
  }
8501
8532
  }
8502
- // Takes the space below the rows. Without it the layout's maximum
8503
- // height (three 20px rows) would cap the player's main window.
8504
- Rectangle { vertical-stretch: 1; }
8505
8533
  }
8506
8534
  }
8507
8535
  ```
@@ -9403,6 +9431,7 @@ panel.bind(() => ({ rows: items, selected }));
9403
9431
  `ui/panel.slint`:
9404
9432
 
9405
9433
  ```slint
9434
+ import { ListView } from "std-widgets.slint";
9406
9435
  import { Theme } from "@vestra/theme.slint";
9407
9436
 
9408
9437
  export global ListBridge {
@@ -9415,7 +9444,9 @@ export global ListBridge {
9415
9444
  export component ListPanel inherits Rectangle {
9416
9445
  background: Theme.bg-list;
9417
9446
  min-width: 200px;
9418
- VerticalLayout {
9447
+ // A ListView builds only the rows in view and scrolls the rest, so the
9448
+ // list costs the same with three rows or thirty thousand.
9449
+ ListView {
9419
9450
  for row[index] in ListBridge.rows: Rectangle {
9420
9451
  height: 20px;
9421
9452
  background: index == ListBridge.selected ? Theme.selected-bg : transparent;
@@ -9438,9 +9469,6 @@ export component ListPanel inherits Rectangle {
9438
9469
  }
9439
9470
  }
9440
9471
  }
9441
- // Takes the space below the rows. Without it the layout's maximum
9442
- // height (three 20px rows) would cap the player's main window.
9443
- Rectangle { vertical-stretch: 1; }
9444
9472
  }
9445
9473
  }
9446
9474
  ```
@@ -17151,8 +17179,11 @@ undeclared capability fail with `capability`, as in the player.
17151
17179
 
17152
17180
  Rejects when `manifest.json` cannot be read or parsed, when `activate`
17153
17181
  throws, or when a registration made during activation fails (for example
17154
- `limit`, `conflict`, `invalid`), which is when the player would fail the
17155
- activation.
17182
+ `limit`, `conflict`, `invalid`). Stricter than the player's runner for
17183
+ commands (SDK 1.5.49): there the SDK logs a command declaration the
17184
+ player refuses during activation, skips it and activates the rest of the
17185
+ plugin (`onError` hears it); `startPlugin` fails, so a test shows the
17186
+ bad declaration.
17156
17187
 
17157
17188
  The way into a plugin's tests: one call starts the plugin the way the
17158
17189
  player's runner does and returns the host to drive, the `plugin` object
@@ -19419,7 +19450,7 @@ Add a window teaches the view surface panels share with tool windows; the rules
19419
19450
  - **The global `ListBridge`** holds the properties and callbacks. A panel is part of the player's main window, and the player makes a new instance of the component whenever it rebuilds that window. The global stays put, so the plugin talks to it, not to the component.
19420
19451
  - `rows` is the list (an array in TypeScript) and `selected` the selected index.
19421
19452
  - `select(int)` reports a click; `context-menu(float, float)` asks for the menu at a point.
19422
- - **The component** inherits `Rectangle`, not `Window`: the player places it. `for row[index] in ListBridge.rows` draws one 20-pixel row per item; the selected row gets `Theme.selected-bg`.
19453
+ - **The component** inherits `Rectangle`, not `Window`: the player places it. A `ListView` from `std-widgets.slint` fills it; `for row[index] in ListBridge.rows` inside it gives one 20-pixel row per item, and the selected row gets `Theme.selected-bg`. The `ListView` creates only the rows in view and scrolls the rest, so the panel stays cheap however long the list grows; a bare `for` in a layout would build every row up front (and `vestra-plugin check` warns about one over a bound array).
19423
19454
  - **The `TouchArea`** of each row: any button going down selects the row. The right button coming up asks for the menu, at the pointer's position in the window (`absolute-position` is the row's position, `mouse-x`/`mouse-y` the pointer's position in it, and `/ 1px` makes a plain number).
19424
19455
 
19425
19456
  #### The code
@@ -19521,6 +19552,7 @@ export default async function activate(plugin: Plugin) {
19521
19552
  #### examples/panel-menu/ui/panel.slint
19522
19553
 
19523
19554
  ```slint
19555
+ import { ListView } from "std-widgets.slint";
19524
19556
  import { Theme } from "@vestra/theme.slint";
19525
19557
 
19526
19558
  export global ListBridge {
@@ -19533,7 +19565,9 @@ export global ListBridge {
19533
19565
  export component ListPanel inherits Rectangle {
19534
19566
  background: Theme.bg-list;
19535
19567
  min-width: 200px;
19536
- VerticalLayout {
19568
+ // A ListView builds only the rows in view and scrolls the rest, so the
19569
+ // list costs the same with three rows or thirty thousand.
19570
+ ListView {
19537
19571
  for row[index] in ListBridge.rows: Rectangle {
19538
19572
  height: 20px;
19539
19573
  background: index == ListBridge.selected ? Theme.selected-bg : transparent;
@@ -19556,9 +19590,6 @@ export component ListPanel inherits Rectangle {
19556
19590
  }
19557
19591
  }
19558
19592
  }
19559
- // Takes the space below the rows. Without it the layout's maximum
19560
- // height (three 20px rows) would cap the player's main window.
19561
- Rectangle { vertical-stretch: 1; }
19562
19593
  }
19563
19594
  }
19564
19595
  ```
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vestra Player SDK
2
2
 
3
- > @vestra-player/sdk 1.5.48: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls; it runs in its own Node.js process (the player's built-in plugins share one). Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle that is released automatically when the plugin stops. L<n>: a line of llms-full.txt (next to this file), which has every page, declaration and example.
3
+ > @vestra-player/sdk 1.5.49: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls; it runs in its own Node.js process (the player's built-in plugins share one). Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle that is released automatically when the plugin stops. L<n>: a line of llms-full.txt (next to this file), which has every page, declaration and example.
4
4
 
5
5
  **Rules**
6
6
 
@@ -80,7 +80,7 @@ export default async function activate(plugin: Plugin) {
80
80
  - `video.upscaling`: VideoUpscaling.state/set/onChange
81
81
  - No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme, packages.list and rendering.state. Details: L2293–2353.
82
82
 
83
- **Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`, `host` an in-memory player (TestHost): act (`host.command(id).run()`, `host.press(key)`), `await host.settle()`, assert on `host.osd`, `host.window(id).props`… Guide: L1861–2021; TestHost: L15502.
83
+ **Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`, `host` an in-memory player (TestHost): act (`host.command(id).run()`, `host.press(key)`), `await host.settle()`, assert on `host.osd`, `host.window(id).props`… Guide: L1861–2021; TestHost: L15530.
84
84
 
85
85
  **Command line**
86
86
 
@@ -108,7 +108,7 @@ export default async function activate(plugin: Plugin) {
108
108
  - `limit`: A player-wide limit was reached, a property value is over the limits of PropValue
109
109
  - `reserved`: The id is `vestra` or starts with `vestra.`
110
110
  - `versionMismatch`: The package needs a newer player
111
- - Others: PluginError, llms-full.txt L5966.
111
+ - Others: PluginError, llms-full.txt L5972.
112
112
 
113
113
  ## Reference
114
114
 
@@ -116,32 +116,32 @@ export default async function activate(plugin: Plugin) {
116
116
  - Tutorials: Getting started L9–134; Add a window L135–213; Add settings and translations L214–367; Test, check, pack and publish L368–424; Write your own blur kernel L425–685
117
117
  - How-to: Control playback L686–854; Store a plugin's data L855–961; Load subtitles from a plugin L962–1074; Keep a slider responsive while dragging L1075–1262; Remember a window's state between runs L1263–1414; Offer a GPU rendering switch L1415–1540; Keep a command's state without keeping the plugin running L1541–1697; Translate a plugin L1698–1860; Test a plugin L1861–2021; Publish a plugin L2022–2123; Troubleshooting L2124–2217
118
118
  - Reference: Manifest L2218–2292; Capabilities L2293–2353; Limits L2354–2506; Commands, menus and bindings L2507–3033; Platforms L3034–3064; Theme L3065–3414; Command line L3415–3472; Error codes L3473–3520; Translations L3521–3613; Test host L3614–3800
119
- - Explanation: How a plugin runs L3801–4003; Commands, menus and key bindings L4004–4158; Playback state, controls and sources L4159–4318; Video and audio processing L4319–4439; Windows, panels, settings pages, dialogs and the OSD L4440–4704; GPU and software rendering L4705–4721; The player's Rust core L4722–5063; How the player runs and talks to plugins L5064–5403
120
- - Guidelines: Plugin rules L5404–5415; Interface guidelines L5416–5532; Menu guidelines L5533–5591
121
- - Upgrading: Migrate a plugin to English source text L5592–5741; Migrating off package rollback (1.5.16, removed in 1.5.17) L5742–5819; Who must change, release by release L5820–5888
122
- - [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L5889–15175
123
- - [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L15176–18059
119
+ - Explanation: How a plugin runs L3801–4007; Commands, menus and key bindings L4008–4162; Playback state, controls and sources L4163–4322; Video and audio processing L4323–4443; Windows, panels, settings pages, dialogs and the OSD L4444–4708; GPU and software rendering L4709–4725; The player's Rust core L4726–5067; How the player runs and talks to plugins L5068–5407
120
+ - Guidelines: Plugin rules L5408–5419; Interface guidelines L5420–5537; Menu guidelines L5538–5596
121
+ - Upgrading: Migrate a plugin to English source text L5597–5746; Migrating off package rollback (1.5.16, removed in 1.5.17) L5747–5824; Who must change, release by release L5825–5894
122
+ - [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L5895–15203
123
+ - [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L15204–18090
124
124
 
125
125
  ## API
126
126
 
127
127
  Where each `plugin.<module>` is declared:
128
128
 
129
- - Plugin L6305, Scope L6041, PluginError L5966, Disposable L5904
130
- - Commands L6900; When L6492; Windows L7847; Panels L9293; Dialogs L9963; Player L10704; video: VideoFilter L11584, VideoView L11684, RenderOptions L11771, VideoShaders L11849, VideoUpscaling L12207; audio: AudioFilter L11629; Subtitles L12443; media: MediaCapture L12847; Resolvers L13077; Packages L13479; Osd L11449; Host L11291; Clipboard L13808; Power L13895; Tools L13954; Storage L14106; System L14305; Secrets L14385; window: MainWindow L9610; App L9790; Rendering L14648; Bindings L13707; Ui L7193; Timeline L9522; Drops L8540; Appearance L8621; I18n L8854; Settings L9137.
129
+ - Plugin L6311, Scope L6047, PluginError L5972, Disposable L5910
130
+ - Commands L6927; When L6502; Windows L7875; Panels L9321; Dialogs L9991; Player L10732; video: VideoFilter L11612, VideoView L11712, RenderOptions L11799, VideoShaders L11877, VideoUpscaling L12235; audio: AudioFilter L11657; Subtitles L12471; media: MediaCapture L12875; Resolvers L13105; Packages L13507; Osd L11477; Host L11319; Clipboard L13836; Power L13923; Tools L13982; Storage L14134; System L14333; Secrets L14413; window: MainWindow L9638; App L9818; Rendering L14676; Bindings L13735; Ui L7221; Timeline L9550; Drops L8568; Appearance L8649; I18n L8882; Settings L9165.
131
131
 
132
132
  ## Examples
133
133
 
134
134
  Tested plugins in [examples/](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples):
135
135
 
136
- - hello: The README Quick start: one menu command and an on-screen message. L18494–18589
137
- - blur: A tool window with one slider that blurs the playing video. L18590–18857
138
- - signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L18858–19196
139
- - bookmark: A tool window bound to the playback state: remember the position and jump back to it. L19197–19389
140
- - panel-menu: A sidebar panel with its own right-click menu. L19390–19604
141
- - menus: Radio menu items, separator groups and a menu item whose title changes. L19605–19770
142
- - theme-widgets: A tool window drawn with the player's theme and widgets. L19771–19940
143
- - markdown: A window that shows a Markdown document. L19941–20143
144
- - i18n: A plugin in the player's language: English source text, a Chinese translation. L20144–20317
136
+ - hello: The README Quick start: one menu command and an on-screen message. L18525–18620
137
+ - blur: A tool window with one slider that blurs the playing video. L18621–18888
138
+ - signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L18889–19227
139
+ - bookmark: A tool window bound to the playback state: remember the position and jump back to it. L19228–19420
140
+ - panel-menu: A sidebar panel with its own right-click menu. L19421–19635
141
+ - menus: Radio menu items, separator groups and a menu item whose title changes. L19636–19801
142
+ - theme-widgets: A tool window drawn with the player's theme and widgets. L19802–19971
143
+ - markdown: A window that shows a Markdown document. L19972–20174
144
+ - i18n: A plugin in the player's language: English source text, a Chinese translation. L20175–20348
145
145
 
146
146
  ## Optional
147
147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vestra-player/sdk",
3
- "version": "1.5.48",
3
+ "version": "1.5.49",
4
4
  "description": "Plugin API types, test host and plugin CLI for Vestra Player (apiVersion vestra/1)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {