@k2b/ui 0.26.0 → 0.27.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 (65) hide show
  1. package/dist/browser/chat/ChatComposer.js +811 -406
  2. package/dist/browser/chat/ChatComposer.js.map +1 -1
  3. package/dist/browser/chat/chat-behavior.js +4 -0
  4. package/dist/browser/chat/chat-behavior.js.map +1 -1
  5. package/dist/browser/chat/chat-messages.js +14 -0
  6. package/dist/browser/chat/chat-messages.js.map +1 -1
  7. package/dist/browser/chat/command-search.js +170 -0
  8. package/dist/browser/chat/command-search.js.map +1 -0
  9. package/dist/browser/chat/composer-document.js +1 -1
  10. package/dist/browser/chat/composer-document.js.map +1 -1
  11. package/dist/browser/chat/index.js.map +1 -1
  12. package/dist/browser/content/CalendarDayView.js +28 -3
  13. package/dist/browser/content/CalendarDayView.js.map +1 -1
  14. package/dist/browser/inputs/Combobox.js +4 -2
  15. package/dist/browser/inputs/Combobox.js.map +1 -1
  16. package/dist/browser/inputs/SelectChip.js +43 -12
  17. package/dist/browser/inputs/SelectChip.js.map +1 -1
  18. package/dist/browser/layout/AppWorkspace.js +708 -515
  19. package/dist/browser/layout/AppWorkspace.js.map +1 -1
  20. package/dist/browser/layout/BottomSheet.js +8 -0
  21. package/dist/browser/layout/BottomSheet.js.map +1 -1
  22. package/dist/browser/layout/app-workspace-controller.js +155 -23
  23. package/dist/browser/layout/app-workspace-controller.js.map +1 -1
  24. package/dist/browser/layout/app-workspace-state.js +5 -1
  25. package/dist/browser/layout/app-workspace-state.js.map +1 -1
  26. package/dist/browser/layout/index.js +1 -1
  27. package/dist/browser/layout/index.js.map +1 -1
  28. package/dist/ssr/chat/ChatComposer.js +410 -93
  29. package/dist/ssr/chat/ChatComposer.js.map +1 -1
  30. package/dist/ssr/chat/chat-behavior.js +4 -0
  31. package/dist/ssr/chat/chat-behavior.js.map +1 -1
  32. package/dist/ssr/chat/chat-messages.js +14 -0
  33. package/dist/ssr/chat/chat-messages.js.map +1 -1
  34. package/dist/ssr/chat/command-search.js +170 -0
  35. package/dist/ssr/chat/command-search.js.map +1 -0
  36. package/dist/ssr/chat/composer-document.js +1 -1
  37. package/dist/ssr/chat/composer-document.js.map +1 -1
  38. package/dist/ssr/chat/index.js.map +1 -1
  39. package/dist/ssr/content/CalendarDayView.js +28 -3
  40. package/dist/ssr/content/CalendarDayView.js.map +1 -1
  41. package/dist/ssr/inputs/Combobox.js +4 -2
  42. package/dist/ssr/inputs/Combobox.js.map +1 -1
  43. package/dist/ssr/inputs/SelectChip.js +35 -11
  44. package/dist/ssr/inputs/SelectChip.js.map +1 -1
  45. package/dist/ssr/layout/AppWorkspace.js +242 -105
  46. package/dist/ssr/layout/AppWorkspace.js.map +1 -1
  47. package/dist/ssr/layout/BottomSheet.js +8 -0
  48. package/dist/ssr/layout/BottomSheet.js.map +1 -1
  49. package/dist/ssr/layout/app-workspace-controller.js +155 -23
  50. package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
  51. package/dist/ssr/layout/app-workspace-state.js +5 -1
  52. package/dist/ssr/layout/app-workspace-state.js.map +1 -1
  53. package/dist/ssr/layout/index.js +1 -1
  54. package/dist/ssr/layout/index.js.map +1 -1
  55. package/dist/styles.css +1 -1
  56. package/dist/types/chat/ChatComposer.d.ts +44 -2
  57. package/dist/types/chat/chat-behavior.d.ts +4 -0
  58. package/dist/types/chat/chat-messages.d.ts +7 -0
  59. package/dist/types/chat/command-search.d.ts +39 -0
  60. package/dist/types/chat/index.d.ts +1 -1
  61. package/dist/types/inputs/SelectChip.d.ts +4 -0
  62. package/dist/types/layout/AppWorkspace.d.ts +13 -1
  63. package/dist/types/layout/BottomSheet.d.ts +8 -0
  64. package/dist/types/layout/index.d.ts +1 -1
  65. package/package.json +4 -4
@@ -17,33 +17,41 @@ 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 = ["<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>"],
22
+ _tmpl$13 = ["<b>", "</b>"],
23
+ _tmpl$14 = ["<div class=\"k2b-chat-composer-accessory\"><div", " class=\"k2b-chat-composer__commands\" role=\"listbox\"", ">", "</div></div>"],
24
+ _tmpl$15 = ["<div role=\"status\" class=\"k2b-chat-composer__loading\"><i class=\"ti ti-loader-2 k2b-spin\" aria-hidden=\"true\"></i><strong>", "</strong></div>"],
25
+ _tmpl$16 = ["<div role=\"status\"><i class=\"ti ti-alert-circle\"></i> ", "</div>"],
26
+ _tmpl$17 = ["<div role=\"status\">", "</div>"],
27
+ _tmpl$18 = ["<div class=\"k2b-chat-composer-accessory\"><div", " class=\"k2b-chat-composer__commands\" role=\"listbox\"", ">", "", "", "", "</div></div>"],
28
+ _tmpl$19 = ["<div class=\"k2b-chat-composer-slot\"", "><div style=\"", "\"", ">", "</div>", "", "</div>"],
29
+ _tmpl$20 = ["<div class=\"k2b-chat-composer__hint\" aria-live=\"polite\">", "</div>"],
30
+ _tmpl$21 = ["<div class=\"k2b-chat-composer__attachments\" role=\"list\"", " tabindex=\"0\">", "</div>"],
31
+ _tmpl$22 = ["<div class=\"k2b-chat-composer__error\" role=\"alert\"><i class=\"ti ti-alert-circle\" aria-hidden=\"true\"></i>", "</div>"],
32
+ _tmpl$23 = ["<div class=\"k2b-chat-composer__row\">", "", "<div class=\"k2b-chat-composer__field\">", "<div class=\"k2b-chat-composer__field-tools\">", "", "", "</div></div>", "</div>"],
33
+ _tmpl$24 = ["<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>"],
34
+ _tmpl$25 = ["<button id=\"", "\" type=\"button\" role=\"option\" tabindex=\"-1\"", "><span class=\"k2b-chat-composer__emoji-glyph\" aria-hidden=\"true\">", "</span><strong>", "</strong>", "</button>"],
35
+ _tmpl$26 = ["<small>:", ":</small>"],
36
+ _tmpl$27 = "<i class=\"ti ti-loader-2 k2b-spin\" aria-hidden=\"true\"></i>",
37
+ _tmpl$28 = "<div class=\"k2b-chat-composer__command-placeholder\" aria-hidden=\"true\"><span></span><span><span></span><span></span></span></div>",
38
+ _tmpl$29 = ["<div class=\"k2b-chat-composer__command-group\" role=\"group\"", "><div class=\"k2b-chat-composer__command-heading\"", " role=\"presentation\"><span>", "</span>", "</div>", "", "</div>"],
39
+ _tmpl$30 = ["<span class=\"k2b-chat-composer__hint-text\">", "</span>"],
40
+ _tmpl$31 = ["<span class=\"k2b-chat-composer__dictation\"", "><i", " aria-hidden=\"true\"></i><span>", "</span></span>"],
41
+ _tmpl$32 = ["<button type=\"button\" class=\"k2b-chat-composer__hint-action\"", ">", "</button>"],
42
+ _tmpl$33 = ["<button type=\"button\" class=\"k2b-chat-composer__attachment-remove\"", "", "><i class=\"ti ti-x\" aria-hidden=\"true\"></i></button>"],
43
+ _tmpl$34 = ["<div class=\"k2b-chat-composer__attachment\" role=\"listitem\"><div class=\"", "\">", "", "</div>", "</div>"],
44
+ _tmpl$35 = ["<img", ">"],
45
+ _tmpl$36 = ["<div class=\"k2b-chat-composer__attachment-identity\">", "", "</div>"],
46
+ _tmpl$37 = ["<a class=\"k2b-chat-composer__attachment-link\"", " target=\"_blank\" rel=\"noopener noreferrer\"", ">", "", "</a>"],
47
+ _tmpl$38 = ["<button type=\"button\" class=\"k2b-chat-composer__attachment-action\"", ">", "", "</button>"],
48
+ _tmpl$39 = ["<footer class=\"k2b-chat-composer__footer\">", "</footer>"],
49
+ _tmpl$40 = ["<div class=\"k2b-chat-composer__tools\">", "", "", "", "", "", "", "</div>"],
50
+ _tmpl$41 = ["<div class=\"k2b-chat-composer__submit\">", "", "", "", "", "</div>"],
51
+ _tmpl$42 = ["<span>", "</span>"],
52
+ _tmpl$43 = ["<div class=\"k2b-chat-composer__status-line\"", "><i aria-hidden=\"true\"", "></i><span>", "</span></div>"],
53
+ _tmpl$44 = ["<span class=\"k2b-sr-only\">", "</span>"];
54
+ import { batch, children, createEffect, createMemo, createSignal, createUniqueId, For, onCleanup, onMount, Show, untrack } from "solid-js";
47
55
  import { Dropdown } from "../actions/Dropdown";
48
56
  import { Tooltip } from "../feedback/Tooltip";
49
57
  import { replaceTextareaRange } from "../inputs/editor-dom";
@@ -57,14 +65,25 @@ import { useSelectMessages } from "../inputs/select-messages";
57
65
  import { useLocale } from "../intl/locale";
58
66
  import { useUiMessages } from "../intl/messages";
59
67
  import { ChatContextUsage as ContextUsage } from "./ChatPrimitives";
60
- import { conversationEnterAction, executeChatAction, nextChatCommandIndex, reportChatFailure, runChatSubmission } from "./chat-behavior";
68
+ import { chatCommandKey, conversationEnterAction, executeChatAction, nextChatCommandIndex, reportChatFailure, runChatSubmission } from "./chat-behavior";
61
69
  import { useChatMessages } from "./chat-messages";
70
+ import { chatCommandListModel, parseChatCommandQuery } from "./command-search";
62
71
  import { chatCommandQuery, chatMentionSegments, reconcileChatMentions } from "./composer-document";
63
72
  const composerMaxInputHeight = 309;
73
+ const commandDoneMs = 3_000;
64
74
  /** Emoji suggestions for a `:shortcode`, as many as fit the list without scrolling on a phone. */
65
75
  const emojiSuggestionLimit = 8;
66
76
  // Keep editor undo text within approximately 2 MiB, plus the current edit.
67
77
  const composerUndoBudgetChars = 1_000_000;
78
+
79
+ /** An action can cancel and restore its token, or report a short completion message. */
80
+
81
+ /** A fixed place in the slash list. Sections keep the given order, also while searching. */
82
+
83
+ /** First-word narrowing; Tab writes `words[0]` followed by a space. */
84
+
85
+ /** Passed as the third argument to `searchCommands`. */
86
+
68
87
  /** The row stays short: what a chat message needs. Other Markdown, typed by hand, keeps working. */
69
88
  const formatTools = [{
70
89
  id: "bold",
@@ -107,7 +126,36 @@ export function ChatComposer(props) {
107
126
  const selectMessages = useSelectMessages();
108
127
  const commandListId = `k2b-chat-commands-${createUniqueId().replace(/[^A-Za-z0-9_-]/g, "-")}`;
109
128
  const formatGroupId = `${commandListId}-format`;
129
+ // Emoji suggestions keep their index-based selection and ids.
110
130
  const [selectedCommandIndex, setSelectedCommandIndex] = createSignal(0);
131
+ const [activeCommandKey, setActiveCommandKey] = createSignal();
132
+ const [commandMoved, setCommandMoved] = createSignal(false);
133
+ const commandIds = new Map();
134
+ let commandIdCounter = 0;
135
+ const commandOptionId = command => {
136
+ const key = chatCommandKey(command);
137
+ if (!commandIds.has(key)) commandIds.set(key, commandIdCounter++);
138
+ return `${commandListId}-c${commandIds.get(key)}`;
139
+ };
140
+ const [closedAt, setClosedAt] = createSignal();
141
+ // Where the pending native edit starts; a selection replaced as a whole starts at its beginning.
142
+ let inputStart;
143
+ const [commandStatus, setCommandStatus] = createSignal();
144
+ const [announcement, setAnnouncement] = createSignal();
145
+ let statusTimer;
146
+ let disposed = false;
147
+ const showCommandStatus = status => {
148
+ if (disposed) return;
149
+ clearTimeout(statusTimer);
150
+ setCommandStatus(status);
151
+ if (status?.tone === "done") {
152
+ statusTimer = setTimeout(() => setCommandStatus(undefined), commandDoneMs);
153
+ }
154
+ };
155
+ onCleanup(() => {
156
+ disposed = true;
157
+ clearTimeout(statusTimer);
158
+ });
111
159
  const [addingFiles, setAddingFiles] = createSignal(false);
112
160
  const [submitting, setSubmitting] = createSignal(false);
113
161
  let composerRef;
@@ -131,6 +179,9 @@ export function ChatComposer(props) {
131
179
  const [dismissed, setDismissed] = createSignal(false);
132
180
  const [composing, setComposing] = createSignal(false);
133
181
  const [searchResults, setSearchResults] = createSignal([]);
182
+ const [searchResultsQuery, setSearchResultsQuery] = createSignal();
183
+ const [searchResultsScope, setSearchResultsScope] = createSignal();
184
+ const [suggestionAnnouncement, setSuggestionAnnouncement] = createSignal();
134
185
  const [searching, setSearching] = createSignal(false);
135
186
  const [searchError, setSearchError] = createSignal(false);
136
187
  const [executing, setExecuting] = createSignal(false);
@@ -150,13 +201,51 @@ export function ChatComposer(props) {
150
201
  return token && index ? suggestEmoji(index, token.query, emojiSuggestionLimit) : [];
151
202
  });
152
203
  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()];
204
+ const caretCommandToken = createMemo(() => chatCommandQuery(props.value, caret(), selectionEnd()));
205
+ const caretCommandStart = createMemo(() => caretCommandToken()?.start);
206
+ // Retained server rows belong to one `/`: they go when it does or when another `/` starts.
207
+ createEffect(() => {
208
+ caretCommandStart();
209
+ setSearchResults([]);
210
+ setSearchResultsQuery(undefined);
211
+ });
212
+ // Escape closes the list for one `/` character, wherever the caret or selection goes; only that `/` going away ends it.
213
+ createEffect(() => {
214
+ const closed = closedAt();
215
+ if (closed !== undefined && props.value[closed] !== "/") setClosedAt(undefined);
216
+ });
217
+ const commandQuery = createMemo(() => {
218
+ const token = caretCommandToken();
219
+ return dismissed() || composing() || token?.start === closedAt() ? null : token;
220
+ });
221
+ const parsedCommandQuery = createMemo(() => parseChatCommandQuery(commandQuery()?.query ?? "", props.commandScopes ?? []));
222
+ const currentCommandScope = createMemo(() => parsedCommandQuery().scope?.id);
223
+ createEffect(() => {
224
+ currentCommandScope();
225
+ setSearchResults([]);
226
+ setSearchResultsQuery(undefined);
227
+ });
228
+ const localCommands = createMemo(() => props.commands ?? []);
229
+ const commandModel = createMemo(() => chatCommandListModel({
230
+ query: commandQuery()?.query ?? "",
231
+ scopes: props.commandScopes,
232
+ sections: props.commandSections,
233
+ commands: localCommands(),
234
+ searchResults: searchResultsScope() === parsedCommandQuery().scope?.id ? searchResults() : [],
235
+ searchResultsFresh: searchResultsQuery() === commandQuery()?.query && searchResultsScope() === parsedCommandQuery().scope?.id,
236
+ searchSettled: searchResultsQuery() !== undefined && searchResultsScope() === parsedCommandQuery().scope?.id,
237
+ searching: searching()
238
+ }));
239
+ const commandMatches = createMemo(() => commandQuery() ? commandModel().rows.map(row => row.command) : []);
240
+ const currentCommandQuery = createMemo(() => caretCommandToken()?.query);
241
+ // Arrow marking belongs to one token in one draft; another draft or `/` starts at the top row again.
242
+ createEffect(() => {
243
+ props.draftKey;
244
+ caretCommandStart();
245
+ currentCommandQuery();
246
+ setCommandMoved(false);
158
247
  });
159
- const commandsOpen = () => Boolean(!emojiOpen() && commandQuery() && (props.commands?.length || props.searchCommands));
248
+ const commandsOpen = () => Boolean(!emojiOpen() && commandQuery() && (localCommands().length || props.searchCommands));
160
249
  const suggestionsOpen = () => commandsOpen() || emojiOpen();
161
250
  createEffect(() => {
162
251
  if (!suggestionsOpen()) return;
@@ -184,20 +273,33 @@ export function ChatComposer(props) {
184
273
  });
185
274
  });
186
275
  createEffect(() => {
276
+ props.draftKey;
187
277
  const token = commandQuery();
188
- setSearchResults([]);
189
278
  setSearchError(false);
190
279
  if (!token || !props.searchCommands) {
191
280
  setSearching(false);
192
281
  return;
193
282
  }
283
+ const parsed = parsedCommandQuery();
194
284
  const abort = new AbortController();
195
285
  setSearching(true);
196
286
  const timer = setTimeout(() => {
197
- Promise.resolve(props.searchCommands(token.query, abort.signal)).then(results => {
198
- if (!abort.signal.aborted) setSearchResults(results);
287
+ Promise.resolve().then(() => props.searchCommands(parsed.query, abort.signal, {
288
+ scope: parsed.scope?.id
289
+ })).then(results => {
290
+ if (!abort.signal.aborted) {
291
+ batch(() => {
292
+ setSearchResults(results);
293
+ setSearchResultsQuery(token.query);
294
+ setSearchResultsScope(parsed.scope?.id);
295
+ });
296
+ }
199
297
  }).catch(() => {
200
- if (!abort.signal.aborted) setSearchError(true);
298
+ if (!abort.signal.aborted) {
299
+ setSearchResults([]);
300
+ setSearchResultsQuery(undefined);
301
+ setSearchError(true);
302
+ }
201
303
  }).finally(() => {
202
304
  if (!abort.signal.aborted) setSearching(false);
203
305
  });
@@ -207,6 +309,52 @@ export function ChatComposer(props) {
207
309
  abort.abort();
208
310
  });
209
311
  });
312
+ let settledEmpty;
313
+ createEffect(() => {
314
+ const token = commandQuery();
315
+ if (!token || parsedCommandQuery().scope || searchError()) {
316
+ settledEmpty = undefined;
317
+ return;
318
+ }
319
+ const previous = settledEmpty;
320
+ if (/\s$/.test(token.query) && previous?.start === token.start && previous.query.trimEnd() === token.query.trimEnd()) {
321
+ setClosedAt(token.start);
322
+ return;
323
+ }
324
+ const settled = !searching() && (!props.searchCommands || searchResultsQuery() === token.query);
325
+ if (settled && commandModel().empty) {
326
+ settledEmpty = {
327
+ start: token.start,
328
+ query: token.query
329
+ };
330
+ if (/\s$/.test(token.query)) setClosedAt(token.start);
331
+ } else if (!searching()) settledEmpty = undefined;
332
+ });
333
+ let lastSuggestion;
334
+ createEffect(() => {
335
+ const model = commandModel();
336
+ const query = commandQuery()?.query;
337
+ if (!commandsOpen()) {
338
+ setSuggestionAnnouncement(undefined);
339
+ lastSuggestion = undefined;
340
+ return;
341
+ }
342
+ if (searching() || searchError() || props.searchCommands && searchResultsQuery() !== query) return;
343
+ const text = chatMessages().commandSuggestions({
344
+ count: model.rows.length
345
+ });
346
+ const timer = setTimeout(() => {
347
+ if (lastSuggestion && lastSuggestion.query === query && lastSuggestion.text === text) return;
348
+ lastSuggestion = {
349
+ query: query ?? "",
350
+ text
351
+ };
352
+ setSuggestionAnnouncement({
353
+ text
354
+ });
355
+ }, 350);
356
+ onCleanup(() => clearTimeout(timer));
357
+ });
210
358
  const history = [];
211
359
  let historyIndex = -1;
212
360
  createEffect(() => {
@@ -215,6 +363,11 @@ export function ChatComposer(props) {
215
363
  history.length = 0;
216
364
  historyIndex = -1;
217
365
  setDismissed(true);
366
+ setClosedAt(undefined);
367
+ setSearchResults([]);
368
+ setSearchResultsQuery(undefined);
369
+ // A failure belongs to the draft it happened in; success notes may follow a command into the chat it opened.
370
+ if (commandStatus()?.tone === "error") showCommandStatus();
218
371
  });
219
372
  });
220
373
  const snapshot = () => ({
@@ -233,6 +386,15 @@ export function ChatComposer(props) {
233
386
  }
234
387
  };
235
388
  const edit = (value, nextMentions = reconcileChatMentions(props.value, value, mentions())) => {
389
+ if (commandStatus()?.tone === "error") showCommandStatus();
390
+ // An edit that starts at or before the closed `/` (select all, then type `/`) replaces it; the new `/` may open again.
391
+ const closed = closedAt();
392
+ if (closed !== undefined) {
393
+ let same = 0;
394
+ while (same <= closed && props.value[same] === value[same]) same++;
395
+ if (Math.min(same, inputStart ?? same) <= closed) setClosedAt(undefined);
396
+ }
397
+ inputStart = undefined;
236
398
  record();
237
399
  props.onValueChange(value);
238
400
  props.onMentionsChange?.(nextMentions);
@@ -252,10 +414,25 @@ export function ChatComposer(props) {
252
414
  });
253
415
  };
254
416
  const syncCaret = () => {
255
- setCaret(textareaRef?.selectionStart ?? props.value.length);
256
- setSelectionEnd(textareaRef?.selectionEnd ?? props.value.length);
417
+ // One update, so no effect sees a half-moved selection (it would read as "no `/` here" and reopen a closed list).
418
+ batch(() => {
419
+ setCaret(textareaRef?.selectionStart ?? props.value.length);
420
+ setSelectionEnd(textareaRef?.selectionEnd ?? props.value.length);
421
+ });
257
422
  };
258
- const selectedCommand = () => commandMatches()[selectedCommandIndex()];
423
+ const selectedCommand = createMemo(() => {
424
+ const rows = commandMatches();
425
+ return commandMoved() && rows.find(command => chatCommandKey(command) === activeCommandKey()) || rows[0];
426
+ });
427
+ const selectedCommandKey = createMemo(() => {
428
+ const command = selectedCommand();
429
+ return command ? chatCommandKey(command) : undefined;
430
+ });
431
+ createEffect(() => {
432
+ if (!commandsOpen()) commandIds.clear();
433
+ selectedCommandKey();
434
+ setAnnouncement(undefined);
435
+ });
259
436
  const busy = () => Boolean(props.disabled || stopping() || state() === "submitting" || executing() || addingFiles());
260
437
  const blocked = () => busy() || submitting();
261
438
  /** The conversation field stays editable while a message is sent, so it keeps the focus and a phone its keyboard. */
@@ -318,21 +495,23 @@ export function ChatComposer(props) {
318
495
  }
319
496
  });
320
497
  createEffect(() => {
321
- commandMatches();
322
498
  emojiMatches();
323
499
  setSelectedCommandIndex(0);
324
500
  });
325
501
  createEffect(() => {
326
- const index = selectedCommandIndex();
327
- commandMatches();
328
- emojiMatches();
502
+ const command = selectedCommand();
503
+ const id = emojiOpen() ? `${commandListId}-${selectedCommandIndex()}` : command ? commandOptionId(command) : undefined;
329
504
  queueMicrotask(() => {
330
505
  const list = commandListRef;
331
- const item = list?.querySelector(`[id="${commandListId}-${index}"]`);
506
+ const item = id ? list?.querySelector(`[id="${id}"]`) : undefined;
332
507
  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
- }
508
+ // Measured against the visible part of the list: a sticky section heading covers the top of it.
509
+ const heading = item.closest(".k2b-chat-composer__command-group")?.querySelector(".k2b-chat-composer__command-heading");
510
+ const listTop = list.getBoundingClientRect().top + list.clientTop;
511
+ const box = item.getBoundingClientRect();
512
+ const top = box.top - listTop - (heading?.offsetHeight ?? 0);
513
+ const bottom = box.bottom - listTop - list.clientHeight;
514
+ if (top < 0) list.scrollTop += top;else if (bottom > 0) list.scrollTop += bottom;
336
515
  });
337
516
  });
338
517
  createEffect(() => {
@@ -422,9 +601,26 @@ export function ChatComposer(props) {
422
601
  });
423
602
  }
424
603
  };
425
- const executeCommand = async command => {
604
+ const narrowCommand = command => {
605
+ const scope = props.commandScopes?.find(scope => scope.id === command.scope);
606
+ const word = scope?.words[0];
426
607
  const token = commandQuery();
427
- if (!token || command.disabled || blocked()) return;
608
+ if (!word || !token || !textareaRef) return false;
609
+ replaceTextareaRange(textareaRef, token.start + 1, token.end, `${word} `);
610
+ syncCaret();
611
+ return true;
612
+ };
613
+ const executeCommand = async (command, tab = false) => {
614
+ const token = commandQuery();
615
+ if (!token || blocked()) return;
616
+ if (command.disabled || command.reason) {
617
+ // A new object each time, so choosing the same row again inserts and announces its reason again.
618
+ setAnnouncement(command.reason ? {
619
+ text: command.reason
620
+ } : undefined);
621
+ return;
622
+ }
623
+ if (command.scope && (tab || !command.action && !command.mention) && narrowCommand(command)) return;
428
624
  const previous = snapshot();
429
625
  const key = props.draftKey;
430
626
  const replacement = command.mention ? command.mention.name + " " : "";
@@ -435,32 +631,66 @@ export function ChatComposer(props) {
435
631
  end: token.start + command.mention.name.length,
436
632
  attachment: command.mention
437
633
  });
634
+ // A cancelled or failed command leaves no undo step behind: restoring puts the history back as well.
635
+ const previousHistory = {
636
+ entries: [...history],
637
+ index: historyIndex
638
+ };
438
639
  edit(nextValue, nextMentions);
439
640
  setDismissed(true);
440
641
  setExecuting(true);
441
642
  let submitRequested = false;
643
+ let restored = false;
644
+ const restore = () => {
645
+ if (props.draftKey !== key || props.value !== nextValue) return;
646
+ setDismissed(true);
647
+ props.onValueChange(previous.value);
648
+ props.onMentionsChange?.(previous.mentions);
649
+ history.splice(0, history.length, ...previousHistory.entries);
650
+ historyIndex = previousHistory.index;
651
+ restored = true;
652
+ };
653
+ showCommandStatus(command.busyLabel ? {
654
+ tone: "busy",
655
+ text: command.busyLabel
656
+ } : undefined);
442
657
  try {
443
- await command.action?.({
658
+ const result = await command.action?.({
444
659
  setValue: value => edit(value),
445
660
  submit: () => {
446
661
  submitRequested = true;
447
662
  },
448
663
  focus
449
664
  });
665
+ if (result === "cancelled") {
666
+ submitRequested = false;
667
+ restore();
668
+ showCommandStatus();
669
+ } else if (result) {
670
+ showCommandStatus({
671
+ tone: "done",
672
+ text: result.done
673
+ });
674
+ }
450
675
  } catch (error) {
451
676
  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);
677
+ restore();
678
+ // An unmounted composer has no status line left; the app's error channel keeps the failure from going silent.
679
+ if (disposed) props.onError?.(error);else showCommandStatus({
680
+ tone: "error",
681
+ text: error instanceof Error && error.message ? error.message : chatMessages().commandFailed
682
+ });
457
683
  } finally {
684
+ if (commandStatus()?.tone === "busy") showCommandStatus();
458
685
  setExecuting(false);
459
686
  }
460
687
  if (submitRequested && props.draftKey === key) await submit();
461
688
  queueMicrotask(() => {
462
689
  if (props.draftKey !== key) return;
463
- textareaRef?.setSelectionRange(token.start + replacement.length, token.start + replacement.length);
690
+ if (restored || props.value === nextValue) {
691
+ const position = restored ? token.end : token.start + replacement.length;
692
+ textareaRef?.setSelectionRange(position, position);
693
+ }
464
694
  syncCaret();
465
695
  autoResize();
466
696
  focus();
@@ -538,19 +768,28 @@ export function ChatComposer(props) {
538
768
  if (commandsOpen()) {
539
769
  if (event.key === "ArrowUp" || event.key === "ArrowDown") {
540
770
  event.preventDefault();
541
- setSelectedCommandIndex(index => nextChatCommandIndex(index, matches.length, event.key === "ArrowUp" ? -1 : 1));
771
+ const index = matches.indexOf(selectedCommand());
772
+ const next = matches[nextChatCommandIndex(index, matches.length, event.key === "ArrowUp" ? -1 : 1)];
773
+ setActiveCommandKey(next ? chatCommandKey(next) : undefined);
774
+ setCommandMoved(true);
775
+ setAnnouncement(undefined);
542
776
  return;
543
777
  }
544
778
  // Ctrl/⌘+Enter sends, also while suggestions are open.
545
- if ((event.key === "Tab" || event.key === "Enter" && !event.ctrlKey && !event.metaKey) && matches.length > 0) {
779
+ const plain = !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey;
780
+ if (plain && (event.key === "Tab" || event.key === "Enter") && matches.length > 0) {
546
781
  event.preventDefault();
547
782
  const command = selectedCommand();
548
- if (command) void executeCommand(command);
783
+ // A server row answering an earlier query may no longer match; wait for fresh results. Local rows match now.
784
+ const row = commandModel().rows.find(row => chatCommandKey(row.command) === selectedCommandKey());
785
+ const staleServerRow = row?.server && (searchResultsQuery() !== commandQuery()?.query || searchResultsScope() !== parsedCommandQuery().scope?.id);
786
+ if (staleServerRow) return;
787
+ if (command) void executeCommand(command, event.key === "Tab");
549
788
  return;
550
789
  }
551
790
  if (event.key === "Escape") {
552
791
  event.preventDefault();
553
- setDismissed(true);
792
+ setClosedAt(commandQuery()?.start);
554
793
  return;
555
794
  }
556
795
  }
@@ -964,7 +1203,7 @@ export function ChatComposer(props) {
964
1203
  children: part => _$ssr(_tmpl$10, Boolean(part.mention) ? "k2b-chat-composer__mention" : "", _$escape(part.text))
965
1204
  })));
966
1205
  }
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, {
1206
+ })), 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
1207
  get when() {
969
1208
  return conversation();
970
1209
  },
@@ -972,25 +1211,47 @@ export function ChatComposer(props) {
972
1211
  return _$ssr(_tmpl$0);
973
1212
  }
974
1213
  })));
975
- return _$ssr(_tmpl$21, _$escape(_$createComponent(Show, {
1214
+ const commandRowMap = createMemo(() => new Map(commandModel().rows.map(row => [row.command, row])));
1215
+ const commandGroupKey = group => group.scopeTitle ? "scope" : group.section ? `section:${group.section.id}` : "flat";
1216
+ const renderCommandRow = command => {
1217
+ const highlight = () => commandRowMap().get(command)?.highlight;
1218
+ const title = () => command.label ?? command.name;
1219
+ return _$ssr(_tmpl$12, _$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, {
1220
+ get when() {
1221
+ return highlight();
1222
+ },
1223
+ get fallback() {
1224
+ return title();
1225
+ },
1226
+ children: range => [title().slice(0, range()[0]), _$ssr(_tmpl$13, _$escape(title().slice(range()[0], range()[1]))), title().slice(range()[1])]
1227
+ })), _$escape(_$createComponent(Show, {
1228
+ get when() {
1229
+ return command.reason ?? command.description;
1230
+ },
1231
+ get children() {
1232
+ return _$ssr(_tmpl$11, _$ssrAttribute("id", command.reason ? `${_$escape(commandOptionId(command), true)}-reason` : _$escape(undefined, true), false), _$escape(command.reason ?? command.description));
1233
+ }
1234
+ })));
1235
+ };
1236
+ return _$ssr(_tmpl$24, _$escape(_$createComponent(Show, {
976
1237
  get when() {
977
1238
  return suggestionsOpen() || hasAccessory();
978
1239
  },
979
1240
  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, {
1241
+ return _$ssr(_tmpl$19, _$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
1242
  get when() {
982
1243
  return emojiOpen();
983
1244
  },
984
1245
  get children() {
985
- return _$ssr(_tmpl$11, _$ssrAttribute("id", _$escape(commandListId, true), false), _$ssrAttribute("aria-label", _$escape(chatMessages().emojiSuggestions, true), false), _$escape(_$createComponent(For, {
1246
+ return _$ssr(_tmpl$14, _$ssrAttribute("id", _$escape(commandListId, true), false), _$ssrAttribute("aria-label", _$escape(chatMessages().emojiSuggestions, true), false), _$escape(_$createComponent(For, {
986
1247
  get each() {
987
1248
  return emojiMatches();
988
1249
  },
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, {
1250
+ children: (entry, index) => _$ssr(_tmpl$25, `${_$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
1251
  get when() {
991
1252
  return entry.shortcodes[0];
992
1253
  },
993
- children: code => _$ssr(_tmpl$23, _$escape(code()))
1254
+ children: code => _$ssr(_tmpl$26, _$escape(code()))
994
1255
  })))
995
1256
  })));
996
1257
  }
@@ -999,31 +1260,70 @@ export function ChatComposer(props) {
999
1260
  return commandsOpen();
1000
1261
  },
1001
1262
  get children() {
1002
- return _$ssr(_tmpl$15, _$ssrAttribute("id", _$escape(commandListId, true), false), _$ssrAttribute("aria-label", _$escape(chatMessages().commands, true), false), _$escape(_$createComponent(For, {
1263
+ return _$ssr(_tmpl$18, _$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
1264
  get each() {
1004
- return commandMatches();
1265
+ return commandModel().groups.map(commandGroupKey);
1005
1266
  },
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))
1267
+ children: key => {
1268
+ const group = createMemo(() => commandModel().groups.find(group => commandGroupKey(group) === key));
1269
+ const headingId = () => `${commandListId}-g-${key === "scope" ? "scope" : props.commandSections?.findIndex(section => section.id === group()?.section?.id)}`;
1270
+ const rows = () => group()?.rows.map(row => row.command) ?? [];
1271
+ return _$createComponent(Show, {
1272
+ when: key !== "flat",
1273
+ get fallback() {
1274
+ return _$createComponent(For, {
1275
+ get each() {
1276
+ return rows();
1277
+ },
1278
+ children: renderCommandRow
1279
+ });
1280
+ },
1281
+ get children() {
1282
+ return _$ssr(_tmpl$29, _$ssrAttribute("aria-labelledby", _$escape(headingId(), true), false), _$ssrAttribute("id", _$escape(headingId(), true), false), _$escape(group()?.scopeTitle ?? group()?.section?.label), _$escape(_$createComponent(Show, {
1283
+ get when() {
1284
+ return group()?.loading;
1285
+ },
1286
+ get children() {
1287
+ return _$ssr(_tmpl$27);
1288
+ }
1289
+ })), _$escape(_$createComponent(For, {
1290
+ get each() {
1291
+ return rows();
1292
+ },
1293
+ children: renderCommandRow
1294
+ })), _$escape(_$createComponent(Show, {
1295
+ get when() {
1296
+ return group()?.placeholder;
1297
+ },
1298
+ get children() {
1299
+ return _$ssr(_tmpl$28);
1300
+ }
1301
+ })));
1302
+ }
1303
+ });
1304
+ }
1007
1305
  })), _$escape(_$createComponent(Show, {
1008
1306
  get when() {
1009
- return searching();
1307
+ return _$memo(() => !!(searching() && commandMatches().length === 0))() && !commandModel().groups.some(group => group.section || group.scopeTitle);
1010
1308
  },
1011
1309
  get children() {
1012
- return _$ssr(_tmpl$12, _$escape(messages().loading));
1310
+ return _$ssr(_tmpl$15, _$escape(messages().loading));
1013
1311
  }
1014
1312
  })), _$escape(_$createComponent(Show, {
1015
1313
  get when() {
1016
1314
  return searchError();
1017
1315
  },
1018
1316
  get children() {
1019
- return _$ssr(_tmpl$13, _$escape(messages().error));
1317
+ return _$ssr(_tmpl$16, _$escape(messages().error));
1020
1318
  }
1021
1319
  })), _$escape(_$createComponent(Show, {
1022
1320
  get when() {
1023
1321
  return _$memo(() => !!(!searching() && !searchError()))() && commandMatches().length === 0;
1024
1322
  },
1025
1323
  get children() {
1026
- return _$ssr(_tmpl$14, _$escape(messages().noResults));
1324
+ return _$ssr(_tmpl$17, !parsedCommandQuery().query.trim() ? _$escape(messages().noResults) : _$escape(chatMessages().commandsNothingFound({
1325
+ query: parsedCommandQuery().query.trim()
1326
+ })));
1027
1327
  }
1028
1328
  })));
1029
1329
  }
@@ -1034,18 +1334,18 @@ export function ChatComposer(props) {
1034
1334
  return showHint();
1035
1335
  },
1036
1336
  get children() {
1037
- return _$ssr(_tmpl$17, _$escape(_$createComponent(Show, {
1337
+ return _$ssr(_tmpl$20, _$escape(_$createComponent(Show, {
1038
1338
  get when() {
1039
1339
  return dictationStatus();
1040
1340
  },
1041
1341
  get fallback() {
1042
- return _$ssr(_tmpl$25, _$escape(hint()));
1342
+ return _$ssr(_tmpl$30, _$escape(hint()));
1043
1343
  },
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, {
1344
+ children: status => [_$ssr(_tmpl$31, _$ssrAttribute("data-state", _$escape(dictation() ?? undefined, true), false), _$ssrAttribute("class", _$escape(status().icon, true), false), _$escape(status().text)), _$createComponent(Show, {
1045
1345
  get when() {
1046
1346
  return _$memo(() => dictation() === "refined")() ? props.microphone?.onRestoreOriginal : undefined;
1047
1347
  },
1048
- children: restore => _$ssr(_tmpl$27, _$ssrAttribute("disabled", microphoneDisabled(), true), _$escape(chatMessages().restoreOriginal))
1348
+ children: restore => _$ssr(_tmpl$32, _$ssrAttribute("disabled", microphoneDisabled(), true), _$escape(chatMessages().restoreOriginal))
1049
1349
  })]
1050
1350
  })));
1051
1351
  }
@@ -1054,16 +1354,16 @@ export function ChatComposer(props) {
1054
1354
  return attachments().length > 0;
1055
1355
  },
1056
1356
  get children() {
1057
- return _$ssr(_tmpl$18, _$ssrAttribute("aria-label", _$escape(chatMessages().attachments, true), false), _$escape(_$createComponent(For, {
1357
+ return _$ssr(_tmpl$21, _$ssrAttribute("aria-label", _$escape(chatMessages().attachments, true), false), _$escape(_$createComponent(For, {
1058
1358
  get each() {
1059
1359
  return attachments();
1060
1360
  },
1061
- children: attachment => _$ssr(_tmpl$29, `k2b-chat-composer__attachment-content ${Boolean(attachment.action) ? "k2b-chat-composer__attachment-content--action" : ""}`, _$escape(_$createComponent(Show, {
1361
+ children: attachment => _$ssr(_tmpl$34, `k2b-chat-composer__attachment-content ${Boolean(attachment.action) ? "k2b-chat-composer__attachment-content--action" : ""}`, _$escape(_$createComponent(Show, {
1062
1362
  get when() {
1063
1363
  return attachment.href;
1064
1364
  },
1065
1365
  get fallback() {
1066
- return _$ssr(_tmpl$31, _$escape(_$createComponent(Show, {
1366
+ return _$ssr(_tmpl$36, _$escape(_$createComponent(Show, {
1067
1367
  get when() {
1068
1368
  return _$memo(() => attachment.kind === "image")() && attachment.previewUrl;
1069
1369
  },
@@ -1071,11 +1371,11 @@ export function ChatComposer(props) {
1071
1371
  return _$ssr(_tmpl$3, _$ssrAttribute("class", _$escape(attachmentIcon(attachment), true), false));
1072
1372
  },
1073
1373
  get children() {
1074
- return _$ssr(_tmpl$30, _$ssrAttribute("src", _$escape(attachment.previewUrl, true), false) + _$ssrAttribute("alt", _$escape(attachment.alt ?? "", true), false));
1374
+ return _$ssr(_tmpl$35, _$ssrAttribute("src", _$escape(attachment.previewUrl, true), false) + _$ssrAttribute("alt", _$escape(attachment.alt ?? "", true), false));
1075
1375
  }
1076
1376
  })), _$escape(renderAttachmentCopy(attachment)));
1077
1377
  },
1078
- children: href => _$ssr(_tmpl$32, _$ssrAttribute("href", _$escape(href(), true), false), _$ssrAttribute("aria-label", _$escape(chatMessages().openInNewTab({
1378
+ children: href => _$ssr(_tmpl$37, _$ssrAttribute("href", _$escape(href(), true), false), _$ssrAttribute("aria-label", _$escape(chatMessages().openInNewTab({
1079
1379
  name: attachment.name
1080
1380
  }), true), false), _$escape(_$createComponent(Show, {
1081
1381
  get when() {
@@ -1085,14 +1385,14 @@ export function ChatComposer(props) {
1085
1385
  return _$ssr(_tmpl$3, _$ssrAttribute("class", _$escape(attachmentIcon(attachment), true), false));
1086
1386
  },
1087
1387
  get children() {
1088
- return _$ssr(_tmpl$30, _$ssrAttribute("src", _$escape(attachment.previewUrl, true), false) + _$ssrAttribute("alt", _$escape(attachment.alt ?? "", true), false));
1388
+ return _$ssr(_tmpl$35, _$ssrAttribute("src", _$escape(attachment.previewUrl, true), false) + _$ssrAttribute("alt", _$escape(attachment.alt ?? "", true), false));
1089
1389
  }
1090
1390
  })), _$escape(renderAttachmentCopy(attachment)))
1091
1391
  })), _$escape(_$createComponent(Show, {
1092
1392
  get when() {
1093
1393
  return attachment.action;
1094
1394
  },
1095
- children: action => _$ssr(_tmpl$33, _$ssrAttribute("disabled", blocked() || action().disabled, true), _$escape(_$createComponent(Show, {
1395
+ children: action => _$ssr(_tmpl$38, _$ssrAttribute("disabled", blocked() || action().disabled, true), _$escape(_$createComponent(Show, {
1096
1396
  get when() {
1097
1397
  return action().icon;
1098
1398
  },
@@ -1103,7 +1403,7 @@ export function ChatComposer(props) {
1103
1403
  return props.onAttachmentsChange;
1104
1404
  },
1105
1405
  get children() {
1106
- return _$ssr(_tmpl$28, _$ssrAttribute("aria-label", _$escape(selectMessages().removeNamed({
1406
+ return _$ssr(_tmpl$33, _$ssrAttribute("aria-label", _$escape(selectMessages().removeNamed({
1107
1407
  name: attachment.name
1108
1408
  }), true), false), _$ssrAttribute("disabled", blocked(), true));
1109
1409
  }
@@ -1120,14 +1420,14 @@ export function ChatComposer(props) {
1120
1420
  return props.error;
1121
1421
  },
1122
1422
  get children() {
1123
- return _$ssr(_tmpl$19, _$escape(props.error));
1423
+ return _$ssr(_tmpl$22, _$escape(props.error));
1124
1424
  }
1125
- }), formatRow(), _$ssr(_tmpl$34, _$escape(_$createComponent(Show, {
1425
+ }), formatRow(), _$ssr(_tmpl$39, _$escape(_$createComponent(Show, {
1126
1426
  get when() {
1127
1427
  return props.footerContent;
1128
1428
  },
1129
1429
  get fallback() {
1130
- return [_$ssr(_tmpl$35, _$escape(props.footerTools), _$escape(_$createComponent(Show, {
1430
+ return [_$ssr(_tmpl$40, _$escape(props.footerTools), _$escape(_$createComponent(Show, {
1131
1431
  get when() {
1132
1432
  return hasAddMenu();
1133
1433
  },
@@ -1168,7 +1468,7 @@ export function ChatComposer(props) {
1168
1468
  }
1169
1469
  });
1170
1470
  }
1171
- })), _$escape(props.modelDetails)), _$ssr(_tmpl$36, _$escape(_$createComponent(For, {
1471
+ })), _$escape(props.modelDetails)), _$ssr(_tmpl$41, _$escape(_$createComponent(For, {
1172
1472
  get each() {
1173
1473
  return props.contextActions;
1174
1474
  },
@@ -1218,9 +1518,9 @@ export function ChatComposer(props) {
1218
1518
  return props.error;
1219
1519
  },
1220
1520
  get children() {
1221
- return _$ssr(_tmpl$19, _$escape(props.error));
1521
+ return _$ssr(_tmpl$22, _$escape(props.error));
1222
1522
  }
1223
- }), formatRow(), _$ssr(_tmpl$20, _$escape(attachControl()), _$escape(fileInput()), _$escape(field()), _$escape(formatToggle()), _$escape(emojiButton()), _$escape(microphoneControl()), _$escape(sendControl()))];
1523
+ }), formatRow(), _$ssr(_tmpl$23, _$escape(attachControl()), _$escape(fileInput()), _$escape(field()), _$escape(formatToggle()), _$escape(emojiButton()), _$escape(microphoneControl()), _$escape(sendControl()))];
1224
1524
  }
1225
1525
  })), _$escape(_$createComponent(Show, {
1226
1526
  get when() {
@@ -1241,6 +1541,23 @@ export function ChatComposer(props) {
1241
1541
  },
1242
1542
  onDrop: files => void runFiles(files)
1243
1543
  })
1544
+ })), _$escape(_$createComponent(Show, {
1545
+ get when() {
1546
+ return suggestionAnnouncement();
1547
+ },
1548
+ keyed: true,
1549
+ children: announced => _$ssr(_tmpl$42, _$escape(announced.text))
1550
+ })), _$escape(_$createComponent(Show, {
1551
+ get when() {
1552
+ return _$memo(() => !!!suggestionsOpen())() && commandStatus();
1553
+ },
1554
+ children: status => _$ssr(_tmpl$43, _$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))
1555
+ })), _$escape(_$createComponent(Show, {
1556
+ get when() {
1557
+ return _$memo(() => !!suggestionsOpen())() && announcement();
1558
+ },
1559
+ keyed: true,
1560
+ children: announced => _$ssr(_tmpl$44, _$escape(announced.text))
1244
1561
  })));
1245
1562
  }
1246
1563
  //# sourceMappingURL=ChatComposer.js.map