@marimo-team/islands 0.15.5 → 0.16.1

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 (217) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CBeIYi8p.js → ConnectedDataExplorerComponent-DyqLQGPc.js} +1567 -1544
  2. package/dist/{ImageComparisonComponent-Bk0a0xBq.js → ImageComparisonComponent-CQDGJfUA.js} +1 -1
  3. package/dist/{_baseUniq-utU5_Vu-.js → _baseUniq-B2Nna6Kt.js} +1 -1
  4. package/dist/{any-language-editor-PrUUh2lr.js → any-language-editor-D-wq0tOG.js} +1 -1
  5. package/dist/{architectureDiagram-W76B3OCA-D-vOp0UU.js → architectureDiagram-W76B3OCA-C6tdnMBf.js} +4 -4
  6. package/dist/assets/{worker-BcG8m3h5.js → worker-B0C57BK8.js} +40 -38
  7. package/dist/{blockDiagram-QIGZ2CNN-IG-z8q8A.js → blockDiagram-QIGZ2CNN-IagL8LCN.js} +5 -5
  8. package/dist/{c4Diagram-FPNF74CW-5AEXIX3t.js → c4Diagram-FPNF74CW-D3_lIWUP.js} +2 -2
  9. package/dist/{channel-ECVsTGGL.js → channel-DCJI_DKk.js} +1 -1
  10. package/dist/{chunk-4BX2VUAB-DfJcd9e-.js → chunk-4BX2VUAB-B2DrODwN.js} +1 -1
  11. package/dist/{chunk-55IACEB6-BwT8MejR.js → chunk-55IACEB6-BUWDsQ-t.js} +1 -1
  12. package/dist/{chunk-FMBD7UC4-DW7uxNR6.js → chunk-FMBD7UC4-BExPNFv1.js} +1 -1
  13. package/dist/{chunk-K7UQS3LO-BGn2ZPDQ.js → chunk-K7UQS3LO-Cixi-Yko.js} +4 -4
  14. package/dist/{chunk-QN33PNHL-BcIbOumv.js → chunk-QN33PNHL-B83MtvER.js} +1 -1
  15. package/dist/{chunk-QZHKN3VN-CMSnhk6x.js → chunk-QZHKN3VN-CXvbu85X.js} +1 -1
  16. package/dist/{chunk-TVAH2DTR-CZF2JRya.js → chunk-TVAH2DTR-CpiumCHg.js} +3 -3
  17. package/dist/{chunk-TZMSLE5B-BHzN_BY6.js → chunk-TZMSLE5B-DIzaZjcI.js} +1 -1
  18. package/dist/{classDiagram-v2-RKCZMP56-2H7MseyB.js → classDiagram-KNZD7YFC-DyN5HPdk.js} +2 -2
  19. package/dist/{classDiagram-KNZD7YFC-2H7MseyB.js → classDiagram-v2-RKCZMP56-DyN5HPdk.js} +2 -2
  20. package/dist/{clone-DKQcSK7N.js → clone-DrJYap2i.js} +1 -1
  21. package/dist/{cose-bilkent-S5V4N54A-CgvKFxTr.js → cose-bilkent-S5V4N54A-D39b4WrQ.js} +2 -2
  22. package/dist/{dagre-5GWH7T2D-VNFIipzt.js → dagre-5GWH7T2D-BLjRxDpS.js} +6 -6
  23. package/dist/{data-grid-overlay-editor-XdqkKCVx.js → data-grid-overlay-editor-DTALqerV.js} +2 -2
  24. package/dist/{diagram-N5W7TBWH-D1s8h-eH.js → diagram-N5W7TBWH-MM8AIKGR.js} +5 -5
  25. package/dist/{diagram-QEK2KX5R-DOa-AstT.js → diagram-QEK2KX5R-BZGarWuJ.js} +3 -3
  26. package/dist/{diagram-S2PKOQOG-CFZ-Y2zi.js → diagram-S2PKOQOG-CnPinN9Q.js} +3 -3
  27. package/dist/{dockerfile-zE-2DWBS.js → dockerfile-U8DnCJ4X.js} +1 -1
  28. package/dist/{erDiagram-AWTI2OKA-WxUYJfbS.js → erDiagram-AWTI2OKA-CvDVbxOO.js} +4 -4
  29. package/dist/{flowDiagram-PVAE7QVJ-dDZH2O1W.js → flowDiagram-PVAE7QVJ-C2uuBTZS.js} +5 -5
  30. package/dist/{ganttDiagram-OWAHRB6G-D3CCqPQq.js → ganttDiagram-OWAHRB6G-BEff10RF.js} +4 -4
  31. package/dist/{gitGraphDiagram-NY62KEGX-BHFylEwc.js → gitGraphDiagram-NY62KEGX-wggu0kb2.js} +4 -4
  32. package/dist/{glide-data-editor-D0aJSGV_.js → glide-data-editor-Bqh5_dzJ.js} +3 -3
  33. package/dist/{graph-BPGEu6c8.js → graph-DKpp_wzf.js} +3 -3
  34. package/dist/{index-HtOEKQ3O.js → index-4XruEJkp.js} +1 -1
  35. package/dist/{index-eDB61tLS.js → index-DW0BCGJE.js} +1 -1
  36. package/dist/{index-DotQhzoN.js → index-DdfF_cLK.js} +1 -1
  37. package/dist/{index-Bx2b23rX.js → index-DzJ_YPCG.js} +3 -3
  38. package/dist/{infoDiagram-STP46IZ2-DWhhqGPi.js → infoDiagram-STP46IZ2-DF7KW-Op.js} +2 -2
  39. package/dist/{journeyDiagram-BIP6EPQ6-CU8FpryL.js → journeyDiagram-BIP6EPQ6-B_jmhmqd.js} +3 -3
  40. package/dist/{kanban-definition-6OIFK2YF-CWhF_a4g.js → kanban-definition-6OIFK2YF-B-M9FTyw.js} +2 -2
  41. package/dist/{layout-DGonEvAZ.js → layout-C4oVYZZD.js} +4 -4
  42. package/dist/{linear-Cww2a6nQ.js → linear-C-HCGr0T.js} +1 -1
  43. package/dist/{main-Bc0LY9fB.js → main-B9x2-9f2.js} +93798 -93495
  44. package/dist/main.js +1 -1
  45. package/dist/{mermaid-DpJuOhRr.js → mermaid-BE4cM3Qs.js} +30 -30
  46. package/dist/{min-CFQjsG4L.js → min-DTpHJ698.js} +2 -2
  47. package/dist/{mindmap-definition-Q6HEUPPD-K513Ef1t.js → mindmap-definition-Q6HEUPPD-Cpd-hO1E.js} +3 -3
  48. package/dist/{number-overlay-editor-DuSchUfE.js → number-overlay-editor-CvURA2Ud.js} +2 -2
  49. package/dist/{pieDiagram-ADFJNKIX-DAIIUJJO.js → pieDiagram-ADFJNKIX-D9f_f6fn.js} +3 -3
  50. package/dist/{quadrantDiagram-LMRXKWRM-yuf-j7Os.js → quadrantDiagram-LMRXKWRM-DgllE7xw.js} +2 -2
  51. package/dist/{react-plotly-B378DZ9U.js → react-plotly-BU-JRJSi.js} +1 -1
  52. package/dist/{requirementDiagram-4UW4RH46-BBWvEl6q.js → requirementDiagram-4UW4RH46-Dk_G8eUb.js} +3 -3
  53. package/dist/{sankeyDiagram-GR3RE2ED-B_TwV-dS.js → sankeyDiagram-GR3RE2ED-BhLIhDc1.js} +1 -1
  54. package/dist/{sequenceDiagram-C3RYC4MD-BVC6lltp.js → sequenceDiagram-C3RYC4MD-DHoZdMFJ.js} +3 -3
  55. package/dist/{slides-component-CPX3S0Y9.js → slides-component-DXAgdf7K.js} +2 -2
  56. package/dist/{stateDiagram-KXAO66HF-BCU1tYTD.js → stateDiagram-KXAO66HF-C1Ie-7Xf.js} +4 -4
  57. package/dist/{stateDiagram-v2-UMBNRL4Z-BdvN6wTu.js → stateDiagram-v2-UMBNRL4Z--CRuIHtM.js} +2 -2
  58. package/dist/style.css +1 -1
  59. package/dist/{time-CSIip6fV.js → time-yQjlGPwa.js} +2 -2
  60. package/dist/{timeline-definition-XQNQX7LJ-CCxCPNQI.js → timeline-definition-XQNQX7LJ-D_PjxB1B.js} +1 -1
  61. package/dist/{treemap-75Q7IDZK-Du6v0BzD.js → treemap-75Q7IDZK--NYqQjUZ.js} +134 -134
  62. package/dist/{vega-component-Da93sTnp.js → vega-component-CCUOMM5K.js} +2 -2
  63. package/dist/{xychartDiagram-6GGTOJPD-Oq6xaZKR.js → xychartDiagram-6GGTOJPD-WLKsEnzs.js} +2 -2
  64. package/package.json +10 -5
  65. package/src/__tests__/mocks.ts +43 -0
  66. package/src/components/app-config/user-config-form.tsx +78 -1
  67. package/src/components/chat/acp/__tests__/__snapshots__/prompt.test.ts.snap +116 -65
  68. package/src/components/chat/acp/__tests__/atoms.test.ts +1 -1
  69. package/src/components/chat/acp/__tests__/context-utils.test.ts +222 -0
  70. package/src/components/chat/acp/__tests__/prompt.test.ts +1 -1
  71. package/src/components/chat/acp/__tests__/state.test.ts +38 -42
  72. package/src/components/chat/acp/agent-docs.tsx +33 -6
  73. package/src/components/chat/acp/agent-panel.css +0 -18
  74. package/src/components/chat/acp/agent-panel.tsx +394 -72
  75. package/src/components/chat/acp/agent-selector.tsx +7 -1
  76. package/src/components/chat/acp/blocks.tsx +40 -10
  77. package/src/components/chat/acp/common.tsx +10 -2
  78. package/src/components/chat/acp/context-utils.ts +127 -0
  79. package/src/components/chat/acp/prompt.ts +96 -53
  80. package/src/components/chat/acp/state.ts +1 -1
  81. package/src/components/chat/acp/types.ts +8 -0
  82. package/src/components/chat/chat-panel.tsx +28 -89
  83. package/src/components/chat/chat-utils.ts +127 -1
  84. package/src/components/chat/markdown-renderer.css +39 -0
  85. package/src/components/chat/markdown-renderer.tsx +12 -47
  86. package/src/components/chat/tool-call-accordion.tsx +148 -26
  87. package/src/components/data-table/SearchBar.tsx +8 -7
  88. package/src/components/data-table/__tests__/column_formatting.test.ts +50 -35
  89. package/src/components/data-table/__tests__/data-table.test.tsx +39 -1
  90. package/src/components/data-table/cell-hover-template/feature.ts +14 -0
  91. package/src/components/data-table/cell-hover-template/types.ts +11 -0
  92. package/src/components/data-table/charts/components/form-fields.tsx +41 -37
  93. package/src/components/data-table/charts/forms/common-chart.tsx +2 -2
  94. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +5 -2
  95. package/src/components/data-table/column-formatting/feature.ts +62 -29
  96. package/src/components/data-table/column-formatting/types.ts +1 -0
  97. package/src/components/data-table/column-header.tsx +3 -1
  98. package/src/components/data-table/column-summary/chart-spec-model.tsx +24 -7
  99. package/src/components/data-table/column-summary/column-summary.tsx +18 -9
  100. package/src/components/data-table/columns.tsx +42 -18
  101. package/src/components/data-table/data-table.tsx +10 -2
  102. package/src/components/data-table/date-popover.tsx +85 -75
  103. package/src/components/data-table/filter-pills.tsx +14 -9
  104. package/src/components/data-table/header-items.tsx +5 -1
  105. package/src/components/data-table/pagination.tsx +20 -13
  106. package/src/components/data-table/renderers.tsx +28 -0
  107. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +10 -8
  108. package/src/components/datasources/column-preview.tsx +6 -2
  109. package/src/components/datasources/datasources.tsx +8 -12
  110. package/src/components/editor/Cell.tsx +6 -0
  111. package/src/components/editor/actions/name-cell-input.tsx +6 -1
  112. package/src/components/editor/actions/useCellActionButton.tsx +3 -1
  113. package/src/components/editor/ai/__tests__/completion-utils.test.ts +178 -1
  114. package/src/components/editor/ai/add-cell-with-ai.tsx +68 -66
  115. package/src/components/editor/ai/ai-completion-editor.tsx +29 -26
  116. package/src/components/editor/ai/completion-handlers.tsx +44 -6
  117. package/src/components/editor/ai/completion-utils.ts +92 -0
  118. package/src/components/editor/ai/transport/chat-transport.tsx +39 -0
  119. package/src/components/editor/cell/CellStatus.tsx +23 -20
  120. package/src/components/editor/cell/CreateCellButton.tsx +3 -4
  121. package/src/components/editor/cell/StagedAICell.tsx +51 -0
  122. package/src/components/editor/cell/cell-actions.tsx +2 -1
  123. package/src/components/editor/cell/code/language-toggle.tsx +3 -4
  124. package/src/components/editor/chrome/wrapper/footer-items/machine-stats.tsx +39 -28
  125. package/src/components/editor/controls/notebook-menu-dropdown.tsx +4 -2
  126. package/src/components/editor/file-tree/requesting-tree.tsx +14 -8
  127. package/src/components/editor/renderers/CellArray.tsx +3 -4
  128. package/src/components/editor/renderers/slides-layout/slides-layout.tsx +3 -3
  129. package/src/components/editor/renderers/slides-layout/types.ts +1 -0
  130. package/src/components/pages/home-page.tsx +4 -1
  131. package/src/components/slides/slides-component.tsx +1 -1
  132. package/src/components/slides/slides.css +6 -0
  133. package/src/components/terminal/__tests__/state.test.ts +207 -0
  134. package/src/components/terminal/hooks.ts +41 -0
  135. package/src/components/terminal/state.ts +75 -0
  136. package/src/components/terminal/terminal.tsx +334 -13
  137. package/src/components/terminal/theme.tsx +57 -0
  138. package/src/components/tracing/tracing-spec.ts +5 -4
  139. package/src/components/ui/range-slider.tsx +4 -2
  140. package/src/components/ui/slider.tsx +3 -1
  141. package/src/components/variables/variables-table.tsx +3 -0
  142. package/src/core/MarimoApp.tsx +9 -6
  143. package/src/core/ai/__tests__/staged-cells.test.ts +356 -0
  144. package/src/core/ai/context/__tests__/registry.test.ts +6 -4
  145. package/src/core/ai/context/providers/cell-output.ts +3 -2
  146. package/src/core/ai/context/providers/error.ts +3 -1
  147. package/src/core/ai/context/providers/file.ts +7 -2
  148. package/src/core/ai/context/providers/tables.ts +3 -2
  149. package/src/core/ai/context/providers/variable.ts +6 -4
  150. package/src/core/ai/staged-cells.ts +241 -0
  151. package/src/core/cells/__tests__/add-missing-import.test.ts +67 -22
  152. package/src/core/cells/add-missing-import.ts +24 -7
  153. package/src/core/cells/cells.ts +27 -28
  154. package/src/core/cells/logs.ts +1 -1
  155. package/src/core/codemirror/find-replace/search-highlight.ts +3 -1
  156. package/src/core/codemirror/language/LanguageAdapters.ts +9 -3
  157. package/src/core/codemirror/lsp/federated-lsp.ts +1 -1
  158. package/src/core/codemirror/lsp/notebook-lsp.ts +8 -2
  159. package/src/core/codemirror/readonly/__tests__/extension.test.ts +1 -1
  160. package/src/core/codemirror/rtc/loro/awareness.ts +52 -17
  161. package/src/core/codemirror/rtc/loro/sync.ts +12 -4
  162. package/src/core/config/config-schema.ts +1 -0
  163. package/src/core/config/config.ts +4 -0
  164. package/src/core/hotkeys/hotkeys.ts +8 -4
  165. package/src/core/i18n/__tests__/locale-provider.test.tsx +176 -0
  166. package/src/core/i18n/locale-provider.tsx +35 -0
  167. package/src/core/i18n/with-locale.tsx +12 -0
  168. package/src/core/islands/components/web-components.tsx +13 -10
  169. package/src/core/islands/main.ts +2 -2
  170. package/src/core/kernel/RuntimeState.ts +4 -1
  171. package/src/core/kernel/messages.ts +8 -12
  172. package/src/core/network/DeferredRequestRegistry.ts +16 -4
  173. package/src/core/runtime/runtime.ts +5 -4
  174. package/src/core/saving/__tests__/filename.test.ts +37 -0
  175. package/src/core/static/__tests__/download-html.test.ts +43 -1
  176. package/src/core/wasm/bridge.ts +5 -1
  177. package/src/core/wasm/store.ts +4 -1
  178. package/src/core/wasm/worker/message-buffer.ts +3 -2
  179. package/src/core/websocket/types.ts +22 -16
  180. package/src/core/websocket/useMarimoWebSocket.tsx +2 -2
  181. package/src/css/app/Cell.css +11 -0
  182. package/src/hooks/useFormatting.ts +97 -0
  183. package/src/hooks/useTimer.ts +8 -5
  184. package/src/plugins/core/RenderHTML.tsx +36 -2
  185. package/src/plugins/core/__test__/RenderHTML.test.ts +72 -0
  186. package/src/plugins/core/registerReactComponent.tsx +44 -10
  187. package/src/plugins/impl/DataTablePlugin.tsx +4 -0
  188. package/src/plugins/impl/FileBrowserPlugin.tsx +8 -2
  189. package/src/plugins/impl/RangeSliderPlugin.tsx +5 -3
  190. package/src/plugins/impl/SliderPlugin.tsx +3 -1
  191. package/src/plugins/impl/anywidget/model.ts +16 -5
  192. package/src/plugins/impl/data-editor/types.ts +7 -5
  193. package/src/plugins/impl/data-explorer/components/column-summary.tsx +20 -13
  194. package/src/plugins/impl/panel/utils.ts +6 -4
  195. package/src/plugins/layout/OutlinePlugin.tsx +69 -0
  196. package/src/plugins/layout/StatPlugin.tsx +4 -1
  197. package/src/plugins/plugins.ts +2 -0
  198. package/src/stories/cell.stories.tsx +1 -1
  199. package/src/stories/layout/vertical/one-column.stories.tsx +1 -1
  200. package/src/utils/__tests__/cell-urls.test.ts +29 -0
  201. package/src/utils/__tests__/dates.test.ts +45 -24
  202. package/src/utils/__tests__/filenames.test.ts +18 -0
  203. package/src/utils/__tests__/numbers.test.ts +42 -30
  204. package/src/utils/__tests__/once.test.ts +187 -0
  205. package/src/utils/__tests__/path.test.ts +38 -0
  206. package/src/utils/__tests__/urls.test.ts +56 -1
  207. package/src/utils/dates.ts +15 -10
  208. package/src/utils/edit-distance.ts +8 -6
  209. package/src/utils/errors.ts +9 -0
  210. package/src/utils/id-tree.tsx +21 -10
  211. package/src/utils/localStorage.ts +13 -4
  212. package/src/utils/numbers.ts +11 -11
  213. package/src/utils/once.ts +32 -0
  214. package/src/utils/paths.ts +4 -1
  215. package/src/utils/pluralize.ts +12 -5
  216. package/src/utils/python-poet/poet.ts +30 -15
  217. package/src/utils/time.ts +5 -1
@@ -0,0 +1,207 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import { exportedForTesting } from "../state";
5
+
6
+ const { reducer, initialState } = exportedForTesting;
7
+
8
+ describe("terminal state", () => {
9
+ describe("initialState", () => {
10
+ it("should return initial state with empty pendingCommands and isReady false", () => {
11
+ const state = initialState();
12
+ expect(state).toEqual({
13
+ pendingCommands: [],
14
+ isReady: false,
15
+ });
16
+ });
17
+ });
18
+
19
+ describe("reducer", () => {
20
+ it("should add a command to pendingCommands", () => {
21
+ const state = initialState();
22
+ const text = "ls -la";
23
+
24
+ const newState = reducer(state, { type: "addCommand", payload: text });
25
+
26
+ expect(newState.pendingCommands).toHaveLength(1);
27
+ expect(newState.pendingCommands[0]).toMatchObject({
28
+ text: "ls -la",
29
+ timestamp: expect.any(Number),
30
+ });
31
+ expect(newState.pendingCommands[0].id).toBeDefined();
32
+ expect(newState.isReady).toBe(false);
33
+ });
34
+
35
+ it("should add multiple commands to pendingCommands", () => {
36
+ const state = initialState();
37
+
38
+ let newState = reducer(state, { type: "addCommand", payload: "ls -la" });
39
+ newState = reducer(newState, { type: "addCommand", payload: "cd /home" });
40
+ newState = reducer(newState, { type: "addCommand", payload: "pwd" });
41
+
42
+ expect(newState.pendingCommands).toHaveLength(3);
43
+ expect(newState.pendingCommands[0].text).toBe("ls -la");
44
+ expect(newState.pendingCommands[1].text).toBe("cd /home");
45
+ expect(newState.pendingCommands[2].text).toBe("pwd");
46
+ });
47
+
48
+ it("should remove a command by id", () => {
49
+ const state = initialState();
50
+
51
+ let newState = reducer(state, { type: "addCommand", payload: "ls -la" });
52
+ newState = reducer(newState, { type: "addCommand", payload: "cd /home" });
53
+ newState = reducer(newState, { type: "addCommand", payload: "pwd" });
54
+
55
+ const commandToRemove = newState.pendingCommands[1];
56
+ newState = reducer(newState, {
57
+ type: "removeCommand",
58
+ payload: commandToRemove.id,
59
+ });
60
+
61
+ expect(newState.pendingCommands).toHaveLength(2);
62
+ expect(newState.pendingCommands[0].text).toBe("ls -la");
63
+ expect(newState.pendingCommands[1].text).toBe("pwd");
64
+ });
65
+
66
+ it("should not remove anything if command id does not exist", () => {
67
+ const state = initialState();
68
+
69
+ let newState = reducer(state, { type: "addCommand", payload: "ls -la" });
70
+ newState = reducer(newState, { type: "addCommand", payload: "cd /home" });
71
+
72
+ const originalLength = newState.pendingCommands.length;
73
+ newState = reducer(newState, {
74
+ type: "removeCommand",
75
+ payload: "non-existent-id",
76
+ });
77
+
78
+ expect(newState.pendingCommands).toHaveLength(originalLength);
79
+ });
80
+
81
+ it("should set isReady to true", () => {
82
+ const state = initialState();
83
+
84
+ const newState = reducer(state, { type: "setReady", payload: true });
85
+
86
+ expect(newState.isReady).toBe(true);
87
+ expect(newState.pendingCommands).toEqual([]);
88
+ });
89
+
90
+ it("should set isReady to false", () => {
91
+ const state = { ...initialState(), isReady: true };
92
+
93
+ const newState = reducer(state, { type: "setReady", payload: false });
94
+
95
+ expect(newState.isReady).toBe(false);
96
+ });
97
+
98
+ it("should clear all pending commands", () => {
99
+ const state = initialState();
100
+
101
+ let newState = reducer(state, { type: "addCommand", payload: "ls -la" });
102
+ newState = reducer(newState, { type: "addCommand", payload: "cd /home" });
103
+ newState = reducer(newState, { type: "addCommand", payload: "pwd" });
104
+
105
+ expect(newState.pendingCommands).toHaveLength(3);
106
+
107
+ newState = reducer(newState, {
108
+ type: "clearCommands",
109
+ payload: undefined,
110
+ });
111
+
112
+ expect(newState.pendingCommands).toHaveLength(0);
113
+ expect(newState.isReady).toBe(false);
114
+ });
115
+
116
+ it("should preserve other state when adding commands", () => {
117
+ const state = { ...initialState(), isReady: true };
118
+
119
+ const newState = reducer(state, {
120
+ type: "addCommand",
121
+ payload: "ls -la",
122
+ });
123
+
124
+ expect(newState.isReady).toBe(true);
125
+ expect(newState.pendingCommands).toHaveLength(1);
126
+ });
127
+
128
+ it("should preserve other state when removing commands", () => {
129
+ const state = { ...initialState(), isReady: true };
130
+
131
+ let newState = reducer(state, { type: "addCommand", payload: "ls -la" });
132
+ newState = reducer(newState, {
133
+ type: "removeCommand",
134
+ payload: newState.pendingCommands[0].id,
135
+ });
136
+
137
+ expect(newState.isReady).toBe(true);
138
+ expect(newState.pendingCommands).toHaveLength(0);
139
+ });
140
+
141
+ it("should preserve other state when setting ready", () => {
142
+ const state = initialState();
143
+
144
+ let newState = reducer(state, { type: "addCommand", payload: "ls -la" });
145
+ newState = reducer(newState, { type: "addCommand", payload: "cd /home" });
146
+ newState = reducer(newState, { type: "setReady", payload: true });
147
+
148
+ expect(newState.isReady).toBe(true);
149
+ expect(newState.pendingCommands).toHaveLength(2);
150
+ });
151
+
152
+ it("should preserve other state when clearing commands", () => {
153
+ const state = { ...initialState(), isReady: true };
154
+
155
+ let newState = reducer(state, { type: "addCommand", payload: "ls -la" });
156
+ newState = reducer(newState, {
157
+ type: "clearCommands",
158
+ payload: undefined,
159
+ });
160
+
161
+ expect(newState.isReady).toBe(true);
162
+ expect(newState.pendingCommands).toHaveLength(0);
163
+ });
164
+ });
165
+
166
+ describe("command properties", () => {
167
+ it("should generate unique ids for commands", () => {
168
+ const state = initialState();
169
+
170
+ let newState = reducer(state, {
171
+ type: "addCommand",
172
+ payload: "command1",
173
+ });
174
+ newState = reducer(newState, { type: "addCommand", payload: "command2" });
175
+
176
+ const ids = newState.pendingCommands.map((cmd) => cmd.id);
177
+ expect(ids[0]).not.toBe(ids[1]);
178
+ expect(ids[0]).toBeDefined();
179
+ expect(ids[1]).toBeDefined();
180
+ });
181
+
182
+ it("should set timestamp when adding commands", () => {
183
+ const state = initialState();
184
+ const beforeTime = Date.now();
185
+
186
+ const newState = reducer(state, {
187
+ type: "addCommand",
188
+ payload: "ls -la",
189
+ });
190
+
191
+ const afterTime = Date.now();
192
+ const command = newState.pendingCommands[0];
193
+
194
+ expect(command.timestamp).toBeGreaterThanOrEqual(beforeTime);
195
+ expect(command.timestamp).toBeLessThanOrEqual(afterTime);
196
+ });
197
+
198
+ it("should preserve text when adding commands", () => {
199
+ const state = initialState();
200
+ const text = "echo 'Hello World'";
201
+
202
+ const newState = reducer(state, { type: "addCommand", payload: text });
203
+
204
+ expect(newState.pendingCommands[0].text).toBe(text);
205
+ });
206
+ });
207
+ });
@@ -0,0 +1,41 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import { useChromeActions } from "../editor/chrome/state";
4
+ import { useTerminalActions } from "./state";
5
+
6
+ /**
7
+ * Hook for sending commands to the terminal programmatically.
8
+ * This will:
9
+ * 1. Open the terminal if it's not already open
10
+ * 2. Wait for the terminal to be connected
11
+ * 3. Send the command text to the terminal
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * function CopyButton({ command }: { command: string }) {
16
+ * const { sendCommand } = useTerminalCommands();
17
+ *
18
+ * return (
19
+ * <button onClick={() => sendCommand(command)}>
20
+ * Copy to Terminal
21
+ * </button>
22
+ * );
23
+ * }
24
+ * ```
25
+ */
26
+ export function useTerminalCommands() {
27
+ const { addCommand } = useTerminalActions();
28
+ const { setIsTerminalOpen } = useChromeActions();
29
+
30
+ const sendCommand = (text: string) => {
31
+ // First, ensure the terminal is open
32
+ setIsTerminalOpen(true);
33
+
34
+ // Add the command to the queue
35
+ addCommand(text);
36
+ };
37
+
38
+ return {
39
+ sendCommand,
40
+ };
41
+ }
@@ -0,0 +1,75 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import { useAtomValue } from "jotai";
4
+ import { createReducerAndAtoms } from "@/utils/createReducer";
5
+ import { generateUUID } from "@/utils/uuid";
6
+
7
+ export interface TerminalCommand {
8
+ id: string;
9
+ text: string;
10
+ timestamp: number;
11
+ }
12
+
13
+ export interface TerminalState {
14
+ pendingCommands: TerminalCommand[];
15
+ isReady: boolean;
16
+ }
17
+
18
+ function initialState(): TerminalState {
19
+ return {
20
+ pendingCommands: [],
21
+ isReady: false,
22
+ };
23
+ }
24
+
25
+ const {
26
+ reducer,
27
+ createActions,
28
+ valueAtom: terminalStateAtom,
29
+ useActions,
30
+ } = createReducerAndAtoms(initialState, {
31
+ addCommand: (state, text: string) => {
32
+ const command: TerminalCommand = {
33
+ id: generateUUID(),
34
+ text,
35
+ timestamp: Date.now(),
36
+ };
37
+
38
+ return {
39
+ ...state,
40
+ pendingCommands: [...state.pendingCommands, command],
41
+ };
42
+ },
43
+ removeCommand: (state, commandId: string) => ({
44
+ ...state,
45
+ pendingCommands: state.pendingCommands.filter(
46
+ (cmd) => cmd.id !== commandId,
47
+ ),
48
+ }),
49
+ setReady: (state, isReady: boolean) => ({
50
+ ...state,
51
+ isReady,
52
+ }),
53
+ clearCommands: (state) => ({
54
+ ...state,
55
+ pendingCommands: [],
56
+ }),
57
+ });
58
+
59
+ /**
60
+ * React hook to get the terminal state.
61
+ */
62
+ export const useTerminalState = () => useAtomValue(terminalStateAtom);
63
+
64
+ /**
65
+ * React hook to get the terminal actions.
66
+ */
67
+ export function useTerminalActions() {
68
+ return useActions();
69
+ }
70
+
71
+ export const exportedForTesting = {
72
+ reducer,
73
+ createActions,
74
+ initialState,
75
+ };