@jxsuite/studio 3.0.0 → 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 (173) hide show
  1. package/README.md +5 -5
  2. package/dist/chunks/Overlay.dev-723bv0df.js +23 -0
  3. package/dist/chunks/{cf-settings-zj2cj1ar.js → cf-settings-45fgyx8y.js} +6 -5
  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-6vvx96n2.js → convert-to-repeater-dhw1nrz9.js} +27 -19
  7. package/dist/chunks/entry-commands-6z6bhxg0.js +46 -0
  8. package/dist/chunks/{extension-sections-kwzvpemr.js → extension-sections-3ydgjbdy.js} +32 -24
  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-8fr3545e.js → format-host-tgcmfs4p.js} +3 -3
  12. package/dist/chunks/fs-events-hrrg328g.js +21 -0
  13. package/dist/chunks/{git-panel-g01d93vc.js → git-panel-0yywyk76.js} +21 -16
  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-8ezdegcx.js → studio-1hbqnt19.js} +36 -32
  33. package/dist/chunks/{studio-r5xbtctf.js → studio-2zkz8evn.js} +446 -2451
  34. package/dist/chunks/{studio-pkp61mh8.js → studio-4pqqxbrt.js} +23 -15
  35. package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
  36. package/dist/chunks/{studio-3dkcd4c5.js → studio-5q69gfag.js} +60 -56
  37. package/dist/chunks/{studio-rh1cnamv.js → studio-5rywrkxz.js} +563 -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-xntfdy1x.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-ry1vz5sv.js → studio-9c4k4vhv.js} +15 -13
  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-2x9zbxmx.js → studio-ahjfdy6e.js} +9 -7
  47. package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
  48. package/dist/chunks/{studio-dc8s8heb.js → studio-cb12yz9w.js} +8528 -8780
  49. package/dist/chunks/studio-cft9r4r7.js +119 -0
  50. package/dist/chunks/{studio-hcxzyk3x.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-7p35254k.js → studio-ex8zyp98.js} +30 -26
  54. package/dist/chunks/studio-gv8vvshc.js +118 -0
  55. package/dist/chunks/studio-hbtp0eh1.js +1315 -0
  56. package/dist/chunks/{studio-a0yp7syn.js → studio-htsf0w6m.js} +29 -114
  57. package/dist/chunks/{studio-j595e748.js → studio-kjjsjak4.js} +139 -63
  58. package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
  59. package/dist/chunks/{studio-kfpek536.js → studio-kyh9avvv.js} +2 -2
  60. package/dist/chunks/{studio-g5gdqnyd.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-p5dja96s.js → studio-p8j86wag.js} +2 -2
  66. package/dist/chunks/{studio-6z5eyjwn.js → studio-pdfjntbk.js} +66 -129
  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-k4jvjcfx.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-5efrtn1m.js → studio-wx5609xd.js} +21 -17
  81. package/dist/chunks/{studio-qpfq7xhm.js → studio-xrapxwwc.js} +22 -285
  82. package/dist/chunks/{studio-hj2se583.js → studio-yxrkyv8a.js} +41 -31
  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 +108 -24
  86. package/dist/studio.js +2362 -1695
  87. package/package.json +16 -15
  88. package/src/browse/library-pane.ts +57 -3
  89. package/src/canvas/canvas-render.ts +23 -1
  90. package/src/canvas/iframe-patch.ts +47 -21
  91. package/src/canvas/iframe-render.ts +22 -6
  92. package/src/commands/app-commands.ts +4 -0
  93. package/src/commands/command-args.ts +29 -0
  94. package/src/commands/context.ts +15 -1
  95. package/src/commands/defaults.ts +29 -1
  96. package/src/commands/levels.ts +16 -0
  97. package/src/commands/live-context.ts +3 -0
  98. package/src/content/collection-match.ts +267 -0
  99. package/src/content/entry-commands.ts +38 -29
  100. package/src/content/entry-fields.ts +6 -0
  101. package/src/editor/shortcuts.ts +5 -0
  102. package/src/files/file-ops.ts +10 -42
  103. package/src/files/files.ts +485 -59
  104. package/src/files/fs-events.ts +9 -0
  105. package/src/files/gitignore.ts +432 -0
  106. package/src/files/serialize-document.ts +60 -0
  107. package/src/format/convert-file.ts +511 -0
  108. package/src/format/format-choices.ts +199 -0
  109. package/src/grid/grid-view.ts +16 -2
  110. package/src/media/media-open.ts +58 -0
  111. package/src/media/media-pane.ts +310 -0
  112. package/src/new-project/import-tab.ts +332 -160
  113. package/src/new-project/new-project-modal.ts +27 -34
  114. package/src/packages/ensure-deps.ts +14 -7
  115. package/src/packages/jxsuite-update.ts +11 -11
  116. package/src/page-params.ts +10 -46
  117. package/src/panels/activity-bar.ts +45 -16
  118. package/src/panels/ai-chat/attached-context.ts +6 -1
  119. package/src/panels/ai-chat/chat-view.ts +255 -13
  120. package/src/panels/ai-chat/composer.ts +21 -97
  121. package/src/panels/ai-panel.ts +116 -2
  122. package/src/panels/navigator-panels.ts +7 -6
  123. package/src/panels/settings-menu.ts +799 -0
  124. package/src/panels/toolbar.ts +135 -15
  125. package/src/platforms/cloud.ts +20 -0
  126. package/src/platforms/devserver.ts +67 -33
  127. package/src/preview/preview-overlay.ts +226 -0
  128. package/src/services/ai-ask.ts +247 -0
  129. package/src/services/ai-import-tools.ts +494 -0
  130. package/src/services/ai-project-tools.ts +20 -26
  131. package/src/services/ai-system-prompt.ts +88 -2
  132. package/src/services/ai-turn-signal.ts +70 -0
  133. package/src/services/connection.ts +74 -0
  134. package/src/services/context-manager.ts +89 -0
  135. package/src/services/document-assistant.ts +45 -4
  136. package/src/services/import-client.ts +33 -6
  137. package/src/services/import-run.ts +158 -0
  138. package/src/services/import-seed.ts +120 -0
  139. package/src/services/jx-validate.ts +58 -6
  140. package/src/services/project-adoption.ts +71 -0
  141. package/src/services/references.ts +0 -0
  142. package/src/services/settings/definitions.ts +17 -0
  143. package/src/services/tool-executor.ts +34 -2
  144. package/src/settings/contexts-section.ts +45 -3
  145. package/src/settings/dependencies-editor.ts +43 -23
  146. package/src/settings/general-settings.ts +30 -12
  147. package/src/settings/preferences-dialog.ts +18 -41
  148. package/src/settings/preferences-sections.ts +57 -0
  149. package/src/settings/settings-document.ts +65 -9
  150. package/src/studio.ts +33 -6
  151. package/src/tabs/transact.ts +42 -19
  152. package/src/types.ts +103 -5
  153. package/src/ui/ai-model-picker.ts +162 -0
  154. package/src/ui/choice-field.ts +72 -0
  155. package/src/ui/layers.ts +83 -5
  156. package/src/utils/studio-utils.ts +22 -52
  157. package/styles/forced-colors.css +7 -0
  158. package/styles/overlays.css +256 -0
  159. package/styles/panels.css +13 -1
  160. package/styles/shell.css +111 -0
  161. package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
  162. package/dist/chunks/content-source-7fy9qrm0.js +0 -40
  163. package/dist/chunks/files-xcybanxe.js +0 -70
  164. package/dist/chunks/layers-panel-27essgkc.js +0 -60
  165. package/dist/chunks/publish-panel-09sy2bab.js +0 -22
  166. package/dist/chunks/quick-search-sefb61v8.js +0 -55
  167. package/dist/chunks/right-panel-9r32jd5j.js +0 -39
  168. package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
  169. package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
  170. package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
  171. package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
  172. package/dist/chunks/studio-c31yb4t6.js +0 -55
  173. package/src/canvas/site-style-css.ts +0 -117
@@ -1,21 +1,32 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Dependencies settings section — an outdated-aware table of the project's npm dependencies. Shows
3
+ * Dependencies settings section — a registry-aware table of the project's npm dependencies. Shows
4
4
  * current vs latest version, with per-row update/remove, an add-package field, "update all", and
5
- * reinstall. @jxsuite/* rows target the version this Studio build embeds; other packages target the
6
- * newest published version reported by the backend.
5
+ * reinstall.
6
+ *
7
+ * **Every row's Latest is that package's own newest version on npm.** Two assumptions used to sit
8
+ * between this table and the registry, and both are gone:
9
+ *
10
+ * 1. `@jxsuite/*` rows targeted `VERSION`, the version this Studio build embeds — right only while the
11
+ * whole suite released as one number. The packages release on their own cadences now, so that
12
+ * proposed a version that may never have been published, for a package whose real latest the
13
+ * table had not looked at. The same correction `packages/jxsuite-update.ts` already made.
14
+ * 2. The backend was asked for _outdated_ packages, so a package already at its latest arrived as an
15
+ * absence and the column read `—` — the registry's answer was known and thrown away. It now asks
16
+ * `platform.packageVersions()`, which reports every dependency's latest either way, and the
17
+ * comparison that decides whether an update button appears happens here.
18
+ *
19
+ * A row shows `—` only where there is genuinely nothing to show: a `workspace:`/`file:`/git spec, a
20
+ * package the registry does not answer for, or a host with no registry lookup at all.
7
21
  */
8
22
 
9
23
  import { html, render as litRender } from "lit-html";
10
24
  import { getPlatform } from "../platform";
11
- import { VERSION } from "../version";
12
25
  import { notify } from "../services/notify";
13
26
  import { showProgressModal } from "../ui/progress-modal";
14
- import { isComparable, isUpgrade, stripRange } from "../packages/semver";
27
+ import { isUpgrade, stripRange } from "../packages/semver";
15
28
  import type { PackageInfo } from "../types";
16
29
 
17
- const JXSUITE_PREFIX = "@jxsuite/";
18
-
19
30
  interface Update {
20
31
  name: string;
21
32
  version: string;
@@ -28,13 +39,21 @@ let _latest = new Map<string, string>();
28
39
  let _busy = false;
29
40
  let _addName = "";
30
41
 
31
- /** The version a package can be updated to, or null if it is already current. */
42
+ /** This package's newest published version, or null when the registry did not answer for it. */
32
43
  function latestFor(p: PackageInfo): string | null {
33
- if (p.name.startsWith(JXSUITE_PREFIX) && isComparable(VERSION) && isUpgrade(p.version, VERSION)) {
34
- return VERSION;
35
- }
36
- const registry = _latest.get(p.name);
37
- return registry && isUpgrade(p.version, registry) ? registry : null;
44
+ return _latest.get(p.name) ?? null;
45
+ }
46
+
47
+ /**
48
+ * The version a package can be updated TO, or null when it is current or ahead.
49
+ *
50
+ * `isUpgrade`, not "differs from latest": a project deliberately pinned ahead of the registry — a
51
+ * prerelease, or a range bumped before the publish landed — would otherwise be offered a downgrade
52
+ * with an update button.
53
+ */
54
+ function upgradeFor(p: PackageInfo): string | null {
55
+ const latest = latestFor(p);
56
+ return latest && isUpgrade(p.version, latest) ? latest : null;
38
57
  }
39
58
 
40
59
  async function load() {
@@ -45,10 +64,10 @@ async function load() {
45
64
  _packages = [];
46
65
  }
47
66
  _latest = new Map();
48
- if (platform.outdatedPackages) {
67
+ if (platform.packageVersions) {
49
68
  try {
50
- for (const o of await platform.outdatedPackages()) {
51
- _latest.set(o.name, o.latest);
69
+ for (const info of await platform.packageVersions()) {
70
+ _latest.set(info.name, info.latest);
52
71
  }
53
72
  } catch {
54
73
  /* Registry lookups are best-effort */
@@ -118,9 +137,9 @@ async function onUpdateAll() {
118
137
  }
119
138
  const updates = (_packages ?? [])
120
139
  .map((p): Update | null => {
121
- const latest = latestFor(p);
122
- return latest
123
- ? { dev: Boolean(p.dev), name: p.name, version: `^${stripRange(latest)}` }
140
+ const target = upgradeFor(p);
141
+ return target
142
+ ? { dev: Boolean(p.dev), name: p.name, version: `^${stripRange(target)}` }
124
143
  : null;
125
144
  })
126
145
  .filter((u): u is Update => u !== null);
@@ -152,6 +171,7 @@ async function onReinstall() {
152
171
 
153
172
  function row(p: PackageInfo) {
154
173
  const latest = latestFor(p);
174
+ const upgrade = upgradeFor(p);
155
175
  return html`
156
176
  <sp-table-row>
157
177
  <sp-table-cell>
@@ -163,13 +183,13 @@ function row(p: PackageInfo) {
163
183
  <sp-table-cell>${latest ?? "—"}</sp-table-cell>
164
184
  <sp-table-cell>
165
185
  ${
166
- latest
186
+ upgrade
167
187
  ? html`<sp-action-button
168
188
  size="s"
169
189
  quiet
170
190
  ?disabled=${_busy}
171
- title="Update to ${latest}"
172
- @click=${() => onUpdate(p, latest)}
191
+ title="Update to ${upgrade}"
192
+ @click=${() => onUpdate(p, upgrade)}
173
193
  >
174
194
  <sp-icon-refresh slot="icon"></sp-icon-refresh>
175
195
  </sp-action-button>`
@@ -194,7 +214,7 @@ function render() {
194
214
  return;
195
215
  }
196
216
  const pkgs = _packages ?? [];
197
- const hasUpdates = pkgs.some((p) => latestFor(p) !== null);
217
+ const hasUpdates = pkgs.some((p) => upgradeFor(p) !== null);
198
218
 
199
219
  const tpl = html`
200
220
  <div class="settings-section">
@@ -12,9 +12,9 @@
12
12
  import { html, render as litRender, nothing } from "lit-html";
13
13
  import { live } from "lit-html/directives/live.js";
14
14
  import { errorMessage } from "@jxsuite/schema/parse";
15
- import { projectState, updateUi } from "../store";
15
+ import { projectState } from "../store";
16
16
  import { updateSiteConfig } from "../site-context";
17
- import { tabOfContainer } from "../canvas/canvas-surface";
17
+ import { activeRegistry } from "../commands/active-registry";
18
18
  import { getPlatform } from "../platform";
19
19
 
20
20
  import type { JxHeadEntry, ProjectConfig } from "@jxsuite/schema/types";
@@ -165,16 +165,27 @@ export function renderGeneralSettings(container: HTMLElement) {
165
165
  };
166
166
 
167
167
  /*
168
- * Project Styles is the SAME document in a different editor, so this is a mode switch rather
169
- * than a navigation. The predecessor closed the settings modal and opened `project.json` in a
170
- * tab — the configuration IA jumping into the document IA, plan §9.3's own example of the split
171
- * P6 removes. Nothing closes now, because nothing was covering anything.
168
+ * Project Styles is the SAME document in a different editor, and `styles.open` is the one place
169
+ * that says so. This used to write `session.ui.canvasMode` here directly — a second
170
+ * implementation of a capability, §12.5's defect in miniature: the button and the command could
171
+ * disagree about what "open Project Styles" means, and only one of them was reachable by name.
172
+ * The mode is still a fact about the TAB, and there is exactly one `project.json` tab, so
173
+ * `revealTab`'s `focusPane` puts the keyboard on whichever pane holds it.
174
+ *
175
+ * Rendered FROM the record, so the button is disabled with its `requires` sentence rather than
176
+ * absent when it cannot act (§12.3), and it cannot drift from the palette row or the gear menu.
172
177
  */
173
- const onEditGlobalStyles = () => {
174
- // THIS pane's tab. The Project Settings editor is stage content, so a settings document
175
- // Open in the side pane used to send the PRIMARY into the Stylebook.
176
- updateUi(tabOfContainer(container), "canvasMode", "stylebook");
177
- };
178
+ const registry = activeRegistry();
179
+ const stylesCommand = registry?.get("styles.open");
180
+ /* The record's TITLE, and nothing else off the registry.
181
+ `disabledReason` would be the §12.3 shape for a control that can be inapplicable — but this one
182
+ cannot be. `styles.open` is gated on `ctx.project.open`, and Overview is a section of the
183
+ project's own configuration document, so by the time this renders the answer is always yes.
184
+ Asking anyway would also be a FOCUS read inside a function that was handed a container, which
185
+ is what `scripts/check-pane-singletons.ts` rule 4 forbids: a pane-scoped surface must not report
186
+ the focused pane's state. A project-level verb happens to read the same in every pane, but the
187
+ way to be right about that is not to ask. The one thing that can genuinely be missing is the
188
+ registry itself (bootstrap order, a test), and that is an existence check, not a state read. */
178
189
 
179
190
  const currentFavicon = config.favicon;
180
191
 
@@ -266,7 +277,14 @@ export function renderGeneralSettings(container: HTMLElement) {
266
277
  Design tokens and the default element styles that apply across every page. Opens Project
267
278
  Styles over this same document.
268
279
  </p>
269
- <sp-action-button size="s" @click=${onEditGlobalStyles}>
280
+ <sp-action-button
281
+ size="s"
282
+ ?disabled=${!stylesCommand}
283
+ title=${stylesCommand?.title ?? ""}
284
+ @click=${() => {
285
+ void registry?.run("styles.open");
286
+ }}
287
+ >
270
288
  Edit Global Styles
271
289
  </sp-action-button>
272
290
  </div>
@@ -48,47 +48,24 @@ import type { ShortcutRow } from "../commands/reference";
48
48
  import type { RebindResult } from "./preferences-keymap";
49
49
  import type { AnyCommand, CommandRegistry } from "../commands/registry";
50
50
 
51
- /** One section of the sheet. The `id` is what `app.preferences { section }` accepts. */
52
- export interface PreferencesSection {
53
- id: string;
54
- title: string;
55
- /** The one-line answer to "what is in here", shown under the heading. */
56
- blurb: string;
57
- }
58
-
59
- /**
60
- * The sections, in sheet order.
61
- *
62
- * §9.3 lists six for the finished surface (Editor behaviour and Updates/About are the other two).
63
- * Four are built; a section is added here when it has something to configure, never before — an
64
- * empty pane is the "declared but unbuilt" state the rail already refuses to render.
51
+ /*
52
+ * The sections moved to `./preferences-sections`, a leaf with no imports, so the rail's gear menu
53
+ * can enumerate `app.preferences`'s own `section` argument without pulling this module's graph
54
+ * (ai-models, github-auth, cf-settings, the settings kernel) into the rail. Re-exported here so
55
+ * every existing import site is unchanged.
65
56
  */
66
- export const PREFERENCES_SECTIONS: readonly PreferencesSection[] = [
67
- { id: "appearance", title: "Appearance", blurb: "How the Studio chrome looks." },
68
- {
69
- id: "assistant",
70
- title: "Assistant",
71
- blurb: "The AI provider the assistant talks to. Stored on this machine.",
72
- },
73
- {
74
- id: "accounts",
75
- title: "Accounts",
76
- blurb: "Every credential Studio holds, and how to make it forget one.",
77
- },
78
- {
79
- id: "keyboard",
80
- title: "Keyboard",
81
- blurb: "Generated from the command registry — the app's own record of every chord.",
82
- },
83
- ];
84
-
85
- /** The section the sheet opens on when none is named. */
86
- export const DEFAULT_PREFERENCES_SECTION = PREFERENCES_SECTIONS[0]!.id;
87
-
88
- /** Whether a string names a section. */
89
- export function isPreferencesSection(id: unknown): boolean {
90
- return PREFERENCES_SECTIONS.some((section) => section.id === id);
91
- }
57
+ import {
58
+ DEFAULT_PREFERENCES_SECTION,
59
+ isPreferencesSection,
60
+ PREFERENCES_SECTIONS,
61
+ } from "./preferences-sections";
62
+
63
+ export {
64
+ DEFAULT_PREFERENCES_SECTION,
65
+ isPreferencesSection,
66
+ PREFERENCES_SECTIONS,
67
+ } from "./preferences-sections";
68
+ export type { PreferencesSection } from "./preferences-sections";
92
69
 
93
70
  // ─── Open state ───────────────────────────────────────────────────────────────
94
71
 
@@ -588,7 +565,7 @@ export function preferencesCommands(): AnyCommand[] {
588
565
  id: "app.preferences",
589
566
  level: "application",
590
567
  keybinding: "mod+,",
591
- menus: ["commandbar/overflow", "palette"],
568
+ menus: ["commandbar/overflow", "settings/menu", "palette"],
592
569
  group: "7_settings",
593
570
  run: (_ctx, args) => {
594
571
  const section = (args as { section?: unknown } | undefined)?.section;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The Preferences sections — the values `app.preferences { section }` accepts.
3
+ *
4
+ * A LEAF: it imports nothing, and that is the whole reason it is a file. `preferences-dialog.ts`
5
+ * reaches `ui/layers`, `ui/ai-credentials-form`, `ui/ai-managed-connect`, `preferences-accounts` (→
6
+ * github-auth, cf-settings, ai-settings, ai-models) and `preferences-keymap` (→
7
+ * services/settings/kernel) — a graph so wide that `tests/preferences-dialog.test.ts` mocks
8
+ * `ai-models` merely to import the module at all. The rail's gear menu needs four labels from it
9
+ * and nothing else, so it takes the labels rather than the graph.
10
+ *
11
+ * Same split, and the same stated reason, as `settings/section-registry.ts`: what is contributed to
12
+ * is not the surface that renders it, and keeping the two apart is what lets either be replaced.
13
+ *
14
+ * `preferences-dialog.ts` re-exports every symbol here, so no existing import site moved.
15
+ */
16
+
17
+ /** One section of the sheet. The `id` is what `app.preferences { section }` accepts. */
18
+ export interface PreferencesSection {
19
+ id: string;
20
+ title: string;
21
+ /** The one-line answer to "what is in here", shown under the heading. */
22
+ blurb: string;
23
+ }
24
+
25
+ /**
26
+ * The sections, in sheet order.
27
+ *
28
+ * §9.3 lists six for the finished surface (Editor behaviour and Updates/About are the other two).
29
+ * Four are built; a section is added here when it has something to configure, never before — an
30
+ * empty pane is the "declared but unbuilt" state the rail already refuses to render.
31
+ */
32
+ export const PREFERENCES_SECTIONS: readonly PreferencesSection[] = [
33
+ { id: "appearance", title: "Appearance", blurb: "How the Studio chrome looks." },
34
+ {
35
+ id: "assistant",
36
+ title: "Assistant",
37
+ blurb: "The AI provider the assistant talks to. Stored on this machine.",
38
+ },
39
+ {
40
+ id: "accounts",
41
+ title: "Accounts",
42
+ blurb: "Every credential Studio holds, and how to make it forget one.",
43
+ },
44
+ {
45
+ id: "keyboard",
46
+ title: "Keyboard",
47
+ blurb: "Generated from the command registry — the app's own record of every chord.",
48
+ },
49
+ ];
50
+
51
+ /** The section the sheet opens on when none is named. */
52
+ export const DEFAULT_PREFERENCES_SECTION = PREFERENCES_SECTIONS[0]!.id;
53
+
54
+ /** Whether a string names a section. */
55
+ export function isPreferencesSection(id: unknown): boolean {
56
+ return PREFERENCES_SECTIONS.some((section) => section.id === id);
57
+ }
@@ -52,6 +52,8 @@ import { renderGeneralSettings } from "./general-settings";
52
52
  import { renderHeadEditor } from "./head-editor";
53
53
  import { renderLocalesSection } from "./locales-section";
54
54
  import { selectContributedEntry } from "./contributed-section";
55
+ // The NAME and the wire value, kept apart by that module on purpose — see its docstring.
56
+ import { PROJECT_STYLES_TITLE, PROJECT_STYLES_VIEW } from "../style/project-styles";
55
57
  import type { Tab } from "../tabs/tab";
56
58
  import type { AnyCommand, CommandRegistry } from "../commands/registry";
57
59
 
@@ -192,28 +194,37 @@ export function settingsDocumentOpen(): boolean {
192
194
  *
193
195
  * @returns {Tab | null} The tab, or null when no project is open
194
196
  */
195
- export function showSettingsDocument(): Tab | null {
197
+ export function showSettingsDocument(mode: string = SETTINGS_MODE): Tab | null {
196
198
  const state = requireProjectState();
197
199
  if (!state) {
198
200
  return null;
199
201
  }
200
202
  const existing = workspace.tabs.get(PROJECT_CONFIG_PATH);
201
203
  if (existing) {
202
- for (const mode of SETTINGS_TAB_MODES) {
203
- if (!existing.capabilities.modes.includes(mode)) {
204
- existing.capabilities.modes.unshift(mode);
204
+ for (const candidate of SETTINGS_TAB_MODES) {
205
+ if (!existing.capabilities.modes.includes(candidate)) {
206
+ existing.capabilities.modes.unshift(candidate);
205
207
  }
206
208
  }
207
- existing.session.ui.canvasMode = SETTINGS_MODE;
209
+ existing.session.ui.canvasMode = mode;
208
210
  existing.session.ui.preview = false;
209
211
  return revealTab(existing as unknown as Tab);
210
212
  }
211
- return openTab({
213
+ const tab = openTab({
212
214
  capabilities: { modes: [...SETTINGS_TAB_MODES] },
213
215
  document: (state.projectConfig ?? {}) as unknown as Record<string, unknown>,
214
216
  documentPath: PROJECT_CONFIG_PATH,
215
217
  id: PROJECT_CONFIG_PATH,
216
218
  });
219
+ /* The mode is written here rather than left to the capability list, because `createTab` seeds
220
+ `session.ui.canvasMode` from `resolvedModes.find(m => m !== "preview")` (`tabs/tab.ts`) — i.e.
221
+ from `SETTINGS_TAB_MODES[0]`, which is `settings`. A second door over the same document has to
222
+ SAY which editor it is, or `styles.open` from a cold workspace opens the settings form. The
223
+ capability list stays in its declared order for the same reason it always had one: the tab's
224
+ Editor control offers the modes in that order. */
225
+ tab.session.ui.canvasMode = mode;
226
+ tab.session.ui.preview = false;
227
+ return tab;
217
228
  }
218
229
 
219
230
  /**
@@ -328,10 +339,17 @@ export function settingsCommands(): AnyCommand[] {
328
339
  // ⌘⇧, — the other half of §5.3's `⌘, / ⌘⇧,` pair. `app.preferences` shipped with its chord
329
340
  // And this one did not, so the two halves of "settings" were a keystroke and a palette search.
330
341
  keybinding: "mod+shift+,",
331
- menus: ["commandbar/overflow", "palette"],
342
+ menus: ["commandbar/overflow", "settings/menu", "palette"],
332
343
  group: "7_settings",
333
344
  requires: "an open project",
334
- when: (ctx) => ctx.project.open,
345
+ /* `enablement`, not `when`: with no project open this row is DRAWN, greyed, carrying the
346
+ sentence above — §12.3's rule that a control which cannot act explains itself rather than
347
+ vanishing, and the reason the palette greys unavailable commands instead of hiding them.
348
+ It used to be `when`, so the rail's Settings menu held one row on the welcome screen and
349
+ said nothing about the other two; "why can't I" had no answer anywhere. The gate itself is
350
+ unchanged — `enabledWith` still refuses, so `registry.run` and the assistant's tool still
351
+ throw `CommandUnavailableError` exactly as before. */
352
+ enablement: (ctx) => ctx.project.open,
335
353
  aiTool: {
336
354
  description:
337
355
  "Open the project's Settings, optionally on a named section (overview, contexts, head, " +
@@ -370,7 +388,45 @@ export function settingsCommands(): AnyCommand[] {
370
388
  }
371
389
  notifySettingsDocument();
372
390
  },
373
- title: "Open Settings",
391
+ title: "Open Project Settings",
392
+ },
393
+ /*
394
+ * The SECOND door over the same document. `project.json` has three editors — the settings form,
395
+ * Project Styles and the raw source — and until now only one of them had a name: Project Styles
396
+ * was reached by the pane's Editor control (which can only re-mode a tab that is ALREADY open)
397
+ * and by a hand-written `canvasMode` assignment inside Overview. `canvas.setMode` cannot stand
398
+ * in for this: it is level `document` and requires an open one, so from a cold workspace there
399
+ * is nothing to re-mode.
400
+ *
401
+ * It is a peer of `settings.open` and declares the same gate, byte for byte (§12.4: two verbs
402
+ * over one document declare ONE availability rule). `styles.*` is the project-level plural of
403
+ * the selection-level `style.*` verbs in `panels/style-panel.ts`; the ids are one letter apart
404
+ * and the levels are the reason.
405
+ */
406
+ {
407
+ category: "Project",
408
+ id: "styles.open",
409
+ level: "project",
410
+ menus: ["settings/menu", "palette"],
411
+ /* A distinct ordinal inside the settings family, so the gear reads Settings → Styles.
412
+ §12.3's lever is `group`, never a per-menu sort; `styles.open` is not in the ⬢ menu, so
413
+ nothing else sees this string. */
414
+ group: "7_settings_styles",
415
+ requires: "an open project",
416
+ // §12.4: two verbs over ONE document declare ONE availability rule, byte-identical.
417
+ enablement: (ctx) => ctx.project.open,
418
+ aiTool: {
419
+ description:
420
+ "Open the project's configuration document in its Project Styles editor — the design " +
421
+ "tokens and the default element styles that apply across every page.",
422
+ name: "open_project_styles",
423
+ },
424
+ run: () => {
425
+ showSettingsDocument(PROJECT_STYLES_VIEW);
426
+ },
427
+ // Named, never spelled from the wire value: `stylebook` is shared with `dist/iframe-entry.js`
428
+ // And may never be renamed, so nothing a reader sees is derived from it (`style/project-styles`).
429
+ title: `Open ${PROJECT_STYLES_TITLE}`,
374
430
  },
375
431
  ];
376
432
  }
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";
@@ -228,6 +238,7 @@ import { registerPreferencesCommands } from "./settings/preferences-dialog";
228
238
  import { registerAboutCommands } from "./about/about-modal";
229
239
  import { registerCollabCommands } from "./collab/collab-commands";
230
240
  import { registerLibraryCommands } from "./browse/library-commands";
241
+ import { registerFileFormatCommands } from "./format/convert-file";
231
242
  import { registerPublishCommands } from "./publish/publish-commands";
232
243
  import { registerGridViewCommands } from "./grid/grid-panel";
233
244
  import { registerRedirectsCommands } from "./grid/redirects-grid";
@@ -799,6 +810,11 @@ frontmatterPanelMod.mount();
799
810
  // The assistant's create_project tool adopts freshly scaffolded projects through the same
800
811
  // Flow as the recent-projects list.
801
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();
802
818
 
803
819
  leftPanelMod.mount({
804
820
  cloneRepository: () => cloneRepository({ openRecentProject }),
@@ -971,6 +987,7 @@ if (_projectParam) {
971
987
  }
972
988
  }
973
989
 
990
+ resetIgnoreCache();
974
991
  setProjectState({
975
992
  dirs: new Map(),
976
993
  expanded: new Set(),
@@ -1000,15 +1017,18 @@ if (_projectParam) {
1000
1017
  "public",
1001
1018
  "styles",
1002
1019
  ]);
1003
- const dirEntries = await platform.listDirectory(".");
1004
- 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(".") ?? [];
1005
1026
  const foundDirs = [];
1006
1027
  for (const e of dirEntries) {
1007
1028
  if (e.type === "directory" && conventionalDirs.has(e.name)) {
1008
1029
  foundDirs.push(e.name);
1009
1030
  requireProjectState().expanded.add(e.path || e.name);
1010
- const sub = await platform.listDirectory(e.path || e.name);
1011
- requireProjectState().dirs.set(e.path || e.name, sub);
1031
+ await loadDirectory(e.path || e.name);
1012
1032
  }
1013
1033
  }
1014
1034
  requireProjectState().projectDirs = foundDirs;
@@ -1297,6 +1317,7 @@ async function openRecentProject(root: string) {
1297
1317
  const config = JSON.parse(content) as ProjectConfig;
1298
1318
 
1299
1319
  closeAllTabs();
1320
+ resetIgnoreCache();
1300
1321
 
1301
1322
  setProjectState({
1302
1323
  ...projectState,
@@ -1402,6 +1423,7 @@ const commandRegistry = createCommandRegistry({
1402
1423
  // The probe `live-context.ts` declared optional and nobody ever passed, so `ctx.ai.streaming`
1403
1424
  // Read false forever. `assistant.stop` is gated on it.
1404
1425
  aiStreaming: isAssistantStreaming,
1426
+ aiWaiting: isAssistantWaiting,
1405
1427
  canvasMode: getCanvasMode,
1406
1428
  isCaretActive,
1407
1429
  isModalOpen,
@@ -1416,6 +1438,10 @@ registerStudioCommands(
1416
1438
  // Hands the URL to the launcher's preview-navigate handler — the OS browser — where a bare
1417
1439
  // `window.open` would open a webview with no address bar. The browser build falls back to a
1418
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(),
1419
1445
  openInBrowser: () => toolbarPanel.runOpenInBrowser(),
1420
1446
  openProject,
1421
1447
  // Wrapped rather than passed by reference: `saveFile` takes an optional tab and reports
@@ -1483,6 +1509,7 @@ registerGridCommands(commandRegistry);
1483
1509
  registerSettingsCommands(commandRegistry);
1484
1510
  registerPreferencesCommands(commandRegistry);
1485
1511
  registerLibraryCommands(commandRegistry);
1512
+ registerFileFormatCommands(commandRegistry);
1486
1513
  registerContentCommands(commandRegistry);
1487
1514
  /* The four translation verbs: open a sibling translation, create the missing one, show the
1488
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
  }