@k2b/ui 0.26.0 → 0.28.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 (231) hide show
  1. package/README.md +1 -1
  2. package/dist/base.css +147 -2
  3. package/dist/browser/actions/ContextMenu.js +30 -4
  4. package/dist/browser/actions/ContextMenu.js.map +1 -1
  5. package/dist/browser/actions/Dropdown.js +184 -158
  6. package/dist/browser/actions/Dropdown.js.map +1 -1
  7. package/dist/browser/actions/GestureMenu.js +71 -138
  8. package/dist/browser/actions/GestureMenu.js.map +1 -1
  9. package/dist/browser/actions/Toolbar.js +379 -48
  10. package/dist/browser/actions/Toolbar.js.map +1 -1
  11. package/dist/browser/actions/index.js.map +1 -1
  12. package/dist/browser/actions/menu-rows.js +15 -0
  13. package/dist/browser/actions/menu-rows.js.map +1 -0
  14. package/dist/browser/actions/menu-sheet.js +154 -0
  15. package/dist/browser/actions/menu-sheet.js.map +1 -0
  16. package/dist/browser/actions/toolbar-messages.js +20 -0
  17. package/dist/browser/actions/toolbar-messages.js.map +1 -0
  18. package/dist/browser/chart-inspection.js +3 -0
  19. package/dist/browser/chart-inspection.js.map +1 -0
  20. package/dist/browser/chat/ChatComposer.js +1020 -431
  21. package/dist/browser/chat/ChatComposer.js.map +1 -1
  22. package/dist/browser/chat/chat-behavior.js +4 -0
  23. package/dist/browser/chat/chat-behavior.js.map +1 -1
  24. package/dist/browser/chat/chat-messages.js +33 -1
  25. package/dist/browser/chat/chat-messages.js.map +1 -1
  26. package/dist/browser/chat/command-search.js +173 -0
  27. package/dist/browser/chat/command-search.js.map +1 -0
  28. package/dist/browser/chat/composer-document.js +1 -1
  29. package/dist/browser/chat/composer-document.js.map +1 -1
  30. package/dist/browser/chat/index.js +2 -0
  31. package/dist/browser/chat/index.js.map +1 -1
  32. package/dist/browser/chat/reaction-people.js +15 -0
  33. package/dist/browser/chat/reaction-people.js.map +1 -0
  34. package/dist/browser/content/CalendarDayView.js +28 -3
  35. package/dist/browser/content/CalendarDayView.js.map +1 -1
  36. package/dist/browser/content/Chart.js +6 -15
  37. package/dist/browser/content/Chart.js.map +1 -1
  38. package/dist/browser/content/ChartSnapshotView.js +7 -10
  39. package/dist/browser/content/ChartSnapshotView.js.map +1 -1
  40. package/dist/browser/content/MessageRow.js +330 -448
  41. package/dist/browser/content/MessageRow.js.map +1 -1
  42. package/dist/browser/content/ReactionBar.js +419 -0
  43. package/dist/browser/content/ReactionBar.js.map +1 -0
  44. package/dist/browser/content/chart-inspection-core.js +557 -0
  45. package/dist/browser/content/chart-inspection-core.js.map +1 -0
  46. package/dist/browser/content/chart-inspection.js +10 -445
  47. package/dist/browser/content/chart-inspection.js.map +1 -1
  48. package/dist/browser/content/chart-line-gaps.js +1 -1
  49. package/dist/browser/content/chart-line-gaps.js.map +1 -1
  50. package/dist/browser/content/chart-snapshot.js +1 -1
  51. package/dist/browser/content/chart-snapshot.js.map +1 -1
  52. package/dist/browser/content/index.js +1 -0
  53. package/dist/browser/content/index.js.map +1 -1
  54. package/dist/browser/feedback/dialog-core.js +135 -23
  55. package/dist/browser/feedback/dialog-core.js.map +1 -1
  56. package/dist/browser/index.js +1 -0
  57. package/dist/browser/index.js.map +1 -1
  58. package/dist/browser/input-mode.js +40 -0
  59. package/dist/browser/input-mode.js.map +1 -0
  60. package/dist/browser/inputs/Combobox.js +4 -2
  61. package/dist/browser/inputs/Combobox.js.map +1 -1
  62. package/dist/browser/inputs/FileDropTarget.js +24 -9
  63. package/dist/browser/inputs/FileDropTarget.js.map +1 -1
  64. package/dist/browser/inputs/FileInputs.js +2 -2
  65. package/dist/browser/inputs/FileInputs.js.map +1 -1
  66. package/dist/browser/inputs/SelectChip.js +52 -12
  67. package/dist/browser/inputs/SelectChip.js.map +1 -1
  68. package/dist/browser/inputs/emoji/EmojiPicker.js +13 -0
  69. package/dist/browser/inputs/emoji/EmojiPicker.js.map +1 -1
  70. package/dist/browser/inputs/file-drop.js.map +1 -1
  71. package/dist/browser/layout/AppWorkspace.js +708 -515
  72. package/dist/browser/layout/AppWorkspace.js.map +1 -1
  73. package/dist/browser/layout/BottomSheet.js +8 -0
  74. package/dist/browser/layout/BottomSheet.js.map +1 -1
  75. package/dist/browser/layout/DetailPanel.js +283 -197
  76. package/dist/browser/layout/DetailPanel.js.map +1 -1
  77. package/dist/browser/layout/Discussion.js +233 -185
  78. package/dist/browser/layout/Discussion.js.map +1 -1
  79. package/dist/browser/layout/TableOfContents.js +10 -2
  80. package/dist/browser/layout/TableOfContents.js.map +1 -1
  81. package/dist/browser/layout/app-workspace-controller.js +155 -23
  82. package/dist/browser/layout/app-workspace-controller.js.map +1 -1
  83. package/dist/browser/layout/app-workspace-state.js +5 -1
  84. package/dist/browser/layout/app-workspace-state.js.map +1 -1
  85. package/dist/browser/layout/detail-panel-messages.js +35 -0
  86. package/dist/browser/layout/detail-panel-messages.js.map +1 -0
  87. package/dist/browser/layout/index.js +1 -1
  88. package/dist/browser/layout/index.js.map +1 -1
  89. package/dist/browser/surfaces/Avatar.js +50 -20
  90. package/dist/browser/surfaces/Avatar.js.map +1 -1
  91. package/dist/browser/surfaces/AvatarStack.js +406 -0
  92. package/dist/browser/surfaces/AvatarStack.js.map +1 -0
  93. package/dist/browser/surfaces/avatar-messages.js +8 -2
  94. package/dist/browser/surfaces/avatar-messages.js.map +1 -1
  95. package/dist/browser/surfaces/index.js +1 -0
  96. package/dist/browser/surfaces/index.js.map +1 -1
  97. package/dist/ssr/actions/ContextMenu.js +21 -3
  98. package/dist/ssr/actions/ContextMenu.js.map +1 -1
  99. package/dist/ssr/actions/Dropdown.js +140 -72
  100. package/dist/ssr/actions/Dropdown.js.map +1 -1
  101. package/dist/ssr/actions/GestureMenu.js +52 -106
  102. package/dist/ssr/actions/GestureMenu.js.map +1 -1
  103. package/dist/ssr/actions/Toolbar.js +294 -14
  104. package/dist/ssr/actions/Toolbar.js.map +1 -1
  105. package/dist/ssr/actions/index.js.map +1 -1
  106. package/dist/ssr/actions/menu-rows.js +15 -0
  107. package/dist/ssr/actions/menu-rows.js.map +1 -0
  108. package/dist/ssr/actions/menu-sheet.js +138 -0
  109. package/dist/ssr/actions/menu-sheet.js.map +1 -0
  110. package/dist/ssr/actions/toolbar-messages.js +20 -0
  111. package/dist/ssr/actions/toolbar-messages.js.map +1 -0
  112. package/dist/ssr/chart-inspection.js +3 -0
  113. package/dist/ssr/chart-inspection.js.map +1 -0
  114. package/dist/ssr/chat/ChatComposer.js +604 -113
  115. package/dist/ssr/chat/ChatComposer.js.map +1 -1
  116. package/dist/ssr/chat/chat-behavior.js +4 -0
  117. package/dist/ssr/chat/chat-behavior.js.map +1 -1
  118. package/dist/ssr/chat/chat-messages.js +33 -1
  119. package/dist/ssr/chat/chat-messages.js.map +1 -1
  120. package/dist/ssr/chat/command-search.js +173 -0
  121. package/dist/ssr/chat/command-search.js.map +1 -0
  122. package/dist/ssr/chat/composer-document.js +1 -1
  123. package/dist/ssr/chat/composer-document.js.map +1 -1
  124. package/dist/ssr/chat/index.js +2 -0
  125. package/dist/ssr/chat/index.js.map +1 -1
  126. package/dist/ssr/chat/reaction-people.js +15 -0
  127. package/dist/ssr/chat/reaction-people.js.map +1 -0
  128. package/dist/ssr/content/CalendarDayView.js +28 -3
  129. package/dist/ssr/content/CalendarDayView.js.map +1 -1
  130. package/dist/ssr/content/Chart.js +6 -15
  131. package/dist/ssr/content/Chart.js.map +1 -1
  132. package/dist/ssr/content/ChartSnapshotView.js +7 -10
  133. package/dist/ssr/content/ChartSnapshotView.js.map +1 -1
  134. package/dist/ssr/content/MessageRow.js +98 -152
  135. package/dist/ssr/content/MessageRow.js.map +1 -1
  136. package/dist/ssr/content/ReactionBar.js +304 -0
  137. package/dist/ssr/content/ReactionBar.js.map +1 -0
  138. package/dist/ssr/content/chart-inspection-core.js +557 -0
  139. package/dist/ssr/content/chart-inspection-core.js.map +1 -0
  140. package/dist/ssr/content/chart-inspection.js +10 -445
  141. package/dist/ssr/content/chart-inspection.js.map +1 -1
  142. package/dist/ssr/content/chart-line-gaps.js +1 -1
  143. package/dist/ssr/content/chart-line-gaps.js.map +1 -1
  144. package/dist/ssr/content/chart-snapshot.js +1 -1
  145. package/dist/ssr/content/chart-snapshot.js.map +1 -1
  146. package/dist/ssr/content/index.js +1 -0
  147. package/dist/ssr/content/index.js.map +1 -1
  148. package/dist/ssr/feedback/dialog-core.js +135 -23
  149. package/dist/ssr/feedback/dialog-core.js.map +1 -1
  150. package/dist/ssr/index.js +1 -0
  151. package/dist/ssr/index.js.map +1 -1
  152. package/dist/ssr/input-mode.js +40 -0
  153. package/dist/ssr/input-mode.js.map +1 -0
  154. package/dist/ssr/inputs/Combobox.js +4 -2
  155. package/dist/ssr/inputs/Combobox.js.map +1 -1
  156. package/dist/ssr/inputs/FileDropTarget.js +17 -5
  157. package/dist/ssr/inputs/FileDropTarget.js.map +1 -1
  158. package/dist/ssr/inputs/FileInputs.js +2 -2
  159. package/dist/ssr/inputs/FileInputs.js.map +1 -1
  160. package/dist/ssr/inputs/SelectChip.js +44 -11
  161. package/dist/ssr/inputs/SelectChip.js.map +1 -1
  162. package/dist/ssr/inputs/emoji/EmojiPicker.js +12 -0
  163. package/dist/ssr/inputs/emoji/EmojiPicker.js.map +1 -1
  164. package/dist/ssr/inputs/file-drop.js.map +1 -1
  165. package/dist/ssr/layout/AppWorkspace.js +242 -105
  166. package/dist/ssr/layout/AppWorkspace.js.map +1 -1
  167. package/dist/ssr/layout/BottomSheet.js +8 -0
  168. package/dist/ssr/layout/BottomSheet.js.map +1 -1
  169. package/dist/ssr/layout/DetailPanel.js +112 -58
  170. package/dist/ssr/layout/DetailPanel.js.map +1 -1
  171. package/dist/ssr/layout/Discussion.js +63 -36
  172. package/dist/ssr/layout/Discussion.js.map +1 -1
  173. package/dist/ssr/layout/TableOfContents.js +10 -2
  174. package/dist/ssr/layout/TableOfContents.js.map +1 -1
  175. package/dist/ssr/layout/app-workspace-controller.js +155 -23
  176. package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
  177. package/dist/ssr/layout/app-workspace-state.js +5 -1
  178. package/dist/ssr/layout/app-workspace-state.js.map +1 -1
  179. package/dist/ssr/layout/detail-panel-messages.js +35 -0
  180. package/dist/ssr/layout/detail-panel-messages.js.map +1 -0
  181. package/dist/ssr/layout/index.js +1 -1
  182. package/dist/ssr/layout/index.js.map +1 -1
  183. package/dist/ssr/surfaces/Avatar.js +20 -2
  184. package/dist/ssr/surfaces/Avatar.js.map +1 -1
  185. package/dist/ssr/surfaces/AvatarStack.js +286 -0
  186. package/dist/ssr/surfaces/AvatarStack.js.map +1 -0
  187. package/dist/ssr/surfaces/avatar-messages.js +8 -2
  188. package/dist/ssr/surfaces/avatar-messages.js.map +1 -1
  189. package/dist/ssr/surfaces/index.js +1 -0
  190. package/dist/ssr/surfaces/index.js.map +1 -1
  191. package/dist/styles.css +1 -1
  192. package/dist/types/actions/ContextMenu.d.ts +8 -0
  193. package/dist/types/actions/Dropdown.d.ts +12 -5
  194. package/dist/types/actions/GestureMenu.d.ts +6 -4
  195. package/dist/types/actions/Toolbar.d.ts +20 -0
  196. package/dist/types/actions/index.d.ts +1 -1
  197. package/dist/types/actions/menu-rows.d.ts +5 -0
  198. package/dist/types/actions/menu-sheet.d.ts +22 -0
  199. package/dist/types/actions/toolbar-messages.d.ts +8 -0
  200. package/dist/types/chart-inspection.d.ts +2 -0
  201. package/dist/types/chat/ChatComposer.d.ts +51 -2
  202. package/dist/types/chat/chat-behavior.d.ts +4 -0
  203. package/dist/types/chat/chat-messages.d.ts +17 -2
  204. package/dist/types/chat/command-search.d.ts +41 -0
  205. package/dist/types/chat/index.d.ts +3 -1
  206. package/dist/types/chat/reaction-people.d.ts +9 -0
  207. package/dist/types/content/Chart.d.ts +2 -2
  208. package/dist/types/content/MessageRow.d.ts +3 -10
  209. package/dist/types/content/ReactionBar.d.ts +32 -0
  210. package/dist/types/content/chart-inspection-core.d.ts +61 -0
  211. package/dist/types/content/chart-inspection.d.ts +4 -20
  212. package/dist/types/content/chart-snapshot.d.ts +1 -1
  213. package/dist/types/content/index.d.ts +2 -1
  214. package/dist/types/feedback/dialog-core.d.ts +16 -1
  215. package/dist/types/index.d.ts +1 -0
  216. package/dist/types/input-mode.d.ts +7 -0
  217. package/dist/types/inputs/FileDropTarget.d.ts +6 -3
  218. package/dist/types/inputs/SelectChip.d.ts +6 -0
  219. package/dist/types/inputs/file-drop.d.ts +2 -0
  220. package/dist/types/layout/AppWorkspace.d.ts +13 -1
  221. package/dist/types/layout/BottomSheet.d.ts +8 -0
  222. package/dist/types/layout/DetailPanel.d.ts +11 -0
  223. package/dist/types/layout/Discussion.d.ts +8 -0
  224. package/dist/types/layout/TableOfContents.d.ts +2 -0
  225. package/dist/types/layout/detail-panel-messages.d.ts +15 -0
  226. package/dist/types/layout/index.d.ts +1 -1
  227. package/dist/types/surfaces/Avatar.d.ts +12 -0
  228. package/dist/types/surfaces/AvatarStack.d.ts +41 -0
  229. package/dist/types/surfaces/avatar-messages.d.ts +3 -0
  230. package/dist/types/surfaces/index.d.ts +3 -1
  231. package/package.json +9 -4
@@ -17,34 +17,44 @@ var _tmpl$ = ["<span><strong", ">", "</strong></span>"],
17
17
  _tmpl$0 = "<textarea class=\"k2b-chat-composer__measure\" rows=\"1\" tabindex=\"-1\" aria-hidden=\"true\" readonly></textarea>",
18
18
  _tmpl$1 = ["<div class=\"k2b-chat-composer__input\" role=\"group\"", ">", "<textarea class=\"", "\" rows=\"1\"", "", "></textarea>", "</div>"],
19
19
  _tmpl$10 = ["<span class=\"", "\">", "</span>"],
20
- _tmpl$11 = ["<div class=\"k2b-chat-composer-accessory\"><div", " class=\"k2b-chat-composer__commands\" role=\"listbox\"", ">", "</div></div>"],
21
- _tmpl$12 = ["<div role=\"status\" class=\"k2b-chat-composer__loading\"><i class=\"ti ti-loader-2 k2b-spin\" aria-hidden=\"true\"></i><strong>", "</strong></div>"],
22
- _tmpl$13 = ["<div role=\"status\"><i class=\"ti ti-alert-circle\"></i> ", "</div>"],
23
- _tmpl$14 = ["<div role=\"status\">", "</div>"],
24
- _tmpl$15 = ["<div class=\"k2b-chat-composer-accessory\"><div", " class=\"k2b-chat-composer__commands\" role=\"listbox\"", ">", "", "", "", "</div></div>"],
25
- _tmpl$16 = ["<div class=\"k2b-chat-composer-slot\"", "><div style=\"", "\"", ">", "</div>", "", "</div>"],
26
- _tmpl$17 = ["<div class=\"k2b-chat-composer__hint\" aria-live=\"polite\">", "</div>"],
27
- _tmpl$18 = ["<div class=\"k2b-chat-composer__attachments\" role=\"list\"", " tabindex=\"0\">", "</div>"],
28
- _tmpl$19 = ["<div class=\"k2b-chat-composer__error\" role=\"alert\"><i class=\"ti ti-alert-circle\" aria-hidden=\"true\"></i>", "</div>"],
29
- _tmpl$20 = ["<div class=\"k2b-chat-composer__row\">", "", "<div class=\"k2b-chat-composer__field\">", "<div class=\"k2b-chat-composer__field-tools\">", "", "", "</div></div>", "</div>"],
30
- _tmpl$21 = ["<div class=\"k2b-chat-composer-shell\">", "<section class=\"", "\"", " role=\"group\"", ">", "", "", "", "</section></div>"],
31
- _tmpl$22 = ["<button id=\"", "\" type=\"button\" role=\"option\" tabindex=\"-1\"", "><span class=\"k2b-chat-composer__emoji-glyph\" aria-hidden=\"true\">", "</span><strong>", "</strong>", "</button>"],
32
- _tmpl$23 = ["<small>:", ":</small>"],
33
- _tmpl$24 = ["<button id=\"", "\" type=\"button\" role=\"option\" tabindex=\"-1\"", "><i", " aria-hidden=\"true\"></i><strong>", "</strong><small>", "</small></button>"],
34
- _tmpl$25 = ["<span class=\"k2b-chat-composer__hint-text\">", "</span>"],
35
- _tmpl$26 = ["<span class=\"k2b-chat-composer__dictation\"", "><i", " aria-hidden=\"true\"></i><span>", "</span></span>"],
36
- _tmpl$27 = ["<button type=\"button\" class=\"k2b-chat-composer__hint-action\"", ">", "</button>"],
37
- _tmpl$28 = ["<button type=\"button\" class=\"k2b-chat-composer__attachment-remove\"", "", "><i class=\"ti ti-x\" aria-hidden=\"true\"></i></button>"],
38
- _tmpl$29 = ["<div class=\"k2b-chat-composer__attachment\" role=\"listitem\"><div class=\"", "\">", "", "</div>", "</div>"],
39
- _tmpl$30 = ["<img", ">"],
40
- _tmpl$31 = ["<div class=\"k2b-chat-composer__attachment-identity\">", "", "</div>"],
41
- _tmpl$32 = ["<a class=\"k2b-chat-composer__attachment-link\"", " target=\"_blank\" rel=\"noopener noreferrer\"", ">", "", "</a>"],
42
- _tmpl$33 = ["<button type=\"button\" class=\"k2b-chat-composer__attachment-action\"", ">", "", "</button>"],
43
- _tmpl$34 = ["<footer class=\"k2b-chat-composer__footer\">", "</footer>"],
44
- _tmpl$35 = ["<div class=\"k2b-chat-composer__tools\">", "", "", "", "", "", "", "</div>"],
45
- _tmpl$36 = ["<div class=\"k2b-chat-composer__submit\">", "", "", "", "", "</div>"];
46
- import { children, createEffect, createMemo, createSignal, createUniqueId, For, onCleanup, onMount, Show, untrack } from "solid-js";
20
+ _tmpl$11 = ["<small", ">", "</small>"],
21
+ _tmpl$12 = ["<span class=\"k2b-chat-composer__command-end\"><i class=\"ti ti-check\" aria-hidden=\"true\"></i><span class=\"k2b-sr-only\">", "</span></span>"],
22
+ _tmpl$13 = ["<button class=\"k2b-chat-composer__command\"", " type=\"button\" role=\"option\" tabindex=\"-1\"", "><i", " aria-hidden=\"true\"></i><span class=\"k2b-chat-composer__command-text\"><strong>", "</strong>", "</span>", "</button>"],
23
+ _tmpl$14 = ["<b>", "</b>"],
24
+ _tmpl$15 = ["<div class=\"k2b-chat-composer-accessory\"><div", " class=\"k2b-chat-composer__commands\" role=\"listbox\"", ">", "</div></div>"],
25
+ _tmpl$16 = ["<div role=\"status\" class=\"k2b-chat-composer__loading\"><i class=\"ti ti-loader-2 k2b-spin\" aria-hidden=\"true\"></i><strong>", "</strong></div>"],
26
+ _tmpl$17 = ["<div role=\"status\"><i class=\"ti ti-alert-circle\"></i> ", "</div>"],
27
+ _tmpl$18 = ["<div role=\"status\">", "</div>"],
28
+ _tmpl$19 = ["<div class=\"k2b-chat-composer-accessory\" data-commands><div", " class=\"k2b-chat-composer__commands\" role=\"listbox\"", ">", "", "", "", "</div></div>"],
29
+ _tmpl$20 = ["<div class=\"k2b-chat-composer-slot\"", "><div style=\"", "\"", ">", "</div>", "", "</div>"],
30
+ _tmpl$21 = ["<div class=\"k2b-chat-composer__hint\" aria-live=\"polite\">", "</div>"],
31
+ _tmpl$22 = ["<div class=\"k2b-chat-composer__attachments\" role=\"list\"", " tabindex=\"0\">", "</div>"],
32
+ _tmpl$23 = ["<div class=\"k2b-chat-composer__error\" role=\"alert\"><i class=\"ti ti-alert-circle\" aria-hidden=\"true\"></i>", "</div>"],
33
+ _tmpl$24 = ["<div class=\"k2b-chat-composer__row\">", "", "<div class=\"k2b-chat-composer__field\">", "<div class=\"k2b-chat-composer__field-tools\">", "", "", "</div></div>", "</div>"],
34
+ _tmpl$25 = ["<div class=\"k2b-chat-composer-shell\">", "<section class=\"", "\"", " role=\"group\"", ">", "", "", "", "</section><div class=\"k2b-sr-only\" role=\"status\" aria-live=\"polite\">", "</div><div class=\"k2b-chat-composer__status\" role=\"status\">", "", "</div></div>"],
35
+ _tmpl$26 = ["<button id=\"", "\" type=\"button\" role=\"option\" tabindex=\"-1\"", "><span class=\"k2b-chat-composer__emoji-glyph\" aria-hidden=\"true\">", "</span><strong>", "</strong>", "</button>"],
36
+ _tmpl$27 = ["<small>:", ":</small>"],
37
+ _tmpl$28 = "<i class=\"ti ti-loader-2 k2b-spin\" aria-hidden=\"true\"></i>",
38
+ _tmpl$29 = "<div class=\"k2b-chat-composer__command-placeholder\" aria-hidden=\"true\"><span></span><span><span></span><span></span></span></div>",
39
+ _tmpl$30 = ["<div class=\"k2b-chat-composer__command-group\" role=\"group\"", "><div class=\"k2b-chat-composer__command-heading\"", " role=\"presentation\"><span>", "</span>", "</div>", "", "</div>"],
40
+ _tmpl$31 = ["<span class=\"k2b-chat-composer__hint-text\">", "</span>"],
41
+ _tmpl$32 = ["<span class=\"k2b-chat-composer__dictation\"", "><i", " aria-hidden=\"true\"></i><span>", "</span></span>"],
42
+ _tmpl$33 = ["<button type=\"button\" class=\"k2b-chat-composer__hint-action\"", ">", "</button>"],
43
+ _tmpl$34 = ["<button type=\"button\" class=\"k2b-chat-composer__attachment-remove\"", "", "><i class=\"ti ti-x\" aria-hidden=\"true\"></i></button>"],
44
+ _tmpl$35 = ["<div class=\"k2b-chat-composer__attachment\" role=\"listitem\"><div class=\"", "\">", "", "</div>", "</div>"],
45
+ _tmpl$36 = ["<img", ">"],
46
+ _tmpl$37 = ["<div class=\"k2b-chat-composer__attachment-identity\">", "", "</div>"],
47
+ _tmpl$38 = ["<a class=\"k2b-chat-composer__attachment-link\"", " target=\"_blank\" rel=\"noopener noreferrer\"", ">", "", "</a>"],
48
+ _tmpl$39 = ["<button type=\"button\" class=\"k2b-chat-composer__attachment-action\"", ">", "", "</button>"],
49
+ _tmpl$40 = ["<footer class=\"k2b-chat-composer__footer\">", "</footer>"],
50
+ _tmpl$41 = ["<div class=\"k2b-chat-composer__tools\">", "", "", "", "", "", "", "</div>"],
51
+ _tmpl$42 = ["<div class=\"k2b-chat-composer__submit\">", "", "", "", "", "</div>"],
52
+ _tmpl$43 = ["<span>", "</span>"],
53
+ _tmpl$44 = ["<div class=\"k2b-chat-composer__status-line\"", "><i aria-hidden=\"true\"", "></i><span>", "</span></div>"],
54
+ _tmpl$45 = ["<span class=\"k2b-sr-only\">", "</span>"];
55
+ import { batch, children, createEffect, createMemo, createSignal, createUniqueId, For, onCleanup, onMount, Show, untrack } from "solid-js";
47
56
  import { Dropdown } from "../actions/Dropdown";
57
+ import { dialogHistorySettled, holdDialogHistory } from "../feedback/dialog-core";
48
58
  import { Tooltip } from "../feedback/Tooltip";
49
59
  import { replaceTextareaRange } from "../inputs/editor-dom";
50
60
  import { completedEmojiShortcode, emojiInTone, emojiName, emojiShortcodeQuery, loadEmojiIndex, suggestEmoji } from "../inputs/emoji/emoji-index";
@@ -57,14 +67,25 @@ import { useSelectMessages } from "../inputs/select-messages";
57
67
  import { useLocale } from "../intl/locale";
58
68
  import { useUiMessages } from "../intl/messages";
59
69
  import { ChatContextUsage as ContextUsage } from "./ChatPrimitives";
60
- import { conversationEnterAction, executeChatAction, nextChatCommandIndex, reportChatFailure, runChatSubmission } from "./chat-behavior";
70
+ import { chatCommandKey, conversationEnterAction, executeChatAction, nextChatCommandIndex, reportChatFailure, runChatSubmission } from "./chat-behavior";
61
71
  import { useChatMessages } from "./chat-messages";
72
+ import { chatCommandListModel, parseChatCommandQuery } from "./command-search";
62
73
  import { chatCommandQuery, chatMentionSegments, reconcileChatMentions } from "./composer-document";
63
74
  const composerMaxInputHeight = 309;
75
+ const commandDoneMs = 3_000;
64
76
  /** Emoji suggestions for a `:shortcode`, as many as fit the list without scrolling on a phone. */
65
77
  const emojiSuggestionLimit = 8;
66
78
  // Keep editor undo text within approximately 2 MiB, plus the current edit.
67
79
  const composerUndoBudgetChars = 1_000_000;
80
+
81
+ /** An action can cancel and restore its token, or report a short completion message. */
82
+
83
+ /** A fixed place in the slash list. Sections keep the given order, also while searching. */
84
+
85
+ /** First-word narrowing; Tab writes `words[0]` followed by a space. */
86
+
87
+ /** Passed as the third argument to `searchCommands`. */
88
+
68
89
  /** The row stays short: what a chat message needs. Other Markdown, typed by hand, keeps working. */
69
90
  const formatTools = [{
70
91
  id: "bold",
@@ -95,21 +116,80 @@ const isTouchOnly = () => typeof matchMedia === "function" && matchMedia(touchOn
95
116
  const microphoneHoldMs = 500;
96
117
  /** Keeps the field focused (and a phone's keyboard open) when a composer button is pressed with a mouse or finger. */
97
118
  const keepFieldFocus = event => event.preventDefault();
119
+ /**
120
+ * Keeps one tap from reaching the page: its pointer, touch and mouse events stop at the window, and neither focus nor a
121
+ * click happens. A finger's tap ends with `touchend`, a scroll with `pointercancel`, a mouse's press with `click`.
122
+ */
123
+ const swallowTap = down => {
124
+ const blocked = ["pointerup", "pointercancel", "touchstart", "touchend", "mousedown", "mouseup", "click", "contextmenu"];
125
+ const stop = () => {
126
+ window.removeEventListener("pointerdown", next, true);
127
+ for (const type of blocked) window.removeEventListener(type, block, true);
128
+ };
129
+ const block = event => {
130
+ event.stopPropagation();
131
+ if (event.type !== "pointercancel" && event.type !== "touchstart" && event.type !== "pointerup") event.preventDefault();
132
+ if (event.type === "click" || event.type === "touchend" || event.type === "pointercancel") stop();
133
+ };
134
+ // The next press is a new gesture, whatever became of this one.
135
+ const next = event => {
136
+ if (event !== down) stop();
137
+ };
138
+ down.stopPropagation();
139
+ down.preventDefault();
140
+ window.addEventListener("pointerdown", next, true);
141
+ for (const type of blocked) window.addEventListener(type, block, {
142
+ capture: true,
143
+ passive: false
144
+ });
145
+ };
98
146
  const attachmentIcon = attachment => attachment.icon ?? (attachment.kind === "image" ? "ti ti-photo" : attachment.kind === "resource" ? "ti ti-link" : "ti ti-file");
99
147
  const hasContent = items => items.some(item => item != null && typeof item !== "boolean" && item !== "");
100
148
  export function ChatComposer(props) {
101
149
  const accessory = children(() => props.accessory);
102
150
  const hasAccessory = () => hasContent(accessory.toArray());
103
151
  const hint = children(() => props.hint);
152
+ const footer = children(() => props.footerContent);
104
153
  const messages = useUiMessages();
105
154
  const markdownToolbarMessages = useMarkdownToolbarMessages();
106
155
  const chatMessages = useChatMessages();
107
156
  const selectMessages = useSelectMessages();
108
157
  const commandListId = `k2b-chat-commands-${createUniqueId().replace(/[^A-Za-z0-9_-]/g, "-")}`;
109
158
  const formatGroupId = `${commandListId}-format`;
159
+ // Emoji suggestions keep their index-based selection and ids.
110
160
  const [selectedCommandIndex, setSelectedCommandIndex] = createSignal(0);
161
+ const [activeCommandKey, setActiveCommandKey] = createSignal();
162
+ const [commandMoved, setCommandMoved] = createSignal(false);
163
+ const commandIds = new Map();
164
+ let commandIdCounter = 0;
165
+ const commandOptionId = command => {
166
+ const key = chatCommandKey(command);
167
+ if (!commandIds.has(key)) commandIds.set(key, commandIdCounter++);
168
+ return `${commandListId}-c${commandIds.get(key)}`;
169
+ };
170
+ const [closedAt, setClosedAt] = createSignal();
171
+ // Where the pending native edit starts; a selection replaced as a whole starts at its beginning.
172
+ let inputStart;
173
+ let inputEnd;
174
+ const [commandStatus, setCommandStatus] = createSignal();
175
+ const [announcement, setAnnouncement] = createSignal();
176
+ let statusTimer;
177
+ let disposed = false;
178
+ const showCommandStatus = status => {
179
+ if (disposed) return;
180
+ clearTimeout(statusTimer);
181
+ setCommandStatus(status);
182
+ if (status?.tone === "done") {
183
+ statusTimer = setTimeout(() => setCommandStatus(undefined), commandDoneMs);
184
+ }
185
+ };
186
+ onCleanup(() => {
187
+ disposed = true;
188
+ clearTimeout(statusTimer);
189
+ });
111
190
  const [addingFiles, setAddingFiles] = createSignal(false);
112
191
  const [submitting, setSubmitting] = createSignal(false);
192
+ let shellRef;
113
193
  let composerRef;
114
194
  let textareaRef;
115
195
  let fileInputRef;
@@ -131,9 +211,51 @@ export function ChatComposer(props) {
131
211
  const [dismissed, setDismissed] = createSignal(false);
132
212
  const [composing, setComposing] = createSignal(false);
133
213
  const [searchResults, setSearchResults] = createSignal([]);
214
+ const [searchResultsQuery, setSearchResultsQuery] = createSignal();
215
+ const [searchResultsScope, setSearchResultsScope] = createSignal();
216
+ const [suggestionAnnouncement, setSuggestionAnnouncement] = createSignal();
134
217
  const [searching, setSearching] = createSignal(false);
135
218
  const [searchError, setSearchError] = createSignal(false);
136
219
  const [executing, setExecuting] = createSignal(false);
220
+ const [modelOpen, setModelOpen] = createSignal();
221
+ const [choosingModel, setChoosingModel] = createSignal(false);
222
+ let resolveModel;
223
+ const finishModelSelection = id => {
224
+ const resolve = resolveModel;
225
+ resolveModel = undefined;
226
+ if (choosingModel()) {
227
+ setModelOpen(false);
228
+ // Let the controlled close reach Dropdown before returning normal clicks to its own state.
229
+ queueMicrotask(() => {
230
+ if (!choosingModel()) setModelOpen(undefined);
231
+ });
232
+ }
233
+ setChoosingModel(false);
234
+ resolve?.(id);
235
+ };
236
+ const blockedBesidesCommand = () => Boolean(props.disabled || stopping() || state() === "submitting" || addingFiles() || submitting());
237
+ const usableModelControl = () => !conversation() && !footer() && !blockedBesidesCommand() && !running() && Boolean(props.models?.length && props.onModelChange);
238
+ const openModelSelection = () => {
239
+ if (!usableModelControl()) return Promise.resolve(null);
240
+ finishModelSelection(null);
241
+ return new Promise(resolve => {
242
+ resolveModel = resolve;
243
+ setChoosingModel(true);
244
+ setModelOpen(true);
245
+ queueMicrotask(() => {
246
+ if (!choosingModel() || !modelOpen()) return;
247
+ // Start on the current model, as a click on the chip would show it checked.
248
+ const menu = composerRef?.querySelector(".k2b-chat-composer__model");
249
+ (menu?.querySelector('[role="menuitemradio"][aria-checked="true"]:not(:disabled)') ?? menu?.querySelector('[role="menuitemradio"]:not(:disabled)'))?.focus();
250
+ });
251
+ });
252
+ };
253
+ createEffect(() => {
254
+ if (!usableModelControl()) finishModelSelection(null);
255
+ });
256
+ onCleanup(() => finishModelSelection(null));
257
+ const [commandOpening, setCommandOpening] = createSignal(0);
258
+ let retrySearch = false;
137
259
  let highlightRef;
138
260
  let commandListRef;
139
261
  const mentions = () => props.mentions ?? [];
@@ -150,27 +272,96 @@ export function ChatComposer(props) {
150
272
  return token && index ? suggestEmoji(index, token.query, emojiSuggestionLimit) : [];
151
273
  });
152
274
  const emojiOpen = () => emojiMatches().length > 0;
153
- const commandQuery = createMemo(() => dismissed() || composing() ? null : chatCommandQuery(props.value, caret(), selectionEnd()));
154
- const commandMatches = createMemo(() => {
155
- const token = commandQuery();
156
- if (!token) return [];
157
- return [...(props.commands ?? []).filter(command => command.name.toLowerCase().includes(token.query.toLowerCase())), ...searchResults()];
275
+ const caretCommandToken = createMemo(() => chatCommandQuery(props.value, caret(), selectionEnd()));
276
+ const caretCommandStart = createMemo(() => caretCommandToken()?.start);
277
+ // Retained server rows belong to one `/`: they go when it does or when another `/` starts.
278
+ createEffect(() => {
279
+ caretCommandStart();
280
+ setSearchResults([]);
281
+ setSearchResultsQuery(undefined);
282
+ });
283
+ // Escape closes the list for one `/` character, wherever the caret or selection goes; only that `/` going away ends it.
284
+ createEffect(() => {
285
+ const closed = closedAt();
286
+ if (closed !== undefined && props.value[closed] !== "/") setClosedAt(undefined);
287
+ });
288
+ const commandQuery = createMemo(() => {
289
+ const token = caretCommandToken();
290
+ return dismissed() || composing() || token?.start === closedAt() ? null : token;
291
+ });
292
+ const openingStart = createMemo(() => commandQuery()?.start);
293
+ createEffect(() => {
294
+ props.draftKey;
295
+ const start = openingStart();
296
+ if (start !== undefined) setCommandOpening(opening => opening + 1);
297
+ });
298
+ const parsedCommandQuery = createMemo(() => parseChatCommandQuery(commandQuery()?.query ?? "", props.commandScopes ?? []));
299
+ const currentCommandScope = createMemo(() => parsedCommandQuery().scope?.id);
300
+ createEffect(() => {
301
+ currentCommandScope();
302
+ setSearchResults([]);
303
+ setSearchResultsQuery(undefined);
304
+ });
305
+ const localCommands = createMemo(() => props.commands ?? []);
306
+ const commandModel = createMemo(() => chatCommandListModel({
307
+ query: commandQuery()?.query ?? "",
308
+ scopes: props.commandScopes,
309
+ sections: props.commandSections,
310
+ commands: localCommands(),
311
+ searchResults: searchResultsScope() === parsedCommandQuery().scope?.id ? searchResults() : [],
312
+ searchResultsFresh: searchResultsQuery() === commandQuery()?.query && searchResultsScope() === parsedCommandQuery().scope?.id,
313
+ searchSettled: searchResultsQuery() !== undefined && searchResultsScope() === parsedCommandQuery().scope?.id,
314
+ searching: searching()
315
+ }));
316
+ const commandMatches = createMemo(() => commandQuery() ? commandModel().rows.map(row => row.command) : []);
317
+ const currentCommandQuery = createMemo(() => caretCommandToken()?.query);
318
+ // Arrow marking belongs to one token in one draft; another draft or `/` starts at the top row again.
319
+ createEffect(() => {
320
+ props.draftKey;
321
+ caretCommandStart();
322
+ currentCommandQuery();
323
+ setCommandMoved(false);
324
+ });
325
+ const commandsOpen = () => Boolean(!emojiOpen() && commandQuery() && (localCommands().length || props.searchCommands));
326
+ const commandSheetHeld = createMemo(() => commandsOpen() && isTouchOnly());
327
+ createEffect(() => {
328
+ if (!commandSheetHeld()) return;
329
+ onCleanup(holdDialogHistory(() => setClosedAt(commandQuery()?.start)));
158
330
  });
159
- const commandsOpen = () => Boolean(!emojiOpen() && commandQuery() && (props.commands?.length || props.searchCommands));
160
331
  const suggestionsOpen = () => commandsOpen() || emojiOpen();
161
332
  createEffect(() => {
162
333
  if (!suggestionsOpen()) return;
163
334
  let disposed = false;
164
335
  const measure = () => {
165
336
  if (disposed || !commandListRef) return;
166
- let top = window.visualViewport?.offsetTop ?? 0;
167
- for (let parent = commandListRef.parentElement; parent; parent = parent.parentElement) {
168
- if (/(auto|scroll|hidden|clip)/.test(window.getComputedStyle(parent).overflowY)) {
337
+ const accessory = commandListRef.parentElement;
338
+ const viewportTop = window.visualViewport?.offsetTop ?? 0;
339
+ let top = viewportTop;
340
+ // The column a phone sheet spans: the nearest ancestor that clips sideways, or the page.
341
+ let column;
342
+ for (let parent = accessory.parentElement; parent; parent = parent.parentElement) {
343
+ const style = window.getComputedStyle(parent);
344
+ if (/(auto|scroll|hidden|clip)/.test(style.overflowY)) {
169
345
  top = Math.max(top, parent.getBoundingClientRect().top);
170
346
  }
347
+ if (!column && /(auto|scroll|hidden|clip)/.test(style.overflowX)) {
348
+ const left = parent.getBoundingClientRect().left + parent.clientLeft;
349
+ column = {
350
+ left,
351
+ right: left + parent.clientWidth
352
+ };
353
+ }
171
354
  }
172
- const bottom = commandListRef.parentElement.getBoundingClientRect().bottom;
355
+ const page = document.documentElement.clientWidth;
356
+ const left = Math.max(0, column?.left ?? 0);
357
+ const right = Math.min(page, column?.right ?? page);
358
+ const slot = accessory.parentElement.getBoundingClientRect();
359
+ const bottom = accessory.getBoundingClientRect().bottom;
173
360
  commandListRef.style.setProperty("--k2b-chat-command-space", `${Math.max(40, bottom - top - 8)}px`);
361
+ // Only a list that reaches the screen's top edge keeps clear of a notch or the status bar.
362
+ commandListRef.style.setProperty("--k2b-chat-command-safe-top", top === viewportTop ? "env(safe-area-inset-top, 0px)" : "0px");
363
+ accessory.style.setProperty("--k2b-chat-command-reach-left", `${Math.max(0, slot.left - left)}px`);
364
+ accessory.style.setProperty("--k2b-chat-command-reach-right", `${Math.max(0, right - slot.right)}px`);
174
365
  };
175
366
  queueMicrotask(measure);
176
367
  window.addEventListener("resize", measure);
@@ -183,30 +374,108 @@ export function ChatComposer(props) {
183
374
  window.visualViewport?.removeEventListener("resize", measure);
184
375
  });
185
376
  });
377
+ // A press outside the command list and the field closes the list, as Escape does; the text stays. With a mouse or a
378
+ // trackpad the press then acts as usual. On a touch-only device a tap outside the composer closes only the list: it
379
+ // reaches nothing beneath, and the field keeps focus, so the keyboard stays open.
380
+ createEffect(() => {
381
+ if (!commandsOpen()) return;
382
+ const onPointerDown = event => {
383
+ const target = event.target;
384
+ if (!(target instanceof Node) || commandListRef?.contains(target) || textareaRef?.parentElement?.contains(target)) return;
385
+ setClosedAt(commandQuery()?.start);
386
+ if (isTouchOnly() && !shellRef?.contains(target)) swallowTap(event);
387
+ };
388
+ window.addEventListener("pointerdown", onPointerDown, true);
389
+ onCleanup(() => window.removeEventListener("pointerdown", onPointerDown, true));
390
+ });
186
391
  createEffect(() => {
392
+ props.draftKey;
187
393
  const token = commandQuery();
188
- setSearchResults([]);
189
394
  setSearchError(false);
190
395
  if (!token || !props.searchCommands) {
191
396
  setSearching(false);
192
397
  return;
193
398
  }
399
+ running();
400
+ const parsed = parsedCommandQuery();
401
+ const opening = commandOpening();
194
402
  const abort = new AbortController();
195
403
  setSearching(true);
404
+ const delay = retrySearch ? 0 : 120;
405
+ retrySearch = false;
196
406
  const timer = setTimeout(() => {
197
- Promise.resolve(props.searchCommands(token.query, abort.signal)).then(results => {
198
- if (!abort.signal.aborted) setSearchResults(results);
407
+ Promise.resolve().then(() => props.searchCommands(parsed.query, abort.signal, {
408
+ scope: parsed.scope?.id,
409
+ opening
410
+ })).then(results => {
411
+ if (!abort.signal.aborted) {
412
+ batch(() => {
413
+ setSearchResults(results);
414
+ setSearchResultsQuery(token.query);
415
+ setSearchResultsScope(parsed.scope?.id);
416
+ });
417
+ }
199
418
  }).catch(() => {
200
- if (!abort.signal.aborted) setSearchError(true);
419
+ if (!abort.signal.aborted) {
420
+ setSearchResults([]);
421
+ setSearchResultsQuery(undefined);
422
+ setSearchError(true);
423
+ }
201
424
  }).finally(() => {
202
425
  if (!abort.signal.aborted) setSearching(false);
203
426
  });
204
- }, 120);
427
+ }, delay);
205
428
  onCleanup(() => {
206
429
  clearTimeout(timer);
207
430
  abort.abort();
208
431
  });
209
432
  });
433
+ let settledEmpty;
434
+ createEffect(() => {
435
+ const token = commandQuery();
436
+ if (!token || parsedCommandQuery().scope || searchError()) {
437
+ settledEmpty = undefined;
438
+ return;
439
+ }
440
+ const previous = settledEmpty;
441
+ if (/\s$/.test(token.query) && previous?.start === token.start && previous.query.trimEnd() === token.query.trimEnd()) {
442
+ setClosedAt(token.start);
443
+ return;
444
+ }
445
+ const settled = !searching() && (!props.searchCommands || searchResultsQuery() === token.query);
446
+ if (settled && commandModel().empty) {
447
+ settledEmpty = {
448
+ start: token.start,
449
+ query: token.query
450
+ };
451
+ if (/\s$/.test(token.query)) setClosedAt(token.start);
452
+ } else if (!searching()) settledEmpty = undefined;
453
+ });
454
+ let lastSuggestion;
455
+ createEffect(() => {
456
+ const model = commandModel();
457
+ const query = commandQuery()?.query;
458
+ if (!commandsOpen()) {
459
+ setSuggestionAnnouncement(undefined);
460
+ lastSuggestion = undefined;
461
+ return;
462
+ }
463
+ if (searching() || searchError() || props.searchCommands && searchResultsQuery() !== query) return;
464
+ const text = chatMessages().commandSuggestions({
465
+ count: model.rows.length
466
+ });
467
+ const timer = setTimeout(() => {
468
+ if (lastSuggestion && lastSuggestion.query === query && lastSuggestion.text === text) return;
469
+ lastSuggestion = {
470
+ query: query ?? "",
471
+ text
472
+ };
473
+ setSuggestionAnnouncement({
474
+ text
475
+ });
476
+ }, 350);
477
+ onCleanup(() => clearTimeout(timer));
478
+ });
210
479
  const history = [];
211
480
  let historyIndex = -1;
212
481
  createEffect(() => {
@@ -214,7 +483,13 @@ export function ChatComposer(props) {
214
483
  untrack(() => {
215
484
  history.length = 0;
216
485
  historyIndex = -1;
486
+ finishModelSelection(null);
217
487
  setDismissed(true);
488
+ setClosedAt(undefined);
489
+ setSearchResults([]);
490
+ setSearchResultsQuery(undefined);
491
+ // A failure belongs to the draft it happened in; success notes may follow a command into the chat it opened.
492
+ if (commandStatus()?.tone === "error") showCommandStatus();
218
493
  });
219
494
  });
220
495
  const snapshot = () => ({
@@ -233,6 +508,23 @@ export function ChatComposer(props) {
233
508
  }
234
509
  };
235
510
  const edit = (value, nextMentions = reconcileChatMentions(props.value, value, mentions())) => {
511
+ if (commandStatus()?.tone === "error") showCommandStatus();
512
+ // Replacing `/` can leave its position and query identical, but starts a new opening.
513
+ if (inputStart !== undefined && inputEnd !== undefined && props.value.slice(inputStart, inputEnd).includes("/")) {
514
+ setCommandOpening(opening => opening + 1);
515
+ setSearchResults([]);
516
+ setSearchResultsQuery(undefined);
517
+ setCommandMoved(false);
518
+ }
519
+ // An edit that starts at or before the closed `/` (select all, then type `/`) replaces it; the new `/` may open again.
520
+ const closed = closedAt();
521
+ if (closed !== undefined) {
522
+ let same = 0;
523
+ while (same <= closed && props.value[same] === value[same]) same++;
524
+ if (Math.min(same, inputStart ?? same) <= closed) setClosedAt(undefined);
525
+ }
526
+ inputStart = undefined;
527
+ inputEnd = undefined;
236
528
  record();
237
529
  props.onValueChange(value);
238
530
  props.onMentionsChange?.(nextMentions);
@@ -252,16 +544,33 @@ export function ChatComposer(props) {
252
544
  });
253
545
  };
254
546
  const syncCaret = () => {
255
- setCaret(textareaRef?.selectionStart ?? props.value.length);
256
- setSelectionEnd(textareaRef?.selectionEnd ?? props.value.length);
547
+ // One update, so no effect sees a half-moved selection (it would read as "no `/` here" and reopen a closed list).
548
+ batch(() => {
549
+ setCaret(textareaRef?.selectionStart ?? props.value.length);
550
+ setSelectionEnd(textareaRef?.selectionEnd ?? props.value.length);
551
+ });
257
552
  };
258
- const selectedCommand = () => commandMatches()[selectedCommandIndex()];
553
+ const selectedCommand = createMemo(() => {
554
+ const rows = commandMatches();
555
+ return commandMoved() && rows.find(command => chatCommandKey(command) === activeCommandKey()) || rows[0];
556
+ });
557
+ const selectedCommandKey = createMemo(() => {
558
+ const command = selectedCommand();
559
+ return command ? chatCommandKey(command) : undefined;
560
+ });
561
+ createEffect(() => {
562
+ if (!commandsOpen()) commandIds.clear();
563
+ selectedCommandKey();
564
+ setAnnouncement(undefined);
565
+ });
259
566
  const busy = () => Boolean(props.disabled || stopping() || state() === "submitting" || executing() || addingFiles());
260
567
  const blocked = () => busy() || submitting();
261
568
  /** The conversation field stays editable while a message is sent, so it keeps the focus and a phone its keyboard. */
262
569
  const editBlocked = () => conversation() ? busy() : blocked();
263
570
  const hasDraft = () => Boolean(props.value.trim() || !running() && attachments().length > 0);
264
- const canSubmit = () => !blocked() && hasDraft() && !(running() && runningSubmitIntent() === "steer" && mentions().length);
571
+ /** The draft may go out now, apart from a submission that is already on its way. */
572
+ const submittable = () => !busy() && hasDraft() && !(running() && runningSubmitIntent() === "steer" && mentions().length);
573
+ const canSubmit = () => !submitting() && submittable();
265
574
  const canSelectFiles = () => Boolean(props.fileSelection && !props.fileSelection.disabled && !running() && !blocked());
266
575
  const hasAddMenu = () => Boolean(props.fileSelection || props.menuActions?.length);
267
576
  const hasContextUsage = () => {
@@ -318,21 +627,23 @@ export function ChatComposer(props) {
318
627
  }
319
628
  });
320
629
  createEffect(() => {
321
- commandMatches();
322
630
  emojiMatches();
323
631
  setSelectedCommandIndex(0);
324
632
  });
325
633
  createEffect(() => {
326
- const index = selectedCommandIndex();
327
- commandMatches();
328
- emojiMatches();
634
+ const command = selectedCommand();
635
+ const id = emojiOpen() ? `${commandListId}-${selectedCommandIndex()}` : command ? commandOptionId(command) : undefined;
329
636
  queueMicrotask(() => {
330
637
  const list = commandListRef;
331
- const item = list?.querySelector(`[id="${commandListId}-${index}"]`);
638
+ const item = id ? list?.querySelector(`[id="${id}"]`) : undefined;
332
639
  if (!list || !item?.isConnected) return;
333
- if (item.offsetTop < list.scrollTop) list.scrollTop = item.offsetTop;else if (item.offsetTop + item.offsetHeight > list.scrollTop + list.clientHeight) {
334
- list.scrollTop = item.offsetTop + item.offsetHeight - list.clientHeight;
335
- }
640
+ // Measured against the visible part of the list: a sticky section heading covers the top of it.
641
+ const heading = item.closest(".k2b-chat-composer__command-group")?.querySelector(".k2b-chat-composer__command-heading");
642
+ const listTop = list.getBoundingClientRect().top + list.clientTop;
643
+ const box = item.getBoundingClientRect();
644
+ const top = box.top - listTop - (heading?.offsetHeight ?? 0);
645
+ const bottom = box.bottom - listTop - list.clientHeight;
646
+ if (top < 0) list.scrollTop += top;else if (bottom > 0) list.scrollTop += bottom;
336
647
  });
337
648
  });
338
649
  createEffect(() => {
@@ -379,18 +690,26 @@ export function ChatComposer(props) {
379
690
  };
380
691
  const submit = async () => {
381
692
  if (!canSubmit()) return;
382
- const intent = running() ? runningSubmitIntent() : "send";
383
- const previousValue = props.value;
384
- const previousAttachments = attachments();
385
- const previousMentions = mentions();
386
- const input = {
387
- intent,
388
- text: previousValue,
389
- mentions: previousMentions,
390
- attachments: intent !== "steer" ? previousAttachments : []
391
- };
693
+ const key = props.draftKey;
392
694
  setSubmitting(true);
393
695
  try {
696
+ if (isTouchOnly()) {
697
+ if (commandSheetHeld()) setClosedAt(commandQuery()?.start);
698
+ // The sheet's Back entry leaves first, so a submit handler that navigates finds the history settled.
699
+ await dialogHistorySettled();
700
+ // A draft that another one replaced, or that may no longer go out, is not sent and keeps its text.
701
+ if (disposed || props.draftKey !== key || !submittable()) return;
702
+ }
703
+ const intent = running() ? runningSubmitIntent() : "send";
704
+ const previousValue = props.value;
705
+ const previousAttachments = attachments();
706
+ const previousMentions = mentions();
707
+ const input = {
708
+ intent,
709
+ text: previousValue,
710
+ mentions: previousMentions,
711
+ attachments: intent !== "steer" ? previousAttachments : []
712
+ };
394
713
  await runChatSubmission({
395
714
  clear: () => {
396
715
  props.onValueChange("");
@@ -422,9 +741,32 @@ export function ChatComposer(props) {
422
741
  });
423
742
  }
424
743
  };
425
- const executeCommand = async command => {
744
+ const narrowCommand = command => {
745
+ const scope = props.commandScopes?.find(scope => scope.id === command.scope);
746
+ const word = scope?.words[0];
747
+ const token = commandQuery();
748
+ if (!word || !token || !textareaRef) return false;
749
+ replaceTextareaRange(textareaRef, token.start + 1, token.end, `${word} `);
750
+ syncCaret();
751
+ return true;
752
+ };
753
+ const executeCommand = async (command, tab = false) => {
426
754
  const token = commandQuery();
427
- if (!token || command.disabled || blocked()) return;
755
+ const heldHistory = commandSheetHeld();
756
+ if (!token || blocked()) return;
757
+ if (command.disabled || command.reason) {
758
+ // A new object each time, so choosing the same row again inserts and announces its reason again.
759
+ setAnnouncement(command.reason ? {
760
+ text: command.reason
761
+ } : undefined);
762
+ return;
763
+ }
764
+ if (command.searchAgain) {
765
+ retrySearch = true;
766
+ setCommandOpening(opening => opening + 1);
767
+ return;
768
+ }
769
+ if (command.scope && (tab || !command.action && !command.mention) && narrowCommand(command)) return;
428
770
  const previous = snapshot();
429
771
  const key = props.draftKey;
430
772
  const replacement = command.mention ? command.mention.name + " " : "";
@@ -435,32 +777,73 @@ export function ChatComposer(props) {
435
777
  end: token.start + command.mention.name.length,
436
778
  attachment: command.mention
437
779
  });
780
+ // A cancelled or failed command leaves no undo step behind: restoring puts the history back as well.
781
+ const previousHistory = {
782
+ entries: [...history],
783
+ index: historyIndex
784
+ };
438
785
  edit(nextValue, nextMentions);
439
786
  setDismissed(true);
440
787
  setExecuting(true);
441
788
  let submitRequested = false;
789
+ let restored = false;
790
+ const restore = () => {
791
+ if (props.draftKey !== key || props.value !== nextValue) return;
792
+ setDismissed(true);
793
+ props.onValueChange(previous.value);
794
+ props.onMentionsChange?.(previous.mentions);
795
+ history.splice(0, history.length, ...previousHistory.entries);
796
+ historyIndex = previousHistory.index;
797
+ restored = true;
798
+ };
799
+ showCommandStatus(command.busyLabel ? {
800
+ tone: "busy",
801
+ text: command.busyLabel
802
+ } : undefined);
442
803
  try {
443
- await command.action?.({
804
+ // The sheet's Back entry leaves first, so an action that navigates or opens a dialog finds the history settled.
805
+ if (heldHistory && command.action) {
806
+ await dialogHistorySettled();
807
+ // Another draft or no composer any more: the command does not run against it.
808
+ if (disposed || props.draftKey !== key) return;
809
+ }
810
+ const result = await command.action?.({
444
811
  setValue: value => edit(value),
445
812
  submit: () => {
446
813
  submitRequested = true;
447
814
  },
448
- focus
815
+ focus,
816
+ openModelSelection
449
817
  });
818
+ if (result === "cancelled") {
819
+ submitRequested = false;
820
+ restore();
821
+ showCommandStatus();
822
+ } else if (result) {
823
+ showCommandStatus({
824
+ tone: "done",
825
+ text: result.done
826
+ });
827
+ }
450
828
  } catch (error) {
451
829
  submitRequested = false;
452
- if (props.draftKey === key && props.value === nextValue) {
453
- props.onValueChange(previous.value);
454
- props.onMentionsChange?.(previous.mentions);
455
- }
456
- props.onError?.(error);
830
+ restore();
831
+ // An unmounted composer has no status line left; the app's error channel keeps the failure from going silent.
832
+ if (disposed) props.onError?.(error);else showCommandStatus({
833
+ tone: "error",
834
+ text: error instanceof Error && error.message ? error.message : chatMessages().commandFailed
835
+ });
457
836
  } finally {
837
+ if (commandStatus()?.tone === "busy") showCommandStatus();
458
838
  setExecuting(false);
459
839
  }
460
840
  if (submitRequested && props.draftKey === key) await submit();
461
841
  queueMicrotask(() => {
462
842
  if (props.draftKey !== key) return;
463
- textareaRef?.setSelectionRange(token.start + replacement.length, token.start + replacement.length);
843
+ if (restored || props.value === nextValue) {
844
+ const position = restored ? token.end : token.start + replacement.length;
845
+ textareaRef?.setSelectionRange(position, position);
846
+ }
464
847
  syncCaret();
465
848
  autoResize();
466
849
  focus();
@@ -538,19 +921,28 @@ export function ChatComposer(props) {
538
921
  if (commandsOpen()) {
539
922
  if (event.key === "ArrowUp" || event.key === "ArrowDown") {
540
923
  event.preventDefault();
541
- setSelectedCommandIndex(index => nextChatCommandIndex(index, matches.length, event.key === "ArrowUp" ? -1 : 1));
924
+ const index = matches.indexOf(selectedCommand());
925
+ const next = matches[nextChatCommandIndex(index, matches.length, event.key === "ArrowUp" ? -1 : 1)];
926
+ setActiveCommandKey(next ? chatCommandKey(next) : undefined);
927
+ setCommandMoved(true);
928
+ setAnnouncement(undefined);
542
929
  return;
543
930
  }
544
931
  // Ctrl/⌘+Enter sends, also while suggestions are open.
545
- if ((event.key === "Tab" || event.key === "Enter" && !event.ctrlKey && !event.metaKey) && matches.length > 0) {
932
+ const plain = !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey;
933
+ if (plain && (event.key === "Tab" || event.key === "Enter") && matches.length > 0) {
546
934
  event.preventDefault();
547
935
  const command = selectedCommand();
548
- if (command) void executeCommand(command);
936
+ // A server row answering an earlier query may no longer match; wait for fresh results. Local rows match now.
937
+ const row = commandModel().rows.find(row => chatCommandKey(row.command) === selectedCommandKey());
938
+ const staleServerRow = row?.server && (searchResultsQuery() !== commandQuery()?.query || searchResultsScope() !== parsedCommandQuery().scope?.id);
939
+ if (staleServerRow) return;
940
+ if (command) void executeCommand(command, event.key === "Tab");
549
941
  return;
550
942
  }
551
943
  if (event.key === "Escape") {
552
944
  event.preventDefault();
553
- setDismissed(true);
945
+ setClosedAt(commandQuery()?.start);
554
946
  return;
555
947
  }
556
948
  }
@@ -964,7 +1356,7 @@ export function ChatComposer(props) {
964
1356
  children: part => _$ssr(_tmpl$10, Boolean(part.mention) ? "k2b-chat-composer__mention" : "", _$escape(part.text))
965
1357
  })));
966
1358
  }
967
- })), mentions().length > 0 ? "k2b-chat-composer__textarea--highlighted" : "", _$ssrAttribute("value", _$escape(props.value, true), false), _$ssrAttribute("disabled", editBlocked(), true) + _$ssrAttribute("enterkeyhint", conversation() ? "enter" : _$escape(undefined, true), false) + _$ssrAttribute("placeholder", _$escape(props.placeholder ?? (running() ? chatMessages().addGuidance : conversation() ? chatMessages().writeConversationMessage : chatMessages().writeMessage), true), false) + _$ssrAttribute("aria-label", _$escape(props.inputLabel ?? messages().message, true), false) + _$ssrAttribute("role", suggestionsOpen() ? "combobox" : _$escape(undefined, true), false) + _$ssrAttribute("aria-autocomplete", suggestionsOpen() ? "list" : _$escape(undefined, true), false) + _$ssrAttribute("aria-controls", suggestionsOpen() ? _$escape(commandListId, true) : _$escape(undefined, true), false) + _$ssrAttribute("aria-expanded", suggestionsOpen() ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("aria-activedescendant", (emojiOpen() ? emojiMatches()[selectedCommandIndex()] : selectedCommand()) ? `${_$escape(commandListId, true)}-${_$escape(selectedCommandIndex(), true)}` : _$escape(undefined, true), false), _$escape(_$createComponent(Show, {
1359
+ })), mentions().length > 0 ? "k2b-chat-composer__textarea--highlighted" : "", _$ssrAttribute("value", _$escape(props.value, true), false), _$ssrAttribute("disabled", editBlocked(), true) + _$ssrAttribute("enterkeyhint", conversation() ? "enter" : _$escape(undefined, true), false) + _$ssrAttribute("placeholder", _$escape(props.placeholder ?? (running() ? chatMessages().addGuidance : conversation() ? chatMessages().writeConversationMessage : chatMessages().writeMessage), true), false) + _$ssrAttribute("aria-label", _$escape(props.inputLabel ?? messages().message, true), false) + _$ssrAttribute("role", suggestionsOpen() ? "combobox" : _$escape(undefined, true), false) + _$ssrAttribute("aria-autocomplete", suggestionsOpen() ? "list" : _$escape(undefined, true), false) + _$ssrAttribute("aria-controls", suggestionsOpen() ? _$escape(commandListId, true) : _$escape(undefined, true), false) + _$ssrAttribute("aria-expanded", suggestionsOpen() ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("aria-activedescendant", emojiOpen() ? `${_$escape(commandListId, true)}-${_$escape(selectedCommandIndex(), true)}` : selectedCommand() ? _$escape(commandOptionId(selectedCommand()), true) : _$escape(undefined, true), false), _$escape(_$createComponent(Show, {
968
1360
  get when() {
969
1361
  return conversation();
970
1362
  },
@@ -972,25 +1364,54 @@ export function ChatComposer(props) {
972
1364
  return _$ssr(_tmpl$0);
973
1365
  }
974
1366
  })));
975
- return _$ssr(_tmpl$21, _$escape(_$createComponent(Show, {
1367
+ const commandRowMap = createMemo(() => new Map(commandModel().rows.map(row => [row.command, row])));
1368
+ const commandGroupKey = group => group.scopeTitle ? "scope" : group.section ? `section:${group.section.id}` : "flat";
1369
+ const renderCommandRow = command => {
1370
+ const highlight = () => commandRowMap().get(command)?.highlight;
1371
+ const title = () => command.label ?? command.name;
1372
+ return _$ssr(_tmpl$13, _$ssrAttribute("id", _$escape(commandOptionId(command), true), false), _$ssrAttribute("aria-selected", _$escape(selectedCommand(), true) === _$escape(command, true), false) + _$ssrAttribute("aria-disabled", _$escape(Boolean(command.disabled || command.reason), true), false) + _$ssrAttribute("aria-describedby", command.reason ? `${_$escape(commandOptionId(command), true)}-reason` : _$escape(undefined, true), false) + _$ssrAttribute("data-reason", command.reason ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("data-active", selectedCommand() === command ? "true" : _$escape(undefined, true), false), _$ssrAttribute("class", _$escape(command.icon ?? "ti ti-slash", true), false), _$escape(_$createComponent(Show, {
1373
+ get when() {
1374
+ return highlight();
1375
+ },
1376
+ get fallback() {
1377
+ return title();
1378
+ },
1379
+ children: range => [title().slice(0, range()[0]), _$ssr(_tmpl$14, _$escape(title().slice(range()[0], range()[1]))), title().slice(range()[1])]
1380
+ })), _$escape(_$createComponent(Show, {
1381
+ get when() {
1382
+ return command.reason ?? command.description;
1383
+ },
1384
+ get children() {
1385
+ return _$ssr(_tmpl$11, _$ssrAttribute("id", command.reason ? `${_$escape(commandOptionId(command), true)}-reason` : _$escape(undefined, true), false), _$escape(command.reason ?? command.description));
1386
+ }
1387
+ })), _$escape(_$createComponent(Show, {
1388
+ get when() {
1389
+ return command.selected;
1390
+ },
1391
+ get children() {
1392
+ return _$ssr(_tmpl$12, _$escape(chatMessages().commandSelected));
1393
+ }
1394
+ })));
1395
+ };
1396
+ return _$ssr(_tmpl$25, _$escape(_$createComponent(Show, {
976
1397
  get when() {
977
1398
  return suggestionsOpen() || hasAccessory();
978
1399
  },
979
1400
  get children() {
980
- return _$ssr(_tmpl$16, _$ssrAttribute("data-overlay", hasAccessory() ? _$escape(undefined, true) : "true", false), _$ssrStyleProperty("visibility:", suggestionsOpen() ? "hidden" : _$escape(undefined, true)), _$ssrAttribute("inert", suggestionsOpen(), true), _$escape(accessory()), _$escape(_$createComponent(Show, {
1401
+ return _$ssr(_tmpl$20, _$ssrAttribute("data-overlay", hasAccessory() ? _$escape(undefined, true) : "true", false), _$ssrStyleProperty("visibility:", suggestionsOpen() ? "hidden" : _$escape(undefined, true)), _$ssrAttribute("inert", suggestionsOpen(), true), _$escape(accessory()), _$escape(_$createComponent(Show, {
981
1402
  get when() {
982
1403
  return emojiOpen();
983
1404
  },
984
1405
  get children() {
985
- return _$ssr(_tmpl$11, _$ssrAttribute("id", _$escape(commandListId, true), false), _$ssrAttribute("aria-label", _$escape(chatMessages().emojiSuggestions, true), false), _$escape(_$createComponent(For, {
1406
+ return _$ssr(_tmpl$15, _$ssrAttribute("id", _$escape(commandListId, true), false), _$ssrAttribute("aria-label", _$escape(chatMessages().emojiSuggestions, true), false), _$escape(_$createComponent(For, {
986
1407
  get each() {
987
1408
  return emojiMatches();
988
1409
  },
989
- children: (entry, index) => _$ssr(_tmpl$22, `${_$escape(commandListId, true)}-${_$escape(index(), true)}`, _$ssrAttribute("aria-selected", _$escape(index(), true) === _$escape(selectedCommandIndex(), true), false) + _$ssrAttribute("aria-label", _$escape(emojiName(entry, locale()), true), false) + _$ssrAttribute("data-active", index() === selectedCommandIndex() ? "true" : _$escape(undefined, true), false), _$escape(emojiInTone(entry, props.emoji?.skinTone ?? 0)), _$escape(emojiName(entry, locale())), _$escape(_$createComponent(Show, {
1410
+ children: (entry, index) => _$ssr(_tmpl$26, `${_$escape(commandListId, true)}-${_$escape(index(), true)}`, _$ssrAttribute("aria-selected", _$escape(index(), true) === _$escape(selectedCommandIndex(), true), false) + _$ssrAttribute("aria-label", _$escape(emojiName(entry, locale()), true), false) + _$ssrAttribute("data-active", index() === selectedCommandIndex() ? "true" : _$escape(undefined, true), false), _$escape(emojiInTone(entry, props.emoji?.skinTone ?? 0)), _$escape(emojiName(entry, locale())), _$escape(_$createComponent(Show, {
990
1411
  get when() {
991
1412
  return entry.shortcodes[0];
992
1413
  },
993
- children: code => _$ssr(_tmpl$23, _$escape(code()))
1414
+ children: code => _$ssr(_tmpl$27, _$escape(code()))
994
1415
  })))
995
1416
  })));
996
1417
  }
@@ -999,31 +1420,70 @@ export function ChatComposer(props) {
999
1420
  return commandsOpen();
1000
1421
  },
1001
1422
  get children() {
1002
- return _$ssr(_tmpl$15, _$ssrAttribute("id", _$escape(commandListId, true), false), _$ssrAttribute("aria-label", _$escape(chatMessages().commands, true), false), _$escape(_$createComponent(For, {
1423
+ return _$ssr(_tmpl$19, _$ssrAttribute("id", _$escape(commandListId, true), false), _$ssrAttribute("aria-label", _$escape(chatMessages().commands, true), false) + _$ssrAttribute("aria-busy", searching() ? "true" : _$escape(undefined, true), false), _$escape(_$createComponent(For, {
1003
1424
  get each() {
1004
- return commandMatches();
1425
+ return commandModel().groups.map(commandGroupKey);
1005
1426
  },
1006
- children: (command, index) => _$ssr(_tmpl$24, `${_$escape(commandListId, true)}-${_$escape(index(), true)}`, _$ssrAttribute("aria-selected", _$escape(index(), true) === _$escape(selectedCommandIndex(), true), false) + _$ssrAttribute("aria-disabled", _$escape(command.disabled, true), false) + _$ssrAttribute("data-active", index() === selectedCommandIndex() ? "true" : _$escape(undefined, true), false), _$ssrAttribute("class", _$escape(command.icon ?? "ti ti-slash", true), false), _$escape(command.label ?? `/${command.name}`), _$escape(command.description))
1427
+ children: key => {
1428
+ const group = createMemo(() => commandModel().groups.find(group => commandGroupKey(group) === key));
1429
+ const headingId = () => `${commandListId}-g-${key === "scope" ? "scope" : props.commandSections?.findIndex(section => section.id === group()?.section?.id)}`;
1430
+ const rows = () => group()?.rows.map(row => row.command) ?? [];
1431
+ return _$createComponent(Show, {
1432
+ when: key !== "flat",
1433
+ get fallback() {
1434
+ return _$createComponent(For, {
1435
+ get each() {
1436
+ return rows();
1437
+ },
1438
+ children: renderCommandRow
1439
+ });
1440
+ },
1441
+ get children() {
1442
+ return _$ssr(_tmpl$30, _$ssrAttribute("aria-labelledby", _$escape(headingId(), true), false), _$ssrAttribute("id", _$escape(headingId(), true), false), _$escape(group()?.scopeTitle ?? group()?.section?.label), _$escape(_$createComponent(Show, {
1443
+ get when() {
1444
+ return group()?.loading;
1445
+ },
1446
+ get children() {
1447
+ return _$ssr(_tmpl$28);
1448
+ }
1449
+ })), _$escape(_$createComponent(For, {
1450
+ get each() {
1451
+ return rows();
1452
+ },
1453
+ children: renderCommandRow
1454
+ })), _$escape(_$createComponent(Show, {
1455
+ get when() {
1456
+ return group()?.placeholder;
1457
+ },
1458
+ get children() {
1459
+ return _$ssr(_tmpl$29);
1460
+ }
1461
+ })));
1462
+ }
1463
+ });
1464
+ }
1007
1465
  })), _$escape(_$createComponent(Show, {
1008
1466
  get when() {
1009
- return searching();
1467
+ return _$memo(() => !!(searching() && commandMatches().length === 0))() && !commandModel().groups.some(group => group.section || group.scopeTitle);
1010
1468
  },
1011
1469
  get children() {
1012
- return _$ssr(_tmpl$12, _$escape(messages().loading));
1470
+ return _$ssr(_tmpl$16, _$escape(messages().loading));
1013
1471
  }
1014
1472
  })), _$escape(_$createComponent(Show, {
1015
1473
  get when() {
1016
1474
  return searchError();
1017
1475
  },
1018
1476
  get children() {
1019
- return _$ssr(_tmpl$13, _$escape(messages().error));
1477
+ return _$ssr(_tmpl$17, _$escape(messages().error));
1020
1478
  }
1021
1479
  })), _$escape(_$createComponent(Show, {
1022
1480
  get when() {
1023
1481
  return _$memo(() => !!(!searching() && !searchError()))() && commandMatches().length === 0;
1024
1482
  },
1025
1483
  get children() {
1026
- return _$ssr(_tmpl$14, _$escape(messages().noResults));
1484
+ return _$ssr(_tmpl$18, !parsedCommandQuery().query.trim() ? _$escape(messages().noResults) : _$escape(chatMessages().commandsNothingFound({
1485
+ query: parsedCommandQuery().query.trim()
1486
+ })));
1027
1487
  }
1028
1488
  })));
1029
1489
  }
@@ -1034,18 +1494,18 @@ export function ChatComposer(props) {
1034
1494
  return showHint();
1035
1495
  },
1036
1496
  get children() {
1037
- return _$ssr(_tmpl$17, _$escape(_$createComponent(Show, {
1497
+ return _$ssr(_tmpl$21, _$escape(_$createComponent(Show, {
1038
1498
  get when() {
1039
1499
  return dictationStatus();
1040
1500
  },
1041
1501
  get fallback() {
1042
- return _$ssr(_tmpl$25, _$escape(hint()));
1502
+ return _$ssr(_tmpl$31, _$escape(hint()));
1043
1503
  },
1044
- children: status => [_$ssr(_tmpl$26, _$ssrAttribute("data-state", _$escape(dictation() ?? undefined, true), false), _$ssrAttribute("class", _$escape(status().icon, true), false), _$escape(status().text)), _$createComponent(Show, {
1504
+ children: status => [_$ssr(_tmpl$32, _$ssrAttribute("data-state", _$escape(dictation() ?? undefined, true), false), _$ssrAttribute("class", _$escape(status().icon, true), false), _$escape(status().text)), _$createComponent(Show, {
1045
1505
  get when() {
1046
1506
  return _$memo(() => dictation() === "refined")() ? props.microphone?.onRestoreOriginal : undefined;
1047
1507
  },
1048
- children: restore => _$ssr(_tmpl$27, _$ssrAttribute("disabled", microphoneDisabled(), true), _$escape(chatMessages().restoreOriginal))
1508
+ children: restore => _$ssr(_tmpl$33, _$ssrAttribute("disabled", microphoneDisabled(), true), _$escape(chatMessages().restoreOriginal))
1049
1509
  })]
1050
1510
  })));
1051
1511
  }
@@ -1054,16 +1514,16 @@ export function ChatComposer(props) {
1054
1514
  return attachments().length > 0;
1055
1515
  },
1056
1516
  get children() {
1057
- return _$ssr(_tmpl$18, _$ssrAttribute("aria-label", _$escape(chatMessages().attachments, true), false), _$escape(_$createComponent(For, {
1517
+ return _$ssr(_tmpl$22, _$ssrAttribute("aria-label", _$escape(chatMessages().attachments, true), false), _$escape(_$createComponent(For, {
1058
1518
  get each() {
1059
1519
  return attachments();
1060
1520
  },
1061
- children: attachment => _$ssr(_tmpl$29, `k2b-chat-composer__attachment-content ${Boolean(attachment.action) ? "k2b-chat-composer__attachment-content--action" : ""}`, _$escape(_$createComponent(Show, {
1521
+ children: attachment => _$ssr(_tmpl$35, `k2b-chat-composer__attachment-content ${Boolean(attachment.action) ? "k2b-chat-composer__attachment-content--action" : ""}`, _$escape(_$createComponent(Show, {
1062
1522
  get when() {
1063
1523
  return attachment.href;
1064
1524
  },
1065
1525
  get fallback() {
1066
- return _$ssr(_tmpl$31, _$escape(_$createComponent(Show, {
1526
+ return _$ssr(_tmpl$37, _$escape(_$createComponent(Show, {
1067
1527
  get when() {
1068
1528
  return _$memo(() => attachment.kind === "image")() && attachment.previewUrl;
1069
1529
  },
@@ -1071,11 +1531,11 @@ export function ChatComposer(props) {
1071
1531
  return _$ssr(_tmpl$3, _$ssrAttribute("class", _$escape(attachmentIcon(attachment), true), false));
1072
1532
  },
1073
1533
  get children() {
1074
- return _$ssr(_tmpl$30, _$ssrAttribute("src", _$escape(attachment.previewUrl, true), false) + _$ssrAttribute("alt", _$escape(attachment.alt ?? "", true), false));
1534
+ return _$ssr(_tmpl$36, _$ssrAttribute("src", _$escape(attachment.previewUrl, true), false) + _$ssrAttribute("alt", _$escape(attachment.alt ?? "", true), false));
1075
1535
  }
1076
1536
  })), _$escape(renderAttachmentCopy(attachment)));
1077
1537
  },
1078
- children: href => _$ssr(_tmpl$32, _$ssrAttribute("href", _$escape(href(), true), false), _$ssrAttribute("aria-label", _$escape(chatMessages().openInNewTab({
1538
+ children: href => _$ssr(_tmpl$38, _$ssrAttribute("href", _$escape(href(), true), false), _$ssrAttribute("aria-label", _$escape(chatMessages().openInNewTab({
1079
1539
  name: attachment.name
1080
1540
  }), true), false), _$escape(_$createComponent(Show, {
1081
1541
  get when() {
@@ -1085,14 +1545,14 @@ export function ChatComposer(props) {
1085
1545
  return _$ssr(_tmpl$3, _$ssrAttribute("class", _$escape(attachmentIcon(attachment), true), false));
1086
1546
  },
1087
1547
  get children() {
1088
- return _$ssr(_tmpl$30, _$ssrAttribute("src", _$escape(attachment.previewUrl, true), false) + _$ssrAttribute("alt", _$escape(attachment.alt ?? "", true), false));
1548
+ return _$ssr(_tmpl$36, _$ssrAttribute("src", _$escape(attachment.previewUrl, true), false) + _$ssrAttribute("alt", _$escape(attachment.alt ?? "", true), false));
1089
1549
  }
1090
1550
  })), _$escape(renderAttachmentCopy(attachment)))
1091
1551
  })), _$escape(_$createComponent(Show, {
1092
1552
  get when() {
1093
1553
  return attachment.action;
1094
1554
  },
1095
- children: action => _$ssr(_tmpl$33, _$ssrAttribute("disabled", blocked() || action().disabled, true), _$escape(_$createComponent(Show, {
1555
+ children: action => _$ssr(_tmpl$39, _$ssrAttribute("disabled", blocked() || action().disabled, true), _$escape(_$createComponent(Show, {
1096
1556
  get when() {
1097
1557
  return action().icon;
1098
1558
  },
@@ -1103,7 +1563,7 @@ export function ChatComposer(props) {
1103
1563
  return props.onAttachmentsChange;
1104
1564
  },
1105
1565
  get children() {
1106
- return _$ssr(_tmpl$28, _$ssrAttribute("aria-label", _$escape(selectMessages().removeNamed({
1566
+ return _$ssr(_tmpl$34, _$ssrAttribute("aria-label", _$escape(selectMessages().removeNamed({
1107
1567
  name: attachment.name
1108
1568
  }), true), false), _$ssrAttribute("disabled", blocked(), true));
1109
1569
  }
@@ -1120,14 +1580,14 @@ export function ChatComposer(props) {
1120
1580
  return props.error;
1121
1581
  },
1122
1582
  get children() {
1123
- return _$ssr(_tmpl$19, _$escape(props.error));
1583
+ return _$ssr(_tmpl$23, _$escape(props.error));
1124
1584
  }
1125
- }), formatRow(), _$ssr(_tmpl$34, _$escape(_$createComponent(Show, {
1585
+ }), formatRow(), _$ssr(_tmpl$40, _$escape(_$createComponent(Show, {
1126
1586
  get when() {
1127
- return props.footerContent;
1587
+ return footer();
1128
1588
  },
1129
1589
  get fallback() {
1130
- return [_$ssr(_tmpl$35, _$escape(props.footerTools), _$escape(_$createComponent(Show, {
1590
+ return [_$ssr(_tmpl$41, _$escape(props.footerTools), _$escape(_$createComponent(Show, {
1131
1591
  get when() {
1132
1592
  return hasAddMenu();
1133
1593
  },
@@ -1159,16 +1619,28 @@ export function ChatComposer(props) {
1159
1619
  image: model.image
1160
1620
  }));
1161
1621
  },
1622
+ get open() {
1623
+ return modelOpen();
1624
+ },
1625
+ onOpenChange: open => {
1626
+ if (!choosingModel()) return;
1627
+ setModelOpen(open);
1628
+ // Dropdown closes before committing the choice; let onValueChange settle it first.
1629
+ if (!open) queueMicrotask(() => {
1630
+ if (!modelOpen()) finishModelSelection(null);
1631
+ });
1632
+ },
1162
1633
  get disabled() {
1163
- return blocked() || running() || !props.onModelChange;
1634
+ return (choosingModel() ? blockedBesidesCommand() : blocked()) || running() || !props.onModelChange;
1164
1635
  },
1165
1636
  onValueChange: modelId => {
1166
1637
  props.onModelChange?.(modelId);
1638
+ finishModelSelection(modelId);
1167
1639
  queueMicrotask(focus);
1168
1640
  }
1169
1641
  });
1170
1642
  }
1171
- })), _$escape(props.modelDetails)), _$ssr(_tmpl$36, _$escape(_$createComponent(For, {
1643
+ })), _$escape(props.modelDetails)), _$ssr(_tmpl$42, _$escape(_$createComponent(For, {
1172
1644
  get each() {
1173
1645
  return props.contextActions;
1174
1646
  },
@@ -1209,7 +1681,9 @@ export function ChatComposer(props) {
1209
1681
  }))
1210
1682
  })), _$escape(props.submitTools), _$escape(microphoneControl()), _$escape(sendControl()))];
1211
1683
  },
1212
- children: content => content()
1684
+ get children() {
1685
+ return footer();
1686
+ }
1213
1687
  })))];
1214
1688
  },
1215
1689
  get children() {
@@ -1218,9 +1692,9 @@ export function ChatComposer(props) {
1218
1692
  return props.error;
1219
1693
  },
1220
1694
  get children() {
1221
- return _$ssr(_tmpl$19, _$escape(props.error));
1695
+ return _$ssr(_tmpl$23, _$escape(props.error));
1222
1696
  }
1223
- }), formatRow(), _$ssr(_tmpl$20, _$escape(attachControl()), _$escape(fileInput()), _$escape(field()), _$escape(formatToggle()), _$escape(emojiButton()), _$escape(microphoneControl()), _$escape(sendControl()))];
1697
+ }), formatRow(), _$ssr(_tmpl$24, _$escape(attachControl()), _$escape(fileInput()), _$escape(field()), _$escape(formatToggle()), _$escape(emojiButton()), _$escape(microphoneControl()), _$escape(sendControl()))];
1224
1698
  }
1225
1699
  })), _$escape(_$createComponent(Show, {
1226
1700
  get when() {
@@ -1241,6 +1715,23 @@ export function ChatComposer(props) {
1241
1715
  },
1242
1716
  onDrop: files => void runFiles(files)
1243
1717
  })
1718
+ })), _$escape(_$createComponent(Show, {
1719
+ get when() {
1720
+ return suggestionAnnouncement();
1721
+ },
1722
+ keyed: true,
1723
+ children: announced => _$ssr(_tmpl$43, _$escape(announced.text))
1724
+ })), _$escape(_$createComponent(Show, {
1725
+ get when() {
1726
+ return _$memo(() => !!!suggestionsOpen())() && commandStatus();
1727
+ },
1728
+ children: status => _$ssr(_tmpl$44, _$ssrAttribute("data-tone", _$escape(status().tone, true), false), _$ssrAttribute("class", status().tone === "busy" ? "ti ti-loader-2 k2b-spin" : status().tone === "done" ? "ti ti-check" : "ti ti-alert-circle", false), _$escape(status().text))
1729
+ })), _$escape(_$createComponent(Show, {
1730
+ get when() {
1731
+ return _$memo(() => !!suggestionsOpen())() && announcement();
1732
+ },
1733
+ keyed: true,
1734
+ children: announced => _$ssr(_tmpl$45, _$escape(announced.text))
1244
1735
  })));
1245
1736
  }
1246
1737
  //# sourceMappingURL=ChatComposer.js.map