@jxsuite/studio 4.0.0 → 5.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 (613) hide show
  1. package/README.md +1 -1
  2. package/data/elements-meta.json +5 -15
  3. package/data/html-meta.json +51 -6
  4. package/dist/chunks/{2020-xp8vnwma.js → 2020-g79n01v9.js} +14 -14
  5. package/dist/chunks/action-group-sp6tna8v.js +118 -0
  6. package/dist/chunks/adapter-nj7tnjfn.js +13 -0
  7. package/dist/chunks/cf-account-picker-fyf63pmy.js +495 -0
  8. package/dist/chunks/cf-settings-71bv5zn9.js +20 -0
  9. package/dist/chunks/color-area-v17df7b8.js +154 -0
  10. package/dist/chunks/color-field-anwybnkn.js +208 -0
  11. package/dist/chunks/color-slider-44ez96fc.js +79 -0
  12. package/dist/chunks/color-xgg3kfw7.js +35 -0
  13. package/dist/chunks/combobox-5e7jev23.js +236 -0
  14. package/dist/chunks/components-2w1f35qs.js +18 -0
  15. package/dist/chunks/content-source-3wqeas45.js +46 -0
  16. package/dist/chunks/convert-to-repeater-d0cgnb6d.js +639 -0
  17. package/dist/chunks/dialog-s890b918.js +85 -0
  18. package/dist/chunks/editor-3qhbg51x.js +35 -0
  19. package/dist/chunks/entry-commands-d6g50j7s.js +42 -0
  20. package/dist/chunks/extension-sections-fy7q4fe9.js +651 -0
  21. package/dist/chunks/file-ops-3btj6st0.js +47 -0
  22. package/dist/chunks/files-ef8fgs3w.js +72 -0
  23. package/dist/chunks/format-host-3c33nwr8.js +40 -0
  24. package/dist/chunks/fs-events-13tj6sex.js +18 -0
  25. package/dist/chunks/git-panel-4wp2rn9b.js +49 -0
  26. package/dist/chunks/icons-yy10wk9r.js +402 -0
  27. package/dist/chunks/iframe-entry-15g3n6xv.js +114 -0
  28. package/dist/chunks/iframe-entry-b27vq81n.js +203 -0
  29. package/dist/chunks/iframe-entry-nbyxee1y.js +154 -0
  30. package/dist/chunks/iframe-host-jgesd924.js +115 -0
  31. package/dist/chunks/{index-nj44jpzj.js → index-mdmyx6s2.js} +11 -11
  32. package/dist/chunks/index-n4j7qxyr.js +109 -0
  33. package/dist/chunks/{javascript-q8kpmfah.js → javascript-xvrfrpkb.js} +8 -9
  34. package/dist/chunks/{jsonMode-dkr040ga.js → jsonMode-tyx2kj6p.js} +526 -527
  35. package/dist/chunks/jx-validate-j0wxk72w.js +15 -0
  36. package/dist/chunks/layers-1nq5evsh.js +29 -0
  37. package/dist/chunks/layers-panel-ar2gh90g.js +40 -0
  38. package/dist/chunks/listbox-6v2ac08j.js +44 -0
  39. package/dist/chunks/lit-html-m244w9nh.js +17 -0
  40. package/dist/chunks/media-picker-2qbetq73.js +33 -0
  41. package/dist/chunks/menu-q99sy9rf.js +285 -0
  42. package/dist/chunks/menu-rf7n72mf.js +14 -0
  43. package/dist/chunks/{monaco-binding-rj6xpx9y.js → monaco-binding-sw6en4dw.js} +12 -15
  44. package/dist/chunks/monaco-buffer-bhy3hb6m.js +24 -0
  45. package/dist/chunks/{monaco-setup-rws7k4br.js → monaco-setup-8r62fj6y.js} +27 -37
  46. package/dist/chunks/notify-628t0anh.js +21 -0
  47. package/dist/chunks/number-field-5cndyft8.js +72 -0
  48. package/dist/chunks/parse-0zx14y15.js +10 -0
  49. package/dist/chunks/platform-ts9cg1a4.js +15 -0
  50. package/dist/chunks/popover-47z3ytva.js +25 -0
  51. package/dist/chunks/publish-panel-98f7sr3g.js +27 -0
  52. package/dist/chunks/quick-search-srzz83k2.js +45 -0
  53. package/dist/chunks/references-n553f0e1.js +26 -0
  54. package/dist/chunks/right-panel-rrgx2dn3.js +45 -0
  55. package/dist/chunks/section-registry-5pq32t0q.js +25 -0
  56. package/dist/chunks/select-2r16ez41.js +49 -0
  57. package/dist/chunks/split-g7nm1rgb.js +191 -0
  58. package/dist/chunks/studio-0hqbpbd0.js +62 -0
  59. package/dist/chunks/studio-18avnhjp.js +2430 -0
  60. package/dist/chunks/{studio-kryvsw6j.js → studio-1k325pwf.js} +48 -48
  61. package/dist/chunks/{studio-hbtp0eh1.js → studio-25hkdrkw.js} +356 -85
  62. package/dist/chunks/studio-26d7myd1.js +258 -0
  63. package/dist/chunks/{studio-p8j86wag.js → studio-38emantv.js} +22 -5
  64. package/dist/chunks/studio-3gxv60tn.js +422 -0
  65. package/dist/chunks/{studio-8swrjbny.js → studio-3hxkd92t.js} +275 -163
  66. package/dist/chunks/studio-4m0haseg.js +185 -0
  67. package/dist/chunks/{studio-ynxwkrzd.js → studio-4wbb9ct0.js} +1953 -1785
  68. package/dist/chunks/{studio-gv8vvshc.js → studio-5523bpzt.js} +11 -11
  69. package/dist/chunks/{studio-5q69gfag.js → studio-57mf66w2.js} +1753 -2484
  70. package/dist/chunks/studio-5jy81t3s.js +17463 -0
  71. package/dist/chunks/studio-5rt7sqa0.js +1168 -0
  72. package/dist/chunks/studio-5te5tqng.js +81 -0
  73. package/dist/chunks/{studio-p4ngc046.js → studio-6ewqv390.js} +9 -9
  74. package/dist/chunks/{studio-9ab8gqsm.js → studio-6nzaktvy.js} +7 -7
  75. package/dist/chunks/studio-6xa21ej9.js +28 -0
  76. package/dist/chunks/{studio-5ehhackm.js → studio-727xh0e3.js} +19 -19
  77. package/dist/chunks/{studio-eenfkt4e.js → studio-79y5sz51.js} +52 -38
  78. package/dist/chunks/{studio-4q0wznzf.js → studio-7e11jpa9.js} +461 -145
  79. package/dist/chunks/{studio-q7v10cg2.js → studio-7r42c6ca.js} +17 -7
  80. package/dist/chunks/studio-7xptrg0j.js +954 -0
  81. package/dist/chunks/{studio-cft9r4r7.js → studio-81390n6f.js} +22 -22
  82. package/dist/chunks/{studio-q43a5y9h.js → studio-8jn5yhyn.js} +16 -16
  83. package/dist/chunks/studio-8qtrpzr3.js +839 -0
  84. package/dist/chunks/{studio-dg7zyt5x.js → studio-98b3z07n.js} +1 -1
  85. package/dist/chunks/studio-9xxz1q69.js +1356 -0
  86. package/dist/chunks/studio-akbww0j5.js +13615 -0
  87. package/dist/chunks/studio-arfpe389.js +195 -0
  88. package/dist/chunks/{studio-2zkz8evn.js → studio-az3zd49t.js} +473 -281
  89. package/dist/chunks/studio-cpvb2ed1.js +1171 -0
  90. package/dist/chunks/{studio-9r6j5ev1.js → studio-d0300636.js} +5078 -19
  91. package/dist/chunks/{studio-bgxt93ga.js → studio-d055c37e.js} +13 -13
  92. package/dist/chunks/studio-d1ja2x9r.js +362 -0
  93. package/dist/chunks/{studio-b9b6t1dg.js → studio-e1f24eff.js} +5664 -5666
  94. package/dist/chunks/{studio-4pqqxbrt.js → studio-em9nw8eh.js} +301 -112
  95. package/dist/chunks/studio-erdatzvp.js +1190 -0
  96. package/dist/chunks/{studio-g1wdche0.js → studio-esrxgwh6.js} +80 -80
  97. package/dist/chunks/studio-fabxk6r9.js +2811 -0
  98. package/dist/chunks/{studio-3n4ndrzh.js → studio-h4nvnjvz.js} +303 -303
  99. package/dist/chunks/studio-hhvs4sdg.js +195 -0
  100. package/dist/chunks/{studio-68n67094.js → studio-kcgff4r0.js} +15 -18
  101. package/dist/chunks/{studio-685s0ytq.js → studio-nqgawfks.js} +572 -572
  102. package/dist/chunks/{studio-xrapxwwc.js → studio-p1dw8wfw.js} +55 -29
  103. package/dist/chunks/studio-pm6hgy7r.js +4763 -0
  104. package/dist/chunks/studio-q0a3yk4n.js +33 -0
  105. package/dist/chunks/{studio-wcy8trfx.js → studio-q3x06vmj.js} +23 -22
  106. package/dist/chunks/studio-qahxvnan.js +111 -0
  107. package/dist/chunks/{studio-53n8bp04.js → studio-r6xmmtfh.js} +1 -1
  108. package/dist/chunks/{studio-kjjsjak4.js → studio-rkstz940.js} +3661 -6136
  109. package/dist/chunks/{studio-tvgyw9xw.js → studio-rnyf3f8e.js} +83 -250
  110. package/dist/chunks/studio-rpyah2ew.js +2805 -0
  111. package/dist/chunks/studio-rs76k67r.js +12 -0
  112. package/dist/chunks/{studio-yxrkyv8a.js → studio-s6z853aq.js} +12108 -11766
  113. package/dist/chunks/{studio-sf7zjgv1.js → studio-sgdc5pfc.js} +8 -8
  114. package/dist/chunks/studio-tb3ev01j.js +763 -0
  115. package/dist/chunks/{studio-t5wqwzdh.js → studio-tjjk7jvk.js} +1 -1
  116. package/dist/chunks/studio-vhv5tgxm.js +51 -0
  117. package/dist/chunks/{studio-htsf0w6m.js → studio-vz68yn9e.js} +31 -154
  118. package/dist/chunks/studio-wqpngpsk.js +174 -0
  119. package/dist/chunks/{studio-eesa1myv.js → studio-x3rty6tr.js} +58 -58
  120. package/dist/chunks/studio-xsykpvsh.js +8448 -0
  121. package/dist/chunks/{studio-92ab2jqe.js → studio-y9zedyjn.js} +14 -14
  122. package/dist/chunks/studio-yxa81f45.js +12 -0
  123. package/dist/chunks/swatch-group-t0n6h2rn.js +139 -0
  124. package/dist/chunks/tabs-wbp1cdws.js +156 -0
  125. package/dist/chunks/textfield-abk6qyee.js +73 -0
  126. package/dist/chunks/toast-780pvfbv.js +27 -0
  127. package/dist/chunks/toast-host-3sqv46sn.js +116 -0
  128. package/dist/chunks/toolbar-xbh5bhtr.js +194 -0
  129. package/dist/chunks/tooltip-eptb614w.js +369 -0
  130. package/dist/chunks/tree-a73p0pqm.js +367 -0
  131. package/dist/chunks/{tsMode-76kr53q0.js → tsMode-jy4m86rv.js} +83 -85
  132. package/dist/chunks/workspace-wrew8zk4.js +69 -0
  133. package/dist/chunks/{ws-client-6shc5rze.js → ws-client-ny4zc51r.js} +20 -22
  134. package/dist/iframe-entry.js +1812 -698
  135. package/dist/manifest.json +7 -0
  136. package/dist/studio.js +52900 -36573
  137. package/dist/workers/editor.worker.js +1040 -1085
  138. package/dist/workers/json.worker.js +1946 -1991
  139. package/dist/workers/ts.worker.js +4960 -5005
  140. package/favicon.ico +0 -0
  141. package/index.html +2 -0
  142. package/package.json +22 -55
  143. package/src/about/about-modal.ts +73 -123
  144. package/src/browse/library-commands.ts +20 -49
  145. package/src/browse/library-layouts.ts +87 -218
  146. package/src/browse/library-pane.ts +406 -438
  147. package/src/canvas/canvas-helpers.ts +0 -49
  148. package/src/canvas/canvas-live-render.ts +25 -2
  149. package/src/canvas/canvas-render.ts +666 -231
  150. package/src/canvas/canvas-surface.ts +0 -1
  151. package/src/canvas/canvas-utils.ts +310 -117
  152. package/src/canvas/dialog-path.ts +76 -0
  153. package/src/canvas/dialog-state.ts +67 -0
  154. package/src/canvas/diff-marks.ts +252 -0
  155. package/src/canvas/diff-toolbar.ts +349 -0
  156. package/src/canvas/diff-view.ts +123 -0
  157. package/src/canvas/edit-width-drag.ts +187 -0
  158. package/src/canvas/edit-width.ts +150 -0
  159. package/src/canvas/iframe-entry.ts +125 -8
  160. package/src/canvas/iframe-host.ts +271 -15
  161. package/src/canvas/iframe-interaction.ts +133 -13
  162. package/src/canvas/iframe-patch.ts +7 -1
  163. package/src/canvas/iframe-position.ts +5 -6
  164. package/src/canvas/iframe-protocol.ts +106 -5
  165. package/src/canvas/iframe-render.ts +373 -5
  166. package/src/canvas/iframe-slash.ts +2 -2
  167. package/src/canvas/iframe-subtree.ts +5 -8
  168. package/src/canvas/path-mapping.ts +25 -3
  169. package/src/canvas/popover-path.ts +106 -0
  170. package/src/canvas/popover-state.ts +155 -0
  171. package/src/canvas/surface-registry.ts +21 -10
  172. package/src/collab/presence-chips.ts +51 -73
  173. package/src/commands/app-commands.ts +10 -0
  174. package/src/commands/budget.ts +12 -0
  175. package/src/commands/command-args.ts +369 -6
  176. package/src/commands/defaults.ts +44 -14
  177. package/src/commands/live-context.ts +11 -5
  178. package/src/commands/reference.ts +12 -2
  179. package/src/commands/registry.ts +165 -8
  180. package/src/content/entry-commands.ts +16 -23
  181. package/src/content/entry-editor.ts +144 -101
  182. package/src/editor/canvas-slash-bridge.ts +2 -2
  183. package/src/editor/context-menu.ts +49 -200
  184. package/src/editor/convert-to-component.ts +42 -82
  185. package/src/editor/convert-to-repeater.ts +132 -173
  186. package/src/editor/inline-edit.ts +11 -0
  187. package/src/editor/shortcuts.ts +65 -26
  188. package/src/editor/slash-menu.ts +150 -157
  189. package/src/files/file-ops.ts +117 -11
  190. package/src/files/files.ts +949 -664
  191. package/src/files/json-layout.ts +474 -0
  192. package/src/files/media-paths.ts +13 -42
  193. package/src/files/media-usage.ts +41 -131
  194. package/src/files/serialize-document.ts +8 -1
  195. package/src/format/convert-file.ts +16 -10
  196. package/src/format/format-host.ts +50 -1
  197. package/src/github/github-auth.ts +71 -29
  198. package/src/github/github-publish.ts +57 -65
  199. package/src/grid/cell-editors.ts +46 -4
  200. package/src/grid/cell-popovers.ts +65 -62
  201. package/src/grid/grid-open.ts +86 -64
  202. package/src/grid/grid-panel.ts +390 -469
  203. package/src/grid/redirects-grid.ts +40 -44
  204. package/src/hosting/document.ts +2 -0
  205. package/src/hosting/layout.ts +10 -0
  206. package/src/i18n/i18n-commands.ts +127 -62
  207. package/src/media/media-pane.ts +83 -133
  208. package/src/new-project/add-repo-modal.ts +125 -176
  209. package/src/new-project/import-tab.ts +201 -250
  210. package/src/new-project/location-fields.ts +101 -165
  211. package/src/new-project/new-project-modal.ts +376 -367
  212. package/src/packages/jxsuite-update.ts +76 -18
  213. package/src/panels/activity-panel.ts +137 -104
  214. package/src/panels/ai-chat/chat-markdown.ts +15 -13
  215. package/src/panels/ai-chat/chat-view.ts +307 -472
  216. package/src/panels/ai-chat/composer.ts +175 -153
  217. package/src/panels/ai-chat/sessions-view.ts +31 -62
  218. package/src/panels/ai-panel.ts +210 -185
  219. package/src/panels/block-action-bar.ts +437 -643
  220. package/src/panels/bottom-dock.ts +112 -47
  221. package/src/panels/chat-panel.ts +19 -14
  222. package/src/panels/data-explorer.ts +141 -102
  223. package/src/panels/data-grid.ts +183 -150
  224. package/src/panels/dnd.ts +75 -20
  225. package/src/panels/editors.ts +26 -19
  226. package/src/panels/elements-panel.ts +243 -186
  227. package/src/panels/empty-state.ts +26 -42
  228. package/src/panels/events-panel.ts +950 -729
  229. package/src/panels/formula-workspace.ts +434 -207
  230. package/src/panels/frontmatter-fields.ts +185 -153
  231. package/src/panels/frontmatter-panel.ts +411 -187
  232. package/src/panels/git-diff-open.ts +148 -0
  233. package/src/panels/git-panel.ts +679 -605
  234. package/src/panels/head-panel.ts +617 -435
  235. package/src/panels/i18n-panel.ts +153 -133
  236. package/src/panels/imports-panel.ts +388 -414
  237. package/src/panels/jump-bar.ts +174 -147
  238. package/src/panels/layers-panel.ts +785 -671
  239. package/src/panels/left-panel.ts +166 -94
  240. package/src/panels/navigator-panels.ts +1 -1
  241. package/src/panels/pane-context.ts +529 -677
  242. package/src/panels/pane-grid.ts +265 -241
  243. package/src/panels/panel-registry.ts +15 -4
  244. package/src/panels/problems-panel.ts +177 -99
  245. package/src/panels/properties-panel.ts +1525 -966
  246. package/src/panels/provenance.ts +31 -113
  247. package/src/panels/quick-search.ts +217 -172
  248. package/src/panels/right-panel.ts +135 -169
  249. package/src/panels/seo-modal.ts +346 -167
  250. package/src/panels/settings-menu.ts +109 -646
  251. package/src/panels/settings-pane.ts +102 -86
  252. package/src/panels/signals-panel.ts +1318 -1172
  253. package/src/panels/statement-editor.ts +556 -428
  254. package/src/panels/style-panel.ts +1367 -630
  255. package/src/panels/style-utils.ts +36 -32
  256. package/src/panels/stylebook-doc.ts +30 -73
  257. package/src/panels/stylebook-layers-panel.ts +202 -109
  258. package/src/panels/stylebook-panel.ts +124 -88
  259. package/src/panels/tab-strip.ts +385 -287
  260. package/src/platforms/cloud.ts +347 -86
  261. package/src/platforms/devserver.ts +13 -0
  262. package/src/publish/deploy-checklist.ts +124 -78
  263. package/src/publish/publish-commands.ts +4 -12
  264. package/src/publish/publish-panel.ts +165 -299
  265. package/src/services/a11y-report.ts +100 -43
  266. package/src/services/ai-command-tools.ts +468 -0
  267. package/src/services/ai-import-tools.ts +50 -12
  268. package/src/services/ai-models.ts +108 -1
  269. package/src/services/ai-session-store.ts +6 -0
  270. package/src/services/ai-system-prompt.ts +110 -18
  271. package/src/services/ai-tools.ts +28 -143
  272. package/src/services/ai-write-report.ts +174 -0
  273. package/src/services/context-manager.ts +12 -1
  274. package/src/services/document-assistant.ts +50 -2
  275. package/src/services/idle.ts +16 -14
  276. package/src/services/live-surfaces.ts +237 -0
  277. package/src/services/model-uri.ts +75 -0
  278. package/src/services/notify.ts +63 -6
  279. package/src/services/popover-report.ts +325 -0
  280. package/src/services/surface-registry.ts +56 -0
  281. package/src/services/tool-executor.ts +17 -0
  282. package/src/settings/contexts-section.ts +301 -299
  283. package/src/settings/contributed-section.ts +361 -222
  284. package/src/settings/css-vars-editor.ts +320 -325
  285. package/src/settings/defs-editor.ts +572 -535
  286. package/src/settings/dependencies-editor.ts +79 -107
  287. package/src/settings/extension-commands.ts +379 -0
  288. package/src/settings/extension-rows.ts +113 -0
  289. package/src/settings/extensions-section.ts +287 -0
  290. package/src/settings/general-settings.ts +130 -181
  291. package/src/settings/head-editor.ts +10 -305
  292. package/src/settings/locales-section.ts +146 -196
  293. package/src/settings/preferences-accounts.ts +193 -12
  294. package/src/settings/preferences-dialog.ts +384 -307
  295. package/src/settings/project-sections.ts +108 -181
  296. package/src/settings/schema-field-ui.ts +11 -399
  297. package/src/settings/section-registry.ts +8 -1
  298. package/src/settings/settings-document.ts +31 -29
  299. package/src/shell/tree.ts +42 -87
  300. package/src/shell.ts +48 -36
  301. package/src/studio.ts +90 -67
  302. package/src/style/token-ref.ts +0 -39
  303. package/src/surfaces/about.json +263 -0
  304. package/src/surfaces/about.ts +163 -0
  305. package/src/surfaces/add-repo.json +486 -0
  306. package/src/surfaces/add-repo.ts +236 -0
  307. package/src/surfaces/ai-chat.json +2185 -0
  308. package/src/surfaces/ai-chat.ts +549 -0
  309. package/src/surfaces/ai-credentials-form.json +332 -0
  310. package/src/surfaces/ai-credentials-form.ts +151 -0
  311. package/src/surfaces/ai-managed-connect.json +108 -0
  312. package/src/surfaces/ai-managed-connect.ts +123 -0
  313. package/src/surfaces/ai-model-picker.json +114 -0
  314. package/src/surfaces/ai-model-picker.ts +139 -0
  315. package/src/surfaces/block-action-bar.json +640 -0
  316. package/src/surfaces/block-action-bar.ts +368 -0
  317. package/src/surfaces/bottom-dock.json +154 -0
  318. package/src/surfaces/bottom-dock.ts +125 -0
  319. package/src/surfaces/canvas-stage.json +444 -0
  320. package/src/surfaces/canvas-stage.ts +253 -0
  321. package/src/surfaces/cf-account-picker.json +300 -0
  322. package/src/surfaces/cf-account-picker.ts +214 -0
  323. package/src/surfaces/commandbar.json +984 -0
  324. package/src/{panels/toolbar.ts → surfaces/commandbar.ts} +414 -389
  325. package/src/surfaces/convert-repeater.json +309 -0
  326. package/src/surfaces/convert-repeater.ts +276 -0
  327. package/src/surfaces/data-actions.json +157 -0
  328. package/src/surfaces/data-actions.ts +115 -0
  329. package/src/surfaces/dialog.json +289 -0
  330. package/src/surfaces/dialog.ts +343 -0
  331. package/src/surfaces/diff-toolbar.json +266 -0
  332. package/src/surfaces/diff-toolbar.ts +139 -0
  333. package/src/surfaces/doc-header.json +739 -0
  334. package/src/surfaces/doc-header.ts +223 -0
  335. package/src/surfaces/empty-state.json +152 -0
  336. package/src/surfaces/empty-state.ts +173 -0
  337. package/src/surfaces/entry-editor.json +281 -0
  338. package/src/surfaces/entry-editor.ts +191 -0
  339. package/src/surfaces/expression-editor.json +977 -0
  340. package/src/surfaces/expression-editor.ts +266 -0
  341. package/src/surfaces/files-panel.json +592 -0
  342. package/src/surfaces/files-panel.ts +301 -0
  343. package/src/surfaces/formula-palette.json +364 -0
  344. package/src/surfaces/formula-palette.ts +340 -0
  345. package/src/surfaces/git-panel.json +1822 -0
  346. package/src/surfaces/git-panel.ts +272 -0
  347. package/src/surfaces/github-auth.json +112 -0
  348. package/src/surfaces/github-auth.ts +139 -0
  349. package/src/surfaces/github-publish.json +206 -0
  350. package/src/surfaces/github-publish.ts +169 -0
  351. package/src/surfaces/grid-cell.json +218 -0
  352. package/src/surfaces/grid-cell.ts +120 -0
  353. package/src/surfaces/grid-open.json +147 -0
  354. package/src/surfaces/grid-open.ts +87 -0
  355. package/src/surfaces/grid-panel.json +706 -0
  356. package/src/surfaces/grid-panel.ts +206 -0
  357. package/src/surfaces/grid-replace.json +179 -0
  358. package/src/surfaces/grid-replace.ts +89 -0
  359. package/src/surfaces/grid-views.json +446 -0
  360. package/src/surfaces/grid-views.ts +172 -0
  361. package/src/surfaces/inspector-dock.json +187 -0
  362. package/src/surfaces/inspector-dock.ts +161 -0
  363. package/src/surfaces/jump-bar.json +243 -0
  364. package/src/surfaces/jump-bar.ts +129 -0
  365. package/src/surfaces/jxsuite-update.json +146 -0
  366. package/src/surfaces/jxsuite-update.ts +149 -0
  367. package/src/surfaces/library-pane.json +1509 -0
  368. package/src/surfaces/library-pane.ts +363 -0
  369. package/src/surfaces/logic-panel.json +1949 -0
  370. package/src/surfaces/logic-panel.ts +369 -0
  371. package/src/surfaces/logic-workspace.json +685 -0
  372. package/src/surfaces/logic-workspace.ts +238 -0
  373. package/src/surfaces/media-browser.json +303 -0
  374. package/src/surfaces/media-browser.ts +145 -0
  375. package/src/surfaces/media-field.json +163 -0
  376. package/src/surfaces/media-field.ts +116 -0
  377. package/src/surfaces/media-pane.json +513 -0
  378. package/src/surfaces/media-pane.ts +197 -0
  379. package/src/surfaces/menu.json +232 -0
  380. package/src/surfaces/menu.ts +375 -0
  381. package/src/surfaces/navigator-dock.json +113 -0
  382. package/src/surfaces/navigator-dock.ts +130 -0
  383. package/src/surfaces/new-project.json +1654 -0
  384. package/src/surfaces/new-project.ts +411 -0
  385. package/src/surfaces/palette.json +535 -0
  386. package/src/surfaces/pane-context.json +1497 -0
  387. package/src/surfaces/pane-context.ts +513 -0
  388. package/src/surfaces/pane-grid.json +188 -0
  389. package/src/surfaces/pane-grid.ts +250 -0
  390. package/src/surfaces/panel-activity.json +563 -0
  391. package/src/surfaces/panel-activity.ts +187 -0
  392. package/src/surfaces/panel-data.json +141 -0
  393. package/src/surfaces/panel-data.ts +156 -0
  394. package/src/surfaces/panel-deploy-checklist.json +283 -0
  395. package/src/surfaces/panel-deploy-checklist.ts +175 -0
  396. package/src/surfaces/panel-elements.json +367 -0
  397. package/src/surfaces/panel-elements.ts +175 -0
  398. package/src/surfaces/panel-i18n.json +569 -0
  399. package/src/surfaces/panel-i18n.ts +212 -0
  400. package/src/surfaces/panel-imports.json +780 -0
  401. package/src/surfaces/panel-imports.ts +209 -0
  402. package/src/surfaces/panel-outline.json +734 -0
  403. package/src/surfaces/panel-outline.ts +326 -0
  404. package/src/surfaces/panel-page.json +1017 -0
  405. package/src/surfaces/panel-page.ts +240 -0
  406. package/src/surfaces/panel-problems.json +497 -0
  407. package/src/surfaces/panel-problems.ts +157 -0
  408. package/src/surfaces/panel-signals.json +1973 -0
  409. package/src/surfaces/panel-signals.ts +436 -0
  410. package/src/surfaces/panel-stylebook-layers.json +317 -0
  411. package/src/surfaces/panel-stylebook-layers.ts +155 -0
  412. package/src/surfaces/preferences.json +850 -0
  413. package/src/surfaces/preferences.ts +332 -0
  414. package/src/surfaces/progress-modal.json +110 -0
  415. package/src/surfaces/progress-modal.ts +150 -0
  416. package/src/surfaces/properties-panel.json +1578 -0
  417. package/src/surfaces/properties-panel.ts +337 -0
  418. package/src/surfaces/publish.json +945 -0
  419. package/src/surfaces/publish.ts +392 -0
  420. package/src/surfaces/push-plan.json +110 -0
  421. package/src/surfaces/push-plan.ts +87 -0
  422. package/src/surfaces/rail.json +255 -0
  423. package/src/surfaces/rail.ts +206 -0
  424. package/src/surfaces/reference-field.json +191 -0
  425. package/src/surfaces/reference-field.ts +168 -0
  426. package/src/surfaces/schema-builder.json +1011 -0
  427. package/src/surfaces/schema-builder.ts +181 -0
  428. package/src/surfaces/schema-form.json +1013 -0
  429. package/src/surfaces/schema-form.ts +287 -0
  430. package/src/surfaces/secret-field.json +60 -0
  431. package/src/surfaces/secret-field.ts +146 -0
  432. package/src/surfaces/seo.json +932 -0
  433. package/src/surfaces/seo.ts +334 -0
  434. package/src/surfaces/settings-contexts.json +538 -0
  435. package/src/surfaces/settings-contexts.ts +196 -0
  436. package/src/surfaces/settings-contributed.json +489 -0
  437. package/src/surfaces/settings-contributed.ts +199 -0
  438. package/src/surfaces/settings-css-vars.json +820 -0
  439. package/src/surfaces/settings-css-vars.ts +199 -0
  440. package/src/surfaces/settings-defs.json +1363 -0
  441. package/src/surfaces/settings-defs.ts +232 -0
  442. package/src/surfaces/settings-deploy.json +108 -0
  443. package/src/surfaces/settings-deploy.ts +135 -0
  444. package/src/surfaces/settings-extensions.json +486 -0
  445. package/src/surfaces/settings-extensions.ts +201 -0
  446. package/src/surfaces/settings-head.json +779 -0
  447. package/src/surfaces/settings-head.ts +397 -0
  448. package/src/surfaces/settings-locales.json +420 -0
  449. package/src/surfaces/settings-locales.ts +137 -0
  450. package/src/surfaces/settings-overview.json +423 -0
  451. package/src/surfaces/settings-overview.ts +218 -0
  452. package/src/surfaces/settings-packages.json +549 -0
  453. package/src/surfaces/settings-packages.ts +158 -0
  454. package/src/surfaces/settings-pane.json +147 -0
  455. package/src/surfaces/settings-pane.ts +150 -0
  456. package/src/surfaces/settings-rawjson.json +104 -0
  457. package/src/surfaces/settings-rawjson.ts +107 -0
  458. package/src/surfaces/shell.json +99 -0
  459. package/src/surfaces/shell.ts +30 -0
  460. package/src/surfaces/slash-menu.json +180 -0
  461. package/src/surfaces/slash-menu.ts +272 -0
  462. package/src/surfaces/statements.json +607 -0
  463. package/src/surfaces/statements.ts +238 -0
  464. package/src/surfaces/statusbar.json +94 -0
  465. package/src/{panels → surfaces}/statusbar.ts +163 -90
  466. package/src/surfaces/style-panel.json +1643 -0
  467. package/src/surfaces/style-panel.ts +338 -0
  468. package/src/surfaces/stylebook-chrome.json +122 -0
  469. package/src/surfaces/stylebook-chrome.ts +122 -0
  470. package/src/surfaces/tab-strip.json +448 -0
  471. package/src/surfaces/tab-strip.ts +200 -0
  472. package/src/surfaces/target-line.json +419 -0
  473. package/src/surfaces/target-line.ts +349 -0
  474. package/src/surfaces/toasts.json +189 -0
  475. package/src/surfaces/welcome.json +571 -0
  476. package/src/surfaces/welcome.ts +338 -0
  477. package/src/tabs/project-config.ts +71 -15
  478. package/src/tabs/tab.ts +52 -1
  479. package/src/types.ts +49 -3
  480. package/src/ui/ai-credentials-form.ts +104 -100
  481. package/src/ui/ai-managed-connect.ts +116 -49
  482. package/src/ui/ai-model-picker.ts +102 -47
  483. package/src/ui/cf-account-picker.ts +135 -0
  484. package/src/ui/color-selector.ts +80 -301
  485. package/src/ui/dynamic-slot.ts +43 -283
  486. package/src/ui/expression-editor.ts +797 -576
  487. package/src/ui/field-input.ts +8 -181
  488. package/src/ui/form-controls.ts +838 -366
  489. package/src/ui/formula-chips.ts +80 -85
  490. package/src/ui/kit.ts +33 -0
  491. package/src/ui/layers.ts +293 -593
  492. package/src/ui/media-picker.ts +203 -284
  493. package/src/ui/panel-resize.ts +251 -64
  494. package/src/ui/popover-surface.ts +153 -0
  495. package/src/ui/progress-modal.ts +22 -60
  496. package/src/ui/regions.ts +13 -7
  497. package/src/ui/schema-form.ts +906 -440
  498. package/src/ui/surface.ts +122 -0
  499. package/src/ui/unit-selector.ts +15 -131
  500. package/src/ui/value-source.ts +14 -0
  501. package/src/ui/virtual-window.ts +88 -9
  502. package/src/utils/canvas-media.ts +72 -0
  503. package/src/utils/edit-display.ts +7 -2
  504. package/src/utils/element-selectors.ts +69 -0
  505. package/src/utils/link-target.ts +2 -2
  506. package/src/utils/studio-utils.ts +0 -10
  507. package/src/view.ts +15 -0
  508. package/src/workspace/pane-derive.ts +36 -14
  509. package/src/workspace/workspace.ts +15 -6
  510. package/styles/canvas.css +72 -467
  511. package/styles/forced-colors.css +48 -82
  512. package/styles/forced-colors.json +41 -0
  513. package/styles/inspector.css +2 -1301
  514. package/styles/overlays.css +96 -2823
  515. package/styles/panels.css +6 -1423
  516. package/styles/shell-frame.css +272 -0
  517. package/styles/shell-frame.json +227 -0
  518. package/styles/shell.css +0 -1532
  519. package/styles/tokens.css +65 -136
  520. package/styles/tokens.json +84 -0
  521. package/dist/chunks/Overlay.dev-723bv0df.js +0 -23
  522. package/dist/chunks/adapter-jkcyhgtr.js +0 -14
  523. package/dist/chunks/cf-settings-45fgyx8y.js +0 -21
  524. package/dist/chunks/components-zmx10t62.js +0 -21
  525. package/dist/chunks/content-source-t88vb2dr.js +0 -49
  526. package/dist/chunks/convert-to-repeater-dhw1nrz9.js +0 -268
  527. package/dist/chunks/editor-cptq5jp1.js +0 -37
  528. package/dist/chunks/entry-commands-6z6bhxg0.js +0 -46
  529. package/dist/chunks/extension-sections-3ydgjbdy.js +0 -311
  530. package/dist/chunks/file-ops-rskmhxyq.js +0 -48
  531. package/dist/chunks/files-0cratsr0.js +0 -92
  532. package/dist/chunks/flow-qbfgrpeh.js +0 -634
  533. package/dist/chunks/focus-trap.esm-ggawf1hy.js +0 -1024
  534. package/dist/chunks/focus-visible-6eg70z18.js +0 -167
  535. package/dist/chunks/format-host-tgcmfs4p.js +0 -43
  536. package/dist/chunks/fs-events-hrrg328g.js +0 -21
  537. package/dist/chunks/git-panel-0yywyk76.js +0 -58
  538. package/dist/chunks/index-zqxmqxmh.js +0 -109
  539. package/dist/chunks/jx-validate-8q2g05x1.js +0 -19
  540. package/dist/chunks/layers-panel-z3tqe5cd.js +0 -67
  541. package/dist/chunks/layers-q75tj2mf.js +0 -45
  542. package/dist/chunks/lit-html-j055mkh8.js +0 -22
  543. package/dist/chunks/monaco-buffer-dqw0xxpx.js +0 -25
  544. package/dist/chunks/notify-yjm79n4s.js +0 -30
  545. package/dist/chunks/parse-24d0gay3.js +0 -19
  546. package/dist/chunks/platform-ba1j1gbh.js +0 -16
  547. package/dist/chunks/publish-panel-54jvhskq.js +0 -28
  548. package/dist/chunks/quick-search-cgfmjwq5.js +0 -65
  549. package/dist/chunks/references-3qx8fkyp.js +0 -29
  550. package/dist/chunks/right-panel-cqp8bc4v.js +0 -46
  551. package/dist/chunks/section-registry-sx1ykvkw.js +0 -30
  552. package/dist/chunks/sp-overlay.dev-yztmtrs1.js +0 -18
  553. package/dist/chunks/sp-popover.dev-n2etmqdp.js +0 -9
  554. package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +0 -15
  555. package/dist/chunks/sp-tooltip.dev-xcvpnqr0.js +0 -15
  556. package/dist/chunks/sp-tray.dev-tkbscm06.js +0 -199
  557. package/dist/chunks/studio-0a5qrh01.js +0 -3
  558. package/dist/chunks/studio-18j3bmcz.js +0 -14
  559. package/dist/chunks/studio-1hbqnt19.js +0 -1823
  560. package/dist/chunks/studio-4rzyrtjz.js +0 -426
  561. package/dist/chunks/studio-5rywrkxz.js +0 -1903
  562. package/dist/chunks/studio-7afd21nw.js +0 -67
  563. package/dist/chunks/studio-7nte2hj4.js +0 -613
  564. package/dist/chunks/studio-8jpzqk5s.js +0 -128
  565. package/dist/chunks/studio-8q517dfr.js +0 -24
  566. package/dist/chunks/studio-9c4k4vhv.js +0 -357
  567. package/dist/chunks/studio-9n4mrmnb.js +0 -184
  568. package/dist/chunks/studio-a0tswy0w.js +0 -58
  569. package/dist/chunks/studio-ahjfdy6e.js +0 -387
  570. package/dist/chunks/studio-ahnzd9rg.js +0 -23
  571. package/dist/chunks/studio-bw5dz6km.js +0 -608
  572. package/dist/chunks/studio-cb12yz9w.js +0 -15055
  573. package/dist/chunks/studio-d1ks410h.js +0 -175
  574. package/dist/chunks/studio-eqq1w638.js +0 -113
  575. package/dist/chunks/studio-ex8zyp98.js +0 -1813
  576. package/dist/chunks/studio-h4rxscfc.js +0 -10
  577. package/dist/chunks/studio-kyh9avvv.js +0 -28
  578. package/dist/chunks/studio-kyk2v92a.js +0 -659
  579. package/dist/chunks/studio-m3hwn7y1.js +0 -120
  580. package/dist/chunks/studio-mmv72a27.js +0 -4344
  581. package/dist/chunks/studio-n8b9wqwy.js +0 -291
  582. package/dist/chunks/studio-pdfjntbk.js +0 -6939
  583. package/dist/chunks/studio-qq6jym04.js +0 -53
  584. package/dist/chunks/studio-r1r7afxx.js +0 -1066
  585. package/dist/chunks/studio-rdx8jcxy.js +0 -83
  586. package/dist/chunks/studio-rn9ta2mz.js +0 -111
  587. package/dist/chunks/studio-t7qb9db8.js +0 -665
  588. package/dist/chunks/studio-tmc5emrw.js +0 -8
  589. package/dist/chunks/studio-txwkm6p9.js +0 -12
  590. package/dist/chunks/studio-v38hmvx1.js +0 -390
  591. package/dist/chunks/studio-vadzgkrz.js +0 -318
  592. package/dist/chunks/studio-wx5609xd.js +0 -341
  593. package/dist/chunks/studio-z6nw2ek2.js +0 -18
  594. package/dist/chunks/studio-zczps70q.js +0 -378
  595. package/dist/chunks/workspace-xpres40v.js +0 -89
  596. package/src/panels/activity-bar.ts +0 -251
  597. package/src/panels/panel-scheduler.ts +0 -219
  598. package/src/panels/style-inputs.ts +0 -237
  599. package/src/panels/target-line.ts +0 -274
  600. package/src/panels/welcome-screen.ts +0 -346
  601. package/src/ui/button-group.ts +0 -102
  602. package/src/ui/choice-field.ts +0 -72
  603. package/src/ui/field-row.ts +0 -249
  604. package/src/ui/formula-palette.ts +0 -213
  605. package/src/ui/icons.ts +0 -52
  606. package/src/ui/jx-theme.ts +0 -130
  607. package/src/ui/spectrum.ts +0 -340
  608. package/src/ui/value-selector.ts +0 -174
  609. package/src/ui/widgets.ts +0 -172
  610. /package/dist/chunks/{javascript-s56a8dpk.css → javascript-4abzjcy9.css} +0 -0
  611. /package/dist/chunks/{jsonMode-zr9sw0c7.css → jsonMode-vvmpmk34.css} +0 -0
  612. /package/dist/chunks/{monaco-setup-pav68602.css → monaco-setup-8vbyqnhr.css} +0 -0
  613. /package/dist/chunks/{tsMode-ef187txx.css → tsMode-hnxxb73h.css} +0 -0
@@ -1,13 +1,14 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Chat-view.js — the active-conversation templates: header and message list.
3
+ * Chat-view.ts — the transcript's PROJECTION: what the assistant surface is told about a turn.
4
4
  *
5
- * Message-row anatomy: user messages render as right-aligned bubbles (attached-context
6
- * blocks become chips), assistant messages render sanitized markdown plus tool-call
7
- * chips, tool messages surface failures only (ADR §11.3), the streaming tail renders
8
- * as plain text with a cursor (markdown parses once on finalize), and chat errors get
9
- * a danger row with recovery advice and a Retry. Templates only — chat state lives in ai-panel, and
10
- * the three buttons are records the registry holds (see below).
5
+ * Message-row anatomy: user messages render as right-aligned bubbles (attached-context blocks
6
+ * become chips), assistant messages render sanitized markdown plus tool-call chips, tool messages
7
+ * surface failures only (ADR §11.3), the streaming tail renders as plain text with a cursor
8
+ * (markdown parses once on finalize), and chat errors get a danger row with recovery advice and a
9
+ * Retry. **None of that is markup here any more** — `surfaces/ai-chat.json` draws it and this
10
+ * module decides what it says, which is the same split the file always had with a document on the
11
+ * other side of it instead of a lit template.
11
12
  *
12
13
  * §7.4 (AI honesty) is why three things here are not what they were:
13
14
  *
@@ -24,24 +25,28 @@
24
25
  * §11.1 is why the three buttons here are not callbacks any more. History, New Chat and Retry were
25
26
  * closures this module received and invoked, so the capabilities existed ONLY as buttons: the
26
27
  * `Assistant` category held zero records, and nothing could reach them from the palette, a chord,
27
- * the automation runner or the generated commands sheet. They run {@link commandButton} now, in the
28
- * idiom `panels/statusbar.ts` established — the record is the definition site, and this file only
29
- * decides where it is drawn.
28
+ * the automation runner or the generated commands sheet. They are {@link projectCommand} now, in
29
+ * the idiom `surfaces/statusbar.ts` established — the record is the definition site, and this file
30
+ * only decides where it is drawn and with which glyph.
30
31
  *
31
32
  * @license MIT
32
33
  */
33
34
 
34
- import { html, nothing } from "lit-html";
35
- import { repeat } from "lit-html/directives/repeat.js";
36
- import type { TemplateResult } from "lit-html";
37
- import { ref } from "lit-html/directives/ref.js";
38
35
  import type { Message, ToolCallRecord } from "@jxsuite/ai/chat-state";
39
36
  import { splitAttachedContext } from "./attached-context";
40
37
  import { renderMarkdown } from "./chat-markdown";
41
38
  import { summarizeWrites, writesForTurn } from "../../services/ai-writes";
42
39
  import { activeRegistry } from "../../commands/active-registry";
43
40
 
41
+ import type { CommandRegistry } from "../../commands/registry";
44
42
  import type { ImportRunRecord } from "../../services/import-run";
43
+ import type {
44
+ ChatChangeView,
45
+ ChatChipView,
46
+ ChatCommandView,
47
+ ChatContextChip,
48
+ ChatRowView,
49
+ } from "../../surfaces/ai-chat";
45
50
 
46
51
  // ─── Helpers (moved from ai-panel.ts) ────────────────────────────────────────
47
52
 
@@ -116,146 +121,93 @@ export function formatErrorAdvice(error: string): string {
116
121
 
117
122
  // ─── Commands as buttons ────────────────────────────────────────────────────
118
123
 
119
- /** What {@link commandButton} draws inside the button, and how. */
124
+ /** Where a projected command is drawn, and with what. */
120
125
  export interface CommandButtonOptions {
121
- /** Slotted content — an icon element for the header, a label for the error row's Retry. */
122
- content: TemplateResult | string;
123
- /** Extra class, so the CSS that already targets `.ai-msg-retry` keeps landing. */
124
- className?: string;
125
- /** Quiet chrome. The header's icon buttons are quiet; the labelled Retry is not. */
126
- quiet?: boolean;
126
+ /** The kit glyph, by its name in the manifest. Empty for a text-only button. */
127
+ icon?: string;
128
+ /** Visible text. Empty on an icon-only button. */
129
+ text?: string;
127
130
  }
128
131
 
129
132
  /**
130
- * One control that IS a command — `panels/statusbar.ts`'s `itemTpl`, for the assistant.
133
+ * One control that IS a command — `surfaces/statusbar.ts`'s `projectItem`, for the assistant.
131
134
  *
132
- * A command the registry does not hold, or whose `when` is false, renders NOTHING rather than a
133
- * dead button; a visible-but-refused one renders disabled with its `requires` sentence in the
134
- * tooltip. That is what keeps this file a rendering of the registry instead of a second place the
135
- * assistant's capabilities are decided — and it is why `tests/ai-chat-view.test.ts` asserts the
136
- * ids, exactly as `tests/statusbar.test.ts` does: an id is not an interface between two files
137
- * unless something checks it.
135
+ * A command the registry does not hold, or whose `when` is false, projects to NOTHING rather than a
136
+ * dead button; a visible-but-refused one projects disabled with its `requires` sentence in the
137
+ * tooltip. That is what keeps the assistant a rendering of the registry instead of a second place
138
+ * its capabilities are decided — and it is why `tests/ai-chat-view.test.ts` asserts the ids,
139
+ * exactly as `tests/statusbar.test.ts` does: an id is not an interface between two files unless
140
+ * something checks it.
138
141
  *
139
142
  * Before any registry exists (the bootstrap composes one at the END of `studio.ts`, and a reduced
140
143
  * test fixture may compose none) the button is simply absent. The chat is still readable, which is
141
144
  * the same bargain the status bar strikes for the frame it paints early.
145
+ *
146
+ * @param {string} id
147
+ * @param {CommandButtonOptions} [opts]
148
+ * @param {CommandRegistry | null} [registry] The registry to ask; the active one by default.
149
+ * @returns {ChatCommandView | null}
142
150
  */
143
- export function commandButton(
151
+ export function projectCommand(
144
152
  id: string,
145
- opts: CommandButtonOptions,
146
- ): TemplateResult | typeof nothing {
147
- const registry = activeRegistry();
153
+ opts: CommandButtonOptions = {},
154
+ registry: CommandRegistry | null = activeRegistry(),
155
+ ): ChatCommandView | null {
148
156
  const command = registry?.get(id);
149
157
  if (!registry || !command || !registry.isVisible(id)) {
150
- return nothing;
158
+ return null;
151
159
  }
152
160
  const reason = registry.disabledReason(id);
153
161
  const chord = registry.keymap.formatBinding(id);
154
- const title = reason
162
+ const hint = reason
155
163
  ? `${command.title} — requires ${reason}`
156
164
  : chord
157
165
  ? `${command.title} (${chord})`
158
166
  : command.title;
159
- return html`<sp-action-button
160
- size="s"
161
- ?quiet=${opts.quiet ?? false}
162
- class=${opts.className ?? ""}
163
- ?disabled=${reason !== undefined}
164
- title=${title}
165
- @click=${() => {
166
- /* Re-asked at click time, not trusted from the render: state moves between the two, and
167
- `registry.run` THROWS on a refusal. Same bargain `registry.handleKeyEvent` strikes for a
168
- chord bound to a disabled command — swallow it here rather than make every surface wrap a
169
- dispatch in try/catch. */
170
- if (registry.isEnabled(id)) {
171
- void registry.run(id);
172
- }
173
- }}
174
- >
175
- ${opts.content}
176
- </sp-action-button>`;
167
+ return {
168
+ disabled: reason !== undefined,
169
+ hint,
170
+ icon: opts.icon ?? "",
171
+ id,
172
+ key: id,
173
+ label: command.title,
174
+ text: opts.text ?? "",
175
+ };
177
176
  }
178
177
 
179
- // ─── Header ─────────────────────────────────────────────────────────────────
180
-
181
- export interface ChatHeaderOptions {
182
- /** The open session's title, or null for a fresh unsaved chat. */
183
- title: string | null;
184
- streaming: boolean;
185
- /**
186
- * The conversation's estimated token count, and whether it is over the warning line.
187
- *
188
- * `services/context-manager.ts` has computed both on every turn since it was written, and
189
- * `chat-state.ts` has stored them — with NO READER anywhere. Plan §11.6: "Context budget manager
190
- * → tokenCount / contextWarning actually rendered". So a conversation was silently trimmed, the
191
- * assistant forgot what you told it ten turns ago, and the two numbers that would have explained
192
- * why sat in the store.
193
- */
194
- tokens: number;
195
- overBudget: boolean;
178
+ /** The projections that survived, as a list — a `null` is a command that is not on offer. */
179
+ export function projectCommands(entries: readonly (ChatCommandView | null)[]): ChatCommandView[] {
180
+ return entries.filter((entry): entry is ChatCommandView => entry !== null);
196
181
  }
197
182
 
183
+ // ─── Header ─────────────────────────────────────────────────────────────────
184
+
198
185
  /** Compact token count: 18400 → "18.4k". A four-digit number in a 28px header is noise. */
199
- function tokenLabel(tokens: number): string {
186
+ export function tokenLabel(tokens: number): string {
200
187
  return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
201
188
  }
202
189
 
203
- /** The chat header: history button, session title, the context budget, spinner, New Chat. */
204
- export function renderChatHeader(opts: ChatHeaderOptions): TemplateResult {
205
- return html`
206
- <div class="ai-chat-header">
207
- ${commandButton("assistant.history", {
208
- content: html`<sp-icon-history slot="icon"></sp-icon-history>`,
209
- quiet: true,
210
- })}
211
- <span class="ai-chat-title">${opts.title ?? "New chat"}</span>
212
- <span class="ai-header-spacer"></span>
213
- ${
214
- opts.tokens > 0
215
- ? html`<span
216
- class=${opts.overBudget ? "ai-tokens ai-tokens--warn" : "ai-tokens"}
217
- title=${
218
- opts.overBudget
219
- ? `About ${opts.tokens.toLocaleString()} tokens — past half the model's context. ` +
220
- `The oldest turns are dropped as this grows; start a new chat to keep them.`
221
- : `About ${opts.tokens.toLocaleString()} tokens of the model's context in use`
222
- }
223
- >${tokenLabel(opts.tokens)}</span
224
- >`
225
- : nothing
226
- }
227
- ${
228
- opts.streaming
229
- ? html`<sp-progress-circle size="s" indeterminate></sp-progress-circle>`
230
- : nothing
231
- }
232
- ${commandButton("assistant.newChat", {
233
- content: html`<sp-icon-add slot="icon"></sp-icon-add>`,
234
- quiet: true,
235
- })}
236
- </div>
237
- `;
190
+ /**
191
+ * The sentence behind the token readout.
192
+ *
193
+ * `services/context-manager.ts` has computed both numbers on every turn since it was written, and
194
+ * `chat-state.ts` has stored them — with NO READER anywhere. Plan §11.6: "Context budget manager →
195
+ * tokenCount / contextWarning actually rendered". So a conversation was silently trimmed, the
196
+ * assistant forgot what you told it ten turns ago, and the two numbers that would have explained
197
+ * why sat in the store.
198
+ *
199
+ * @param {number} tokens
200
+ * @param {boolean} overBudget
201
+ * @returns {string}
202
+ */
203
+ export function tokenHint(tokens: number, overBudget: boolean): string {
204
+ return overBudget
205
+ ? `About ${tokens.toLocaleString()} tokens — past half the model's context. ` +
206
+ "The oldest turns are dropped as this grows; start a new chat to keep them."
207
+ : `About ${tokens.toLocaleString()} tokens of the model's context in use`;
238
208
  }
239
209
 
240
- // ─── Message rows ───────────────────────────────────────────────────────────
241
-
242
- function renderUserMessage(msg: Message): TemplateResult {
243
- const { body, contextLines } = splitAttachedContext(msg.content);
244
- return html`
245
- <div class="ai-msg-user">
246
- <div class="ai-msg-user-body">${body}</div>
247
- ${
248
- contextLines.length > 0
249
- ? html`
250
- <div class="ai-msg-context-chips">
251
- ${contextLines.map((line) => html`<span class="ai-context-chip">${line}</span>`)}
252
- </div>
253
- `
254
- : nothing
255
- }
256
- </div>
257
- `;
258
- }
210
+ // ─── Tool calls ─────────────────────────────────────────────────────────────
259
211
 
260
212
  /** The tool whose chip is a question card rather than a chip. */
261
213
  const ASK_TOOL = "ask_user";
@@ -263,32 +215,6 @@ const ASK_TOOL = "ask_user";
263
215
  /** The tool whose chip grows a live progress line while it runs. */
264
216
  const IMPORT_TOOL = "import_site";
265
217
 
266
- /** Log lines drawn under a running import. The tail is what a reader wants. */
267
- /**
268
- * How close to the bottom counts as "following along".
269
- *
270
- * The same threshold and the same reasoning as the transcript's own scroller
271
- * (`panels/ai-panel.ts`): a reader who has scrolled up is reading something, and yanking them back
272
- * on every new line is the behaviour that makes a live log unreadable.
273
- */
274
- const IMPORT_STICK_THRESHOLD = 24;
275
-
276
- /**
277
- * Keep an import log pinned to its newest line, unless the reader has scrolled away from it.
278
- *
279
- * The log is a scroller now rather than a six-line tail, and a scroller that does not follow is a
280
- * box that shows the same six lines it started with while the interesting ones pile up below.
281
- */
282
- function stickToBottom(element: Element | undefined): void {
283
- if (!(element instanceof HTMLElement)) {
284
- return;
285
- }
286
- const distance = element.scrollHeight - element.scrollTop - element.clientHeight;
287
- if (distance <= IMPORT_STICK_THRESHOLD || element.scrollTop === 0) {
288
- element.scrollTop = element.scrollHeight;
289
- }
290
- }
291
-
292
218
  /** What `ask_user` was called with, as far as the arguments actually parse. */
293
219
  interface AskArgs {
294
220
  question: string;
@@ -366,14 +292,56 @@ export function toolOutcomeText(tc: ToolCallRecord): string {
366
292
  return (result.success ? result.summary : result.error) ?? "";
367
293
  }
368
294
 
295
+ /** How a question card reaches the store that resolves it, and an import its record. */
296
+ export interface AskHandlers {
297
+ /** The id of the question the loop is still waiting on, or null. */
298
+ pendingId?: string | null;
299
+ /** The live record for an `import_site` call, if this renderer's host has one. */
300
+ importRun?: (id: string) => ImportRunRecord | null;
301
+ }
302
+
303
+ /** The empty import fields every chip carries, so a `$switch` never reads an absent path. */
304
+ function noImport(): Pick<
305
+ ChatChipView,
306
+ | "hasPhase"
307
+ | "importCount"
308
+ | "importLog"
309
+ | "importMessage"
310
+ | "importOpen"
311
+ | "importPhase"
312
+ | "importProgress"
313
+ | "importSpinner"
314
+ | "importState"
315
+ > {
316
+ return {
317
+ hasPhase: false,
318
+ importCount: "",
319
+ importLog: [],
320
+ importMessage: "",
321
+ importOpen: false,
322
+ importPhase: "",
323
+ importProgress: "",
324
+ importSpinner: "none",
325
+ importState: "none",
326
+ };
327
+ }
328
+
369
329
  /**
370
330
  * A running import, under the chip that started it.
371
331
  *
372
- * An import takes minutes and says a line at a time. Rendering it here rather than in the modal it
373
- * used to live in is what stops a successful run destroying its own account of what it did: the
374
- * chip and the progress are one thing, joined by the tool-call id.
332
+ * An import takes minutes and says a line at a time. Rendering it under the chip rather than in the
333
+ * modal it used to live in is what stops a successful run destroying its own account of what it
334
+ * did: the chip and the progress are one thing, joined by the tool-call id.
335
+ *
336
+ * Projected for EVERY import chip, not only a running one. A finished run used to lose its whole
337
+ * account of itself at the instant it finished — the record was fetched only while the call was
338
+ * pending, so the log vanished on success. That is the failure the hand-off from the wizard to the
339
+ * assistant was made to fix, reproduced one layer in.
340
+ *
341
+ * @param {ImportRunRecord} record
342
+ * @returns {Partial<ChatChipView>}
375
343
  */
376
- function renderImportProgress(record: ImportRunRecord): TemplateResult {
344
+ function projectImport(record: ImportRunRecord): Partial<ChatChipView> {
377
345
  const running = record.status === "running";
378
346
  const determinate = record.total !== null && record.current !== null;
379
347
  const outcome =
@@ -384,357 +352,224 @@ function renderImportProgress(record: ImportRunRecord): TemplateResult {
384
352
  : record.status === "stopped"
385
353
  ? "Import stopped"
386
354
  : record.message;
387
- return html`
388
- <details class="ai-import-progress" ?open=${running}>
389
- <summary class="ai-import-head">
390
- ${
391
- running
392
- ? determinate
393
- ? html`<sp-progress-circle
394
- size="s"
395
- progress=${Math.round((record.current! / Math.max(record.total!, 1)) * 100)}
396
- ></sp-progress-circle>`
397
- : html`<sp-progress-circle indeterminate size="s"></sp-progress-circle>`
398
- : nothing
399
- }
400
- ${running ? html`<span class="ai-import-phase">${record.phase}</span>` : nothing}
401
- <span class="ai-import-message">${outcome}</span>
402
- <span class="ai-import-count">${record.log.length}</span>
403
- </summary>
404
- <div class="ai-import-log" ${ref(stickToBottom)}>
405
- ${repeat(
406
- record.log,
407
- (evt, index) => `${index}:${evt.phase}`,
408
- (evt) => html`
409
- <div class="ai-import-log-line">
410
- <span class="ai-import-phase">${evt.phase}</span>${evt.message}
411
- </div>
412
- `,
413
- )}
414
- </div>
415
- </details>
416
- `;
417
- }
418
-
419
- /** How a question card reaches the store that resolves it. */
420
- export interface AskHandlers {
421
- /** The id of the question the loop is still waiting on, or null. */
422
- pendingId?: string | null;
423
- onAnswer?: (text: string) => void;
424
- onSkip?: () => void;
425
- /** The live record for an `import_site` call, if this renderer's host has one. */
426
- importRun?: (id: string) => ImportRunRecord | null;
355
+ return {
356
+ hasPhase: running && record.phase !== "",
357
+ importCount: String(record.log.length),
358
+ importLog: record.log.map((evt, index) => ({
359
+ key: `${index}:${evt.phase}`,
360
+ message: evt.message,
361
+ phase: evt.phase,
362
+ })),
363
+ importMessage: outcome,
364
+ importOpen: running,
365
+ importPhase: record.phase,
366
+ importProgress: determinate
367
+ ? String(Math.round((record.current! / Math.max(record.total!, 1)) * 100))
368
+ : "",
369
+ importSpinner: running ? (determinate ? "progress" : "busy") : "none",
370
+ importState: "run",
371
+ };
427
372
  }
428
373
 
429
374
  /**
430
- * A question, as the last thing in the transcript, waiting on the reader.
375
+ * One tool call, as the transcript draws it.
431
376
  *
432
- * The card IS the tool chip rather than a row beside it: the question, its options and its answer
433
- * are all facts about one tool call, and rendering them anywhere else would give the transcript two
434
- * accounts of the same event that can disagree after a reload.
377
+ * The question card IS the tool chip rather than a row beside it: the question, its options and its
378
+ * answer are all facts about one tool call, and putting them anywhere else would give the
379
+ * transcript two accounts of the same event that can disagree after a reload.
435
380
  *
436
- * The options are a shortcut, never the whole answer — the composer is always live beneath, and its
437
- * placeholder says so. A question whose real answer is "neither, do this instead" must stay
438
- * answerable.
381
+ * @param {ToolCallRecord} tc
382
+ * @param {AskHandlers} handlers
383
+ * @returns {ChatChipView}
439
384
  */
440
- function renderAskCard(
441
- tc: ToolCallRecord,
442
- ask: AskArgs,
443
- outcome: ReturnType<typeof toolOutcome>,
444
- handlers: AskHandlers,
445
- ): TemplateResult {
385
+ export function projectChip(tc: ToolCallRecord, handlers: AskHandlers = {}): ChatChipView {
386
+ const outcome = toolOutcome(tc, tc.id === handlers.pendingId || Boolean(tc.result));
387
+ const text = toolOutcomeText(tc);
388
+ const run = tc.name === IMPORT_TOOL ? (handlers.importRun?.(tc.id) ?? null) : null;
389
+ const ask = tc.name === ASK_TOOL ? parseAsk(tc) : null;
446
390
  const answered = tc.result?.success
447
391
  ? ((tc.result.data as { answer?: string | null; skipped?: boolean } | undefined) ?? null)
448
392
  : null;
449
- return html`
450
- <div class="ai-ask" data-outcome=${outcome}>
451
- <div class="ai-ask-question">${ask.question}</div>
452
- ${ask.context ? html`<div class="ai-ask-context">${ask.context}</div>` : nothing}
453
- ${
454
- outcome === "pending"
455
- ? html`
456
- <div class="ai-ask-options">
457
- ${ask.options.map(
458
- (option) => html`
459
- <sp-button
460
- size="s"
461
- variant="secondary"
462
- treatment="outline"
463
- @click=${() => handlers.onAnswer?.(option)}
464
- >
465
- ${option}
466
- </sp-button>
467
- `,
468
- )}
469
- <sp-action-button
470
- size="s"
471
- quiet
472
- class="ai-ask-skip"
473
- title="Let the assistant decide"
474
- @click=${() => handlers.onSkip?.()}
475
- >
476
- You decide
477
- </sp-action-button>
478
- </div>
479
- `
480
- : nothing
481
- }
482
- ${
483
- outcome === "unanswered"
484
- ? html`<div class="ai-ask-outcome">
485
- This question was still open when the session was reloaded — reply below to carry on.
486
- </div>`
487
- : nothing
488
- }
489
- ${
490
- answered
491
- ? html`<div class="ai-ask-answer">
492
- ${answered.skipped ? "You decide" : answered.answer}
493
- </div>`
494
- : nothing
495
- }
496
- ${
497
- outcome === "failed"
498
- ? html`<div class="ai-ask-outcome">${toolOutcomeText(tc)}</div>`
499
- : nothing
500
- }
501
- </div>
502
- `;
503
- }
504
-
505
- function renderToolChips(
506
- toolCalls: ToolCallRecord[],
507
- handlers: AskHandlers = {},
508
- ): TemplateResult | typeof nothing {
509
- if (toolCalls.length === 0) {
510
- return nothing;
393
+ const base: ChatChipView = {
394
+ answer: answered ? (answered.skipped ? "You decide" : (answered.answer ?? "")) : "",
395
+ askState: "none",
396
+ context: ask?.context ?? "",
397
+ hasContext: Boolean(ask?.context),
398
+ hint: text || formatToolLabel(tc),
399
+ key: tc.id,
400
+ kind: ask ? "ask" : "chip",
401
+ label: formatToolLabel(tc),
402
+ mark: outcome === "ok" ? "✓" : "✗",
403
+ options: (ask?.options ?? []).map((option) => ({ key: option, label: option })),
404
+ outcome,
405
+ outcomeState: outcome === "pending" || outcome === "unanswered" ? "hidden" : "shown",
406
+ outcomeText: text,
407
+ question: ask?.question ?? "",
408
+ tool: tc.name,
409
+ ...noImport(),
410
+ ...(run ? projectImport(run) : {}),
411
+ };
412
+ if (ask) {
413
+ /* The options are a shortcut, never the whole answer — the composer is always live beneath, and
414
+ its placeholder says so. A question whose real answer is "neither, do this instead" must stay
415
+ answerable, which is why `pending` draws the options AND leaves the field alone. */
416
+ base.askState =
417
+ outcome === "pending"
418
+ ? "pending"
419
+ : outcome === "unanswered"
420
+ ? "unanswered"
421
+ : answered
422
+ ? "answered"
423
+ : outcome === "failed"
424
+ ? "failed"
425
+ : "none";
511
426
  }
512
- return html`
513
- <div class="ai-msg-tools">
514
- ${toolCalls.map((tc) => {
515
- const ask = tc.name === ASK_TOOL ? parseAsk(tc) : null;
516
- const outcome = toolOutcome(tc, tc.id === handlers.pendingId || Boolean(tc.result));
517
- if (ask) {
518
- return renderAskCard(tc, ask, outcome, handlers);
519
- }
520
- const text = toolOutcomeText(tc);
521
- /*
522
- * For EVERY import chip, not only a running one.
523
- *
524
- * A finished run used to lose its whole account of itself at the instant it finished — the
525
- * record was fetched only while the call was pending, so the log vanished on success. That
526
- * is the failure the hand-off from the wizard to the assistant was made to fix, reproduced
527
- * one layer in. It stays, collapsed, under the chip that produced it.
528
- */
529
- const run = tc.name === IMPORT_TOOL ? (handlers.importRun?.(tc.id) ?? null) : null;
530
- return html`
531
- <span class="ai-tool-chip" data-outcome=${outcome} title=${text || formatToolLabel(tc)}>
532
- <sp-icon-gears size="xs"></sp-icon-gears>
533
- <span class="ai-tool-chip-name">${formatToolLabel(tc)}</span>
534
- ${
535
- outcome === "pending" || outcome === "unanswered"
536
- ? nothing
537
- : html`<span class="ai-tool-chip-outcome"
538
- >${outcome === "ok" ? "✓" : "✗"} ${text}</span
539
- >`
540
- }
541
- </span>
542
- ${run ? renderImportProgress(run) : nothing}
543
- `;
544
- })}
545
- </div>
546
- `;
427
+ return base;
547
428
  }
548
429
 
430
+ // ─── Changed files ──────────────────────────────────────────────────────────
431
+
549
432
  /**
550
433
  * The turn's changed-files summary, with the two things it can honestly offer.
551
434
  *
552
- * Rendered only for a turn that changed something — "Changed 0 files" is noise, and a turn that
435
+ * Projected only for a turn that changed something — "Changed 0 files" is noise, and a turn that
553
436
  * only read is the common case. **Restore to here** is offered only when every recorded change went
554
437
  * through a transaction: a disk write has no history behind it, so a button that claimed to restore
555
438
  * one would be the same lie the model-facing caveat used to be.
439
+ *
440
+ * @param {string} messageId
441
+ * @returns {{
442
+ * changesState: string;
443
+ * changesSummary: string;
444
+ * canRestore: boolean;
445
+ * changes: ChatChangeView[];
446
+ * }}
556
447
  */
557
- function renderChangedFiles(
558
- msg: Message,
559
- onRestore?: (messageId: string) => void,
560
- ): TemplateResult | typeof nothing {
561
- const writes = writesForTurn(msg.id);
562
- if (writes.length === 0) {
563
- return nothing;
564
- }
565
- const summary = summarizeWrites(writes);
448
+ export function projectChanges(messageId: string): {
449
+ changesState: string;
450
+ changesSummary: string;
451
+ canRestore: boolean;
452
+ changes: ChatChangeView[];
453
+ } {
454
+ const writes = writesForTurn(messageId);
455
+ const summary = writes.length > 0 ? summarizeWrites(writes) : "";
566
456
  if (!summary) {
567
- return nothing;
457
+ return { canRestore: false, changes: [], changesState: "none", changesSummary: "" };
568
458
  }
569
- const restorable = writes.every((w) => !w.disk);
570
- return html`
571
- <details class="ai-msg-changes">
572
- <summary>
573
- ${summary}
574
- ${
575
- onRestore && restorable
576
- ? html`<sp-action-button
577
- size="xs"
578
- quiet
579
- title="Undo everything this turn changed"
580
- @click=${(e: Event) => {
581
- e.preventDefault();
582
- e.stopPropagation();
583
- onRestore(msg.id);
584
- }}
585
- >
586
- Restore to here
587
- </sp-action-button>`
588
- : nothing
589
- }
590
- </summary>
591
- <ul class="ai-msg-changes-list">
592
- ${writes.map(
593
- (w) => html`
594
- <li data-disk=${String(w.disk)} data-ok=${String(w.ok)}>
595
- <code>${w.path}</code>
596
- <span>${w.ok ? w.tool : `failed — ${w.error ?? w.tool}`}</span>
597
- ${w.disk ? html`<em>written to disk — undo cannot reach it</em>` : nothing}
598
- </li>
599
- `,
600
- )}
601
- </ul>
602
- </details>
603
- `;
459
+ return {
460
+ canRestore: writes.every((w) => !w.disk),
461
+ changes: writes.map((w, index) => ({
462
+ disk: w.disk,
463
+ key: `${index}:${w.path}`,
464
+ note: w.ok ? w.tool : `failed — ${w.error ?? w.tool}`,
465
+ ok: w.ok,
466
+ path: w.path,
467
+ })),
468
+ changesState: "list",
469
+ changesSummary: summary,
470
+ };
604
471
  }
605
472
 
606
- function renderAssistantMessage(
607
- msg: Message,
608
- onRestore?: (messageId: string) => void,
609
- ask: AskHandlers = {},
610
- ): TemplateResult | typeof nothing {
611
- const toolCalls = msg.toolCalls ?? [];
612
- if (!msg.content && toolCalls.length === 0) {
613
- return nothing;
614
- }
615
- return html`
616
- <div class="ai-msg-assistant">
617
- ${msg.content ? renderMarkdown(msg.id, msg.content) : nothing}
618
- ${renderToolChips(toolCalls, ask)} ${renderChangedFiles(msg, onRestore)}
619
- </div>
620
- `;
621
- }
473
+ // ─── Rows ───────────────────────────────────────────────────────────────────
622
474
 
623
- function renderToolMessage(msg: Message): TemplateResult | typeof nothing {
624
- // Show only failed tool results so the user knows why an edit didn't land
625
- // (ADR §11.3). Successful tool results stay hidden to reduce noise.
626
- const parsed = tryParseToolResult(msg.content);
627
- if (!parsed || parsed.success) {
628
- return nothing;
629
- }
630
- return html`<div class="ai-msg-tool-error">⚠️ ${parsed.error || "Tool call failed"}</div>`;
475
+ /** Every field a row carries, so a `$switch` case never reads an absent path. */
476
+ function emptyRow(key: string, kind: string): ChatRowView {
477
+ return {
478
+ body: "",
479
+ canRestore: false,
480
+ changes: [],
481
+ changesState: "none",
482
+ changesSummary: "",
483
+ chips: [],
484
+ contextChips: [],
485
+ hasChips: false,
486
+ hasContextChips: false,
487
+ key,
488
+ kind,
489
+ markdown: "",
490
+ };
631
491
  }
632
492
 
633
- function renderStreamingTail(msg: Message, ask: AskHandlers = {}): TemplateResult {
634
- if (!msg.content) {
635
- return html`
636
- <div class="ai-msg-typing">
637
- <span></span>
638
- <span></span>
639
- <span></span>
640
- </div>
641
- `;
642
- }
643
- return html`
644
- <div class="ai-msg-assistant">
645
- <span class="ai-msg-streaming">${msg.content}</span>
646
- ${renderToolChips(msg.toolCalls ?? [], ask)}
647
- </div>
648
- `;
493
+ /** The attached-context block on a user turn, back apart into chips. */
494
+ function contextChips(lines: readonly string[]): ChatContextChip[] {
495
+ return lines.map((line, index) => ({ key: `${index}:${line}`, label: line }));
649
496
  }
650
497
 
651
- // ─── Message list ───────────────────────────────────────────────────────────
652
-
653
498
  export interface MessageListOptions {
654
- messages: Message[];
655
- /** ChatState status — "streaming" renders the tail live. */
499
+ messages: readonly Message[];
500
+ /** ChatState status — "streaming" projects the tail live. */
656
501
  status: string;
657
- error: string | null;
658
- onScroll: (e: Event) => void;
659
- /** Ref to the scrolling element, for stick-to-bottom maintenance. */
660
- listRef: (el: Element | undefined) => void;
661
- /** Undo everything one turn changed. Offered only for turns whose changes are all transactional. */
662
- onRestore?: ((messageId: string) => void) | undefined;
663
- /**
664
- * The outstanding question and how to settle it.
665
- *
666
- * Passed down rather than read from the store here, because this module renders templates and
667
- * holds no state — the same reason the three header buttons are command records.
668
- */
502
+ /** The outstanding question and the import records, passed down rather than read here. */
669
503
  ask?: AskHandlers | undefined;
670
504
  }
671
505
 
672
- /** The scrollable message list — THE scroller of the chat view. */
673
- export function renderMessageList(opts: MessageListOptions): TemplateResult {
674
- const { messages, status } = opts;
506
+ /**
507
+ * The transcript, as rows.
508
+ *
509
+ * Keyed on `msg.id`, for two reasons a reader can see. The last assistant row swaps between the
510
+ * streaming tail and the finished message the moment a stream completes, so an unkeyed list would
511
+ * tear down and rebuild the longest node in the transcript every time one finishes. And an
512
+ * assistant row holds the reader's OWN open/closed state on its changed-files disclosure, which
513
+ * position-based reuse hands to a different message.
514
+ *
515
+ * A message that would draw nothing — an empty assistant turn with no tool calls, a successful tool
516
+ * result — is absent rather than an empty row, which is the same thing the lit template's `nothing`
517
+ * did and one fewer node for the runtime to reconcile.
518
+ *
519
+ * @param {MessageListOptions} opts
520
+ * @returns {ChatRowView[]}
521
+ */
522
+ export function projectRows(opts: MessageListOptions): ChatRowView[] {
675
523
  const ask = opts.ask ?? {};
676
- const lastIdx = messages.length - 1;
677
- return html`
678
- <div class="ai-chat-messages" ${ref(opts.listRef)} @scroll=${opts.onScroll}>
679
- ${
680
- messages.length === 0 && status !== "streaming"
681
- ? html`
682
- <div class="ai-chat-empty">
683
- Ask the assistant to build or edit this page — it can add sections, restyle
684
- elements, and wire up components.
685
- </div>
686
- `
687
- : nothing
688
- }
689
- ${repeat(
690
- messages,
691
- /* Keyed on msg.id, for two reasons a reader can see. The last assistant row swaps between
692
- renderStreamingTail and renderAssistantMessage the moment a stream completes, so an
693
- unkeyed list tears down and rebuilds the longest node in the transcript every time one
694
- finishes. And an assistant row holds the reader's OWN open/closed state on its
695
- ai-msg-changes details element, which position-based reuse hands to a different
696
- message. */
697
- (msg) => msg.id,
698
- (msg, i) => {
699
- if (msg.role === "user") {
700
- return renderUserMessage(msg);
701
- }
702
- if (msg.role === "tool") {
703
- return renderToolMessage(msg);
704
- }
705
- if (msg.role === "assistant") {
706
- if (status === "streaming" && i === lastIdx) {
707
- return renderStreamingTail(msg, ask);
708
- }
709
- return renderAssistantMessage(msg, opts.onRestore, ask);
710
- }
711
- return nothing;
712
- },
713
- )}
714
- ${
715
- status !== "streaming" && opts.error
716
- ? html`
717
- <div class="ai-msg-error">
718
- <div>${opts.error}</div>
719
- ${
720
- formatErrorAdvice(opts.error)
721
- ? html`<div class="ai-msg-error-advice">${formatErrorAdvice(opts.error)}</div>`
722
- : nothing
723
- }
724
- ${
725
- /* `assistant.retry`, not a closure. Its `enablement` reads `ctx.ai.configured`,
726
- so the one error this row cannot recover from — no provider connected, whose
727
- advice line above already says to add a key — draws the button disabled with
728
- that sentence rather than offering a send that will fail identically. */
729
- commandButton("assistant.retry", {
730
- className: "ai-msg-retry",
731
- content: "Retry",
732
- })
733
- }
734
- </div>
735
- `
736
- : nothing
524
+ const lastIdx = opts.messages.length - 1;
525
+ const rows: ChatRowView[] = [];
526
+ for (const [i, msg] of opts.messages.entries()) {
527
+ if (msg.role === "user") {
528
+ const { body, contextLines } = splitAttachedContext(msg.content);
529
+ rows.push({
530
+ ...emptyRow(msg.id, "user"),
531
+ body,
532
+ contextChips: contextChips(contextLines),
533
+ hasContextChips: contextLines.length > 0,
534
+ });
535
+ continue;
536
+ }
537
+ if (msg.role === "tool") {
538
+ // Show only failed tool results so the user knows why an edit didn't land (ADR §11.3).
539
+ // Successful tool results stay hidden to reduce noise.
540
+ const parsed = tryParseToolResult(msg.content);
541
+ if (parsed && !parsed.success) {
542
+ rows.push({
543
+ ...emptyRow(msg.id, "tool-error"),
544
+ body: `⚠️ ${parsed.error || "Tool call failed"}`,
545
+ });
737
546
  }
738
- </div>
739
- `;
547
+ continue;
548
+ }
549
+ if (msg.role !== "assistant") {
550
+ continue;
551
+ }
552
+ const toolCalls = msg.toolCalls ?? [];
553
+ const chips = toolCalls.map((tc) => projectChip(tc, ask));
554
+ if (opts.status === "streaming" && i === lastIdx) {
555
+ rows.push({
556
+ ...emptyRow(msg.id, msg.content ? "streaming" : "typing"),
557
+ body: msg.content,
558
+ chips,
559
+ hasChips: chips.length > 0,
560
+ });
561
+ continue;
562
+ }
563
+ if (!msg.content && toolCalls.length === 0) {
564
+ continue;
565
+ }
566
+ rows.push({
567
+ ...emptyRow(msg.id, "assistant"),
568
+ chips,
569
+ hasChips: chips.length > 0,
570
+ markdown: msg.content ? renderMarkdown(msg.id, msg.content) : "",
571
+ ...projectChanges(msg.id),
572
+ });
573
+ }
574
+ return rows;
740
575
  }