@jxsuite/studio 8.1.0 → 10.0.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 (266) hide show
  1. package/dist/chunks/{2020-dg097k3e.js → 2020-j56903f1.js} +3 -3
  2. package/dist/chunks/adapter-wc5zvg9a.js +4 -0
  3. package/dist/chunks/add-new-flow-snx1s4b0.js +4 -0
  4. package/dist/chunks/cf-account-picker-xyqw0jex.js +4 -0
  5. package/dist/chunks/cf-settings-hp1pjm0n.js +4 -0
  6. package/dist/chunks/color-04361ttk.js +4 -0
  7. package/dist/chunks/{color-area-vh9cew51.js → color-area-h8dbasp1.js} +2 -2
  8. package/dist/chunks/{color-field-e78f16bs.js → color-field-qv5r2b16.js} +3 -3
  9. package/dist/chunks/{color-slider-fg4yhvk6.js → color-slider-nz2mrzky.js} +2 -2
  10. package/dist/chunks/components-nkwphrx3.js +4 -0
  11. package/dist/chunks/content-source-6a253wmk.js +4 -0
  12. package/dist/chunks/{convert-to-repeater-grnmea6c.js → convert-to-repeater-fhdwyg14.js} +3 -3
  13. package/dist/chunks/editor-wqrc68ca.js +4 -0
  14. package/dist/chunks/entry-commands-nef0wrmp.js +4 -0
  15. package/dist/chunks/{extension-sections-rp7fkega.js → extension-sections-8ez2rrk8.js} +3 -3
  16. package/dist/chunks/file-ops-y3w71k1n.js +4 -0
  17. package/dist/chunks/files-de90apn9.js +4 -0
  18. package/dist/chunks/format-host-zfjz4rzm.js +4 -0
  19. package/dist/chunks/fs-events-77c0mr0x.js +4 -0
  20. package/dist/chunks/grid-open-4n4fjfpf.js +4 -0
  21. package/dist/chunks/{grid-view-x26rfpz1.js → grid-view-ka55z38f.js} +10 -10
  22. package/dist/chunks/{iframe-entry-pkgmcgp0.js → iframe-entry-2x9bn5ng.js} +4 -4
  23. package/dist/chunks/{iframe-entry-02wzcyvy.js → iframe-entry-btck0e9s.js} +4 -4
  24. package/dist/chunks/iframe-host-na2mywz4.js +4 -0
  25. package/dist/chunks/index-ewmgdb7c.js +4 -0
  26. package/dist/chunks/{index-203h71wy.js → index-qb98xhse.js} +3 -3
  27. package/dist/chunks/{javascript-ww5pv5ae.js → javascript-ces13wny.js} +2 -2
  28. package/dist/chunks/{jsonMode-vwbdwwq3.js → jsonMode-6md16d4v.js} +6 -6
  29. package/dist/chunks/jx-validate-b309c020.js +4 -0
  30. package/dist/chunks/layers-8rn8y6qv.js +4 -0
  31. package/dist/chunks/layers-panel-eyah7h9z.js +4 -0
  32. package/dist/chunks/lit-html-wyqw04y2.js +4 -0
  33. package/dist/chunks/media-library-gs3np47y.js +4 -0
  34. package/dist/chunks/media-picker-tt7dk4tb.js +4 -0
  35. package/dist/chunks/{monaco-binding-63tqze27.js → monaco-binding-p8j80s4w.js} +3 -3
  36. package/dist/chunks/monaco-buffer-wsf6cmnq.js +4 -0
  37. package/dist/chunks/{monaco-setup-zyyynnj8.js → monaco-setup-wasktzfg.js} +3 -3
  38. package/dist/chunks/notify-qwst108r.js +4 -0
  39. package/dist/chunks/parse-n8dfxnqf.js +4 -0
  40. package/dist/chunks/platform-c16mqvmp.js +4 -0
  41. package/dist/chunks/publish-panel-d34bhvwj.js +4 -0
  42. package/dist/chunks/{publish-quick-ajzdhhs6.js → publish-quick-vr695j26.js} +3 -3
  43. package/dist/chunks/publish-workspace-2yr60mt4.js +4 -0
  44. package/dist/chunks/quick-search-e6xsdv0d.js +4 -0
  45. package/dist/chunks/references-gsj6trdv.js +4 -0
  46. package/dist/chunks/right-panel-33jv1knc.js +4 -0
  47. package/dist/chunks/source-control-bj9bejjv.js +4 -0
  48. package/dist/chunks/studio-03mqh8ax.js +5 -0
  49. package/dist/chunks/{studio-957jg8g3.js → studio-06j4en5h.js} +4 -4
  50. package/dist/chunks/studio-0ydxv2mr.js +7 -0
  51. package/dist/chunks/{studio-v2ek69cb.js → studio-1e8dw2w2.js} +4 -4
  52. package/dist/chunks/studio-2hsqbq7b.js +7 -0
  53. package/dist/chunks/studio-2j86y34y.js +6 -0
  54. package/dist/chunks/studio-2ks6ka4g.js +5 -0
  55. package/dist/chunks/studio-34jrz12k.js +5 -0
  56. package/dist/chunks/{studio-1meqbxns.js → studio-35vrtpdq.js} +4 -4
  57. package/dist/chunks/studio-38g0jdz7.js +5 -0
  58. package/dist/chunks/{studio-25a5by3h.js → studio-396srjbk.js} +4 -4
  59. package/dist/chunks/{studio-zm7dnr9g.js → studio-3c8pp7pb.js} +4 -4
  60. package/dist/chunks/{studio-x3ge4rts.js → studio-3v5cagy9.js} +4 -4
  61. package/dist/chunks/studio-3w35qvay.js +5 -0
  62. package/dist/chunks/studio-3zge48z4.js +7 -0
  63. package/dist/chunks/studio-45gyxpcq.js +5 -0
  64. package/dist/chunks/{studio-hw60n5dh.js → studio-4kzbrr5t.js} +3 -3
  65. package/dist/chunks/{studio-533py3yy.js → studio-5tzgq6fx.js} +4 -4
  66. package/dist/chunks/studio-5wzdy1jc.js +16 -0
  67. package/dist/chunks/studio-732mw0g8.js +5 -0
  68. package/dist/chunks/{studio-3q6n9qvq.js → studio-771k2n12.js} +4 -4
  69. package/dist/chunks/{studio-z42kysg7.js → studio-7dzyz9n8.js} +137 -137
  70. package/dist/chunks/{studio-fqs1a22a.js → studio-7xag4na8.js} +4 -4
  71. package/dist/chunks/{studio-jqrg9xxg.js → studio-881z1xrx.js} +4 -4
  72. package/dist/chunks/studio-88bmt9rh.js +5 -0
  73. package/dist/chunks/{studio-8nyw10dp.js → studio-9b7177jq.js} +4 -4
  74. package/dist/chunks/{studio-nm9qev4p.js → studio-9cxmp61c.js} +5 -5
  75. package/dist/chunks/{studio-4dsdnan2.js → studio-9dz76yqc.js} +5 -5
  76. package/dist/chunks/{studio-a6x058ew.js → studio-agyvzfm4.js} +4 -4
  77. package/dist/chunks/{studio-2b8443ms.js → studio-ajw4k8hp.js} +3 -3
  78. package/dist/chunks/studio-c1pxszat.js +5 -0
  79. package/dist/chunks/studio-cpkamjdn.js +14 -0
  80. package/dist/chunks/studio-d038kqbs.js +5 -0
  81. package/dist/chunks/studio-d83m0wyq.js +5 -0
  82. package/dist/chunks/studio-dbxv7w4s.js +5 -0
  83. package/dist/chunks/studio-de1s2x9v.js +5 -0
  84. package/dist/chunks/{studio-94ppxeqy.js → studio-dx73md4n.js} +8 -8
  85. package/dist/chunks/studio-ekgtrndg.js +5 -0
  86. package/dist/chunks/studio-epenqmvk.js +7 -0
  87. package/dist/chunks/studio-et2q4s8n.js +5 -0
  88. package/dist/chunks/{studio-txkx6586.js → studio-ewarnzm4.js} +7 -7
  89. package/dist/chunks/{studio-bf0rgyf7.js → studio-fy7jzj9j.js} +4 -4
  90. package/dist/chunks/{studio-332hesdk.js → studio-gr36vnte.js} +4 -4
  91. package/dist/chunks/{studio-rdnezzkd.js → studio-gwrhcrss.js} +5 -5
  92. package/dist/chunks/{studio-9wyqyfqy.js → studio-h310mbqm.js} +4 -4
  93. package/dist/chunks/{studio-zht0x0v3.js → studio-h5tpz5ke.js} +4 -4
  94. package/dist/chunks/studio-h7weqf2q.js +5 -0
  95. package/dist/chunks/{studio-1gdfsr49.js → studio-hcsba201.js} +7 -7
  96. package/dist/chunks/studio-hwejq03c.js +5 -0
  97. package/dist/chunks/{studio-wcakp5bt.js → studio-hy4ye0dx.js} +7 -7
  98. package/dist/chunks/{studio-02phtmqn.js → studio-jttyqrmh.js} +4 -4
  99. package/dist/chunks/{studio-j5jqvyn3.js → studio-jw0q5j6k.js} +4 -4
  100. package/dist/chunks/studio-k3a8rp3d.js +5 -0
  101. package/dist/chunks/{studio-hnx1h1pr.js → studio-k3bcy02e.js} +4 -4
  102. package/dist/chunks/studio-m48v13s3.js +4 -0
  103. package/dist/chunks/{studio-bqgs90r8.js → studio-ndskgqef.js} +4 -4
  104. package/dist/chunks/studio-p1n2xjda.js +5 -0
  105. package/dist/chunks/studio-pgm83a2f.js +6 -0
  106. package/dist/chunks/studio-pqqq6v85.js +5 -0
  107. package/dist/chunks/studio-psh8mwnw.js +4 -0
  108. package/dist/chunks/{studio-cpdzrdgp.js → studio-r17asgs0.js} +4 -4
  109. package/dist/chunks/{studio-6vrxzkba.js → studio-ry81j4q1.js} +4 -4
  110. package/dist/chunks/studio-rzg8hnkf.js +5 -0
  111. package/dist/chunks/{studio-nsv7g1qn.js → studio-s0fg2wav.js} +4 -4
  112. package/dist/chunks/{studio-v0gsjav0.js → studio-tj2s0tks.js} +4 -4
  113. package/dist/chunks/studio-tyw7tk0x.js +5 -0
  114. package/dist/chunks/studio-v9ywz2se.js +6 -0
  115. package/dist/chunks/{studio-dmn7gpba.js → studio-vswcpzk7.js} +5 -5
  116. package/dist/chunks/studio-vzgmyt0v.js +5 -0
  117. package/dist/chunks/{studio-2dtc90zs.js → studio-wxdcd42g.js} +6 -6
  118. package/dist/chunks/{studio-c5dj3dzk.js → studio-wxhy1yh7.js} +4 -4
  119. package/dist/chunks/studio-x08a3m0a.js +5 -0
  120. package/dist/chunks/studio-x3mjr5cc.js +5 -0
  121. package/dist/chunks/{studio-xddgbwhe.js → studio-x97ay0v4.js} +4 -4
  122. package/dist/chunks/{studio-mmqr9awv.js → studio-xdpz9rpe.js} +33 -33
  123. package/dist/chunks/studio-xh683qzy.js +5 -0
  124. package/dist/chunks/{studio-hxq1ebp0.js → studio-xkxetm65.js} +6 -6
  125. package/dist/chunks/{studio-73wz63nn.js → studio-y26hx9re.js} +4 -4
  126. package/dist/chunks/{studio-kdseew9m.js → studio-yg3djpnm.js} +4 -4
  127. package/dist/chunks/studio-yx8hzqja.js +5 -0
  128. package/dist/chunks/studio-zg16eeq1.js +5 -0
  129. package/dist/chunks/studio-zgx7svjd.js +9 -0
  130. package/dist/chunks/studio-zkq3r91p.js +5 -0
  131. package/dist/chunks/{toast-host-1yg5ar8d.js → toast-host-g3r2s0kn.js} +2 -2
  132. package/dist/chunks/toast-w11qpkyz.js +4 -0
  133. package/dist/chunks/{tsMode-vra2hh7y.js → tsMode-h6tkz0ea.js} +5 -5
  134. package/dist/chunks/workspace-home-zkkrxq3r.js +4 -0
  135. package/dist/chunks/workspace-tdqqjqaa.js +4 -0
  136. package/dist/chunks/{ws-client-72x5n99j.js → ws-client-e951my95.js} +3 -3
  137. package/dist/iframe-entry.js +13 -13
  138. package/dist/manifest.json +77 -77
  139. package/dist/studio.js +91 -92
  140. package/package.json +11 -12
  141. package/src/canvas/iframe-editable-root.ts +32 -2
  142. package/src/collab/collab-session.ts +6 -2
  143. package/src/commands/context.ts +4 -0
  144. package/src/commands/defaults.ts +2 -0
  145. package/src/commands/live-context.ts +2 -0
  146. package/src/files/files.ts +6 -3
  147. package/src/format/format-host.ts +1 -1
  148. package/src/github/github-auth-status.ts +2 -2
  149. package/src/media/media-library.ts +73 -3
  150. package/src/new-project/new-project-modal.ts +1 -4
  151. package/src/packages/ensure-deps.ts +5 -5
  152. package/src/packages/pull-package-sync.ts +4 -4
  153. package/src/panels/activity-panel.ts +1 -1
  154. package/src/panels/ai-panel.ts +27 -34
  155. package/src/platform.ts +20 -0
  156. package/src/platforms/cloud.ts +48 -34
  157. package/src/platforms/devserver.ts +2 -120
  158. package/src/services/ai-ask.ts +29 -171
  159. package/src/services/ai-command-tools.ts +40 -50
  160. package/src/services/ai-import-tools.ts +4 -5
  161. package/src/services/ai-project-tools.ts +13 -3
  162. package/src/services/ai-tools.ts +26 -564
  163. package/src/services/ai-writes.ts +84 -2
  164. package/src/services/document-assistant.ts +29 -58
  165. package/src/services/harness/assistant-tools.ts +68 -0
  166. package/src/services/harness/interaction-store.ts +118 -0
  167. package/src/services/harness/studio-documents.ts +105 -0
  168. package/src/services/harness/turn-hooks.ts +1 -6
  169. package/src/services/project-adoption.ts +8 -16
  170. package/src/services/tool-executor.ts +25 -22
  171. package/src/settings/extension-commands.ts +1 -1
  172. package/src/settings/extension-rows.ts +1 -6
  173. package/src/settings/extensions-section.ts +3 -6
  174. package/src/state.ts +3 -13
  175. package/src/surfaces/block-action-bar.json +1 -3
  176. package/src/surfaces/files-panel.json +12 -0
  177. package/src/surfaces/files-panel.ts +5 -2
  178. package/src/surfaces/media-library.json +35 -0
  179. package/src/surfaces/media-library.ts +2 -0
  180. package/src/surfaces/menu.ts +62 -1
  181. package/src/surfaces/settings-packages.ts +1 -1
  182. package/src/surfaces/welcome.ts +4 -1
  183. package/src/tabs/selection.ts +14 -109
  184. package/src/tabs/tab.ts +10 -1
  185. package/src/tabs/transact.ts +316 -124
  186. package/src/types.ts +23 -16
  187. package/src/ui/layers.ts +1 -1
  188. package/src/ui/progress-modal.ts +3 -3
  189. package/dist/chunks/adapter-tck81zft.js +0 -4
  190. package/dist/chunks/add-new-flow-a1w0bnm6.js +0 -4
  191. package/dist/chunks/cf-account-picker-ce3kp3x1.js +0 -4
  192. package/dist/chunks/cf-settings-7ye6x7g2.js +0 -4
  193. package/dist/chunks/color-4gj49xq6.js +0 -4
  194. package/dist/chunks/components-qce0v93f.js +0 -4
  195. package/dist/chunks/content-source-jjx8kgzr.js +0 -4
  196. package/dist/chunks/editor-bkja5dbv.js +0 -4
  197. package/dist/chunks/entry-commands-1wh0btvg.js +0 -4
  198. package/dist/chunks/file-ops-6pe4z7an.js +0 -4
  199. package/dist/chunks/files-ybpp3j38.js +0 -4
  200. package/dist/chunks/format-host-3mtbhxk1.js +0 -4
  201. package/dist/chunks/fs-events-xfanfxds.js +0 -4
  202. package/dist/chunks/grid-open-dg5qmnaj.js +0 -4
  203. package/dist/chunks/iframe-host-j2bk2xkc.js +0 -4
  204. package/dist/chunks/index-fkrm7pdk.js +0 -4
  205. package/dist/chunks/jx-validate-w7v32f6a.js +0 -4
  206. package/dist/chunks/layers-g7j732cm.js +0 -4
  207. package/dist/chunks/layers-panel-gjrsxcck.js +0 -4
  208. package/dist/chunks/lit-html-jdfz9rhc.js +0 -4
  209. package/dist/chunks/media-library-9j0v4wmk.js +0 -4
  210. package/dist/chunks/media-picker-cpyyt2m0.js +0 -4
  211. package/dist/chunks/monaco-buffer-9wcme176.js +0 -4
  212. package/dist/chunks/notify-rahnnnv4.js +0 -4
  213. package/dist/chunks/parse-v77xtwv7.js +0 -4
  214. package/dist/chunks/platform-b2aqv7v5.js +0 -4
  215. package/dist/chunks/publish-panel-4yk2461b.js +0 -4
  216. package/dist/chunks/publish-workspace-mxestzcf.js +0 -4
  217. package/dist/chunks/quick-search-9g6z65b0.js +0 -4
  218. package/dist/chunks/references-c26enpxs.js +0 -4
  219. package/dist/chunks/right-panel-9d0e0yg7.js +0 -4
  220. package/dist/chunks/source-control-xyn3a8ds.js +0 -4
  221. package/dist/chunks/studio-0khd0xdw.js +0 -5
  222. package/dist/chunks/studio-1fvzq9vs.js +0 -5
  223. package/dist/chunks/studio-1yhz0s8p.js +0 -5
  224. package/dist/chunks/studio-2c2mjyzj.js +0 -6
  225. package/dist/chunks/studio-3pndhat0.js +0 -9
  226. package/dist/chunks/studio-4fjqdzrb.js +0 -6
  227. package/dist/chunks/studio-5abge3wy.js +0 -5
  228. package/dist/chunks/studio-74nzv280.js +0 -5
  229. package/dist/chunks/studio-7cg75gtj.js +0 -14
  230. package/dist/chunks/studio-89cwmnae.js +0 -5
  231. package/dist/chunks/studio-8m1t83kn.js +0 -7
  232. package/dist/chunks/studio-a3evp7f6.js +0 -6
  233. package/dist/chunks/studio-ana67wax.js +0 -5
  234. package/dist/chunks/studio-b4xhpcwy.js +0 -4
  235. package/dist/chunks/studio-c4rcaftm.js +0 -5
  236. package/dist/chunks/studio-cgjt1zce.js +0 -5
  237. package/dist/chunks/studio-cq87hree.js +0 -5
  238. package/dist/chunks/studio-d140sy8e.js +0 -5
  239. package/dist/chunks/studio-e1g64q43.js +0 -5
  240. package/dist/chunks/studio-e8pzjjhf.js +0 -5
  241. package/dist/chunks/studio-f8kz3krj.js +0 -7
  242. package/dist/chunks/studio-fw3jp9m7.js +0 -5
  243. package/dist/chunks/studio-g6ysm05p.js +0 -7
  244. package/dist/chunks/studio-gmhd0j3f.js +0 -5
  245. package/dist/chunks/studio-gqpty626.js +0 -5
  246. package/dist/chunks/studio-grp684wx.js +0 -5
  247. package/dist/chunks/studio-h98520qr.js +0 -5
  248. package/dist/chunks/studio-hvxt8z79.js +0 -5
  249. package/dist/chunks/studio-j4e627w1.js +0 -5
  250. package/dist/chunks/studio-kt2j51sa.js +0 -5
  251. package/dist/chunks/studio-m4w6sygj.js +0 -16
  252. package/dist/chunks/studio-nqxqf8p2.js +0 -4
  253. package/dist/chunks/studio-p89w12s2.js +0 -7
  254. package/dist/chunks/studio-pbagym4j.js +0 -5
  255. package/dist/chunks/studio-pr23qhrw.js +0 -5
  256. package/dist/chunks/studio-r4ya3sb2.js +0 -5
  257. package/dist/chunks/studio-rm9m3z60.js +0 -5
  258. package/dist/chunks/studio-tpmnbgmt.js +0 -5
  259. package/dist/chunks/studio-ttxhq5yt.js +0 -5
  260. package/dist/chunks/studio-y1x65h5r.js +0 -5
  261. package/dist/chunks/studio-zf188hnd.js +0 -5
  262. package/dist/chunks/toast-1gv0h53h.js +0 -4
  263. package/dist/chunks/workspace-3nt93k48.js +0 -4
  264. package/dist/chunks/workspace-home-zbgyqqws.js +0 -4
  265. package/src/services/directory-picker.ts +0 -187
  266. package/src/services/gated-registry.ts +0 -77
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "8.1.0",
3
+ "version": "10.0.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,7 +31,6 @@
31
31
  "./platform": "./src/platform.ts",
32
32
  "./types": "./src/types.ts",
33
33
  "./base64": "./src/utils/base64.ts",
34
- "./directory-picker": "./src/services/directory-picker.ts",
35
34
  "./import-client": "./src/services/import-client.ts",
36
35
  "./github-auth": "./src/github/github-auth.ts",
37
36
  "./github-auth-status": "./src/github/github-auth-status.ts",
@@ -61,16 +60,16 @@
61
60
  "dependencies": {
62
61
  "@atlaskit/pragmatic-drag-and-drop": "^4.0.0",
63
62
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^3.0.0",
64
- "@jxsuite/ai": "^0.40.0",
65
- "@jxsuite/collab": "^0.11.2",
66
- "@jxsuite/create": "^1.4.0",
67
- "@jxsuite/formulas": "^0.0.24",
68
- "@jxsuite/markup": "^1.0.0",
69
- "@jxsuite/protocol": "^4.0.0",
70
- "@jxsuite/runtime": "^5.2.0",
71
- "@jxsuite/schema": "^4.0.0",
72
- "@jxsuite/site": "^2.2.0",
73
- "@jxsuite/ui": "^0.8.0",
63
+ "@jxsuite/ai": "^0.41.1",
64
+ "@jxsuite/collab": "^0.11.4",
65
+ "@jxsuite/create": "^1.5.0",
66
+ "@jxsuite/formulas": "^0.0.26",
67
+ "@jxsuite/markup": "^1.0.2",
68
+ "@jxsuite/protocol": "^6.0.0",
69
+ "@jxsuite/runtime": "^6.0.1",
70
+ "@jxsuite/schema": "^5.0.0",
71
+ "@jxsuite/site": "^2.2.2",
72
+ "@jxsuite/ui": "^0.8.2",
74
73
  "@vue/reactivity": "3.5.44",
75
74
  "ajv": "^8.20.0",
76
75
  "ajv-formats": "^3.0.1",
@@ -445,8 +445,9 @@ export function startEditableRoot(
445
445
  * something no caret can enter, so the caret went on blinking in the block clicked BEFORE, with
446
446
  * the component now selected in the editor and every keystroke still typed into that block.
447
447
  */
448
- const onPointerDownCapture = (e: Event) => {
449
- if (!editingAllowed()) {
448
+ const onPointerDownCapture = (e: PointerEvent) => {
449
+ // A secondary press asks for the element menu, not an edit: see `onMouseDownCapture`.
450
+ if (!editingAllowed() || e.button === 2) {
450
451
  return;
451
452
  }
452
453
  const target = e.target instanceof Element ? e.target : null;
@@ -478,6 +479,33 @@ export function startEditableRoot(
478
479
  }
479
480
  };
480
481
 
482
+ /**
483
+ * A secondary press outside the active block selects, it never starts an edit.
484
+ *
485
+ * The whole canvas is one `contenteditable`, and a right-button press places the caret where it
486
+ * lands, as a left one does. That caret activated the block through `selectionchange`, so every
487
+ * right-click on a block also entered inline editing: the bridge's edit start ran ahead of the
488
+ * menu's mount in the same task, which held the element menu back by hundreds of milliseconds on
489
+ * a real page, and the block was left in edit mode after the menu closed. Cancelling the press's
490
+ * default keeps the caret where it was; `contextmenu` still fires, so the menu is unaffected. The
491
+ * block being edited keeps its native press, because the caret placed there is what the native
492
+ * spelling and edit menu reads. A right-click elsewhere leaves that block, as a left click
493
+ * would.
494
+ */
495
+ const onMouseDownCapture = (e: MouseEvent) => {
496
+ if (e.button !== 2 || !editingAllowed()) {
497
+ return;
498
+ }
499
+ if (activeEl && e.target instanceof Node && activeEl.contains(e.target)) {
500
+ return;
501
+ }
502
+ e.preventDefault();
503
+ if (activeEl) {
504
+ deactivate();
505
+ doc.getSelection()?.removeAllRanges();
506
+ }
507
+ };
508
+
481
509
  /**
482
510
  * Reordering is the block action bar's drag handle, full stop. A `contenteditable` region is
483
511
  * natively draggable by its text, which would both fight text selection and move content behind
@@ -501,6 +529,7 @@ export function startEditableRoot(
501
529
  container.addEventListener("compositionend", onCompositionEnd);
502
530
  container.addEventListener("dragstart", onDragStart);
503
531
  doc.addEventListener("pointerdown", onPointerDownCapture, true);
532
+ doc.addEventListener("mousedown", onMouseDownCapture, true);
504
533
 
505
534
  return {
506
535
  capture: () => captureDocSelection(container, deps.isEditableBlock),
@@ -542,6 +571,7 @@ export function startEditableRoot(
542
571
  container.removeEventListener("compositionend", onCompositionEnd);
543
572
  container.removeEventListener("dragstart", onDragStart);
544
573
  doc.removeEventListener("pointerdown", onPointerDownCapture, true);
574
+ doc.removeEventListener("mousedown", onMouseDownCapture, true);
545
575
  // Deactivating commits, so the pending tick is cancelled rather than fired.
546
576
  deactivate();
547
577
  },
@@ -26,7 +26,7 @@ import { jsonClone } from "../utils/studio-utils";
26
26
  import { cloneSelection } from "../tabs/selection";
27
27
  import {
28
28
  applyExternalDocOps,
29
- isBatching,
29
+ isBatched,
30
30
  setBatchEndNotifier,
31
31
  setHistoryDelegate,
32
32
  setTransactGate,
@@ -318,7 +318,11 @@ function onBatchEnd(tab: Tab): void {
318
318
 
319
319
  function publishRecord(session: ActiveSession, record: TransactionRecord): void {
320
320
  const forward = record.docOps.map((pair) => pair.forward);
321
- if (isBatching()) {
321
+ /* Buffered only while THIS tab has a lane segment open, which means the transaction just
322
+ observed was the lane owner's: anyone else's write closes the segment before it runs, and the
323
+ close flushes the buffer, so the Y document receives the owner's ops before the other writer's
324
+ and in local order. */
325
+ if (isBatched(session.tab)) {
322
326
  session.batchOps ??= [];
323
327
  if (forward.length === 0) {
324
328
  session.batchNeedsDiff = true;
@@ -31,6 +31,9 @@ export const CAPABILITIES = [
31
31
  "gitClone",
32
32
  "importSite",
33
33
  "openProjectInNewWindow",
34
+ /* Open Project's gate: a native dialog or the cloud's repository picker. A dev-server window opens
35
+ its project from `?project=`, so the command is not offered there. */
36
+ "openProject",
34
37
  "dataRows",
35
38
  "windowControls",
36
39
  "findReferences",
@@ -339,6 +342,7 @@ export function emptyContext(): CommandContext {
339
342
  gitClone: false,
340
343
  importSite: false,
341
344
  openProjectInNewWindow: false,
345
+ openProject: false,
342
346
  dataRows: false,
343
347
  windowControls: false,
344
348
  findReferences: false,
@@ -563,6 +563,8 @@ export function defaultCommands(deps: CommandDeps): AnyCommand[] {
563
563
  keybinding: "mod+o",
564
564
  menus: ["commandbar/overflow", "statusbar/project", "palette"],
565
565
  group: "1_file",
566
+ when: (ctx: CommandContext) => ctx.capability.openProject,
567
+ requires: "a host that can open a project from a dialog",
566
568
  run: () => deps.openProject(),
567
569
  },
568
570
  {
@@ -38,6 +38,7 @@ import { collabState } from "../collab/collab-state";
38
38
 
39
39
  import { canvasViewForMode, editorKindForMode, emptyContext } from "./context";
40
40
  import type { CommandContext } from "./context";
41
+ import { canOpenProject } from "../platform";
41
42
  import type { StudioPlatform } from "../types";
42
43
 
43
44
  /** Elements that own the keyboard while focused. Matches the old listener's target test exactly. */
@@ -95,6 +96,7 @@ function capabilities(platform: StudioPlatform | null): CommandContext["capabili
95
96
  findReferences: has("findReferences"),
96
97
  gitClone: has("gitClone"),
97
98
  importSite: has("importSite"),
99
+ openProject: canOpenProject(platform),
98
100
  openProjectInNewWindow: has("openProjectInNewWindow"),
99
101
  readFileBytes: has("readFileBytes"),
100
102
  publish: has("cfApi"),
@@ -39,7 +39,7 @@ import { errorMessage } from "@jxsuite/schema/parse";
39
39
  import { localeLabel, localeOfPath, resolveI18n } from "@jxsuite/schema/locale";
40
40
  import { showPromptDialog } from "../ui/layers";
41
41
  import { projectState, requireProjectState, setProjectState } from "../store";
42
- import { getPlatform } from "../platform";
42
+ import { canOpenProject, getPlatform } from "../platform";
43
43
  import { disarmPreviewOverlay } from "../preview/preview-overlay";
44
44
  import { notify } from "../services/notify";
45
45
  import { loadComponentRegistry } from "./components";
@@ -309,7 +309,8 @@ export async function openProject({
309
309
  }): Promise<boolean> {
310
310
  try {
311
311
  const platform = getPlatform();
312
- const result = await platform.openProject();
312
+ // The command is gated on the capability, so a host without the member never gets here.
313
+ const result = await platform.openProject?.();
313
314
  if (!result) {
314
315
  return false;
315
316
  } // User cancelled
@@ -864,7 +865,9 @@ function filesPanelValues(): FilesPanelValues {
864
865
  }
865
866
  if (!projectState.isSiteProject && projectState.projectRoot === ".") {
866
867
  _fileRows = [];
867
- const values: FilesPanelValues = { ...emptyFilesPanelValues(), view: "welcome" };
868
+ // A host with no Open Project (the dev server) is told how its windows DO open one instead.
869
+ const view = canOpenProject(getPlatform()) ? "welcome" : "address";
870
+ const values: FilesPanelValues = { ...emptyFilesPanelValues(), view };
868
871
  _lastValues = values;
869
872
  return values;
870
873
  }
@@ -217,7 +217,7 @@ async function shareProjectSchemas(
217
217
  */
218
218
  export function refreshExtensionUi(platform: Pick<StudioPlatform, "fetchProjectSchemas">): void {
219
219
  void shareProjectSchemas(platform);
220
- // `installed` and `bundled` are per-project facts, so an install or a project switch can move
220
+ // `installed` is a per-project fact, so an install or a project switch can move
221
221
  // Them even when the shipped half has not changed.
222
222
  void loadExtensionCatalog();
223
223
  void import("../settings/extension-sections")
@@ -21,8 +21,8 @@ const STORAGE_KEY = "jx_github_token";
21
21
  /**
22
22
  * The GitHub OAuth app every Jx sign-in authorizes against: the browser's device flow in
23
23
  * `github-auth.ts`, and the desktop launcher's RFC 8252 loopback redirect
24
- * (`packages/desktop/src/github-signin.ts`). Here, in the module with no UI dependency, because the
25
- * launcher's main process imports it too.
24
+ * (`packages/desktop/src/backend/github-signin.ts`). Here, in the module with no UI dependency,
25
+ * because the launcher's main process imports it too.
26
26
  */
27
27
  export const GITHUB_OAUTH_CLIENT_ID = "Ov23liYVlMFpgjOEPXJH";
28
28
 
@@ -67,6 +67,7 @@ import { dirName, previewFileSrc } from "../files/media-paths";
67
67
  import { forgetVersions } from "../files/asset-versions";
68
68
  import { loadMediaUsages, peekMediaUsages } from "../files/media-usage";
69
69
  import { onProjectMediaChange, projectMediaFiles } from "../ui/media-picker";
70
+ import { openMenu } from "../surfaces/menu";
70
71
  import { openMediaTab } from "./media-open";
71
72
  import { factsLine, mediaStageFor, specimenOf, usageOf } from "./media-pane";
72
73
  import {
@@ -93,6 +94,7 @@ import type {
93
94
  MediaTileRow,
94
95
  } from "../surfaces/media-library";
95
96
  import type { MediaPickDialogHandle } from "../surfaces/media-pick-dialog";
97
+ import type { MenuHandle, MenuRowProjection } from "../surfaces/menu";
96
98
  import type { PreviewObserver } from "../browse/library-preview";
97
99
  import type { Tab } from "../tabs/tab";
98
100
  import type { CanvasSurface } from "../canvas/canvas-surface";
@@ -788,11 +790,11 @@ function actionsFor(mount: LibraryMount) {
788
790
  activate(mount, selected);
789
791
  }
790
792
  },
793
+ contextMenu: (file: string, x: number, y: number) => showTileMenu(mount, file, x, y),
791
794
  copyRef: () => {
792
795
  const entry = selectedEntry(mount);
793
- // `navigator.clipboard` is [SecureContext]: a plain-http host has none, whatever lib.dom says.
794
- if (entry && "clipboard" in navigator) {
795
- void navigator.clipboard.writeText(entry.ref);
796
+ if (entry) {
797
+ copySiteUrl(entry);
796
798
  }
797
799
  },
798
800
  dragOut: () => {
@@ -913,6 +915,74 @@ function actionsFor(mount: LibraryMount) {
913
915
  }
914
916
 
915
917
  /** Enter or a double-click: open the viewer, or in pick mode, use the file. */
918
+ /** Put a file's site URL on the clipboard. */
919
+ function copySiteUrl(entry: ProjectMediaFile): void {
920
+ // `navigator.clipboard` is [SecureContext]: a plain-http host has none, whatever lib.dom says.
921
+ if ("clipboard" in navigator) {
922
+ void navigator.clipboard.writeText(entry.ref);
923
+ }
924
+ }
925
+
926
+ /**
927
+ * The tile menu that is up, if any. One handle, because only one can be open: a second kit menu
928
+ * opened over the first is how two of them light-dismiss each other.
929
+ */
930
+ let _tileMenu: MenuHandle | null = null;
931
+
932
+ /**
933
+ * A right-click on a tile: select it, then offer the detail pane's actions at the pointer.
934
+ *
935
+ * The same four verbs as the detail pane's (studio.md §22.6), in the order the pane draws them, and
936
+ * each calls what the pane's control calls, so the menu cannot drift from the pane. Selecting first
937
+ * is what makes the pane describe the file the menu acts on, as a right-click on a row does in the
938
+ * Content workspace.
939
+ */
940
+ function showTileMenu(mount: LibraryMount, file: string, x: number, y: number): void {
941
+ select(mount, file);
942
+ const entry = selectedEntry(mount);
943
+ if (!entry) {
944
+ return;
945
+ }
946
+ _tileMenu?.close();
947
+ const row = (
948
+ id: string,
949
+ title: string,
950
+ run: () => void,
951
+ extra: Partial<MenuRowProjection> = {},
952
+ ): MenuRowProjection => ({
953
+ destructive: false,
954
+ disabled: false,
955
+ dividerAbove: false,
956
+ id,
957
+ run,
958
+ title,
959
+ ...extra,
960
+ });
961
+ _tileMenu = openMenu({
962
+ label: `${entry.name} actions`,
963
+ onClosed: (handle) => {
964
+ if (_tileMenu === handle) {
965
+ _tileMenu = null;
966
+ }
967
+ },
968
+ origin: { x, y },
969
+ region: "media-tile",
970
+ rows: [
971
+ row("open", "Open in Viewer", () => openMediaTab(file)),
972
+ row("copy-url", "Copy Site URL", () => copySiteUrl(entry)),
973
+ row("replace", "Replace…", () => requestMediaReplace(file), { dividerAbove: true }),
974
+ row(
975
+ "delete",
976
+ "Delete",
977
+ () => {
978
+ void deleteMediaFile(file);
979
+ },
980
+ { destructive: true, dividerAbove: true },
981
+ ),
982
+ ],
983
+ });
984
+ }
985
+
916
986
  function activate(mount: LibraryMount, file: string): void {
917
987
  if (mount.mode === "pick") {
918
988
  const entry = selectedEntry(mount);
@@ -346,10 +346,7 @@ function finish(result: { root: string; config: ProjectConfig } | null) {
346
346
  * runs this same sequence itself.
347
347
  */
348
348
  async function finishCreated(result: { root: string; config: ProjectConfig }) {
349
- const { adopted, error, openedElsewhere } = await adoptCreatedProject(result.root, {
350
- // The wizard runs outside the agent loop — there is never a batch to flush and re-anchor.
351
- getTab: () => null,
352
- });
349
+ const { adopted, error, openedElsewhere } = await adoptCreatedProject(result.root);
353
350
  if (!adopted && !openedElsewhere) {
354
351
  notify.warn(
355
352
  "Created the project, but could not open it here.",
@@ -1,8 +1,8 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
3
  * Ensure the active project's dependencies are installed before the editor loads. When the backend
4
- * reports node_modules is missing, run `bun install` behind a blocking progress modal so component
5
- * and format resolution see the real dependency tree. No-op on platforms without package support.
4
+ * reports node_modules is missing, install them behind a blocking progress modal so component and
5
+ * format resolution see the real dependency tree. No-op on platforms without package support.
6
6
  *
7
7
  * It REPORTS whether an install actually ran, because "format resolution sees the real dependency
8
8
  * tree" was only half true: project activation fetches the format registry before calling this, and
@@ -17,7 +17,7 @@ import { shouldInstallAutomation } from "../services/automation";
17
17
 
18
18
  export async function ensureDependenciesInstalled(): Promise<boolean> {
19
19
  // Automation/screenshot runs open projects read-only to drive the canvas — they must never mutate
20
- // The project on disk (a `bun install` would litter node_modules/bun.lock into starter templates).
20
+ // The project on disk (an install would litter node_modules and a lockfile into starter templates).
21
21
  if (shouldInstallAutomation(location.search)) {
22
22
  return false;
23
23
  }
@@ -35,7 +35,7 @@ export async function ensureDependenciesInstalled(): Promise<boolean> {
35
35
  return false;
36
36
  }
37
37
  const progress = showProgressModal({
38
- status: "Running bun install…",
38
+ status: "Installing dependencies…",
39
39
  title: "Installing dependencies",
40
40
  });
41
41
  try {
@@ -44,7 +44,7 @@ export async function ensureDependenciesInstalled(): Promise<boolean> {
44
44
  progress.done();
45
45
  return true;
46
46
  }
47
- progress.fail(result.log ?? "bun install failed");
47
+ progress.fail(result.log ?? "Install failed");
48
48
  } catch (error) {
49
49
  progress.fail(error instanceof Error ? error.message : String(error));
50
50
  }
@@ -4,7 +4,7 @@
4
4
  * updates. The on-open @jxsuite update rewrites package.json and bun.lock without committing, so a
5
5
  * teammate pushing the same update makes a later `git pull` refuse to merge. Since those local
6
6
  * changes are machine-generated and reproducible, we discard them, pull, and re-run the package
7
- * sync (re-apply the update if the pulled project is still behind, or `bun install` if the pull
7
+ * sync (re-apply the update if the pulled project is still behind, or an install if the pull
8
8
  * brought it). Detection is state-based (git status + content comparison against `@{u}`), never git
9
9
  * stderr parsing, so it works identically across backends and locales.
10
10
  */
@@ -188,7 +188,7 @@ async function applyDiscard(plan: PackagePullPlan): Promise<void> {
188
188
 
189
189
  /**
190
190
  * Bring packages back in line after a pull that touched (or followed a discard of) package files:
191
- * re-apply the @jxsuite update if the pulled project is still behind, otherwise `bun install` so
191
+ * re-apply the @jxsuite update if the pulled project is still behind, otherwise an install so
192
192
  * node_modules matches the pulled lockfile. Never throws — a successful pull must not turn into an
193
193
  * error banner; failures surface through the progress modal.
194
194
  */
@@ -209,7 +209,7 @@ async function syncPackagesAfterPull(): Promise<void> {
209
209
  return;
210
210
  }
211
211
  const progress = showProgressModal({
212
- status: "Running bun install…",
212
+ status: "Installing dependencies…",
213
213
  title: "Syncing dependencies",
214
214
  });
215
215
  try {
@@ -217,7 +217,7 @@ async function syncPackagesAfterPull(): Promise<void> {
217
217
  if (result.ok) {
218
218
  progress.done();
219
219
  } else {
220
- progress.fail(result.log ?? "bun install failed");
220
+ progress.fail(result.log ?? "Install failed");
221
221
  }
222
222
  } catch (error) {
223
223
  progress.fail(errorMessage(error));
@@ -5,7 +5,7 @@
5
5
  * What this replaces: `ui/progress-modal.ts` was **the only surface in Studio with a real error
6
6
  * view** — a red headline, the captured log in a `<pre>`, an explicit Close — and it had four call
7
7
  * sites, all package operations. It blocked the whole app, offered no cancel, and vanished
8
- * completely on success, so a `bun install` that hung had no exit and one that failed had a log you
8
+ * completely on success, so an install that hung had no exit and one that failed had a log you
9
9
  * could read exactly once. Every OTHER long operation — git clone, publish, media upload, site
10
10
  * import, project open — got a three-second grey line or nothing at all.
11
11
  *
@@ -28,13 +28,13 @@
28
28
 
29
29
  import { effect, effectScope } from "../reactivity";
30
30
  import { createDocumentAssistant } from "../services/document-assistant";
31
- import { writesForTurn } from "../services/ai-writes";
31
+ import { restoreTurn } from "../services/ai-writes";
32
+ import { isString } from "@jxsuite/schema/guards";
32
33
  import { notify } from "../services/notify";
33
- import { undo } from "../tabs/transact";
34
+ import type { NotifyOptions } from "../services/notify";
34
35
  import { activeTab } from "../workspace/workspace";
35
36
  import { setOpenAiKey } from "../services/ai-settings";
36
37
  import { hasAiCredentials } from "../services/ai-models";
37
- import { answerAsk, isAwaitingAnswer, pendingAsk, skipAsk } from "../services/ai-ask";
38
38
  import { activeImportRun, importRun } from "../services/import-run";
39
39
  import type { ImportBrief } from "../services/import-seed";
40
40
  import type { ImportBreakpointPolicy } from "../types";
@@ -124,7 +124,7 @@ export function bindAiPanelHost(el: HTMLElement | null) {
124
124
  run: runCommand,
125
125
  send: () => composer.send(),
126
126
  skip: () => {
127
- skipAsk();
127
+ assistant.interactions.skip();
128
128
  renderAiPanel();
129
129
  },
130
130
  stop,
@@ -208,10 +208,10 @@ function watchAssistant() {
208
208
  // The registry is composed AFTER the bootstrap mounts this, and it is a reactive holder —
209
209
  // Reading it here is what turns the header's skeleton into its real buttons.
210
210
  void activeRegistry();
211
- // The question is not chat state — it lives in `services/ai-ask.ts` — but it is drawn into
212
- // This panel, so the same effect has to track it or a question would appear a turn late
213
- // (and its ANSWER would never repaint the card at all).
214
- void pendingAsk();
211
+ // The question is not chat state — it lives in the assistant's interaction store — but it is
212
+ // Drawn into this panel, so the same effect has to track it or a question would appear a turn
213
+ // Late (and its ANSWER would never repaint the card at all).
214
+ void assistant.interactions.pending;
215
215
  /* The run record, for the same reason: an import reports a line at a time from a store that
216
216
  is not chat state, and its chip has to follow every one of them. Reading the ACTIVE run
217
217
  tracks the whole record, so any field moving re-projects. */
@@ -268,7 +268,7 @@ async function handleAssistantSend(text: string) {
268
268
  }
269
269
  /* Answering comes first: a question is put by a turn that is still running, so a send while one
270
270
  waits is its answer, never a second turn. */
271
- if (answerAsk(text.trim())) {
271
+ if (assistant.interactions.answer(text.trim())) {
272
272
  surface?.pin();
273
273
  renderAiPanel();
274
274
  return;
@@ -319,38 +319,31 @@ async function handleRetry(): Promise<void> {
319
319
  /**
320
320
  * Undo everything one assistant turn changed — ai.md §3.2's "Restore to here".
321
321
  *
322
- * The loop opens one batch per turn per document, so undoing the turn is undoing that batch. The
323
- * button is offered by the projection only when every recorded change was transactional; this guard
324
- * is the second half of the same promise, because a ledger can be trimmed (MAX_TURNS) between the
325
- * projection and the click and a Restore that silently restored SOME of a turn would be worse than
326
- * one that refused.
322
+ * The turn's batch lane left one step per document it wrote, and {@link restoreTurn} takes each
323
+ * back while it is still the top of its tab's history, or refuses the whole turn with a sentence.
324
+ * The button is offered by the projection only when every recorded change was transactional; the
325
+ * refusals are the second half of the same promise, because a ledger can be trimmed (MAX_TURNS) and
326
+ * a document edited between the projection and the click.
327
327
  *
328
328
  * @param {string} messageId
329
329
  */
330
330
  /* Exported so the guard can be exercised directly: the button is not DRAWN for a turn that touched
331
331
  disk, which would otherwise make the refusal path unreachable from the panel. */
332
332
  export function handleRestore(messageId: string): void {
333
- const writes = writesForTurn(messageId);
334
- if (writes.length === 0) {
335
- notify.warn("There is no longer a record of what that turn changed.", { source: "Assistant" });
333
+ const restored = restoreTurn(messageId);
334
+ if (isString(restored)) {
335
+ notify.warn(restored, { source: "Assistant" });
336
336
  return;
337
337
  }
338
- const disk = writes.filter((w) => w.disk).map((w) => w.path);
339
- if (disk.length > 0) {
340
- notify.warn(
341
- `Cannot restore: ${disk.join(", ")} ${disk.length === 1 ? "was" : "were"} written straight ` +
342
- "to disk, which undo cannot reach.",
343
- { source: "Assistant" },
344
- );
345
- return;
346
- }
347
- const tab = activeTab.value;
348
- if (!tab) {
349
- notify.warn("Open the document that turn edited to restore it.", { source: "Assistant" });
350
- return;
338
+ /* Redo reaches the active tab only, so it is offered only when that is the one tab restored;
339
+ across documents it would bring back part of the turn. */
340
+ const [only, ...rest] = restored;
341
+ const redoable = rest.length === 0 && only === activeTab.value;
342
+ const options: NotifyOptions = { source: "Assistant" };
343
+ if (redoable) {
344
+ options.action = "edit.redo";
351
345
  }
352
- undo(tab);
353
- notify.success("Restored to before that turn.", { action: "edit.redo", source: "Assistant" });
346
+ notify.success("Restored to before that turn.", options);
354
347
  renderAiPanel();
355
348
  }
356
349
 
@@ -509,7 +502,7 @@ export function revealAssistant(): void {
509
502
  * turn waiting forever on a question nobody wants to answer is exactly what Stop is for.
510
503
  */
511
504
  export function isAssistantWaiting(): boolean {
512
- return isAwaitingAnswer();
505
+ return assistant.interactions.pending !== null;
513
506
  }
514
507
 
515
508
  /**
@@ -617,7 +610,7 @@ function projectPanel(): AiChatView {
617
610
  const advice = error ? formatErrorAdvice(error) : "";
618
611
  const newChatCommand = projectCommand("assistant.newChat", { icon: "plus" });
619
612
  const rows = projectRows({
620
- ask: { importRun, pendingId: pendingAsk()?.id ?? null },
613
+ ask: { importRun, pendingId: assistant.interactions.pending?.callId ?? null },
621
614
  messages: cs.messages,
622
615
  status: cs.status,
623
616
  });
package/src/platform.ts CHANGED
@@ -50,6 +50,26 @@ declare global {
50
50
  var __jxLauncher: LauncherSignal | undefined;
51
51
  }
52
52
 
53
+ /**
54
+ * Whether this host offers Open Project at all: a native dialog (`openProject`), or the cloud's
55
+ * repository picker. The dev server has neither, because its windows open a project from the
56
+ * `?project=` URL parameter, so every surface that offers Open Project asks this first rather than
57
+ * drawing a control that cannot do anything.
58
+ */
59
+ export function canOpenProject(platform: StudioPlatform | null | undefined): boolean {
60
+ if (!platform) {
61
+ return false;
62
+ }
63
+ if (isFunction(platform.openProject)) {
64
+ return true;
65
+ }
66
+ return (
67
+ platform.openProjectPicker === "repo-list" &&
68
+ isFunction(platform.listRepos) &&
69
+ isFunction(platform.importProject)
70
+ );
71
+ }
72
+
53
73
  /** @param {StudioPlatform} platform */
54
74
  export function registerPlatform(platform: StudioPlatform) {
55
75
  globalThis.__jxPlatform = platform;