@marimo-team/frontend 0.15.5 → 0.16.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 (223) hide show
  1. package/dist/assets/{ConnectedDataExplorerComponent-Cn5-l2X1.js → ConnectedDataExplorerComponent-BErMbWvG.js} +1 -1
  2. package/dist/assets/{ImageComparisonComponent-CEXMKKA4.js → ImageComparisonComponent-fTHv1Ih0.js} +1 -1
  3. package/dist/assets/{VegaLite-Bt14Ds9k.js → VegaLite-Bdi-TyfY.js} +6 -6
  4. package/dist/assets/_baseEach-CNBxBxvS.js +1 -0
  5. package/dist/assets/_baseMap-D1WHjKrd.js +1 -0
  6. package/dist/assets/_baseUniq-CCgDNtZb.js +1 -0
  7. package/dist/assets/_createAggregator-DcD0kTA5.js +1 -0
  8. package/dist/assets/agent-panel-Crv430aI.js +268 -0
  9. package/dist/assets/agent-panel-D92Mfy1i.css +1 -0
  10. package/dist/assets/{any-language-editor-DiwNT6zp.js → any-language-editor-CQh552Wu.js} +1 -1
  11. package/dist/assets/architectureDiagram-W76B3OCA-BAJeBxzt.js +36 -0
  12. package/dist/assets/{between-horizontal-start-FyewyCGn.js → between-horizontal-start-Boxgxbt_.js} +1 -1
  13. package/dist/assets/{blockDiagram-QIGZ2CNN-BrOkAf_c.js → blockDiagram-QIGZ2CNN-CL-1svEK.js} +1 -1
  14. package/dist/assets/{c4Diagram-FPNF74CW-BHPzDxE2.js → c4Diagram-FPNF74CW-BbEqbCTl.js} +5 -5
  15. package/dist/assets/channel-_2eNSz0n.js +1 -0
  16. package/dist/assets/chat-panel-CXh5Wl6C.js +3 -0
  17. package/dist/assets/{chunk-4BX2VUAB-DLxaCNYh.js → chunk-4BX2VUAB-C--8TXeE.js} +1 -1
  18. package/dist/assets/{chunk-55IACEB6-DdzvO3HR.js → chunk-55IACEB6-Bj00HDqq.js} +1 -1
  19. package/dist/assets/{chunk-FMBD7UC4-R5o-nSiG.js → chunk-FMBD7UC4-C-lhB6hN.js} +1 -1
  20. package/dist/assets/{chunk-K7UQS3LO-DxaMrGgG.js → chunk-K7UQS3LO-B-pGTXPt.js} +1 -1
  21. package/dist/assets/{chunk-QN33PNHL-DqS9-FYm.js → chunk-QN33PNHL-DqUzGhvm.js} +1 -1
  22. package/dist/assets/{chunk-QZHKN3VN-BZ-TzajS.js → chunk-QZHKN3VN-TntJHfSk.js} +1 -1
  23. package/dist/assets/{chunk-TVAH2DTR-BsgP2dyv.js → chunk-TVAH2DTR-HUJb1psV.js} +1 -1
  24. package/dist/assets/{chunk-TZMSLE5B-D-h3ahXI.js → chunk-TZMSLE5B-BK3C__t3.js} +1 -1
  25. package/dist/assets/{circle-play-CQtRZ-rT.js → circle-play-DBLOv1Yu.js} +1 -1
  26. package/dist/assets/classDiagram-KNZD7YFC-BGmh9POF.js +1 -0
  27. package/dist/assets/classDiagram-v2-RKCZMP56-BGmh9POF.js +1 -0
  28. package/dist/assets/{clear-button-BY6Z_ViL.js → clear-button-BeoFbEKH.js} +1 -1
  29. package/dist/assets/clone-BFDSPAj3.js +1 -0
  30. package/dist/assets/command-palette-CXZiSv0I.js +1 -0
  31. package/dist/assets/common-C7oJcmCT.js +1 -0
  32. package/dist/assets/{compile-Ct_jzdKr.js → compile-7L0MwhyI.js} +1 -1
  33. package/dist/assets/cose-bilkent-S5V4N54A-BMkGLcVC.js +1 -0
  34. package/dist/assets/dagre-5GWH7T2D-BJtRienS.js +4 -0
  35. package/dist/assets/{data-grid-overlay-editor-BN_wulc3.js → data-grid-overlay-editor-DBkmGtNs.js} +1 -1
  36. package/dist/assets/datasources-panel-B7FbYLiy.js +1 -0
  37. package/dist/assets/{dependency-graph-panel-BOmSCZf7.js → dependency-graph-panel-DEdOxp2X.js} +4 -4
  38. package/dist/assets/diagram-N5W7TBWH-CmECY3nb.js +24 -0
  39. package/dist/assets/diagram-QEK2KX5R-DMOVSNKD.js +43 -0
  40. package/dist/assets/diagram-S2PKOQOG-BiJ96PNQ.js +24 -0
  41. package/dist/assets/{documentation-panel-BxjJO_Gw.js → documentation-panel-xULhaEv3.js} +1 -1
  42. package/dist/assets/edit-page-BrYda9VE.js +129 -0
  43. package/dist/assets/{ellipsis-vertical-UHbmjI2n.js → ellipsis-vertical-BBqXIlc2.js} +1 -1
  44. package/dist/assets/{empty-state-BIBXzY_0.js → empty-state-B3dA3G5P.js} +1 -1
  45. package/dist/assets/{erDiagram-AWTI2OKA-E84mAle_.js → erDiagram-AWTI2OKA-MP1DiFRo.js} +1 -1
  46. package/dist/assets/{error-panel-MEvQ6K7h.js → error-panel-Cc1sv-Ag.js} +1 -1
  47. package/dist/assets/file-explorer-panel-Bw59Kva1.js +1 -0
  48. package/dist/assets/{flowDiagram-PVAE7QVJ-DfbIRSAW.js → flowDiagram-PVAE7QVJ-BX7caPp7.js} +1 -1
  49. package/dist/assets/{ganttDiagram-OWAHRB6G-DR4HZ1z_.js → ganttDiagram-OWAHRB6G-B462g4Yf.js} +3 -3
  50. package/dist/assets/gitGraphDiagram-NY62KEGX-CGgvZ9-9.js +65 -0
  51. package/dist/assets/{glide-data-editor-nNmo1lPq.js → glide-data-editor-C0gUFZON.js} +4 -4
  52. package/dist/assets/graph-CHRVBzY5.js +1 -0
  53. package/dist/assets/{home-page-9eW6qida.js → home-page-Fb2osjys.js} +3 -3
  54. package/dist/assets/{index-DMomwMcN.js → index-BVgAenPd.js} +1 -1
  55. package/dist/assets/{index-B8llrTSo.js → index-BY93Ejhl.js} +1 -1
  56. package/dist/assets/{index-BFSnz7iM.js → index-C-8WADat.js} +1 -1
  57. package/dist/assets/{index-CPN7TRA1.js → index-C-GhZ7ti.js} +1 -1
  58. package/dist/assets/{index-DyLSuOH1.js → index-C1v_Z9et.js} +1 -1
  59. package/dist/assets/{index-VPWqq2Pg.js → index-C4Tn5NvJ.js} +1 -1
  60. package/dist/assets/{index-BAH034Ue.js → index-C77h_TXN.js} +1 -1
  61. package/dist/assets/{index-Dt9UWeWn.js → index-CQDrxQ0j.js} +1 -1
  62. package/dist/assets/{index-DWOaniGT.js → index-CWMgowgL.js} +1 -1
  63. package/dist/assets/{index-B1_GXGaP.js → index-Clbi_Yaq.js} +1 -1
  64. package/dist/assets/{index-B7yXbrLa.js → index-CpTPJo4k.js} +1 -1
  65. package/dist/assets/{index-CknhX2Vy.css → index-Cx0bsY1w.css} +1 -1
  66. package/dist/assets/{index-DqzMPAC8.js → index-D1vmG6DS.js} +2 -2
  67. package/dist/assets/{index-c6If577Q.js → index-D9UKkrr2.js} +1 -1
  68. package/dist/assets/{index-CB2pnVQG.js → index-DEQvTChO.js} +1 -1
  69. package/dist/assets/{index-OC46250R.js → index-DKEudB02.js} +205 -197
  70. package/dist/assets/{index-CSgxTUzD.js → index-DRMm6SNo.js} +1 -1
  71. package/dist/assets/{index-Bq516OmX.js → index-DoRmcrKM.js} +1 -1
  72. package/dist/assets/{index-DSU75csX.js → index-lYa_leQE.js} +1 -1
  73. package/dist/assets/{index-BLu5CX6z.js → index-vmICa5KN.js} +1 -1
  74. package/dist/assets/{index-uacyUula.js → index-z9bohSQJ.js} +1 -1
  75. package/dist/assets/infoDiagram-STP46IZ2-CVyrdLc8.js +2 -0
  76. package/dist/assets/isEmpty-DU_ogP_D.js +1 -0
  77. package/dist/assets/{journeyDiagram-BIP6EPQ6-BBiFyygf.js → journeyDiagram-BIP6EPQ6-C6EgLP_Q.js} +1 -1
  78. package/dist/assets/{kanban-definition-6OIFK2YF-DhgA6Nt6.js → kanban-definition-6OIFK2YF-BXzYO1yj.js} +4 -4
  79. package/dist/assets/layout-jihVw5-i.js +1 -0
  80. package/dist/assets/linear-C4blANlC.js +1 -0
  81. package/dist/assets/{links-CbvGxbsJ.js → links-D59GIweI.js} +3 -3
  82. package/dist/assets/{logs-panel-B9SmTZAW.js → logs-panel-D401qzZh.js} +1 -1
  83. package/dist/assets/markdown-renderer-Cd9eYyaL.js +263 -0
  84. package/dist/assets/{agent-panel-DpQ6muj-.css → markdown-renderer-ClyzDMmG.css} +1 -1
  85. package/dist/assets/mermaid-BEVuRz_O.js +1 -0
  86. package/dist/assets/{mermaid.core-4nVOEVX3.js → mermaid.core-CaSnaLH0.js} +41 -41
  87. package/dist/assets/min-DUMu_zeK.js +1 -0
  88. package/dist/assets/{mindmap-definition-Q6HEUPPD-CVLQNn1q.js → mindmap-definition-Q6HEUPPD-BXUM5MT2.js} +2 -2
  89. package/dist/assets/{number-overlay-editor-CzRzXLcd.js → number-overlay-editor-4uWXGlPG.js} +1 -1
  90. package/dist/assets/{outline-panel-uvsS-YEQ.js → outline-panel-DIzkvm2I.js} +1 -1
  91. package/dist/assets/packages-panel-CJL0MVlj.js +1 -0
  92. package/dist/assets/{pieDiagram-ADFJNKIX-C5IQ5DBZ.js → pieDiagram-ADFJNKIX-Dxt5PVNo.js} +3 -3
  93. package/dist/assets/{quadrantDiagram-LMRXKWRM-CFXFnQxx.js → quadrantDiagram-LMRXKWRM-D4pUaA31.js} +1 -1
  94. package/dist/assets/{react-plotly-mzdv02_Y.js → react-plotly-cJZ0VWBq.js} +1 -1
  95. package/dist/assets/{requirementDiagram-4UW4RH46-D9bPC89T.js → requirementDiagram-4UW4RH46-DVRTjgas.js} +1 -1
  96. package/dist/assets/run-page-BUEnMC9w.js +1 -0
  97. package/dist/assets/sankeyDiagram-GR3RE2ED-CVFnD9C-.js +10 -0
  98. package/dist/assets/scratchpad-panel-BIgRENkI.js +1 -0
  99. package/dist/assets/secrets-panel-xY5-V_BD.js +1 -0
  100. package/dist/assets/{sequenceDiagram-C3RYC4MD-6N7_hY4k.js → sequenceDiagram-C3RYC4MD-_lY4ZN_S.js} +4 -4
  101. package/dist/assets/{slides-component-EcjC8sDK.js → slides-component-Xjymwj7X.js} +1 -1
  102. package/dist/assets/snippets-panel-CTPYW41n.js +1 -0
  103. package/dist/assets/sortBy-BNZKwiq_.js +1 -0
  104. package/dist/assets/state-C4NiC9tO.js +1 -0
  105. package/dist/assets/stateDiagram-KXAO66HF-Da0JQWCn.js +1 -0
  106. package/dist/assets/stateDiagram-v2-UMBNRL4Z-D5lYZOOt.js +1 -0
  107. package/dist/assets/storage-CMdLzB_c.js +26 -0
  108. package/dist/assets/terminal-BPwTkXae.js +10 -0
  109. package/dist/assets/time-Dv5_Ouz_.js +1 -0
  110. package/dist/assets/{timeline-definition-XQNQX7LJ-BEaynAiY.js → timeline-definition-XQNQX7LJ-Dxh5Zu2e.js} +1 -1
  111. package/dist/assets/tracing-BCIurUfa.js +2 -0
  112. package/dist/assets/{tracing-panel-BmuHLPrY.js → tracing-panel-DAzrzNmm.js} +2 -2
  113. package/dist/assets/{trash-UBqfK4mR.js → trash-Dc6DSjz_.js} +1 -1
  114. package/dist/assets/{tree-XiEycetl.js → tree-jheoerAX.js} +1 -1
  115. package/dist/assets/{treemap-75Q7IDZK-CnuVFbBG.js → treemap-75Q7IDZK-IgpxeGaf.js} +21 -21
  116. package/dist/assets/{ts-tags-CloPe9IY.js → ts-tags-DxCDHihD.js} +1 -1
  117. package/dist/assets/variable-panel-DYAiLBmF.js +1 -0
  118. package/dist/assets/{vega-component-DsTH4tuX.js → vega-component-BpfpiPKI.js} +1 -1
  119. package/dist/assets/{xychartDiagram-6GGTOJPD-Dcz3O-A3.js → xychartDiagram-6GGTOJPD-CmNigJ31.js} +1 -1
  120. package/dist/index.html +2 -2
  121. package/package.json +8 -4
  122. package/src/__tests__/mocks.ts +43 -0
  123. package/src/components/app-config/user-config-form.tsx +32 -0
  124. package/src/components/chat/acp/__tests__/__snapshots__/prompt.test.ts.snap +55 -23
  125. package/src/components/chat/acp/__tests__/context-utils.test.ts +222 -0
  126. package/src/components/chat/acp/__tests__/prompt.test.ts +1 -1
  127. package/src/components/chat/acp/__tests__/state.test.ts +2 -6
  128. package/src/components/chat/acp/agent-docs.tsx +33 -6
  129. package/src/components/chat/acp/agent-panel.css +0 -18
  130. package/src/components/chat/acp/agent-panel.tsx +397 -72
  131. package/src/components/chat/acp/agent-selector.tsx +7 -1
  132. package/src/components/chat/acp/blocks.tsx +40 -10
  133. package/src/components/chat/acp/common.tsx +10 -2
  134. package/src/components/chat/acp/context-utils.ts +127 -0
  135. package/src/components/chat/acp/prompt.ts +34 -10
  136. package/src/components/chat/acp/state.ts +1 -1
  137. package/src/components/chat/acp/types.ts +8 -0
  138. package/src/components/chat/chat-panel.tsx +23 -88
  139. package/src/components/chat/chat-utils.ts +127 -1
  140. package/src/components/chat/markdown-renderer.css +39 -0
  141. package/src/components/chat/markdown-renderer.tsx +7 -38
  142. package/src/components/chat/tool-call-accordion.tsx +113 -23
  143. package/src/components/editor/Cell.tsx +6 -0
  144. package/src/components/editor/actions/name-cell-input.tsx +6 -1
  145. package/src/components/editor/actions/useCellActionButton.tsx +3 -1
  146. package/src/components/editor/ai/__tests__/completion-utils.test.ts +178 -1
  147. package/src/components/editor/ai/add-cell-with-ai.tsx +68 -66
  148. package/src/components/editor/ai/ai-completion-editor.tsx +29 -26
  149. package/src/components/editor/ai/completion-handlers.tsx +44 -6
  150. package/src/components/editor/ai/completion-utils.ts +92 -0
  151. package/src/components/editor/ai/transport/chat-transport.tsx +36 -0
  152. package/src/components/editor/cell/StagedAICell.tsx +51 -0
  153. package/src/components/editor/cell/cell-actions.tsx +2 -1
  154. package/src/components/terminal/__tests__/state.test.ts +207 -0
  155. package/src/components/terminal/hooks.ts +41 -0
  156. package/src/components/terminal/state.ts +75 -0
  157. package/src/components/terminal/terminal.tsx +334 -13
  158. package/src/components/terminal/theme.tsx +56 -0
  159. package/src/core/ai/__tests__/staged-cells.test.ts +356 -0
  160. package/src/core/ai/staged-cells.ts +208 -0
  161. package/src/core/cells/cells.ts +1 -1
  162. package/src/core/codemirror/lsp/federated-lsp.ts +1 -1
  163. package/src/core/islands/main.ts +2 -2
  164. package/src/core/kernel/messages.ts +8 -12
  165. package/src/core/saving/__tests__/filename.test.ts +37 -0
  166. package/src/core/static/__tests__/download-html.test.ts +43 -1
  167. package/src/core/websocket/useMarimoWebSocket.tsx +2 -2
  168. package/src/css/app/Cell.css +11 -0
  169. package/src/plugins/core/RenderHTML.tsx +36 -2
  170. package/src/plugins/core/__test__/RenderHTML.test.ts +72 -0
  171. package/src/plugins/core/registerReactComponent.tsx +28 -0
  172. package/src/plugins/impl/FileBrowserPlugin.tsx +8 -2
  173. package/src/stories/cell.stories.tsx +1 -1
  174. package/src/stories/layout/vertical/one-column.stories.tsx +1 -1
  175. package/src/utils/__tests__/cell-urls.test.ts +29 -0
  176. package/src/utils/__tests__/filenames.test.ts +18 -0
  177. package/src/utils/__tests__/path.test.ts +38 -0
  178. package/src/utils/__tests__/urls.test.ts +56 -1
  179. package/src/utils/errors.ts +9 -0
  180. package/dist/assets/_baseEach-C1FLm7WW.js +0 -1
  181. package/dist/assets/_baseMap-DBVArUYD.js +0 -1
  182. package/dist/assets/_baseUniq-Dk7ZPJ3N.js +0 -1
  183. package/dist/assets/_createAggregator-Bn38fDd3.js +0 -1
  184. package/dist/assets/agent-panel-COUYnuIK.js +0 -475
  185. package/dist/assets/architectureDiagram-W76B3OCA-DBzWQKKu.js +0 -36
  186. package/dist/assets/channel-CjhbjOv4.js +0 -1
  187. package/dist/assets/chat-panel-BPXKoTnZ.js +0 -7
  188. package/dist/assets/chat-panel-Brrs_eeH.css +0 -1
  189. package/dist/assets/classDiagram-KNZD7YFC-DHs5cFzy.js +0 -1
  190. package/dist/assets/classDiagram-v2-RKCZMP56-DHs5cFzy.js +0 -1
  191. package/dist/assets/clone-DM1YNjEn.js +0 -1
  192. package/dist/assets/command-palette-S0bzQp7v.js +0 -1
  193. package/dist/assets/common-B8U9k2Ly.js +0 -1
  194. package/dist/assets/cose-bilkent-S5V4N54A-wz1Sfx7j.js +0 -1
  195. package/dist/assets/dagre-5GWH7T2D-BfpcVBgq.js +0 -4
  196. package/dist/assets/datasources-panel-DfuURLJw.js +0 -1
  197. package/dist/assets/diagram-N5W7TBWH-Bf0oqqQh.js +0 -24
  198. package/dist/assets/diagram-QEK2KX5R-ZTc3qikh.js +0 -43
  199. package/dist/assets/diagram-S2PKOQOG-tLScBy7Z.js +0 -24
  200. package/dist/assets/edit-page-DJ8kJZ9w.js +0 -129
  201. package/dist/assets/file-explorer-panel-CzNUJ63G.js +0 -1
  202. package/dist/assets/gitGraphDiagram-NY62KEGX-C1t6QtVa.js +0 -65
  203. package/dist/assets/graph-CssCVWIq.js +0 -1
  204. package/dist/assets/index-DcCIe7np.js +0 -28
  205. package/dist/assets/infoDiagram-STP46IZ2-CwiAoz9f.js +0 -2
  206. package/dist/assets/layout-DpQrxGW-.js +0 -1
  207. package/dist/assets/linear-NsreOeBF.js +0 -1
  208. package/dist/assets/mermaid-DSt0r6IQ.js +0 -1
  209. package/dist/assets/min-D259kI3t.js +0 -1
  210. package/dist/assets/packages-panel-xMz9W2hW.js +0 -1
  211. package/dist/assets/run-page-Bb68qdhQ.js +0 -1
  212. package/dist/assets/sankeyDiagram-GR3RE2ED-BSJOau8E.js +0 -10
  213. package/dist/assets/scratchpad-panel-BF4BO-U4.js +0 -1
  214. package/dist/assets/secrets-panel-CdIX44dQ.js +0 -1
  215. package/dist/assets/snippets-panel-Dco9h0rb.js +0 -1
  216. package/dist/assets/sortBy-aLGA-PGK.js +0 -1
  217. package/dist/assets/stateDiagram-KXAO66HF-Bd68WT3b.js +0 -1
  218. package/dist/assets/stateDiagram-v2-UMBNRL4Z-BXz_GSwb.js +0 -1
  219. package/dist/assets/storage-CGlP4lCF.js +0 -26
  220. package/dist/assets/terminal-CxkHubcu.js +0 -9
  221. package/dist/assets/time-D2nr1UgQ.js +0 -1
  222. package/dist/assets/tracing-kTqHxa7q.js +0 -2
  223. package/dist/assets/variable-panel-noTnH-AQ.js +0 -1
@@ -1,35 +1,249 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
2
 
3
3
  import { AttachAddon } from "@xterm/addon-attach";
4
+ import { CanvasAddon } from "@xterm/addon-canvas";
4
5
  import { FitAddon } from "@xterm/addon-fit";
6
+ import { SearchAddon } from "@xterm/addon-search";
7
+ import { Unicode11Addon } from "@xterm/addon-unicode11";
8
+ import { WebLinksAddon } from "@xterm/addon-web-links";
5
9
  import { Terminal } from "@xterm/xterm";
6
- import React, { useEffect, useRef, useState } from "react";
10
+ import React, { useEffect, useMemo, useRef, useState } from "react";
7
11
  import "@xterm/xterm/css/xterm.css";
8
12
  import "./xterm.css";
13
+ import {
14
+ ClipboardPasteIcon,
15
+ CopyIcon,
16
+ TextSelectionIcon,
17
+ Trash2Icon,
18
+ } from "lucide-react";
19
+ import useEvent from "react-use-event-hook";
9
20
  import { waitForConnectionOpen } from "@/core/network/connection";
10
21
  import { useRuntimeManager } from "@/core/runtime/config";
22
+ import { useDebouncedCallback } from "@/hooks/useDebounce";
23
+ import { cn } from "@/utils/cn";
24
+ import { copyToClipboard } from "@/utils/copy";
11
25
  import { Logger } from "@/utils/Logger";
26
+ import { MinimalHotkeys } from "../shortcuts/renderShortcut";
27
+ import { useTerminalActions, useTerminalState } from "./state";
28
+ import { createTerminalTheme } from "./theme";
12
29
 
13
- const TerminalComponent: React.FC<{
30
+ interface TerminalButtonProps {
31
+ onClick: () => void;
32
+ disabled?: boolean;
33
+ icon: React.ComponentType<{ className?: string }>;
34
+ children: React.ReactNode;
35
+ keyboardShortcut?: string;
36
+ }
37
+
38
+ const TerminalButton: React.FC<TerminalButtonProps> = ({
39
+ onClick,
40
+ disabled = false,
41
+ icon: Icon,
42
+ children,
43
+ keyboardShortcut,
44
+ }) => (
45
+ <button
46
+ className={cn(
47
+ "w-full text-left px-3 py-2 text-sm flex items-center gap-3 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-muted",
48
+ )}
49
+ type="button"
50
+ onClick={onClick}
51
+ disabled={disabled}
52
+ >
53
+ <Icon className="w-4 h-4" />
54
+ {children}
55
+ {keyboardShortcut && (
56
+ <MinimalHotkeys className="ml-auto" shortcut={keyboardShortcut} />
57
+ )}
58
+ </button>
59
+ );
60
+
61
+ interface TerminalComponentProps {
14
62
  visible: boolean;
15
63
  onClose: () => void;
16
- }> = ({ visible, onClose }) => {
64
+ }
65
+
66
+ interface Position {
67
+ x: number;
68
+ y: number;
69
+ placement: "bottom" | "top"; // Whether to place the menu above or below the cursor
70
+ }
71
+
72
+ // Keyboard shortcut handlers
73
+ function createKeyboardHandler(terminal: Terminal, _searchAddon: SearchAddon) {
74
+ return (event: KeyboardEvent) => {
75
+ const { ctrlKey, metaKey, key } = event;
76
+ const modifier = ctrlKey || metaKey;
77
+
78
+ if (modifier) {
79
+ switch (key) {
80
+ case "c":
81
+ if (terminal.hasSelection()) {
82
+ event.preventDefault();
83
+ void copyToClipboard(terminal.getSelection());
84
+ }
85
+ break;
86
+ case "v":
87
+ event.preventDefault();
88
+ void navigator.clipboard.readText().then((text) => {
89
+ terminal.paste(text);
90
+ });
91
+ break;
92
+ case "a":
93
+ event.preventDefault();
94
+ terminal.selectAll();
95
+ break;
96
+ case "l":
97
+ event.preventDefault();
98
+ terminal.clear();
99
+ break;
100
+ }
101
+ }
102
+ };
103
+ }
104
+
105
+ // Context menu actions
106
+ function createContextMenuActions(
107
+ terminal: Terminal,
108
+ setContextMenu: (menu: Position | null) => void,
109
+ ) {
110
+ const closeMenu = () => setContextMenu(null);
111
+
112
+ return {
113
+ handleCopy: () => {
114
+ if (terminal.hasSelection()) {
115
+ navigator.clipboard.writeText(terminal.getSelection());
116
+ }
117
+ closeMenu();
118
+ },
119
+ handlePaste: () => {
120
+ navigator.clipboard.readText().then((text) => {
121
+ terminal.paste(text);
122
+ });
123
+ closeMenu();
124
+ },
125
+ handleSelectAll: () => {
126
+ terminal.selectAll();
127
+ closeMenu();
128
+ },
129
+ handleClear: () => {
130
+ terminal.clear();
131
+ closeMenu();
132
+ },
133
+ closeMenu,
134
+ };
135
+ }
136
+
137
+ const RESIZE_DEBOUNCE_TIME = 100;
138
+
139
+ const TerminalComponent: React.FC<TerminalComponentProps> = ({
140
+ visible,
141
+ onClose,
142
+ }) => {
17
143
  const terminalRef = useRef<HTMLDivElement>(null);
144
+ const wsRef = useRef<WebSocket | null>(null);
145
+
18
146
  // eslint-disable-next-line react/hook-use-state
19
- const [{ terminal, fitAddon }] = useState(() => {
147
+ const [{ terminal, fitAddon, searchAddon }] = useState(() => {
20
148
  // Create a new terminal instance
21
149
  const term = new Terminal({
22
150
  fontFamily:
23
151
  "Menlo, DejaVu Sans Mono, Consolas, Lucida Console, monospace",
24
152
  fontSize: 14,
153
+ scrollback: 10_000,
154
+ cursorBlink: true,
155
+ cursorStyle: "block",
156
+ allowTransparency: false,
157
+ theme: createTerminalTheme("dark"),
158
+ rightClickSelectsWord: true,
159
+ wordSeparator: " \t\r\n\"'`(){}[]<>|&;",
160
+ allowProposedApi: true,
25
161
  });
162
+
163
+ // Load essential addons
26
164
  const fitAddon = new FitAddon();
165
+ const searchAddon = new SearchAddon();
166
+ const canvasAddon = new CanvasAddon();
167
+ const unicode11Addon = new Unicode11Addon();
168
+ const webLinksAddon = new WebLinksAddon();
169
+
27
170
  term.loadAddon(fitAddon);
28
- return { terminal: term, fitAddon };
171
+ term.loadAddon(searchAddon);
172
+ term.loadAddon(canvasAddon);
173
+ term.loadAddon(unicode11Addon);
174
+ term.loadAddon(webLinksAddon);
175
+
176
+ // Set Unicode version
177
+ term.unicode.activeVersion = "11";
178
+
179
+ return { terminal: term, fitAddon, searchAddon };
29
180
  });
181
+
30
182
  const [initialized, setInitialized] = React.useState(false);
183
+ const [contextMenu, setContextMenu] = useState<Position | null>(null);
31
184
  const runtimeManager = useRuntimeManager();
32
185
 
186
+ // Terminal command state management
187
+ const terminalState = useTerminalState();
188
+ const { removeCommand, setReady } = useTerminalActions();
189
+
190
+ // Keyboard shortcuts handler
191
+ const handleKeyDown = useEvent(createKeyboardHandler(terminal, searchAddon));
192
+
193
+ // Context menu handler
194
+ const handleContextMenu = useEvent((event: MouseEvent) => {
195
+ event.preventDefault();
196
+
197
+ const menuHeight = 200; // Approximate height of the context menu
198
+ const viewportHeight = window.innerHeight;
199
+ const cursorY = event.clientY;
200
+
201
+ // Check if there's enough space below the cursor
202
+ const spaceBelow = viewportHeight - cursorY;
203
+ const shouldPlaceAbove = spaceBelow < menuHeight;
204
+
205
+ setContextMenu({
206
+ x: event.clientX,
207
+ y: event.clientY,
208
+ placement: shouldPlaceAbove ? "top" : "bottom",
209
+ });
210
+ });
211
+
212
+ // Close context menu on click outside
213
+ const handleClickOutside = useEvent((event: MouseEvent) => {
214
+ const target = event.target;
215
+ const isInsideContextMenu =
216
+ target &&
217
+ target instanceof HTMLElement &&
218
+ target.closest(".xterm-context-menu");
219
+ if (contextMenu && !isInsideContextMenu) {
220
+ setContextMenu(null);
221
+ }
222
+ });
223
+
224
+ const handleBackendResizeDebounced = useDebouncedCallback(
225
+ ({ cols, rows }: { cols: number; rows: number }) => {
226
+ if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
227
+ Logger.debug("Sending resize to backend terminal", { cols, rows });
228
+ wsRef.current.send(JSON.stringify({ type: "resize", cols, rows }));
229
+ }
230
+ },
231
+ RESIZE_DEBOUNCE_TIME,
232
+ );
233
+
234
+ const handleResize = useEvent(() => {
235
+ if (!terminal || !fitAddon) {
236
+ return;
237
+ }
238
+ fitAddon.fit();
239
+ });
240
+
241
+ // Context menu actions
242
+ const { handleCopy, handlePaste, handleSelectAll, handleClear } = useMemo(
243
+ () => createContextMenuActions(terminal, setContextMenu),
244
+ [terminal],
245
+ );
246
+
33
247
  // Websocket Connection
34
248
  useEffect(() => {
35
249
  if (initialized) {
@@ -43,16 +257,37 @@ const TerminalComponent: React.FC<{
43
257
  const socket = new WebSocket(runtimeManager.getTerminalWsURL());
44
258
  const attachAddon = new AttachAddon(socket);
45
259
  terminal.loadAddon(attachAddon);
260
+ wsRef.current = socket;
261
+
262
+ // Terminal is ready when the websocket is open
263
+ const updateReadyState = () => {
264
+ setReady(socket.readyState === WebSocket.OPEN);
265
+ };
266
+
267
+ const handleError = () => {
268
+ updateReadyState();
269
+ };
270
+
271
+ const handleOpen = () => {
272
+ updateReadyState();
273
+ };
46
274
 
47
275
  const handleDisconnect = () => {
48
276
  onClose();
49
277
  // Reset
50
278
  attachAddon.dispose();
279
+ wsRef.current = null;
51
280
  terminal.clear();
52
281
  setInitialized(false);
282
+ setReady(false);
53
283
  };
54
284
 
285
+ socket.addEventListener("open", handleOpen);
55
286
  socket.addEventListener("close", handleDisconnect);
287
+ socket.addEventListener("error", handleError);
288
+
289
+ // Set initial ready state
290
+ updateReadyState();
56
291
  setInitialized(true);
57
292
  } catch (error) {
58
293
  Logger.error("Runtime health check failed for terminal", error);
@@ -68,6 +303,30 @@ const TerminalComponent: React.FC<{
68
303
  // eslint-disable-next-line react-hooks/exhaustive-deps
69
304
  }, [initialized]);
70
305
 
306
+ // Process pending commands when terminal is ready
307
+ useEffect(() => {
308
+ if (!terminalState.isReady || terminalState.pendingCommands.length === 0) {
309
+ return;
310
+ }
311
+
312
+ // Process all pending commands
313
+ for (const command of terminalState.pendingCommands) {
314
+ if (terminal && wsRef.current?.readyState === WebSocket.OPEN) {
315
+ Logger.debug("Sending programmatic command to terminal", {
316
+ command: command.text,
317
+ });
318
+ terminal.input(command.text);
319
+ terminal.focus();
320
+ removeCommand(command.id);
321
+ }
322
+ }
323
+ }, [
324
+ terminal,
325
+ terminalState.isReady,
326
+ terminalState.pendingCommands,
327
+ removeCommand,
328
+ ]);
329
+
71
330
  // When visible
72
331
  useEffect(() => {
73
332
  if (visible) {
@@ -86,23 +345,85 @@ const TerminalComponent: React.FC<{
86
345
  }
87
346
 
88
347
  terminal.open(terminalRef.current);
89
- fitAddon.fit();
90
- terminal.focus();
91
- // Handle resize
92
- const handleResize = () => {
348
+
349
+ // Initial fit with delay to ensure DOM is ready
350
+ setTimeout(() => {
93
351
  fitAddon.fit();
94
- };
95
- window.addEventListener("resize", handleResize);
352
+ }, RESIZE_DEBOUNCE_TIME);
353
+
354
+ terminal.focus();
355
+
356
+ const abortController = new AbortController();
357
+
358
+ // Add event listeners
359
+ window.addEventListener("resize", handleResize, {
360
+ signal: abortController.signal,
361
+ });
362
+ terminalRef.current.addEventListener("keydown", handleKeyDown, {
363
+ signal: abortController.signal,
364
+ });
365
+ terminalRef.current.addEventListener("contextmenu", handleContextMenu, {
366
+ signal: abortController.signal,
367
+ });
368
+ terminal.onResize(handleBackendResizeDebounced);
369
+ document.addEventListener("click", handleClickOutside, {
370
+ signal: abortController.signal,
371
+ });
96
372
 
97
373
  return () => {
98
- window.removeEventListener("resize", handleResize);
374
+ abortController.abort();
99
375
  };
100
376
  // eslint-disable-next-line react-hooks/exhaustive-deps
101
377
  }, []);
102
378
 
103
379
  return (
104
- <div className="relative w-full h-[calc(100%-4px)] dark bg-(--slate-1)">
380
+ <div className={"relative w-full h-[calc(100%-4px)] bg-popover"}>
105
381
  <div className="w-full h-full" ref={terminalRef} />
382
+ {contextMenu && (
383
+ <div
384
+ className={
385
+ "xterm-context-menu fixed z-50 rounded-md shadow-lg py-1 min-w-[160px] border bg-popover"
386
+ }
387
+ style={{
388
+ left: contextMenu.x,
389
+ [contextMenu.placement === "top" ? "bottom" : "top"]:
390
+ contextMenu.placement === "top"
391
+ ? window.innerHeight - contextMenu.y
392
+ : contextMenu.y,
393
+ }}
394
+ >
395
+ <TerminalButton
396
+ onClick={handleCopy}
397
+ disabled={!terminal.hasSelection()}
398
+ icon={CopyIcon}
399
+ keyboardShortcut="mod-c"
400
+ >
401
+ Copy
402
+ </TerminalButton>
403
+ <TerminalButton
404
+ onClick={handlePaste}
405
+ icon={ClipboardPasteIcon}
406
+ keyboardShortcut="mod-v"
407
+ >
408
+ Paste
409
+ </TerminalButton>
410
+ <hr className={cn("my-1 border-border")} />
411
+ <TerminalButton
412
+ onClick={handleSelectAll}
413
+ icon={TextSelectionIcon}
414
+ keyboardShortcut="mod-a"
415
+ >
416
+ Select all
417
+ </TerminalButton>
418
+ <TerminalButton
419
+ onClick={handleClear}
420
+ icon={Trash2Icon}
421
+ keyboardShortcut="mod-l"
422
+ >
423
+ Clear terminal
424
+ </TerminalButton>
425
+ </div>
426
+ )}
106
427
  </div>
107
428
  );
108
429
  };
@@ -0,0 +1,56 @@
1
+ import type { ResolvedTheme } from "@/theme/useTheme";
2
+
3
+ // Terminal theme configuration
4
+ export function createTerminalTheme(theme: ResolvedTheme) {
5
+ const baseTheme = {
6
+ cursor: "#ffffff",
7
+ cursorAccent: "#000000",
8
+ };
9
+
10
+ return theme === "dark"
11
+ ? {
12
+ ...baseTheme,
13
+ background: "#0f172a", // slate-900
14
+ foreground: "#f8fafc", // slate-50
15
+ black: "#0f172a",
16
+ red: "#ef4444",
17
+ green: "#22c55e",
18
+ yellow: "#eab308",
19
+ blue: "#3b82f6",
20
+ magenta: "#a855f7",
21
+ cyan: "#06b6d4",
22
+ white: "#f1f5f9",
23
+ brightBlack: "#475569",
24
+ brightRed: "#f87171",
25
+ brightGreen: "#4ade80",
26
+ brightYellow: "#facc15",
27
+ brightBlue: "#60a5fa",
28
+ brightMagenta: "#c084fc",
29
+ brightCyan: "#22d3ee",
30
+ brightWhite: "#ffffff",
31
+ selection: "rgba(148, 163, 184, 0.3)", // slate-400 with opacity
32
+ }
33
+ : {
34
+ ...baseTheme,
35
+ background: "#ffffff", // white
36
+ foreground: "#0f172a", // slate-900
37
+ cursor: "#0f172a",
38
+ black: "#0f172a",
39
+ red: "#dc2626",
40
+ green: "#16a34a",
41
+ yellow: "#ca8a04",
42
+ blue: "#2563eb",
43
+ magenta: "#9333ea",
44
+ cyan: "#0891b2",
45
+ white: "#e2e8f0",
46
+ brightBlack: "#64748b",
47
+ brightRed: "#ef4444",
48
+ brightGreen: "#22c55e",
49
+ brightYellow: "#eab308",
50
+ brightBlue: "#3b82f6",
51
+ brightMagenta: "#a855f7",
52
+ brightCyan: "#06b6d4",
53
+ brightWhite: "#ffffff",
54
+ selection: "rgba(71, 85, 105, 0.2)", // slate-600 with opacity
55
+ };
56
+ }