@cardstack/boxel-cli 0.11.0-unstable.1 → 0.11.0-unstable.3

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 (177) hide show
  1. package/bundled-realms/pretui/components/agent-mark.gts +98 -0
  2. package/bundled-realms/pretui/components/agent-question.gts +340 -37
  3. package/bundled-realms/pretui/components/answer-footer.gts +249 -0
  4. package/bundled-realms/pretui/components/card-embed.gts +523 -0
  5. package/bundled-realms/pretui/components/card-pills.gts +725 -0
  6. package/bundled-realms/pretui/components/code-block.gts +87 -45
  7. package/bundled-realms/pretui/components/collapsed-turn.gts +133 -12
  8. package/bundled-realms/pretui/components/compaction-chip.gts +81 -67
  9. package/bundled-realms/pretui/components/composer-cap.gts +516 -0
  10. package/bundled-realms/pretui/components/context-ring.gts +108 -0
  11. package/bundled-realms/pretui/components/diff-block.gts +1002 -31
  12. package/bundled-realms/pretui/components/diff-stat.gts +102 -0
  13. package/bundled-realms/pretui/components/jump-chip.gts +86 -53
  14. package/bundled-realms/pretui/components/markdown-stream.gts +799 -0
  15. package/bundled-realms/pretui/components/markdown-surface.gts +71 -0
  16. package/bundled-realms/pretui/components/plan-mark.gts +147 -0
  17. package/bundled-realms/pretui/components/prose.gts +185 -8
  18. package/bundled-realms/pretui/components/prose.md +6 -8
  19. package/bundled-realms/pretui/components/quick-look.gts +377 -0
  20. package/bundled-realms/pretui/components/reactions.gts +136 -0
  21. package/bundled-realms/pretui/components/recommendation.gts +67 -36
  22. package/bundled-realms/pretui/components/result-card.gts +58 -22
  23. package/bundled-realms/pretui/components/segmented-control.gts +13 -8
  24. package/bundled-realms/pretui/components/session-prep.gts +12 -11
  25. package/bundled-realms/pretui/components/sliding-highlight.gts +8 -3
  26. package/bundled-realms/pretui/components/speaker-label.gts +132 -0
  27. package/bundled-realms/pretui/components/state-glyph.gts +121 -0
  28. package/bundled-realms/pretui/components/step-list.gts +62 -60
  29. package/bundled-realms/pretui/components/stick-to-bottom.gts +1150 -0
  30. package/bundled-realms/pretui/components/system-line.gts +143 -0
  31. package/bundled-realms/pretui/components/task-checklist.gts +184 -0
  32. package/bundled-realms/pretui/components/thinking.gts +63 -94
  33. package/bundled-realms/pretui/components/tool-change-table.gts +285 -0
  34. package/bundled-realms/pretui/components/tool-gate.gts +462 -0
  35. package/bundled-realms/pretui/components/tool-group.gts +920 -0
  36. package/bundled-realms/pretui/components/tool-icon.ts +64 -0
  37. package/bundled-realms/pretui/components/tool-meta.gts +82 -0
  38. package/bundled-realms/pretui/components/tool-plan.gts +197 -0
  39. package/bundled-realms/pretui/components/tool-row.gts +2087 -0
  40. package/bundled-realms/pretui/components/turn-changes.gts +227 -0
  41. package/bundled-realms/pretui/components/user-bubble.gts +575 -0
  42. package/bundled-realms/pretui/components/work-item.gts +185 -48
  43. package/bundled-realms/pretui/docs/scroll-anchoring-protocol.md +136 -0
  44. package/bundled-realms/pretui/internal/markdown-projection.ts +438 -0
  45. package/bundled-realms/pretui/internal/mount-queue.ts +79 -0
  46. package/bundled-realms/pretui/internal/tool-phrases.ts +168 -0
  47. package/bundled-realms/pretui/package.json +2 -1
  48. package/bundled-test-harness/assets/{ai-module-creation-test-beRNwH-e.js → ai-module-creation-test-B6FQKqIl.js} +1 -1
  49. package/bundled-test-harness/assets/{app-QAuw0l8v.js → app-KpPqdyX3.js} +4 -4
  50. package/bundled-test-harness/assets/{binary-upload-dedupe-test-bI8T0QLl.js → binary-upload-dedupe-test-DN00y1tM.js} +1 -1
  51. package/bundled-test-harness/assets/{can-invoke-test-DVaU_yRT.js → can-invoke-test-Bl-EUFM3.js} +1 -1
  52. package/bundled-test-harness/assets/{card-chooser-paging-test-DnxMnXKR.js → card-chooser-paging-test-BNuZRHX3.js} +1 -1
  53. package/bundled-test-harness/assets/{card-chooser-test-DUSnLr-u.js → card-chooser-test-wKfPOOT4.js} +1 -1
  54. package/bundled-test-harness/assets/{card-def-field-def-relationships-test-GBUr3tFK.js → card-def-field-def-relationships-test-CduQ6zan.js} +1 -1
  55. package/bundled-test-harness/assets/{card-delete-test-CQveO1VC.js → card-delete-test-DKxolj5o.js} +1 -1
  56. package/bundled-test-harness/assets/{check-correctness-test-D2M2p66N.js → check-correctness-test-B-kgHztp.js} +1 -1
  57. package/bundled-test-harness/assets/choreo-BEHW-NOc.js +2 -0
  58. package/bundled-test-harness/assets/{choreo-CJfBb_EY.js → choreo-C2cpCfhA.js} +164 -13
  59. package/bundled-test-harness/assets/{choreo-feature-reel-test-D2OrygZv.js → choreo-feature-reel-test-CPI_6nkT.js} +2 -2
  60. package/bundled-test-harness/assets/{choreo-gallery-w07xqtO-.js → choreo-gallery-BxA9cyzF.js} +3 -3
  61. package/bundled-test-harness/assets/{choreo-gallery-build-order-test-yQg4JER6.js → choreo-gallery-build-order-test-CLVoseAJ.js} +1 -1
  62. package/bundled-test-harness/assets/{choreo-gallery-compositor-test-C2THCXJN.js → choreo-gallery-compositor-test-BpT5Ia6B.js} +1 -1
  63. package/bundled-test-harness/assets/{choreo-gallery-cover-test-D4T_Tk4s.js → choreo-gallery-cover-test-DL_fdSsh.js} +1 -1
  64. package/bundled-test-harness/assets/{choreo-gallery-crossing-stage-test-tHUPJ-Hy.js → choreo-gallery-crossing-stage-test-BcKHhpOK.js} +2 -2
  65. package/bundled-test-harness/assets/{choreo-gallery-demos-alive-test-BOtBOnEZ.js → choreo-gallery-demos-alive-test-KufLhXG8.js} +1 -1
  66. package/bundled-test-harness/assets/{choreo-gallery-drift-test-D_6U1Z2c.js → choreo-gallery-drift-test-Cx58rYIA.js} +1 -1
  67. package/bundled-test-harness/assets/{choreo-gallery-escort-test-DDBU0n4C.js → choreo-gallery-escort-test-Wz-emmID.js} +1 -1
  68. package/bundled-test-harness/assets/{choreo-gallery-films-test-CgW1NSdr.js → choreo-gallery-films-test-BFrVs5WW.js} +2 -2
  69. package/bundled-test-harness/assets/{choreo-gallery-filter-test-DUIsNwdO.js → choreo-gallery-filter-test-DCKQkhwe.js} +1 -1
  70. package/bundled-test-harness/assets/{choreo-gallery-fold-test-Chzlxvdr.js → choreo-gallery-fold-test-UtpDgpG9.js} +1 -1
  71. package/bundled-test-harness/assets/{choreo-gallery-guide-pages-test-DOWnqALT.js → choreo-gallery-guide-pages-test-rXDU9JLM.js} +1 -1
  72. package/bundled-test-harness/assets/{choreo-gallery-guides-test-Dra2J4LJ.js → choreo-gallery-guides-test-DfVFdIuU.js} +3 -3
  73. package/bundled-test-harness/assets/{choreo-gallery-hang-test-zzn1eiB-.js → choreo-gallery-hang-test-Cj7MbkFf.js} +1 -1
  74. package/bundled-test-harness/assets/{choreo-gallery-highlight-test-CW99hncQ.js → choreo-gallery-highlight-test-D6CF3IYZ.js} +2 -2
  75. package/bundled-test-harness/assets/{choreo-gallery-inline-edit-test-CYFm1kUz.js → choreo-gallery-inline-edit-test-LfHWsbqc.js} +1 -1
  76. package/bundled-test-harness/assets/{choreo-gallery-interruption-test-CYIBrKV9.js → choreo-gallery-interruption-test-CvQQAgTp.js} +2 -2
  77. package/bundled-test-harness/assets/{choreo-gallery-lone-stack-test-DL8R3SG7.js → choreo-gallery-lone-stack-test-D7Zci7fm.js} +1 -1
  78. package/bundled-test-harness/assets/{choreo-gallery-navigation-test-Dcv04kDx.js → choreo-gallery-navigation-test-DB1EnBEW.js} +1 -1
  79. package/bundled-test-harness/assets/{choreo-gallery-playhead-transport-test-Cfz7GsXl.js → choreo-gallery-playhead-transport-test-CtBlc6AJ.js} +1 -1
  80. package/bundled-test-harness/assets/{choreo-gallery-poplayout-subtree-test-CwoklBkW.js → choreo-gallery-poplayout-subtree-test-BQbmjsvI.js} +1 -1
  81. package/bundled-test-harness/assets/{choreo-gallery-presence-modes-test-BDCIZzJ0.js → choreo-gallery-presence-modes-test-CIdXVG1K.js} +1 -1
  82. package/bundled-test-harness/assets/{choreo-gallery-presentation-test-DlyiHwFf.js → choreo-gallery-presentation-test-C_rPFp29.js} +1 -1
  83. package/bundled-test-harness/assets/{choreo-gallery-reduced-motion-test-DVz5SKNR.js → choreo-gallery-reduced-motion-test-CH5w8Lw-.js} +2 -2
  84. package/bundled-test-harness/assets/{choreo-gallery-sheet-standalone-test-CpD_-BvS.js → choreo-gallery-sheet-standalone-test-CDgI4mGX.js} +1 -1
  85. package/bundled-test-harness/assets/{choreo-gallery-sheet-test-BjvAKHDL.js → choreo-gallery-sheet-test-5lsFX7Rk.js} +1 -1
  86. package/bundled-test-harness/assets/{choreo-gallery-site-test-oq_7y_PN.js → choreo-gallery-site-test-DbMa2xeC.js} +2 -2
  87. package/bundled-test-harness/assets/{choreo-gallery-stage-Bzr4_8Xd.js → choreo-gallery-stage-b3Sk86hp.js} +2 -2
  88. package/bundled-test-harness/assets/{choreo-gallery-stage-keyboard-test-a83d2euJ.js → choreo-gallery-stage-keyboard-test-ds6fT5Eg.js} +1 -1
  89. package/bundled-test-harness/assets/{choreo-gallery-stages-test-C9Sl1kH6.js → choreo-gallery-stages-test-ahioKCct.js} +2 -2
  90. package/bundled-test-harness/assets/{choreo-gallery-subdivision-test-BVVnkOwk.js → choreo-gallery-subdivision-test-g7anTDhT.js} +1 -1
  91. package/bundled-test-harness/assets/{choreo-gallery-table-plan-test-B0YdYEA0.js → choreo-gallery-table-plan-test-Bozc4uUf.js} +1 -1
  92. package/bundled-test-harness/assets/{choreo-gallery-theater-test-BLA147xw.js → choreo-gallery-theater-test-kE535d-b.js} +3 -3
  93. package/bundled-test-harness/assets/{choreo-gallery-tuning-test-DSXiNzCh.js → choreo-gallery-tuning-test-6rHkQBnZ.js} +1 -1
  94. package/bundled-test-harness/assets/{choreo-gallery-widget-room-test-BI574PdO.js → choreo-gallery-widget-room-test-0Vq-uTo1.js} +2 -2
  95. package/bundled-test-harness/assets/{choreo-gallery-wires-test-B6Mz2WZl.js → choreo-gallery-wires-test-CdtnAWYO.js} +1 -1
  96. package/bundled-test-harness/assets/clip-BLfuHi8g.js +2 -0
  97. package/bundled-test-harness/assets/{clip-C_pXjVvY.js → clip-q1QE5f73.js} +1 -1
  98. package/bundled-test-harness/assets/{codeblocks-test-AdolhdCA.js → codeblocks-test-BDD7qK_V.js} +1 -1
  99. package/bundled-test-harness/assets/{compaction-test-DvyAQq56.js → compaction-test-BweolkhG.js} +1 -1
  100. package/bundled-test-harness/assets/{container-DIF_9lZ9.css → container-B5B6u-xm.css} +29 -22
  101. package/bundled-test-harness/assets/{container-DvvqmNpg.js → container-BnJjIM6J.js} +9 -6
  102. package/bundled-test-harness/assets/{create-listing-modal-test-D95JfQab.js → create-listing-modal-test-FvcAKpIa.js} +1 -1
  103. package/bundled-test-harness/assets/{debug-message-test-BcNtj7Wr.js → debug-message-test-CJ18Evt7.js} +1 -1
  104. package/bundled-test-harness/assets/{externals-6ZWftYIb.js → externals-BWjhmCD4.js} +12 -12
  105. package/bundled-test-harness/assets/{fallback-banner-live-delete-test-5lnN1pv2.js → fallback-banner-live-delete-test-C2H3BrT-.js} +1 -1
  106. package/bundled-test-harness/assets/{fallback-banner-test-DFb_m_Kq.js → fallback-banner-test-C10aYjkj.js} +1 -1
  107. package/bundled-test-harness/assets/{fallback-models-test-p3YIwj1L.js → fallback-models-test-BxoPiaGF.js} +1 -1
  108. package/bundled-test-harness/assets/{file-attachment-test-CmyjNWRR.js → file-attachment-test-DeKyzYaz.js} +1 -1
  109. package/bundled-test-harness/assets/film-B68PjRlr.js +2 -0
  110. package/bundled-test-harness/assets/{film-D7vtJO5q.js → film-CeUpnHN_.js} +6 -6
  111. package/bundled-test-harness/assets/{film-DN0A8JHx.js → film-DBZqyqW4.js} +6 -6
  112. package/bundled-test-harness/assets/{general-test-CX-TFDuX.js → general-test-BewzrpoB.js} +1 -1
  113. package/bundled-test-harness/assets/{glimmer-motion-and-choreo-in-cards-test-BHte2t9G.js → glimmer-motion-and-choreo-in-cards-test-HA-INN3d.js} +14 -14
  114. package/bundled-test-harness/assets/{glimmer-motion-and-choreo-test-BPNVeIio.js → glimmer-motion-and-choreo-test-BZ0ZuuAw.js} +3 -3
  115. package/bundled-test-harness/assets/{index-query-engine-test-Dz7Q4Xmk.js → index-query-engine-test-BtI8Y2LR.js} +1 -1
  116. package/bundled-test-harness/assets/{instance-filter-matcher-test-BoLU3bVg.js → instance-filter-matcher-test-BHXHGHjE.js} +1 -1
  117. package/bundled-test-harness/assets/{main-Dlgp8c7v.js → main-CqAHZ2kK.js} +1 -1
  118. package/bundled-test-harness/assets/{message-tool-test-CfjRSJvt.js → message-tool-test-NMaPJFsd.js} +1 -1
  119. package/bundled-test-harness/assets/{model-cost-tier-test-mZG84Uso.js → model-cost-tier-test-BgbfN2LH.js} +1 -1
  120. package/bundled-test-harness/assets/{operation-lowering-test-B0KpI1xu.js → operation-lowering-test-C4IaOjsA.js} +1 -1
  121. package/bundled-test-harness/assets/{operator-mode-card-chooser-test-KWS8lq16.js → operator-mode-card-chooser-test-Bwe47Rn7.js} +1 -1
  122. package/bundled-test-harness/assets/{operator-mode-links-test-CViFt0Ws.js → operator-mode-links-test-BU4jkC-B.js} +1 -1
  123. package/bundled-test-harness/assets/{operator-mode-test-D0gZM2yw.js → operator-mode-test-Bh0rGM9z.js} +1 -1
  124. package/bundled-test-harness/assets/{operator-mode-ui-test-CEvHJ9DS.js → operator-mode-ui-test-whgUw0ju.js} +1 -1
  125. package/bundled-test-harness/assets/{overlay-menu-items-test-Pw80RXsr.js → overlay-menu-items-test-Cwn1vr8q.js} +1 -1
  126. package/bundled-test-harness/assets/{overlays-BnOM360u.js → overlays-BOzLbVDK.js} +1 -1
  127. package/bundled-test-harness/assets/{overlays-kBVfa4i8.js → overlays-C_PuGDGh.js} +1 -1
  128. package/bundled-test-harness/assets/{past-sessions-test-CW0-Bjav.js → past-sessions-test-Bh24N-Kz.js} +1 -1
  129. package/bundled-test-harness/assets/{permission-checking-realm-test-D5uXQypS.js → permission-checking-realm-test-DKnnAy0f.js} +1 -1
  130. package/bundled-test-harness/assets/{plate-CFvCDv2t.js → plate-BL7UtyKE.js} +1 -1
  131. package/bundled-test-harness/assets/plate-CqG3igCJ.js +2 -0
  132. package/bundled-test-harness/assets/player-BJfsgdSI.js +2 -0
  133. package/bundled-test-harness/assets/{player-DzuYjrTU.js → player-Da_CIsp_.js} +1 -1
  134. package/bundled-test-harness/assets/{realm-config-policy-standing-test-Btatheni.js → realm-config-policy-standing-test-DngdYypq.js} +1 -1
  135. package/bundled-test-harness/assets/{realm-config-routing-rule-edit-test-Dh5zW13o.js → realm-config-routing-rule-edit-test-_rkFU8SN.js} +1 -1
  136. package/bundled-test-harness/assets/{realm-config-settings-test-BJa8gv_C.js → realm-config-settings-test-CR9NH0Zn.js} +1 -1
  137. package/bundled-test-harness/assets/{reasoning-test-2Zp37cQB.js → reasoning-test-0lH10kRl.js} +1 -1
  138. package/bundled-test-harness/assets/{room-message-test-BsrON13e.js → room-message-test-C-PddE4Y.js} +1 -1
  139. package/bundled-test-harness/assets/{run-BMyvn_UL.js → run-D4n4LPcK.js} +96 -6
  140. package/bundled-test-harness/assets/{run-realm-code-BxktQf6R.js → run-realm-code-C8N7DtN-.js} +1 -1
  141. package/bundled-test-harness/assets/{run-realm-code-test-7grpTos-.js → run-realm-code-test-DBdp147q.js} +1 -1
  142. package/bundled-test-harness/assets/{schema-generation-test-CM9ggQji.js → schema-generation-test-CB1Hb7GQ.js} +1 -1
  143. package/bundled-test-harness/assets/{scrolling-test-BER00htB.js → scrolling-test-3PhVin8h.js} +1 -1
  144. package/bundled-test-harness/assets/{searchable-option-test-BBr5BPow.js → searchable-option-test-BAdOiL4U.js} +1 -1
  145. package/bundled-test-harness/assets/{sending-test-BJGobs2E.js → sending-test-KSmu1tSJ.js} +1 -1
  146. package/bundled-test-harness/assets/{skills-test-GPOsHRN6.js → skills-test-D-dlcFpd.js} +1 -1
  147. package/bundled-test-harness/assets/{src-Df148w_b.js → src-B_DEhPze.js} +4 -4
  148. package/bundled-test-harness/assets/{src-Cf2wv-Le.js → src-BoTgwqUA.js} +2 -2
  149. package/bundled-test-harness/assets/{store-test-QvvHimzH.js → store-test-CEW7xQjZ.js} +1 -1
  150. package/bundled-test-harness/assets/{test-support-rNHuieEc.js → test-support-FXDPpIl3.js} +1 -1
  151. package/bundled-test-harness/assets/{test-support-C6N_2X-l.js → test-support-VX6ukQNR.js} +1 -1
  152. package/bundled-test-harness/assets/{tests-CmsQ-Z_3.js → tests-QYqqa6nv.js} +84 -84
  153. package/bundled-test-harness/assets/{text-input-validator-test-CYYH2N0L.js → text-input-validator-test-D8iNOWep.js} +1 -1
  154. package/bundled-test-harness/assets/{titles-B1pTaIfP.js → titles-DFJj5O0E.js} +1 -1
  155. package/bundled-test-harness/assets/titles-xGlgu7ys.js +2 -0
  156. package/bundled-test-harness/assets/{to-device-streaming-test-Ctqa0bVp.js → to-device-streaming-test-trCX-505.js} +1 -1
  157. package/bundled-test-harness/assets/{token-usage-test-BQVbK3X0.js → token-usage-test-C6sULooH.js} +1 -1
  158. package/bundled-test-harness/assets/{tools-DU8Rc15s.js → tools-UJfD-_al.js} +1 -1
  159. package/bundled-test-harness/assets/{tools-test-B13yvEKE.js → tools-test-Brav31AV.js} +1 -1
  160. package/bundled-test-harness/assets/{worker-DHBEBihX.js → worker-9ph9_Lpv.js} +31 -26
  161. package/bundled-test-harness/index.html +8 -8
  162. package/bundled-test-harness/tests/index.html +8 -8
  163. package/bundled-types/choreo/beacons.ts +16 -4
  164. package/bundled-types/choreo/changeset.ts +25 -5
  165. package/bundled-types/choreo/choreo.gts +245 -3
  166. package/bundled-types/choreo/far.ts +39 -0
  167. package/bundled-types/choreo/index.ts +2 -0
  168. package/bundled-types/choreo/run.ts +104 -1
  169. package/bundled-types/host-app/lib/realm-runner/worker.ts +8 -51
  170. package/bundled-types/runtime-common/realm-runner-script.d.ts +3 -0
  171. package/package.json +1 -1
  172. package/bundled-test-harness/assets/choreo-B3Z9QhDH.js +0 -2
  173. package/bundled-test-harness/assets/clip-BjcXuTwN.js +0 -2
  174. package/bundled-test-harness/assets/film-DMDPP7ma.js +0 -2
  175. package/bundled-test-harness/assets/plate-66qnQ-M9.js +0 -2
  176. package/bundled-test-harness/assets/player-pygg1PwA.js +0 -2
  177. package/bundled-test-harness/assets/titles-Ccpcl-jq.js +0 -2
@@ -0,0 +1,920 @@
1
+ // Pretui — ToolGroup: a run of consecutive tool calls folded into one receipt line ("Used 12 tools · Read, Patch fields and Search"), with a rolling count while the run grows, a header that stays in view while you read inside it, and merging of repetitive runs.
2
+ import Component from '@glimmer/component';
3
+ import { tracked } from '@glimmer/tracking';
4
+ import { on } from '@ember/modifier';
5
+ import { guidFor } from '@ember/object/internals';
6
+ import { cssNumber } from '../pretui-css';
7
+ import { htmlSafe } from '@ember/template';
8
+ import { modifier } from 'ember-modifier';
9
+ import type { SafeString } from '@ember/template';
10
+ import { Collapse } from '../internal/agentic-chat';
11
+ import { Odometer } from './odometer';
12
+ import { PlanMark } from './plan-mark';
13
+ import PointIcon from '@cardstack/boxel-icons/point';
14
+ import StackIcon from '@cardstack/boxel-icons/stack-2';
15
+ import { ToolRow, humanizeToolName } from './tool-row';
16
+ import type { ToolRenderers, ToolSource } from './tool-row';
17
+ import {
18
+ KIND_PHRASES,
19
+ joinPhrases,
20
+ listSeparators,
21
+ phraseRun,
22
+ } from '../internal/tool-phrases';
23
+ import type {
24
+ PhraseItem,
25
+ ToolKind,
26
+ ToolPhrase,
27
+ } from '../internal/tool-phrases';
28
+ import { workTone } from './work-item';
29
+ import type { WorkTone } from './work-item';
30
+
31
+ export interface ToolPart {
32
+ /** stable: the tool call id */
33
+ id: string;
34
+ name: string;
35
+ state: string;
36
+ subtitle?: string;
37
+ source?: ToolSource;
38
+ /** seconds */
39
+ duration?: number;
40
+ error?: string;
41
+ params?: unknown;
42
+ result?: unknown;
43
+ /** drives phrasing and merging. Default: the renderer's kind, else
44
+ * 'other' */
45
+ kind?: ToolKind;
46
+ }
47
+
48
+ /** How the receipt reads: what the calls did ('kind', the default: "Read 3
49
+ * files and edited 1 file"), or which tools they used ('names': "Used 4
50
+ * tools · Read and Patch fields"). */
51
+ export type ToolGroupPhrasing = 'kind' | 'names';
52
+
53
+ export type ToolGroupMerge = 'none' | 'reads' | 'all';
54
+
55
+ /** One rendered row: a single call, or a merged run of reads or writes. */
56
+ export interface ToolGroupEntry {
57
+ /** the first call's id, so a run that grows keeps its key */
58
+ id: string;
59
+ merged: boolean;
60
+ kind: ToolKind;
61
+ parts: ToolPart[];
62
+ title: string;
63
+ subtitle?: string;
64
+ state: string;
65
+ duration?: number;
66
+ }
67
+
68
+ export interface ToolGroupSummary {
69
+ /** what the calls did, grouped by phrase in order of first use */
70
+ phrases: PhraseItem[];
71
+ /** the phrases as one list, in English */
72
+ text: string;
73
+ count: number;
74
+ names: string[];
75
+ /** the names as one list, in English, truncated after @maxNames */
76
+ list: string;
77
+ failed: number;
78
+ tone: WorkTone;
79
+ pending: boolean;
80
+ }
81
+
82
+ export interface ToolGroupSignature {
83
+ Args: {
84
+ /** the calls in this group, in order */
85
+ tools: ToolPart[];
86
+ renderers?: ToolRenderers;
87
+ /** controlled disclosure */
88
+ open?: boolean;
89
+ /** uncontrolled initial disclosure. Default false */
90
+ defaultOpen?: boolean;
91
+ /** fires when the reader toggles the group */
92
+ onOpenChange?: (open: boolean) => void;
93
+ /** more calls may still arrive: the count rolls and a spinner shows */
94
+ pending?: boolean;
95
+ /** override the derived receipt text */
96
+ summary?: string;
97
+ /** 'kind' (default): what the calls did. 'names': which tools they used */
98
+ phrasing?: ToolGroupPhrasing;
99
+ /** whether a settled, closed group recedes. Default: when it is done and
100
+ * not pending. It only shows inside a container that sets
101
+ * --pretui-recede-opacity */
102
+ recede?: boolean;
103
+ /** while open, the header sticks inside the scroll container. Default true */
104
+ sticky?: boolean;
105
+ /** CSS length below which the header sticks. Default 0 */
106
+ stickyOffset?: string;
107
+ /** an open group shows about this many rows and scrolls the rest inside
108
+ * itself, so forty calls never push the transcript forty rows down.
109
+ * Default 8; 0 for no limit */
110
+ maxRows?: number;
111
+ /** merge consecutive reads ('reads'), and writes to distinct targets too
112
+ * ('all', the default), into one row */
113
+ merge?: ToolGroupMerge;
114
+ /** tool names listed before "N more". Default 4 */
115
+ maxNames?: number;
116
+ };
117
+ Blocks: {
118
+ tool: [ToolGroupEntry];
119
+ summary: [ToolGroupSummary];
120
+ };
121
+ Element: HTMLDivElement;
122
+ }
123
+
124
+ const TONE_RANK: Record<WorkTone, number> = {
125
+ attention: 4,
126
+ failed: 3,
127
+ running: 2,
128
+ idle: 1,
129
+ done: 0,
130
+ };
131
+
132
+ function toneOf(state: string): WorkTone {
133
+ return state === 'streaming' ? 'running' : workTone(state);
134
+ }
135
+
136
+ function scrollParent(el: HTMLElement): HTMLElement | undefined {
137
+ let node = el.parentElement;
138
+ while (node) {
139
+ let overflowY = getComputedStyle(node).overflowY;
140
+ if (
141
+ (overflowY === 'auto' || overflowY === 'scroll') &&
142
+ node.scrollHeight > node.clientHeight
143
+ ) {
144
+ return node;
145
+ }
146
+ node = node.parentElement;
147
+ }
148
+ return undefined;
149
+ }
150
+
151
+ function firstPart(entry: ToolGroupEntry): ToolPart {
152
+ return entry.parts[0]!;
153
+ }
154
+
155
+ const registerElement = modifier(
156
+ (el: HTMLElement, [register]: [(el: HTMLElement) => void]) => {
157
+ register(el);
158
+ },
159
+ );
160
+
161
+ /** Reports the open state on install and on change, so the rows render
162
+ * after the first open even when a call needing the reader opened it. */
163
+ const noteOpen = modifier(
164
+ (_el: HTMLElement, [callback, open]: [(open: boolean) => void, boolean]) => {
165
+ callback(open);
166
+ },
167
+ );
168
+
169
+ /**
170
+ * A run of tool calls as one receipt.
171
+ *
172
+ * ```hbs
173
+ * <ToolGroup @tools={{group.tools}} @renderers={{this.toolRenderers}}
174
+ * @pending={{group.isLive}} @stickyOffset='var(--session-header-height)' />
175
+ * ```
176
+ *
177
+ * The attention state is part of the group's tone, so a call that needs
178
+ * the reader opens a collapsed group instead of hiding inside it; merging
179
+ * is a knob; and the rows inside a group that has never been opened are not
180
+ * rendered at all.
181
+ */
182
+ export class ToolGroup extends Component<ToolGroupSignature> {
183
+ @tracked private userOpen: boolean | undefined;
184
+ /** rows render only once the group has been open: a closed group of forty
185
+ * calls costs one header */
186
+ @tracked private everOpened = false;
187
+ readonly bodyId = guidFor(this) + '-rows';
188
+ private root: HTMLElement | undefined;
189
+
190
+ get tools(): ToolPart[] {
191
+ return this.args.tools ?? [];
192
+ }
193
+ get maxNames(): number {
194
+ return Math.max(1, this.args.maxNames ?? 4);
195
+ }
196
+ get merge(): ToolGroupMerge {
197
+ return this.args.merge ?? 'all';
198
+ }
199
+
200
+ private titleOf(part: ToolPart): string {
201
+ return (
202
+ this.args.renderers?.[part.name]?.title ?? humanizeToolName(part.name)
203
+ );
204
+ }
205
+ private kindOf(part: ToolPart): ToolKind {
206
+ return part.kind ?? this.args.renderers?.[part.name]?.kind ?? 'other';
207
+ }
208
+ private phraseOf = (part: ToolPart): ToolPhrase =>
209
+ this.args.renderers?.[part.name]?.phrase ?? KIND_PHRASES[this.kindOf(part)];
210
+
211
+ get phrasing(): ToolGroupPhrasing {
212
+ return this.args.phrasing ?? 'kind';
213
+ }
214
+ /** The call the run is on now, whose phrase reads in the present tense:
215
+ * the newest call while the group is pending or it is still running. */
216
+ private get activeIndex(): number | undefined {
217
+ let last = this.tools.length - 1;
218
+ if (last < 0) {
219
+ return undefined;
220
+ }
221
+ let lastRunning = toneOf(this.tools[last]!.state) === 'running';
222
+ return this.args.pending || lastRunning ? last : undefined;
223
+ }
224
+ get phrases(): PhraseItem[] {
225
+ return phraseRun(this.tools, this.phraseOf, this.activeIndex);
226
+ }
227
+ /** each phrase with the literal separator that goes before it, so the
228
+ * template can give every count its own Odometer and still read as one
229
+ * grammatical list */
230
+ get phraseParts(): { item: PhraseItem; before: string }[] {
231
+ let phrases = this.phrases;
232
+ let separators = listSeparators(phrases.length);
233
+ return phrases.map((item, i) => ({ item, before: separators[i] ?? '' }));
234
+ }
235
+ get phraseText(): string {
236
+ return joinPhrases(this.phrases.map((p) => p.text));
237
+ }
238
+ get byKind(): boolean {
239
+ return this.phrasing === 'kind';
240
+ }
241
+
242
+ // ── summary ────────────────────────────────────────────────────────────
243
+
244
+ get names(): string[] {
245
+ let seen = new Set<string>();
246
+ let names: string[] = [];
247
+ for (let part of this.tools) {
248
+ let title = this.titleOf(part);
249
+ if (!seen.has(title)) {
250
+ seen.add(title);
251
+ names.push(title);
252
+ }
253
+ }
254
+ return names;
255
+ }
256
+ get nameList(): string {
257
+ let names = this.names;
258
+ if (names.length <= this.maxNames) {
259
+ return joinPhrases(names);
260
+ }
261
+ let shown = names.slice(0, this.maxNames);
262
+ return joinPhrases([...shown, `${names.length - shown.length} more`]);
263
+ }
264
+ get failedCount(): number {
265
+ return this.tools.filter((part) => toneOf(part.state) === 'failed').length;
266
+ }
267
+ get tone(): WorkTone {
268
+ let worst: WorkTone = 'done';
269
+ for (let part of this.tools) {
270
+ let tone = toneOf(part.state);
271
+ if (TONE_RANK[tone] > TONE_RANK[worst]) {
272
+ worst = tone;
273
+ }
274
+ }
275
+ if (this.args.pending && TONE_RANK[worst] < TONE_RANK.running) {
276
+ worst = 'running';
277
+ }
278
+ if (worst === 'idle') {
279
+ worst = 'done';
280
+ }
281
+ return worst;
282
+ }
283
+ get isRunning(): boolean {
284
+ return this.tone === 'running';
285
+ }
286
+ get isAttention(): boolean {
287
+ return this.tone === 'attention';
288
+ }
289
+ get countNoun(): string {
290
+ return this.tools.length === 1 ? 'tool' : 'tools';
291
+ }
292
+ get statusText(): string | undefined {
293
+ if (this.isAttention) {
294
+ return 'needs you';
295
+ }
296
+ if (this.isRunning) {
297
+ return 'running…';
298
+ }
299
+ return undefined;
300
+ }
301
+ /** The whole receipt in words: the header's accessible name, which does
302
+ * not depend on the rolling digits or the glyph. */
303
+ get receipt(): string {
304
+ if (this.args.summary) {
305
+ return this.args.summary;
306
+ }
307
+ let parts: string[] = [];
308
+ if (this.byKind) {
309
+ if (this.phraseText) {
310
+ parts.push(this.phraseText);
311
+ }
312
+ } else {
313
+ parts.push(`Used ${this.tools.length} ${this.countNoun}`);
314
+ if (this.nameList) {
315
+ parts.push(this.nameList);
316
+ }
317
+ }
318
+ if (this.failedCount) {
319
+ parts.push(`${this.failedCount} failed`);
320
+ }
321
+ if (this.statusText) {
322
+ parts.push(this.isAttention ? 'needs you' : 'running');
323
+ }
324
+ return parts.join(' · ');
325
+ }
326
+ get summary(): ToolGroupSummary {
327
+ return {
328
+ phrases: this.phrases,
329
+ text: this.phraseText,
330
+ count: this.tools.length,
331
+ names: this.names,
332
+ list: this.nameList,
333
+ failed: this.failedCount,
334
+ tone: this.tone,
335
+ pending: Boolean(this.args.pending),
336
+ };
337
+ }
338
+
339
+ // ── entries (merging) ──────────────────────────────────────────────────
340
+
341
+ get entries(): ToolGroupEntry[] {
342
+ let entries: ToolGroupEntry[] = [];
343
+ let run: ToolPart[] = [];
344
+ let runKind: 'read' | 'write' | undefined;
345
+
346
+ let flush = () => {
347
+ if (run.length === 1) {
348
+ entries.push(this.single(run[0]!));
349
+ } else if (run.length > 1 && runKind) {
350
+ entries.push(this.merged(run, runKind));
351
+ }
352
+ run = [];
353
+ runKind = undefined;
354
+ };
355
+
356
+ for (let part of this.tools) {
357
+ let kind = this.kindOf(part);
358
+ // a failed call keeps its own row for its error, and a call that
359
+ // needs the reader keeps its own for its gate and body: a merged row
360
+ // is a plain list with nothing to act on
361
+ let tone = toneOf(part.state);
362
+ let mergeable =
363
+ tone !== 'failed' &&
364
+ tone !== 'attention' &&
365
+ ((kind === 'read' && this.merge !== 'none') ||
366
+ (kind === 'write' && this.merge === 'all'));
367
+ if (!mergeable) {
368
+ flush();
369
+ entries.push(this.single(part));
370
+ continue;
371
+ }
372
+ let k = kind as 'read' | 'write';
373
+ let repeatsTarget =
374
+ k === 'write' &&
375
+ part.subtitle !== undefined &&
376
+ run.some((p) => p.subtitle === part.subtitle);
377
+ if (runKind !== k || repeatsTarget) {
378
+ flush();
379
+ runKind = k;
380
+ }
381
+ run.push(part);
382
+ }
383
+ flush();
384
+ return entries;
385
+ }
386
+
387
+ private single(part: ToolPart): ToolGroupEntry {
388
+ return {
389
+ id: part.id,
390
+ merged: false,
391
+ kind: this.kindOf(part),
392
+ parts: [part],
393
+ title: this.titleOf(part),
394
+ subtitle: part.subtitle,
395
+ state: part.state,
396
+ duration: part.duration,
397
+ };
398
+ }
399
+
400
+ /** "Read 3 files", "Editing 2 files": the run's own phrase when its
401
+ * calls share one, else its kind's. */
402
+ private mergedTitle(
403
+ parts: ToolPart[],
404
+ kind: ToolKind,
405
+ running: boolean,
406
+ ): string {
407
+ let phrases = new Set(parts.map((p) => this.phraseOf(p)));
408
+ let phrase = phrases.size === 1 ? [...phrases][0]! : KIND_PHRASES[kind];
409
+ let verb = running ? phrase.active : phrase.done;
410
+ let noun = parts.length === 1 ? phrase.one : phrase.many;
411
+ return `${verb} ${parts.length} ${noun}`;
412
+ }
413
+
414
+ private merged(parts: ToolPart[], kind: 'read' | 'write'): ToolGroupEntry {
415
+ let worst = parts[0]!;
416
+ for (let part of parts) {
417
+ if (TONE_RANK[toneOf(part.state)] > TONE_RANK[toneOf(worst.state)]) {
418
+ worst = part;
419
+ }
420
+ }
421
+ let durations = parts.map((p) => p.duration);
422
+ let duration = durations.every((d) => d !== undefined)
423
+ ? (durations as number[]).reduce((a, b) => a + b, 0)
424
+ : undefined;
425
+ let targets = parts
426
+ .map((p) => p.subtitle)
427
+ .filter((s): s is string => Boolean(s));
428
+ return {
429
+ id: parts[0]!.id,
430
+ merged: true,
431
+ kind,
432
+ parts,
433
+ title: this.mergedTitle(parts, kind, toneOf(worst.state) === 'running'),
434
+ subtitle: targets.length ? targets.join(', ') : undefined,
435
+ state: worst.state,
436
+ duration,
437
+ };
438
+ }
439
+
440
+ // ── disclosure ─────────────────────────────────────────────────────────
441
+
442
+ get isOpen(): boolean {
443
+ if (this.args.open !== undefined) {
444
+ return this.args.open;
445
+ }
446
+ // a call that needs the reader opens the group, whatever they chose
447
+ if (this.isAttention) {
448
+ return true;
449
+ }
450
+ return this.userOpen ?? this.args.defaultOpen ?? false;
451
+ }
452
+ get recedes(): boolean {
453
+ if (this.args.recede !== undefined) {
454
+ return this.args.recede;
455
+ }
456
+ return this.tone === 'done' && !this.isOpen;
457
+ }
458
+ get renderRows(): boolean {
459
+ return this.isOpen || this.everOpened;
460
+ }
461
+ get sticky(): boolean {
462
+ return (this.args.sticky ?? true) && this.isOpen;
463
+ }
464
+ get headerStyle(): SafeString | undefined {
465
+ // inline rather than in the stylesheet: the offset is the caller's
466
+ // value, and position and offset belong together
467
+ if (!this.sticky) {
468
+ return undefined;
469
+ }
470
+ let top = this.args.stickyOffset ?? '0px';
471
+ return htmlSafe(`position: sticky; top: ${top}; z-index: 1`);
472
+ }
473
+ get rootStyle(): SafeString {
474
+ // what a nested sticky header (a diff's file header) sticks below
475
+ let top = this.args.stickyOffset ?? '0px';
476
+ let rows = cssNumber(this.args.maxRows ?? 8, 0, 1000) ?? 8;
477
+ let max = rows > 0 ? `calc(${rows} * 1.875rem + 0.375rem)` : 'none';
478
+ return htmlSafe(
479
+ `--pretui-tool-group-header: calc(${top} + 2rem); --pretui-tool-group-max: ${max}`,
480
+ );
481
+ }
482
+
483
+ noteOpen = (open: boolean) => {
484
+ if (open && !this.everOpened) {
485
+ // after this render, not during it: renderRows already read the flag
486
+ queueMicrotask(() => {
487
+ if (!this.isDestroying && !this.isDestroyed) {
488
+ this.everOpened = true;
489
+ }
490
+ });
491
+ }
492
+ };
493
+
494
+ toggle = (event: Event) => {
495
+ let next = !this.isOpen;
496
+ if (!next) {
497
+ this.keepHeaderInPlace(event.currentTarget as HTMLElement);
498
+ }
499
+ if (this.args.open === undefined) {
500
+ this.userOpen = next;
501
+ }
502
+ if (next) {
503
+ this.everOpened = true;
504
+ }
505
+ this.args.onOpenChange?.(next);
506
+ };
507
+
508
+ /** Collapsing from the stuck position would leave the reader far below
509
+ * the group: the shorter content clamps scrollTop. So first scroll up by
510
+ * how far the header is stuck, which puts it back at its natural place in
511
+ * the same spot on screen, then collapse. */
512
+ private keepHeaderInPlace(header: HTMLElement) {
513
+ let root = this.root;
514
+ if (!root) {
515
+ return;
516
+ }
517
+ let stuck =
518
+ header.getBoundingClientRect().top - root.getBoundingClientRect().top;
519
+ if (stuck <= 0.5) {
520
+ return;
521
+ }
522
+ let scroller = scrollParent(root);
523
+ if (!scroller) {
524
+ return;
525
+ }
526
+ // rects are transformed by any scaled ancestor; scrollTop is not
527
+ let rendered = scroller.getBoundingClientRect().height;
528
+ let scale =
529
+ scroller.offsetHeight > 0 && rendered > 0
530
+ ? rendered / scroller.offsetHeight
531
+ : 1;
532
+ scroller.scrollTop -= stuck / scale;
533
+ }
534
+
535
+ registerRoot = (el: HTMLElement) => {
536
+ this.root = el;
537
+ };
538
+
539
+ <template>
540
+ <div
541
+ class='pretui-toolgroup'
542
+ role='group'
543
+ aria-label={{this.receipt}}
544
+ data-tone={{this.tone}}
545
+ data-open={{if this.isOpen 'true' 'false'}}
546
+ data-recede={{if this.recedes 'true'}}
547
+ style={{this.rootStyle}}
548
+ {{registerElement this.registerRoot}}
549
+ data-test-pretui-tool-group
550
+ ...attributes
551
+ >
552
+ <button
553
+ type='button'
554
+ class='pretui-toolgroup-header'
555
+ aria-expanded={{if this.isOpen 'true' 'false'}}
556
+ aria-controls={{this.bodyId}}
557
+ aria-label={{this.receipt}}
558
+ style={{this.headerStyle}}
559
+ data-sticky={{if this.sticky 'true'}}
560
+ {{on 'click' this.toggle}}
561
+ data-test-pretui-tool-group-header
562
+ >
563
+ <span
564
+ class='pretui-toolgroup-disc'
565
+ data-tone={{this.tone}}
566
+ aria-hidden='true'
567
+ >
568
+ {{#if this.isRunning}}<PlanMark
569
+ @state='current'
570
+ @size={{14}}
571
+ />{{else}}<StackIcon
572
+ class='pretui-toolgroup-icon'
573
+ width='14'
574
+ height='14'
575
+ />{{/if}}
576
+ </span>
577
+ <span
578
+ class='pretui-toolgroup-receipt'
579
+ aria-hidden='true'
580
+ data-test-pretui-tool-group-receipt
581
+ >
582
+ {{#if (has-block 'summary')}}
583
+ {{yield this.summary to='summary'}}
584
+ {{else if @summary}}
585
+ {{@summary}}
586
+ {{else if this.byKind}}
587
+ <span
588
+ class='pretui-toolgroup-phrases'
589
+ data-test-pretui-tool-group-phrases
590
+ >
591
+ {{#each
592
+ this.phraseParts key='item.key'
593
+ as |part|
594
+ }}{{part.before}}<span
595
+ class='pretui-toolgroup-phrase'
596
+ data-active={{if part.item.active 'true'}}
597
+ data-test-pretui-tool-group-phrase
598
+ >{{part.item.verb}}
599
+ <Odometer @value={{part.item.count}} />
600
+ {{part.item.noun}}</span>{{/each}}
601
+ </span>
602
+ {{#if this.failedCount}}
603
+ <PointIcon
604
+ class='pretui-toolgroup-sep'
605
+ width='10'
606
+ height='10'
607
+ aria-hidden='true'
608
+ />
609
+ <span
610
+ class='pretui-toolgroup-failed'
611
+ data-test-pretui-tool-group-failed
612
+ >{{this.failedCount}}
613
+ failed</span>
614
+ {{/if}}
615
+ {{else}}
616
+ <span class='pretui-toolgroup-used'>Used
617
+ <Odometer
618
+ @value={{this.tools.length}}
619
+ data-test-pretui-tool-group-count
620
+ />
621
+ {{this.countNoun}}</span>
622
+ {{#if this.nameList}}
623
+ <PointIcon
624
+ class='pretui-toolgroup-sep'
625
+ width='10'
626
+ height='10'
627
+ aria-hidden='true'
628
+ />
629
+ <span
630
+ class='pretui-toolgroup-names'
631
+ data-test-pretui-tool-group-names
632
+ >{{this.nameList}}</span>
633
+ {{/if}}
634
+ {{#if this.failedCount}}
635
+ <PointIcon
636
+ class='pretui-toolgroup-sep'
637
+ width='10'
638
+ height='10'
639
+ aria-hidden='true'
640
+ />
641
+ <span
642
+ class='pretui-toolgroup-failed'
643
+ data-test-pretui-tool-group-failed
644
+ >{{this.failedCount}}
645
+ failed</span>
646
+ {{/if}}
647
+ {{/if}}
648
+ </span>
649
+ {{#if this.statusText}}
650
+ <span
651
+ class='pretui-toolgroup-status'
652
+ data-tone={{this.tone}}
653
+ aria-hidden='true'
654
+ data-test-pretui-tool-group-status
655
+ >{{this.statusText}}</span>
656
+ {{/if}}
657
+ <span
658
+ class='pretui-toolgroup-chevron'
659
+ data-open={{if this.isOpen 'true'}}
660
+ aria-hidden='true'
661
+ ></span>
662
+ </button>
663
+ <Collapse
664
+ @open={{this.isOpen}}
665
+ id={{this.bodyId}}
666
+ {{noteOpen this.noteOpen this.isOpen}}
667
+ >
668
+ <div class='pretui-toolgroup-rows' data-test-pretui-tool-group-rows>
669
+ {{#if this.renderRows}}
670
+ {{#each this.entries key='id' as |entry|}}
671
+ {{#if (has-block 'tool')}}
672
+ {{yield entry to='tool'}}
673
+ {{else if entry.merged}}
674
+ <ToolRow
675
+ @name={{entry.kind}}
676
+ @title={{entry.title}}
677
+ @subtitle={{entry.subtitle}}
678
+ @state={{entry.state}}
679
+ @duration={{entry.duration}}
680
+ @recede={{false}}
681
+ data-merged='true'
682
+ data-transcript-id={{entry.id}}
683
+ >
684
+ <:body>
685
+ <ul
686
+ class='pretui-toolgroup-merged'
687
+ data-test-pretui-tool-group-merged
688
+ >
689
+ {{#each entry.parts key='id' as |part|}}
690
+ <li>{{if part.subtitle part.subtitle part.name}}</li>
691
+ {{/each}}
692
+ </ul>
693
+ </:body>
694
+ </ToolRow>
695
+ {{else}}
696
+ {{#let (firstPart entry) as |part|}}
697
+ <ToolRow
698
+ @name={{part.name}}
699
+ @subtitle={{part.subtitle}}
700
+ @state={{part.state}}
701
+ @source={{part.source}}
702
+ @duration={{part.duration}}
703
+ @error={{part.error}}
704
+ @params={{part.params}}
705
+ @result={{part.result}}
706
+ @renderers={{@renderers}}
707
+ @recede={{false}}
708
+ data-transcript-id={{entry.id}}
709
+ />
710
+ {{/let}}
711
+ {{/if}}
712
+ {{/each}}
713
+ {{/if}}
714
+ </div>
715
+ </Collapse>
716
+ </div>
717
+ <style scoped>
718
+ @layer PretComponent, PretComposite;
719
+ @layer PretComposite {
720
+ .pretui-toolgroup {
721
+ display: flex;
722
+ flex-direction: column;
723
+ font-size: var(--boxel-ui-label-font-size);
724
+ line-height: var(--boxel-ui-label-line-height);
725
+ }
726
+ .pretui-toolgroup-header {
727
+ display: flex;
728
+ align-items: center;
729
+ gap: var(--boxel-sp-xs);
730
+ min-height: 2rem;
731
+ padding: var(--boxel-sp-4xs) var(--boxel-sp-xs);
732
+ border: 0;
733
+ border-radius: var(--boxel-border-radius-sm);
734
+ background-color: transparent;
735
+ color: inherit;
736
+ font-family: inherit;
737
+ font-size: inherit;
738
+ font-weight: inherit;
739
+ line-height: inherit;
740
+ text-align: start;
741
+ cursor: pointer;
742
+ min-width: 0;
743
+ }
744
+ .pretui-toolgroup-header {
745
+ transition: opacity 200ms linear;
746
+ }
747
+ .pretui-toolgroup[data-recede] > .pretui-toolgroup-header {
748
+ opacity: var(--pretui-recede-opacity, 1);
749
+ }
750
+ .pretui-toolgroup[data-recede]
751
+ > .pretui-toolgroup-header:is(:hover, :focus-visible) {
752
+ opacity: 1;
753
+ }
754
+ .pretui-toolgroup-header:hover {
755
+ background-color: var(--inset);
756
+ }
757
+ .pretui-toolgroup-header:focus-visible {
758
+ outline: 2px solid var(--ring);
759
+ outline-offset: -2px;
760
+ }
761
+ .pretui-toolgroup-header[data-sticky] {
762
+ background-color: var(--card);
763
+ box-shadow: 0 1px 0 var(--border);
764
+ border-radius: 0;
765
+ }
766
+ /* the group's mark is a stack of calls in the quiet ink, as each
767
+ row's is its tool's icon; a failure or a question tints it */
768
+ .pretui-toolgroup-disc {
769
+ width: var(--boxel-sp);
770
+ height: var(--boxel-sp);
771
+ display: grid;
772
+ place-items: center;
773
+ flex: none;
774
+ color: var(--subtle-foreground);
775
+ }
776
+ .pretui-toolgroup-icon {
777
+ display: block;
778
+ stroke-width: 1.75;
779
+ }
780
+ .pretui-toolgroup-disc[data-tone='failed'] {
781
+ color: var(--destructive-ink);
782
+ }
783
+ .pretui-toolgroup-disc[data-tone='attention'] {
784
+ color: var(--attention-ink);
785
+ }
786
+ .pretui-toolgroup-receipt {
787
+ display: flex;
788
+ align-items: baseline;
789
+ gap: var(--boxel-sp-xs);
790
+ min-width: 0;
791
+ flex: 1;
792
+ color: var(--muted-foreground);
793
+ }
794
+ .pretui-toolgroup-phrases {
795
+ min-width: 0;
796
+ white-space: nowrap;
797
+ overflow: hidden;
798
+ text-overflow: ellipsis;
799
+ color: var(--foreground);
800
+ font-variant-numeric: tabular-nums;
801
+ }
802
+ /* what the run is doing now shimmers; what it did is still */
803
+ .pretui-toolgroup-phrase[data-active] {
804
+ font-weight: 600;
805
+ background: linear-gradient(
806
+ 90deg,
807
+ var(--foreground) 0%,
808
+ var(--muted-foreground) 50%,
809
+ var(--foreground) 100%
810
+ );
811
+ background-size: 200% 100%;
812
+ background-clip: text;
813
+ -webkit-background-clip: text;
814
+ color: transparent;
815
+ animation: pretui-toolgroup-shimmer 1.6s linear infinite;
816
+ }
817
+ @keyframes pretui-toolgroup-shimmer {
818
+ to {
819
+ background-position: -200% 0;
820
+ }
821
+ }
822
+ /* the dot between the summary's parts, as a boxel-icons mark */
823
+ .pretui-toolgroup-sep {
824
+ flex: none;
825
+ align-self: center;
826
+ color: var(--subtle-foreground);
827
+ }
828
+ .pretui-toolgroup-used {
829
+ flex: none;
830
+ color: var(--foreground);
831
+ font-weight: 600;
832
+ font-variant-numeric: tabular-nums;
833
+ }
834
+ .pretui-toolgroup-names {
835
+ min-width: 0;
836
+ white-space: nowrap;
837
+ overflow: hidden;
838
+ text-overflow: ellipsis;
839
+ }
840
+ /* a state colours the word ("2 failed"), never the row */
841
+ .pretui-toolgroup-failed {
842
+ flex: none;
843
+ font-variant-numeric: tabular-nums;
844
+ color: var(--destructive-ink);
845
+ }
846
+ .pretui-toolgroup-status {
847
+ flex: none;
848
+ font-size: var(--boxel-caption-font-size);
849
+ line-height: var(--boxel-caption-line-height);
850
+ font-variant-numeric: tabular-nums;
851
+ color: var(--subtle-foreground);
852
+ }
853
+ .pretui-toolgroup-status[data-tone='attention'] {
854
+ color: var(--attention-ink);
855
+ font-weight: 600;
856
+ }
857
+ .pretui-toolgroup-chevron {
858
+ flex: none;
859
+ width: 0.375rem;
860
+ height: 0.375rem;
861
+ border-inline-end: 1.5px solid var(--subtle-foreground);
862
+ border-block-end: 1.5px solid var(--subtle-foreground);
863
+ transform: rotate(-45deg);
864
+ transition: transform 160ms
865
+ var(--pretui-ease-enter, cubic-bezier(0.22, 0.61, 0.25, 1));
866
+ }
867
+ .pretui-toolgroup-chevron[data-open] {
868
+ transform: rotate(45deg);
869
+ }
870
+ /* the group's rows hang on ToolRow's grid: the rail runs under the
871
+ group's disc centre (the inset plus half the disc), and the
872
+ padding after it plus each row's own inset puts every child's
873
+ disc on the group's text edge. A group nested in a group repeats
874
+ the same step. */
875
+ .pretui-toolgroup-rows {
876
+ display: flex;
877
+ flex-direction: column;
878
+ /* past @maxRows the rows scroll inside the group; the header
879
+ above them stays put */
880
+ max-height: var(--pretui-tool-group-max, none);
881
+ overflow-y: auto;
882
+ overscroll-behavior: contain;
883
+ scrollbar-width: thin;
884
+ gap: 1px;
885
+ padding: var(--boxel-sp-6xs) 0 var(--boxel-sp-4xs) var(--boxel-sp-xs);
886
+ margin-inline-start: var(--boxel-sp);
887
+ box-shadow: inset 1px 0 0 var(--border);
888
+ }
889
+ /* rows inside a group are one step quieter than rows on their own */
890
+ .pretui-toolgroup-rows > :deep(.pretui-toolrow .pretui-toolrow-header) {
891
+ min-height: 1.75rem;
892
+ padding-block: 0.3125rem;
893
+ }
894
+ .pretui-toolgroup-merged {
895
+ margin: 0;
896
+ padding: 0 0 0 var(--boxel-sp);
897
+ /* each line is a call's subtitle (a path, a command) or its name:
898
+ a pasteable string, so the mono voice */
899
+ font-family: var(--font-mono);
900
+ font-size: var(--boxel-font-size-xs);
901
+ line-height: var(--boxel-line-height-xs);
902
+ color: var(--muted-foreground);
903
+ display: flex;
904
+ flex-direction: column;
905
+ gap: var(--boxel-sp-6xs);
906
+ }
907
+ @media (prefers-reduced-motion: reduce) {
908
+ .pretui-toolgroup-chevron {
909
+ transition: none;
910
+ }
911
+ .pretui-toolgroup-phrase[data-active] {
912
+ animation: none;
913
+ color: var(--foreground);
914
+ background: none;
915
+ }
916
+ }
917
+ }
918
+ </style>
919
+ </template>
920
+ }