@k2b/ui 0.29.0 → 0.30.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 (173) hide show
  1. package/README.md +1 -1
  2. package/dist/base.css +2 -2
  3. package/dist/browser/actions/Dropdown.js +6 -3
  4. package/dist/browser/actions/Dropdown.js.map +1 -1
  5. package/dist/browser/actions/FilterChip.js +137 -7
  6. package/dist/browser/actions/FilterChip.js.map +1 -1
  7. package/dist/browser/actions/SegmentedControl.js +5 -9
  8. package/dist/browser/actions/SegmentedControl.js.map +1 -1
  9. package/dist/browser/actions/SpotlightSearch.js +6 -3
  10. package/dist/browser/actions/SpotlightSearch.js.map +1 -1
  11. package/dist/browser/actions/Tabs.js +5 -2
  12. package/dist/browser/actions/Tabs.js.map +1 -1
  13. package/dist/browser/actions/ViewTabs.js +294 -0
  14. package/dist/browser/actions/ViewTabs.js.map +1 -0
  15. package/dist/browser/actions/filter-messages.js +6 -2
  16. package/dist/browser/actions/filter-messages.js.map +1 -1
  17. package/dist/browser/actions/index.js +2 -1
  18. package/dist/browser/actions/index.js.map +1 -1
  19. package/dist/browser/chat/ChatComposer.js +1108 -604
  20. package/dist/browser/chat/ChatComposer.js.map +1 -1
  21. package/dist/browser/chat/ChatDecision.js +120 -84
  22. package/dist/browser/chat/ChatDecision.js.map +1 -1
  23. package/dist/browser/chat/ChatPrimitives.js +292 -159
  24. package/dist/browser/chat/ChatPrimitives.js.map +1 -1
  25. package/dist/browser/chat/chat-messages.js +8 -0
  26. package/dist/browser/chat/chat-messages.js.map +1 -1
  27. package/dist/browser/chat/command-search.js +26 -15
  28. package/dist/browser/chat/command-search.js.map +1 -1
  29. package/dist/browser/chat/index.js +4 -2
  30. package/dist/browser/chat/index.js.map +1 -1
  31. package/dist/browser/content/Calendar.js +143 -116
  32. package/dist/browser/content/Calendar.js.map +1 -1
  33. package/dist/browser/content/CalendarWeekView.js +291 -266
  34. package/dist/browser/content/CalendarWeekView.js.map +1 -1
  35. package/dist/browser/content/DataTable.js +224 -155
  36. package/dist/browser/content/DataTable.js.map +1 -1
  37. package/dist/browser/content/data-table-messages.js +4 -2
  38. package/dist/browser/content/data-table-messages.js.map +1 -1
  39. package/dist/browser/feedback/MovedHint.js +197 -0
  40. package/dist/browser/feedback/MovedHint.js.map +1 -0
  41. package/dist/browser/feedback/dialog-core.js +66 -9
  42. package/dist/browser/feedback/dialog-core.js.map +1 -1
  43. package/dist/browser/feedback/index.js +1 -0
  44. package/dist/browser/feedback/index.js.map +1 -1
  45. package/dist/browser/feedback/prompts.js +6 -1
  46. package/dist/browser/feedback/prompts.js.map +1 -1
  47. package/dist/browser/inputs/DatePicker.js +337 -212
  48. package/dist/browser/inputs/DatePicker.js.map +1 -1
  49. package/dist/browser/inputs/date-picker.js +59 -5
  50. package/dist/browser/inputs/date-picker.js.map +1 -1
  51. package/dist/browser/intl/messages.js +12 -0
  52. package/dist/browser/intl/messages.js.map +1 -1
  53. package/dist/browser/intl/weekend.js +39 -0
  54. package/dist/browser/intl/weekend.js.map +1 -0
  55. package/dist/browser/layout/AppOverview.js +0 -1
  56. package/dist/browser/layout/AppOverview.js.map +1 -1
  57. package/dist/browser/layout/AppWorkspace.js +688 -471
  58. package/dist/browser/layout/AppWorkspace.js.map +1 -1
  59. package/dist/browser/layout/PageTitle.js +3 -2
  60. package/dist/browser/layout/PageTitle.js.map +1 -1
  61. package/dist/browser/layout/SettingsModal.js +2 -2
  62. package/dist/browser/layout/SettingsModal.js.map +1 -1
  63. package/dist/browser/layout/SidebarSwitcher.js +13 -12
  64. package/dist/browser/layout/SidebarSwitcher.js.map +1 -1
  65. package/dist/browser/layout/TabBar.js +1 -1
  66. package/dist/browser/layout/TabBar.js.map +1 -1
  67. package/dist/browser/layout/app-host-width.js +44 -0
  68. package/dist/browser/layout/app-host-width.js.map +1 -0
  69. package/dist/browser/layout/app-workspace-controller.js +219 -39
  70. package/dist/browser/layout/app-workspace-controller.js.map +1 -1
  71. package/dist/browser/layout/index.js +1 -0
  72. package/dist/browser/layout/index.js.map +1 -1
  73. package/dist/browser/surfaces/Placeholder.js +36 -13
  74. package/dist/browser/surfaces/Placeholder.js.map +1 -1
  75. package/dist/ssr/actions/Dropdown.js +5 -2
  76. package/dist/ssr/actions/Dropdown.js.map +1 -1
  77. package/dist/ssr/actions/FilterChip.js +59 -7
  78. package/dist/ssr/actions/FilterChip.js.map +1 -1
  79. package/dist/ssr/actions/SegmentedControl.js +1 -2
  80. package/dist/ssr/actions/SegmentedControl.js.map +1 -1
  81. package/dist/ssr/actions/SpotlightSearch.js +2 -2
  82. package/dist/ssr/actions/SpotlightSearch.js.map +1 -1
  83. package/dist/ssr/actions/Tabs.js +2 -2
  84. package/dist/ssr/actions/Tabs.js.map +1 -1
  85. package/dist/ssr/actions/ViewTabs.js +211 -0
  86. package/dist/ssr/actions/ViewTabs.js.map +1 -0
  87. package/dist/ssr/actions/filter-messages.js +6 -2
  88. package/dist/ssr/actions/filter-messages.js.map +1 -1
  89. package/dist/ssr/actions/index.js +2 -1
  90. package/dist/ssr/actions/index.js.map +1 -1
  91. package/dist/ssr/chat/ChatComposer.js +546 -131
  92. package/dist/ssr/chat/ChatComposer.js.map +1 -1
  93. package/dist/ssr/chat/ChatDecision.js +33 -19
  94. package/dist/ssr/chat/ChatDecision.js.map +1 -1
  95. package/dist/ssr/chat/ChatPrimitives.js +95 -24
  96. package/dist/ssr/chat/ChatPrimitives.js.map +1 -1
  97. package/dist/ssr/chat/chat-messages.js +8 -0
  98. package/dist/ssr/chat/chat-messages.js.map +1 -1
  99. package/dist/ssr/chat/command-search.js +26 -15
  100. package/dist/ssr/chat/command-search.js.map +1 -1
  101. package/dist/ssr/chat/index.js +4 -2
  102. package/dist/ssr/chat/index.js.map +1 -1
  103. package/dist/ssr/content/Calendar.js +109 -100
  104. package/dist/ssr/content/Calendar.js.map +1 -1
  105. package/dist/ssr/content/CalendarWeekView.js +42 -28
  106. package/dist/ssr/content/CalendarWeekView.js.map +1 -1
  107. package/dist/ssr/content/DataTable.js +78 -31
  108. package/dist/ssr/content/DataTable.js.map +1 -1
  109. package/dist/ssr/content/data-table-messages.js +4 -2
  110. package/dist/ssr/content/data-table-messages.js.map +1 -1
  111. package/dist/ssr/feedback/MovedHint.js +166 -0
  112. package/dist/ssr/feedback/MovedHint.js.map +1 -0
  113. package/dist/ssr/feedback/dialog-core.js +66 -9
  114. package/dist/ssr/feedback/dialog-core.js.map +1 -1
  115. package/dist/ssr/feedback/index.js +1 -0
  116. package/dist/ssr/feedback/index.js.map +1 -1
  117. package/dist/ssr/feedback/prompts.js +6 -1
  118. package/dist/ssr/feedback/prompts.js.map +1 -1
  119. package/dist/ssr/inputs/DatePicker.js +138 -69
  120. package/dist/ssr/inputs/DatePicker.js.map +1 -1
  121. package/dist/ssr/inputs/date-picker.js +59 -5
  122. package/dist/ssr/inputs/date-picker.js.map +1 -1
  123. package/dist/ssr/intl/messages.js +12 -0
  124. package/dist/ssr/intl/messages.js.map +1 -1
  125. package/dist/ssr/intl/weekend.js +39 -0
  126. package/dist/ssr/intl/weekend.js.map +1 -0
  127. package/dist/ssr/layout/AppOverview.js +0 -1
  128. package/dist/ssr/layout/AppOverview.js.map +1 -1
  129. package/dist/ssr/layout/AppWorkspace.js +203 -39
  130. package/dist/ssr/layout/AppWorkspace.js.map +1 -1
  131. package/dist/ssr/layout/PageTitle.js +3 -2
  132. package/dist/ssr/layout/PageTitle.js.map +1 -1
  133. package/dist/ssr/layout/SettingsModal.js +2 -2
  134. package/dist/ssr/layout/SettingsModal.js.map +1 -1
  135. package/dist/ssr/layout/SidebarSwitcher.js +13 -12
  136. package/dist/ssr/layout/SidebarSwitcher.js.map +1 -1
  137. package/dist/ssr/layout/TabBar.js +1 -1
  138. package/dist/ssr/layout/TabBar.js.map +1 -1
  139. package/dist/ssr/layout/app-host-width.js +44 -0
  140. package/dist/ssr/layout/app-host-width.js.map +1 -0
  141. package/dist/ssr/layout/app-workspace-controller.js +219 -39
  142. package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
  143. package/dist/ssr/layout/index.js +1 -0
  144. package/dist/ssr/layout/index.js.map +1 -1
  145. package/dist/ssr/surfaces/Placeholder.js +24 -7
  146. package/dist/ssr/surfaces/Placeholder.js.map +1 -1
  147. package/dist/styles.css +1 -1
  148. package/dist/types/actions/Dropdown.d.ts +4 -0
  149. package/dist/types/actions/FilterChip.d.ts +22 -0
  150. package/dist/types/actions/ViewTabs.d.ts +30 -0
  151. package/dist/types/actions/filter-messages.d.ts +2 -0
  152. package/dist/types/actions/index.d.ts +4 -2
  153. package/dist/types/chat/ChatComposer.d.ts +52 -0
  154. package/dist/types/chat/ChatDecision.d.ts +18 -2
  155. package/dist/types/chat/ChatPrimitives.d.ts +10 -0
  156. package/dist/types/chat/chat-messages.d.ts +4 -0
  157. package/dist/types/chat/command-search.d.ts +2 -0
  158. package/dist/types/chat/index.d.ts +9 -8
  159. package/dist/types/content/DataTable.d.ts +3 -0
  160. package/dist/types/content/data-table-messages.d.ts +1 -0
  161. package/dist/types/feedback/MovedHint.d.ts +25 -0
  162. package/dist/types/feedback/dialog-core.d.ts +5 -2
  163. package/dist/types/feedback/index.d.ts +2 -0
  164. package/dist/types/feedback/prompts.d.ts +2 -0
  165. package/dist/types/inputs/DatePicker.d.ts +7 -0
  166. package/dist/types/inputs/date-picker.d.ts +25 -1
  167. package/dist/types/intl/messages.d.ts +6 -0
  168. package/dist/types/intl/weekend.d.ts +12 -0
  169. package/dist/types/layout/AppWorkspace.d.ts +43 -7
  170. package/dist/types/layout/app-host-width.d.ts +6 -0
  171. package/dist/types/layout/index.d.ts +2 -1
  172. package/dist/types/surfaces/Placeholder.d.ts +2 -1
  173. package/package.json +4 -4
@@ -17,42 +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 = ["<small", ">", "</small>"],
20
+ _tmpl$11 = ["<span class=\"k2b-chat-composer__command-detail\"", "><small", ">", "</small><small", "><i class=\"ti ti-loader-2 k2b-spin\" aria-hidden=\"true\"></i>", "</small></span>"],
21
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
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
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";
24
+ _tmpl$15 = ["<small", ">", "</small>"],
25
+ _tmpl$16 = ["<div class=\"k2b-chat-composer-accessory\"><div", " class=\"k2b-chat-composer__commands\" role=\"listbox\"", ">", "</div></div>"],
26
+ _tmpl$17 = ["<label class=\"k2b-chat-composer__command-search\"><i class=\"ti ti-search\" aria-hidden=\"true\"></i><input type=\"text\" role=\"combobox\"", " aria-autocomplete=\"list\"", " aria-expanded=\"true\"", " autocomplete=\"off\" autocapitalize=\"off\" spellcheck=\"false\"", "></label>"],
27
+ _tmpl$18 = ["<div role=\"status\" class=\"k2b-chat-composer__loading\"><i class=\"ti ti-loader-2 k2b-spin\" aria-hidden=\"true\"></i><strong>", "</strong></div>"],
28
+ _tmpl$19 = ["<div role=\"status\"><i class=\"ti ti-alert-circle\"></i> ", "</div>"],
29
+ _tmpl$20 = ["<div role=\"status\">", "</div>"],
30
+ _tmpl$21 = ["<div class=\"k2b-chat-composer-accessory\" data-commands", ">", "<div", " class=\"k2b-chat-composer__commands\" role=\"listbox\"", ">", "", "", "", "", "</div></div>"],
31
+ _tmpl$22 = ["<div class=\"k2b-chat-composer-slot\"", "><div style=\"", "\"", ">", "</div>", "", "</div>"],
32
+ _tmpl$23 = ["<div class=\"k2b-chat-composer__hint\" aria-live=\"polite\">", "</div>"],
33
+ _tmpl$24 = ["<div class=\"k2b-chat-composer__attachments\" role=\"list\"", " tabindex=\"0\">", "</div>"],
34
+ _tmpl$25 = ["<div class=\"k2b-chat-composer__error\" role=\"alert\"><i class=\"ti ti-alert-circle\" aria-hidden=\"true\"></i>", "</div>"],
35
+ _tmpl$26 = ["<div class=\"k2b-chat-composer__row\">", "", "<div class=\"k2b-chat-composer__field\">", "<div class=\"k2b-chat-composer__field-tools\">", "", "", "</div></div>", "</div>"],
36
+ _tmpl$27 = ["<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>"],
37
+ _tmpl$28 = ["<button id=\"", "\" type=\"button\" role=\"option\" tabindex=\"-1\"", "><span class=\"k2b-chat-composer__emoji-glyph\" aria-hidden=\"true\">", "</span><strong>", "</strong>", "</button>"],
38
+ _tmpl$29 = ["<small>:", ":</small>"],
39
+ _tmpl$30 = "<i class=\"ti ti-loader-2 k2b-spin\" aria-hidden=\"true\"></i>",
40
+ _tmpl$31 = ["<div class=\"k2b-chat-composer__command-group\" role=\"group\"", "><div class=\"k2b-chat-composer__command-heading\"", " role=\"presentation\"><span>", "</span>", "</div>", "", "</div>"],
41
+ _tmpl$32 = "<div class=\"k2b-chat-composer__command-placeholder\" aria-hidden=\"true\"><span></span><span><span></span><span></span></span></div>",
42
+ _tmpl$33 = ["<span class=\"k2b-chat-composer__hint-text\">", "</span>"],
43
+ _tmpl$34 = ["<span class=\"k2b-chat-composer__dictation\"", "><i", " aria-hidden=\"true\"></i><span>", "</span></span>"],
44
+ _tmpl$35 = ["<button type=\"button\" class=\"k2b-chat-composer__hint-action\"", ">", "</button>"],
45
+ _tmpl$36 = ["<button type=\"button\" class=\"k2b-chat-composer__attachment-remove\"", "", "><i class=\"ti ti-x\" aria-hidden=\"true\"></i></button>"],
46
+ _tmpl$37 = ["<div class=\"k2b-chat-composer__attachment\" role=\"listitem\"><div class=\"", "\">", "", "</div>", "</div>"],
47
+ _tmpl$38 = ["<img", ">"],
48
+ _tmpl$39 = ["<div class=\"k2b-chat-composer__attachment-identity\">", "", "</div>"],
49
+ _tmpl$40 = ["<a class=\"k2b-chat-composer__attachment-link\"", " target=\"_blank\" rel=\"noopener noreferrer\"", ">", "", "</a>"],
50
+ _tmpl$41 = ["<button type=\"button\" class=\"k2b-chat-composer__attachment-action\"", ">", "", "</button>"],
51
+ _tmpl$42 = ["<footer class=\"k2b-chat-composer__footer\">", "</footer>"],
52
+ _tmpl$43 = ["<div class=\"k2b-chat-composer__tools\">", "", "", "", "", "", "", "</div>"],
53
+ _tmpl$44 = ["<div class=\"k2b-chat-composer__submit\">", "", "", "", "", "</div>"],
54
+ _tmpl$45 = ["<span>", "</span>"],
55
+ _tmpl$46 = ["<div class=\"k2b-chat-composer__status-line\"", "><i aria-hidden=\"true\"", "></i><span>", "</span></div>"],
56
+ _tmpl$47 = ["<span class=\"k2b-sr-only\">", "</span>"];
57
+ import { batch, children, createEffect, createMemo, createSignal, createUniqueId, For, Index, on, onCleanup, onMount, Show, untrack } from "solid-js";
56
58
  import { Dropdown } from "../actions/Dropdown";
57
59
  import { dialogHistorySettled, holdDialogHistory } from "../feedback/dialog-core";
58
60
  import { Tooltip } from "../feedback/Tooltip";
@@ -83,6 +85,13 @@ const composerUndoBudgetChars = 1_000_000;
83
85
  * dialog: `mention` takes the token's place exactly as a `mention` row does, and Ctrl/⌘+Z brings the token back.
84
86
  */
85
87
 
88
+ /** The draft as a successful `open` leaves it: without the `/…` token, with its references moved along. Save this one. */
89
+
90
+ /**
91
+ * How an `open` row ends: on success (`void`) the token leaves as for an action; `"cancelled"` changes nothing; `failed`
92
+ * keeps the list open and the draft unchanged, and its text replaces the row's description until the row runs again.
93
+ */
94
+
86
95
  /** A fixed place in the slash list. Sections keep the given order, also while searching. */
87
96
 
88
97
  /** First-word narrowing; Tab writes `words[0]` followed by a space. */
@@ -171,6 +180,17 @@ export function ChatComposer(props) {
171
180
  return `${commandListId}-c${commandIds.get(key)}`;
172
181
  };
173
182
  const [closedAt, setClosedAt] = createSignal();
183
+ /** The list opened from "+": the field's selection when "+" was pressed, and the search field's text. */
184
+ const [addSearch, setAddSearch] = createSignal();
185
+ const [addQuery, setAddQuery] = createSignal("");
186
+ let addSearchRef;
187
+ let addButtonRef;
188
+ let addComposing = false;
189
+ /** The `open` row running now, and why `open` rows failed in this opening, by row key. */
190
+ const [openingRow, setOpeningRow] = createSignal();
191
+ const [openFailures, setOpenFailures] = createSignal({});
192
+ /** Cancels the `open` row running now; `key` follows announced draft moves, `label` is its busy line. */
193
+ let openingRun;
174
194
  // Where the pending native edit starts; a selection replaced as a whole starts at its beginning.
175
195
  let inputStart;
176
196
  let inputEnd;
@@ -275,7 +295,17 @@ export function ChatComposer(props) {
275
295
  return token && index ? suggestEmoji(index, token.query, emojiSuggestionLimit) : [];
276
296
  });
277
297
  const emojiOpen = () => emojiMatches().length > 0;
278
- const caretCommandToken = createMemo(() => chatCommandQuery(props.value, caret(), selectionEnd()));
298
+ // From "+", the search field's text is the query of an empty token at the field's stored caret.
299
+ const caretCommandToken = createMemo(() => {
300
+ const search = addSearch();
301
+ if (!search) return chatCommandQuery(props.value, caret(), selectionEnd());
302
+ const at = Math.min(search.end, props.value.length);
303
+ return {
304
+ start: at,
305
+ end: at,
306
+ query: addQuery()
307
+ };
308
+ });
279
309
  const caretCommandStart = createMemo(() => caretCommandToken()?.start);
280
310
  // Retained server rows belong to one `/`: they go when it does or when another `/` starts.
281
311
  createEffect(() => {
@@ -290,6 +320,7 @@ export function ChatComposer(props) {
290
320
  });
291
321
  const commandQuery = createMemo(() => {
292
322
  const token = caretCommandToken();
323
+ if (addSearch()) return token;
293
324
  return dismissed() || composing() || token?.start === closedAt() ? null : token;
294
325
  });
295
326
  const openingStart = createMemo(() => commandQuery()?.start);
@@ -298,6 +329,12 @@ export function ChatComposer(props) {
298
329
  const start = openingStart();
299
330
  if (start !== undefined) setCommandOpening(opening => opening + 1);
300
331
  });
332
+ // A failed `open` row says so for the rest of its list opening.
333
+ createEffect(() => {
334
+ props.draftKey;
335
+ commandOpening();
336
+ setOpenFailures({});
337
+ });
301
338
  const parsedCommandQuery = createMemo(() => parseChatCommandQuery(commandQuery()?.query ?? "", props.commandScopes ?? []));
302
339
  const commandFallbackRow = createMemo(() => {
303
340
  const parsed = parsedCommandQuery();
@@ -313,7 +350,23 @@ export function ChatComposer(props) {
313
350
  setSearchResults([]);
314
351
  setSearchResultsQuery(undefined);
315
352
  });
316
- const localCommands = createMemo(() => props.commands ?? []);
353
+ // Rows of a `fixedWhileOpen` section are the ones it had when this `/` opened the list; later ones wait for the next
354
+ // `/`. A retry row starts another opening in the same open list, so it does not count.
355
+ const fixedCommandKeys = createMemo(() => {
356
+ props.draftKey;
357
+ if (openingStart() === undefined) return undefined;
358
+ return untrack(() => {
359
+ const fixed = new Set((props.commandSections ?? []).filter(section => section.fixedWhileOpen).map(section => section.id));
360
+ return fixed.size ? new Set((props.commands ?? []).filter(command => fixed.has(command.section ?? "")).map(chatCommandKey)) : undefined;
361
+ });
362
+ });
363
+ const localCommands = createMemo(() => {
364
+ const commands = props.commands ?? [];
365
+ const keys = fixedCommandKeys();
366
+ if (!keys) return commands;
367
+ const fixed = new Set((props.commandSections ?? []).filter(section => section.fixedWhileOpen).map(section => section.id));
368
+ return commands.filter(command => !fixed.has(command.section ?? "") || keys.has(chatCommandKey(command)));
369
+ });
317
370
  const commandModel = createMemo(() => chatCommandListModel({
318
371
  query: commandQuery()?.query ?? "",
319
372
  scopes: props.commandScopes,
@@ -337,12 +390,86 @@ export function ChatComposer(props) {
337
390
  setCommandMoved(false);
338
391
  });
339
392
  const commandsOpen = () => Boolean(!emojiOpen() && commandQuery() && (localCommands().length || props.searchCommands || commandFallbackRow()));
340
- const commandSheetHeld = createMemo(() => commandsOpen() && isTouchOnly());
393
+ // Rows that arrive for a pending section never move a visible row of the overview: until the person types, the list
394
+ // keeps the height it had with its placeholders and scrolls what does not fit. A list below its maximum height grows
395
+ // upward from the field, so a change of height alone would move its first row. While searching, the rows arrive
396
+ // with the search's own answer, which changes the list anyway.
397
+ // From "+", the whole panel keeps the height it opened with instead.
398
+ const commandsPending = createMemo(() => commandsOpen() && !addSearch() && !parsedCommandQuery().scope && !parsedCommandQuery().query.trim() && commandModel().groups.some(group => group.pendingRows > 0));
399
+ // Typing, another opening or closing the list lets it size itself again.
400
+ const commandListSizeKey = createMemo(() => commandsOpen() ? `${commandOpening()}\u0000${caretCommandToken()?.query}` : undefined);
401
+ // The list's height with its placeholders, for the size key it was measured under.
402
+ let pendingList;
403
+ createEffect(() => {
404
+ const list = commandListRef;
405
+ if (!commandsPending() || !list || typeof ResizeObserver === "undefined") return;
406
+ const measure = () => {
407
+ pendingList = {
408
+ height: list.getBoundingClientRect().height,
409
+ key: untrack(commandListSizeKey)
410
+ };
411
+ };
412
+ measure();
413
+ const observer = new ResizeObserver(measure);
414
+ observer.observe(list);
415
+ onCleanup(() => observer.disconnect());
416
+ });
417
+ createEffect(on(commandsPending, (pending, wasPending) => {
418
+ const held = pendingList;
419
+ pendingList = undefined;
420
+ // Only rows arriving hold the list; the person's typing or closing it does not.
421
+ if (pending || !wasPending || !held || held.key !== commandListSizeKey() || !commandListRef?.isConnected) return;
422
+ commandListRef.style.height = `${held.height}px`;
423
+ commandListRef.setAttribute("data-held", "");
424
+ }));
425
+ createEffect(on(commandListSizeKey, () => {
426
+ commandListRef?.style.removeProperty("height");
427
+ commandListRef?.removeAttribute("data-held");
428
+ }, {
429
+ defer: true
430
+ }));
431
+ // Escape, a press outside, or moving the caret away closes the list; while an `open` row runs, that cancels it, so
432
+ // nothing opens after the person backed out. Success ends the opening and closes the list in one step. A new draft
433
+ // cancels too, unless the opener announced that next move with `follow()`: its busy line moves to the status line,
434
+ // and Escape still cancels there. Any later unannounced draft change cancels.
435
+ createEffect(() => {
436
+ const draftKey = props.draftKey;
437
+ const open = commandsOpen();
438
+ const row = openingRow();
439
+ const run = openingRun;
440
+ if (!run || row === undefined) return;
441
+ if (draftKey !== run.key) {
442
+ if (run.follow) {
443
+ run.follow = false;
444
+ run.key = draftKey;
445
+ run.moved = true;
446
+ showCommandStatus({
447
+ tone: "busy",
448
+ text: run.label
449
+ });
450
+ } else run.controller.abort();
451
+ return;
452
+ }
453
+ if (!open && !run.moved) run.controller.abort();
454
+ });
455
+ onCleanup(() => openingRun?.controller.abort());
456
+ // An `open` row gives the sheet's Back entry up first, so it never races the navigation it starts.
457
+ const commandSheetHeld = createMemo(() => commandsOpen() && isTouchOnly() && openingRow() === undefined);
341
458
  createEffect(() => {
342
459
  if (!commandSheetHeld()) return;
343
- onCleanup(holdDialogHistory(() => setClosedAt(commandQuery()?.start)));
460
+ onCleanup(holdDialogHistory(() => closeCommandList()));
344
461
  });
345
462
  const suggestionsOpen = () => commandsOpen() || emojiOpen();
463
+ const fieldOwnsList = () => suggestionsOpen() && !addSearch();
464
+ // From "+", the panel keeps the height it opened with while the person types or rows arrive, so neither the search
465
+ // field nor the first row moves; what does not fit scrolls. The space above the field still bounds it.
466
+ const addPanelOpen = createMemo(() => addSearch() !== undefined && commandsOpen());
467
+ createEffect(() => {
468
+ const accessory = addPanelOpen() ? commandListRef?.parentElement : undefined;
469
+ if (!accessory) return;
470
+ const frame = requestAnimationFrame(() => accessory.style.setProperty("--k2b-chat-command-held", `${accessory.getBoundingClientRect().height}px`));
471
+ onCleanup(() => cancelAnimationFrame(frame));
472
+ });
346
473
  createEffect(() => {
347
474
  if (!suggestionsOpen()) return;
348
475
  let disposed = false;
@@ -370,10 +497,13 @@ export function ChatComposer(props) {
370
497
  const left = Math.max(0, column?.left ?? 0);
371
498
  const right = Math.min(page, column?.right ?? page);
372
499
  const slot = accessory.parentElement.getBoundingClientRect();
373
- const bottom = accessory.getBoundingClientRect().bottom;
374
- commandListRef.style.setProperty("--k2b-chat-command-space", `${Math.max(40, bottom - top - 8)}px`);
500
+ // From its place in the slot, not its box: the sheet opened from "+" rises into place, and a box measured during
501
+ // that animation would include its offset.
502
+ const bottom = slot.bottom - (Number.parseFloat(window.getComputedStyle(accessory).bottom) || 0);
503
+ // On the accessory, so a panel opened from "+" is bounded as a whole, with its search field.
504
+ accessory.style.setProperty("--k2b-chat-command-space", `${Math.max(40, bottom - top - 8)}px`);
375
505
  // Only a list that reaches the screen's top edge keeps clear of a notch or the status bar.
376
- commandListRef.style.setProperty("--k2b-chat-command-safe-top", top === viewportTop ? "env(safe-area-inset-top, 0px)" : "0px");
506
+ accessory.style.setProperty("--k2b-chat-command-safe-top", top === viewportTop ? "env(safe-area-inset-top, 0px)" : "0px");
377
507
  accessory.style.setProperty("--k2b-chat-command-reach-left", `${Math.max(0, slot.left - left)}px`);
378
508
  accessory.style.setProperty("--k2b-chat-command-reach-right", `${Math.max(0, right - slot.right)}px`);
379
509
  };
@@ -391,11 +521,29 @@ export function ChatComposer(props) {
391
521
  // A press outside the command list and the field closes the list, as Escape does; the text stays. With a mouse or a
392
522
  // trackpad the press then acts as usual. On a touch-only device a tap outside the composer closes only the list: it
393
523
  // reaches nothing beneath, and the field keeps focus, so the keyboard stays open.
524
+ // A list opened from "+" is a menu: any press outside it closes only the list, with every pointer, and the field gets
525
+ // its focus back. "+" itself toggles with its click; a press in the field closes the list and lands there as usual.
394
526
  createEffect(() => {
395
527
  if (!commandsOpen()) return;
396
528
  const onPointerDown = event => {
397
529
  const target = event.target;
398
- if (!(target instanceof Node) || commandListRef?.contains(target) || textareaRef?.parentElement?.contains(target)) return;
530
+ if (!(target instanceof Node) || commandListRef?.parentElement?.contains(target)) return;
531
+ if (addSearch()) {
532
+ if (addButtonRef?.contains(target) && !addButtonRef.disabled) return;
533
+ if (textareaRef?.parentElement?.contains(target)) {
534
+ // The field takes the focus before the search field goes, so a phone keeps its keyboard; the press itself
535
+ // then places the caret.
536
+ textareaRef.focus({
537
+ preventScroll: true
538
+ });
539
+ endAddSearch(false);
540
+ return;
541
+ }
542
+ swallowTap(event);
543
+ endAddSearch(true);
544
+ return;
545
+ }
546
+ if (textareaRef?.parentElement?.contains(target)) return;
399
547
  setClosedAt(commandQuery()?.start);
400
548
  if (isTouchOnly() && !shellRef?.contains(target)) swallowTap(event);
401
549
  };
@@ -445,9 +593,10 @@ export function ChatComposer(props) {
445
593
  });
446
594
  });
447
595
  let settledEmpty;
596
+ // A space after "Nothing found" ends the `/…` as plain text; a search field opened from "+" just keeps its text.
448
597
  createEffect(() => {
449
598
  const token = commandQuery();
450
- if (!token || parsedCommandQuery().scope || searchError()) {
599
+ if (!token || addSearch() || parsedCommandQuery().scope || searchError()) {
451
600
  settledEmpty = undefined;
452
601
  return;
453
602
  }
@@ -498,6 +647,7 @@ export function ChatComposer(props) {
498
647
  history.length = 0;
499
648
  historyIndex = -1;
500
649
  finishModelSelection(null);
650
+ endAddSearch(false);
501
651
  setDismissed(true);
502
652
  setClosedAt(undefined);
503
653
  setSearchResults([]);
@@ -546,6 +696,8 @@ export function ChatComposer(props) {
546
696
  setDismissed(false);
547
697
  };
548
698
  const restoreHistory = direction => {
699
+ // A field that only stays enabled to keep focus, while an `open` row runs, is not edited by undo either.
700
+ if (editBlocked()) return;
549
701
  record();
550
702
  const next = history[historyIndex + direction];
551
703
  if (!next) return;
@@ -564,6 +716,45 @@ export function ChatComposer(props) {
564
716
  setSelectionEnd(textareaRef?.selectionEnd ?? props.value.length);
565
717
  });
566
718
  };
719
+ /** Closes the list opened from "+"; `refocus` gives the field its focus and the selection it had when "+" was pressed. */
720
+ const endAddSearch = refocus => {
721
+ const search = addSearch();
722
+ if (!search) return;
723
+ addComposing = false;
724
+ batch(() => {
725
+ setAddSearch(undefined);
726
+ setAddQuery("");
727
+ });
728
+ if (!refocus || !textareaRef) return;
729
+ const length = textareaRef.value.length;
730
+ textareaRef.focus();
731
+ textareaRef.setSelectionRange(Math.min(search.start, length), Math.min(search.end, length));
732
+ syncCaret();
733
+ };
734
+ /** Escape, Back, a press outside: the list closes and the text stays. */
735
+ const closeCommandList = (refocus = true) => {
736
+ if (addSearch()) endAddSearch(refocus);else setClosedAt(commandQuery()?.start);
737
+ };
738
+ const toggleAddSearch = () => {
739
+ if (addSearch()) {
740
+ endAddSearch(true);
741
+ return;
742
+ }
743
+ if (blocked() || !textareaRef) return;
744
+ const start = textareaRef.selectionStart;
745
+ const end = textareaRef.selectionEnd;
746
+ batch(() => {
747
+ // Emoji suggestions for a `:shortcode` at the caret give way; they would hold the list closed.
748
+ setDismissed(true);
749
+ setAddQuery("");
750
+ setAddSearch({
751
+ start,
752
+ end
753
+ });
754
+ });
755
+ // Within the press, so a phone opens its keyboard for the search field.
756
+ addSearchRef?.focus();
757
+ };
567
758
  const selectedCommand = createMemo(() => {
568
759
  const rows = commandMatches();
569
760
  return commandMoved() && rows.find(command => chatCommandKey(command) === activeCommandKey()) || rows[0];
@@ -587,6 +778,8 @@ export function ChatComposer(props) {
587
778
  const canSubmit = () => !submitting() && submittable();
588
779
  const canSelectFiles = () => Boolean(props.fileSelection && !props.fileSelection.disabled && !running() && !blocked());
589
780
  const hasAddMenu = () => Boolean(props.fileSelection || props.menuActions?.length);
781
+ /** "+" opens the slash list, as long as there is one; otherwise it stays the add menu. */
782
+ const addOpensCommands = () => Boolean(props.addOpensCommands && (props.commands?.length || props.searchCommands));
590
783
  const hasContextUsage = () => {
591
784
  if (props.contextPopupAction) return true;
592
785
  const context = props.contextUsage;
@@ -723,7 +916,7 @@ export function ChatComposer(props) {
723
916
  * key or click that chose the row, because the device's file dialog needs that activation. Closing the selection
724
917
  * without a file brings the token back, unless the draft changed meanwhile.
725
918
  */
726
- const attachFilesFromCommand = token => {
919
+ const attachFilesFromCommand = (token, selection) => {
727
920
  if (!canSelectFiles()) return;
728
921
  const key = props.draftKey;
729
922
  const previous = snapshot();
@@ -742,7 +935,7 @@ export function ChatComposer(props) {
742
935
  history.splice(0, history.length, ...previousHistory.entries);
743
936
  historyIndex = previousHistory.index;
744
937
  queueMicrotask(() => {
745
- textareaRef?.setSelectionRange(token.end, token.end);
938
+ textareaRef?.setSelectionRange(selection?.start ?? token.end, selection?.end ?? token.end);
746
939
  syncCaret();
747
940
  focus();
748
941
  });
@@ -774,7 +967,7 @@ export function ChatComposer(props) {
774
967
  setSubmitting(true);
775
968
  try {
776
969
  if (isTouchOnly()) {
777
- if (commandSheetHeld()) setClosedAt(commandQuery()?.start);
970
+ if (commandSheetHeld()) closeCommandList(false);
778
971
  // The sheet's Back entry leaves first, so a submit handler that navigates finds the history settled.
779
972
  await dialogHistorySettled();
780
973
  // A draft that another one replaced, or that may no longer go out, is not sent and keeps its text.
@@ -826,6 +1019,10 @@ export function ChatComposer(props) {
826
1019
  const word = scope?.words[0];
827
1020
  const token = commandQuery();
828
1021
  if (!word || !token || !textareaRef) return false;
1022
+ if (addSearch()) {
1023
+ setAddQuery(`${word} `);
1024
+ return true;
1025
+ }
829
1026
  replaceTextareaRange(textareaRef, token.start + 1, token.end, `${word} `);
830
1027
  syncCaret();
831
1028
  return true;
@@ -833,6 +1030,8 @@ export function ChatComposer(props) {
833
1030
  const executeCommand = async (command, tab = false) => {
834
1031
  const token = commandQuery();
835
1032
  const heldHistory = commandSheetHeld();
1033
+ const addSelection = addSearch();
1034
+ const fromAdd = addSelection !== undefined;
836
1035
  if (!token || blocked()) return;
837
1036
  if (command.disabled || command.reason) {
838
1037
  // A new object each time, so choosing the same row again inserts and announces its reason again.
@@ -848,19 +1047,33 @@ export function ChatComposer(props) {
848
1047
  }
849
1048
  if (command.scope && (tab || !command.action && !command.mention && !command.attachFiles) && narrowCommand(command)) return;
850
1049
  if (command.attachFiles) {
851
- attachFilesFromCommand(token);
1050
+ if (!canSelectFiles()) return;
1051
+ // The field has the focus before the selection opens, so closing it without a file comes back there.
1052
+ endAddSearch(true);
1053
+ attachFilesFromCommand(token, addSelection);
852
1054
  return;
853
1055
  }
1056
+ if (command.open) {
1057
+ await openFromCommand(command, command.open, token);
1058
+ return;
1059
+ }
1060
+ // The field has the focus again before an action opens a dialog, so the dialog gives it back there.
1061
+ endAddSearch(true);
1062
+ // From "+" a reference joins the text at the caret, apart from a word right before it.
1063
+ const lead = value => fromAdd && token.start > 0 && !/\s/.test(value[token.start - 1] ?? "") ? " " : "";
854
1064
  const previous = snapshot();
855
1065
  const key = props.draftKey;
856
- const replacement = command.mention ? command.mention.name + " " : "";
1066
+ const replacement = command.mention ? lead(props.value) + command.mention.name + " " : "";
857
1067
  const nextValue = props.value.slice(0, token.start) + replacement + props.value.slice(token.end);
858
1068
  const nextMentions = reconcileChatMentions(props.value, nextValue, mentions());
859
- if (command.mention) nextMentions.push({
860
- start: token.start,
861
- end: token.start + command.mention.name.length,
862
- attachment: command.mention
863
- });
1069
+ if (command.mention) {
1070
+ const start = token.start + lead(props.value).length;
1071
+ nextMentions.push({
1072
+ start,
1073
+ end: start + command.mention.name.length,
1074
+ attachment: command.mention
1075
+ });
1076
+ }
864
1077
  // A cancelled or failed command leaves no undo step behind: restoring puts the history back as well.
865
1078
  const previousHistory = {
866
1079
  entries: [...history],
@@ -910,16 +1123,17 @@ export function ChatComposer(props) {
910
1123
  restore();
911
1124
  restored = false;
912
1125
  const name = result.mention.name;
913
- const withMention = previous.value.slice(0, token.start) + name + " " + previous.value.slice(token.end);
1126
+ const start = token.start + lead(previous.value).length;
1127
+ const withMention = previous.value.slice(0, token.start) + lead(previous.value) + name + " " + previous.value.slice(token.end);
914
1128
  edit(withMention, [...reconcileChatMentions(previous.value, withMention, previous.mentions), {
915
- start: token.start,
916
- end: token.start + name.length,
1129
+ start,
1130
+ end: start + name.length,
917
1131
  attachment: result.mention
918
1132
  }]);
919
1133
  setDismissed(true);
920
1134
  mentioned = {
921
1135
  value: withMention,
922
- caret: token.start + name.length + 1
1136
+ caret: start + name.length + 1
923
1137
  };
924
1138
  showCommandStatus();
925
1139
  } else {
@@ -953,7 +1167,7 @@ export function ChatComposer(props) {
953
1167
  if (props.draftKey !== key) return;
954
1168
  if (restored || props.value === nextValue || mentioned && props.value === mentioned.value) {
955
1169
  const position = restored ? token.end : mentioned?.caret ?? token.start + replacement.length;
956
- textareaRef?.setSelectionRange(position, position);
1170
+ textareaRef?.setSelectionRange(restored && addSelection ? addSelection.start : position, restored && addSelection ? addSelection.end : position);
957
1171
  }
958
1172
  syncCaret();
959
1173
  autoResize();
@@ -961,6 +1175,110 @@ export function ChatComposer(props) {
961
1175
  });
962
1176
  };
963
1177
 
1178
+ /**
1179
+ * Runs an `open` row with the token and the list in place. Success removes the token in one undo step; `failed`
1180
+ * stays in the row while the list is still there, otherwise the status line says it, so it never ends in silence.
1181
+ */
1182
+ const openFromCommand = async (command, open, token) => {
1183
+ const key = props.draftKey;
1184
+ const row = chatCommandKey(command);
1185
+ const value = props.value;
1186
+ const nextValue = value.slice(0, token.start) + value.slice(token.end);
1187
+ const nextMentions = reconcileChatMentions(value, nextValue, mentions());
1188
+ // Still the same draft and `/…`, so the row is where the person chose it.
1189
+ const inPlace = () => !disposed && props.draftKey === key && props.value === value && commandQuery()?.start === token.start;
1190
+ const heldHistory = commandSheetHeld();
1191
+ // Closing the list or leaving the composer cancels the opening; the row ends at once, without waiting for it.
1192
+ const controller = new AbortController();
1193
+ const run = {
1194
+ controller,
1195
+ key,
1196
+ label: command.busyLabel ?? chatMessages().commandOpening,
1197
+ follow: false,
1198
+ moved: false
1199
+ };
1200
+ openingRun = run;
1201
+ const cancelled = new Promise(resolve => controller.signal.addEventListener("abort", () => resolve("cancelled"), {
1202
+ once: true
1203
+ }));
1204
+ setOpenFailures(({
1205
+ [row]: _,
1206
+ ...rest
1207
+ }) => rest);
1208
+ setOpeningRow(row);
1209
+ setExecuting(true);
1210
+ // An opening that ends without leaving, by failure, rejection or cancelling, also aborts `signal`, so the opener
1211
+ // can put back what it saved without the token.
1212
+ const finish = (left = false) => {
1213
+ if (!left) controller.abort();
1214
+ if (openingRun === run) openingRun = undefined;
1215
+ if (commandStatus()?.tone === "busy") showCommandStatus();
1216
+ setOpeningRow(undefined);
1217
+ setExecuting(false);
1218
+ };
1219
+ let result;
1220
+ try {
1221
+ if (heldHistory) {
1222
+ await dialogHistorySettled();
1223
+ if (!inPlace()) {
1224
+ finish();
1225
+ return;
1226
+ }
1227
+ }
1228
+ result = await Promise.race([open({
1229
+ value: nextValue,
1230
+ mentions: nextMentions
1231
+ }, controller.signal, () => {
1232
+ run.follow = true;
1233
+ }), cancelled]);
1234
+ } catch (error) {
1235
+ finish();
1236
+ if (disposed) {
1237
+ props.onError?.(error);
1238
+ return;
1239
+ }
1240
+ if (inPlace()) closeCommandList();
1241
+ showCommandStatus({
1242
+ tone: "error",
1243
+ text: error instanceof Error && error.message ? error.message : chatMessages().commandFailed
1244
+ });
1245
+ return;
1246
+ }
1247
+ if (!result) {
1248
+ // Success: the token leaves and the list closes in the same step that ends the opening, so a phone sheet never
1249
+ // takes its Back entry again while the page navigates.
1250
+ batch(() => {
1251
+ if (!disposed && props.draftKey === key && props.value === value) {
1252
+ edit(nextValue, nextMentions);
1253
+ setDismissed(true);
1254
+ }
1255
+ endAddSearch(!disposed && props.draftKey === key);
1256
+ finish(true);
1257
+ });
1258
+ queueMicrotask(() => {
1259
+ if (disposed || props.draftKey !== key || props.value !== nextValue) return;
1260
+ textareaRef?.setSelectionRange(token.start, token.start);
1261
+ syncCaret();
1262
+ autoResize();
1263
+ });
1264
+ return;
1265
+ }
1266
+ finish();
1267
+ if (result === "cancelled") return;
1268
+ if (inPlace()) {
1269
+ setOpenFailures(failures => ({
1270
+ ...failures,
1271
+ [row]: result.failed
1272
+ }));
1273
+ setAnnouncement({
1274
+ text: result.failed
1275
+ });
1276
+ } else if (disposed) props.onError?.(new Error(result.failed));else showCommandStatus({
1277
+ tone: "error",
1278
+ text: result.failed
1279
+ });
1280
+ };
1281
+
964
1282
  /** Replaces `start`–`end` with the emoji through the editing path, so the browser's undo restores the shortcode. */
965
1283
  const insertEmoji = (start, end, emoji) => {
966
1284
  if (!textareaRef || editBlocked()) return;
@@ -1001,9 +1319,66 @@ export function ChatComposer(props) {
1001
1319
  if (untouched && textarea.matches(":focus")) convert(loaded);
1002
1320
  }, () => undefined);
1003
1321
  };
1322
+
1323
+ /** Moving through and choosing in the command list, from the field or from the search field opened by "+". */
1324
+ const commandListKey = event => {
1325
+ if (!commandsOpen()) return false;
1326
+ const matches = commandMatches();
1327
+ if (event.key === "ArrowUp" || event.key === "ArrowDown") {
1328
+ event.preventDefault();
1329
+ const index = matches.indexOf(selectedCommand());
1330
+ const next = matches[nextChatCommandIndex(index, matches.length, event.key === "ArrowUp" ? -1 : 1)];
1331
+ setActiveCommandKey(next ? chatCommandKey(next) : undefined);
1332
+ setCommandMoved(true);
1333
+ setAnnouncement(undefined);
1334
+ return true;
1335
+ }
1336
+ // Ctrl/⌘+Enter sends, also while suggestions are open.
1337
+ const plain = !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey;
1338
+ if (plain && (event.key === "Tab" || event.key === "Enter") && matches.length > 0) {
1339
+ event.preventDefault();
1340
+ const command = selectedCommand();
1341
+ // A server row answering an earlier query may no longer match; wait for fresh results. Local rows match now.
1342
+ const row = commandModel().rows.find(row => chatCommandKey(row.command) === selectedCommandKey());
1343
+ const staleServerRow = row?.server && (searchResultsQuery() !== commandQuery()?.query || searchResultsScope() !== parsedCommandQuery().scope?.id);
1344
+ if (staleServerRow) return true;
1345
+ if (command) void executeCommand(command, event.key === "Tab");
1346
+ return true;
1347
+ }
1348
+ if (event.key === "Escape") {
1349
+ event.preventDefault();
1350
+ closeCommandList();
1351
+ return true;
1352
+ }
1353
+ return false;
1354
+ };
1355
+ const onAddSearchKeyDown = event => {
1356
+ // Nothing chooses while an input method composes, as in the field.
1357
+ if (event.isComposing || event.keyCode === 229 || addComposing) return;
1358
+ if (event.key === "Escape" && openingRun) {
1359
+ event.preventDefault();
1360
+ openingRun.controller.abort();
1361
+ endAddSearch(true);
1362
+ return;
1363
+ }
1364
+ if (commandListKey(event)) return;
1365
+ // Ctrl/⌘+Enter sends from the search field as from the field, with the list closed first.
1366
+ if (event.key === "Enter" && (event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
1367
+ event.preventDefault();
1368
+ endAddSearch(true);
1369
+ void submit();
1370
+ }
1371
+ };
1004
1372
  const onKeyDown = event => {
1005
1373
  // WebKit ends the composition before the keydown of the Enter that confirms it; that keydown keeps keyCode 229.
1006
1374
  if (event.isComposing || event.keyCode === 229 || composing()) return;
1375
+ // Escape cancels a running `open` row, also once its list is gone because the draft moved to a new chat.
1376
+ if (event.key === "Escape" && openingRun) {
1377
+ event.preventDefault();
1378
+ openingRun.controller.abort();
1379
+ if (commandsOpen()) closeCommandList();
1380
+ return;
1381
+ }
1007
1382
  if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "z") {
1008
1383
  event.preventDefault();
1009
1384
  restoreHistory(event.shiftKey ? 1 : -1);
@@ -1028,38 +1403,11 @@ export function ChatComposer(props) {
1028
1403
  return;
1029
1404
  }
1030
1405
  }
1031
- const matches = commandMatches();
1032
- if (commandsOpen()) {
1033
- if (event.key === "ArrowUp" || event.key === "ArrowDown") {
1034
- event.preventDefault();
1035
- const index = matches.indexOf(selectedCommand());
1036
- const next = matches[nextChatCommandIndex(index, matches.length, event.key === "ArrowUp" ? -1 : 1)];
1037
- setActiveCommandKey(next ? chatCommandKey(next) : undefined);
1038
- setCommandMoved(true);
1039
- setAnnouncement(undefined);
1040
- return;
1041
- }
1042
- // Ctrl/⌘+Enter sends, also while suggestions are open.
1043
- const plain = !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey;
1044
- if (plain && (event.key === "Tab" || event.key === "Enter") && matches.length > 0) {
1045
- event.preventDefault();
1046
- const command = selectedCommand();
1047
- // A server row answering an earlier query may no longer match; wait for fresh results. Local rows match now.
1048
- const row = commandModel().rows.find(row => chatCommandKey(row.command) === selectedCommandKey());
1049
- const staleServerRow = row?.server && (searchResultsQuery() !== commandQuery()?.query || searchResultsScope() !== parsedCommandQuery().scope?.id);
1050
- if (staleServerRow) return;
1051
- if (command) void executeCommand(command, event.key === "Tab");
1052
- return;
1053
- }
1054
- if (event.key === "Escape") {
1055
- event.preventDefault();
1056
- setClosedAt(commandQuery()?.start);
1057
- return;
1058
- }
1059
- }
1406
+ if (commandListKey(event)) return;
1060
1407
  const format = props.formatting && textareaRef ? formatShortcut(event) : undefined;
1061
1408
  if (format && textareaRef) {
1062
1409
  event.preventDefault();
1410
+ if (editBlocked()) return;
1063
1411
  format.run(textareaRef);
1064
1412
  return;
1065
1413
  }
@@ -1182,8 +1530,46 @@ export function ChatComposer(props) {
1182
1530
  }
1183
1531
  });
1184
1532
 
1533
+ /** "+" as the way to the slash list on keyboards without a handy "/". */
1534
+ const addSearchButton = (extraClass = "") => _$createComponent(Tooltip.Trigger, {
1535
+ type: "button",
1536
+ "class": `k2b-chat-composer__icon-action${extraClass}`,
1537
+ get ["aria-label"]() {
1538
+ return chatMessages().addMenu;
1539
+ },
1540
+ "aria-haspopup": "listbox",
1541
+ get ["aria-expanded"]() {
1542
+ return addSearch() !== undefined;
1543
+ },
1544
+ get ["aria-controls"]() {
1545
+ return addSearch() ? commandListId : undefined;
1546
+ },
1547
+ get content() {
1548
+ return chatMessages().addMenu;
1549
+ },
1550
+ get disabled() {
1551
+ return blocked();
1552
+ },
1553
+ onMouseDown: keepFieldFocus,
1554
+ onClick: toggleAddSearch,
1555
+ get children() {
1556
+ return _$ssr(_tmpl$3, _$ssrAttribute("class", addingFiles() ? "ti ti-loader-2 k2b-spin" : "ti ti-plus", false));
1557
+ }
1558
+ });
1559
+
1185
1560
  /** The conversation's attach button opens the file choice directly; other actions turn it into the add menu. */
1186
1561
  const attachControl = () => _$createComponent(Show, {
1562
+ get when() {
1563
+ return !addOpensCommands();
1564
+ },
1565
+ get fallback() {
1566
+ return addSearchButton(" k2b-chat-composer__attach");
1567
+ },
1568
+ get children() {
1569
+ return attachChoice();
1570
+ }
1571
+ });
1572
+ const attachChoice = () => _$createComponent(Show, {
1187
1573
  get when() {
1188
1574
  return props.menuActions?.length;
1189
1575
  },
@@ -1467,7 +1853,7 @@ export function ChatComposer(props) {
1467
1853
  children: part => _$ssr(_tmpl$10, Boolean(part.mention) ? "k2b-chat-composer__mention" : "", _$escape(part.text))
1468
1854
  })));
1469
1855
  }
1470
- })), 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, {
1856
+ })), mentions().length > 0 ? "k2b-chat-composer__textarea--highlighted" : "", _$ssrAttribute("value", _$escape(props.value, true), false), _$ssrAttribute("disabled", editBlocked() && openingRow() === undefined, true) + _$ssrAttribute("readonly", _$escape(openingRow(), true) !== _$escape(undefined, true), false) + _$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", fieldOwnsList() ? "combobox" : _$escape(undefined, true), false) + _$ssrAttribute("aria-autocomplete", fieldOwnsList() ? "list" : _$escape(undefined, true), false) + _$ssrAttribute("aria-controls", fieldOwnsList() ? _$escape(commandListId, true) : _$escape(undefined, true), false) + _$ssrAttribute("aria-expanded", fieldOwnsList() ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("aria-activedescendant", !fieldOwnsList() ? _$escape(undefined, true) : emojiOpen() ? `${_$escape(commandListId, true)}-${_$escape(selectedCommandIndex(), true)}` : selectedCommand() ? _$escape(commandOptionId(selectedCommand()), true) : _$escape(undefined, true), false), _$escape(_$createComponent(Show, {
1471
1857
  get when() {
1472
1858
  return conversation();
1473
1859
  },
@@ -1480,7 +1866,9 @@ export function ChatComposer(props) {
1480
1866
  const renderCommandRow = command => {
1481
1867
  const highlight = () => commandRowMap().get(command)?.highlight;
1482
1868
  const title = () => command.label ?? command.name;
1483
- 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, {
1869
+ const opening = () => openingRow() === chatCommandKey(command);
1870
+ const failure = () => openFailures()[chatCommandKey(command)];
1871
+ 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-busy", opening() ? "true" : _$escape(undefined, 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, {
1484
1872
  get when() {
1485
1873
  return highlight();
1486
1874
  },
@@ -1490,10 +1878,20 @@ export function ChatComposer(props) {
1490
1878
  children: range => [title().slice(0, range()[0]), _$ssr(_tmpl$14, _$escape(title().slice(range()[0], range()[1]))), title().slice(range()[1])]
1491
1879
  })), _$escape(_$createComponent(Show, {
1492
1880
  get when() {
1493
- return command.reason ?? command.description;
1881
+ return _$memo(() => !!command.open)() && !command.reason;
1882
+ },
1883
+ get fallback() {
1884
+ return _$createComponent(Show, {
1885
+ get when() {
1886
+ return command.reason ?? command.description;
1887
+ },
1888
+ get children() {
1889
+ return _$ssr(_tmpl$15, _$ssrAttribute("id", command.reason ? `${_$escape(commandOptionId(command), true)}-reason` : _$escape(undefined, true), false), _$escape(command.reason ?? command.description));
1890
+ }
1891
+ });
1494
1892
  },
1495
1893
  get children() {
1496
- return _$ssr(_tmpl$11, _$ssrAttribute("id", command.reason ? `${_$escape(commandOptionId(command), true)}-reason` : _$escape(undefined, true), false), _$escape(command.reason ?? command.description));
1894
+ return _$ssr(_tmpl$11, _$ssrAttribute("data-state", opening() ? "busy" : failure() ? "failed" : _$escape(undefined, true), false), _$ssrAttribute("aria-hidden", opening() ? "true" : _$escape(undefined, true), false), _$escape(failure() ?? command.description), _$ssrAttribute("aria-hidden", opening() ? _$escape(undefined, true) : "true", false), _$escape(command.busyLabel ?? chatMessages().commandOpening));
1497
1895
  }
1498
1896
  })), _$escape(_$createComponent(Show, {
1499
1897
  get when() {
@@ -1504,25 +1902,25 @@ export function ChatComposer(props) {
1504
1902
  }
1505
1903
  })));
1506
1904
  };
1507
- return _$ssr(_tmpl$25, _$escape(_$createComponent(Show, {
1905
+ return _$ssr(_tmpl$27, _$escape(_$createComponent(Show, {
1508
1906
  get when() {
1509
1907
  return suggestionsOpen() || hasAccessory();
1510
1908
  },
1511
1909
  get children() {
1512
- 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, {
1910
+ return _$ssr(_tmpl$22, _$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, {
1513
1911
  get when() {
1514
1912
  return emojiOpen();
1515
1913
  },
1516
1914
  get children() {
1517
- return _$ssr(_tmpl$15, _$ssrAttribute("id", _$escape(commandListId, true), false), _$ssrAttribute("aria-label", _$escape(chatMessages().emojiSuggestions, true), false), _$escape(_$createComponent(For, {
1915
+ return _$ssr(_tmpl$16, _$ssrAttribute("id", _$escape(commandListId, true), false), _$ssrAttribute("aria-label", _$escape(chatMessages().emojiSuggestions, true), false), _$escape(_$createComponent(For, {
1518
1916
  get each() {
1519
1917
  return emojiMatches();
1520
1918
  },
1521
- 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, {
1919
+ children: (entry, index) => _$ssr(_tmpl$28, `${_$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, {
1522
1920
  get when() {
1523
1921
  return entry.shortcodes[0];
1524
1922
  },
1525
- children: code => _$ssr(_tmpl$27, _$escape(code()))
1923
+ children: code => _$ssr(_tmpl$29, _$escape(code()))
1526
1924
  })))
1527
1925
  })));
1528
1926
  }
@@ -1531,7 +1929,14 @@ export function ChatComposer(props) {
1531
1929
  return commandsOpen();
1532
1930
  },
1533
1931
  get children() {
1534
- 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, {
1932
+ return _$ssr(_tmpl$21, _$ssrAttribute("data-search", addSearch() ? "" : _$escape(undefined, true), false), _$escape(_$createComponent(Show, {
1933
+ get when() {
1934
+ return addSearch();
1935
+ },
1936
+ get children() {
1937
+ return _$ssr(_tmpl$17, _$ssrAttribute("value", _$escape(addQuery(), true), false) + _$ssrAttribute("placeholder", _$escape(chatMessages().commandSearch, true), false) + _$ssrAttribute("aria-label", _$escape(chatMessages().commandSearchLabel, true), false), _$ssrAttribute("aria-controls", _$escape(commandListId, true), false), _$ssrAttribute("aria-activedescendant", selectedCommand() ? _$escape(commandOptionId(selectedCommand()), true) : _$escape(undefined, true), false), _$ssrAttribute("readonly", _$escape(openingRow(), true) !== _$escape(undefined, true), false));
1938
+ }
1939
+ })), _$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, {
1535
1940
  get each() {
1536
1941
  return commandModel().groups.map(commandGroupKey);
1537
1942
  },
@@ -1550,25 +1955,25 @@ export function ChatComposer(props) {
1550
1955
  });
1551
1956
  },
1552
1957
  get children() {
1553
- 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, {
1958
+ return _$ssr(_tmpl$31, _$ssrAttribute("aria-labelledby", _$escape(headingId(), true), false), _$ssrAttribute("id", _$escape(headingId(), true), false), _$escape(group()?.scopeTitle ?? group()?.section?.label), _$escape(_$createComponent(Show, {
1554
1959
  get when() {
1555
1960
  return group()?.loading;
1556
1961
  },
1557
1962
  get children() {
1558
- return _$ssr(_tmpl$28);
1963
+ return _$ssr(_tmpl$30);
1559
1964
  }
1560
1965
  })), _$escape(_$createComponent(For, {
1561
1966
  get each() {
1562
1967
  return rows();
1563
1968
  },
1564
1969
  children: renderCommandRow
1565
- })), _$escape(_$createComponent(Show, {
1566
- get when() {
1567
- return group()?.placeholder;
1970
+ })), _$escape(_$createComponent(Index, {
1971
+ get each() {
1972
+ return Array.from({
1973
+ length: Math.max(group()?.placeholder ? 1 : 0, group()?.pendingRows ?? 0)
1974
+ });
1568
1975
  },
1569
- get children() {
1570
- return _$ssr(_tmpl$29);
1571
- }
1976
+ children: () => _$ssr(_tmpl$32)
1572
1977
  })));
1573
1978
  }
1574
1979
  });
@@ -1578,21 +1983,21 @@ export function ChatComposer(props) {
1578
1983
  return _$memo(() => !!(searching() && foundCommandCount() === 0))() && !commandModel().groups.some(group => group.section || group.scopeTitle);
1579
1984
  },
1580
1985
  get children() {
1581
- return _$ssr(_tmpl$16, _$escape(messages().loading));
1986
+ return _$ssr(_tmpl$18, _$escape(messages().loading));
1582
1987
  }
1583
1988
  })), _$escape(_$createComponent(Show, {
1584
1989
  get when() {
1585
1990
  return searchError();
1586
1991
  },
1587
1992
  get children() {
1588
- return _$ssr(_tmpl$17, _$escape(messages().error));
1993
+ return _$ssr(_tmpl$19, _$escape(messages().error));
1589
1994
  }
1590
1995
  })), _$escape(_$createComponent(Show, {
1591
1996
  get when() {
1592
- return _$memo(() => !!(!searching() && !searchError()))() && foundCommandCount() === 0;
1997
+ return _$memo(() => !!(!searching() && !searchError() && foundCommandCount() === 0))() && !commandModel().groups.some(group => group.pendingRows);
1593
1998
  },
1594
1999
  get children() {
1595
- return _$ssr(_tmpl$18, !parsedCommandQuery().query.trim() ? _$escape(messages().noResults) : _$escape(chatMessages().commandsNothingFound({
2000
+ return _$ssr(_tmpl$20, !parsedCommandQuery().query.trim() ? _$escape(messages().noResults) : _$escape(chatMessages().commandsNothingFound({
1596
2001
  query: parsedCommandQuery().query.trim()
1597
2002
  })));
1598
2003
  }
@@ -1611,18 +2016,18 @@ export function ChatComposer(props) {
1611
2016
  return showHint();
1612
2017
  },
1613
2018
  get children() {
1614
- return _$ssr(_tmpl$21, _$escape(_$createComponent(Show, {
2019
+ return _$ssr(_tmpl$23, _$escape(_$createComponent(Show, {
1615
2020
  get when() {
1616
2021
  return dictationStatus();
1617
2022
  },
1618
2023
  get fallback() {
1619
- return _$ssr(_tmpl$31, _$escape(hint()));
2024
+ return _$ssr(_tmpl$33, _$escape(hint()));
1620
2025
  },
1621
- 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, {
2026
+ children: status => [_$ssr(_tmpl$34, _$ssrAttribute("data-state", _$escape(dictation() ?? undefined, true), false), _$ssrAttribute("class", _$escape(status().icon, true), false), _$escape(status().text)), _$createComponent(Show, {
1622
2027
  get when() {
1623
2028
  return _$memo(() => dictation() === "refined")() ? props.microphone?.onRestoreOriginal : undefined;
1624
2029
  },
1625
- children: restore => _$ssr(_tmpl$33, _$ssrAttribute("disabled", microphoneDisabled(), true), _$escape(chatMessages().restoreOriginal))
2030
+ children: restore => _$ssr(_tmpl$35, _$ssrAttribute("disabled", microphoneDisabled(), true), _$escape(chatMessages().restoreOriginal))
1626
2031
  })]
1627
2032
  })));
1628
2033
  }
@@ -1631,16 +2036,16 @@ export function ChatComposer(props) {
1631
2036
  return attachments().length > 0;
1632
2037
  },
1633
2038
  get children() {
1634
- return _$ssr(_tmpl$22, _$ssrAttribute("aria-label", _$escape(chatMessages().attachments, true), false), _$escape(_$createComponent(For, {
2039
+ return _$ssr(_tmpl$24, _$ssrAttribute("aria-label", _$escape(chatMessages().attachments, true), false), _$escape(_$createComponent(For, {
1635
2040
  get each() {
1636
2041
  return attachments();
1637
2042
  },
1638
- children: attachment => _$ssr(_tmpl$35, `k2b-chat-composer__attachment-content ${Boolean(attachment.action) ? "k2b-chat-composer__attachment-content--action" : ""}`, _$escape(_$createComponent(Show, {
2043
+ children: attachment => _$ssr(_tmpl$37, `k2b-chat-composer__attachment-content ${Boolean(attachment.action) ? "k2b-chat-composer__attachment-content--action" : ""}`, _$escape(_$createComponent(Show, {
1639
2044
  get when() {
1640
2045
  return attachment.href;
1641
2046
  },
1642
2047
  get fallback() {
1643
- return _$ssr(_tmpl$37, _$escape(_$createComponent(Show, {
2048
+ return _$ssr(_tmpl$39, _$escape(_$createComponent(Show, {
1644
2049
  get when() {
1645
2050
  return _$memo(() => attachment.kind === "image")() && attachment.previewUrl;
1646
2051
  },
@@ -1648,11 +2053,11 @@ export function ChatComposer(props) {
1648
2053
  return _$ssr(_tmpl$3, _$ssrAttribute("class", _$escape(attachmentIcon(attachment), true), false));
1649
2054
  },
1650
2055
  get children() {
1651
- return _$ssr(_tmpl$36, _$ssrAttribute("src", _$escape(attachment.previewUrl, true), false) + _$ssrAttribute("alt", _$escape(attachment.alt ?? "", true), false));
2056
+ return _$ssr(_tmpl$38, _$ssrAttribute("src", _$escape(attachment.previewUrl, true), false) + _$ssrAttribute("alt", _$escape(attachment.alt ?? "", true), false));
1652
2057
  }
1653
2058
  })), _$escape(renderAttachmentCopy(attachment)));
1654
2059
  },
1655
- children: href => _$ssr(_tmpl$38, _$ssrAttribute("href", _$escape(href(), true), false), _$ssrAttribute("aria-label", _$escape(chatMessages().openInNewTab({
2060
+ children: href => _$ssr(_tmpl$40, _$ssrAttribute("href", _$escape(href(), true), false), _$ssrAttribute("aria-label", _$escape(chatMessages().openInNewTab({
1656
2061
  name: attachment.name
1657
2062
  }), true), false), _$escape(_$createComponent(Show, {
1658
2063
  get when() {
@@ -1662,14 +2067,14 @@ export function ChatComposer(props) {
1662
2067
  return _$ssr(_tmpl$3, _$ssrAttribute("class", _$escape(attachmentIcon(attachment), true), false));
1663
2068
  },
1664
2069
  get children() {
1665
- return _$ssr(_tmpl$36, _$ssrAttribute("src", _$escape(attachment.previewUrl, true), false) + _$ssrAttribute("alt", _$escape(attachment.alt ?? "", true), false));
2070
+ return _$ssr(_tmpl$38, _$ssrAttribute("src", _$escape(attachment.previewUrl, true), false) + _$ssrAttribute("alt", _$escape(attachment.alt ?? "", true), false));
1666
2071
  }
1667
2072
  })), _$escape(renderAttachmentCopy(attachment)))
1668
2073
  })), _$escape(_$createComponent(Show, {
1669
2074
  get when() {
1670
2075
  return attachment.action;
1671
2076
  },
1672
- children: action => _$ssr(_tmpl$39, _$ssrAttribute("disabled", blocked() || action().disabled, true), _$escape(_$createComponent(Show, {
2077
+ children: action => _$ssr(_tmpl$41, _$ssrAttribute("disabled", blocked() || action().disabled, true), _$escape(_$createComponent(Show, {
1673
2078
  get when() {
1674
2079
  return action().icon;
1675
2080
  },
@@ -1680,7 +2085,7 @@ export function ChatComposer(props) {
1680
2085
  return props.onAttachmentsChange;
1681
2086
  },
1682
2087
  get children() {
1683
- return _$ssr(_tmpl$34, _$ssrAttribute("aria-label", _$escape(selectMessages().removeNamed({
2088
+ return _$ssr(_tmpl$36, _$ssrAttribute("aria-label", _$escape(selectMessages().removeNamed({
1684
2089
  name: attachment.name
1685
2090
  }), true), false), _$ssrAttribute("disabled", blocked(), true));
1686
2091
  }
@@ -1697,19 +2102,29 @@ export function ChatComposer(props) {
1697
2102
  return props.error;
1698
2103
  },
1699
2104
  get children() {
1700
- return _$ssr(_tmpl$23, _$escape(props.error));
2105
+ return _$ssr(_tmpl$25, _$escape(props.error));
1701
2106
  }
1702
- }), formatRow(), _$ssr(_tmpl$40, _$escape(_$createComponent(Show, {
2107
+ }), formatRow(), _$ssr(_tmpl$42, _$escape(_$createComponent(Show, {
1703
2108
  get when() {
1704
2109
  return footer();
1705
2110
  },
1706
2111
  get fallback() {
1707
- return [_$ssr(_tmpl$41, _$escape(props.footerTools), _$escape(_$createComponent(Show, {
2112
+ return [_$ssr(_tmpl$43, _$escape(props.footerTools), _$escape(_$createComponent(Show, {
1708
2113
  get when() {
1709
- return hasAddMenu();
2114
+ return addOpensCommands();
2115
+ },
2116
+ get fallback() {
2117
+ return _$createComponent(Show, {
2118
+ get when() {
2119
+ return hasAddMenu();
2120
+ },
2121
+ get children() {
2122
+ return addMenu("ti ti-plus");
2123
+ }
2124
+ });
1710
2125
  },
1711
2126
  get children() {
1712
- return addMenu("ti ti-plus");
2127
+ return addSearchButton();
1713
2128
  }
1714
2129
  })), _$escape(fileInput()), _$escape(formatToggle()), _$escape(emojiButton()), _$escape(_$createComponent(Show, {
1715
2130
  get when() {
@@ -1757,7 +2172,7 @@ export function ChatComposer(props) {
1757
2172
  }
1758
2173
  });
1759
2174
  }
1760
- })), _$escape(props.modelDetails)), _$ssr(_tmpl$42, _$escape(_$createComponent(For, {
2175
+ })), _$escape(props.modelDetails)), _$ssr(_tmpl$44, _$escape(_$createComponent(For, {
1761
2176
  get each() {
1762
2177
  return props.contextActions;
1763
2178
  },
@@ -1809,9 +2224,9 @@ export function ChatComposer(props) {
1809
2224
  return props.error;
1810
2225
  },
1811
2226
  get children() {
1812
- return _$ssr(_tmpl$23, _$escape(props.error));
2227
+ return _$ssr(_tmpl$25, _$escape(props.error));
1813
2228
  }
1814
- }), formatRow(), _$ssr(_tmpl$24, _$escape(attachControl()), _$escape(fileInput()), _$escape(field()), _$escape(formatToggle()), _$escape(emojiButton()), _$escape(microphoneControl()), _$escape(sendControl()))];
2229
+ }), formatRow(), _$ssr(_tmpl$26, _$escape(attachControl()), _$escape(fileInput()), _$escape(field()), _$escape(formatToggle()), _$escape(emojiButton()), _$escape(microphoneControl()), _$escape(sendControl()))];
1815
2230
  }
1816
2231
  })), _$escape(_$createComponent(Show, {
1817
2232
  get when() {
@@ -1837,18 +2252,18 @@ export function ChatComposer(props) {
1837
2252
  return suggestionAnnouncement();
1838
2253
  },
1839
2254
  keyed: true,
1840
- children: announced => _$ssr(_tmpl$43, _$escape(announced.text))
2255
+ children: announced => _$ssr(_tmpl$45, _$escape(announced.text))
1841
2256
  })), _$escape(_$createComponent(Show, {
1842
2257
  get when() {
1843
2258
  return _$memo(() => !!!suggestionsOpen())() && commandStatus();
1844
2259
  },
1845
- 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))
2260
+ children: status => _$ssr(_tmpl$46, _$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))
1846
2261
  })), _$escape(_$createComponent(Show, {
1847
2262
  get when() {
1848
2263
  return _$memo(() => !!suggestionsOpen())() && announcement();
1849
2264
  },
1850
2265
  keyed: true,
1851
- children: announced => _$ssr(_tmpl$45, _$escape(announced.text))
2266
+ children: announced => _$ssr(_tmpl$47, _$escape(announced.text))
1852
2267
  })));
1853
2268
  }
1854
2269
  //# sourceMappingURL=ChatComposer.js.map