@vestra-player/sdk 1.5.3 → 1.5.4

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.4
4
+
5
+ **Markdown in list rows** (the player after 0.4.0). Additive; `apiVersion` stays `vestra/1`, and plugins written for 1.5.3 need no change.
6
+
7
+ - **A `MarkdownRef` at any depth.** A document from `plugin.ui.markdown` may now be a field at any depth of a property value, wherever the Slint type is `[MarkdownBlock]`: for example the `body` of each row of a chat's `[Message]` list. Before, it could only be the whole value of a `[MarkdownBlock]` property. `PropValue` is now `PropData | ImageRef`, where the new **`PropData`** is JSON whose values may also be `MarkdownRef`s, at any depth. A released document is refused with `invalid` wherever it is in the value. An `ImageRef` stays a whole value only: nested in an array or object it is `invalid` (a type error in TypeScript, and refused by the SDK and the player). Older players refuse a nested document like any value they cannot convert.
8
+ - **Limits:** a document still holds at most 256 KB of text, 4000 blocks and 64 images; all of a value's text, its documents' text included, counts toward the value's 1 MiB; each document is one of its 4096 values.
9
+ - **`MarkdownBlocks`** (new in `@vestra/markdown.slint`): the blocks of a document without scrolling, as tall as their content, for your own layout or a `ListView` row. `in [MarkdownBlock] blocks`, `in bool code-copy` (default `false`), callbacks `link-clicked(string)` and `copy-code(string)`. `MarkdownView` is now a `ScrollView` around it and gains the same `code-copy` and `copy-code`; it looks as before.
10
+ - **Code copy, opt in.** With `code-copy: true`, code blocks get a 复制 button that reports `copy-code(code)` with the block's text; the plugin copies it (`plugin.clipboard.writeText`). Off by default: views that do not set it look and behave as before.
11
+ - **Cheaper updates** (player): writing a value again with unchanged documents does not parse them again (a new reference for the same text included), and a value with documents is updated in place where its shape allows: changed rows, rows added or removed at the end, and only the changed blocks of a changed document. Streaming a reply into the last row of a 200-message list therefore keeps the list, its scroll position and the other rows. Nothing to change in plugins.
12
+ - `TestHost` keeps nested documents in the view's properties as they were sent; `host.markdown(value)` reads one back from a field (`host.markdown(rows[0].body)`).
3
13
  ## 1.5.3
4
14
 
5
15
  **File resolvers: local files as generated streams** (player 0.4.1). Additive; `apiVersion` stays `vestra/1`. Plugins that register one set `"minPlayerVersion": "0.4.1"`: an older player refuses the declaration (`invalid`).
@@ -1128,9 +1128,16 @@ export declare class TestHost implements Transport {
1128
1128
  */
1129
1129
  readonly remoteImages: Map<string, string | null>;
1130
1130
  /**
1131
- * The Markdown document a property holds (its wire form read back), or
1131
+ * The Markdown document a value holds (its wire form read back), or
1132
1132
  * `null` when the value is not one. `images` maps each remote address to
1133
- * the downloaded file.
1133
+ * the downloaded file. Pass a whole property value or a field of one: a
1134
+ * document nested in rows is read the same way.
1135
+ *
1136
+ * @example
1137
+ * ```ts
1138
+ * const rows = host.window("chat").props["messages"] as { body: Json }[];
1139
+ * assert.equal(host.markdown(rows[0]!.body)?.text, "**Hello**");
1140
+ * ```
1134
1141
  */
1135
1142
  markdown(value: Json | undefined): MarkdownWire | null;
1136
1143
  /**
@@ -1482,9 +1482,16 @@ export class TestHost {
1482
1482
  */
1483
1483
  remoteImages = new Map();
1484
1484
  /**
1485
- * The Markdown document a property holds (its wire form read back), or
1485
+ * The Markdown document a value holds (its wire form read back), or
1486
1486
  * `null` when the value is not one. `images` maps each remote address to
1487
- * the downloaded file.
1487
+ * the downloaded file. Pass a whole property value or a field of one: a
1488
+ * document nested in rows is read the same way.
1489
+ *
1490
+ * @example
1491
+ * ```ts
1492
+ * const rows = host.window("chat").props["messages"] as { body: Json }[];
1493
+ * assert.equal(host.markdown(rows[0]!.body)?.text, "**Hello**");
1494
+ * ```
1488
1495
  */
1489
1496
  markdown(value) {
1490
1497
  const doc = value
@@ -562,18 +562,31 @@ export interface ImageRef {
562
562
  readonly __vestraImage: true;
563
563
  }
564
564
  /**
565
- * A Markdown document for `MarkdownView`, made by {@link Ui.markdown}.
565
+ * A Markdown document for `MarkdownView` or `MarkdownBlocks`, made by
566
+ * {@link Ui.markdown}.
566
567
  *
567
568
  * @remarks
568
- * Opaque, like {@link ImageRef}: pass it as the whole value of a
569
- * `[MarkdownBlock]` property (the `blocks` of `MarkdownView` from
570
- * `@vestra/markdown.slint`). It carries the text, the base address and the
571
- * local copies of its remote images; the player parses and draws it. It
572
- * belongs to the scope that created it: once that scope is disposed, writing
573
- * it fails with `invalid`.
574
- *
575
- * @player 0.2.0
576
- * @see {@link Ui.markdown}, {@link MarkdownOptions}
569
+ * Opaque, like {@link ImageRef}: pass it where the Slint type is
570
+ * `[MarkdownBlock]`: the whole value of such a property (the `blocks` of
571
+ * `MarkdownView` from `@vestra/markdown.slint`) or, from SDK 1.5.4, a
572
+ * `[MarkdownBlock]` field at any depth of a property value, such as the
573
+ * `body` of each row of a chat's message list. It carries the text, the
574
+ * base address and the local copies of its remote images; the player parses
575
+ * and draws it. Writing a document whose text did not change (a new
576
+ * reference for the same text included) costs the player no parsing, and a
577
+ * changed document updates only the blocks that changed. It belongs to the
578
+ * scope that created it: once that scope is disposed, writing it fails with
579
+ * `invalid`, wherever it is in the value.
580
+ *
581
+ * @example Chat messages, one document per row
582
+ * ```ts
583
+ * const chat = plugin.windows.create("ui/chat.slint");
584
+ * const body = await plugin.ui.markdown("**Hello**", { remoteImages: false });
585
+ * chat.set({ messages: [{ id: "m1", role: "assistant", body }] });
586
+ * ```
587
+ *
588
+ * @player 0.2.0; nested in a value: the player after 0.4.0
589
+ * @see {@link Ui.markdown}, {@link MarkdownOptions}, {@link PropValue}
577
590
  * @since 1.1.0
578
591
  * @category UI
579
592
  */
@@ -607,8 +620,9 @@ export interface MarkdownOptions {
607
620
  remoteImages?: boolean;
608
621
  }
609
622
  /**
610
- * The value of one Slint property: JSON, or an image or a Markdown document
611
- * as the whole value of a property.
623
+ * The value of one Slint property: JSON with Markdown documents at any
624
+ * depth ({@link PropData}), or an image as the whole value of an `image`
625
+ * property.
612
626
  *
613
627
  * @remarks
614
628
  * JSON becomes Slint values as you would expect: booleans for `bool`,
@@ -618,14 +632,37 @@ export interface MarkdownOptions {
618
632
  * is refused (Slint treats `_` and `-` as the same, so use camelCase or
619
633
  * kebab-case).
620
634
  *
621
- * Limits per value: nesting depth 8, 4096 values in total, 1024 rows per
622
- * array, 64 KiB per string and 1 MiB of text in total. A value over a limit
623
- * is a failed write (see {@link View}).
635
+ * A {@link MarkdownRef} goes where the Slint type is `[MarkdownBlock]`: the
636
+ * whole value, or a field at any depth (SDK 1.5.4; older players refuse a
637
+ * nested one). An {@link ImageRef} only as the whole value: nested in an
638
+ * array or object it is `invalid`.
639
+ *
640
+ * Limits per value: nesting depth 8, 4096 values in total (a document is
641
+ * one), 1024 rows per array, 64 KiB per string and 1 MiB of text in total,
642
+ * the text of its Markdown documents included (256 KB per document). A
643
+ * value over a limit is a failed write (see {@link View}).
624
644
  *
625
645
  * @since 1.0.0
626
646
  * @category UI
627
647
  */
628
- export type PropValue = Json | ImageRef | MarkdownRef;
648
+ export type PropValue = PropData | ImageRef;
649
+ /**
650
+ * JSON whose values may also be Markdown documents, at any depth: the part
651
+ * of {@link PropValue} that nests.
652
+ *
653
+ * @example A message list whose rows carry a document
654
+ * ```ts
655
+ * const messages: PropData = [
656
+ * { id: "m1", role: "user", body: await plugin.ui.markdown("Hi") },
657
+ * ];
658
+ * ```
659
+ *
660
+ * @since 1.5.4
661
+ * @category UI
662
+ */
663
+ export type PropData = null | boolean | number | string | MarkdownRef | PropData[] | {
664
+ [key: string]: PropData;
665
+ };
629
666
  /**
630
667
  * Properties to write to a view, by name.
631
668
  *
package/dist/plugin/ui.js CHANGED
@@ -31,18 +31,33 @@ export class ImageHandle {
31
31
  return { $vestraImage: this.path, ref: this.#ref };
32
32
  }
33
33
  }
34
- /** A property value as sent: images and documents in wire form; released ones refused. */
35
- function wireValue(value) {
34
+ /**
35
+ * A property value as sent: images and documents in wire form, documents at
36
+ * any depth (released ones refused wherever they are); an image only as the
37
+ * whole value, like the player.
38
+ */
39
+ function wireValue(value, nested = false) {
36
40
  if (value instanceof MarkdownHandle) {
37
41
  if (value.released)
38
42
  throw failure("invalid", "Markdown document was released with the scope that created it");
39
43
  return value.toJSON();
40
44
  }
41
- if (!(value instanceof ImageHandle))
42
- return value;
43
- if (value.released)
44
- throw failure("invalid", `image ${value.path} was released with the scope that created it`);
45
- return value.toJSON();
45
+ if (value instanceof ImageHandle) {
46
+ if (nested)
47
+ throw failure("invalid", `image ${value.path}: an image reference must be the whole value of an image property`);
48
+ if (value.released)
49
+ throw failure("invalid", `image ${value.path} was released with the scope that created it`);
50
+ return value.toJSON();
51
+ }
52
+ if (Array.isArray(value))
53
+ return value.map((item) => wireValue(item, true));
54
+ if (value !== null && typeof value === "object") {
55
+ const out = {};
56
+ for (const [key, item] of Object.entries(value))
57
+ out[key] = wireValue(item, true);
58
+ return out;
59
+ }
60
+ return value;
46
61
  }
47
62
  export const ui = (scope) => ({
48
63
  image(path) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vestra-player/sdk",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
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": {
@@ -1,9 +1,14 @@
1
1
  // Markdown display (store proposal §5):
2
- // import { MarkdownView, MarkdownBlock } from "@vestra/markdown.slint";
2
+ // import { MarkdownView, MarkdownBlocks, MarkdownBlock } from "@vestra/markdown.slint";
3
3
  // Give `blocks` the value of `plugin.ui.markdown(text)`; the player parses
4
4
  // the Markdown and fills the rows. Links report `link-clicked(url)` with the
5
5
  // address resolved against the document's base; the plugin decides what to
6
6
  // open (usually `plugin.system.openUrl`).
7
+ // `MarkdownView` scrolls one document. `MarkdownBlocks` does not scroll and
8
+ // is as tall as its content: put it in your own layout or in a `ListView`
9
+ // row (a chat message: a `[MarkdownBlock]` field of the row's struct).
10
+ // `code-copy: true` adds a 复制 button to code blocks that reports
11
+ // `copy-code(code)`; the plugin copies it (`plugin.clipboard`).
7
12
  import { ScrollView } from "std-widgets.slint";
8
13
  import { Theme } from "theme.slint";
9
14
  import { Icon } from "icons.slint";
@@ -45,7 +50,9 @@ export struct MarkdownBlock {
45
50
  component MarkdownBlockView {
46
51
  in property <MarkdownBlock> block;
47
52
  in property <length> available;
53
+ in property <bool> code-copy;
48
54
  callback link-clicked(string);
55
+ callback copy-code(string);
49
56
  private property <length> quote-indent: block.quote * 12px;
50
57
  private property <length> list-indent: block.indent * 16px;
51
58
  private property <length> inner: max(16px, root.available - root.quote-indent - root.list-indent);
@@ -124,6 +131,24 @@ component MarkdownBlockView {
124
131
  default-font-size: Theme.font-body;
125
132
  }
126
133
  }
134
+ if root.code-copy: Rectangle {
135
+ x: parent.width - self.width - 4px;
136
+ y: 4px;
137
+ width: copy-label.preferred-width + 12px;
138
+ height: Theme.button-small-height;
139
+ border-radius: Theme.radius-control;
140
+ background: copy-touch.pressed ? Theme.button-press : copy-touch.has-hover ? Theme.button-hover : Theme.button-bg;
141
+ copy-label := Text {
142
+ text: "复制";
143
+ color: copy-touch.has-hover ? Theme.text : Theme.text-dim;
144
+ font-size: Theme.font-body;
145
+ vertical-alignment: center;
146
+ }
147
+ copy-touch := TouchArea {
148
+ mouse-cursor: pointer;
149
+ clicked => { root.copy-code(block.code); }
150
+ }
151
+ }
127
152
  }
128
153
  if block.kind == "image" && block.has-image: HorizontalLayout {
129
154
  alignment: start;
@@ -186,12 +211,38 @@ component MarkdownBlockView {
186
211
  }
187
212
  }
188
213
 
214
+ // Markdown blocks without scrolling, as tall as their content.
215
+ export component MarkdownBlocks inherits Rectangle {
216
+ in property <[MarkdownBlock]> blocks;
217
+ // A 复制 button on code blocks, reporting `copy-code(code)`.
218
+ in property <bool> code-copy: false;
219
+ callback link-clicked(string);
220
+ callback copy-code(string);
221
+ min-height: column.min-height;
222
+ preferred-height: column.preferred-height;
223
+ column := VerticalLayout {
224
+ width: root.width;
225
+ spacing: 8px;
226
+ alignment: start;
227
+ for block in root.blocks: MarkdownBlockView {
228
+ block: block;
229
+ available: root.width;
230
+ code-copy: root.code-copy;
231
+ link-clicked(url) => { root.link-clicked(url); }
232
+ copy-code(code) => { root.copy-code(code); }
233
+ }
234
+ }
235
+ }
236
+
189
237
  // A scrolling Markdown document.
190
238
  export component MarkdownView inherits Rectangle {
191
239
  in property <[MarkdownBlock]> blocks;
192
240
  // Space around the document.
193
241
  in property <length> inset: 10px;
242
+ // A 复制 button on code blocks, reporting `copy-code(code)`.
243
+ in property <bool> code-copy: false;
194
244
  callback link-clicked(string);
245
+ callback copy-code(string);
195
246
  scroll := ScrollView {
196
247
  width: 100%;
197
248
  height: 100%;
@@ -200,12 +251,12 @@ export component MarkdownView inherits Rectangle {
200
251
  column := VerticalLayout {
201
252
  width: scroll.visible-width;
202
253
  padding: root.inset;
203
- spacing: 8px;
204
254
  alignment: start;
205
- for block in root.blocks: MarkdownBlockView {
206
- block: block;
207
- available: scroll.visible-width - 2 * root.inset;
255
+ MarkdownBlocks {
256
+ blocks: root.blocks;
257
+ code-copy: root.code-copy;
208
258
  link-clicked(url) => { root.link-clicked(url); }
259
+ copy-code(code) => { root.copy-code(code); }
209
260
  }
210
261
  }
211
262
  }