@jxsuite/studio 2.4.3 → 4.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 (208) hide show
  1. package/README.md +5 -5
  2. package/dist/chunks/Overlay.dev-723bv0df.js +23 -0
  3. package/dist/chunks/cf-settings-45fgyx8y.js +21 -0
  4. package/dist/chunks/{components-z6cnkyy1.js → components-zmx10t62.js} +6 -5
  5. package/dist/chunks/content-source-t88vb2dr.js +49 -0
  6. package/dist/chunks/{convert-to-repeater-k10dvcc7.js → convert-to-repeater-dhw1nrz9.js} +27 -18
  7. package/dist/chunks/entry-commands-6z6bhxg0.js +46 -0
  8. package/dist/chunks/{extension-sections-0ada0xcw.js → extension-sections-3ydgjbdy.js} +32 -23
  9. package/dist/chunks/file-ops-rskmhxyq.js +48 -0
  10. package/dist/chunks/files-0cratsr0.js +92 -0
  11. package/dist/chunks/{format-host-wewba4nq.js → format-host-tgcmfs4p.js} +3 -3
  12. package/dist/chunks/fs-events-hrrg328g.js +21 -0
  13. package/dist/chunks/{git-panel-2dh1aw50.js → git-panel-0yywyk76.js} +21 -15
  14. package/dist/chunks/{jx-validate-jj9rnt4n.js → jx-validate-8q2g05x1.js} +6 -4
  15. package/dist/chunks/layers-panel-z3tqe5cd.js +67 -0
  16. package/dist/chunks/layers-q75tj2mf.js +45 -0
  17. package/dist/chunks/lit-html-j055mkh8.js +22 -0
  18. package/dist/chunks/monaco-buffer-dqw0xxpx.js +25 -0
  19. package/dist/chunks/{monaco-setup-a5wkmbgh.js → monaco-setup-rws7k4br.js} +4 -4
  20. package/dist/chunks/notify-yjm79n4s.js +30 -0
  21. package/dist/chunks/parse-24d0gay3.js +19 -0
  22. package/dist/chunks/platform-ba1j1gbh.js +16 -0
  23. package/dist/chunks/publish-panel-54jvhskq.js +28 -0
  24. package/dist/chunks/quick-search-cgfmjwq5.js +65 -0
  25. package/dist/chunks/references-3qx8fkyp.js +29 -0
  26. package/dist/chunks/right-panel-cqp8bc4v.js +46 -0
  27. package/dist/chunks/sp-overlay.dev-yztmtrs1.js +18 -0
  28. package/dist/chunks/sp-popover.dev-n2etmqdp.js +9 -0
  29. package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +15 -0
  30. package/dist/chunks/sp-tooltip.dev-xcvpnqr0.js +15 -0
  31. package/dist/chunks/{sp-tray.dev-64v4pxw8.js → sp-tray.dev-tkbscm06.js} +5 -5
  32. package/dist/chunks/{studio-f5yyvqxv.js → studio-1hbqnt19.js} +37 -31
  33. package/dist/chunks/studio-2zkz8evn.js +3163 -0
  34. package/dist/chunks/{studio-0b6fs05q.js → studio-4pqqxbrt.js} +23 -15
  35. package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
  36. package/dist/chunks/{studio-ef8t53vc.js → studio-5q69gfag.js} +60 -56
  37. package/dist/chunks/{studio-v2c5pmjj.js → studio-5rywrkxz.js} +565 -133
  38. package/dist/chunks/{studio-6kv3apa9.js → studio-68n67094.js} +25 -9
  39. package/dist/chunks/{studio-0rqqhrdy.js → studio-7nte2hj4.js} +2 -2
  40. package/dist/chunks/{studio-hyn3vhwr.js → studio-8swrjbny.js} +31 -18
  41. package/dist/chunks/studio-92ab2jqe.js +282 -0
  42. package/dist/chunks/{studio-sas7mt3k.js → studio-9ab8gqsm.js} +2 -2
  43. package/dist/chunks/{studio-28xc8ws4.js → studio-9c4k4vhv.js} +14 -10
  44. package/dist/chunks/studio-9n4mrmnb.js +184 -0
  45. package/dist/chunks/{studio-tr20m8g8.js → studio-9r6j5ev1.js} +37 -2
  46. package/dist/chunks/{studio-aweyr4v1.js → studio-ahjfdy6e.js} +9 -7
  47. package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
  48. package/dist/chunks/{studio-26sv44br.js → studio-cb12yz9w.js} +8543 -8784
  49. package/dist/chunks/studio-cft9r4r7.js +119 -0
  50. package/dist/chunks/{studio-rqemfhz6.js → studio-eenfkt4e.js} +6 -6
  51. package/dist/chunks/studio-eesa1myv.js +498 -0
  52. package/dist/chunks/{studio-ay8wcr2b.js → studio-eqq1w638.js} +7 -9
  53. package/dist/chunks/{studio-ke244pam.js → studio-ex8zyp98.js} +51 -25
  54. package/dist/chunks/studio-gv8vvshc.js +118 -0
  55. package/dist/chunks/studio-hbtp0eh1.js +1315 -0
  56. package/dist/chunks/{studio-cz2j1c0g.js → studio-htsf0w6m.js} +75 -123
  57. package/dist/chunks/{studio-pyrw8h0w.js → studio-kjjsjak4.js} +528 -212
  58. package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
  59. package/dist/chunks/studio-kyh9avvv.js +28 -0
  60. package/dist/chunks/{studio-jnakhg87.js → studio-kyk2v92a.js} +14 -14
  61. package/dist/chunks/{studio-twsafr1e.js → studio-m3hwn7y1.js} +3 -3
  62. package/dist/chunks/{studio-tzx3xmvx.js → studio-mmv72a27.js} +8 -8
  63. package/dist/chunks/{studio-vhk5fmfv.js → studio-n8b9wqwy.js} +5 -5
  64. package/dist/chunks/{studio-cjm7fdjf.js → studio-p4ngc046.js} +8 -104
  65. package/dist/chunks/{studio-sccge2wf.js → studio-p8j86wag.js} +2 -2
  66. package/dist/chunks/{studio-v1va0vhd.js → studio-pdfjntbk.js} +75 -139
  67. package/dist/chunks/studio-q43a5y9h.js +394 -0
  68. package/dist/chunks/studio-q7v10cg2.js +106 -0
  69. package/dist/chunks/{studio-2w11grav.js → studio-qq6jym04.js} +3 -3
  70. package/dist/chunks/studio-r1r7afxx.js +1066 -0
  71. package/dist/chunks/{studio-3fk1ac9z.js → studio-rdx8jcxy.js} +4 -4
  72. package/dist/chunks/{studio-4mqb9a0j.js → studio-rn9ta2mz.js} +4 -4
  73. package/dist/chunks/studio-sf7zjgv1.js +91 -0
  74. package/dist/chunks/studio-t7qb9db8.js +665 -0
  75. package/dist/chunks/studio-tmc5emrw.js +8 -0
  76. package/dist/chunks/{studio-wdt9y92a.js → studio-txwkm6p9.js} +2 -2
  77. package/dist/chunks/{studio-dgj7gq44.js → studio-v38hmvx1.js} +7 -7
  78. package/dist/chunks/{studio-7ejemhej.js → studio-vadzgkrz.js} +6 -6
  79. package/dist/chunks/studio-wcy8trfx.js +263 -0
  80. package/dist/chunks/{studio-hb7c46jy.js → studio-wx5609xd.js} +21 -15
  81. package/dist/chunks/studio-xrapxwwc.js +1539 -0
  82. package/dist/chunks/{studio-ws6ef6js.js → studio-yxrkyv8a.js} +41 -29
  83. package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
  84. package/dist/chunks/workspace-xpres40v.js +89 -0
  85. package/dist/iframe-entry.js +450 -48
  86. package/dist/studio.js +2455 -1761
  87. package/package.json +16 -15
  88. package/src/browse/library-layouts.ts +3 -3
  89. package/src/browse/library-pane.ts +59 -5
  90. package/src/browse/library-preview.ts +19 -3
  91. package/src/canvas/asset-refs.ts +119 -0
  92. package/src/canvas/asset-resolve.ts +256 -0
  93. package/src/canvas/canvas-live-render.ts +23 -15
  94. package/src/canvas/canvas-render.ts +23 -1
  95. package/src/canvas/iframe-entry.ts +1 -0
  96. package/src/canvas/iframe-host.ts +4 -0
  97. package/src/canvas/iframe-patch.ts +47 -21
  98. package/src/canvas/iframe-protocol.ts +9 -0
  99. package/src/canvas/iframe-render.ts +50 -16
  100. package/src/commands/app-commands.ts +4 -0
  101. package/src/commands/command-args.ts +29 -0
  102. package/src/commands/context.ts +15 -1
  103. package/src/commands/defaults.ts +29 -1
  104. package/src/commands/levels.ts +16 -0
  105. package/src/commands/live-context.ts +3 -0
  106. package/src/content/collection-match.ts +267 -0
  107. package/src/content/entry-commands.ts +38 -29
  108. package/src/content/entry-fields.ts +6 -0
  109. package/src/editor/file-drop-action.ts +2 -1
  110. package/src/editor/shortcuts.ts +5 -0
  111. package/src/files/file-ops.ts +10 -42
  112. package/src/files/files.ts +487 -61
  113. package/src/files/fs-events.ts +9 -0
  114. package/src/files/gitignore.ts +432 -0
  115. package/src/files/media-paths.ts +37 -14
  116. package/src/files/media-upload.ts +47 -6
  117. package/src/files/serialize-document.ts +60 -0
  118. package/src/format/convert-file.ts +511 -0
  119. package/src/format/format-choices.ts +199 -0
  120. package/src/grid/grid-idle.ts +45 -0
  121. package/src/grid/grid-view.ts +35 -0
  122. package/src/grid/schema-columns.ts +2 -1
  123. package/src/grid/sources/content-source.ts +24 -3
  124. package/src/media/media-open.ts +58 -0
  125. package/src/media/media-pane.ts +310 -0
  126. package/src/new-project/import-tab.ts +333 -160
  127. package/src/new-project/new-project-modal.ts +27 -34
  128. package/src/packages/ensure-deps.ts +14 -7
  129. package/src/packages/jxsuite-update.ts +11 -11
  130. package/src/page-params.ts +10 -46
  131. package/src/panels/activity-bar.ts +45 -16
  132. package/src/panels/ai-chat/attached-context.ts +6 -1
  133. package/src/panels/ai-chat/chat-view.ts +255 -13
  134. package/src/panels/ai-chat/composer.ts +21 -80
  135. package/src/panels/ai-panel.ts +116 -2
  136. package/src/panels/frontmatter-fields.ts +6 -2
  137. package/src/panels/navigator-panels.ts +7 -6
  138. package/src/panels/properties-panel.ts +8 -2
  139. package/src/panels/seo-modal.ts +5 -6
  140. package/src/panels/settings-menu.ts +799 -0
  141. package/src/panels/signals-panel.ts +2 -1
  142. package/src/panels/toolbar.ts +135 -15
  143. package/src/platforms/cloud.ts +34 -3
  144. package/src/platforms/devserver.ts +72 -34
  145. package/src/preview/preview-overlay.ts +226 -0
  146. package/src/services/ai-ask.ts +247 -0
  147. package/src/services/ai-import-tools.ts +494 -0
  148. package/src/services/ai-models.ts +101 -11
  149. package/src/services/ai-project-tools.ts +20 -26
  150. package/src/services/ai-settings.ts +69 -71
  151. package/src/services/ai-system-prompt.ts +88 -2
  152. package/src/services/ai-turn-signal.ts +70 -0
  153. package/src/services/cf-settings.ts +19 -37
  154. package/src/services/connection.ts +74 -0
  155. package/src/services/context-manager.ts +89 -0
  156. package/src/services/document-assistant.ts +49 -7
  157. package/src/services/idle.ts +6 -1
  158. package/src/services/import-client.ts +33 -6
  159. package/src/services/import-run.ts +158 -0
  160. package/src/services/import-seed.ts +120 -0
  161. package/src/services/jx-validate.ts +58 -6
  162. package/src/services/project-adoption.ts +71 -0
  163. package/src/services/references.ts +0 -0
  164. package/src/services/settings/definitions.ts +181 -0
  165. package/src/services/settings/kernel.ts +335 -0
  166. package/src/services/settings/write-queue.ts +116 -0
  167. package/src/services/tool-executor.ts +34 -2
  168. package/src/settings/contexts-section.ts +45 -3
  169. package/src/settings/dependencies-editor.ts +43 -23
  170. package/src/settings/general-settings.ts +30 -12
  171. package/src/settings/preferences-accounts.ts +6 -19
  172. package/src/settings/preferences-dialog.ts +18 -41
  173. package/src/settings/preferences-keymap.ts +8 -18
  174. package/src/settings/preferences-sections.ts +57 -0
  175. package/src/settings/schema-field-ui.ts +2 -2
  176. package/src/settings/settings-document.ts +65 -9
  177. package/src/shell.ts +6 -14
  178. package/src/studio.ts +65 -7
  179. package/src/tabs/transact.ts +42 -19
  180. package/src/types.ts +164 -8
  181. package/src/ui/ai-credentials-form.ts +52 -27
  182. package/src/ui/ai-model-picker.ts +162 -0
  183. package/src/ui/choice-field.ts +72 -0
  184. package/src/ui/layers.ts +83 -5
  185. package/src/ui/media-picker.ts +6 -11
  186. package/src/utils/studio-utils.ts +43 -31
  187. package/styles/forced-colors.css +7 -0
  188. package/styles/overlays.css +256 -0
  189. package/styles/panels.css +13 -1
  190. package/styles/shell.css +111 -0
  191. package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
  192. package/dist/chunks/cf-settings-g5fbf2zp.js +0 -17
  193. package/dist/chunks/content-source-k9724vcv.js +0 -39
  194. package/dist/chunks/files-yew27sn9.js +0 -69
  195. package/dist/chunks/layers-panel-qnsghwwd.js +0 -59
  196. package/dist/chunks/publish-panel-a0pdft5w.js +0 -21
  197. package/dist/chunks/quick-search-9avyc4pc.js +0 -54
  198. package/dist/chunks/right-panel-kcnhtc5j.js +0 -38
  199. package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
  200. package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
  201. package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
  202. package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
  203. package/dist/chunks/studio-a7pzveq1.js +0 -110
  204. package/dist/chunks/studio-c31yb4t6.js +0 -55
  205. package/dist/chunks/studio-ra7ar50f.js +0 -6682
  206. package/src/canvas/content-assets.ts +0 -263
  207. package/src/canvas/site-style-css.ts +0 -121
  208. package/src/services/settings-store.ts +0 -99
package/src/studio.ts CHANGED
@@ -94,7 +94,8 @@ import { mountJumpBar } from "./panels/jump-bar";
94
94
  import { cellForPane } from "./panels/pane-grid";
95
95
  import { notify } from "./services/notify";
96
96
  import { beginActivity } from "./panels/activity-panel";
97
- import { exportFile, parseSourceForPath, saveFile, serializeDocument } from "./files/file-ops";
97
+ import { exportFile, parseSourceForPath, saveFile } from "./files/file-ops";
98
+ import { serializeDocument } from "./files/serialize-document";
98
99
  import {
99
100
  formatForPath,
100
101
  loadFormats,
@@ -113,6 +114,7 @@ import {
113
114
  registerFileTreeDnD,
114
115
  reloadCleanTab,
115
116
  } from "./files/files";
117
+ import { resetIgnoreCache } from "./files/gitignore";
116
118
  import { startFsSync } from "./files/fs-events";
117
119
  import { invalidateParamValues } from "./page-params";
118
120
  import {
@@ -128,7 +130,13 @@ import { invalidateLibrary } from "./browse/library-pane";
128
130
  import { invalidateMediaCache } from "./ui/media-picker";
129
131
  import { setMediaChangedHandler } from "./files/media-upload";
130
132
  import { applyFileDrop } from "./editor/file-drop-action";
131
- import { assistantCommands, isAssistantStreaming, seedAssistantMessages } from "./panels/ai-panel";
133
+ import {
134
+ assistantCommands,
135
+ isAssistantStreaming,
136
+ isAssistantWaiting,
137
+ revealImportHandoff,
138
+ seedAssistantMessages,
139
+ } from "./panels/ai-panel";
132
140
  import { seedPublishConnected } from "./publish/publish-panel";
133
141
 
134
142
  import { getPlatform, hasPlatform, registerPlatform } from "./platform";
@@ -180,6 +188,8 @@ import * as frontmatterPanelMod from "./panels/frontmatter-panel";
180
188
  import * as rightPanelMod from "./panels/right-panel";
181
189
  import * as chatPanelMod from "./panels/chat-panel";
182
190
  import { setProjectAdopter } from "./services/project-adoption";
191
+ import { setImportHandoff } from "./services/import-seed";
192
+ import { watchConnection } from "./services/connection";
183
193
  import { tabBufferUnsaved } from "./services/monaco-buffer";
184
194
  import * as leftPanelMod from "./panels/left-panel";
185
195
  import * as tabStrip from "./panels/tab-strip";
@@ -204,7 +214,12 @@ import { initQuickSearch } from "./panels/quick-search";
204
214
  import { hydrateAccountStatus } from "./account-status";
205
215
  import { hydrateProjectList } from "./project-list";
206
216
  import { addRecentProject, hydrateRecentProjects, removeRecentProject } from "./recent-projects";
207
- import { hydrateSettings } from "./services/settings-store";
217
+ import {
218
+ hydrateSettings,
219
+ onSettingsChanged,
220
+ settingsSettled,
221
+ watchRemoteSettings,
222
+ } from "./services/settings/kernel";
208
223
  import { initWelcome } from "./panels/welcome-screen";
209
224
  import {
210
225
  openAddRepoModal,
@@ -223,6 +238,7 @@ import { registerPreferencesCommands } from "./settings/preferences-dialog";
223
238
  import { registerAboutCommands } from "./about/about-modal";
224
239
  import { registerCollabCommands } from "./collab/collab-commands";
225
240
  import { registerLibraryCommands } from "./browse/library-commands";
241
+ import { registerFileFormatCommands } from "./format/convert-file";
226
242
  import { registerPublishCommands } from "./publish/publish-commands";
227
243
  import { registerGridViewCommands } from "./grid/grid-panel";
228
244
  import { registerRedirectsCommands } from "./grid/redirects-grid";
@@ -794,6 +810,11 @@ frontmatterPanelMod.mount();
794
810
  // The assistant's create_project tool adopts freshly scaffolded projects through the same
795
811
  // Flow as the recent-projects list.
796
812
  setProjectAdopter(openRecentProject);
813
+ setImportHandoff(revealImportHandoff);
814
+
815
+ /* A launcher whose backend can go away says so. Installs nothing on a platform that cannot lose
816
+ one — see services/connection.ts for why only the chromium shell can. */
817
+ watchConnection();
797
818
 
798
819
  leftPanelMod.mount({
799
820
  cloneRepository: () => cloneRepository({ openRecentProject }),
@@ -966,6 +987,7 @@ if (_projectParam) {
966
987
  }
967
988
  }
968
989
 
990
+ resetIgnoreCache();
969
991
  setProjectState({
970
992
  dirs: new Map(),
971
993
  expanded: new Set(),
@@ -995,15 +1017,18 @@ if (_projectParam) {
995
1017
  "public",
996
1018
  "styles",
997
1019
  ]);
998
- const dirEntries = await platform.listDirectory(".");
999
- requireProjectState().dirs.set(".", dirEntries);
1020
+ /* Through `loadDirectory`, not `platform.listDirectory`: this is the second copy of the
1021
+ conventional-dir walk (the first is `openProject` in files/files.ts), and the loader is
1022
+ what reads the `.gitignore` chain governing each directory. Listing around it left the
1023
+ `?project=` boot as the one door into a project whose tree knew no ignore rules. */
1024
+ await loadDirectory(".");
1025
+ const dirEntries = requireProjectState().dirs.get(".") ?? [];
1000
1026
  const foundDirs = [];
1001
1027
  for (const e of dirEntries) {
1002
1028
  if (e.type === "directory" && conventionalDirs.has(e.name)) {
1003
1029
  foundDirs.push(e.name);
1004
1030
  requireProjectState().expanded.add(e.path || e.name);
1005
- const sub = await platform.listDirectory(e.path || e.name);
1006
- requireProjectState().dirs.set(e.path || e.name, sub);
1031
+ await loadDirectory(e.path || e.name);
1007
1032
  }
1008
1033
  }
1009
1034
  requireProjectState().projectDirs = foundDirs;
@@ -1136,6 +1161,32 @@ void hydrateSettings().then(() => {
1136
1161
  render();
1137
1162
  });
1138
1163
 
1164
+ /*
1165
+ * A setting changing repaints, wherever the change came from — this window's Preferences, or
1166
+ * hydration landing after boot. Key-gated surfaces (the assistant's setup notice, the New Project
1167
+ * gates, the Accounts list) read the store on the render path rather than caching a copy, so this
1168
+ * one subscription is what keeps every one of them honest.
1169
+ */
1170
+ onSettingsChanged(() => {
1171
+ render();
1172
+ });
1173
+
1174
+ /*
1175
+ * Another window changing a setting is news this one wants. Nothing is lost without it — a patch
1176
+ * cannot clobber a key it does not name — but a second window would otherwise show a provider that
1177
+ * had been reconfigured somewhere else until it was restarted.
1178
+ */
1179
+ watchRemoteSettings();
1180
+
1181
+ /*
1182
+ * Settings are written on a queue that coalesces a burst into one send, so a change made in the
1183
+ * last moments before the window closes may still be in flight. Draining it here is the difference
1184
+ * between "your provider was saved" and a settings file that never heard about it.
1185
+ */
1186
+ globalThis.addEventListener("beforeunload", () => {
1187
+ void settingsSettled();
1188
+ });
1189
+
1139
1190
  // ─── Left panel: delegated to panels/left-panel.js ───────────────────────────
1140
1191
 
1141
1192
  function renderLeftPanel() {
@@ -1266,6 +1317,7 @@ async function openRecentProject(root: string) {
1266
1317
  const config = JSON.parse(content) as ProjectConfig;
1267
1318
 
1268
1319
  closeAllTabs();
1320
+ resetIgnoreCache();
1269
1321
 
1270
1322
  setProjectState({
1271
1323
  ...projectState,
@@ -1371,6 +1423,7 @@ const commandRegistry = createCommandRegistry({
1371
1423
  // The probe `live-context.ts` declared optional and nobody ever passed, so `ctx.ai.streaming`
1372
1424
  // Read false forever. `assistant.stop` is gated on it.
1373
1425
  aiStreaming: isAssistantStreaming,
1426
+ aiWaiting: isAssistantWaiting,
1374
1427
  canvasMode: getCanvasMode,
1375
1428
  isCaretActive,
1376
1429
  isModalOpen,
@@ -1385,6 +1438,10 @@ registerStudioCommands(
1385
1438
  // Hands the URL to the launcher's preview-navigate handler — the OS browser — where a bare
1386
1439
  // `window.open` would open a webview with no address bar. The browser build falls back to a
1387
1440
  // New tab either way.
1441
+ // `View: Open in Browser` renders the working tree; `Build Site` runs the compiler. Two
1442
+ // Verbs, because "what does this page look like?" and "does my site build?" are two questions
1443
+ // And only one of them is worth waiting for a bundler and an image pipeline.
1444
+ buildSite: () => toolbarPanel.runBuildSite(),
1388
1445
  openInBrowser: () => toolbarPanel.runOpenInBrowser(),
1389
1446
  openProject,
1390
1447
  // Wrapped rather than passed by reference: `saveFile` takes an optional tab and reports
@@ -1452,6 +1509,7 @@ registerGridCommands(commandRegistry);
1452
1509
  registerSettingsCommands(commandRegistry);
1453
1510
  registerPreferencesCommands(commandRegistry);
1454
1511
  registerLibraryCommands(commandRegistry);
1512
+ registerFileFormatCommands(commandRegistry);
1455
1513
  registerContentCommands(commandRegistry);
1456
1514
  /* The four translation verbs: open a sibling translation, create the missing one, show the
1457
1515
  Languages panel, declare a language. `i18n.switchLocale` is deliberately not among them — it sets
@@ -1135,6 +1135,46 @@ export function mutateUpdateMediaNestedStyle(
1135
1135
  recordPatch({ op: "set-style", path });
1136
1136
  }
1137
1137
 
1138
+ /**
1139
+ * Drop every level of `stylePath` a deletion has just left empty, DEEPEST FIRST.
1140
+ *
1141
+ * Emptying the leaf usually empties its parent with it — `["table", "th"]` losing its last
1142
+ * declaration leaves `table` holding nothing — and an empty object still serialises, so it reaches
1143
+ * the file, the diff, and every equality check the collab layer makes. Every other style writer in
1144
+ * this module is careful about that; these two were not.
1145
+ *
1146
+ * The walk this replaces went top-down and stopped at the first empty level it found, which is the
1147
+ * one order that cannot work: on the way down the shallow levels are still non-empty _because_ the
1148
+ * leaf it is looking for is inside them, so it deleted the leaf and returned with every ancestor
1149
+ * left behind. Writing then deleting `["table", "th"]` on a node with no style at all produced `{
1150
+ * table: {} }` — a style the author never wrote, on a node that should have had none, because
1151
+ * `ensureNestedStyle` creates the intermediates on the way in.
1152
+ *
1153
+ * @param {JxStyle} root - The object `stylePath[0]` is a key of: a node's `style`, or a media
1154
+ * block.
1155
+ * @param {string[]} stylePath
1156
+ */
1157
+ function pruneEmptyStylePath(root: JxStyle, stylePath: string[]) {
1158
+ // Each entry holds the segment at its own index; an absent (or scalar) one ends the descent.
1159
+ const owners: JxStyle[] = [root];
1160
+ for (const seg of stylePath) {
1161
+ const next = getNestedStyle(owners.at(-1), seg);
1162
+ if (!next) {
1163
+ break;
1164
+ }
1165
+ owners.push(next);
1166
+ }
1167
+ for (let i = owners.length - 2; i >= 0; i--) {
1168
+ const owner = owners[i]!;
1169
+ const seg = stylePath[i]!;
1170
+ const block = getNestedStyle(owner, seg);
1171
+ // Re-read rather than reuse `owners[i + 1]`: the level below may have gone this same loop.
1172
+ if (block && Object.keys(block).length === 0) {
1173
+ delete owner[seg];
1174
+ }
1175
+ }
1176
+ }
1177
+
1138
1178
  /**
1139
1179
  * Update a style property at a nested style path (e.g., ["table", "th"]). Creates intermediate
1140
1180
  * objects as needed.
@@ -1163,16 +1203,7 @@ export function mutateUpdateNestedStylePath(
1163
1203
  }
1164
1204
  if (value === undefined || value === "") {
1165
1205
  delete obj[prop];
1166
- // Clean up empty parent objects
1167
- let cur: JxStyle | undefined = node.style;
1168
- for (let i = 0; i < stylePath.length && cur; i++) {
1169
- const child = getNestedStyle(cur, stylePath[i]!);
1170
- if (child && Object.keys(child).length === 0) {
1171
- delete cur[stylePath[i]!];
1172
- break;
1173
- }
1174
- cur = child;
1175
- }
1206
+ pruneEmptyStylePath(node.style, stylePath);
1176
1207
  } else {
1177
1208
  obj[prop] = value;
1178
1209
  }
@@ -1214,15 +1245,7 @@ export function mutateUpdateMediaNestedStylePath(
1214
1245
  }
1215
1246
  if (value === undefined || value === "") {
1216
1247
  delete obj[prop];
1217
- let cur: JxStyle | undefined = media;
1218
- for (let i = 0; i < stylePath.length && cur; i++) {
1219
- const child = getNestedStyle(cur, stylePath[i]!);
1220
- if (child && Object.keys(child).length === 0) {
1221
- delete cur[stylePath[i]!];
1222
- break;
1223
- }
1224
- cur = child;
1225
- }
1248
+ pruneEmptyStylePath(media, stylePath);
1226
1249
  if (Object.keys(media).length === 0) {
1227
1250
  delete node.style[key];
1228
1251
  }
package/src/types.ts CHANGED
@@ -13,6 +13,7 @@ import type {
13
13
 
14
14
  import type {
15
15
  AppInfo,
16
+ AssetCapabilities,
16
17
  CfConnection,
17
18
  CodeServiceResult,
18
19
  ComponentMeta,
@@ -31,10 +32,12 @@ import type {
31
32
  GitLogEntry,
32
33
  GitStatusResult,
33
34
  ImportProgressEvent,
35
+ ImportReadyEvent,
34
36
  ImportSiteOptions,
35
- OutdatedInfo,
37
+ ImportSiteSummary,
36
38
  PackageInfo,
37
39
  PackageOpResult,
40
+ PackageVersionInfo,
38
41
  ProjectListEntry,
39
42
  ProjectSchemasResponse,
40
43
  RecentProjectEntry,
@@ -42,7 +45,9 @@ import type {
42
45
  RenameResult,
43
46
  SecretsSetRequest,
44
47
  SecretsSetResponse,
48
+ SettingsPatch,
45
49
  StarterInfo,
50
+ UploadResult,
46
51
  } from "@jxsuite/protocol";
47
52
 
48
53
  export type {
@@ -76,12 +81,15 @@ export type {
76
81
  GitFileStatus,
77
82
  GitLogEntry,
78
83
  GitStatusResult,
84
+ ImportBreakpointPolicy,
79
85
  ImportProgressEvent,
86
+ ImportReadyEvent,
80
87
  ImportSiteOptions,
88
+ ImportSiteSummary,
81
89
  JsonValue,
82
- OutdatedInfo,
83
90
  PackageInfo,
84
91
  PackageOpResult,
92
+ PackageVersionInfo,
85
93
  ProjectListEntry,
86
94
  ProjectSchemasResponse,
87
95
  PullRequestInfo,
@@ -93,6 +101,7 @@ export type {
93
101
  SecretsListResponse,
94
102
  SecretsSetRequest,
95
103
  SecretsSetResponse,
104
+ SettingsPatch,
96
105
  StarterInfo,
97
106
  } from "@jxsuite/protocol";
98
107
 
@@ -145,6 +154,20 @@ export interface SiteBuildResult {
145
154
  routes: number;
146
155
  files: number;
147
156
  errors: string[];
157
+ /**
158
+ * How the backend produced what {@link url} serves.
159
+ *
160
+ * `"built"` is compiler output — what the published site will literally be. `"live"` is the
161
+ * working tree rendered as a site: the same pages at the same routes, assembled by the runtime in
162
+ * the reader's browser rather than prerendered, because the backend cannot run a build at all.
163
+ *
164
+ * The distinction is reported rather than hidden because the two differ in ways a reader would
165
+ * otherwise be misled by — a live preview has no prerendered HTML, no optimized images, no
166
+ * islands and no server-timing results — and because a live preview has something a build does
167
+ * not: it shows the tree as it stands, unsaved edits included. Absent means `"built"`, so a
168
+ * backend that predates this keeps its meaning.
169
+ */
170
+ mode?: "built" | "live";
148
171
  /**
149
172
  * Origin the built site is browsable at, e.g. `http://127.0.0.1:41234`.
150
173
  *
@@ -156,6 +179,24 @@ export interface SiteBuildResult {
156
179
  url?: string;
157
180
  }
158
181
 
182
+ /**
183
+ * What a live site preview reports back.
184
+ *
185
+ * Everything {@link SiteBuildResult} carries, plus the one answer only a live preview can give:
186
+ * whether the reader already had a tab on this project.
187
+ */
188
+ export interface SitePreviewResult extends SiteBuildResult {
189
+ /**
190
+ * A tab already holding this project's preview took the route, so open nothing.
191
+ *
192
+ * The caller MUST honour it. One tab per project is the contract, and a caller that opens a
193
+ * second one anyway gives the author two tabs on the same project — which is precisely what
194
+ * retargeting the first one exists to prevent. `false` means the backend has no live client, or
195
+ * had one that did not answer, and the caller should open a tab itself.
196
+ */
197
+ reused?: boolean;
198
+ }
199
+
159
200
  export interface StudioPlatform {
160
201
  id: string;
161
202
  projectRoot: string;
@@ -200,6 +241,33 @@ export interface StudioPlatform {
200
241
  * trailing `/` is added: without it `new URL` drops the last segment.
201
242
  */
202
243
  documentBaseUrl?: string;
244
+ /**
245
+ * What the canvas ORIGIN answers for a SITE URL — and therefore how Studio must address media.
246
+ *
247
+ * `"site"` (the default when absent): the canvas origin already serves the published site URL
248
+ * space, so `/hero.jpg` and `/styles/main.css` resolve on their own and Studio touches neither.
249
+ * The dev server and the desktop loopback both do — `serveProjectFile` in `@jxsuite/server` IS
250
+ * that URL space — so neither declares anything here and both stay byte-identical.
251
+ *
252
+ * `"repo"`: the host serves PROJECT-RELATIVE paths under {@link documentBaseUrl}, and nothing
253
+ * answers the site URL space. Studio resolves each authored reference to the project file it
254
+ * names and rebases that path onto `documentBaseUrl`. Inert without one — a host that says its
255
+ * site URLs are wrong without saying what is right has told Studio nothing it can act on.
256
+ *
257
+ * This is about the ORIGIN, not the backend: a host whose files are perfectly reachable can still
258
+ * be `"repo"` space, because what decides it is what answers `GET /hero.jpg` on the document the
259
+ * canvas is running in.
260
+ */
261
+ assetSpace?: "site" | "repo";
262
+ /**
263
+ * What this backend will accept as an upload, when it says.
264
+ *
265
+ * Absent, or any field absent, means "no declared limit", and Studio must not invent one — a
266
+ * limit it made up is a file the user cannot upload for no reason anyone can name. A DECLARED
267
+ * limit is different: Studio refuses before spending the round trip and names the number in the
268
+ * refusal, and narrows the file picker's `accept` to match.
269
+ */
270
+ assetCapabilities?: AssetCapabilities;
203
271
  activate: (root?: string) => Promise<void>;
204
272
  openProject: () => Promise<{
205
273
  config: ProjectConfig;
@@ -224,8 +292,26 @@ export interface StudioPlatform {
224
292
  listDirectory: (dir: string) => Promise<DirEntry[]>;
225
293
  readFile: (path: string) => Promise<string>;
226
294
  writeFile: (path: string, content: string) => Promise<void>;
227
- uploadFile: (path: string, data: string | File | Blob | ArrayBuffer) => Promise<unknown>;
295
+ /**
296
+ * Store bytes at a project path, and report where they really landed.
297
+ *
298
+ * The result's `path` is the ANSWER, not an echo: a backend may de-duplicate by content hash,
299
+ * append a collision suffix, or normalize a name, and the reference Studio writes into the
300
+ * document has to name what was actually written. This was `Promise<unknown>` and every caller
301
+ * used the REQUESTED path, so any such backend produced documents pointing at files that are not
302
+ * there.
303
+ */
304
+ uploadFile: (path: string, data: string | File | Blob | ArrayBuffer) => Promise<UploadResult>;
228
305
  deleteFile: (path: string) => Promise<void>;
306
+ /**
307
+ * Move a file and rewrite the project's references to it.
308
+ *
309
+ * `from` and `to` are project-relative, and so is every path in the report — including
310
+ * `references.files[].path` and the `errors[].path` naming a document the refactor could not
311
+ * write. An adapter over a backend with a different path space translates the REQUEST or the
312
+ * REPLY, never both: which one needs translating is a property of that backend's route, and doing
313
+ * both silently assumes it echoes its own input space (specs/desktop.md §3.1).
314
+ */
229
315
  renameFile: (from: string, to: string) => Promise<RenameResult>;
230
316
  /**
231
317
  * Where a file or a component tag is used across the project — the read side of the same walker
@@ -239,6 +325,9 @@ export interface StudioPlatform {
239
325
  *
240
326
  * At least one of `path` / `tagName` must be given. A `path` naming a component document
241
327
  * contributes its own root tag, so one call answers "as a file AND as an element" together.
328
+ *
329
+ * `path` is project-relative going in, and every path coming back is too. A backend that cannot
330
+ * place the target inside the active project MUST fail rather than answer zero.
242
331
  */
243
332
  findReferences?: (target: { path?: string; tagName?: string }) => Promise<ReferencesResult>;
244
333
  createDirectory: (path: string) => Promise<void>;
@@ -258,8 +347,12 @@ export interface StudioPlatform {
258
347
  installDependencies?: () => Promise<PackageOpResult>;
259
348
  /** Whether the project has uninstalled dependencies (node_modules missing). */
260
349
  dependenciesNeedInstall?: () => Promise<boolean>;
261
- /** List dependencies that have a newer version available. */
262
- outdatedPackages?: () => Promise<OutdatedInfo[]>;
350
+ /**
351
+ * The newest version published on the registry for each dependency, whether or not it is behind.
352
+ * Optional: a platform that cannot reach a registry omits it, and Studio shows no Latest column
353
+ * values rather than inventing them.
354
+ */
355
+ packageVersions?: () => Promise<PackageVersionInfo[]>;
263
356
  /**
264
357
  * Rewrite the version range of each named package (preserving its dependencies/devDependencies
265
358
  * placement) and run `bun install`. Used by the @jxsuite bump and per-dependency updates.
@@ -349,6 +442,31 @@ export interface StudioPlatform {
349
442
  * build happened to leave — which for most projects is nothing at all.
350
443
  */
351
444
  buildSite?: () => Promise<SiteBuildResult>;
445
+ /**
446
+ * Preview the site LIVE at `route`, with no build on the path.
447
+ *
448
+ * The sibling of {@link buildSite} and what `View: Open in Browser` reaches first. Nothing is
449
+ * compiled: the backend composes the working tree on demand and the runtime assembles each page
450
+ * in the reader's browser, so what opens carries edits nobody has saved. `buildSite` keeps its
451
+ * own meaning and its own command, because "what does this page look like at its real URL?" and
452
+ * "does my site build?" are different questions and only one of them wants to wait.
453
+ *
454
+ * Optional, and the fallback is `buildSite`: a backend that predates this keeps working, and a
455
+ * backend that can do neither says so rather than opening an origin where half the site is the
456
+ * wrong file.
457
+ */
458
+ previewSite?: (opts: { route: string }) => Promise<SitePreviewResult>;
459
+ /**
460
+ * Publish the bytes a save WOULD write for one document, so a live preview shows the canvas
461
+ * rather than the disk.
462
+ *
463
+ * Bytes rather than a document, and the same bytes `writeFile` would receive: that is what makes
464
+ * "what the reader sees" and "what saving would produce" one answer instead of two that drift. A
465
+ * backend holds them in memory and writes them nowhere.
466
+ */
467
+ setPreviewOverlay?: (path: string, contents: string) => Promise<void>;
468
+ /** Drop one document's unsaved bytes, or every one of this project's when no path is given. */
469
+ clearPreviewOverlay?: (path?: string) => Promise<void>;
352
470
  gitAddRemote: (name: string, url: string) => Promise<void>;
353
471
  /**
354
472
  * How the New Project modal collects a destination, and which `CreateProjectDestination` variant
@@ -389,12 +507,19 @@ export interface StudioPlatform {
389
507
  * AI-guided import of an existing site into a new project. Runs in the backend (headless Chrome +
390
508
  * fs), streaming progress until the project is written. Absent on platforms without a backend
391
509
  * import pipeline — the New Project modal hides its Import tab then.
510
+ *
511
+ * `onReady` fires the moment the destination holds an openable `project.json` — seconds in, where
512
+ * the resolved promise is minutes away on a real crawl. It is what lets the caller OPEN the
513
+ * project and let its file tree fill up while the run continues, instead of showing a welcome
514
+ * screen throughout. Optional on both sides: a backend that never sends the line simply never
515
+ * calls it, and the caller opens the project when the run ends, as it always did.
392
516
  */
393
517
  importSite?: (
394
518
  opts: ImportSiteOptions,
395
519
  onProgress: (evt: ImportProgressEvent) => void,
396
520
  signal?: AbortSignal,
397
- ) => Promise<{ root: string; config: ProjectConfig }>;
521
+ onReady?: (evt: ImportReadyEvent) => void,
522
+ ) => Promise<{ root: string; config: ProjectConfig; result?: ImportSiteSummary }>;
398
523
  /**
399
524
  * Open a native directory picker and return the chosen absolute path (null when cancelled). Backs
400
525
  * the New Project modal's **Browse…** button on `createDestination: "path"` platforms. Desktop
@@ -508,8 +633,39 @@ export interface StudioPlatform {
508
633
  * settings live in localStorage only.
509
634
  */
510
635
  getSettings?: () => Promise<Record<string, string>>;
511
- /** Persist the full user-level settings map to the backend store. */
512
- saveSettings?: (settings: Record<string, string>) => Promise<void>;
636
+ /**
637
+ * Apply a set of changes to the backend store, and answer with the store as it then stands.
638
+ *
639
+ * A PATCH rather than the whole map, which is a correctness property and not an optimisation: a
640
+ * key named by neither `set` nor `remove` must be left exactly as it was found. Writing the whole
641
+ * map let any window overwrite the shared file with its own view of it, and on the chromium
642
+ * launcher every window is a separate process with a separate browser profile — so a window that
643
+ * held no settings could, and did, clear the credentials another window had just stored. It also
644
+ * preserves keys this build does not know: one written by a newer version, or by hand.
645
+ */
646
+ patchSettings?: (patch: SettingsPatch) => Promise<Record<string, string>>;
647
+ /**
648
+ * Be told when the backend store changes — by another window, or by hand. Returns the
649
+ * unsubscribe.
650
+ *
651
+ * Optional, and a display concern rather than a correctness one: `patchSettings` already stops a
652
+ * window from clobbering another's keys, so a platform without this leaves a second window
653
+ * showing a stale value until its next boot rather than losing anything.
654
+ */
655
+ subscribeSettings?: (handler: (settings: Record<string, string>) => void) => () => void;
656
+ /**
657
+ * Watch the backend connection, on platforms whose transport can lose one.
658
+ *
659
+ * Only a socket-based launcher implements this: the dev server talks over `fetch` and has nothing
660
+ * to lose, and the electrobun launcher's RPC times a request out rather than dropping it. The
661
+ * chromium launcher holds one long-lived WebSocket, and when it died every call after it hung
662
+ * with no error anywhere — so the transport reports the state, and the studio says it out loud.
663
+ *
664
+ * The handler is called on each transition, never repeatedly for one state.
665
+ */
666
+ subscribeConnection?: (
667
+ handler: (state: { online: boolean; reason?: string }) => void,
668
+ ) => () => void;
513
669
  }
514
670
 
515
671
  // ─── Studio Types ───────────────────────────────────────────────────────────