@jxsuite/studio 5.5.0 → 5.7.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 (244) hide show
  1. package/LICENSE +1 -1
  2. package/dist/chunks/{2020-g8mwpbkq.js → 2020-cs2sb9t1.js} +3 -3
  3. package/dist/chunks/adapter-y5gdq19c.js +4 -0
  4. package/dist/chunks/cf-account-picker-882a7ecm.js +4 -0
  5. package/dist/chunks/cf-settings-20dznc3j.js +4 -0
  6. package/dist/chunks/components-q037dya7.js +4 -0
  7. package/dist/chunks/content-source-37x3vsap.js +4 -0
  8. package/dist/chunks/convert-to-repeater-grfep2w8.js +4 -0
  9. package/dist/chunks/editor-j5sq149q.js +4 -0
  10. package/dist/chunks/entry-commands-c5h3nqck.js +4 -0
  11. package/dist/chunks/extension-sections-dp8endhx.js +4 -0
  12. package/dist/chunks/file-ops-937cvqk6.js +4 -0
  13. package/dist/chunks/files-tznwe1p1.js +4 -0
  14. package/dist/chunks/format-host-e2pjvd25.js +4 -0
  15. package/dist/chunks/fs-events-fv64ak3s.js +4 -0
  16. package/dist/chunks/git-panel-gb713ttc.js +4 -0
  17. package/dist/chunks/grid-view-5amhg604.js +51 -0
  18. package/dist/chunks/grid-view-5mqv1cbd.css +1 -0
  19. package/dist/chunks/iframe-host-t819mcg5.js +4 -0
  20. package/dist/chunks/{index-arg188jq.js → index-9ydfe04y.js} +3 -3
  21. package/dist/chunks/index-m0x5ztax.js +4 -0
  22. package/dist/chunks/{javascript-zcdmye2k.js → javascript-d9pqgc2p.js} +2 -2
  23. package/dist/chunks/{jsonMode-6x5e1nbf.js → jsonMode-800yvbem.js} +5 -5
  24. package/dist/chunks/jx-validate-zc9zp77s.js +4 -0
  25. package/dist/chunks/layers-4bsr82wz.js +4 -0
  26. package/dist/chunks/layers-panel-b6shnaax.js +4 -0
  27. package/dist/chunks/lit-html-cfn61dn7.js +4 -0
  28. package/dist/chunks/media-picker-ej0kdh1j.js +4 -0
  29. package/dist/chunks/menu-rtxf9chb.js +4 -0
  30. package/dist/chunks/{monaco-binding-4az2kh5p.js → monaco-binding-62dtmgbf.js} +3 -3
  31. package/dist/chunks/monaco-buffer-nn4dtx27.js +4 -0
  32. package/dist/chunks/{monaco-setup-d7mwkcx3.js → monaco-setup-tj13ndtm.js} +3 -3
  33. package/dist/chunks/notify-ahs57a2z.js +4 -0
  34. package/dist/chunks/platform-v9xyfkbx.js +4 -0
  35. package/dist/chunks/publish-panel-bq36xcmj.js +4 -0
  36. package/dist/chunks/quick-search-zm00h9dh.js +4 -0
  37. package/dist/chunks/references-99ws0b0m.js +4 -0
  38. package/dist/chunks/right-panel-pq2f71q3.js +4 -0
  39. package/dist/chunks/section-registry-b2yzp9sw.js +4 -0
  40. package/dist/chunks/studio-01kbbqg3.js +928 -0
  41. package/dist/chunks/{studio-hf4qdjc8.js → studio-01qza1n3.js} +4 -4
  42. package/dist/chunks/{studio-pkkw32j0.js → studio-03wwfvyt.js} +4 -4
  43. package/dist/chunks/{studio-7gqyhjns.js → studio-0bbxhhce.js} +4 -4
  44. package/dist/chunks/studio-0k9mvpgj.js +14 -0
  45. package/dist/chunks/studio-0scebvb9.js +6 -0
  46. package/dist/chunks/studio-1a99w39s.js +5 -0
  47. package/dist/chunks/studio-1wvfxkyy.js +5 -0
  48. package/dist/chunks/studio-1x8x26k3.js +5 -0
  49. package/dist/chunks/studio-222rqyyc.js +5 -0
  50. package/dist/chunks/studio-4xppwncr.js +4 -0
  51. package/dist/chunks/{studio-xcng8nx7.js → studio-54g077x3.js} +5 -5
  52. package/dist/chunks/studio-56z7evx4.js +10 -0
  53. package/dist/chunks/studio-5e9zsamh.js +9 -0
  54. package/dist/chunks/studio-65fhspdd.js +5 -0
  55. package/dist/chunks/studio-693nhyky.js +5 -0
  56. package/dist/chunks/{studio-7jazdy6y.js → studio-7pgy3xaf.js} +4 -4
  57. package/dist/chunks/studio-888knnkc.js +5 -0
  58. package/dist/chunks/studio-9mqfamps.js +5 -0
  59. package/dist/chunks/studio-a7xqhc3n.js +5 -0
  60. package/dist/chunks/studio-aje966s9.js +5 -0
  61. package/dist/chunks/studio-aqmph6k4.js +5 -0
  62. package/dist/chunks/studio-c6w3tq7t.js +5 -0
  63. package/dist/chunks/{studio-6j2jn55a.js → studio-c6zdm25z.js} +8 -8
  64. package/dist/chunks/{studio-f8jyp45j.js → studio-c9d3pxf7.js} +4 -4
  65. package/dist/chunks/studio-ceb34dxj.js +5 -0
  66. package/dist/chunks/studio-cvbkkfqb.js +5 -0
  67. package/dist/chunks/studio-cxt150pj.js +7 -0
  68. package/dist/chunks/studio-dkn3x3hz.js +5 -0
  69. package/dist/chunks/studio-e6re3rk1.js +5 -0
  70. package/dist/chunks/{studio-5n1km79v.js → studio-edbtvcd1.js} +4 -4
  71. package/dist/chunks/{studio-xc8ebsts.js → studio-eh0rac07.js} +4 -4
  72. package/dist/chunks/{studio-625ezz59.js → studio-evwgp1ym.js} +4 -4
  73. package/dist/chunks/{studio-he2fhrgp.js → studio-f195h2hq.js} +4 -4
  74. package/dist/chunks/studio-fqq890ap.js +4 -0
  75. package/dist/chunks/studio-frb0qbf7.js +6 -0
  76. package/dist/chunks/studio-gfn1kpxj.js +6 -0
  77. package/dist/chunks/studio-gh97gngt.js +5 -0
  78. package/dist/chunks/{studio-awqghqxv.js → studio-gp02nqnw.js} +4 -4
  79. package/dist/chunks/{studio-x9sagq2d.js → studio-hjbg8d5y.js} +7 -7
  80. package/dist/chunks/studio-j6gnhvaa.js +5 -0
  81. package/dist/chunks/studio-mwxgyn9h.js +7 -0
  82. package/dist/chunks/{studio-x5esgzct.js → studio-n78gjfy8.js} +4 -4
  83. package/dist/chunks/studio-nbxqcgvf.js +5 -0
  84. package/dist/chunks/{studio-p1eggxcf.js → studio-nt06yybj.js} +4 -4
  85. package/dist/chunks/{studio-hc9q4j24.js → studio-p7c1agz0.js} +5 -5
  86. package/dist/chunks/{studio-8r4y2x0z.js → studio-pwqfmt4p.js} +4 -4
  87. package/dist/chunks/{studio-5wfrgee9.js → studio-r3kaka4d.js} +4 -4
  88. package/dist/chunks/studio-semd1s8s.js +5 -0
  89. package/dist/chunks/studio-skzanb0k.js +5 -0
  90. package/dist/chunks/studio-spf2skgc.js +5 -0
  91. package/dist/chunks/{studio-b4epb26c.js → studio-st75kn99.js} +7 -7
  92. package/dist/chunks/{studio-eq27rrr0.js → studio-sxc0zvpt.js} +3 -3
  93. package/dist/chunks/studio-t1eedsyx.js +4 -0
  94. package/dist/chunks/{studio-73czgk6a.js → studio-tp642n5g.js} +4 -4
  95. package/dist/chunks/studio-tw7rre6s.js +5 -0
  96. package/dist/chunks/{studio-7by9a93k.js → studio-tzegmy4w.js} +4 -4
  97. package/dist/chunks/studio-vavmwshr.js +5 -0
  98. package/dist/chunks/studio-wma2c6g8.js +5 -0
  99. package/dist/chunks/studio-x92tgnee.js +64 -0
  100. package/dist/chunks/studio-xk0z6msk.js +5 -0
  101. package/dist/chunks/{studio-c4pnt1c6.js → studio-xry7tk3z.js} +5 -5
  102. package/dist/chunks/studio-xta2vxp6.js +9 -0
  103. package/dist/chunks/{studio-21enxdr8.js → studio-xy3tv3az.js} +4 -4
  104. package/dist/chunks/studio-y7jehjrp.js +5 -0
  105. package/dist/chunks/{studio-k1eaqyzm.js → studio-yr04mv2j.js} +4 -4
  106. package/dist/chunks/{studio-mhhc9btt.js → studio-zd4kayn2.js} +4 -4
  107. package/dist/chunks/studio-zpnk016a.js +16 -0
  108. package/dist/chunks/{tsMode-rtwt7w1a.js → tsMode-k9s9fcag.js} +5 -5
  109. package/dist/chunks/workspace-b69mgm0e.js +4 -0
  110. package/dist/chunks/ws-client-0ja08haa.js +4 -0
  111. package/dist/iframe-entry.js +14 -14
  112. package/dist/manifest.json +62 -59
  113. package/dist/studio.css +1 -1
  114. package/dist/studio.js +121 -123
  115. package/package.json +5 -5
  116. package/src/account/action-flow.ts +294 -0
  117. package/src/account/action-required.ts +68 -0
  118. package/src/account-status.ts +29 -1
  119. package/src/browse/library-model.ts +3 -1
  120. package/src/browse/library-preview.ts +6 -2
  121. package/src/canvas/asset-refs.ts +3 -1
  122. package/src/canvas/asset-resolve.ts +79 -1
  123. package/src/canvas/iframe-entry.ts +38 -1
  124. package/src/canvas/iframe-host.ts +85 -0
  125. package/src/canvas/iframe-protocol.ts +13 -0
  126. package/src/canvas/stage-scrollbar.ts +7 -7
  127. package/src/files/asset-versions.ts +294 -0
  128. package/src/files/file-ops.ts +5 -8
  129. package/src/files/files.ts +61 -9
  130. package/src/files/fs-events.ts +279 -28
  131. package/src/files/media-meta.ts +2 -1
  132. package/src/files/media-paths.ts +5 -2
  133. package/src/files/media-upload.ts +5 -1
  134. package/src/new-project/add-repo-modal.ts +12 -1
  135. package/src/new-project/new-project-modal.ts +12 -1
  136. package/src/panels/git-panel.ts +25 -2
  137. package/src/panels/jump-bar.ts +132 -158
  138. package/src/panels/pane-context.ts +8 -8
  139. package/src/panels/pane-grid.ts +18 -26
  140. package/src/panels/style-panel.ts +2 -2
  141. package/src/platform-errors.ts +61 -1
  142. package/src/platforms/cloud.ts +389 -50
  143. package/src/platforms/devserver.ts +17 -1
  144. package/src/publish/pages-service.ts +17 -7
  145. package/src/services/ai-models.ts +30 -6
  146. package/src/services/ai-system-prompt.ts +81 -51
  147. package/src/services/context-manager.ts +41 -70
  148. package/src/services/document-assistant.ts +12 -3
  149. package/src/services/harness/chat-reducer.ts +115 -0
  150. package/src/services/harness/turn-hooks.ts +50 -0
  151. package/src/services/tool-executor.ts +69 -288
  152. package/src/services/tool-outcomes.ts +12 -10
  153. package/src/settings/preferences-accounts.ts +42 -3
  154. package/src/settings/preferences-dialog.ts +8 -1
  155. package/src/studio.ts +20 -6
  156. package/src/surfaces/block-action-bar.json +6 -1
  157. package/src/surfaces/dialog.ts +5 -0
  158. package/src/surfaces/jump-bar.json +9 -12
  159. package/src/surfaces/jump-bar.ts +15 -16
  160. package/src/surfaces/pane-context.json +2 -2
  161. package/src/surfaces/pane-grid.json +8 -25
  162. package/src/surfaces/pane-grid.ts +7 -8
  163. package/src/surfaces/statusbar.json +71 -3
  164. package/src/surfaces/statusbar.ts +66 -139
  165. package/src/surfaces/style-panel.json +3 -3
  166. package/src/surfaces/style-panel.ts +11 -1
  167. package/src/surfaces/welcome.json +86 -1
  168. package/src/surfaces/welcome.ts +61 -2
  169. package/src/types.ts +84 -2
  170. package/src/ui/ai-managed-connect.ts +41 -4
  171. package/src/ui/media-picker.ts +2 -1
  172. package/dist/chunks/adapter-bsbs78x9.js +0 -4
  173. package/dist/chunks/cf-account-picker-gnm7xeqb.js +0 -4
  174. package/dist/chunks/cf-settings-9y6jyscp.js +0 -4
  175. package/dist/chunks/components-s51h3b7c.js +0 -4
  176. package/dist/chunks/content-source-ty3be0qg.js +0 -4
  177. package/dist/chunks/convert-to-repeater-c3e7q61z.js +0 -4
  178. package/dist/chunks/editor-t002b41m.js +0 -4
  179. package/dist/chunks/entry-commands-sb72e9gy.js +0 -4
  180. package/dist/chunks/extension-sections-ktn2ntnj.js +0 -4
  181. package/dist/chunks/file-ops-78zeytkm.js +0 -4
  182. package/dist/chunks/files-gatzrz8v.js +0 -4
  183. package/dist/chunks/format-host-r59b1651.js +0 -4
  184. package/dist/chunks/fs-events-f8h91ndt.js +0 -4
  185. package/dist/chunks/git-panel-y12zf7nj.js +0 -4
  186. package/dist/chunks/grid-view-rfd8b2pg.js +0 -51
  187. package/dist/chunks/grid-view-tafenfm2.css +0 -1
  188. package/dist/chunks/iframe-host-vsrsj20g.js +0 -4
  189. package/dist/chunks/index-5aghyrfh.js +0 -4
  190. package/dist/chunks/jx-validate-653krkrf.js +0 -4
  191. package/dist/chunks/layers-32p8gtae.js +0 -4
  192. package/dist/chunks/layers-panel-rs58m105.js +0 -4
  193. package/dist/chunks/lit-html-54bmkwz3.js +0 -4
  194. package/dist/chunks/media-picker-5bb04wct.js +0 -4
  195. package/dist/chunks/menu-cbha7w2e.js +0 -4
  196. package/dist/chunks/monaco-buffer-9vbbyfr1.js +0 -4
  197. package/dist/chunks/notify-dc1nz5ar.js +0 -4
  198. package/dist/chunks/platform-2m49s1zf.js +0 -4
  199. package/dist/chunks/publish-panel-nx3ycmma.js +0 -4
  200. package/dist/chunks/quick-search-yzrzh9x3.js +0 -4
  201. package/dist/chunks/references-vx7mbj9f.js +0 -4
  202. package/dist/chunks/right-panel-7jzpbxja.js +0 -4
  203. package/dist/chunks/section-registry-gjryse41.js +0 -4
  204. package/dist/chunks/studio-3znfhcxg.js +0 -5
  205. package/dist/chunks/studio-58qam27v.js +0 -4
  206. package/dist/chunks/studio-6j59fwaw.js +0 -5
  207. package/dist/chunks/studio-7z69g1gx.js +0 -5
  208. package/dist/chunks/studio-93j3v6zx.js +0 -5
  209. package/dist/chunks/studio-9gf6v8zy.js +0 -5
  210. package/dist/chunks/studio-9rhmettf.js +0 -4
  211. package/dist/chunks/studio-ac5aq677.js +0 -10
  212. package/dist/chunks/studio-as65vbns.js +0 -5
  213. package/dist/chunks/studio-bkv0sga4.js +0 -6
  214. package/dist/chunks/studio-bttmqef4.js +0 -5
  215. package/dist/chunks/studio-c3y9d41k.js +0 -5
  216. package/dist/chunks/studio-ccegp7gz.js +0 -9
  217. package/dist/chunks/studio-chxabn98.js +0 -6
  218. package/dist/chunks/studio-cktcdsxz.js +0 -16
  219. package/dist/chunks/studio-cnspfke5.js +0 -5
  220. package/dist/chunks/studio-de3p1ktm.js +0 -5
  221. package/dist/chunks/studio-fsbcn530.js +0 -928
  222. package/dist/chunks/studio-gftga7a3.js +0 -64
  223. package/dist/chunks/studio-hf1rpcq0.js +0 -4
  224. package/dist/chunks/studio-hz02hq70.js +0 -5
  225. package/dist/chunks/studio-jnb285we.js +0 -5
  226. package/dist/chunks/studio-jsv3s3s1.js +0 -5
  227. package/dist/chunks/studio-ks7cksh0.js +0 -5
  228. package/dist/chunks/studio-nptx0dbs.js +0 -5
  229. package/dist/chunks/studio-pg0z7k98.js +0 -5
  230. package/dist/chunks/studio-pve0b2f2.js +0 -5
  231. package/dist/chunks/studio-q7zmjv3b.js +0 -6
  232. package/dist/chunks/studio-rpmvw7kj.js +0 -5
  233. package/dist/chunks/studio-rwhss52s.js +0 -5
  234. package/dist/chunks/studio-sbg3g3h0.js +0 -5
  235. package/dist/chunks/studio-sq1bn60g.js +0 -14
  236. package/dist/chunks/studio-vwz7rma2.js +0 -5
  237. package/dist/chunks/studio-wa55e8gf.js +0 -5
  238. package/dist/chunks/studio-x0mzdha2.js +0 -5
  239. package/dist/chunks/studio-xrgpakvc.js +0 -9
  240. package/dist/chunks/studio-xt3djk1w.js +0 -7
  241. package/dist/chunks/studio-yh3nggx1.js +0 -7
  242. package/dist/chunks/studio-zmecp6hq.js +0 -5
  243. package/dist/chunks/workspace-g6dvaete.js +0 -4
  244. package/dist/chunks/ws-client-shf41j5p.js +0 -4
@@ -1,68 +1,60 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * The status bar — ambient state, in scope order, and nothing else.
3
+ * The status bar — ambient state, in scope order, and the breadcrumb trail between it.
4
4
  *
5
5
  * What this replaces: a bar built by `innerHTML` string concatenation with a three-character
6
6
  * escaper, carrying whatever the last of 78 `statusMessage()` calls had said, for three seconds.
7
7
  * Transient messages have left entirely for the toast host (`ui/layers.ts`) and the Problems list
8
8
  * (`services/notify.ts`); what remains here is state that is TRUE for as long as it is shown.
9
9
  *
10
- * **Three fixed fields, in the shell's own left-to-right level order** (§16.2), so the bar restates
11
- * the containment model on every glance:
10
+ * **Three fields, in the shell's own left-to-right level order** (§16.2), so the bar restates the
11
+ * containment model on every glance:
12
12
  *
13
13
  * ```text
14
- * PROJECT ‖ DOCUMENT ‖ SELECTION
15
- * name · branch ↑n↓n · problems ‖ path · view · save state ‖ count · style rule
14
+ * PROJECT ‖ DOCUMENT ‖ SELECTION
15
+ * name · branch ↑n↓n · problems ‖ file › ancestor › … › element ⌃ ‖ count · style rule
16
16
  * ```
17
17
  *
18
- * The SELECTION field held an ancestor breadcrumb until the jump bar landed. The trail is an
19
- * ADDRESS, not ambient state, and it now lives once — in the jump bar (`panels/jump-bar.ts`), which
20
- * merged it with the pane context bar's document-stack chain. What is left here is the count and
21
- * the Stylebook's rule, neither of which is a path.
18
+ * **The DOCUMENT field is the jump bar** (`panels/jump-bar.ts`): the focused pane's address, every
19
+ * step a button that selects it. This module renders the fields either side of it and leaves the
20
+ * middle as an empty `trail` slot, which the jump bar is handed once this document has mounted. The
21
+ * field used to hold the file path, the view name and a save word; the trail's first step is the
22
+ * same file path with the same `palette.openFiles` command, the view is the pane context bar's (a
23
+ * second copy at 11px is the chrome duplication studio-ui-guidelines.md §12.2's budget exists to
24
+ * prevent), and unsaved changes are stated by the tab strip's dirty marker and the Command Bar's
25
+ * Save. A trail of ancestors is worth more on this row than any of the three.
22
26
  *
23
27
  * `statusbar/project`, `statusbar/document` and `statusbar/selection` are three placements the
24
28
  * level × placement matrix already declares, each admitting exactly one level — so the bar's
25
29
  * mixedness is structural rather than an exemption.
26
30
  *
27
- * **The effective view, not a mode string.** The bar used to print "Content Mode" from
28
- * `tab.doc.mode` while the Command Bar printed "Design" from `canvasMode` — two surfaces reading
29
- * two fields to answer one question. Both now read `registry.context()`, which is the same record
30
- * every `when` predicate reads, so they cannot disagree again.
31
- *
32
31
  * **Every interactive item is a command.** There are no click handlers in this file: an item names
33
32
  * a command id, and renders as a button only when the registry has that command and its `when`
34
33
  * holds. That is what let an item with no command yet — the peers count, before the `Collaborate:`
35
34
  * family was registered — sit in the template and start rendering the day its command arrived, with
36
- * no edit here. The three readouts that are NOT buttons are marked: the view name (its control is
37
- * the pane context bar; a second mode picker in 24px would be the chrome duplication
38
- * studio-ui-guidelines.md §12.2's budget exists to prevent), the save wording once a document IS
39
- * saved, and the stylebook selector.
35
+ * no edit here. The one readout that is NOT a button is the stylebook selector.
40
36
  *
41
37
  * The bar is the `statusbar` surface (`surfaces/statusbar.json`): this module is the projection —
42
- * three fields of items, each item a button with a command or a readout without — and the surface
43
- * draws it. The scope is reactive, so one effect recomputes the projection from the state the bar
38
+ * fields of items, each item a button with a command or a readout without — and the surface draws
39
+ * it. The scope is reactive, so one effect recomputes the projection from the state the bar
44
40
  * renders; nothing repaints.
45
41
  *
46
42
  * @docs studio/interface
47
43
  */
48
44
 
49
45
  import { projectState, statusbarEl } from "../store";
50
- import { documentLabel } from "../panels/jump-bar";
46
+ import { attachJumpBarHost, mountJumpBar, unmountJumpBar } from "../panels/jump-bar";
51
47
  import { shell } from "../shell";
52
48
  import { effect, effectScope, reactive } from "../reactivity";
53
49
  import { activeTab } from "../workspace/workspace";
54
- import { EDITOR_KIND_LABELS } from "../commands/context";
55
50
  import { activeRegistry } from "../commands/active-registry";
56
51
  import { runReported } from "../commands/run-reported";
57
52
  import { deployStatusItem } from "../publish/deploy-checklist";
58
53
  import { collabState } from "../collab/collab-state";
59
54
  import { problemCount, problems } from "../services/notify";
60
- import { now } from "../services/clock";
61
- import { relativeTime } from "../panels/ai-chat/sessions-view";
62
55
  import { mountSurface, registerSurface } from "../ui/surface";
63
56
  import statusbarDoc from "./statusbar.json";
64
57
  import type { CommandRegistry } from "../commands/registry";
65
- import type { EditorKind } from "../commands/context";
66
58
  import type { EffectScope } from "@vue/reactivity";
67
59
  import type { JxDocument } from "@jxsuite/schema/types";
68
60
  import type { SurfaceHandle } from "../ui/surface";
@@ -71,28 +63,6 @@ registerSurface("statusbar", statusbarDoc as unknown as JxDocument);
71
63
 
72
64
  let _scope: EffectScope | null = null;
73
65
 
74
- /**
75
- * Document path → when it was last written, from the {@link now} seam.
76
- *
77
- * Kept here rather than on the `Tab` record for one reason: a save time is something the STATUS BAR
78
- * observes, not something a document is. `files/file-ops.ts` reports it after a successful write —
79
- * the same place the old `statusMessage("Saved …")` was raised from, so the fact has not moved, it
80
- * has only stopped being a message that erases itself.
81
- */
82
- const _savedAt = new Map<string, number>();
83
-
84
- /** Record a successful write. Called by the save path; keyed by document path. */
85
- export function noteDocumentSaved(path: string | null | undefined): void {
86
- if (path) {
87
- _savedAt.set(path, now());
88
- }
89
- }
90
-
91
- /** Forget every recorded save. Project close and the tests both want a clean slate. */
92
- export function forgetSavedTimes(): void {
93
- _savedAt.clear();
94
- }
95
-
96
66
  // ─── Items ───────────────────────────────────────────────────────────────────
97
67
 
98
68
  /** One thing the bar can show. A `command` makes it a button; without one it is a readout. */
@@ -248,81 +218,18 @@ function projectField_(registry: CommandRegistry | null): ProjectedField | null
248
218
  ]);
249
219
  }
250
220
 
251
- // ─── DOCUMENT ────────────────────────────────────────────────────────────────
252
-
253
- /**
254
- * What the pane is showing, in the words the pane context bar uses.
255
- *
256
- * Derived from the command context — `editor.kind` and `canvas.view` — so it is the SAME two facts
257
- * the Command Bar, the keyboard scope stack and every `when` predicate read.
258
- */
259
- export function viewLabel(kind: EditorKind, view: string): string {
260
- if (kind === "none") {
261
- return "";
262
- }
263
- if (kind !== "canvas") {
264
- return EDITOR_KIND_LABELS[kind];
265
- }
266
- return view.charAt(0).toUpperCase() + view.slice(1);
267
- }
268
-
269
- /**
270
- * The save state, in words.
271
- *
272
- * A dot glyph said one bit and required a legend nobody has. These four sentences say which of the
273
- * four states the document is in, and the one that needs an action IS the action: "Unsaved changes"
274
- * renders as the `file.save` button.
275
- */
276
- function saveItem(dirty: boolean, readOnly: boolean, savedAt: number | undefined): StatusItem {
277
- if (readOnly) {
278
- return { command: null, label: "Read-only", title: "A collaborator owns this session's file" };
279
- }
280
- if (dirty) {
281
- return { command: "file.save", label: "Unsaved changes" };
282
- }
283
- return savedAt === undefined
284
- ? { command: null, label: "Saved" }
285
- : { command: null, label: `Saved ${relativeTime(savedAt)}` };
286
- }
287
-
288
- function documentField(registry: CommandRegistry | null): ProjectedField | null {
289
- const tab = activeTab.value;
290
- if (!tab) {
291
- return null;
292
- }
293
- const ctx = registry?.context() ?? null;
294
- const view = ctx ? viewLabel(ctx.editor.kind, ctx.canvas.view) : "";
295
- const collab = collabState(tab);
296
- const savedAt = tab.documentPath === null ? undefined : _savedAt.get(tab.documentPath);
297
- return projectField(registry, "document", [
298
- {
299
- command: "palette.openFiles",
300
- label: documentLabel(tab.documentPath),
301
- title: tab.documentPath ?? "Not saved to disk yet",
302
- },
303
- view
304
- ? { command: null, label: view, title: "The pane's view — change it on the pane context bar" }
305
- : null,
306
- saveItem(tab.doc.dirty, collab.readOnly, savedAt),
307
- ]);
308
- }
309
-
310
221
  // ─── SELECTION ───────────────────────────────────────────────────────────────
311
222
 
312
223
  /**
313
224
  * What is selected — the COUNT, and nothing that is an address.
314
225
  *
315
- * This field used to carry a clickable ancestor trail, one `selection.set` per crumb. That trail
316
- * has moved whole to the jump bar (`panels/jump-bar.ts`), which is the jump bar's entire reason to
317
- * exist: Studio had two half-breadcrumbs — that one, and the pane context bar's document-stack
318
- * chain — and neither ever rendered the whole address. Leaving a copy here would have made three.
319
- *
320
- * What is left is what the jump bar CANNOT say, and both are ambient state in the §16.2 sense: how
321
- * many things are selected (a count is not a path, and the jump bar names only the primary), and
322
- * which style rule the Stylebook is editing (a CSS selector is not a node in this document).
226
+ * The ancestor trail is the DOCUMENT field to its left (`panels/jump-bar.ts`). What is left here is
227
+ * what a trail CANNOT say, and both are ambient state in the §16.2 sense: how many things are
228
+ * selected (a count is not a path, and the jump bar names only the primary), and which style rule
229
+ * the Stylebook is editing (a CSS selector is not a node in this document).
323
230
  *
324
- * A single selection therefore leaves this field empty. That is deliberate — the jump bar's leaf
325
- * segment states it permanently, with its ancestors, and a second copy at 11px says nothing new.
231
+ * A single selection therefore leaves this field empty. That is deliberate — the trail's leaf
232
+ * states it permanently, with its ancestors, one field to the left.
326
233
  */
327
234
  function selectionField(registry: CommandRegistry | null): ProjectedField | null {
328
235
  const paths = activeTab.value?.session.selection ?? [];
@@ -334,7 +241,7 @@ function selectionField(registry: CommandRegistry | null): ProjectedField | null
334
241
  {
335
242
  command: null,
336
243
  label: `${paths.length} selected`,
337
- title: `${paths.length} elements are selected; the jump bar names the primary`,
244
+ title: `${paths.length} elements are selected; the trail names the primary`,
338
245
  },
339
246
  ])
340
247
  : null;
@@ -354,16 +261,18 @@ function selectionField(registry: CommandRegistry | null): ProjectedField | null
354
261
 
355
262
  // ─── The bar ─────────────────────────────────────────────────────────────────
356
263
 
357
- /** The whole bar, as data. There is no second variant for the empty states. */
358
- function projectBar(): ProjectedField[] {
359
- const registry = activeRegistry();
360
- return [projectField_(registry), documentField(registry), selectionField(registry)].filter(
361
- (field): field is ProjectedField => field !== null,
362
- );
264
+ /** Drop the absent fields. */
265
+ function present(fields: readonly (ProjectedField | null)[]): ProjectedField[] {
266
+ return fields.filter((field): field is ProjectedField => field !== null);
363
267
  }
364
268
 
365
269
  interface StatusbarScope extends Record<string, unknown> {
366
- fields: ProjectedField[];
270
+ /** The fields before the DOCUMENT slot: PROJECT. */
271
+ leading: ProjectedField[];
272
+ /** Whether the DOCUMENT slot has a trail to show. */
273
+ hasDocument: boolean;
274
+ /** The fields after it: SELECTION. */
275
+ trailing: ProjectedField[];
367
276
  run: (key: string) => void;
368
277
  }
369
278
 
@@ -374,26 +283,40 @@ let _handle: SurfaceHandle | null = null;
374
283
  /** The reactive scope the surface reads, made once. */
375
284
  function state(): StatusbarScope {
376
285
  _state ??= reactive({
377
- fields: [],
286
+ hasDocument: false,
287
+ leading: [],
378
288
  run: (key: string) => {
379
- const item = _state?.fields.flatMap((field) => field.items).find((i) => i.key === key);
289
+ const item = [...(_state?.leading ?? []), ...(_state?.trailing ?? [])]
290
+ .flatMap((field) => field.items)
291
+ .find((i) => i.key === key);
380
292
  const registry = activeRegistry();
381
293
  if (item?.command && registry) {
382
294
  void runReported(registry, item.command, item.args as never, "Status Bar");
383
295
  }
384
296
  },
297
+ trailing: [],
385
298
  }) as StatusbarScope;
386
299
  return _state;
387
300
  }
388
301
 
389
- /** Mount the surface into the bar's host, once. */
302
+ /**
303
+ * Mount the surface into the bar's host, once, and hand its DOCUMENT slot to the jump bar.
304
+ *
305
+ * The slot is a box this document draws empty; the jump bar mounts its own document into it, which
306
+ * is the same hand-over the pane grid gives the tab strip and the context bar.
307
+ */
390
308
  function ensureMounted(): void {
391
309
  if (_mount || !statusbarEl) {
392
310
  return;
393
311
  }
394
- _mount = mountSurface("statusbar", state(), statusbarEl);
395
- void _mount.then((handle) => {
312
+ const mount = mountSurface("statusbar", state(), statusbarEl);
313
+ _mount = mount;
314
+ void mount.then((handle) => {
315
+ if (_mount !== mount) {
316
+ return;
317
+ }
396
318
  _handle = handle;
319
+ attachJumpBarHost(statusbarEl.querySelector<HTMLElement>('[part="trail"]'));
397
320
  });
398
321
  }
399
322
 
@@ -402,13 +325,23 @@ function ensureMounted(): void {
402
325
  * mounting the effect.
403
326
  */
404
327
  export function renderStatusbar(): void {
405
- state().fields = projectBar();
328
+ const registry = activeRegistry();
329
+ const scope = state();
330
+ scope.leading = present([projectField_(registry)]);
331
+ scope.hasDocument = activeTab.value !== null;
332
+ scope.trailing = present([selectionField(registry)]);
406
333
  ensureMounted();
407
334
  }
408
335
 
409
- /** Subscribe the bar to the state it renders. Idempotent. */
336
+ /**
337
+ * Subscribe the bar to the state it renders, and its DOCUMENT field's trail with it. Idempotent.
338
+ *
339
+ * The trail is this bar's field, so this bar owns its lifecycle: the jump bar's effect starts here
340
+ * and stops in {@link unmountStatusbar}, and its host arrives from {@link ensureMounted}.
341
+ */
410
342
  export function mountStatusbar(): void {
411
343
  unmountStatusbar();
344
+ mountJumpBar();
412
345
  _scope = effectScope();
413
346
  _scope.run(() => {
414
347
  effect(() => {
@@ -420,15 +353,8 @@ export function mountStatusbar(): void {
420
353
  void problems.length;
421
354
  const tab = activeTab.value;
422
355
  if (tab) {
423
- void tab.doc.document;
424
- void tab.doc.dirty;
425
- void tab.doc.mode;
426
- void tab.documentPath;
427
- void tab.session.selection.map((path) => path.join("/")).join("|");
428
- void tab.session.ui.canvasMode;
429
- void tab.session.ui.preview;
356
+ void tab.session.selection.length;
430
357
  void collabState(tab).peers.length;
431
- void collabState(tab).readOnly;
432
358
  }
433
359
  renderStatusbar();
434
360
  });
@@ -440,6 +366,7 @@ export function unmountStatusbar(): void {
440
366
  _scope = null;
441
367
  const pending = _mount;
442
368
  _mount = null;
369
+ unmountJumpBar();
443
370
  if (_handle) {
444
371
  _handle.dispose();
445
372
  _handle = null;
@@ -1013,6 +1013,7 @@
1013
1013
  {
1014
1014
  "tagName": "jx-action-group",
1015
1015
  "$props": {
1016
+ "selects": "single",
1016
1017
  "compact": true,
1017
1018
  "label": {
1018
1019
  "$ref": "$map/item/label"
@@ -1044,9 +1045,8 @@
1044
1045
  "hint": {
1045
1046
  "$ref": "$map/item/title"
1046
1047
  },
1047
- "toggles": true,
1048
- "selected": {
1049
- "$ref": "$map/item/selected"
1048
+ "checked": {
1049
+ "$ref": "$map/item/checked"
1050
1050
  }
1051
1051
  },
1052
1052
  "onclick": {
@@ -108,7 +108,17 @@ export interface StyleButtonView extends Record<string, unknown> {
108
108
  icon: string;
109
109
  /** The abbreviation drawn when there is no glyph. Empty when there is one, and its span hidden. */
110
110
  text: string;
111
- selected: boolean;
111
+ /**
112
+ * `"true"` / `"false"` — a row's buttons are RADIOS, one choice of several, and never toggles.
113
+ *
114
+ * The value is a fact about the document, so the button may draw it and must not decide it. With
115
+ * the kit's `toggles` on, the button flipped its own `selected` in a click listener that ran
116
+ * AFTER the row's: a real click flushes microtasks between listeners, so the projection had
117
+ * already drawn the committed value when the flip inverted it, and Display read unset after
118
+ * pressing Flex and set after pressing it again. `checked` draws the state without the button
119
+ * ever writing it.
120
+ */
121
+ checked: "true" | "false";
112
122
  }
113
123
 
114
124
  /**
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://jxsuite.com/schema/v1",
3
3
  "$id": "StudioWelcome",
4
- "$description": "The Start pane, shown when no project is loaded and no document is open. The adapter hands in the start actions, whether the GitHub App still needs installing, the recent projects with their distinguishing folders and last-opened labels, and the platform's catalogue; the document draws them in the order a first run needs them.",
4
+ "$description": "The Start pane, shown when no project is loaded and no document is open. The adapter hands in the start actions, whether the GitHub App still needs installing, the platform's own notices with the actions each offers, the recent projects with their distinguishing folders and last-opened labels, and the platform's catalogue; the document draws them in the order a first run needs them.",
5
5
  "tagName": "div",
6
6
  "attributes": {
7
7
  "part": "screen"
@@ -311,6 +311,91 @@
311
311
  }
312
312
  }
313
313
  },
314
+ {
315
+ "$prototype": "Array",
316
+ "items": {
317
+ "$ref": "#/state/notices"
318
+ },
319
+ "key": {
320
+ "$ref": "$map/item/id"
321
+ },
322
+ "map": {
323
+ "tagName": "section",
324
+ "attributes": {
325
+ "part": "section",
326
+ "data-section": "notice",
327
+ "data-notice": "${$map.item.id}",
328
+ "data-level": "${$map.item.level}"
329
+ },
330
+ "children": [
331
+ {
332
+ "tagName": "h2",
333
+ "attributes": {
334
+ "part": "section-title"
335
+ },
336
+ "textContent": "${$map.item.title}"
337
+ },
338
+ {
339
+ "tagName": "p",
340
+ "attributes": {
341
+ "part": "note"
342
+ },
343
+ "textContent": "${$map.item.message}"
344
+ },
345
+ {
346
+ "tagName": "div",
347
+ "attributes": {
348
+ "part": "actions"
349
+ },
350
+ "children": [
351
+ {
352
+ "$prototype": "Array",
353
+ "items": {
354
+ "$ref": "$map/item/actions"
355
+ },
356
+ "key": {
357
+ "$ref": "$map/item/key"
358
+ },
359
+ "map": {
360
+ "tagName": "jx-button",
361
+ "$props": {
362
+ "variant": {
363
+ "$ref": "$map/item/variant"
364
+ }
365
+ },
366
+ "attributes": {
367
+ "part": "notice-action",
368
+ "data-action": "${$map.item.key}"
369
+ },
370
+ "onclick": {
371
+ "$prototype": "Function",
372
+ "body": [
373
+ {
374
+ "operator": "call",
375
+ "target": {
376
+ "$ref": "#/state/noticeRun"
377
+ },
378
+ "value": [
379
+ {
380
+ "$ref": "$map/item/key"
381
+ }
382
+ ]
383
+ }
384
+ ]
385
+ },
386
+ "children": [
387
+ {
388
+ "tagName": "span",
389
+ "textContent": "${$map.item.label}"
390
+ }
391
+ ]
392
+ }
393
+ }
394
+ ]
395
+ }
396
+ ]
397
+ }
398
+ },
314
399
  {
315
400
  "tagName": "div",
316
401
  "attributes": {
@@ -19,7 +19,8 @@
19
19
  * @docs studio/interface/welcome-screen
20
20
  */
21
21
 
22
- import { getAccountStatus, needsAppInstall } from "../account-status";
22
+ import { getAccountStatus, getBannerNotices, needsAppInstall } from "../account-status";
23
+ import { runOfferedAction } from "../account/action-flow";
23
24
  import { platformSupportsAddRepo } from "../new-project/add-repo-modal";
24
25
  import { getProjectList } from "../project-list";
25
26
  import { reactive } from "../reactivity";
@@ -30,6 +31,7 @@ import { mountSurface, registerSurface } from "../ui/surface";
30
31
  import welcomeDoc from "./welcome.json";
31
32
  import type { JxDocument } from "@jxsuite/schema/types";
32
33
  import type { SurfaceHandle } from "../ui/surface";
34
+ import type { AccountNotice, OfferedAction } from "../types";
33
35
 
34
36
  registerSurface("welcome", welcomeDoc as unknown as JxDocument);
35
37
 
@@ -172,6 +174,21 @@ interface RecentRow {
172
174
  when: string;
173
175
  }
174
176
 
177
+ /** One notice's button: `key` is `<notice id>/<action id>`, which `noticeRun` resolves. */
178
+ interface NoticeActionRow {
179
+ key: string;
180
+ label: string;
181
+ variant: string;
182
+ }
183
+
184
+ interface NoticeRow {
185
+ id: string;
186
+ title: string;
187
+ message: string;
188
+ level: string;
189
+ actions: NoticeActionRow[];
190
+ }
191
+
175
192
  interface CatalogueRow {
176
193
  root: string;
177
194
  name: string;
@@ -182,6 +199,8 @@ interface WelcomeScope extends Record<string, unknown> {
182
199
  actions: StartAction[];
183
200
  installNeeded: boolean;
184
201
  installUrl: string;
202
+ /** The platform's own notices (desktop.md §10.4), each with the actions it offers. */
203
+ notices: NoticeRow[];
185
204
  hasRecent: boolean;
186
205
  recent: RecentRow[];
187
206
  hasCatalogue: boolean;
@@ -190,6 +209,7 @@ interface WelcomeScope extends Record<string, unknown> {
190
209
  open: (root: string) => void;
191
210
  remove: (root: string) => void;
192
211
  clear: () => void;
212
+ noticeRun: (key: string) => void;
193
213
  }
194
214
 
195
215
  let _state: WelcomeScope | null = null;
@@ -198,7 +218,7 @@ let _mount: Promise<SurfaceHandle> | null = null;
198
218
  let _handle: SurfaceHandle | null = null;
199
219
 
200
220
  /** The pane, as the surface reads it, from the platform and the stores as they stand now. */
201
- function project(): Omit<WelcomeScope, "run" | "open" | "remove" | "clear"> {
221
+ function project(): Omit<WelcomeScope, "run" | "open" | "remove" | "clear" | "noticeRun"> {
202
222
  const recent = getRecentProjects();
203
223
  // Catalogue entries already in Recent stay in that section only.
204
224
  const catalogue = getProjectList().filter((p) => !recent.some((r) => r.root === p.root));
@@ -230,6 +250,7 @@ function project(): Omit<WelcomeScope, "run" | "open" | "remove" | "clear"> {
230
250
  structured needs-installation 403 that `platform-errors.ts` decodes. */
231
251
  installNeeded: needsAppInstall(),
232
252
  installUrl: getAccountStatus()?.appInstallUrl ?? "#",
253
+ notices: getBannerNotices().map((notice) => noticeRow(notice)),
233
254
  recent: recent.map((entry) => ({
234
255
  location: recentLabels.get(entry.root) ?? entry.root,
235
256
  name: entry.name,
@@ -239,6 +260,36 @@ function project(): Omit<WelcomeScope, "run" | "open" | "remove" | "clear"> {
239
260
  };
240
261
  }
241
262
 
263
+ /**
264
+ * One notice, flattened for the document. Each action's key names its notice too, because the
265
+ * button's handler sees only the inner row; the primary one is the accent button.
266
+ */
267
+ function noticeRow(notice: AccountNotice): NoticeRow {
268
+ return {
269
+ actions: (notice.actions ?? []).map((action) => ({
270
+ key: `${notice.id}/${action.id}`,
271
+ label: action.label,
272
+ variant: action.primary ? "accent" : "secondary",
273
+ })),
274
+ id: notice.id,
275
+ level: notice.level ?? "info",
276
+ message: notice.message,
277
+ title: notice.title,
278
+ };
279
+ }
280
+
281
+ /** The action a notice button's key names, read from the notices as they stand now. */
282
+ function noticeAction(key: string): OfferedAction | null {
283
+ for (const notice of getBannerNotices()) {
284
+ for (const action of notice.actions ?? []) {
285
+ if (`${notice.id}/${action.id}` === key) {
286
+ return action;
287
+ }
288
+ }
289
+ }
290
+ return null;
291
+ }
292
+
242
293
  /** The reactive scope the surface reads, made once. */
243
294
  function state(): WelcomeScope {
244
295
  _state ??= reactive({
@@ -252,6 +303,14 @@ function state(): WelcomeScope {
252
303
  hasRecent: false,
253
304
  installNeeded: false,
254
305
  installUrl: "#",
306
+ noticeRun: (key: string) => {
307
+ const action = noticeAction(key);
308
+ if (action) {
309
+ // The account status is re-read after every action, so a notice it settled goes away.
310
+ void runOfferedAction(action).then(refresh);
311
+ }
312
+ },
313
+ notices: [],
255
314
  open: (root: string) => {
256
315
  _ctx?.openRecentProject(root);
257
316
  },