@perspective-dev/viewer 5.0.0 → 5.2.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 (213) 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 +2 -0
  26. package/dist/esm/perspective-viewer.inline.js +8 -8
  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/providers.d.ts +79 -0
  31. package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
  32. package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
  33. package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
  34. package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
  35. package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
  36. package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
  37. package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
  38. package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
  39. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
  40. package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
  41. package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
  42. package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
  43. package/dist/esm/ts-rs/Notation.d.ts +17 -0
  44. package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
  45. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +43 -1
  46. package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
  47. package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
  48. package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
  49. package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
  50. package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
  51. package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
  52. package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
  53. package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
  54. package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
  55. package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
  56. package/dist/esm/ts-rs/Unit.d.ts +1 -0
  57. package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
  58. package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
  59. package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
  60. package/dist/esm/ts-rs/ViewerConfig.d.ts +43 -1
  61. package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
  62. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +72 -0
  63. package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
  64. package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
  65. package/dist/esm/ts-rs/WindowSpec.d.ts +47 -0
  66. package/dist/esm/ts-rs/Windows.d.ts +10 -0
  67. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +7 -6
  68. package/dist/wasm/perspective-viewer.d.ts +222 -26
  69. package/dist/wasm/perspective-viewer.js +378 -125
  70. package/dist/wasm/perspective-viewer.wasm +0 -0
  71. package/dist/wasm/perspective-viewer.wasm.d.ts +18 -15
  72. package/package.json +2 -1
  73. package/src/css/_viewer_bundle.css +1 -0
  74. package/src/css/column-selector.css +9 -0
  75. package/src/css/column-settings-panel.css +67 -2
  76. package/src/css/column-style.css +4 -3
  77. package/src/css/config-selector.css +60 -4
  78. package/src/css/containers/context-menu.css +12 -6
  79. package/src/css/containers/tabs.css +2 -2
  80. package/src/css/form/code-editor.css +2 -3
  81. package/src/css/form/mirrored-textarea.css +81 -0
  82. package/src/css/viewer.css +336 -0
  83. package/src/rust/agent/client/mod.rs +19 -0
  84. package/src/rust/agent/client/protocol.rs +320 -0
  85. package/src/rust/agent/client/transport.rs +287 -0
  86. package/src/rust/agent/client/turn.rs +174 -0
  87. package/src/rust/agent/config.rs +159 -0
  88. package/src/rust/agent/docs.rs +293 -0
  89. package/src/rust/agent/markdown/block.rs +509 -0
  90. package/src/rust/agent/markdown/entity.rs +52 -0
  91. package/src/rust/agent/markdown/events.rs +71 -0
  92. package/src/rust/agent/markdown/inline.rs +497 -0
  93. package/src/rust/agent/markdown/mod.rs +466 -0
  94. package/src/rust/agent/mod.rs +23 -0
  95. package/src/rust/agent/preamble.md +16 -0
  96. package/src/rust/agent/runtime.rs +201 -0
  97. package/src/rust/agent/slot.rs +236 -0
  98. package/src/rust/agent/tools.rs +1110 -0
  99. package/src/rust/components/chat_panel.rs +289 -0
  100. package/src/rust/components/column_selector/active_column.rs +21 -27
  101. package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
  102. package/src/rust/components/column_selector/config_selector.rs +83 -8
  103. package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
  104. package/src/rust/components/column_selector/inactive_column.rs +8 -1
  105. package/src/rust/components/column_selector.rs +27 -3
  106. package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
  107. package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
  108. package/src/rust/components/column_settings_sidebar.rs +163 -16
  109. package/src/rust/components/containers/dragdrop_list.rs +110 -3
  110. package/src/rust/components/containers/sidebar.rs +99 -22
  111. package/src/rust/components/containers/split_panel.rs +6 -0
  112. package/src/rust/components/editable_header.rs +2 -0
  113. package/src/rust/components/form/code_editor.rs +14 -23
  114. package/src/rust/components/form/debug.rs +8 -5
  115. package/src/rust/components/form/mirrored_textarea.rs +165 -0
  116. package/src/rust/components/form/mod.rs +1 -0
  117. package/src/rust/components/main_panel.rs +5 -0
  118. package/src/rust/components/mod.rs +3 -0
  119. package/src/rust/components/panel_menu.rs +7 -0
  120. package/src/rust/components/panel_tab.rs +5 -0
  121. package/src/rust/components/portal.rs +7 -0
  122. package/src/rust/components/settings_panel.rs +80 -2
  123. package/src/rust/components/viewer/msg.rs +2 -0
  124. package/src/rust/components/viewer/panels.rs +9 -14
  125. package/src/rust/components/viewer/render.rs +14 -0
  126. package/src/rust/components/viewer/settings.rs +33 -1
  127. package/src/rust/components/viewer.rs +2 -0
  128. package/src/rust/components/window_editor.rs +985 -0
  129. package/src/rust/config/datetime_column_style/custom.rs +17 -0
  130. package/src/rust/config/datetime_column_style/simple.rs +10 -0
  131. package/src/rust/config/number_string_format.rs +33 -0
  132. package/src/rust/config/options.rs +18 -0
  133. package/src/rust/config/plugin_static_config.rs +120 -5
  134. package/src/rust/config/viewer_config.rs +220 -1
  135. package/src/rust/config/workspace_config.rs +7 -6
  136. package/src/rust/custom_elements/viewer.rs +209 -18
  137. package/src/rust/custom_events.rs +29 -0
  138. package/src/rust/lib.rs +17 -1
  139. package/src/rust/presentation/column_locator.rs +16 -1
  140. package/src/rust/presentation.rs +8 -0
  141. package/src/rust/queries/columns_iter_set.rs +7 -1
  142. package/src/rust/queries/get_viewer_config.rs +5 -1
  143. package/src/rust/renderer/dispatch.rs +85 -58
  144. package/src/rust/renderer.rs +34 -1
  145. package/src/rust/session/column_defaults_update.rs +18 -0
  146. package/src/rust/session/drag_drop_update.rs +18 -0
  147. package/src/rust/session/metadata.rs +64 -9
  148. package/src/rust/session/replace_expression_update.rs +2 -0
  149. package/src/rust/session.rs +7 -5
  150. package/src/rust/tasks/create_panel.rs +18 -8
  151. package/src/rust/tasks/edit_window.rs +155 -0
  152. package/src/rust/tasks/mod.rs +2 -0
  153. package/src/rust/tasks/restore_panel.rs +34 -17
  154. package/src/rust/tasks/sync_update_panels.rs +7 -23
  155. package/src/rust/utils/browser/dragdrop.rs +12 -0
  156. package/src/rust/utils/browser/tests/debounce.rs +49 -0
  157. package/src/rust/utils/debounce.rs +76 -27
  158. package/src/rust/workspace.rs +112 -20
  159. package/src/svg/pin-icon.svg +5 -0
  160. package/src/themes/icons.css +1 -0
  161. package/src/themes/intl/de.css +23 -0
  162. package/src/themes/intl/es.css +23 -0
  163. package/src/themes/intl/fr.css +23 -0
  164. package/src/themes/intl/ja.css +23 -0
  165. package/src/themes/intl/pt.css +23 -0
  166. package/src/themes/intl/zh.css +23 -0
  167. package/src/themes/intl.css +26 -1
  168. package/src/ts/column-format.ts +67 -46
  169. package/src/ts/perspective-viewer.ts +2 -0
  170. package/src/ts/providers.ts +96 -0
  171. package/src/ts/ts-rs/CompactDisplay.ts +3 -0
  172. package/src/ts/ts-rs/CurrencyCode.ts +3 -0
  173. package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
  174. package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
  175. package/src/ts/ts-rs/CurrencySign.ts +3 -0
  176. package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
  177. package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
  178. package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
  179. package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
  180. package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
  181. package/src/ts/ts-rs/FormatMode.ts +3 -0
  182. package/src/ts/ts-rs/FormatUnit.ts +3 -0
  183. package/src/ts/ts-rs/Notation.ts +11 -0
  184. package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
  185. package/src/ts/ts-rs/PanelViewerConfig.ts +48 -1
  186. package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
  187. package/src/ts/ts-rs/RestoreOptions.ts +20 -0
  188. package/src/ts/ts-rs/RoundingMode.ts +3 -0
  189. package/src/ts/ts-rs/RoundingPriority.ts +3 -0
  190. package/src/ts/ts-rs/SignDisplay.ts +3 -0
  191. package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
  192. package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
  193. package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
  194. package/src/ts/ts-rs/StringColorMode.ts +3 -0
  195. package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
  196. package/src/ts/ts-rs/Unit.ts +3 -0
  197. package/src/ts/ts-rs/UnitDisplay.ts +3 -0
  198. package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
  199. package/src/ts/ts-rs/UseGrouping.ts +3 -0
  200. package/src/ts/ts-rs/ViewerConfig.ts +48 -1
  201. package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
  202. package/src/ts/ts-rs/ViewerConfigUpdate.ts +81 -2
  203. package/src/ts/ts-rs/WindowSort.ts +9 -0
  204. package/src/ts/ts-rs/WindowSortDir.ts +8 -0
  205. package/src/ts/ts-rs/WindowSpec.ts +47 -0
  206. package/src/ts/ts-rs/Windows.ts +10 -0
  207. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -6
  208. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline0.js +0 -0
  209. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline1.js +0 -0
  210. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline2.js +0 -0
  211. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline3.js +0 -0
  212. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline4.js +0 -0
  213. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline5.js +0 -0
@@ -0,0 +1,293 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ use std::collections::HashMap;
14
+ use std::rc::Rc;
15
+
16
+ use serde::Deserialize;
17
+ use serde_json::Value;
18
+ use wasm_bindgen::prelude::*;
19
+ use wasm_bindgen_futures::JsFuture;
20
+
21
+ /// One retrievable unit. `title`/`path` are optional in host-supplied
22
+ /// entries; oversized `text` is split at paragraph boundaries on ingest so
23
+ /// hosts never need to understand chunking.
24
+ #[derive(Clone, Deserialize)]
25
+ pub struct Chunk {
26
+ #[serde(default)]
27
+ pub title: String,
28
+ #[serde(default)]
29
+ pub path: String,
30
+ pub text: String,
31
+ }
32
+
33
+ const SPLIT_OVER: usize = 2500;
34
+
35
+ /// Words too common to discriminate; deliberately tiny — the caller is a
36
+ /// model that writes keyword-style queries.
37
+ const STOP_WORDS: &[&str] = &["the", "a", "an", "of", "to", "is", "in", "and", "or", "for"];
38
+
39
+ /// Lowercase alphanumeric-and-`_` tokens; identifiers containing `_` also
40
+ /// emit their parts, so `split_by` matches both the exact API token and the
41
+ /// natural-language query "split by".
42
+ fn tokenize(text: &str) -> Vec<String> {
43
+ let mut tokens = vec![];
44
+ for raw in text
45
+ .to_lowercase()
46
+ .split(|x: char| !x.is_alphanumeric() && x != '_')
47
+ {
48
+ if raw.is_empty() || STOP_WORDS.contains(&raw) {
49
+ continue;
50
+ }
51
+
52
+ tokens.push(raw.to_owned());
53
+ if raw.contains('_') {
54
+ for part in raw.split('_').filter(|x| !x.is_empty()) {
55
+ tokens.push(part.to_owned());
56
+ }
57
+ }
58
+ }
59
+
60
+ tokens
61
+ }
62
+
63
+ pub struct DocsIndex {
64
+ chunks: Vec<Chunk>,
65
+ postings: HashMap<String, Vec<(usize, f64)>>,
66
+ lens: Vec<f64>,
67
+ avg_len: f64,
68
+ }
69
+
70
+ impl DocsIndex {
71
+ pub fn build(entries: Vec<Chunk>) -> Self {
72
+ let chunks = normalize(entries);
73
+ let mut postings: HashMap<String, Vec<(usize, f64)>> = HashMap::new();
74
+ let mut lens = Vec::with_capacity(chunks.len());
75
+ for (idx, chunk) in chunks.iter().enumerate() {
76
+ let tokens = tokenize(&format!("{} {}", chunk.title, chunk.text));
77
+ lens.push(tokens.len() as f64);
78
+ let mut counts: HashMap<String, f64> = HashMap::new();
79
+ for token in tokens {
80
+ *counts.entry(token).or_default() += 1.0;
81
+ }
82
+
83
+ for (token, count) in counts {
84
+ postings.entry(token).or_default().push((idx, count));
85
+ }
86
+ }
87
+
88
+ let avg_len = (lens.iter().sum::<f64>() / lens.len().max(1) as f64).max(1.0);
89
+ Self {
90
+ chunks,
91
+ postings,
92
+ lens,
93
+ avg_len,
94
+ }
95
+ }
96
+
97
+ /// BM25 (k1 = 1.2, b = 0.75) top-`limit` chunks for a keyword query.
98
+ pub fn search(&self, query: &str, limit: usize) -> Vec<&Chunk> {
99
+ const K1: f64 = 1.2;
100
+ const B: f64 = 0.75;
101
+ let n = self.chunks.len() as f64;
102
+ let mut terms = tokenize(query);
103
+ terms.sort();
104
+ terms.dedup();
105
+ let mut scores: HashMap<usize, f64> = HashMap::new();
106
+ for term in terms {
107
+ let Some(posting) = self.postings.get(&term) else {
108
+ continue;
109
+ };
110
+
111
+ let df = posting.len() as f64;
112
+ let idf = ((n - df + 0.5) / (df + 0.5) + 1.0).ln();
113
+ for (idx, tf) in posting {
114
+ let norm = K1 * (1.0 - B + B * self.lens[*idx] / self.avg_len);
115
+ *scores.entry(*idx).or_default() += idf * tf / (tf + norm);
116
+ }
117
+ }
118
+
119
+ let mut ranked = scores.into_iter().collect::<Vec<_>>();
120
+ ranked.sort_by(|x, y| y.1.total_cmp(&x.1).then(x.0.cmp(&y.0)));
121
+ ranked
122
+ .into_iter()
123
+ .take(limit)
124
+ .map(|(idx, _)| &self.chunks[idx])
125
+ .collect()
126
+ }
127
+ }
128
+
129
+ /// Default titles from a text prefix; split oversized entries at paragraph
130
+ /// boundaries (a single paragraph longer than the limit stays whole).
131
+ fn normalize(entries: Vec<Chunk>) -> Vec<Chunk> {
132
+ let mut out = vec![];
133
+ for mut chunk in entries {
134
+ if chunk.title.is_empty() {
135
+ chunk.title = chunk.text.chars().take(60).collect::<String>();
136
+ }
137
+
138
+ if chunk.text.len() <= SPLIT_OVER {
139
+ out.push(chunk);
140
+ continue;
141
+ }
142
+
143
+ let mut part = String::new();
144
+ for para in chunk.text.split("\n\n") {
145
+ if !part.is_empty() && part.len() + para.len() > SPLIT_OVER {
146
+ out.push(Chunk {
147
+ text: std::mem::take(&mut part),
148
+ ..chunk.clone()
149
+ });
150
+ }
151
+
152
+ if !part.is_empty() {
153
+ part.push_str("\n\n");
154
+ }
155
+
156
+ part.push_str(para);
157
+ }
158
+
159
+ if !part.is_empty() {
160
+ out.push(Chunk {
161
+ text: part,
162
+ ..chunk
163
+ });
164
+ }
165
+ }
166
+
167
+ out
168
+ }
169
+
170
+ /// One resolved `docs` source: tool-parameter schemas, the searchable
171
+ /// corpus index, and an optional host preamble override.
172
+ pub struct DocsBundle {
173
+ pub schemas: HashMap<String, Value>,
174
+ pub index: DocsIndex,
175
+ pub preamble: Option<String>,
176
+ }
177
+
178
+ /// The two accepted source shapes: a bare entry array (host inline
179
+ /// entries), or the metadata-bundle object.
180
+ #[derive(Deserialize)]
181
+ #[serde(untagged)]
182
+ enum RawDocs {
183
+ Entries(Vec<Chunk>),
184
+ Bundle {
185
+ #[serde(default)]
186
+ schemas: HashMap<String, Value>,
187
+
188
+ #[serde(default)]
189
+ chunks: Vec<Chunk>,
190
+
191
+ #[serde(default)]
192
+ preamble: Option<String>,
193
+ },
194
+ }
195
+
196
+ /// The lazily-resolved, cached bundle for one configured `docs` source.
197
+ /// Owned by `AgentRuntime` (so it persists across `prompt()` calls) and
198
+ /// shared with each turn's `ToolCtx`.
199
+ pub struct DocsCell {
200
+ source: JsValue,
201
+ cache: async_lock::Mutex<Option<Rc<Result<DocsBundle, String>>>>,
202
+ }
203
+
204
+ impl DocsCell {
205
+ pub fn new(source: JsValue) -> Self {
206
+ Self {
207
+ source,
208
+ cache: async_lock::Mutex::new(None),
209
+ }
210
+ }
211
+
212
+ pub async fn bundle(&self) -> Rc<Result<DocsBundle, String>> {
213
+ let mut cache = self.cache.lock().await;
214
+ if let Some(cached) = &*cache {
215
+ return cached.clone();
216
+ }
217
+
218
+ let result = Rc::new(resolve(&self.source).await.map(|raw| {
219
+ let (schemas, chunks, preamble) = match raw {
220
+ RawDocs::Entries(chunks) => (HashMap::new(), chunks, None),
221
+ RawDocs::Bundle {
222
+ schemas,
223
+ chunks,
224
+ preamble,
225
+ } => (schemas, chunks, preamble),
226
+ };
227
+
228
+ DocsBundle {
229
+ schemas,
230
+ index: DocsIndex::build(chunks),
231
+ preamble,
232
+ }
233
+ }));
234
+
235
+ *cache = Some(result.clone());
236
+ result
237
+ }
238
+ }
239
+
240
+ fn js_error(err: JsValue) -> String {
241
+ err.as_string()
242
+ .or_else(|| {
243
+ js_sys::Reflect::get(&err, &JsValue::from_str("message"))
244
+ .ok()
245
+ .and_then(|x| x.as_string())
246
+ })
247
+ .unwrap_or_else(|| format!("{err:?}"))
248
+ }
249
+
250
+ /// Resolve the `docs` source union to a parsed raw bundle.
251
+ async fn resolve(source: &JsValue) -> Result<RawDocs, String> {
252
+ let value = JsFuture::from(js_sys::Promise::resolve(source))
253
+ .await
254
+ .map_err(js_error)?;
255
+
256
+ let text = if js_sys::Array::is_array(&value) {
257
+ js_sys::JSON::stringify(&value)
258
+ .map_err(js_error)?
259
+ .as_string()
260
+ .ok_or_else(|| "Unserializable `docs` array".to_owned())?
261
+ } else if let Some(text) = value.as_string() {
262
+ text
263
+ } else if value.is_instance_of::<web_sys::Response>() {
264
+ let response: web_sys::Response = value.unchecked_into();
265
+ if !response.ok() {
266
+ return Err(format!(
267
+ "HTTP {} {}",
268
+ response.status(),
269
+ response.status_text()
270
+ ));
271
+ }
272
+
273
+ JsFuture::from(response.text().map_err(js_error)?)
274
+ .await
275
+ .map_err(js_error)?
276
+ .as_string()
277
+ .unwrap_or_default()
278
+ } else if value.is_instance_of::<js_sys::Uint8Array>()
279
+ || value.is_instance_of::<js_sys::ArrayBuffer>()
280
+ {
281
+ let bytes = js_sys::Uint8Array::new(&value).to_vec();
282
+ String::from_utf8(bytes).map_err(|x| format!("Docs source is not UTF-8: {x}"))?
283
+ } else if value.is_object() {
284
+ js_sys::JSON::stringify(&value)
285
+ .map_err(js_error)?
286
+ .as_string()
287
+ .ok_or_else(|| "Unserializable `docs` object".to_owned())?
288
+ } else {
289
+ return Err("Unsupported `docs` type".to_owned());
290
+ };
291
+
292
+ serde_json::from_str(&text).map_err(|x| format!("Invalid docs JSON: {x}"))
293
+ }