@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
@@ -303,6 +303,14 @@
303
303
  }
304
304
  }
305
305
 
306
+ #modal_panel.pinned {
307
+ position: static;
308
+ flex: 0 0 auto;
309
+ width: auto;
310
+ pointer-events: all;
311
+ overflow: visible;
312
+ }
313
+
306
314
  #modal_panel > .split-panel-divider {
307
315
  border-left: 1px solid var(--psp-inactive--color, #6e6e6e);
308
316
  margin-right: -5px;
@@ -656,6 +664,10 @@
656
664
  content: var(--psp-label--debug-tab--content);
657
665
  }
658
666
 
667
+ #chat_tabbar_tab:before {
668
+ content: var(--psp-label--chat-tab--content);
669
+ }
670
+
659
671
  label#stack-label:before {
660
672
  content: var(--psp-label--stack--content, "Stack");
661
673
  }
@@ -664,3 +676,327 @@
664
676
  content: var(--psp-label--alt-axis--content, "Alt Axis");
665
677
  }
666
678
  }
679
+
680
+ #chat_panel {
681
+ display: flex;
682
+ flex-direction: column;
683
+ flex: 1 1 auto;
684
+ min-height: 0;
685
+ min-width: 0;
686
+ contain: inline-size;
687
+ padding: 0px;
688
+ gap: 6px;
689
+
690
+ #chat_log:empty {
691
+ flex: 0 0 auto;
692
+ padding-bottom: 0px;
693
+ margin-bottom: -6px; /* offset gap */
694
+ }
695
+
696
+ #chat_log {
697
+ flex: 1 1 auto;
698
+ min-height: 0;
699
+ overflow-y: scroll;
700
+ display: flex;
701
+ flex-direction: column;
702
+ gap: 6px;
703
+ padding: 12px 6px 6px 2px;
704
+ }
705
+
706
+ .chat-message {
707
+ overflow-wrap: anywhere;
708
+ border-radius: 3px;
709
+ padding: 6px 8px;
710
+ max-width: 90%;
711
+ font-size: 1em;
712
+ }
713
+
714
+ /* User/error text is NOT markdown-rendered - preserve its whitespace. */
715
+ .chat-user,
716
+ .chat-error {
717
+ white-space: pre-wrap;
718
+ }
719
+
720
+ .chat-user {
721
+ align-self: flex-end;
722
+ background-color: var(--psp--background-color);
723
+ border: 1px solid var(--psp-inactive--color, #6e6e6e);
724
+ }
725
+
726
+ .chat-assistant {
727
+ align-self: flex-start;
728
+ background-color: #00000010;
729
+ border: 1px solid var(--psp-inactive--color, #6e6e6e);
730
+
731
+ /* Markdown content (see `agent/markdown.rs`). */
732
+ p,
733
+ ul,
734
+ ol,
735
+ blockquote,
736
+ pre,
737
+ table,
738
+ h1,
739
+ h2,
740
+ h3,
741
+ h4,
742
+ h5,
743
+ h6,
744
+ hr {
745
+ margin: 0.4em 0;
746
+
747
+ &:first-child {
748
+ margin-top: 0;
749
+ }
750
+
751
+ &:last-child {
752
+ margin-bottom: 0;
753
+ }
754
+ }
755
+
756
+ h1,
757
+ h2,
758
+ h3,
759
+ h4,
760
+ h5,
761
+ h6 {
762
+ font-size: 1.05em;
763
+ font-weight: 700;
764
+ }
765
+
766
+ ul,
767
+ ol {
768
+ padding-inline-start: 1.5em;
769
+ }
770
+
771
+ code {
772
+ font-family: inherit;
773
+ background-color: #00000028;
774
+ border-radius: 3px;
775
+ padding: 0 3px;
776
+ }
777
+
778
+ pre {
779
+ overflow-x: auto;
780
+ background-color: #00000028;
781
+ border-radius: 3px;
782
+ padding: 6px 8px;
783
+
784
+ code {
785
+ background-color: transparent;
786
+ padding: 0;
787
+ white-space: pre;
788
+ }
789
+ }
790
+
791
+ blockquote {
792
+ border-left: 2px solid var(--psp-inactive--color, #6e6e6e);
793
+ padding-left: 8px;
794
+ color: var(--psp-inactive--color, #6e6e6e);
795
+ }
796
+
797
+ hr {
798
+ border: none;
799
+ border-top: 1px solid var(--psp-inactive--color, #6e6e6e);
800
+ }
801
+
802
+ a {
803
+ color: var(--psp-active--color, #1a7da1);
804
+ }
805
+
806
+ table {
807
+ border-collapse: collapse;
808
+
809
+ th,
810
+ td {
811
+ border: 1px solid var(--psp-inactive--color, #6e6e6e);
812
+ padding: 2px 6px;
813
+ }
814
+
815
+ th {
816
+ font-weight: 700;
817
+ }
818
+ }
819
+
820
+ .chat-md-align-center {
821
+ text-align: center;
822
+ }
823
+
824
+ .chat-md-align-right {
825
+ text-align: right;
826
+ }
827
+ }
828
+
829
+ .chat-error {
830
+ align-self: flex-start;
831
+ color: var(--warning--color, #ff471e);
832
+ border: 1px dashed var(--warning--color, #ff471e);
833
+ }
834
+
835
+ .chat-tool-chip {
836
+ align-self: flex-start;
837
+ font-size: 0.66em;
838
+ text-transform: uppercase;
839
+ color: var(--psp-inactive--color, #6e6e6e);
840
+ border: 1px solid var(--psp-inactive--color, #6e6e6e);
841
+ border-radius: 3px;
842
+ padding: 1px 6px;
843
+ cursor: default;
844
+ }
845
+
846
+ .chat-tool-chip-error {
847
+ color: var(--warning--color, #ff471e);
848
+ border-color: var(--warning--color, #ff471e);
849
+ }
850
+
851
+ .chat-reasoning {
852
+ font-size: 0.9em;
853
+ color: var(--psp-inactive--color, #6e6e6e);
854
+ margin-bottom: 0.4em;
855
+
856
+ summary {
857
+ cursor: pointer;
858
+ user-select: none;
859
+ &:before {
860
+ text-transform: uppercase;
861
+ content: var(--psp-label--chat-reasoning--content);
862
+ font-size: var(--psp-label--font-size, 0.75em);
863
+ }
864
+ }
865
+
866
+ /* The BODY scrolls, not the `<details>` - the summary is the
867
+ * collapse toggle, and auto-scrolling an outer scroller to the
868
+ * bottom would carry it out of view. */
869
+ .chat-reasoning-body {
870
+ white-space: pre-wrap;
871
+ margin-top: 0.4em;
872
+ max-height: 96px;
873
+ overflow-y: auto;
874
+ }
875
+ }
876
+
877
+ .chat-pending span {
878
+ display: inline-block;
879
+ width: 4px;
880
+ height: 4px;
881
+ margin-right: 3px;
882
+ border-radius: 50%;
883
+ background-color: var(--psp-inactive--color, #6e6e6e);
884
+ animation: chat-pending-pulse 1.2s infinite ease-in-out;
885
+
886
+ &:nth-child(2) {
887
+ animation-delay: 0.2s;
888
+ }
889
+
890
+ &:nth-child(3) {
891
+ animation-delay: 0.4s;
892
+ }
893
+ }
894
+
895
+ #chat_input_row {
896
+ display: flex;
897
+ gap: 6px;
898
+ flex: 0 0 auto;
899
+ padding-right: 12px;
900
+
901
+ /* The input grows downward as it fills; the button stays anchored
902
+ * to the bottom rather than stretching with it. */
903
+ align-items: flex-end;
904
+
905
+ /* The prompt is a `MirroredTextarea` (see
906
+ * `form/mirrored-textarea.css`): it is exactly as tall as its
907
+ * text, so this wrapper owns the visible box and caps how far
908
+ * that growth goes before it scrolls instead. */
909
+ #chat_input_scroll {
910
+ flex: 1 1 auto;
911
+ min-width: 0;
912
+ display: flex;
913
+ flex-direction: column;
914
+ min-height: 40px;
915
+ max-height: 160px;
916
+ overflow-y: auto;
917
+ border: 1px solid var(--psp-inactive--color, #6e6e6e);
918
+ border-radius: 3px;
919
+
920
+ &:focus-within {
921
+ border-color: var(--psp-active--color, #1a7da1);
922
+ }
923
+
924
+ /* Fills the resting box so clicks anywhere in it reach the
925
+ * input, then grows past it with the text. */
926
+ .chat-input-box {
927
+ flex: 1 0 auto;
928
+ padding: 4px 6px;
929
+ font-size: 1em;
930
+
931
+ /* The mirror paints the text; the input layer above it is
932
+ * caret-only (`mirrored-textarea.css`). */
933
+ .mirrored-textarea-mirror {
934
+ color: inherit;
935
+
936
+ /* The placeholder, as CSS `content` so the intl
937
+ * label mechanism can localize it - the native
938
+ * `placeholder` attribute is untranslatable and is
939
+ * kept only for assistive tech. */
940
+ &.is-empty:before {
941
+ content: var(--psp-label--chat-placeholder--content);
942
+ color: var(--psp-inactive--color, #6e6e6e);
943
+ }
944
+ }
945
+
946
+ /* The wrapper is the scroller - the input must not
947
+ * scroll independently of the text it mirrors. */
948
+ .mirrored-textarea-input {
949
+ overflow: hidden;
950
+ }
951
+ }
952
+ }
953
+
954
+ button {
955
+ flex: 0 0 auto;
956
+ cursor: pointer;
957
+ font-family: inherit;
958
+ font-size: var(--label--font-size, 0.75em);
959
+ text-transform: uppercase;
960
+ color: inherit;
961
+ background-color: #00000020;
962
+ border: 1px solid var(--psp-inactive--color, #6e6e6e);
963
+ border-radius: 3px;
964
+ padding: 0 10px;
965
+
966
+ /* Matches `#chat_input`'s resting `min-height`: the row no
967
+ * longer stretches its children, so this keeps the button
968
+ * square with an empty input and anchored as it grows. */
969
+ height: 40px;
970
+ }
971
+
972
+ #chat_send_button:before {
973
+ content: var(--psp-label--chat-send--content);
974
+ }
975
+
976
+ #chat_stop_button:before {
977
+ content: var(--psp-label--chat-stop--content);
978
+ }
979
+ }
980
+
981
+ #chat_badge {
982
+ flex: 0 0 auto;
983
+ font-size: 0.66em;
984
+ color: var(--psp-inactive--color, #6e6e6e);
985
+ overflow: hidden;
986
+ white-space: nowrap;
987
+ text-overflow: ellipsis;
988
+ margin-bottom: 6px;
989
+ }
990
+ }
991
+
992
+ @keyframes chat-pending-pulse {
993
+ 0%,
994
+ 80%,
995
+ 100% {
996
+ opacity: 0.25;
997
+ }
998
+
999
+ 40% {
1000
+ opacity: 1;
1001
+ }
1002
+ }
@@ -0,0 +1,19 @@
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
+ pub mod protocol;
14
+ pub mod transport;
15
+ pub mod turn;
16
+
17
+ pub use protocol::ChatMessage;
18
+ pub use transport::{AgentTransport, OnDelta};
19
+ pub use turn::{TurnError, run_turn};
@@ -0,0 +1,320 @@
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
+ //! The OpenAI chat-completions wire shapes — only the fields this client
14
+ //! produces or consumes. Unknown response fields are ignored by serde, so
15
+ //! provider-specific extras (`usage`, `finish_reason`, ids) cost nothing.
16
+ //! The format is pinned by the `test/js/agent/` specs, which assert these
17
+ //! exact shapes against a scripted fake engine.
18
+
19
+ use serde::{Deserialize, Serialize};
20
+ use serde_json::Value;
21
+
22
+ /// One conversation message, tagged by `role`. `Assistant` doubles as the
23
+ /// response-message shape (`choices[n].message`).
24
+ #[derive(Clone, Deserialize, Serialize)]
25
+ #[serde(tag = "role", rename_all = "lowercase")]
26
+ pub enum ChatMessage {
27
+ System {
28
+ content: String,
29
+ },
30
+ User {
31
+ content: String,
32
+ },
33
+ Assistant {
34
+ #[serde(default, skip_serializing_if = "Option::is_none")]
35
+ content: Option<String>,
36
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
37
+ tool_calls: Vec<ToolCall>,
38
+
39
+ /// Reasoning-model "thinking" text, captured for DISPLAY only:
40
+ /// `skip_serializing` is the contract that it is never replayed
41
+ /// into a request (providers reject or mis-tokenize replayed
42
+ /// reasoning). Two wire conventions, one field: DeepSeek-style
43
+ /// `reasoning_content` (also LM Studio/vLLM) and OpenRouter's
44
+ /// `reasoning`.
45
+ #[serde(default, skip_serializing, alias = "reasoning")]
46
+ reasoning_content: Option<String>,
47
+ },
48
+ Tool {
49
+ tool_call_id: String,
50
+ content: String,
51
+ },
52
+ }
53
+
54
+ /// A model-emitted tool invocation, echoed back verbatim in the transcript
55
+ /// so the model can correlate `Tool` results by `id`.
56
+ #[derive(Clone, Deserialize, Serialize)]
57
+ pub struct ToolCall {
58
+ #[serde(default)]
59
+ pub id: String,
60
+ #[serde(default = "function_type", rename = "type")]
61
+ pub kind: String,
62
+ pub function: FunctionCall,
63
+ }
64
+
65
+ /// `arguments` is a JSON-*encoded string* per the OpenAI wire format, not a
66
+ /// JSON object.
67
+ #[derive(Clone, Deserialize, Serialize)]
68
+ pub struct FunctionCall {
69
+ pub name: String,
70
+ #[serde(default)]
71
+ pub arguments: String,
72
+ }
73
+
74
+ fn function_type() -> String {
75
+ "function".to_owned()
76
+ }
77
+
78
+ /// A tool advertised to the model.
79
+ #[derive(Serialize)]
80
+ pub struct ToolDef {
81
+ #[serde(rename = "type")]
82
+ pub kind: &'static str,
83
+ pub function: FunctionDef,
84
+ }
85
+
86
+ #[derive(Serialize)]
87
+ pub struct FunctionDef {
88
+ pub name: &'static str,
89
+ pub description: &'static str,
90
+ pub parameters: Value,
91
+ }
92
+
93
+ #[derive(Serialize)]
94
+ pub struct ChatRequest<'a> {
95
+ pub model: &'a str,
96
+ pub messages: &'a [ChatMessage],
97
+ pub tools: &'a [ToolDef],
98
+
99
+ /// Always `true`; servers that ignore it get the buffered fallback in
100
+ /// `transport.rs`, so streaming needs no configuration surface.
101
+ pub stream: bool,
102
+ }
103
+
104
+ #[derive(Deserialize)]
105
+ pub struct ChatResponse {
106
+ pub choices: Vec<ChatChoice>,
107
+ }
108
+
109
+ #[derive(Deserialize)]
110
+ pub struct ChatChoice {
111
+ pub message: ChatMessage,
112
+ }
113
+
114
+ /// One `stream: true` SSE frame (`data: {...}`) — the delta shapes of the
115
+ /// OpenAI streaming wire format.
116
+ #[derive(Deserialize)]
117
+ pub struct ChatChunk {
118
+ #[serde(default)]
119
+ pub choices: Vec<ChunkChoice>,
120
+ }
121
+
122
+ #[derive(Deserialize)]
123
+ pub struct ChunkChoice {
124
+ #[serde(default)]
125
+ pub delta: ChunkDelta,
126
+ }
127
+
128
+ #[derive(Default, Deserialize)]
129
+ pub struct ChunkDelta {
130
+ #[serde(default)]
131
+ pub content: Option<String>,
132
+ #[serde(default)]
133
+ pub reasoning_content: Option<String>,
134
+ #[serde(default)]
135
+ pub reasoning: Option<String>,
136
+ #[serde(default)]
137
+ pub tool_calls: Vec<ToolCallDelta>,
138
+ }
139
+
140
+ /// Tool-call fragments accumulate BY INDEX: `id`/`name` arrive on the
141
+ /// first fragment, `arguments` arrives as string parts across many.
142
+ #[derive(Deserialize)]
143
+ pub struct ToolCallDelta {
144
+ #[serde(default)]
145
+ pub index: usize,
146
+ #[serde(default)]
147
+ pub id: Option<String>,
148
+ #[serde(default)]
149
+ pub function: Option<FunctionCallDelta>,
150
+ }
151
+
152
+ #[derive(Deserialize)]
153
+ pub struct FunctionCallDelta {
154
+ #[serde(default)]
155
+ pub name: Option<String>,
156
+ #[serde(default)]
157
+ pub arguments: Option<String>,
158
+ }
159
+
160
+ /// Folds a stream of [`ChatChunk`]s into the same
161
+ /// [`ChatMessage::Assistant`] the buffered path produces, so `turn.rs` is
162
+ /// agnostic to how the response arrived. Reasoning takes the first
163
+ /// non-empty of the two wire conventions per delta.
164
+ #[derive(Default)]
165
+ pub struct ChunkAccumulator {
166
+ content: String,
167
+ reasoning: String,
168
+ tool_calls: Vec<ToolCall>,
169
+ }
170
+
171
+ impl ChunkAccumulator {
172
+ pub fn push(&mut self, chunk: ChatChunk) {
173
+ for choice in chunk.choices {
174
+ let delta = choice.delta;
175
+ if let Some(content) = delta.content {
176
+ self.content.push_str(&content);
177
+ }
178
+
179
+ if let Some(reasoning) = delta
180
+ .reasoning_content
181
+ .into_iter()
182
+ .chain(delta.reasoning)
183
+ .find(|x| !x.is_empty())
184
+ {
185
+ self.reasoning.push_str(&reasoning);
186
+ }
187
+
188
+ for fragment in delta.tool_calls {
189
+ if self.tool_calls.len() <= fragment.index {
190
+ self.tool_calls
191
+ .resize_with(fragment.index + 1, || ToolCall {
192
+ id: String::new(),
193
+ kind: function_type(),
194
+ function: FunctionCall {
195
+ name: String::new(),
196
+ arguments: String::new(),
197
+ },
198
+ });
199
+ }
200
+
201
+ let call = &mut self.tool_calls[fragment.index];
202
+ if let Some(id) = fragment.id {
203
+ call.id = id;
204
+ }
205
+
206
+ if let Some(function) = fragment.function {
207
+ if let Some(name) = function.name {
208
+ call.function.name = name;
209
+ }
210
+
211
+ if let Some(arguments) = function.arguments {
212
+ call.function.arguments.push_str(&arguments);
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ /// Accumulated text/reasoning so far, for live-render callbacks.
220
+ pub fn progress(&self) -> (&str, &str) {
221
+ (&self.content, &self.reasoning)
222
+ }
223
+
224
+ pub fn finish(self) -> ChatMessage {
225
+ ChatMessage::Assistant {
226
+ content: (!self.content.is_empty()).then_some(self.content),
227
+ tool_calls: self.tool_calls,
228
+ reasoning_content: (!self.reasoning.is_empty()).then_some(self.reasoning),
229
+ }
230
+ }
231
+ }
232
+
233
+ #[cfg(test)]
234
+ mod tests {
235
+ use super::*;
236
+
237
+ fn chunk(json: &str) -> ChatChunk {
238
+ serde_json::from_str(json).unwrap()
239
+ }
240
+
241
+ #[test]
242
+ fn content_and_both_reasoning_conventions_fold() {
243
+ let mut acc = ChunkAccumulator::default();
244
+ acc.push(chunk(r#"{"choices":[{"delta":{"role":"assistant"}}]}"#));
245
+ acc.push(chunk(
246
+ r#"{"choices":[{"delta":{"reasoning_content":"th"}}]}"#,
247
+ ));
248
+ acc.push(chunk(r#"{"choices":[{"delta":{"reasoning":"ink"}}]}"#));
249
+ acc.push(chunk(r#"{"choices":[{"delta":{"content":"Hel"}}]}"#));
250
+ acc.push(chunk(
251
+ r#"{"choices":[{"delta":{"content":"lo","reasoning":""}}]}"#,
252
+ ));
253
+ assert_eq!(acc.progress(), ("Hello", "think"));
254
+ let ChatMessage::Assistant {
255
+ content,
256
+ tool_calls,
257
+ reasoning_content,
258
+ } = acc.finish()
259
+ else {
260
+ panic!("not assistant");
261
+ };
262
+
263
+ assert_eq!(content.as_deref(), Some("Hello"));
264
+ assert!(tool_calls.is_empty());
265
+ assert_eq!(reasoning_content.as_deref(), Some("think"));
266
+ }
267
+
268
+ #[test]
269
+ fn fragmented_tool_arguments_accumulate_by_index() {
270
+ let mut acc = ChunkAccumulator::default();
271
+ acc.push(chunk(
272
+ r#"{"choices":[{"delta":{"tool_calls":[
273
+ {"index":0,"id":"c0","function":{"name":"get_schema","arguments":""}}]}}]}"#,
274
+ ));
275
+
276
+ // Index 1 opens before index 0 finishes; both argument streams
277
+ // interleave.
278
+ acc.push(chunk(
279
+ r#"{"choices":[{"delta":{"tool_calls":[
280
+ {"index":1,"id":"c1","function":{"name":"set_view_config","arguments":"{\"con"}}]}}]}"#,
281
+ ));
282
+
283
+ acc.push(chunk(
284
+ r#"{"choices":[{"delta":{"tool_calls":[
285
+ {"index":1,"function":{"arguments":"fig\":{}}"}}]}}]}"#,
286
+ ));
287
+
288
+ let ChatMessage::Assistant { tool_calls, .. } = acc.finish() else {
289
+ panic!("not assistant");
290
+ };
291
+
292
+ assert_eq!(tool_calls.len(), 2);
293
+ assert_eq!(tool_calls[0].id, "c0");
294
+ assert_eq!(tool_calls[0].function.name, "get_schema");
295
+ assert_eq!(tool_calls[1].function.arguments, "{\"config\":{}}");
296
+ assert_eq!(tool_calls[1].kind, "function");
297
+ }
298
+
299
+ /// The display-only contract: reasoning deserializes from EITHER wire
300
+ /// field but NEVER serializes back into a request.
301
+ #[test]
302
+ fn assistant_reasoning_reads_both_aliases_and_never_serializes() {
303
+ for field in ["reasoning_content", "reasoning"] {
304
+ let message: ChatMessage = serde_json::from_str(&format!(
305
+ r#"{{"role":"assistant","content":"hi","{field}":"secret"}}"#
306
+ ))
307
+ .unwrap();
308
+
309
+ let ChatMessage::Assistant {
310
+ reasoning_content, ..
311
+ } = &message
312
+ else {
313
+ panic!("not assistant");
314
+ };
315
+
316
+ assert_eq!(reasoning_content.as_deref(), Some("secret"));
317
+ assert!(!serde_json::to_string(&message).unwrap().contains("secret"));
318
+ }
319
+ }
320
+ }