@perspective-dev/viewer 5.1.0 → 5.3.0

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.
Files changed (245) hide show
  1. package/dist/cdn/perspective-viewer.js +9 -9
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/intl/de.css +1 -1
  9. package/dist/css/intl/es.css +1 -1
  10. package/dist/css/intl/fr.css +1 -1
  11. package/dist/css/intl/ja.css +1 -1
  12. package/dist/css/intl/pt.css +1 -1
  13. package/dist/css/intl/zh.css +1 -1
  14. package/dist/css/intl.css +1 -1
  15. package/dist/css/monokai.css +1 -1
  16. package/dist/css/phosphor.css +1 -1
  17. package/dist/css/pro-dark.css +1 -1
  18. package/dist/css/pro.css +1 -1
  19. package/dist/css/solarized-dark.css +1 -1
  20. package/dist/css/solarized.css +1 -1
  21. package/dist/css/themes.css +1 -1
  22. package/dist/css/vaporwave.css +1 -1
  23. package/dist/docs/perspective-docs.json +1 -0
  24. package/dist/esm/column-format.d.ts +19 -28
  25. package/dist/esm/perspective-viewer.d.ts +3 -0
  26. package/dist/esm/perspective-viewer.inline.js +9 -9
  27. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  28. package/dist/esm/perspective-viewer.js +9 -9
  29. package/dist/esm/perspective-viewer.js.map +4 -4
  30. package/dist/esm/plugin.d.ts +10 -0
  31. package/dist/esm/providers.d.ts +79 -0
  32. package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
  33. package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
  34. package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
  35. package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
  36. package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
  37. package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
  38. package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
  39. package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
  40. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
  41. package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
  42. package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
  43. package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
  44. package/dist/esm/ts-rs/Notation.d.ts +17 -0
  45. package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
  46. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +42 -2
  47. package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
  48. package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
  49. package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
  50. package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
  51. package/dist/esm/ts-rs/SaveWorkspaceOptions.d.ts +10 -0
  52. package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
  53. package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
  54. package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
  55. package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
  56. package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
  57. package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
  58. package/dist/esm/ts-rs/Unit.d.ts +1 -0
  59. package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
  60. package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
  61. package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
  62. package/dist/esm/ts-rs/ViewerConfig.d.ts +42 -2
  63. package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
  64. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +63 -0
  65. package/dist/esm/ts-rs/WindowSpec.d.ts +3 -2
  66. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  67. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +20 -9
  68. package/dist/wasm/perspective-viewer.d.ts +157 -41
  69. package/dist/wasm/perspective-viewer.js +452 -151
  70. package/dist/wasm/perspective-viewer.wasm +0 -0
  71. package/dist/wasm/perspective-viewer.wasm.d.ts +22 -17
  72. package/package.json +2 -1
  73. package/src/css/_viewer_bundle.css +1 -0
  74. package/src/css/column-selector.css +8 -0
  75. package/src/css/column-settings-panel.css +9 -5
  76. package/src/css/column-style.css +209 -31
  77. package/src/css/config-selector.css +38 -3
  78. package/src/css/dom/select.css +2 -1
  79. package/src/css/form/code-editor.css +6 -7
  80. package/src/css/form/mirrored-textarea.css +81 -0
  81. package/src/css/viewer.css +328 -0
  82. package/src/rust/agent/client/mod.rs +19 -0
  83. package/src/rust/agent/client/protocol.rs +320 -0
  84. package/src/rust/agent/client/transport.rs +287 -0
  85. package/src/rust/agent/client/turn.rs +174 -0
  86. package/src/rust/agent/config.rs +159 -0
  87. package/src/rust/agent/docs.rs +293 -0
  88. package/src/rust/agent/markdown/block.rs +509 -0
  89. package/src/rust/agent/markdown/entity.rs +52 -0
  90. package/src/rust/agent/markdown/events.rs +71 -0
  91. package/src/rust/agent/markdown/inline.rs +497 -0
  92. package/src/rust/agent/markdown/mod.rs +466 -0
  93. package/src/rust/agent/mod.rs +23 -0
  94. package/src/rust/agent/preamble.md +16 -0
  95. package/src/rust/agent/runtime.rs +201 -0
  96. package/src/rust/agent/slot.rs +236 -0
  97. package/src/rust/agent/tools.rs +1141 -0
  98. package/src/rust/components/chat_panel.rs +289 -0
  99. package/src/rust/components/column_selector/active_column.rs +2 -2
  100. package/src/rust/components/column_selector/add_expression_button.rs +3 -3
  101. package/src/rust/components/column_selector/aggregate_selector.rs +2 -3
  102. package/src/rust/components/column_selector/config_selector.rs +84 -7
  103. package/src/rust/components/column_selector/expr_edit_button.rs +4 -10
  104. package/src/rust/components/column_selector/inactive_column.rs +2 -2
  105. package/src/rust/components/column_selector.rs +55 -4
  106. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +293 -126
  107. package/src/rust/components/column_settings_sidebar/style_tab.rs +262 -225
  108. package/src/rust/components/column_settings_sidebar.rs +88 -69
  109. package/src/rust/components/containers/dragdrop_list.rs +37 -0
  110. package/src/rust/components/editable_header.rs +85 -144
  111. package/src/rust/components/expression_editor.rs +9 -16
  112. package/src/rust/components/form/code_editor.rs +29 -28
  113. package/src/rust/components/form/debug.rs +9 -5
  114. package/src/rust/components/form/mirrored_textarea.rs +165 -0
  115. package/src/rust/components/form/mod.rs +4 -1
  116. package/src/rust/components/form/multi_stop_gradient_selector.rs +476 -0
  117. package/src/rust/components/form/named_value_picker.rs +86 -0
  118. package/src/rust/components/form/palette_selector.rs +440 -0
  119. package/src/rust/components/main_panel/msg.rs +2 -2
  120. package/src/rust/components/main_panel/reconcile.rs +4 -4
  121. package/src/rust/components/mod.rs +2 -0
  122. package/src/rust/components/plugin_tab.rs +4 -28
  123. package/src/rust/components/settings_panel.rs +105 -22
  124. package/src/rust/components/viewer/msg.rs +46 -7
  125. package/src/rust/components/viewer/panels.rs +17 -20
  126. package/src/rust/components/viewer/render.rs +10 -3
  127. package/src/rust/components/viewer/settings.rs +163 -23
  128. package/src/rust/components/viewer/snapshots.rs +46 -21
  129. package/src/rust/components/viewer/wiring.rs +14 -25
  130. package/src/rust/components/viewer.rs +12 -8
  131. package/src/rust/components/window_editor.rs +75 -103
  132. package/src/rust/config/column_config_schema.rs +100 -41
  133. package/src/rust/config/css_scale.rs +882 -0
  134. package/src/rust/config/datetime_column_style/custom.rs +17 -0
  135. package/src/rust/config/datetime_column_style/simple.rs +10 -0
  136. package/src/rust/config/mod.rs +2 -0
  137. package/src/rust/config/number_string_format.rs +33 -0
  138. package/src/rust/config/options.rs +28 -0
  139. package/src/rust/config/plugin_static_config.rs +120 -5
  140. package/src/rust/config/viewer_config.rs +216 -3
  141. package/src/rust/config/workspace_config.rs +138 -10
  142. package/src/rust/custom_elements/viewer.rs +369 -119
  143. package/src/rust/custom_events.rs +43 -1
  144. package/src/rust/js/regular_layout.rs +3 -3
  145. package/src/rust/lib.rs +15 -0
  146. package/src/rust/presentation/column_locator.rs +19 -5
  147. package/src/rust/presentation.rs +200 -36
  148. package/src/rust/queries/column_locator.rs +74 -44
  149. package/src/rust/queries/get_viewer_config.rs +2 -1
  150. package/src/rust/queries/mod.rs +2 -0
  151. package/src/rust/queries/palette_set.rs +82 -0
  152. package/src/rust/queries/plugin_column_styles.rs +6 -2
  153. package/src/rust/renderer/dispatch.rs +16 -4
  154. package/src/rust/renderer/plugin_config.rs +309 -47
  155. package/src/rust/renderer.rs +15 -42
  156. package/src/rust/session/column_defaults_update.rs +32 -7
  157. package/src/rust/session/metadata.rs +23 -21
  158. package/src/rust/session/props.rs +1 -0
  159. package/src/rust/session/replace_expression_update.rs +1 -0
  160. package/src/rust/session.rs +9 -6
  161. package/src/rust/tasks/create_panel.rs +46 -49
  162. package/src/rust/tasks/edit_expression.rs +19 -23
  163. package/src/rust/tasks/edit_window.rs +9 -11
  164. package/src/rust/tasks/eject.rs +4 -1
  165. package/src/rust/tasks/pipeline.rs +2 -2
  166. package/src/rust/tasks/presize_panels.rs +44 -5
  167. package/src/rust/tasks/reset_all.rs +4 -4
  168. package/src/rust/tasks/restore_and_render.rs +17 -44
  169. package/src/rust/tasks/restore_panel.rs +37 -22
  170. package/src/rust/tasks/set_edit_mode.rs +3 -1
  171. package/src/rust/tasks/sync_update_panels.rs +18 -24
  172. package/src/rust/tasks/update_theme.rs +23 -18
  173. package/src/rust/utils/css_vars.rs +26 -0
  174. package/src/rust/utils/mod.rs +2 -0
  175. package/src/rust/utils/spawn.rs +1 -1
  176. package/src/rust/workspace.rs +454 -199
  177. package/src/svg/lock.svg +7 -0
  178. package/src/themes/botanical.css +10 -0
  179. package/src/themes/defaults.css +12 -0
  180. package/src/themes/dracula.css +17 -0
  181. package/src/themes/gruvbox-dark.css +20 -0
  182. package/src/themes/gruvbox.css +10 -0
  183. package/src/themes/icons.css +1 -0
  184. package/src/themes/intl/de.css +16 -0
  185. package/src/themes/intl/es.css +16 -0
  186. package/src/themes/intl/fr.css +16 -0
  187. package/src/themes/intl/ja.css +16 -0
  188. package/src/themes/intl/pt.css +16 -0
  189. package/src/themes/intl/zh.css +16 -0
  190. package/src/themes/intl.css +16 -0
  191. package/src/themes/monokai.css +22 -0
  192. package/src/themes/phosphor.css +5 -0
  193. package/src/themes/pro-dark.css +10 -0
  194. package/src/themes/solarized-dark.css +7 -0
  195. package/src/themes/solarized.css +7 -0
  196. package/src/themes/vaporwave.css +20 -0
  197. package/src/ts/column-format.ts +67 -46
  198. package/src/ts/perspective-viewer.ts +3 -0
  199. package/src/ts/plugin.ts +14 -0
  200. package/src/ts/providers.ts +96 -0
  201. package/src/ts/ts-rs/CompactDisplay.ts +3 -0
  202. package/src/ts/ts-rs/CurrencyCode.ts +3 -0
  203. package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
  204. package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
  205. package/src/ts/ts-rs/CurrencySign.ts +3 -0
  206. package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
  207. package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
  208. package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
  209. package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
  210. package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
  211. package/src/ts/ts-rs/FormatMode.ts +3 -0
  212. package/src/ts/ts-rs/FormatUnit.ts +3 -0
  213. package/src/ts/ts-rs/Notation.ts +11 -0
  214. package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
  215. package/src/ts/ts-rs/PanelViewerConfig.ts +48 -2
  216. package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
  217. package/src/ts/ts-rs/RestoreOptions.ts +20 -0
  218. package/src/ts/ts-rs/RoundingMode.ts +3 -0
  219. package/src/ts/ts-rs/RoundingPriority.ts +3 -0
  220. package/src/ts/ts-rs/SaveWorkspaceOptions.ts +11 -0
  221. package/src/ts/ts-rs/SignDisplay.ts +3 -0
  222. package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
  223. package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
  224. package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
  225. package/src/ts/ts-rs/StringColorMode.ts +3 -0
  226. package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
  227. package/src/ts/ts-rs/Unit.ts +3 -0
  228. package/src/ts/ts-rs/UnitDisplay.ts +3 -0
  229. package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
  230. package/src/ts/ts-rs/UseGrouping.ts +3 -0
  231. package/src/ts/ts-rs/ViewerConfig.ts +48 -2
  232. package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
  233. package/src/ts/ts-rs/ViewerConfigUpdate.ts +72 -2
  234. package/src/ts/ts-rs/WindowSpec.ts +3 -2
  235. package/src/ts/ts-rs/WorkspaceConfig.ts +8 -2
  236. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +18 -9
  237. package/dist/esm/ts-rs/WindowAggregate.d.ts +0 -1
  238. package/src/rust/components/form/color_range_selector.rs +0 -137
  239. package/src/ts/ts-rs/WindowAggregate.ts +0 -3
  240. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline0.js +0 -0
  241. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline1.js +0 -0
  242. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline2.js +0 -0
  243. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline3.js +0 -0
  244. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline4.js +0 -0
  245. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline5.js +0 -0
@@ -29,6 +29,8 @@ use wasm_bindgen_futures::JsFuture;
29
29
  use web_sys::HtmlElement;
30
30
  use yew::Callback;
31
31
 
32
+ #[cfg(feature = "llm-agent")]
33
+ use crate::agent::AgentRuntime;
32
34
  use crate::components::viewer::{PerspectiveViewerMsg, PerspectiveViewerProps};
33
35
  use crate::config::*;
34
36
  use crate::custom_events::*;
@@ -59,6 +61,16 @@ extern "C" {
59
61
  #[wasm_bindgen(typescript_type = "PanelOptions")]
60
62
  pub type JsPanelOptions;
61
63
 
64
+ /// `restore()` options dict
65
+ /// (`{ panel?: string, suppress_errors?: boolean }`).
66
+ #[wasm_bindgen(typescript_type = "RestoreOptions")]
67
+ pub type JsRestoreOptions;
68
+
69
+ /// `addPanel()` argument: a new panel's initial config — `table`
70
+ /// REQUIRED.
71
+ #[wasm_bindgen(typescript_type = "ViewerConfigInitial")]
72
+ pub type JsViewerConfigInitial;
73
+
62
74
  /// `download`/`export`/`copy` options dict
63
75
  /// (`{ method?: ExportMethod, panel?: string }`).
64
76
  #[wasm_bindgen(typescript_type = "ExportOptions")]
@@ -72,11 +84,15 @@ extern "C" {
72
84
  #[wasm_bindgen(typescript_type = "GetClientOptions")]
73
85
  pub type JsGetClientOptions;
74
86
 
75
- /// `restoreWorkspace()` argument: a whole-element config update.
87
+ /// `restoreWorkspace()` argument: a workspace config update.
76
88
  #[wasm_bindgen(typescript_type = "WorkspaceConfigUpdate")]
77
89
  pub type JsWorkspaceConfigUpdate;
78
90
 
79
- /// `saveWorkspace()` return: a whole-element config.
91
+ /// `saveWorkspace()` options dict (`{ full_palette?: boolean }`).
92
+ #[wasm_bindgen(typescript_type = "SaveWorkspaceOptions")]
93
+ pub type JsSaveWorkspaceOptions;
94
+
95
+ /// `saveWorkspace()` return: a workspace config.
80
96
  #[wasm_bindgen(typescript_type = "Promise<WorkspaceConfig>")]
81
97
  pub type JsWorkspaceConfigPromise;
82
98
 
@@ -189,7 +205,8 @@ impl PerspectiveViewerElement {
189
205
  .map(|el| el.unchecked_into())
190
206
  }
191
207
 
192
- async fn workspace_config(this: Self) -> ApiResult<JsValue> {
208
+ async fn workspace_config(this: Self, full_palette: bool) -> ApiResult<JsValue> {
209
+ this.workspace.effects().settle().await;
193
210
  let mut panels: std::collections::BTreeMap<String, PanelViewerConfig> = Default::default();
194
211
  for id in &this.workspace.panel_ids() {
195
212
  let panel = this.workspace.panel(id).into_apierror()?;
@@ -204,6 +221,26 @@ impl PerspectiveViewerElement {
204
221
  panels.insert(id.as_str().to_owned(), config.panel);
205
222
  }
206
223
 
224
+ let mut palette = palette_set(&this.workspace, &this.presentation);
225
+ let mut referenced = std::collections::BTreeSet::new();
226
+ for (id, item) in styles_in_use(&this.workspace) {
227
+ let Some(name) = palette_name_for(&palette, item.kind, &item.literal) else {
228
+ continue;
229
+ };
230
+
231
+ if let Some(entry) = panels
232
+ .get_mut(id.as_str())
233
+ .and_then(|panel| panel.columns_config.get_mut(&item.column))
234
+ {
235
+ entry.insert(item.key, serde_json::Value::String(format_var_ref(&name)));
236
+ referenced.insert(name);
237
+ }
238
+ }
239
+
240
+ if !full_palette {
241
+ palette.retain(|name, _| referenced.contains(name));
242
+ }
243
+
207
244
  let active = this
208
245
  .presentation
209
246
  .is_settings_open()
@@ -228,6 +265,7 @@ impl PerspectiveViewerElement {
228
265
  .iter()
229
266
  .map(|id| id.as_str().to_owned())
230
267
  .collect(),
268
+ palette,
231
269
  })?)
232
270
  }
233
271
  }
@@ -509,7 +547,7 @@ impl PerspectiveViewerElement {
509
547
  renderer
510
548
  .clone()
511
549
  .render_task(|guard| async move {
512
- renderer.set_default_theme(presentation.get_default_theme_name().await);
550
+ seed_panel_theme(&presentation, &renderer).await;
513
551
  renderer.stamp_theme(None);
514
552
  let jstable = JsFuture::from(promise)
515
553
  .await
@@ -524,7 +562,7 @@ impl PerspectiveViewerElement {
524
562
  };
525
563
 
526
564
  if let Some(notify) = &notify {
527
- place_reserved(&workspace, notify);
565
+ place_reserved(&workspace, notify, true);
528
566
  }
529
567
 
530
568
  let _plugin = renderer.ensure_plugin_selected()?;
@@ -582,7 +620,35 @@ impl PerspectiveViewerElement {
582
620
  // commits already applied live (`commit_view_config`).
583
621
  let owned_window = session.take_pending_load(generation).is_some();
584
622
  let discard = if owned_window && notify.is_some() {
585
- workspace.take_reserved()
623
+ match workspace.take_reserved() {
624
+ Some(panel) => Some((panel, None)),
625
+ // The one-shot claim read: a table-less
626
+ // `restore` claimed the reservation, and no
627
+ // table has bound nor is pending — evict the
628
+ // panel `CREATE_REQUIRES_TABLE` forbids.
629
+ None if workspace
630
+ .resolve_claim()
631
+ .is_some_and(|has_table| !has_table)
632
+ && session.get_table().is_none()
633
+ && session.pending_table().is_none() =>
634
+ {
635
+ let evicted = renderer
636
+ .slot_name()
637
+ .map(PanelId::from)
638
+ .and_then(|id| workspace.remove_panel(&id));
639
+
640
+ if evicted.is_some()
641
+ && let Some(notify) = &notify
642
+ {
643
+ notify.emit(());
644
+ }
645
+
646
+ evicted.map(|panel| {
647
+ (panel, Some(ApiError::new(CREATE_REQUIRES_TABLE)))
648
+ })
649
+ },
650
+ None => None,
651
+ }
586
652
  } else {
587
653
  None
588
654
  };
@@ -601,13 +667,19 @@ impl PerspectiveViewerElement {
601
667
  Err(e) => {
602
668
  session.take_pending_load(generation);
603
669
  if let Some(notify) = &notify {
604
- place_reserved(&workspace, notify);
670
+ place_reserved(&workspace, notify, true);
605
671
  }
606
672
 
607
673
  session.set_error(false, e.clone()).await?;
608
674
  Err(e)
609
675
  },
610
- Ok(Some(panel)) => eject_panel(panel).await,
676
+ Ok(Some((panel, error))) => {
677
+ eject_panel(panel).await?;
678
+ match error {
679
+ Some(e) => Err(e),
680
+ None => Ok(()),
681
+ }
682
+ },
611
683
  Ok(None) => Ok(()),
612
684
  }
613
685
  }))
@@ -919,15 +991,18 @@ impl PerspectiveViewerElement {
919
991
  /// optional `{panel}` selector.
920
992
  ///
921
993
  /// If `panel` names no existing panel, a NEW panel is created with that id
922
- /// and the config restored into it (an upsert), equivalent to
923
- /// [`Self::addPanel`] but with a caller-chosen id. As with a created panel,
924
- /// the element-level `settings`/`theme` fields are ignored in that case.
994
+ /// and the config restored into it (an upsert). Creation REQUIRES a
995
+ /// `table` — the same rule [`Self::addPanel`] enforces in its argument
996
+ /// type — and a would-create call without one REJECTS before any state
997
+ /// (including `settings`) is applied: with no panel to target and no
998
+ /// `table`, the patch has no data arrival path. In particular, on an
999
+ /// element with zero panels every `restore` must carry a `table`.
925
1000
  ///
926
1001
  /// On an empty element with a pending [`Self::load`] whose payload is not
927
1002
  /// yet classified, the active-target form (no `panel`) instead claims and
928
1003
  /// restores into that load's reserved first panel — see [`Self::load`].
929
1004
  ///
930
- /// This restores a SINGLE panel; a whole-element config (with a `panels`
1005
+ /// This restores a SINGLE panel; a workspace config (with a `panels`
931
1006
  /// map) must be applied via [`Self::restoreWorkspace`] — its `panels` /
932
1007
  /// `layout` keys are ignored here.
933
1008
  ///
@@ -940,7 +1015,15 @@ impl PerspectiveViewerElement {
940
1015
  ///
941
1016
  /// - `update` - The config to restore to, as returned by [`Self::save`] in
942
1017
  /// either "json", "string" or "arraybuffer" format.
943
- /// - `name` - The panel to target, or `None` for the active panel.
1018
+ /// - `options.panel` - The panel to target, or the active panel when
1019
+ /// omitted.
1020
+ /// - `options.suppress_errors` - when `true`, a failed restore only rejects
1021
+ /// the returned `Promise`; the error is NOT committed to the viewer's
1022
+ /// visible error state and the session remains usable. The view config is
1023
+ /// rolled back to its pre-call value, so a rejected patch cannot re-merge
1024
+ /// into a later restore. Element-level state the call already applied
1025
+ /// (theme, title, a plugin swap) is NOT undone — restore a known-good
1026
+ /// config to recover those exactly.
944
1027
  ///
945
1028
  /// # JavaScript Examples
946
1029
  ///
@@ -960,73 +1043,113 @@ impl PerspectiveViewerElement {
960
1043
  pub fn restore(
961
1044
  &self,
962
1045
  update: JsViewerConfigUpdate,
963
- options: Option<JsPanelOptions>,
1046
+ options: Option<JsRestoreOptions>,
964
1047
  ) -> JsVoidPromise {
965
- let PanelOptions { panel: name } = parse_options(options);
1048
+ let RestoreOptions {
1049
+ panel: name,
1050
+ suppress_errors,
1051
+ } = parse_options(options);
1052
+
1053
+ let errors = if suppress_errors.unwrap_or_default() {
1054
+ RestoreErrors::Suppress
1055
+ } else {
1056
+ RestoreErrors::Publish
1057
+ };
1058
+
966
1059
  let effect = self.workspace.effects().guard();
967
1060
  let this = self.clone();
968
1061
  let fut = ApiFuture::new_throttled(async move {
969
1062
  let _effect = effect;
970
1063
  let id = name.map(PanelId::from);
971
- let update = ViewerConfigUpdate::decode(&update)?;
972
- match this.workspace.panel_or_active(id.as_ref()) {
1064
+ let mut update = ViewerConfigUpdate::decode(&update)?;
1065
+ let settings = std::mem::replace(&mut update.settings, OptionalUpdate::Missing);
1066
+ enum Target {
1067
+ Existing { panel: Panel, active: bool },
1068
+ Claimed(Panel),
1069
+ Create(Box<ViewerConfigInitial>),
1070
+ }
1071
+
1072
+ let notify = this.layout_changed_notify();
1073
+ let target = match this.workspace.panel_or_active(id.as_ref()) {
973
1074
  // An existing (or the active) panel — update it in place.
974
1075
  Some(panel) => {
975
1076
  let active = this.workspace.active_id().as_ref() == Some(&panel.id);
1077
+ Target::Existing { panel, active }
1078
+ },
1079
+ None => {
1080
+ let has_table = matches!(&update.table, OptionalUpdate::Update(_));
1081
+ match id
1082
+ .is_none()
1083
+ .then(|| place_reserved(&this.workspace, &notify, has_table))
1084
+ .flatten()
1085
+ {
1086
+ Some(panel) => Target::Claimed(panel),
1087
+ None => Target::Create(Box::new(ViewerConfigInitial::try_from(
1088
+ std::mem::take(&mut update),
1089
+ )?)),
1090
+ }
1091
+ },
1092
+ };
1093
+
1094
+ if !matches!(settings, OptionalUpdate::Missing) {
1095
+ // Through `ToggleSettingsInit` — the SAME full choreography
1096
+ // the toolbar toggle drives (presize every visible plugin
1097
+ // to its post-toggle box, then the exactness-finalizer
1098
+ // resize) — NOT the bare `ToggleSettingsComplete` leaf,
1099
+ // which only re-renders the pane. The pane is the outer
1100
+ // `SplitPanel` (no `before-resize` event) and the host box
1101
+ // is unchanged, so a leaf-only toggle left every canvas
1102
+ // plugin CSS-stretched at its old backing size.
1103
+ //
1104
+ // No `set_settings_before_open` here: `is_settings_open` is
1105
+ // Init's toggle-vs-no-op DISPATCH state, so pre-writing the
1106
+ // target makes every call resolve as a no-op. Init owns the
1107
+ // write, as it does for the toolbar and `toggleConfig`.
1108
+ let (sender, receiver) = channel::<ApiResult<JsValue>>();
1109
+ this.root.borrow().as_ref().into_apierror()?.send_message(
1110
+ PerspectiveViewerMsg::ToggleSettingsInit(Some(settings), false, Some(sender)),
1111
+ );
1112
+
1113
+ receiver.await.map_err(|_| ApiError::new("Cancelled"))??;
1114
+ }
1115
+
1116
+ match target {
1117
+ Target::Existing { panel, active } => {
976
1118
  restore_panel(
977
1119
  &panel.session,
978
1120
  &panel.renderer,
979
1121
  &this.presentation,
980
1122
  &this.workspace,
981
- Some(&this.root),
982
1123
  RestoreMode::Existing { active },
983
1124
  update,
1125
+ errors,
984
1126
  )
985
1127
  .await
986
1128
  },
987
- // No existing panel matched. The active-target form
988
- // (`panel: None`) CLAIMS a pending `load()`'s reserved panel
989
- // — placing it and restoring into it — so a `restore` fired
990
- // right after an unawaited `load(promise)` configures the
991
- // panel that load's payload will bind, per the call-site
992
- // ordering contract in [`Self::load`]. Named upserts and
993
- // reservation-less elements create a fresh panel instead,
994
- // routing through the shared `create_panel`
995
- // (`RestoreMode::Fresh`) pipeline so the new panel's id is
996
- // the requested `panel`.
997
- None => {
998
- let notify = this.layout_changed_notify();
999
- let claimed = id
1000
- .is_none()
1001
- .then(|| place_reserved(&this.workspace, &notify))
1002
- .flatten();
1003
- match claimed {
1004
- Some(panel) => {
1005
- restore_panel(
1006
- &panel.session,
1007
- &panel.renderer,
1008
- &this.presentation,
1009
- &this.workspace,
1010
- Some(&this.root),
1011
- RestoreMode::Existing { active: true },
1012
- update,
1013
- )
1014
- .await
1015
- },
1016
- None => {
1017
- create_panel(
1018
- &this.elem,
1019
- &this.presentation,
1020
- &this.workspace,
1021
- &notify,
1022
- id,
1023
- update,
1024
- None,
1025
- )
1026
- .await?;
1027
- Ok(())
1028
- },
1029
- }
1129
+ Target::Claimed(panel) => {
1130
+ restore_panel(
1131
+ &panel.session,
1132
+ &panel.renderer,
1133
+ &this.presentation,
1134
+ &this.workspace,
1135
+ RestoreMode::Existing { active: true },
1136
+ update,
1137
+ errors,
1138
+ )
1139
+ .await
1140
+ },
1141
+ Target::Create(config) => {
1142
+ create_panel(
1143
+ &this.elem,
1144
+ &this.presentation,
1145
+ &this.workspace,
1146
+ &notify,
1147
+ id,
1148
+ *config,
1149
+ None,
1150
+ )
1151
+ .await?;
1152
+ Ok(())
1030
1153
  },
1031
1154
  }
1032
1155
  });
@@ -1034,8 +1157,8 @@ impl PerspectiveViewerElement {
1034
1157
  js_sys::Promise::from(fut).unchecked_into()
1035
1158
  }
1036
1159
 
1037
- /// Restore the ENTIRE element from a whole-element
1038
- /// [`WorkspaceConfigUpdate`] (`{version, active?, layout, panels, ...}`) —
1160
+ /// Restore the ENTIRE element from a [`WorkspaceConfigUpdate`]
1161
+ /// (`{version, active?, layout, panels, ...}`) —
1039
1162
  /// the multi-panel counterpart of [`Self::restore`]. Every existing panel
1040
1163
  /// is replaced by the `panels` entries, and the layout tree + master/detail
1041
1164
  /// cross-filter state re-applied. Unlike [`Self::restore`], this never
@@ -1063,9 +1186,9 @@ impl PerspectiveViewerElement {
1063
1186
  &renderer,
1064
1187
  &presentation,
1065
1188
  &workspace,
1066
- None,
1067
1189
  RestoreMode::Fresh,
1068
1190
  config,
1191
+ crate::tasks::RestoreErrors::Publish,
1069
1192
  )
1070
1193
  .await?;
1071
1194
  if workspace.is_master(&id) {
@@ -1144,6 +1267,7 @@ impl PerspectiveViewerElement {
1144
1267
  let PanelOptions { panel: name } = parse_options(options);
1145
1268
  let this = self.clone();
1146
1269
  let fut = ApiFuture::new(async move {
1270
+ this.workspace.effects().settle().await;
1147
1271
  let panel = this.resolve_panel(name)?;
1148
1272
  let viewer_config = panel
1149
1273
  .renderer
@@ -1159,11 +1283,12 @@ impl PerspectiveViewerElement {
1159
1283
  js_sys::Promise::from(fut).unchecked_into()
1160
1284
  }
1161
1285
 
1162
- /// Save the ENTIRE element to a whole-element [`WorkspaceConfig`]
1163
- /// (`{version, active?, layout, panels, ...}`) — the multi-panel
1164
- /// counterpart of [`Self::save`]. Unlike [`Self::save`] (which emits a
1165
- /// single `ViewerConfig` for one panel), this ALWAYS emits the
1166
- /// whole-element format, restorable via [`Self::restoreWorkspace`].
1286
+ /// Save the ENTIRE element to a [`WorkspaceConfig`]
1287
+ /// (`{version, active?, layout, panels, palette?, ...}`) — the
1288
+ /// multi-panel counterpart of [`Self::save`]. Unlike [`Self::save`]
1289
+ /// (which emits a single `ViewerConfig` for one panel), this ALWAYS
1290
+ /// emits the workspace format, restorable via
1291
+ /// [`Self::restoreWorkspace`].
1167
1292
  ///
1168
1293
  /// # JavaScript Examples
1169
1294
  ///
@@ -1171,9 +1296,13 @@ impl PerspectiveViewerElement {
1171
1296
  /// const token = await viewer.saveWorkspace();
1172
1297
  /// await viewer.restoreWorkspace(token);
1173
1298
  /// ```
1174
- pub fn saveWorkspace(&self) -> JsWorkspaceConfigPromise {
1299
+ pub fn saveWorkspace(
1300
+ &self,
1301
+ options: Option<JsSaveWorkspaceOptions>,
1302
+ ) -> JsWorkspaceConfigPromise {
1303
+ let SaveWorkspaceOptions { full_palette } = parse_options(options);
1175
1304
  let this = self.clone();
1176
- let fut = ApiFuture::new(Self::workspace_config(this));
1305
+ let fut = ApiFuture::new(Self::workspace_config(this, full_palette.unwrap_or(false)));
1177
1306
  js_sys::Promise::from(fut).unchecked_into()
1178
1307
  }
1179
1308
 
@@ -1303,9 +1432,10 @@ impl PerspectiveViewerElement {
1303
1432
  /// Reset a panel's `ViewerConfig` to its data-relative default.
1304
1433
  ///
1305
1434
  /// Without a `panel`, this is ELEMENT-LEVEL: EVERY panel is reset and the
1306
- /// cross-filter overlay cleared (symmetric with whole-element
1307
- /// [`Self::save`] / [`Self::restore`]). With `{panel}`, only that panel is
1308
- /// reset — the other panels and the overlay are left untouched.
1435
+ /// cross-filter overlay cleared (symmetric with
1436
+ /// [`Self::saveWorkspace`] / [`Self::restoreWorkspace`]). With `{panel}`,
1437
+ /// only that panel is reset — the other panels and the overlay are left
1438
+ /// untouched.
1309
1439
  ///
1310
1440
  /// # Arguments
1311
1441
  ///
@@ -1556,17 +1686,15 @@ impl PerspectiveViewerElement {
1556
1686
  /// ```
1557
1687
  #[wasm_bindgen]
1558
1688
  pub fn restyleElement(&self) -> ApiFuture<JsValue> {
1559
- clone!(self.workspace, self.presentation);
1689
+ clone!(self.workspace);
1560
1690
  let effect = workspace.effects().guard();
1561
1691
  ApiFuture::new(async move {
1562
1692
  let _effect = effect;
1563
- let default = presentation.get_default_theme_name().await;
1564
1693
  for panel in workspace
1565
1694
  .panel_ids()
1566
1695
  .into_iter()
1567
1696
  .filter_map(|id| workspace.panel(&id))
1568
1697
  {
1569
- panel.renderer.set_default_theme(default.clone());
1570
1698
  panel.renderer.restyle_all().await?;
1571
1699
  }
1572
1700
 
@@ -1574,6 +1702,21 @@ impl PerspectiveViewerElement {
1574
1702
  })
1575
1703
  }
1576
1704
 
1705
+ #[wasm_bindgen]
1706
+ pub fn getThemes(&self) -> ApiFuture<JsValue> {
1707
+ clone!(self.presentation);
1708
+ ApiFuture::new(async move {
1709
+ let x = presentation
1710
+ .get_available_themes()
1711
+ .await?
1712
+ .iter()
1713
+ .cloned()
1714
+ .collect::<Vec<_>>();
1715
+
1716
+ Ok(JsValue::from(x))
1717
+ })
1718
+ }
1719
+
1577
1720
  /// Set the available theme names available in the status bar UI.
1578
1721
  ///
1579
1722
  /// Calling [`Self::resetThemes`] may cause the current theme to switch,
@@ -1594,46 +1737,41 @@ impl PerspectiveViewerElement {
1594
1737
  let effect = workspace.effects().guard();
1595
1738
  ApiFuture::new(async move {
1596
1739
  let _effect = effect;
1597
- let themes: Option<Vec<String>> = themes
1598
- .unwrap_or_default()
1599
- .iter()
1600
- .map(|x| x.as_string())
1601
- .collect();
1740
+ // `None` (re-parse the document) must survive the conversion —
1741
+ // mapping BEFORE defaulting is what keeps that branch reachable
1742
+ // from JavaScript at all.
1743
+ let themes: Option<Vec<String>> = match themes {
1744
+ None => None,
1745
+ Some(themes) => themes.iter().map(|x| x.as_string()).collect(),
1746
+ };
1602
1747
 
1603
- let theme_name = presentation.get_selected_theme_name().await;
1604
- presentation.reset_available_themes(themes).await;
1748
+ let previous = presentation.active_theme_name_sync();
1749
+ let active = presentation.reset_themes(themes).await?;
1605
1750
  let available = presentation.get_available_themes().await?;
1606
- let reset_theme = available
1607
- .iter()
1608
- .find(|y| theme_name.as_ref() == Some(y))
1609
- .cloned();
1610
-
1611
- presentation.set_theme_name(reset_theme.as_deref()).await?;
1612
- let new_default = presentation.get_default_theme_name().await;
1613
1751
  for panel in workspace
1614
1752
  .panel_ids()
1615
1753
  .into_iter()
1616
1754
  .filter_map(|id| workspace.panel(&id))
1617
1755
  {
1618
- // Availability applies to PANELS too: a panel pinned to a
1619
- // theme outside the new set follows the host — clear the pin
1620
- // so it renders (and `save`s) the new registry default, the
1621
- // same keep-if-available-else-default rule applied to the
1622
- // host selection above.
1623
- if panel
1624
- .renderer
1625
- .theme()
1626
- .is_some_and(|t| !available.contains(&t))
1627
- {
1628
- panel.renderer.set_theme(None);
1629
- }
1630
-
1631
- panel.renderer.set_default_theme(new_default.clone());
1632
- if panel.renderer.needs_restyle() {
1633
- panel.renderer.restyle_all().await?;
1756
+ let theme = panel.renderer.theme();
1757
+
1758
+ // A panel follows the host only when it was TRACKING it (it
1759
+ // holds the host's previous theme, which is how every panel
1760
+ // born without an explicit one starts — and what keeps the
1761
+ // active panel and the host in agreement), or when its own
1762
+ // theme has left the registry. A panel explicitly set to a
1763
+ // different, still-available theme is untouched: re-ordering
1764
+ // alone must repaint nothing.
1765
+ let stale = theme.as_ref().is_none_or(|x| !available.contains(x));
1766
+ if (stale || theme == previous) && theme != active {
1767
+ panel.renderer.set_theme(active.clone());
1768
+ if panel.renderer.needs_restyle() {
1769
+ panel.renderer.restyle_all().await?;
1770
+ }
1634
1771
  }
1635
1772
  }
1636
1773
 
1774
+ presentation.publish_theme_config().await?;
1637
1775
  Ok(JsValue::UNDEFINED)
1638
1776
  })
1639
1777
  }
@@ -1688,7 +1826,7 @@ impl PerspectiveViewerElement {
1688
1826
  let force = force.map(SettingsUpdate::Update);
1689
1827
  let (sender, receiver) = channel::<ApiResult<wasm_bindgen::JsValue>>();
1690
1828
  root.borrow().as_ref().into_apierror()?.send_message(
1691
- PerspectiveViewerMsg::ToggleSettingsInit(force, Some(sender)),
1829
+ PerspectiveViewerMsg::ToggleSettingsInit(force, true, Some(sender)),
1692
1830
  );
1693
1831
 
1694
1832
  receiver.await.map_err(|_| JsValue::from("Cancelled"))?
@@ -1726,27 +1864,30 @@ impl PerspectiveViewerElement {
1726
1864
  }
1727
1865
 
1728
1866
  /// Add a new, independent panel to this viewer's layout, rendering the
1729
- /// supplied [`ViewerConfigUpdate`] into it. The panel uses the default
1730
- /// [`perspective_client::Client`] (the first passed to [`Self::load`]) to
1731
- /// resolve its `table`. Returns the generated panel id.
1732
- ///
1733
- /// Element-level config fields (`settings`, `theme`) in the argument are
1734
- /// ignored — those are shared across the element, not per-panel.
1867
+ /// supplied [`ViewerConfigInitial`] into it. Unlike [`Self::restore`]'s
1868
+ /// update-shaped argument, a new panel has no prior state, so `table`
1869
+ /// is REQUIRED — a table-less call rejects before the layout is
1870
+ /// touched. The panel uses the default [`perspective_client::Client`]
1871
+ /// (the first passed to [`Self::load`]) to resolve its `table`. Returns
1872
+ /// the generated panel id.
1873
+ ///
1874
+ /// The element-level `settings` field does not exist on the argument
1875
+ /// type (it is shared across the element, not per-panel).
1735
1876
  #[wasm_bindgen]
1736
- pub fn addPanel(&self, update: JsViewerConfigUpdate) -> ApiFuture<JsValue> {
1877
+ pub fn addPanel(&self, config: JsViewerConfigInitial) -> ApiFuture<JsValue> {
1737
1878
  clone!(self.elem, self.presentation, self.workspace);
1738
1879
  let effect = workspace.effects().guard();
1739
1880
  let notify = self.layout_changed_notify();
1740
1881
  ApiFuture::new(async move {
1741
1882
  let _effect = effect;
1742
- let update = ViewerConfigUpdate::decode(&update)?;
1883
+ let config = ViewerConfigInitial::decode(&config)?;
1743
1884
  let id = create_panel(
1744
1885
  &elem,
1745
1886
  &presentation,
1746
1887
  &workspace,
1747
1888
  &notify,
1748
1889
  None,
1749
- update,
1890
+ config,
1750
1891
  None,
1751
1892
  )
1752
1893
  .await?;
@@ -1844,7 +1985,12 @@ impl PerspectiveViewerElement {
1844
1985
  let _effect = effect;
1845
1986
  let panel = this.resolve_panel(name)?;
1846
1987
  let was_active = this.workspace.active_id().as_ref() == Some(&panel.id);
1847
- let locator = get_column_locator(&panel.session.metadata(), Some(column_name));
1988
+ let target = {
1989
+ let config = panel.session.get_view_config();
1990
+ let metadata = panel.session.metadata();
1991
+ classify_column(&column_name, &config, &metadata)
1992
+ .map(|_| crate::presentation::ColumnSettingsTarget::Column(column_name))
1993
+ };
1848
1994
  if !was_active {
1849
1995
  this.root.borrow().as_ref().into_apierror()?.send_message(
1850
1996
  PerspectiveViewerMsg::SetActivePanel(panel.id.as_str().to_owned(), None),
@@ -1854,7 +2000,7 @@ impl PerspectiveViewerElement {
1854
2000
  let (sender, receiver) = channel::<()>();
1855
2001
  this.root.borrow().as_ref().into_apierror()?.send_message(
1856
2002
  PerspectiveViewerMsg::OpenColumnSettings {
1857
- locator,
2003
+ target,
1858
2004
  sender: Some(sender),
1859
2005
  toggle: was_active,
1860
2006
  },
@@ -1864,3 +2010,107 @@ impl PerspectiveViewerElement {
1864
2010
  })
1865
2011
  }
1866
2012
  }
2013
+
2014
+ #[cfg(feature = "llm-agent")]
2015
+ #[wasm_bindgen]
2016
+ impl PerspectiveViewerElement {
2017
+ /// Configure the embedded LLM agent (see `prompt()`), replacing any prior
2018
+ /// configuration and conversation.
2019
+ ///
2020
+ /// The agent core is provider-agnostic: one OpenAI-chat-completions
2021
+ /// protocol over primitive connection fields. Exactly one of
2022
+ /// `config.url` or `config.engine` is required; the `providers` presets
2023
+ /// exported by this package are plain spreadable collections of these
2024
+ /// fields (`{...providers.anthropic, apiKey}`).
2025
+ ///
2026
+ /// - `config.url` - a full chat-completions endpoint URL (any
2027
+ /// OpenAI-compatible service: Anthropic/Gemini compatibility endpoints,
2028
+ /// LM Studio, Ollama, OpenRouter, a proxy...).
2029
+ /// - `config.engine` - an in-page engine object with an OpenAI-compatible
2030
+ /// `chat.completions.create(request)` method (e.g. WebLLM's `MLCEngine`);
2031
+ /// mutually exclusive with `url`.
2032
+ /// - `config.headers` - extra request headers, sent verbatim.
2033
+ /// - `config.apiKey` - sugar for the `Authorization: Bearer` header.
2034
+ /// - `config.model` - model id sent with each request; local servers and
2035
+ /// engines generally answer with whatever model is loaded.
2036
+ /// - `config.name` - cosmetic label for the chat badge (presets set this).
2037
+ /// - `config.systemPrompt` - extra system-prompt context appended to the
2038
+ /// agent's built-in instructions.
2039
+ /// - `config.maxTurns` - max model turns (tool-call rounds + the final
2040
+ /// answer) per `prompt()` call. Defaults to 16.
2041
+ /// - `config.docs` - the agent metadata bundle, which supplies the
2042
+ /// `search_docs` corpus and the rich tool parameter schemas: the packaged
2043
+ /// `dist/docs/perspective-docs.json` asset as a parsed object (`import
2044
+ /// docs from "…json" with { type: "json" }`), a `fetch()` `Response`, an
2045
+ /// `ArrayBuffer`, a JSON string, or a `Promise` of any of those — and/or
2046
+ /// an inline array of `{title?, text}` entries for host data definitions.
2047
+ /// Omitted, `search_docs` searches an empty corpus and the parameter
2048
+ /// schemas degrade to permissive objects.
2049
+ /// - `config.systemRole` - where the preamble (plus `systemPrompt`) is
2050
+ /// placed: `"system"` (default) or `"user"`. Some engines refuse a system
2051
+ /// message alongside `tools` because they substitute their own — WebLLM's
2052
+ /// Hermes function calling throws `CustomSystemPromptError` on ANY system
2053
+ /// message — and those need `"user"`, which folds the same text into the
2054
+ /// opening user turn.
2055
+ /// - `config.entitlements` - access grants limiting which tools the agent
2056
+ /// is offered (and may call): any of `"read_view"`, `"configure_view"`,
2057
+ /// `"manage_layout"`, `"read_docs"`, `"read_data"`. Omitted, all but
2058
+ /// `"read_data"` are granted; `["read_view", "read_docs"]` yields a
2059
+ /// read-only agent.
2060
+ ///
2061
+ /// # JavaScript Examples
2062
+ ///
2063
+ /// ```javascript
2064
+ /// import { providers } from "@perspective-dev/viewer";
2065
+ ///
2066
+ /// viewer.agentConfig({
2067
+ /// ...providers.anthropic,
2068
+ /// apiKey: "sk-ant-...",
2069
+ /// docs: fetch(
2070
+ /// "node_modules/@perspective-dev/viewer/dist/docs/perspective-docs.json",
2071
+ /// ),
2072
+ /// });
2073
+ /// ```
2074
+ #[wasm_bindgen(js_name = "agentConfig")]
2075
+ pub fn agent_config(&self, config: JsValue) -> ApiResult<()> {
2076
+ let runtime = AgentRuntime::new(&config, self.clone())?;
2077
+ self.presentation.agent.configure(runtime);
2078
+ Ok(())
2079
+ }
2080
+
2081
+ /// Run one conversational turn of the embedded LLM agent (configured via
2082
+ /// `agentConfig()`), resolving with the agent's final text response after
2083
+ /// any tool calls have been applied to this element. Turns share a
2084
+ /// conversation history (and the chat sidebar's transcript) until
2085
+ /// `agentReset()`; a call made while a turn is already running rejects.
2086
+ /// Tool activity is emitted as `perspective-agent-tool` CustomEvents on
2087
+ /// this element.
2088
+ ///
2089
+ /// # JavaScript Examples
2090
+ ///
2091
+ /// ```javascript
2092
+ /// await viewer.agentPrompt("Show me sales by region as a bar chart");
2093
+ /// ```
2094
+ #[wasm_bindgen(js_name = "agentPrompt")]
2095
+ pub fn agent_prompt(&self, prompt: String) -> js_sys::Promise {
2096
+ let presentation = self.presentation.clone();
2097
+ let fut = ApiFuture::new(async move {
2098
+ Ok(JsValue::from(presentation.agent.run_prompt(prompt).await?))
2099
+ });
2100
+
2101
+ js_sys::Promise::from(fut)
2102
+ }
2103
+
2104
+ /// Clear the agent's conversation (history and chat transcript), keeping
2105
+ /// its configuration. Cancels any in-flight turn.
2106
+ #[wasm_bindgen(js_name = "agentReset")]
2107
+ pub fn agent_reset(&self) -> js_sys::Promise {
2108
+ let presentation = self.presentation.clone();
2109
+ let fut = ApiFuture::new(async move {
2110
+ presentation.agent.reset().await;
2111
+ Ok(JsValue::UNDEFINED)
2112
+ });
2113
+
2114
+ js_sys::Promise::from(fut)
2115
+ }
2116
+ }