@jxsuite/studio 2.0.0 → 2.1.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 (419) hide show
  1. package/canvas.html +21 -0
  2. package/dist/chunks/Overlay.dev-qfjzkms8.js +23 -0
  3. package/dist/chunks/{Overlay.dev-cszx7yn5.js.map → Overlay.dev-qfjzkms8.js.map} +1 -1
  4. package/dist/chunks/{adapter-z8fkmbmv.js → adapter-qfnesn5v.js} +2 -2
  5. package/dist/chunks/{cf-settings-mmyv436d.js → cf-settings-73sh5z4p.js} +3 -3
  6. package/dist/chunks/{components-0hzentwq.js → components-2bpqp4mq.js} +5 -7
  7. package/dist/chunks/{components-0hzentwq.js.map → components-2bpqp4mq.js.map} +1 -1
  8. package/dist/chunks/content-source-34gnkbnn.js +38 -0
  9. package/dist/chunks/content-source-34gnkbnn.js.map +9 -0
  10. package/dist/chunks/convert-to-repeater-t8kxre5s.js +258 -0
  11. package/dist/chunks/convert-to-repeater-t8kxre5s.js.map +10 -0
  12. package/dist/chunks/extension-sections-6mqydz35.js +301 -0
  13. package/dist/chunks/extension-sections-6mqydz35.js.map +11 -0
  14. package/dist/chunks/files-8kn321h1.js +70 -0
  15. package/dist/chunks/files-8kn321h1.js.map +9 -0
  16. package/dist/chunks/{format-host-2c188n41.js → format-host-mkwsmt8d.js} +3 -3
  17. package/dist/chunks/git-panel-tjbr191d.js +51 -0
  18. package/dist/chunks/git-panel-tjbr191d.js.map +9 -0
  19. package/dist/chunks/{index-baeg2x3y.js → index-7pxchcce.js} +4 -3
  20. package/dist/chunks/{index-baeg2x3y.js.map → index-7pxchcce.js.map} +3 -3
  21. package/dist/chunks/{jx-validate-qx8degj7.js → jx-validate-v6qgknrs.js} +3 -3
  22. package/dist/chunks/layers-panel-87pb25km.js +58 -0
  23. package/dist/chunks/layers-panel-87pb25km.js.map +9 -0
  24. package/dist/chunks/{monaco-setup-zjdp9zdw.js → monaco-setup-e0yh25j4.js} +4 -75
  25. package/dist/chunks/{monaco-setup-zjdp9zdw.js.map → monaco-setup-e0yh25j4.js.map} +3 -4
  26. package/dist/chunks/publish-panel-m0fcgdrt.js +21 -0
  27. package/dist/chunks/publish-panel-m0fcgdrt.js.map +9 -0
  28. package/dist/chunks/quick-search-rhs6cbsb.js +53 -0
  29. package/dist/chunks/quick-search-rhs6cbsb.js.map +9 -0
  30. package/dist/chunks/right-panel-zpdxd98t.js +37 -0
  31. package/dist/chunks/right-panel-zpdxd98t.js.map +9 -0
  32. package/dist/chunks/section-registry-zmckaxf4.js +30 -0
  33. package/dist/chunks/section-registry-zmckaxf4.js.map +9 -0
  34. package/dist/chunks/sp-overlay.dev-9ran0sfq.js +18 -0
  35. package/dist/chunks/sp-overlay.dev-9ran0sfq.js.map +9 -0
  36. package/dist/chunks/sp-popover.dev-rebmdncr.js +9 -0
  37. package/dist/chunks/sp-popover.dev-rebmdncr.js.map +9 -0
  38. package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js +15 -0
  39. package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js.map +9 -0
  40. package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js +15 -0
  41. package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js.map +9 -0
  42. package/dist/chunks/{sp-tray.dev-pb2fsd65.js → sp-tray.dev-ey59vfh7.js} +10 -8
  43. package/dist/chunks/{sp-tray.dev-pb2fsd65.js.map → sp-tray.dev-ey59vfh7.js.map} +2 -2
  44. package/dist/chunks/{studio-9qr6fq7p.js → studio-0rqqhrdy.js} +10 -378
  45. package/dist/chunks/{studio-9qr6fq7p.js.map → studio-0rqqhrdy.js.map} +4 -14
  46. package/dist/chunks/{studio-5x6tqqec.js → studio-2sj1jj3v.js} +10 -8
  47. package/dist/chunks/{studio-5x6tqqec.js.map → studio-2sj1jj3v.js.map} +2 -2
  48. package/dist/chunks/{studio-h3fw0wn8.js → studio-3fk1ac9z.js} +6 -4
  49. package/dist/chunks/{studio-h3fw0wn8.js.map → studio-3fk1ac9z.js.map} +2 -2
  50. package/dist/chunks/studio-41exgzhq.js +6690 -0
  51. package/dist/chunks/studio-41exgzhq.js.map +38 -0
  52. package/dist/chunks/{studio-7v2bpweq.js → studio-4mqb9a0j.js} +6 -4
  53. package/dist/chunks/{studio-7v2bpweq.js.map → studio-4mqb9a0j.js.map} +2 -2
  54. package/dist/chunks/studio-50k77cca.js +1787 -0
  55. package/dist/chunks/studio-50k77cca.js.map +18 -0
  56. package/dist/chunks/{studio-56v3zwk6.js → studio-6a0h7sne.js} +10 -8
  57. package/dist/chunks/{studio-56v3zwk6.js.map → studio-6a0h7sne.js.map} +2 -2
  58. package/dist/chunks/studio-6f0am8f2.js +53 -0
  59. package/dist/chunks/studio-6f0am8f2.js.map +10 -0
  60. package/dist/chunks/studio-6nmjc5qk.js +7002 -0
  61. package/dist/chunks/studio-6nmjc5qk.js.map +32 -0
  62. package/dist/chunks/studio-7afd21nw.js +67 -0
  63. package/dist/chunks/studio-7afd21nw.js.map +10 -0
  64. package/dist/chunks/{studio-xze4zrs1.js → studio-7bmdvzne.js} +2 -2
  65. package/dist/chunks/studio-7bmdvzne.js.map +10 -0
  66. package/dist/chunks/studio-8jpzqk5s.js +128 -0
  67. package/dist/chunks/studio-8jpzqk5s.js.map +10 -0
  68. package/dist/chunks/studio-94m77tgk.js +1482 -0
  69. package/dist/chunks/studio-94m77tgk.js.map +13 -0
  70. package/dist/chunks/studio-9je6w5sq.js +1000 -0
  71. package/dist/chunks/studio-9je6w5sq.js.map +16 -0
  72. package/dist/chunks/{studio-w7c6kvk7.js → studio-a7pzveq1.js} +2 -2
  73. package/dist/chunks/{studio-97qcgvdn.js → studio-adn0z1k3.js} +14 -8
  74. package/dist/chunks/{studio-97qcgvdn.js.map → studio-adn0z1k3.js.map} +3 -3
  75. package/dist/chunks/{studio-kr88ryr7.js → studio-bsxxx3x2.js} +17 -15
  76. package/dist/chunks/{studio-kr88ryr7.js.map → studio-bsxxx3x2.js.map} +7 -6
  77. package/dist/chunks/studio-bw5dz6km.js +608 -0
  78. package/dist/chunks/studio-bw5dz6km.js.map +21 -0
  79. package/dist/chunks/studio-c6s5getc.js +4232 -0
  80. package/dist/chunks/studio-c6s5getc.js.map +39 -0
  81. package/dist/chunks/studio-cjm7fdjf.js +451 -0
  82. package/dist/chunks/studio-cjm7fdjf.js.map +14 -0
  83. package/dist/chunks/{studio-cx91mnh8.js → studio-ckwz4d3x.js} +53 -53
  84. package/dist/chunks/{studio-cx91mnh8.js.map → studio-ckwz4d3x.js.map} +5 -5
  85. package/dist/chunks/studio-dg7zyt5x.js +28 -0
  86. package/dist/chunks/studio-dg7zyt5x.js.map +10 -0
  87. package/dist/chunks/studio-dzcyvvvz.js +166 -0
  88. package/dist/chunks/studio-dzcyvvvz.js.map +10 -0
  89. package/dist/chunks/{studio-7kqe41sa.js → studio-evkr5d3b.js} +15 -7
  90. package/dist/chunks/{studio-7kqe41sa.js.map → studio-evkr5d3b.js.map} +5 -4
  91. package/dist/chunks/studio-f12h6177.js +411 -0
  92. package/dist/chunks/studio-f12h6177.js.map +13 -0
  93. package/dist/chunks/studio-hbp1s8m9.js +1817 -0
  94. package/dist/chunks/studio-hbp1s8m9.js.map +14 -0
  95. package/dist/chunks/studio-k2sxjph5.js +15295 -0
  96. package/dist/chunks/studio-k2sxjph5.js.map +235 -0
  97. package/dist/chunks/studio-myje51t3.js +385 -0
  98. package/dist/chunks/studio-myje51t3.js.map +12 -0
  99. package/dist/chunks/studio-na6rwzhw.js +353 -0
  100. package/dist/chunks/studio-na6rwzhw.js.map +11 -0
  101. package/dist/chunks/{studio-4k0m4fmp.js → studio-p471gg5n.js} +126 -3
  102. package/dist/chunks/studio-p471gg5n.js.map +14 -0
  103. package/dist/chunks/{studio-9bjwf0d4.js → studio-rgwcpast.js} +5 -46
  104. package/dist/chunks/{studio-9bjwf0d4.js.map → studio-rgwcpast.js.map} +4 -5
  105. package/dist/chunks/{studio-hhj92gcr.js → studio-s519zc99.js} +3 -3
  106. package/dist/chunks/studio-sas7mt3k.js +75 -0
  107. package/dist/chunks/studio-sas7mt3k.js.map +10 -0
  108. package/dist/chunks/studio-t5wqwzdh.js +74 -0
  109. package/dist/chunks/studio-t5wqwzdh.js.map +10 -0
  110. package/dist/chunks/{studio-9qpspwfn.js → studio-temv5bw4.js} +5 -5
  111. package/dist/chunks/{studio-9qpspwfn.js.map → studio-temv5bw4.js.map} +1 -1
  112. package/dist/chunks/{studio-qkvt37c0.js → studio-tr20m8g8.js} +669 -18
  113. package/dist/chunks/studio-tr20m8g8.js.map +9 -0
  114. package/dist/chunks/{studio-ygzav7z5.js → studio-v96qfnww.js} +5514 -4291
  115. package/dist/chunks/studio-v96qfnww.js.map +54 -0
  116. package/dist/chunks/studio-vcetc91k.js +659 -0
  117. package/dist/chunks/studio-vcetc91k.js.map +10 -0
  118. package/dist/chunks/{studio-n9zmpxjg.js → studio-vmymjefp.js} +2972 -4937
  119. package/dist/chunks/studio-vmymjefp.js.map +98 -0
  120. package/dist/chunks/{studio-bghqfmea.js → studio-wwseqyw0.js} +2 -2
  121. package/dist/chunks/studio-x9hmy5kr.js +390 -0
  122. package/dist/chunks/studio-x9hmy5kr.js.map +10 -0
  123. package/dist/chunks/studio-xa0ce0n7.js +335 -0
  124. package/dist/chunks/studio-xa0ce0n7.js.map +10 -0
  125. package/dist/chunks/studio-xm3dbxvn.js +378 -0
  126. package/dist/chunks/studio-xm3dbxvn.js.map +19 -0
  127. package/dist/chunks/{studio-nqw433kv.js → studio-xmvbhg31.js} +2 -2
  128. package/dist/chunks/studio-zv37yrfa.js +1054 -0
  129. package/dist/chunks/studio-zv37yrfa.js.map +13 -0
  130. package/dist/chunks/{ws-client-q7hkz3j9.js → ws-client-654ts66j.js} +6 -4
  131. package/dist/chunks/ws-client-654ts66j.js.map +11 -0
  132. package/dist/iframe-entry.js +668 -230
  133. package/dist/studio.js +23711 -35185
  134. package/index.html +35 -4685
  135. package/package.json +17 -15
  136. package/src/about/about-modal.ts +42 -11
  137. package/src/browse/library-commands.ts +240 -0
  138. package/src/browse/library-layouts.ts +369 -0
  139. package/src/browse/library-model.ts +396 -0
  140. package/src/browse/library-pane.ts +1049 -0
  141. package/src/browse/library-preview.ts +252 -0
  142. package/src/browse/library-source.ts +150 -0
  143. package/src/canvas/canvas-helpers.ts +33 -9
  144. package/src/canvas/canvas-live-render.ts +45 -29
  145. package/src/canvas/canvas-patcher.ts +151 -31
  146. package/src/canvas/canvas-perf.ts +46 -7
  147. package/src/canvas/canvas-render.ts +1092 -220
  148. package/src/canvas/canvas-surface.ts +240 -0
  149. package/src/canvas/canvas-utils.ts +1166 -124
  150. package/src/canvas/iframe-drop.ts +2 -1
  151. package/src/canvas/iframe-entry.ts +241 -4
  152. package/src/canvas/iframe-host.ts +1226 -143
  153. package/src/canvas/iframe-inline-edit.ts +31 -0
  154. package/src/canvas/iframe-interaction.ts +76 -16
  155. package/src/canvas/iframe-keys.ts +107 -45
  156. package/src/canvas/iframe-overlay.ts +38 -1
  157. package/src/canvas/iframe-protocol.ts +137 -2
  158. package/src/canvas/iframe-render.ts +70 -2
  159. package/src/canvas/iframe-slash.ts +6 -0
  160. package/src/canvas/path-mapping.ts +56 -6
  161. package/src/canvas/surface-registry.ts +292 -0
  162. package/src/collab/collab-commands.ts +244 -0
  163. package/src/collab/collab-session.ts +198 -32
  164. package/src/collab/collab-state.ts +28 -2
  165. package/src/collab/presence-chips.ts +100 -9
  166. package/src/commands/active-registry.ts +42 -0
  167. package/src/commands/app-commands.ts +176 -0
  168. package/src/commands/budget.ts +158 -0
  169. package/src/commands/command-args.ts +253 -0
  170. package/src/commands/context.ts +387 -0
  171. package/src/commands/defaults.ts +580 -0
  172. package/src/commands/keymap.ts +571 -0
  173. package/src/commands/levels.ts +345 -0
  174. package/src/commands/live-context.ts +193 -0
  175. package/src/commands/reference.ts +227 -0
  176. package/src/commands/registry.ts +293 -0
  177. package/src/component-props.ts +3 -2
  178. package/src/content/draft-state.ts +106 -0
  179. package/src/content/entry-commands.ts +282 -0
  180. package/src/content/entry-editor.ts +327 -0
  181. package/src/content/entry-fields.ts +116 -0
  182. package/src/content/entry-model.ts +175 -0
  183. package/src/editor/canvas-context-menu.ts +17 -11
  184. package/src/editor/canvas-slash-bridge.ts +11 -4
  185. package/src/editor/context-menu.ts +804 -204
  186. package/src/editor/convert-to-component.ts +31 -10
  187. package/src/editor/convert-to-repeater.ts +3 -2
  188. package/src/editor/inline-edit-apply.ts +5 -5
  189. package/src/editor/inline-edit.ts +86 -50
  190. package/src/editor/insert-zone-action.ts +1 -1
  191. package/src/editor/shortcuts.ts +1043 -329
  192. package/src/editor/slash-menu.ts +1 -0
  193. package/src/files/elements.ts +166 -0
  194. package/src/files/file-ops.ts +178 -31
  195. package/src/files/files.ts +898 -268
  196. package/src/files/fs-events.ts +21 -0
  197. package/src/files/media-meta.ts +203 -0
  198. package/src/files/media-paths.ts +141 -0
  199. package/src/files/media-upload.ts +7 -3
  200. package/src/files/media-usage.ts +189 -0
  201. package/src/github/github-auth.ts +235 -33
  202. package/src/github/github-publish.ts +80 -21
  203. package/src/grid/grid-controller.ts +152 -12
  204. package/src/grid/grid-layout.ts +225 -20
  205. package/src/grid/grid-open.ts +120 -0
  206. package/src/grid/grid-panel.ts +554 -39
  207. package/src/grid/grid-source.ts +27 -1
  208. package/src/grid/redirects-grid.ts +599 -0
  209. package/src/grid/redirects.ts +398 -0
  210. package/src/grid/sources/content-source.ts +108 -2
  211. package/src/i18n/i18n-commands.ts +322 -0
  212. package/src/new-project/add-repo-modal.ts +8 -10
  213. package/src/new-project/import-tab.ts +34 -9
  214. package/src/new-project/location-fields.ts +9 -1
  215. package/src/new-project/new-project-modal.ts +270 -262
  216. package/src/packages/jxsuite-update.ts +97 -48
  217. package/src/packages/pull-package-sync.ts +18 -9
  218. package/src/page-params.ts +2 -1
  219. package/src/panels/activity-bar.ts +152 -91
  220. package/src/panels/activity-panel.ts +481 -0
  221. package/src/panels/ai-chat/chat-markdown.ts +8 -3
  222. package/src/panels/ai-chat/chat-view.ts +245 -18
  223. package/src/panels/ai-chat/composer.ts +31 -5
  224. package/src/panels/ai-chat/sessions-view.ts +17 -10
  225. package/src/panels/ai-panel.ts +279 -120
  226. package/src/panels/block-action-bar.ts +1115 -224
  227. package/src/panels/bottom-dock.ts +337 -0
  228. package/src/panels/canvas-dnd-bridge.ts +37 -8
  229. package/src/panels/chat-panel.ts +29 -12
  230. package/src/panels/data-explorer.ts +254 -112
  231. package/src/panels/data-grid.ts +10 -4
  232. package/src/panels/dnd.ts +28 -51
  233. package/src/panels/editors.ts +434 -107
  234. package/src/panels/elements-panel.ts +66 -2
  235. package/src/panels/empty-state.ts +112 -0
  236. package/src/panels/events-panel.ts +596 -82
  237. package/src/panels/formula-workspace.ts +408 -218
  238. package/src/panels/frontmatter-fields.ts +51 -10
  239. package/src/panels/frontmatter-panel.ts +357 -79
  240. package/src/panels/git-panel.ts +375 -164
  241. package/src/panels/head-panel.ts +791 -85
  242. package/src/panels/i18n-panel.ts +592 -0
  243. package/src/panels/imports-panel.ts +95 -85
  244. package/src/panels/jump-bar.ts +584 -0
  245. package/src/panels/layers-panel.ts +1106 -328
  246. package/src/panels/left-panel.ts +109 -204
  247. package/src/panels/navigator-panels.ts +131 -0
  248. package/src/panels/overlays.ts +7 -2
  249. package/src/panels/pane-context.ts +1477 -0
  250. package/src/panels/pane-grid.ts +406 -0
  251. package/src/panels/panel-registry.ts +340 -0
  252. package/src/panels/panel-scheduler.ts +75 -0
  253. package/src/panels/problems-panel.ts +234 -0
  254. package/src/panels/properties-panel.ts +946 -1139
  255. package/src/panels/provenance.ts +258 -0
  256. package/src/panels/quick-search.ts +791 -122
  257. package/src/panels/right-panel.ts +228 -102
  258. package/src/panels/seo-modal.ts +395 -0
  259. package/src/panels/settings-pane.ts +203 -0
  260. package/src/panels/signals-panel.ts +354 -137
  261. package/src/panels/statement-editor.ts +41 -25
  262. package/src/panels/statusbar.ts +342 -106
  263. package/src/panels/style-panel.ts +974 -463
  264. package/src/panels/style-utils.ts +3 -3
  265. package/src/panels/stylebook-doc.ts +4 -2
  266. package/src/panels/stylebook-layers-panel.ts +3 -2
  267. package/src/panels/stylebook-panel.ts +51 -26
  268. package/src/panels/tab-strip.ts +1058 -78
  269. package/src/panels/target-line.ts +274 -0
  270. package/src/panels/toolbar.ts +631 -484
  271. package/src/panels/welcome-screen.ts +301 -198
  272. package/src/platform-errors.ts +26 -3
  273. package/src/platform.ts +96 -2
  274. package/src/platforms/cloud.ts +71 -8
  275. package/src/platforms/devserver.ts +92 -29
  276. package/src/publish/deploy-checklist.ts +383 -0
  277. package/src/publish/pages-service.ts +10 -1
  278. package/src/publish/publish-commands.ts +218 -0
  279. package/src/publish/publish-panel.ts +110 -16
  280. package/src/recent-projects.ts +3 -2
  281. package/src/services/a11y-report.ts +505 -0
  282. package/src/services/ai-models.ts +75 -2
  283. package/src/services/ai-project-tools.ts +29 -10
  284. package/src/services/ai-system-prompt.ts +53 -14
  285. package/src/services/ai-tools.ts +39 -3
  286. package/src/services/ai-writes.ts +154 -0
  287. package/src/services/announce.ts +97 -0
  288. package/src/services/automation.ts +424 -175
  289. package/src/services/cem-export.ts +9 -4
  290. package/src/services/clock.ts +61 -0
  291. package/src/services/document-assistant.ts +36 -8
  292. package/src/services/idle.ts +179 -0
  293. package/src/services/import-client.ts +26 -0
  294. package/src/services/monaco-buffer.ts +627 -0
  295. package/src/services/monaco-lazy.ts +36 -0
  296. package/src/services/notify.ts +271 -0
  297. package/src/services/profile.ts +155 -0
  298. package/src/services/references.ts +0 -0
  299. package/src/services/token-lint.ts +3 -2
  300. package/src/services/tool-executor.ts +53 -7
  301. package/src/services/trusted-types.ts +123 -0
  302. package/src/settings/contexts-section.ts +427 -0
  303. package/src/settings/contributed-section.ts +149 -11
  304. package/src/settings/css-vars-editor.ts +306 -349
  305. package/src/settings/defs-editor.ts +93 -34
  306. package/src/settings/dependencies-editor.ts +7 -7
  307. package/src/settings/extension-sections.ts +37 -5
  308. package/src/settings/general-settings.ts +204 -121
  309. package/src/settings/locales-section.ts +347 -0
  310. package/src/settings/preferences-accounts.ts +140 -0
  311. package/src/settings/preferences-dialog.ts +620 -0
  312. package/src/settings/preferences-keymap.ts +270 -0
  313. package/src/settings/project-sections.ts +260 -0
  314. package/src/settings/section-registry.ts +169 -0
  315. package/src/settings/settings-document.ts +385 -0
  316. package/src/shell.ts +1549 -0
  317. package/src/site-context.ts +45 -20
  318. package/src/state.ts +14 -275
  319. package/src/store.ts +88 -43
  320. package/src/studio.ts +790 -346
  321. package/src/style/live-preview.ts +36 -0
  322. package/src/style/project-styles.ts +349 -0
  323. package/src/style/token-ref.ts +123 -0
  324. package/src/tabs/patch-ops.ts +16 -4
  325. package/src/tabs/project-config.ts +416 -0
  326. package/src/tabs/selection.ts +323 -0
  327. package/src/tabs/tab.ts +182 -47
  328. package/src/tabs/transact.ts +209 -28
  329. package/src/types.ts +68 -13
  330. package/src/ui/ai-credentials-form.ts +39 -35
  331. package/src/ui/ai-managed-connect.ts +113 -0
  332. package/src/ui/dynamic-slot.ts +224 -81
  333. package/src/ui/expression-editor.ts +71 -63
  334. package/src/ui/field-input.ts +4 -6
  335. package/src/ui/field-row.ts +215 -38
  336. package/src/ui/form-controls.ts +194 -7
  337. package/src/ui/layers.ts +492 -69
  338. package/src/ui/media-picker.ts +115 -25
  339. package/src/ui/panel-resize.ts +109 -91
  340. package/src/ui/progress-modal.ts +131 -50
  341. package/src/ui/regions.ts +462 -0
  342. package/src/ui/schema-form.ts +229 -45
  343. package/src/ui/spectrum.ts +24 -8
  344. package/src/ui/timing.ts +49 -0
  345. package/src/ui/value-selector.ts +20 -0
  346. package/src/ui/value-source.ts +348 -0
  347. package/src/ui/virtual-window.ts +400 -0
  348. package/src/utils/edit-display.ts +3 -2
  349. package/src/utils/inherited-style.ts +67 -37
  350. package/src/view.ts +64 -78
  351. package/src/workspace/pane-derive.ts +1258 -0
  352. package/src/workspace/session.ts +317 -0
  353. package/src/workspace/workspace.ts +1345 -23
  354. package/styles/canvas.css +653 -0
  355. package/styles/forced-colors.css +106 -0
  356. package/styles/inspector.css +1454 -0
  357. package/styles/overlays.css +2704 -0
  358. package/styles/panels.css +1701 -0
  359. package/styles/shell.css +1423 -0
  360. package/styles/tokens.css +151 -0
  361. package/dist/chunks/Overlay.dev-cszx7yn5.js +0 -22
  362. package/dist/chunks/extension-sections-qct0gt8f.js +0 -531
  363. package/dist/chunks/extension-sections-qct0gt8f.js.map +0 -12
  364. package/dist/chunks/files-02zn5y24.js +0 -50
  365. package/dist/chunks/files-02zn5y24.js.map +0 -9
  366. package/dist/chunks/git-panel-mbb0y0hz.js +0 -28
  367. package/dist/chunks/git-panel-mbb0y0hz.js.map +0 -9
  368. package/dist/chunks/layers-panel-9qekmqk1.js +0 -26
  369. package/dist/chunks/layers-panel-9qekmqk1.js.map +0 -9
  370. package/dist/chunks/settings-modal-gj0hrah4.js +0 -29
  371. package/dist/chunks/settings-modal-gj0hrah4.js.map +0 -9
  372. package/dist/chunks/sp-overlay.dev-earn5as8.js +0 -17
  373. package/dist/chunks/sp-overlay.dev-earn5as8.js.map +0 -9
  374. package/dist/chunks/sp-popover.dev-frxtfejn.js +0 -8
  375. package/dist/chunks/sp-popover.dev-frxtfejn.js.map +0 -9
  376. package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js +0 -14
  377. package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js.map +0 -9
  378. package/dist/chunks/sp-tooltip.dev-a9fywesm.js +0 -21
  379. package/dist/chunks/sp-tooltip.dev-a9fywesm.js.map +0 -10
  380. package/dist/chunks/studio-4k0m4fmp.js.map +0 -13
  381. package/dist/chunks/studio-568nx4t5.js +0 -469
  382. package/dist/chunks/studio-568nx4t5.js.map +0 -14
  383. package/dist/chunks/studio-9smfzqhc.js +0 -19
  384. package/dist/chunks/studio-9smfzqhc.js.map +0 -10
  385. package/dist/chunks/studio-asvf0j9e.js +0 -429
  386. package/dist/chunks/studio-asvf0j9e.js.map +0 -13
  387. package/dist/chunks/studio-d77qm3pk.js +0 -3427
  388. package/dist/chunks/studio-d77qm3pk.js.map +0 -26
  389. package/dist/chunks/studio-j6gqyjf4.js +0 -1854
  390. package/dist/chunks/studio-j6gqyjf4.js.map +0 -18
  391. package/dist/chunks/studio-n9zmpxjg.js.map +0 -115
  392. package/dist/chunks/studio-qkvt37c0.js.map +0 -9
  393. package/dist/chunks/studio-rq0vxb19.js +0 -3744
  394. package/dist/chunks/studio-rq0vxb19.js.map +0 -22
  395. package/dist/chunks/studio-wd6ewdvd.js +0 -853
  396. package/dist/chunks/studio-wd6ewdvd.js.map +0 -12
  397. package/dist/chunks/studio-xq0t7n5t.js +0 -140
  398. package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
  399. package/dist/chunks/studio-xze4zrs1.js.map +0 -10
  400. package/dist/chunks/studio-ygzav7z5.js.map +0 -46
  401. package/dist/chunks/studio-zmy7241e.js +0 -399
  402. package/dist/chunks/studio-zmy7241e.js.map +0 -12
  403. package/dist/chunks/ws-client-q7hkz3j9.js.map +0 -11
  404. package/src/browse/browse-modal.ts +0 -69
  405. package/src/browse/browse.ts +0 -904
  406. package/src/canvas/canvas-diff.ts +0 -216
  407. package/src/canvas/nested-site-style.ts +0 -58
  408. package/src/new-project/design-fields.ts +0 -264
  409. package/src/new-project/templates.ts +0 -61
  410. package/src/panels/tab-bar.ts +0 -548
  411. package/src/settings/settings-modal.ts +0 -203
  412. /package/dist/chunks/{adapter-z8fkmbmv.js.map → adapter-qfnesn5v.js.map} +0 -0
  413. /package/dist/chunks/{cf-settings-mmyv436d.js.map → cf-settings-73sh5z4p.js.map} +0 -0
  414. /package/dist/chunks/{format-host-2c188n41.js.map → format-host-mkwsmt8d.js.map} +0 -0
  415. /package/dist/chunks/{jx-validate-qx8degj7.js.map → jx-validate-v6qgknrs.js.map} +0 -0
  416. /package/dist/chunks/{studio-w7c6kvk7.js.map → studio-a7pzveq1.js.map} +0 -0
  417. /package/dist/chunks/{studio-hhj92gcr.js.map → studio-s519zc99.js.map} +0 -0
  418. /package/dist/chunks/{studio-bghqfmea.js.map → studio-wwseqyw0.js.map} +0 -0
  419. /package/dist/chunks/{studio-nqw433kv.js.map → studio-xmvbhg31.js.map} +0 -0
@@ -0,0 +1,627 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * The one question every write into a Monaco buffer has to ask: **may I write into this buffer?**
4
+ *
5
+ * Studio has two Monaco surfaces — the source view on the canvas stage (`view.monacoEditor`,
6
+ * mounted by `canvas/canvas-render.ts`) and the function body in the Bottom dock's Logic tab
7
+ * (`view.functionEditor`, mounted by `panels/editors.ts`).
8
+ *
9
+ * **The count in this file is of WRITE SITES — places that put text INTO a buffer** — because a
10
+ * write into a buffer is the only thing that has to ask this module's question. (Writes OUT are
11
+ * counted nowhere: there are two, one per surface, they are the debounced commits, and they ask
12
+ * `bufferIsLive` instead. See the note at the end of this header.) There are four, and none of them
13
+ * can see the keyboard:
14
+ *
15
+ * 1. **the serializer resolving with the file's contents**, at mount — `canvas-render.ts`'s
16
+ * `mountSourceEditor`;
17
+ * 2. **a repaint pushing the document's text in** — `canvas-render.ts`'s source fast path;
18
+ * 3. **the dock's re-sync**, showing an edit that arrived from somewhere else — `editors.ts`'s
19
+ * `syncFunctionEditor`;
20
+ * 4. **a code-service round trip landing with a formatted body** — `editors.ts`'s format-on-open.
21
+ *
22
+ * Every one of those writes is computed from a buffer state that may be several keystrokes old by
23
+ * the time it arrives, and `setValue` does not merge — it replaces, and takes the user's unsaved
24
+ * work with it.
25
+ *
26
+ * **And on a co-edited tab there is a FIFTH writer, which is not in that list because it never
27
+ * calls anything here.** y-monaco binds the source model to a shared `Y.Text`, so every peer's
28
+ * keystrokes arrive in the buffer directly and say nothing to anyone. It is also the only writer
29
+ * whose text reaches other people's machines: the binding is two-way, so a local `setValue` over a
30
+ * bound model is a whole-document replace PUBLISHED to every peer. A buffer in that state declares
31
+ * itself with {@link BufferWrites.markShared} and clause 5 refuses every write into it.
32
+ *
33
+ * The rule was already discovered once, in one branch of one fast path:
34
+ *
35
+ * ```js
36
+ * if (!editor.hasTextFocus() && editor.getValue() !== newVal) { … }
37
+ * ```
38
+ *
39
+ * …and it was never given a name, so the other three write sites each invented a different guard.
40
+ * All three asked **identity** — "is this still the same editor object?" — which answers a real
41
+ * question (a retarget must not write A's body into B) and is not the same question. An editor can
42
+ * keep its identity for minutes while its buffer moves on with every keypress. Identity is
43
+ * necessary and never sufficient.
44
+ *
45
+ * {@link bufferMovedOn} is that rule's one home. A write must be dropped when any of these is true
46
+ * — the first four are "the buffer moved on", and the fifth is the one the co-edited surface
47
+ * added:
48
+ *
49
+ * 1. **It is gone.** `dispose()` detaches the model, and `CodeEditorWidget.getValue()` opens with `if
50
+ * (!this._modelData) return "";` — so a dead editor answers the empty string and a write into it
51
+ * lands nowhere.
52
+ * 2. **The user is in it.** `hasTextFocus()` means their next keystroke is newer than your value.
53
+ * 3. **It holds text the document has not been given.** A repaint that resolves a buffer/document
54
+ * difference in the document's favour discards whatever put the difference there.
55
+ *
56
+ * This clause was first written as `pending()` — "a commit is armed" — which is the same thing for
57
+ * a KEYSTROKE and a different thing for every other write. A programmatic write sets
58
+ * `_ignoreNextChange`, so the change handler returns before it can arm anything: the buffer is
59
+ * ahead with nothing pending, and a timer-shaped clause reported "settled". That is exactly the
60
+ * state format-on-open leaves — it pretty-prints into the buffer and deliberately never commits —
61
+ * so the next repaint reverted it, and **"Format on open" visibly un-formatted itself** within
62
+ * seconds of every open, now that the dock repaints on the Problems badge, on Activity and on a
63
+ * 30-second git poll.
64
+ *
65
+ * So it is a FACT the buffer carries ({@link BufferWrites.ahead}), not an inference from a timer.
66
+ * Whoever writes into a buffer says which side the text came from: text that did not come from the
67
+ * document marks it {@link BufferWrites.markAhead}, and the document taking the buffer's text — or
68
+ * the buffer taking the document's — marks it {@link BufferWrites.markSettled}. A keystroke marks
69
+ * ahead and arms its commit; the commit marks settled.
70
+ * 4. **It is not what the write was computed from.** A caller that read the buffer before an `await`
71
+ * passes what it read as `expected`; a mismatch means the round trip lost the race.
72
+ * 5. **It is not this process's buffer to replace.** The one clause that is not "the buffer moved on"
73
+ * — it is "the buffer is not yours". A co-edited source buffer is bound to a shared `Y.Text` and
74
+ * the CRDT is the source of truth for that text while the source lock is held: the structure
75
+ * tree is DERIVED from it by the source reconciler (`collab/collab-session.ts`'s
76
+ * `sourceParseNow` parses the shared text back into the tree). A repaint serializing that tree
77
+ * over the buffer is therefore a rendering of the text asserting itself over the text — and
78
+ * because the model is bound, it is published to every peer, on every repaint, for every round
79
+ * trip that is not byte-stable. There is no direction in which a co-edited buffer wants the
80
+ * document's serialized text, so this fact is set once at bind ({@link BufferWrites.markShared})
81
+ * and never cleared: a future writer's `markSettled` must not be able to unlock it, which is why
82
+ * it is its own fact and not a use of clause 3.
83
+ *
84
+ * **Ahead is not the same as unsaved, and the gates that warn about lost work need the narrower
85
+ * fact.** Format-on-open leaves a buffer ahead deliberately and never commits — and since
86
+ * `closeFunctionEditor` writes a MINIFIED body, the pretty-printed buffer differs from the document
87
+ * for as long as the editor is open. Counting THAT as unsaved work would put "you have unsaved
88
+ * changes" in front of every author who merely opened a handler. So a keystroke says
89
+ * {@link BufferWrites.markTyped} — ahead, and the text is the user's own — and the two gates that
90
+ * destroy a tab ({@link commitTabBuffers}'s callers) read {@link BufferWrites.typed}, which is
91
+ * exactly the text a close would take with it.
92
+ *
93
+ * {@link commitBufferWrites} reads the same fact from the other end. A gate asks it BEFORE the
94
+ * destruction, to decide whether to prompt; a disposer asks it AFTER its own flush, when the answer
95
+ * can only be "I have just deleted this" — and is the last thing in a position to say so, because
96
+ * the next line detaches the model and `buffersForTab` stops finding the buffer at all.
97
+ *
98
+ * Writes OUT of a buffer — a debounce reading `getValue()` into the document — ask `bufferIsLive`
99
+ * instead, which is clause 1 alone. Deliberately, and it is the only kind of call site in the
100
+ * codebase entitled to a subset: clauses 2 and 3 describe a buffer that is AHEAD of the document,
101
+ * which is the precondition for committing rather than a reason to refuse, and clause 4 has no
102
+ * meaning for a writer whose whole input is the buffer.
103
+ *
104
+ * @docs studio/logic/code
105
+ */
106
+
107
+ import { toRaw } from "../reactivity";
108
+ import { notify } from "./notify";
109
+ import type { Tab } from "../tabs/tab";
110
+ import { view } from "../view";
111
+ import { allCanvasSurfaces } from "../canvas/surface-registry";
112
+
113
+ /** The slice of a Monaco editor this module needs. Both of Studio's Monacos satisfy it. */
114
+ export interface MonacoBuffer {
115
+ getValue: () => string;
116
+ getModel: () => unknown;
117
+ hasTextFocus: () => boolean;
118
+ /** The debounced work armed over THIS buffer. Installed by {@link bufferWrites}. */
119
+ _writes?: BufferWrites;
120
+ /**
121
+ * The tab this buffer was mounted for — **whose buffer this is**, and both surfaces answer it the
122
+ * same way.
123
+ *
124
+ * Only the dock's editor used to carry it. The source editor named its tab in a closure inside
125
+ * `mountSourceEditor`, which is unreachable from outside — so a caller holding a `Tab` could ask
126
+ * the dock "is this yours?" and had no way at all to ask the source view. Every question about a
127
+ * tab's buffers ({@link commitTabBuffers}, {@link tabBufferUnsaved}) needs both answers, and a
128
+ * question that can only be asked of one of two surfaces is answered wrong half the time.
129
+ */
130
+ _editingTab?: Tab | null;
131
+ }
132
+
133
+ /**
134
+ * The debounced work armed over one buffer, with that buffer's exact lifetime.
135
+ *
136
+ * It hangs off the editor rather than off `view` or a module variable because those two spellings
137
+ * are what the two surfaces had: the dock's canceller was an editor property, the canvas's timer
138
+ * was a closure variable inside `mountSourceEditor` that nothing outside could reach — so of the
139
+ * two, only one teardown could cancel, and the canvas's three disposal sites cancelled nothing at
140
+ * all. Whoever disposes an editor now has its canceller in their hand by construction.
141
+ */
142
+ export interface BufferWrites {
143
+ /**
144
+ * (Re)arm the timer called `key`, replacing whatever was armed under that key.
145
+ *
146
+ * The work may be async — the source view's commit parses through the format host — and
147
+ * {@link BufferWrites.flush} hands that promise back, because a caller that flushes in order to
148
+ * ASK A QUESTION about the result (has this tab unsaved work now?) has to wait for it.
149
+ */
150
+ arm: (key: string, ms: number, run: () => void | Promise<void>) => void;
151
+ /** Cancel every armed timer. The disposer calls this; see {@link cancelBufferWrites}. */
152
+ cancel: () => void;
153
+ /**
154
+ * Run the work armed under `key` NOW, and drop its timer. A no-op when nothing is armed there.
155
+ *
156
+ * The teardown's half of {@link commitBufferWrites}: the debounce is the only thing that carries
157
+ * the last half-second of typing into the document, so a disposer that merely cancels it deletes
158
+ * that typing — silently, because the document is never marked dirty for work it never received.
159
+ *
160
+ * Returns whatever the work returned, so an async commit can be awaited. The disposers ignore it
161
+ * on purpose: they flush precisely because they are about to detach the model, and every commit
162
+ * reads `getValue()` before its own first `await`.
163
+ *
164
+ * **A commit that already fired counts as armed.** The timer drops its key before running, so a
165
+ * flush arriving mid-run would otherwise find nothing and read that as "there was nothing to
166
+ * carry" — which for the source view, whose commit awaits an IPC round trip to the format host,
167
+ * is a window of tens of milliseconds starting the moment the author stops typing. Exactly when
168
+ * they click another tab. The in-flight promise is returned instead, so the caller waits for the
169
+ * answer rather than inventing one.
170
+ */
171
+ flush: (key: string) => void | Promise<void>;
172
+ /** Clause 3, as a fact: the buffer holds text the document has not been given. */
173
+ ahead: () => boolean;
174
+ /** Declare that the buffer just took text which did NOT come from the document. */
175
+ markAhead: () => void;
176
+ /**
177
+ * Clause 3, narrowed to the text a close would DESTROY: the user's own keystrokes, not yet in the
178
+ * document. See the header — `ahead` is also true of format-on-open, whose text nobody loses.
179
+ */
180
+ typed: () => boolean;
181
+ /** Declare a keystroke: {@link BufferWrites.markAhead}, and the text is the user's own. */
182
+ markTyped: () => void;
183
+ /** Declare that buffer and document now hold the same text, whichever way it travelled. */
184
+ markSettled: () => void;
185
+ /** Clause 5, as a fact: this buffer is bound to a shared text that this process does not own. */
186
+ shared: () => boolean;
187
+ /** Declare that the buffer is now bound to a shared text. Permanent — see clause 5. */
188
+ markShared: () => void;
189
+ }
190
+
191
+ /**
192
+ * The key both surfaces arm their document-commit under.
193
+ *
194
+ * A shared constant rather than each surface's own string, because {@link commitBufferWrites} has to
195
+ * be able to flush THE COMMIT and only the commit. A buffer also arms a lint (`"lint"`), and firing
196
+ * a lint request at a buffer that is being disposed is work for an editor that will not exist when
197
+ * the answer arrives.
198
+ */
199
+ export const BUFFER_COMMIT = "commit";
200
+
201
+ /**
202
+ * Install (and return) the debounce holder for `buffer`.
203
+ *
204
+ * Called once per mount, right after the editor is created, so that the very first keystroke arms
205
+ * its commit through a canceller the teardown can already see.
206
+ *
207
+ * @param {MonacoBuffer} buffer The freshly created editor.
208
+ * @returns {BufferWrites}
209
+ */
210
+ export function bufferWrites(buffer: MonacoBuffer): BufferWrites {
211
+ const timers = new Map<string, ReturnType<typeof setTimeout>>();
212
+ const runs = new Map<string, () => void | Promise<void>>();
213
+ /** Runs that have started and not yet answered. See {@link BufferWrites.flush}. */
214
+ const inFlight = new Map<string, Promise<void>>();
215
+ let isAhead = false;
216
+ let isTyped = false;
217
+ let isShared = false;
218
+ /**
219
+ * Remember a run while it is answering, so a flush arriving mid-run returns its promise rather
220
+ * than the `undefined` that means "nothing was armed". Self-clearing, and it never rejects — the
221
+ * caller inspects the settled state through `typed()`, and a rejection is the commit's own to
222
+ * report.
223
+ */
224
+ function track(key: string, result: void | Promise<void>): void | Promise<void> {
225
+ if (typeof (result as Promise<void> | undefined)?.then !== "function") {
226
+ return result;
227
+ }
228
+ const promise = (result as Promise<void>).finally(() => {
229
+ if (inFlight.get(key) === promise) {
230
+ inFlight.delete(key);
231
+ }
232
+ });
233
+ inFlight.set(key, promise);
234
+ return promise;
235
+ }
236
+ const writes: BufferWrites = {
237
+ arm(key, ms, run) {
238
+ clearTimeout(timers.get(key));
239
+ runs.set(key, run);
240
+ timers.set(
241
+ key,
242
+ setTimeout(() => {
243
+ // Dropped BEFORE the body runs: a callback that re-arms its own key must not have that
244
+ // Arm cleared out from under it, and a flush of a key already executing must find nothing.
245
+ timers.delete(key);
246
+ runs.delete(key);
247
+ /* Discarded deliberately: a timer that fires on its own has no caller to answer to.
248
+ `track` has already put the promise in `inFlight`, which is where a flush arriving
249
+ mid-run collects it — and per the contract above, `run` reports its own failures. */
250
+ void track(key, run());
251
+ }, ms),
252
+ );
253
+ },
254
+ cancel() {
255
+ for (const timer of timers.values()) {
256
+ clearTimeout(timer);
257
+ }
258
+ timers.clear();
259
+ runs.clear();
260
+ },
261
+ flush(key) {
262
+ const run = runs.get(key);
263
+ if (!run) {
264
+ // Nothing armed — but something may be RUNNING. Its answer is the honest one.
265
+ return inFlight.get(key);
266
+ }
267
+ clearTimeout(timers.get(key));
268
+ timers.delete(key);
269
+ runs.delete(key);
270
+ return track(key, run());
271
+ },
272
+ ahead: () => isAhead,
273
+ markAhead() {
274
+ isAhead = true;
275
+ },
276
+ typed: () => isTyped,
277
+ markTyped() {
278
+ isAhead = true;
279
+ isTyped = true;
280
+ },
281
+ markSettled() {
282
+ isAhead = false;
283
+ isTyped = false;
284
+ },
285
+ shared: () => isShared,
286
+ markShared() {
287
+ isShared = true;
288
+ },
289
+ };
290
+ buffer._writes = writes;
291
+ return writes;
292
+ }
293
+
294
+ /**
295
+ * The two Monaco surfaces, named so a teardown that loses text can say what the author was looking
296
+ * at when it went. Both spellings are this module's, not the caller's, because the sentence has to
297
+ * read the same whichever disposer produced it.
298
+ */
299
+ export type BufferSurface = "logic" | "source";
300
+
301
+ const SURFACE: Record<BufferSurface, { holds: string; reached: string; source: string }> = {
302
+ logic: { holds: "The handler you were typing", reached: "written into", source: "Logic" },
303
+ source: { holds: "The source you were typing", reached: "parsed into", source: "Editor" },
304
+ };
305
+
306
+ /**
307
+ * Say that a teardown destroyed the author's typing, and answer whether it did.
308
+ *
309
+ * Called once per {@link commitBufferWrites}, at the moment the answer is known — synchronously for
310
+ * the dock's body write, and from the promise's continuation for the source view's parse, which
311
+ * cannot answer before the disposer has returned.
312
+ *
313
+ * **A toast rather than a Problem, and the difference is whether anything can be done.** A commit
314
+ * that THREW leaves the text in the buffer, so {@link commitTabBuffers} files a Problem — the
315
+ * author can still go and rescue it, minutes later. Here the buffer is being detached in the same
316
+ * breath: there is nothing left to fix, and a record the app promises to keep until somebody fixes
317
+ * it is the wrong host for a loss that is already final.
318
+ *
319
+ * **It does not ask whether a gate already said this.** A close that destroys the TAB runs
320
+ * `commitTabBuffers` and its dialog first, so an author who chose "Close Without Saving" hears it
321
+ * twice; "is that tab still open?" is `workspace`'s to answer, and `workspace` imports this module.
322
+ * One extra keyed toast confirming a choice the author just made is the cheaper of the two costs.
323
+ *
324
+ * @param {MonacoBuffer} buffer
325
+ * @param {BufferSurface} surface
326
+ * @param {{ error: unknown } | null} failure What the commit threw, BOXED — a commit is free to
327
+ * throw `undefined`, and "did it throw" must not become "was what it threw interesting".
328
+ * @returns {boolean} True when there was nothing to lose.
329
+ */
330
+ function reportDiscarded(
331
+ buffer: MonacoBuffer,
332
+ surface: BufferSurface,
333
+ failure: { error: unknown } | null,
334
+ ): boolean {
335
+ const writes = buffer._writes;
336
+ if (writes?.typed() !== true) {
337
+ return true;
338
+ }
339
+ const tab = buffer._editingTab ?? null;
340
+ const where = tab?.documentPath ?? "Untitled";
341
+ const { holds, reached, source } = SURFACE[surface];
342
+ notify.warn(`${holds} was discarded — it was never ${reached} "${where}".`, {
343
+ ...(failure ? { detail: describeCommitFailure(failure.error) } : {}),
344
+ key: `buffer-discarded:${surface}:${tab?.id ?? "none"}`,
345
+ source,
346
+ ...(tab?.documentPath ? { path: tab.documentPath } : {}),
347
+ });
348
+ return false;
349
+ }
350
+
351
+ /** What a failed commit is worth showing: the stack when there is one, the value otherwise. */
352
+ function describeCommitFailure(error: unknown): string {
353
+ return error instanceof Error ? (error.stack ?? error.message) : String(error);
354
+ }
355
+
356
+ /**
357
+ * Carry what the buffer is holding into the document, then drop everything armed over it.
358
+ *
359
+ * **The teardown's obligation, and it is two obligations rather than one.** A disposer that only
360
+ * cancels stops a dead buffer from writing `""` over a handler — the round-3 defect — by throwing
361
+ * away the last half-second of typing instead. That is still lost work, and it is worse in one
362
+ * respect: `doc.dirty` stays `false`, so nothing on screen says anything went missing. Five ways
363
+ * out of the code surface lost an edit that way (switch dock tab, open another target, open a
364
+ * formula through `openLogicTarget`, collapse the dock, switch to a tab whose target string
365
+ * matches), and each measured as "the pre-typing body, quietly".
366
+ *
367
+ * **Two more ways out are not a disposer's to fix, and this function cannot reach them.** ⌘W / the
368
+ * tab × and quitting DESTROY THE TAB: `closeTab` deletes it from `workspace.tabs` before anything
369
+ * disposes an editor, so by the time a dock repaint reaches `disposeFunctionEditor` the commit
370
+ * flushed here finds `tabIsLive(tab) === false` and writes nothing. Seven ways out, and the last
371
+ * two are fixed where the decision is made — {@link commitTabBuffers} runs the commit before the
372
+ * gate reads the tab, and {@link tabBufferUnsaved} is what the gate reads when the commit could not
373
+ * land it.
374
+ *
375
+ * The order is the whole point: flush WHILE THE BUFFER IS STILL ALIVE, then cancel, then dispose.
376
+ * Reversed, or deferred by so much as a microtask, the flush reads the empty string off a detached
377
+ * model and writes that — which is the defect this replaces, not a fix for it. The flushed callback
378
+ * must therefore read `getValue()` before its own first `await`, which both surfaces' commits do.
379
+ *
380
+ * Cancel-without-flush ({@link cancelBufferWrites}) stays correct in exactly one place, and only
381
+ * because the commit has already happened by other means: `closeFunctionEditor` reads the buffer
382
+ * itself and writes a MINIFIED body through the same writer, so the armed commit holds nothing but
383
+ * a raw copy of what was just written, and replaying it afterwards would undo the minify. Every
384
+ * other disposal path — `disposeFunctionEditor`, `disposeSourceEditor` and therefore
385
+ * `resetCanvasView`, the model-URI swap and the mode transition — flushes.
386
+ *
387
+ * **AND THE FLUSH IS ALLOWED TO FAIL, which is what the answer is for.** `bodyWriter` returns
388
+ * whether the body reached the document, and it returns `false` whenever the collab gate refuses
389
+ * the write, the tab is gone, or the element the handler hangs off has been deleted; the source
390
+ * view's parse simply declines to settle when the text does not parse. Throwing that answer away
391
+ * here made five exits — dock tab switch, dock collapse, ⌘, opening another Logic target, switching
392
+ * to another tab, plus the source view's model-URI swap and its mode transition — into silent
393
+ * deletions: the buffer holding the only copy is detached on the next line, and with it goes
394
+ * `buffersForTab`'s knowledge that it ever existed, so {@link tabBufferUnsaved} and every gate
395
+ * built on it go back to answering "nothing to lose" about text that has just been destroyed.
396
+ *
397
+ * `closeFunctionEditor` shows the other shape — a refused write keeps the surface, so the text
398
+ * stays on screen and the author can act. **A disposer has no such option.** Every one of its call
399
+ * sites is a repaint or a mode transition that has already replaced the container, and refusing
400
+ * would leave a live Monaco bound to detached DOM, which is the leak this module's teardown exists
401
+ * to prevent. So the disposers all choose the other answer: they cannot keep the text, and they say
402
+ * that it is gone ({@link reportDiscarded}).
403
+ *
404
+ * **The source view's commit answers late, and the report goes with it.** Its parse is an `await`,
405
+ * so the disposer is long finished by the time "did it land" has a value; the promise's
406
+ * continuation reports, and the synchronous return says only what is known at return time. A
407
+ * `false` therefore always means the loss is certain and has been announced.
408
+ *
409
+ * @param {MonacoBuffer | null | undefined} buffer
410
+ * @param {BufferSurface} surface Which editor is being destroyed — names the toast.
411
+ * @returns {boolean} True when the teardown carried everything the author typed, or there was
412
+ * nothing of theirs to carry. False when it destroyed the author's text, which is reported.
413
+ */
414
+ export function commitBufferWrites(
415
+ buffer: MonacoBuffer | null | undefined,
416
+ surface: BufferSurface,
417
+ ): boolean {
418
+ const writes = buffer?._writes;
419
+ if (!buffer || !writes) {
420
+ return true;
421
+ }
422
+ /* THE FLUSH IS GUARDED AND ITS TWIN'S WAS NOT, which is the whole difference between a failed
423
+ commit and a broken repaint. `bodyWriter`'s event branch resolves `editing.path` in the live
424
+ tree, and a collaborator's delete — or a local undo — makes that path resolve to nothing. The
425
+ throw escaped into `disposeFunctionEditor`, into `syncFunctionEditor`, and out of the dock
426
+ panel's `afterRender`: the repaint aborted mid-way, `cancel()` never ran, `dispose()` never
427
+ ran, and a live 500ms timer was left over an editor whose container lit was about to replace.
428
+ A timer reading `""` off a detached model is the exact defect this module was created to
429
+ prevent, so the teardown below must happen whatever the commit does. */
430
+ let pending: Promise<void> | undefined;
431
+ try {
432
+ // Fire-and-forget on purpose: the disposer is synchronous, and the commit read `getValue()`
433
+ // Before its own first `await`. What is left to await is the write INTO the document, which
434
+ // Does not need this editor to still exist.
435
+ pending = writes.flush(BUFFER_COMMIT) as Promise<void> | undefined;
436
+ } catch (error) {
437
+ writes.cancel();
438
+ return reportDiscarded(buffer, surface, { error });
439
+ }
440
+ writes.cancel();
441
+ if (typeof pending?.then === "function") {
442
+ void pending.then(
443
+ () => reportDiscarded(buffer, surface, null),
444
+ (error: unknown) => reportDiscarded(buffer, surface, { error }),
445
+ );
446
+ return true;
447
+ }
448
+ return reportDiscarded(buffer, surface, null);
449
+ }
450
+
451
+ /**
452
+ * Cancel the debounced work armed over `buffer`. Safe on null, and on a buffer that never armed any
453
+ * — which is why every disposal site can call it unconditionally.
454
+ *
455
+ * **This is not housekeeping, it is the correctness of the teardown.** A surviving timer reads
456
+ * `getValue()` off a disposed editor, gets `""`, and writes that into the document: for the dock it
457
+ * deletes a handler half a second after the surface closed, and for the source view it replaces a
458
+ * whole page body with an empty parse 600ms after the user left Code view.
459
+ *
460
+ * @param {MonacoBuffer | null | undefined} buffer
461
+ */
462
+ export function cancelBufferWrites(buffer: MonacoBuffer | null | undefined): void {
463
+ buffer?._writes?.cancel();
464
+ }
465
+
466
+ /**
467
+ * Whether `buffer` still has a model — i.e. whether there is a buffer at all.
468
+ *
469
+ * The question a write OUT of the buffer asks. `dispose()` runs `_detachModel()`, so this is false
470
+ * for a disposed editor and its `getValue()` is the empty string rather than the text it held.
471
+ *
472
+ * @param {MonacoBuffer | null | undefined} buffer
473
+ * @returns {boolean}
474
+ */
475
+ export function bufferIsLive(buffer: MonacoBuffer | null | undefined): boolean {
476
+ return Boolean(buffer?.getModel());
477
+ }
478
+
479
+ /**
480
+ * Whether this write must be dropped — the buffer moved on, or was never this write's to make. The
481
+ * five clauses are stated in this module's header.
482
+ *
483
+ * @param {MonacoBuffer} buffer The editor about to be written into.
484
+ * @param {string} [expected] What the caller read out of the buffer before its `await`, when it
485
+ * made one. Omitted by callers that computed their value from the DOCUMENT rather than from the
486
+ * buffer, for which clauses 1–3 and 5 are the whole question.
487
+ * @returns {boolean} True when the write must be dropped.
488
+ */
489
+ export function bufferMovedOn(buffer: MonacoBuffer, expected?: string): boolean {
490
+ if (
491
+ !bufferIsLive(buffer) ||
492
+ buffer.hasTextFocus() ||
493
+ buffer._writes?.ahead() ||
494
+ buffer._writes?.shared()
495
+ ) {
496
+ return true;
497
+ }
498
+ return expected !== undefined && buffer.getValue() !== expected;
499
+ }
500
+
501
+ /**
502
+ * The Monaco buffers Studio currently has mounted for `tab`, and nothing else.
503
+ *
504
+ * Both surfaces are asked the same question through `_editingTab`, and both are asked clause 1
505
+ * first: a buffer whose model is detached answers `""` to everything and has no work left to hold.
506
+ *
507
+ * **The order is the SOURCE view first and the dock second, and it is load-bearing** — see
508
+ * {@link commitTabBuffers}, which commits them in this order one at a time.
509
+ *
510
+ * @param {Tab} tab
511
+ * @returns {MonacoBuffer[]}
512
+ */
513
+ function buffersForTab(tab: Tab): MonacoBuffer[] {
514
+ const raw = toRaw(tab as object);
515
+ /* Every stage's source buffer, plus the dock's one function editor.
516
+ The source view is per-PANE now: two Code panes are two Monaco instances over two documents,
517
+ and a close gate that asked only "the" source editor would have missed whichever one it was
518
+ not holding — which is the same half-answer that let ⌘W close a tab over unprompted typing. */
519
+ const mounted: (MonacoBuffer | null)[] = [
520
+ ...allCanvasSurfaces().map((surface) => surface.monacoEditor as MonacoBuffer | null),
521
+ view.functionEditor,
522
+ ];
523
+ return mounted.filter(
524
+ (buffer): buffer is MonacoBuffer =>
525
+ buffer != null &&
526
+ buffer._editingTab != null &&
527
+ toRaw(buffer._editingTab as object) === raw &&
528
+ bufferIsLive(buffer),
529
+ );
530
+ }
531
+
532
+ /**
533
+ * Carry what `tab`'s buffers are holding into the document, leaving the buffers standing.
534
+ *
535
+ * **The close path is a WRITE before it is a question.** `shouldWarnOnClose` and `hasUnsavedTabs`
536
+ * both read `tab.doc.dirty`, and nothing makes a buffer's armed commit dirty a document it has not
537
+ * reached yet — so typing the last character of a handler and pressing ⌘W closed the tab with no
538
+ * prompt at all and took the last 500ms (dock) / 600ms (source) with it. No disposer can cover
539
+ * this: `closeTab` deletes the tab first, and every commit checks `tabIsLive` precisely so that it
540
+ * will not write into a tab nobody can read. The flush therefore has to happen while the tab is
541
+ * still open, which is here, before the gate.
542
+ *
543
+ * **The two commits run ONE AT A TIME, source first, because they are not independent.** Both
544
+ * surfaces can be open on one tab — the dock's Logic editor is not a canvas mode, so a handler body
545
+ * and the page source are editable side by side — and their two commits write at different
546
+ * granularities. The dock's writes ONE body at `editing.path`; the source view's parses the whole
547
+ * buffer and assigns a whole new `tab.doc.document`. Started together, the dock's synchronous write
548
+ * lands first and the source's post-await assign — computed from buffer text that predates it —
549
+ * replaces the entire tree on top of it, so the body the author just typed is gone with no trace.
550
+ *
551
+ * Reversed, both survive: `bodyWriter` re-reads `tab.doc.document` and re-resolves `editing.path`
552
+ * at call time, so the dock's targeted write applies to whatever tree is there when it runs. The
553
+ * whole-document assign is the destructive direction and therefore goes first; the body write goes
554
+ * last and is the only one that can be the last word. (Two independent timers could already race
555
+ * this either way — but a race that used to be a coin toss is not a licence to make the losing side
556
+ * certain.)
557
+ *
558
+ * **It is always a promise, and callers `await` it.** It used to return `Promise<void> | void` so
559
+ * that a tab with nothing armed closed without spending a microtask. Nothing a user can perceive
560
+ * distinguishes zero microtasks from one; the entire cost was five synchronous assertions in
561
+ * `tests/shortcuts.test.ts`, and the entire price was a three-line `const committing = …; if
562
+ * (committing) await committing;` dance in every present and future caller — which is exactly the
563
+ * shape that let {@link commitTabBuffers}'s one existing caller forget to re-check its tab across
564
+ * the await.
565
+ *
566
+ * Deliberately NOT {@link commitBufferWrites}: that one cancels afterwards, which is right for a
567
+ * disposer and wrong here. The tab may well survive this call — the author can press Cancel on the
568
+ * dialog the flush just caused to appear — and its buffers must still have their lint armed.
569
+ *
570
+ * **AND IT NEVER REJECTS, because its callers are gestures.** `requestClose` awaits this before it
571
+ * asks anything, and a rejection there propagated out of an `async` function nobody awaits: ⌘W did
572
+ * nothing — no prompt, no close, no message, an unhandled rejection in the console. The other two
573
+ * callers (`openTab`'s preview replacement, the project switch) do not even await it, so a
574
+ * rejection is invisible by construction. A commit is also not all-or-nothing: the source view's
575
+ * commit and the dock's write to DIFFERENT places, so the first one throwing must not cost the
576
+ * second its turn — which is why the guard is per buffer rather than around the loop.
577
+ *
578
+ * What a failed commit leaves behind is the honest state, not a lost one: `markSettled` never ran,
579
+ * so the buffer is still `typed()`, {@link tabBufferUnsaved} still answers yes, and the gate above
580
+ * the close still prompts. This function's remaining job is to make sure somebody is told, which is
581
+ * a problem rather than a toast: an author who reads it minutes later still needs to know that one
582
+ * document's editor holds text the file does not.
583
+ *
584
+ * @param {Tab} tab
585
+ * @returns {Promise<void>} Resolves when every commit this flush started has finished or failed.
586
+ */
587
+ export async function commitTabBuffers(tab: Tab): Promise<void> {
588
+ for (const buffer of buffersForTab(tab)) {
589
+ /* Clause 1, RE-ASKED, because the list was taken before the previous commit's await. Assigning
590
+ a new document repaints the dock, and a repaint can dispose the function editor — whose
591
+ `getValue()` then answers `""`. Committing that is the deletion this module exists to
592
+ prevent. (The ordinary teardown already flushed and cancelled, so this usually finds nothing
593
+ armed; "usually" is not a guard.) */
594
+ if (!bufferIsLive(buffer)) {
595
+ continue;
596
+ }
597
+ try {
598
+ await buffer._writes?.flush(BUFFER_COMMIT);
599
+ } catch (error) {
600
+ notify.error(
601
+ `Could not save the editor's contents into "${tab.documentPath ?? "Untitled"}".`,
602
+ {
603
+ detail: error instanceof Error ? (error.stack ?? error.message) : String(error),
604
+ key: `buffer-commit:${tab.id}`,
605
+ source: "Editor",
606
+ ...(tab.documentPath ? { path: tab.documentPath } : {}),
607
+ },
608
+ );
609
+ }
610
+ }
611
+ }
612
+
613
+ /**
614
+ * Whether a buffer of `tab`'s holds the user's own typing that the document has not been given.
615
+ *
616
+ * What the unsaved-work gates ask AFTER {@link commitTabBuffers}, because a commit is allowed to
617
+ * fail to land: unparseable source deliberately leaves the buffer ahead rather than resyncing over
618
+ * a half-typed heading, and that text exists nowhere but the buffer. `typed()` rather than
619
+ * `ahead()` — the header says why counting format-on-open as unsaved work would warn every author
620
+ * who merely opened a handler.
621
+ *
622
+ * @param {Tab} tab
623
+ * @returns {boolean}
624
+ */
625
+ export function tabBufferUnsaved(tab: Tab): boolean {
626
+ return buffersForTab(tab).some((buffer) => buffer._writes?.typed() === true);
627
+ }
@@ -109,6 +109,42 @@ export function loadedMonaco(): Monaco | null {
109
109
  return _monaco;
110
110
  }
111
111
 
112
+ /**
113
+ * Whether the mount started before {@link loadMonaco} is still the one the app wants.
114
+ *
115
+ * **The question you owe on the far side of the load, which is why it lives beside the load.** A
116
+ * cold `loadMonaco()` is a 12.6 MB dynamic import; the user has hundreds of milliseconds inside it
117
+ * to close the surface, retarget it, or leave the mode — and a second render has the same window to
118
+ * start a mount of its own. Both surfaces need it and each had its own spelling of it, so the
119
+ * source view simply did without: `renderCanvasImpl` assigns `surface.prevCanvasMode` BEFORE it
120
+ * reaches the mount, so a second synchronous `renderCanvas()` during the load sees `modeChanged ===
121
+ * false` and a null `view.monacoEditor`, skips the fast path and mounts again. The second mount
122
+ * then calls `createModel` with a URI the first already registered, which real Monaco throws on
123
+ * ("ERR: Another model with the same URI"), and `store.ts`'s `render()`/`renderOnly()` coalesce
124
+ * nothing.
125
+ *
126
+ * Returning false means creating NOTHING — no model, no editor, no `automaticLayout` observer. That
127
+ * is strictly better than creating and disposing: there is no window in which a doomed editor is
128
+ * attached to anything, and no URI briefly claimed by a model nobody holds.
129
+ *
130
+ * @param {Element | null | undefined} container Where the editor was going to be created.
131
+ * @param {unknown} existing The surface's editor handle (`view.monacoEditor` /
132
+ * `view.functionEditor`). Non-null means another mount already won this race.
133
+ * @param {() => boolean} stillTargeted Whether the app still wants exactly THIS mount — the same
134
+ * tab, the same mode, the same editing target. Lazy: the two cheap clauses above answer first.
135
+ * @returns {boolean}
136
+ */
137
+ export function mountStillWanted(
138
+ container: Element | null | undefined,
139
+ existing: unknown,
140
+ stillTargeted: () => boolean,
141
+ ): boolean {
142
+ if (!container?.isConnected || existing) {
143
+ return false;
144
+ }
145
+ return stillTargeted();
146
+ }
147
+
112
148
  /** Whether Monaco has been loaded (tests, and the odd diagnostic). */
113
149
  export function isMonacoLoaded(): boolean {
114
150
  return _monaco !== null;