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