@jxsuite/studio 4.1.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 (607) 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-n6tfwnda.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-wcnt3r9g.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-vx9bhp1d.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-msd847zn.js → studio-25hkdrkw.js} +357 -86
  62. package/dist/chunks/studio-26d7myd1.js +258 -0
  63. package/dist/chunks/{studio-4r1pv6zf.js → studio-38emantv.js} +22 -5
  64. package/dist/chunks/studio-3gxv60tn.js +422 -0
  65. package/dist/chunks/{studio-fbf92ckf.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-1wagb550.js → studio-5523bpzt.js} +11 -11
  69. package/dist/chunks/{studio-x4qkdat5.js → studio-57mf66w2.js} +1749 -2481
  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-ahn3pdgx.js → studio-79y5sz51.js} +52 -38
  78. package/dist/chunks/{studio-gcye7qka.js → studio-7e11jpa9.js} +21 -21
  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-x813zp48.js → studio-arfpe389.js} +30 -30
  88. package/dist/chunks/{studio-trxrabqf.js → studio-az3zd49t.js} +475 -283
  89. package/dist/chunks/studio-cpvb2ed1.js +1171 -0
  90. package/dist/chunks/{studio-f3wqjtk5.js → studio-d0300636.js} +5076 -18
  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-3bqpr0zh.js → studio-em9nw8eh.js} +299 -110
  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-dm19q43r.js → studio-hhvs4sdg.js} +10 -10
  100. package/dist/chunks/{studio-q9jd1a60.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-2b946z26.js → studio-rkstz940.js} +3658 -6134
  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-jkftxyvz.js → studio-s6z853aq.js} +5858 -2152
  113. package/dist/chunks/{studio-6yw4y3my.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-qjy2z9bv.js → studio-vz68yn9e.js} +32 -137
  118. package/dist/chunks/studio-wqpngpsk.js +174 -0
  119. package/dist/chunks/{studio-b55y6ewx.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 +1780 -667
  135. package/dist/studio.js +52819 -36480
  136. package/dist/workers/editor.worker.js +1040 -1085
  137. package/dist/workers/json.worker.js +1946 -1991
  138. package/dist/workers/ts.worker.js +4960 -5005
  139. package/index.html +1 -0
  140. package/package.json +18 -52
  141. package/src/about/about-modal.ts +72 -123
  142. package/src/browse/library-commands.ts +20 -49
  143. package/src/browse/library-layouts.ts +87 -218
  144. package/src/browse/library-pane.ts +406 -438
  145. package/src/canvas/canvas-helpers.ts +0 -49
  146. package/src/canvas/canvas-live-render.ts +25 -2
  147. package/src/canvas/canvas-render.ts +666 -231
  148. package/src/canvas/canvas-surface.ts +0 -1
  149. package/src/canvas/canvas-utils.ts +310 -117
  150. package/src/canvas/dialog-path.ts +76 -0
  151. package/src/canvas/dialog-state.ts +67 -0
  152. package/src/canvas/diff-marks.ts +252 -0
  153. package/src/canvas/diff-toolbar.ts +349 -0
  154. package/src/canvas/diff-view.ts +123 -0
  155. package/src/canvas/edit-width-drag.ts +187 -0
  156. package/src/canvas/edit-width.ts +150 -0
  157. package/src/canvas/iframe-entry.ts +125 -8
  158. package/src/canvas/iframe-host.ts +271 -15
  159. package/src/canvas/iframe-interaction.ts +133 -13
  160. package/src/canvas/iframe-patch.ts +7 -1
  161. package/src/canvas/iframe-position.ts +5 -6
  162. package/src/canvas/iframe-protocol.ts +106 -5
  163. package/src/canvas/iframe-render.ts +373 -5
  164. package/src/canvas/iframe-slash.ts +2 -2
  165. package/src/canvas/iframe-subtree.ts +5 -8
  166. package/src/canvas/path-mapping.ts +25 -3
  167. package/src/canvas/popover-path.ts +106 -0
  168. package/src/canvas/popover-state.ts +155 -0
  169. package/src/canvas/surface-registry.ts +21 -10
  170. package/src/collab/presence-chips.ts +51 -73
  171. package/src/commands/app-commands.ts +10 -0
  172. package/src/commands/budget.ts +12 -0
  173. package/src/commands/command-args.ts +369 -6
  174. package/src/commands/defaults.ts +44 -14
  175. package/src/commands/live-context.ts +11 -5
  176. package/src/commands/reference.ts +12 -2
  177. package/src/commands/registry.ts +165 -8
  178. package/src/content/entry-commands.ts +16 -23
  179. package/src/content/entry-editor.ts +144 -101
  180. package/src/editor/canvas-slash-bridge.ts +2 -2
  181. package/src/editor/context-menu.ts +49 -200
  182. package/src/editor/convert-to-component.ts +42 -82
  183. package/src/editor/convert-to-repeater.ts +132 -173
  184. package/src/editor/inline-edit.ts +11 -0
  185. package/src/editor/shortcuts.ts +65 -26
  186. package/src/editor/slash-menu.ts +150 -157
  187. package/src/files/file-ops.ts +117 -11
  188. package/src/files/files.ts +943 -660
  189. package/src/files/json-layout.ts +474 -0
  190. package/src/files/serialize-document.ts +8 -1
  191. package/src/format/convert-file.ts +16 -10
  192. package/src/format/format-host.ts +50 -1
  193. package/src/github/github-auth.ts +71 -29
  194. package/src/github/github-publish.ts +57 -65
  195. package/src/grid/cell-editors.ts +46 -4
  196. package/src/grid/cell-popovers.ts +65 -62
  197. package/src/grid/grid-open.ts +86 -64
  198. package/src/grid/grid-panel.ts +390 -469
  199. package/src/grid/redirects-grid.ts +40 -44
  200. package/src/hosting/layout.ts +2 -0
  201. package/src/i18n/i18n-commands.ts +127 -62
  202. package/src/media/media-pane.ts +83 -133
  203. package/src/new-project/add-repo-modal.ts +125 -176
  204. package/src/new-project/import-tab.ts +181 -252
  205. package/src/new-project/location-fields.ts +93 -160
  206. package/src/new-project/new-project-modal.ts +376 -367
  207. package/src/packages/jxsuite-update.ts +76 -18
  208. package/src/panels/activity-panel.ts +137 -104
  209. package/src/panels/ai-chat/chat-markdown.ts +15 -13
  210. package/src/panels/ai-chat/chat-view.ts +307 -472
  211. package/src/panels/ai-chat/composer.ts +175 -163
  212. package/src/panels/ai-chat/sessions-view.ts +31 -62
  213. package/src/panels/ai-panel.ts +210 -185
  214. package/src/panels/block-action-bar.ts +437 -643
  215. package/src/panels/bottom-dock.ts +112 -47
  216. package/src/panels/chat-panel.ts +19 -14
  217. package/src/panels/data-explorer.ts +141 -102
  218. package/src/panels/data-grid.ts +183 -150
  219. package/src/panels/dnd.ts +75 -20
  220. package/src/panels/editors.ts +26 -19
  221. package/src/panels/elements-panel.ts +243 -186
  222. package/src/panels/empty-state.ts +26 -42
  223. package/src/panels/events-panel.ts +950 -729
  224. package/src/panels/formula-workspace.ts +434 -207
  225. package/src/panels/frontmatter-fields.ts +185 -153
  226. package/src/panels/frontmatter-panel.ts +411 -187
  227. package/src/panels/git-diff-open.ts +148 -0
  228. package/src/panels/git-panel.ts +679 -605
  229. package/src/panels/head-panel.ts +617 -435
  230. package/src/panels/i18n-panel.ts +153 -133
  231. package/src/panels/imports-panel.ts +388 -414
  232. package/src/panels/jump-bar.ts +174 -147
  233. package/src/panels/layers-panel.ts +785 -671
  234. package/src/panels/left-panel.ts +166 -94
  235. package/src/panels/navigator-panels.ts +1 -1
  236. package/src/panels/pane-context.ts +529 -677
  237. package/src/panels/pane-grid.ts +265 -241
  238. package/src/panels/panel-registry.ts +15 -4
  239. package/src/panels/problems-panel.ts +177 -99
  240. package/src/panels/properties-panel.ts +1525 -966
  241. package/src/panels/provenance.ts +31 -113
  242. package/src/panels/quick-search.ts +217 -172
  243. package/src/panels/right-panel.ts +135 -169
  244. package/src/panels/seo-modal.ts +346 -167
  245. package/src/panels/settings-menu.ts +109 -646
  246. package/src/panels/settings-pane.ts +102 -86
  247. package/src/panels/signals-panel.ts +1318 -1172
  248. package/src/panels/statement-editor.ts +556 -428
  249. package/src/panels/style-panel.ts +1367 -630
  250. package/src/panels/style-utils.ts +36 -32
  251. package/src/panels/stylebook-doc.ts +30 -73
  252. package/src/panels/stylebook-layers-panel.ts +202 -109
  253. package/src/panels/stylebook-panel.ts +124 -88
  254. package/src/panels/tab-strip.ts +385 -287
  255. package/src/platforms/cloud.ts +106 -16
  256. package/src/platforms/devserver.ts +13 -0
  257. package/src/publish/deploy-checklist.ts +124 -78
  258. package/src/publish/publish-commands.ts +4 -12
  259. package/src/publish/publish-panel.ts +134 -328
  260. package/src/services/a11y-report.ts +100 -43
  261. package/src/services/ai-command-tools.ts +468 -0
  262. package/src/services/ai-session-store.ts +6 -0
  263. package/src/services/ai-system-prompt.ts +110 -18
  264. package/src/services/ai-tools.ts +28 -143
  265. package/src/services/ai-write-report.ts +174 -0
  266. package/src/services/document-assistant.ts +50 -2
  267. package/src/services/idle.ts +16 -14
  268. package/src/services/live-surfaces.ts +237 -0
  269. package/src/services/model-uri.ts +75 -0
  270. package/src/services/notify.ts +63 -6
  271. package/src/services/popover-report.ts +325 -0
  272. package/src/services/surface-registry.ts +56 -0
  273. package/src/services/tool-executor.ts +6 -0
  274. package/src/settings/contexts-section.ts +301 -299
  275. package/src/settings/contributed-section.ts +361 -222
  276. package/src/settings/css-vars-editor.ts +320 -325
  277. package/src/settings/defs-editor.ts +572 -535
  278. package/src/settings/dependencies-editor.ts +79 -107
  279. package/src/settings/extension-commands.ts +379 -0
  280. package/src/settings/extension-rows.ts +113 -0
  281. package/src/settings/extensions-section.ts +287 -0
  282. package/src/settings/general-settings.ts +130 -181
  283. package/src/settings/head-editor.ts +10 -305
  284. package/src/settings/locales-section.ts +146 -196
  285. package/src/settings/preferences-accounts.ts +1 -1
  286. package/src/settings/preferences-dialog.ts +350 -330
  287. package/src/settings/project-sections.ts +108 -181
  288. package/src/settings/schema-field-ui.ts +11 -399
  289. package/src/settings/section-registry.ts +8 -1
  290. package/src/settings/settings-document.ts +31 -29
  291. package/src/shell/tree.ts +42 -87
  292. package/src/shell.ts +48 -36
  293. package/src/studio.ts +90 -67
  294. package/src/style/token-ref.ts +0 -39
  295. package/src/surfaces/about.json +263 -0
  296. package/src/surfaces/about.ts +163 -0
  297. package/src/surfaces/add-repo.json +486 -0
  298. package/src/surfaces/add-repo.ts +236 -0
  299. package/src/surfaces/ai-chat.json +2185 -0
  300. package/src/surfaces/ai-chat.ts +549 -0
  301. package/src/surfaces/ai-credentials-form.json +332 -0
  302. package/src/surfaces/ai-credentials-form.ts +151 -0
  303. package/src/surfaces/ai-managed-connect.json +108 -0
  304. package/src/surfaces/ai-managed-connect.ts +123 -0
  305. package/src/surfaces/ai-model-picker.json +114 -0
  306. package/src/surfaces/ai-model-picker.ts +139 -0
  307. package/src/surfaces/block-action-bar.json +640 -0
  308. package/src/surfaces/block-action-bar.ts +368 -0
  309. package/src/surfaces/bottom-dock.json +154 -0
  310. package/src/surfaces/bottom-dock.ts +125 -0
  311. package/src/surfaces/canvas-stage.json +444 -0
  312. package/src/surfaces/canvas-stage.ts +253 -0
  313. package/src/surfaces/cf-account-picker.json +300 -0
  314. package/src/surfaces/cf-account-picker.ts +214 -0
  315. package/src/surfaces/commandbar.json +984 -0
  316. package/src/{panels/toolbar.ts → surfaces/commandbar.ts} +414 -389
  317. package/src/surfaces/convert-repeater.json +309 -0
  318. package/src/surfaces/convert-repeater.ts +276 -0
  319. package/src/surfaces/data-actions.json +157 -0
  320. package/src/surfaces/data-actions.ts +115 -0
  321. package/src/surfaces/dialog.json +289 -0
  322. package/src/surfaces/dialog.ts +343 -0
  323. package/src/surfaces/diff-toolbar.json +266 -0
  324. package/src/surfaces/diff-toolbar.ts +139 -0
  325. package/src/surfaces/doc-header.json +739 -0
  326. package/src/surfaces/doc-header.ts +223 -0
  327. package/src/surfaces/empty-state.json +152 -0
  328. package/src/surfaces/empty-state.ts +173 -0
  329. package/src/surfaces/entry-editor.json +281 -0
  330. package/src/surfaces/entry-editor.ts +191 -0
  331. package/src/surfaces/expression-editor.json +977 -0
  332. package/src/surfaces/expression-editor.ts +266 -0
  333. package/src/surfaces/files-panel.json +592 -0
  334. package/src/surfaces/files-panel.ts +301 -0
  335. package/src/surfaces/formula-palette.json +364 -0
  336. package/src/surfaces/formula-palette.ts +340 -0
  337. package/src/surfaces/git-panel.json +1822 -0
  338. package/src/surfaces/git-panel.ts +272 -0
  339. package/src/surfaces/github-auth.json +112 -0
  340. package/src/surfaces/github-auth.ts +139 -0
  341. package/src/surfaces/github-publish.json +206 -0
  342. package/src/surfaces/github-publish.ts +169 -0
  343. package/src/surfaces/grid-cell.json +218 -0
  344. package/src/surfaces/grid-cell.ts +120 -0
  345. package/src/surfaces/grid-open.json +147 -0
  346. package/src/surfaces/grid-open.ts +87 -0
  347. package/src/surfaces/grid-panel.json +706 -0
  348. package/src/surfaces/grid-panel.ts +206 -0
  349. package/src/surfaces/grid-replace.json +179 -0
  350. package/src/surfaces/grid-replace.ts +89 -0
  351. package/src/surfaces/grid-views.json +446 -0
  352. package/src/surfaces/grid-views.ts +172 -0
  353. package/src/surfaces/inspector-dock.json +187 -0
  354. package/src/surfaces/inspector-dock.ts +161 -0
  355. package/src/surfaces/jump-bar.json +243 -0
  356. package/src/surfaces/jump-bar.ts +129 -0
  357. package/src/surfaces/jxsuite-update.json +146 -0
  358. package/src/surfaces/jxsuite-update.ts +149 -0
  359. package/src/surfaces/library-pane.json +1509 -0
  360. package/src/surfaces/library-pane.ts +363 -0
  361. package/src/surfaces/logic-panel.json +1949 -0
  362. package/src/surfaces/logic-panel.ts +369 -0
  363. package/src/surfaces/logic-workspace.json +685 -0
  364. package/src/surfaces/logic-workspace.ts +238 -0
  365. package/src/surfaces/media-browser.json +303 -0
  366. package/src/surfaces/media-browser.ts +145 -0
  367. package/src/surfaces/media-field.json +163 -0
  368. package/src/surfaces/media-field.ts +116 -0
  369. package/src/surfaces/media-pane.json +513 -0
  370. package/src/surfaces/media-pane.ts +197 -0
  371. package/src/surfaces/menu.json +232 -0
  372. package/src/surfaces/menu.ts +375 -0
  373. package/src/surfaces/navigator-dock.json +113 -0
  374. package/src/surfaces/navigator-dock.ts +130 -0
  375. package/src/surfaces/new-project.json +1654 -0
  376. package/src/surfaces/new-project.ts +411 -0
  377. package/src/surfaces/palette.json +535 -0
  378. package/src/surfaces/pane-context.json +1497 -0
  379. package/src/surfaces/pane-context.ts +513 -0
  380. package/src/surfaces/pane-grid.json +188 -0
  381. package/src/surfaces/pane-grid.ts +250 -0
  382. package/src/surfaces/panel-activity.json +563 -0
  383. package/src/surfaces/panel-activity.ts +187 -0
  384. package/src/surfaces/panel-data.json +141 -0
  385. package/src/surfaces/panel-data.ts +156 -0
  386. package/src/surfaces/panel-deploy-checklist.json +283 -0
  387. package/src/surfaces/panel-deploy-checklist.ts +175 -0
  388. package/src/surfaces/panel-elements.json +367 -0
  389. package/src/surfaces/panel-elements.ts +175 -0
  390. package/src/surfaces/panel-i18n.json +569 -0
  391. package/src/surfaces/panel-i18n.ts +212 -0
  392. package/src/surfaces/panel-imports.json +780 -0
  393. package/src/surfaces/panel-imports.ts +209 -0
  394. package/src/surfaces/panel-outline.json +734 -0
  395. package/src/surfaces/panel-outline.ts +326 -0
  396. package/src/surfaces/panel-page.json +1017 -0
  397. package/src/surfaces/panel-page.ts +240 -0
  398. package/src/surfaces/panel-problems.json +497 -0
  399. package/src/surfaces/panel-problems.ts +157 -0
  400. package/src/surfaces/panel-signals.json +1973 -0
  401. package/src/surfaces/panel-signals.ts +436 -0
  402. package/src/surfaces/panel-stylebook-layers.json +317 -0
  403. package/src/surfaces/panel-stylebook-layers.ts +155 -0
  404. package/src/surfaces/preferences.json +850 -0
  405. package/src/surfaces/preferences.ts +332 -0
  406. package/src/surfaces/progress-modal.json +110 -0
  407. package/src/surfaces/progress-modal.ts +150 -0
  408. package/src/surfaces/properties-panel.json +1578 -0
  409. package/src/surfaces/properties-panel.ts +337 -0
  410. package/src/surfaces/publish.json +945 -0
  411. package/src/surfaces/publish.ts +392 -0
  412. package/src/surfaces/push-plan.json +110 -0
  413. package/src/surfaces/push-plan.ts +87 -0
  414. package/src/surfaces/rail.json +255 -0
  415. package/src/surfaces/rail.ts +206 -0
  416. package/src/surfaces/reference-field.json +191 -0
  417. package/src/surfaces/reference-field.ts +168 -0
  418. package/src/surfaces/schema-builder.json +1011 -0
  419. package/src/surfaces/schema-builder.ts +181 -0
  420. package/src/surfaces/schema-form.json +1013 -0
  421. package/src/surfaces/schema-form.ts +287 -0
  422. package/src/surfaces/secret-field.json +60 -0
  423. package/src/surfaces/secret-field.ts +146 -0
  424. package/src/surfaces/seo.json +932 -0
  425. package/src/surfaces/seo.ts +334 -0
  426. package/src/surfaces/settings-contexts.json +538 -0
  427. package/src/surfaces/settings-contexts.ts +196 -0
  428. package/src/surfaces/settings-contributed.json +489 -0
  429. package/src/surfaces/settings-contributed.ts +199 -0
  430. package/src/surfaces/settings-css-vars.json +820 -0
  431. package/src/surfaces/settings-css-vars.ts +199 -0
  432. package/src/surfaces/settings-defs.json +1363 -0
  433. package/src/surfaces/settings-defs.ts +232 -0
  434. package/src/surfaces/settings-deploy.json +108 -0
  435. package/src/surfaces/settings-deploy.ts +135 -0
  436. package/src/surfaces/settings-extensions.json +486 -0
  437. package/src/surfaces/settings-extensions.ts +201 -0
  438. package/src/surfaces/settings-head.json +779 -0
  439. package/src/surfaces/settings-head.ts +397 -0
  440. package/src/surfaces/settings-locales.json +420 -0
  441. package/src/surfaces/settings-locales.ts +137 -0
  442. package/src/surfaces/settings-overview.json +423 -0
  443. package/src/surfaces/settings-overview.ts +218 -0
  444. package/src/surfaces/settings-packages.json +549 -0
  445. package/src/surfaces/settings-packages.ts +158 -0
  446. package/src/surfaces/settings-pane.json +147 -0
  447. package/src/surfaces/settings-pane.ts +150 -0
  448. package/src/surfaces/settings-rawjson.json +104 -0
  449. package/src/surfaces/settings-rawjson.ts +107 -0
  450. package/src/surfaces/shell.json +99 -0
  451. package/src/surfaces/shell.ts +30 -0
  452. package/src/surfaces/slash-menu.json +180 -0
  453. package/src/surfaces/slash-menu.ts +272 -0
  454. package/src/surfaces/statements.json +607 -0
  455. package/src/surfaces/statements.ts +238 -0
  456. package/src/surfaces/statusbar.json +94 -0
  457. package/src/{panels → surfaces}/statusbar.ts +163 -90
  458. package/src/surfaces/style-panel.json +1643 -0
  459. package/src/surfaces/style-panel.ts +338 -0
  460. package/src/surfaces/stylebook-chrome.json +122 -0
  461. package/src/surfaces/stylebook-chrome.ts +122 -0
  462. package/src/surfaces/tab-strip.json +448 -0
  463. package/src/surfaces/tab-strip.ts +200 -0
  464. package/src/surfaces/target-line.json +419 -0
  465. package/src/surfaces/target-line.ts +349 -0
  466. package/src/surfaces/toasts.json +189 -0
  467. package/src/surfaces/welcome.json +571 -0
  468. package/src/surfaces/welcome.ts +338 -0
  469. package/src/tabs/project-config.ts +71 -15
  470. package/src/tabs/tab.ts +52 -1
  471. package/src/types.ts +22 -0
  472. package/src/ui/ai-credentials-form.ts +104 -100
  473. package/src/ui/ai-managed-connect.ts +54 -36
  474. package/src/ui/ai-model-picker.ts +75 -47
  475. package/src/ui/cf-account-picker.ts +76 -141
  476. package/src/ui/color-selector.ts +80 -301
  477. package/src/ui/dynamic-slot.ts +43 -283
  478. package/src/ui/expression-editor.ts +797 -576
  479. package/src/ui/field-input.ts +8 -181
  480. package/src/ui/form-controls.ts +838 -366
  481. package/src/ui/formula-chips.ts +80 -85
  482. package/src/ui/kit.ts +33 -0
  483. package/src/ui/layers.ts +293 -593
  484. package/src/ui/media-picker.ts +203 -284
  485. package/src/ui/panel-resize.ts +251 -64
  486. package/src/ui/popover-surface.ts +153 -0
  487. package/src/ui/progress-modal.ts +22 -60
  488. package/src/ui/regions.ts +13 -7
  489. package/src/ui/schema-form.ts +906 -440
  490. package/src/ui/surface.ts +122 -0
  491. package/src/ui/unit-selector.ts +15 -131
  492. package/src/ui/value-source.ts +14 -0
  493. package/src/ui/virtual-window.ts +88 -9
  494. package/src/utils/canvas-media.ts +72 -0
  495. package/src/utils/edit-display.ts +7 -2
  496. package/src/utils/element-selectors.ts +69 -0
  497. package/src/utils/link-target.ts +2 -2
  498. package/src/utils/studio-utils.ts +0 -10
  499. package/src/view.ts +15 -0
  500. package/src/workspace/pane-derive.ts +36 -14
  501. package/src/workspace/workspace.ts +15 -6
  502. package/styles/canvas.css +72 -467
  503. package/styles/forced-colors.css +48 -82
  504. package/styles/forced-colors.json +41 -0
  505. package/styles/inspector.css +2 -1301
  506. package/styles/overlays.css +96 -2823
  507. package/styles/panels.css +6 -1423
  508. package/styles/shell-frame.css +272 -0
  509. package/styles/shell-frame.json +227 -0
  510. package/styles/shell.css +0 -1568
  511. package/styles/tokens.css +65 -136
  512. package/styles/tokens.json +84 -0
  513. package/dist/chunks/Overlay.dev-etqjwyf9.js +0 -23
  514. package/dist/chunks/adapter-jkcyhgtr.js +0 -14
  515. package/dist/chunks/cf-account-picker-en86v7x7.js +0 -163
  516. package/dist/chunks/cf-settings-45fgyx8y.js +0 -21
  517. package/dist/chunks/components-zmx10t62.js +0 -21
  518. package/dist/chunks/content-source-48xg86wj.js +0 -49
  519. package/dist/chunks/convert-to-repeater-43tfn7cs.js +0 -268
  520. package/dist/chunks/editor-cptq5jp1.js +0 -37
  521. package/dist/chunks/entry-commands-e63f1xc0.js +0 -46
  522. package/dist/chunks/extension-sections-txv08xxg.js +0 -311
  523. package/dist/chunks/file-ops-wbh4hhsx.js +0 -48
  524. package/dist/chunks/files-3kfkwhk4.js +0 -93
  525. package/dist/chunks/flow-qbfgrpeh.js +0 -634
  526. package/dist/chunks/focus-trap.esm-9t2drp0g.js +0 -1025
  527. package/dist/chunks/focus-visible-6eg70z18.js +0 -167
  528. package/dist/chunks/format-host-w16q81tf.js +0 -43
  529. package/dist/chunks/fs-events-hrrg328g.js +0 -21
  530. package/dist/chunks/git-panel-532whr1y.js +0 -59
  531. package/dist/chunks/index-zqxmqxmh.js +0 -109
  532. package/dist/chunks/jx-validate-8er6gbxy.js +0 -19
  533. package/dist/chunks/layers-panel-07hm7g5j.js +0 -67
  534. package/dist/chunks/layers-q75tj2mf.js +0 -45
  535. package/dist/chunks/lit-html-j055mkh8.js +0 -22
  536. package/dist/chunks/monaco-buffer-dqw0xxpx.js +0 -25
  537. package/dist/chunks/notify-yjm79n4s.js +0 -30
  538. package/dist/chunks/parse-24d0gay3.js +0 -19
  539. package/dist/chunks/platform-ba1j1gbh.js +0 -16
  540. package/dist/chunks/publish-panel-v9st96ja.js +0 -29
  541. package/dist/chunks/quick-search-zccjp6gv.js +0 -66
  542. package/dist/chunks/references-3qx8fkyp.js +0 -29
  543. package/dist/chunks/right-panel-vm3b6kwy.js +0 -46
  544. package/dist/chunks/section-registry-sx1ykvkw.js +0 -30
  545. package/dist/chunks/sp-overlay.dev-qgvhnxf4.js +0 -18
  546. package/dist/chunks/sp-popover.dev-n2etmqdp.js +0 -9
  547. package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +0 -15
  548. package/dist/chunks/sp-tooltip.dev-4sastdnw.js +0 -15
  549. package/dist/chunks/sp-tray.dev-tkbscm06.js +0 -199
  550. package/dist/chunks/studio-0a5qrh01.js +0 -3
  551. package/dist/chunks/studio-0bq6eh7v.js +0 -12
  552. package/dist/chunks/studio-18j3bmcz.js +0 -14
  553. package/dist/chunks/studio-1ngbejft.js +0 -181
  554. package/dist/chunks/studio-4b5z1x43.js +0 -424
  555. package/dist/chunks/studio-4n2b5gzb.js +0 -357
  556. package/dist/chunks/studio-4rzyrtjz.js +0 -426
  557. package/dist/chunks/studio-5fx3g9jg.js +0 -4325
  558. package/dist/chunks/studio-5myknm0a.js +0 -1903
  559. package/dist/chunks/studio-7afd21nw.js +0 -67
  560. package/dist/chunks/studio-7nte2hj4.js +0 -613
  561. package/dist/chunks/studio-8jpzqk5s.js +0 -128
  562. package/dist/chunks/studio-8q517dfr.js +0 -24
  563. package/dist/chunks/studio-950k0dx6.js +0 -6939
  564. package/dist/chunks/studio-9n4mrmnb.js +0 -184
  565. package/dist/chunks/studio-a0tswy0w.js +0 -58
  566. package/dist/chunks/studio-ahnzd9rg.js +0 -23
  567. package/dist/chunks/studio-cmb03bg5.js +0 -13890
  568. package/dist/chunks/studio-d1ks410h.js +0 -175
  569. package/dist/chunks/studio-ef49pnzx.js +0 -387
  570. package/dist/chunks/studio-eqq1w638.js +0 -113
  571. package/dist/chunks/studio-ew0djmhg.js +0 -341
  572. package/dist/chunks/studio-gfgb8pe6.js +0 -1652
  573. package/dist/chunks/studio-h4rxscfc.js +0 -10
  574. package/dist/chunks/studio-jc994dnr.js +0 -1066
  575. package/dist/chunks/studio-kyh9avvv.js +0 -28
  576. package/dist/chunks/studio-m3hwn7y1.js +0 -120
  577. package/dist/chunks/studio-mtfnaqs9.js +0 -659
  578. package/dist/chunks/studio-n6j9t10q.js +0 -318
  579. package/dist/chunks/studio-n8b9wqwy.js +0 -291
  580. package/dist/chunks/studio-qcy05dp8.js +0 -1743
  581. package/dist/chunks/studio-qq6jym04.js +0 -53
  582. package/dist/chunks/studio-rdx8jcxy.js +0 -83
  583. package/dist/chunks/studio-rn9ta2mz.js +0 -111
  584. package/dist/chunks/studio-t7qb9db8.js +0 -665
  585. package/dist/chunks/studio-tmc5emrw.js +0 -8
  586. package/dist/chunks/studio-z6nw2ek2.js +0 -18
  587. package/dist/chunks/studio-zczps70q.js +0 -378
  588. package/dist/chunks/studio-zqazsct2.js +0 -1823
  589. package/dist/chunks/workspace-fjx52ryv.js +0 -89
  590. package/src/panels/activity-bar.ts +0 -251
  591. package/src/panels/panel-scheduler.ts +0 -219
  592. package/src/panels/style-inputs.ts +0 -237
  593. package/src/panels/target-line.ts +0 -274
  594. package/src/panels/welcome-screen.ts +0 -346
  595. package/src/ui/button-group.ts +0 -102
  596. package/src/ui/choice-field.ts +0 -72
  597. package/src/ui/field-row.ts +0 -249
  598. package/src/ui/formula-palette.ts +0 -213
  599. package/src/ui/icons.ts +0 -52
  600. package/src/ui/jx-theme.ts +0 -130
  601. package/src/ui/spectrum.ts +0 -340
  602. package/src/ui/value-selector.ts +0 -174
  603. package/src/ui/widgets.ts +0 -172
  604. /package/dist/chunks/{javascript-s56a8dpk.css → javascript-4abzjcy9.css} +0 -0
  605. /package/dist/chunks/{jsonMode-zr9sw0c7.css → jsonMode-vvmpmk34.css} +0 -0
  606. /package/dist/chunks/{monaco-setup-pav68602.css → monaco-setup-8vbyqnhr.css} +0 -0
  607. /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
  }