@filigran/chatbot 3.18.1 → 3.19.1

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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,188 @@ the versions follow [semantic versioning](https://semver.org/spec/v2.0.0.html).
8
8
  This file starts at 3.10.0; earlier releases are on the
9
9
  [releases page](https://github.com/FiligranHQ/filigran-ui/releases).
10
10
 
11
+ ## 3.19.1 — unreleased
12
+
13
+ ### Added
14
+
15
+ - The panel speaks German, Italian, Korean, Russian and Simplified Chinese on
16
+ its own: a catalog for each, picked from `locale` by its language whatever
17
+ the region (`de-de`, `it-it`, `ko-kr`, `ru-ru`, `zh-cn`). Those languages
18
+ showed in English unless the host translated every key through `t`.
19
+ Traditional Chinese tags (`zh-TW`, `zh-HK`, `zh-Hant`) keep falling back.
20
+
21
+ ### Fixed
22
+
23
+ - A refusal whose body has no `detail` shows its `error` when that is a
24
+ sentence, as the products' proxies answer (`{ "error": "..." }`, beside a
25
+ `status` or not), never a code (`invalid_token`, `Unauthorized`) nor a
26
+ status's reason phrase (`Not Found`, `Too Many Requests`). It shows above
27
+ the composer, in place of the answer, on a failed upload's chip, under the
28
+ conversation list and on a conversation that could not be loaded. The panel
29
+ showed its generic message instead.
30
+ - The status line has its own wording when it has no agent's name (`The agent
31
+ is answering…`, `Consulting an agent…`, `Transferring to another agent…`),
32
+ where it filled the name with a lowercase `the agent`: `the agent is
33
+ answering…` started without a capital. A host translating through `t` in a
34
+ language the panel has no catalog for translates these three keys; it
35
+ translated `the agent` before, and shows them in English until it does.
36
+
37
+ ## 3.19.0 — unreleased
38
+
39
+ ### Added
40
+
41
+ - `collapseUnnamedFiles`: the files a reply carries but whose markers it does
42
+ not name go behind one collapsed `Working files ({count})` line, all of them
43
+ (`true`) or those a predicate picks, instead of one card each. Opened, each
44
+ is listed with its card, after a thumbnail for a picture (fetched only once
45
+ the list opens; the picture icon when there is no URL, the fetch fails or
46
+ the bytes do not decode), and from 8 files a `Show less` closes it from its
47
+ end. Opened while following the end, it stops following once it pushes the
48
+ end out of view, so the list stays in view. Off unless the host asks.
49
+ - `renderReasoningAction`: a host's action in the corner of the reasoning
50
+ window, shown while the window is and gone with it.
51
+ - An image's lightbox has a Download action when the panel can download the
52
+ file, so a user's uploaded picture, which replaces its download card, can be
53
+ downloaded.
54
+ - A `Scroll to bottom` button shows once the reader has scrolled up, and
55
+ brings the view back to the end, following it again.
56
+ - Older messages load as the reader scrolling up nears the top, a second at
57
+ least after the last load ended, never while the view follows the end, and
58
+ only where the button would load them. A load that brings nothing (the
59
+ backend failing) stops them until the conversation is opened again; the
60
+ button still tries again. The button reads `Loading earlier messages…` while they load.
61
+ - `maxFileSize` limits each attached file, next to `maxTotalSize` (none by
62
+ default). A file refused, whichever limit it went past (`maxFileSize`,
63
+ `maxTotalSize`, `maxFileCount`), is named above the text field with why; it
64
+ was dropped without a word. `onFileRefused(file, reason)` tells the host too
65
+ (`ChatFileRefusal`).
66
+ - `acceptFileTypes` sets the file picker's `accept`, and refuses a pasted or
67
+ dropped file of another type (`'file-type'`). A file the browser gives no
68
+ MIME type (Chrome on Windows for `.md`, `.yaml`) is judged by the type its
69
+ extension stands for, and refused by a MIME token when it stands for none.
70
+ - `placeholder` replaces "Ask a question...": a text, or a function of the
71
+ agent selected.
72
+ - `onBeforeSend`'s draft says how many files the message carries
73
+ (`fileCount`).
74
+ - The composer takes the focus after a send from it and after a switch to
75
+ another conversation or a new one, unless the focus is in a field, a dialog
76
+ or the host's page; opening the panel takes none.
77
+ - `onBeforeMove(conversation, workspace)` is asked before a drag or a row's
78
+ Move menu files a conversation into a workspace (`workspace` is `null` when
79
+ taking it out of one): `false`, or a promise of it, leaves the conversation
80
+ where it was, e.g. once the user declined to share it with a workspace's
81
+ members. One that throws or rejects refuses the move.
82
+ - `onConversationActionFailed(action, conversation, error)` is told when
83
+ deleting, renaming, moving or clearing conversations fails (`'delete'`,
84
+ `'rename'`, `'move'`, `'clearAll'`), with the backend's `detail` when it
85
+ gave one; returning `true` keeps the panel's own notice from showing.
86
+ - A row of the conversation list shows the last message
87
+ (`last_message_preview`) under its title, and a small icon for a
88
+ conversation started in a messaging channel (`channel`), named `Started in
89
+ {channel}`: Slack, Teams, WhatsApp, Telegram, Discord and Google Chat out
90
+ of the box; `channelLabel(channel)` names another, or hides one (`null`).
91
+ `ChatConversationSummary` gains the optional `lastMessagePreview` and
92
+ `channel`.
93
+ - `sidebarSearchThreshold` sets how many conversations the list holds before
94
+ its search box shows (default 7, as before; `0` shows it from the first).
95
+ - `headerTitle` and `headerSubtitle`, a node or a function of what is on
96
+ screen (`ChatHeaderContext`: the conversation as the list holds it, and the
97
+ agent), show a title beside the header's agent name and a line under it.
98
+ - `agentHref(agent)` makes the header's agent name a link, followed in place
99
+ (an http(s) URL or a path of the host's origin), and `onAgentNameClick(agent)`
100
+ takes a plain click on it; the chevron beside the name opens the agent
101
+ picker then.
102
+ - `clearAllAction: 'archive'` words Clear all as an archive (`Archive all
103
+ conversations`).
104
+ - `XtmAgent.badges`: short labels the agents list may send (`badges`, strings
105
+ only), shown after the agent's name in the agent picker.
106
+ - `toolLabels`: the host's names for its tools, by raw name or as a function
107
+ of it, in the status line (a consult's tools too) and the reasoning details.
108
+ A name it does not give keeps the built-in one.
109
+ - `sidebarSections` also takes a function of the conversation list's search,
110
+ `(search: ChatSidebarSearch) => ReactNode`, given what the search box holds,
111
+ trimmed (`''` when empty). It is drawn again when the search or the function
112
+ changes, so a host passes a stable one, and puts hooks in a component the
113
+ function returns. With it the search box shows whatever the number of
114
+ conversations, and a search no conversation of the user's own matches says
115
+ "None of your conversations matches". Sections given as an element show as
116
+ they are.
117
+
118
+ ### Fixed
119
+
120
+ - The view keeps the last line in view when the thread's own box shrinks
121
+ while following the end (the composer growing to several lines, a tray
122
+ opening); only growth inside the thread was followed.
123
+ - Earlier messages already loaded, shown by the button, leave the reader
124
+ where they were in every browser, as older messages fetched do.
125
+ - A failed upload's chip says why: the upload's `detail`
126
+ (`ChatFile.uploadError`), or "Upload failed". It showed a bare ✕.
127
+ - A message holding a file that failed to upload is not sent until the user
128
+ removes the file, turn or no turn; with no turn running it went without the
129
+ file, and nothing said so.
130
+ - Files attached before a host turns `disableFileManagement` on stay shown
131
+ and removable; they were hidden, yet still sent, or a failed one held every
132
+ send without a word.
133
+ - Deleting, renaming or clearing conversations failed silently: the panel now
134
+ says it under the conversation list, or under the header when the list is
135
+ not shown, in the backend's words when it gives some. A refused rename
136
+ still keeps the old title, and now undoes only that row: other changes to
137
+ the list made meanwhile stay.
138
+ - The conversation list's search matches a conversation's last message and
139
+ its agent's name too, not its title alone.
140
+ - A message sent in a conversation moves it to the top of its section at
141
+ once, with the time and its text as the preview; it stayed where it was
142
+ until the turn ended and the list was read again.
143
+ - A read-only panel showed the welcome screen's "Suggestions" heading over no
144
+ suggestions: nothing can be sent there, so the section is left out.
145
+ - The status line says what the turn does where it said `Thinking...`:
146
+ `{agent} is answering…` for each mentioned agent of a turn, with that
147
+ agent's name before the statuses of its run, in the voice call too
148
+ (`AgentStatusState.answeringAgentName`);
149
+ `Transferring to {agent}…` on `transfer_start`; the setup step of
150
+ `preparing` by its `phase` (`Loading conversation history…`, `Summarizing
151
+ earlier conversation…`, `Recalling memory…`, `Connecting integrations…`,
152
+ `Preparing tools…`, else `Preparing…`); `Compacting conversation context…`;
153
+ `Saving response…` on `persisting`; and `Analyzing results (step {step})…`
154
+ from the frame's `iteration` after the first step. A host's `statusLabel`
155
+ reads them as `AgentStatusState.agentName`, `phase` and `iteration`.
156
+ - While mentioned agents answer, a message does not steer: it would reach
157
+ whichever of them runs. It waits for the turn, held with
158
+ `holdSendsDuringTurn`, as a message that cannot steer does.
159
+ - A stored answer the server's restart cut short (`interrupted: true`,
160
+ `ChatMessage.interrupted`) is marked as a truncated one is, and its reasoning
161
+ details say why.
162
+ - A finished answer with no text and no files says `No response was
163
+ generated. Please try again.`; it showed `...`.
164
+ - A paste from an office suite or a spreadsheet (Excel, Word, PowerPoint,
165
+ OneNote, LibreOffice, Numbers) puts the copied text in the composer. These
166
+ apps copy a selection as its text, its HTML and a picture of it, and the
167
+ panel attached the picture and dropped the text. A paste whose files are all
168
+ images, with plain text and an HTML fragment that shows text, attaches
169
+ nothing and reaches the text box; a screenshot, a browser's "Copy image" and
170
+ a file copied in a file manager still attach. A picture copied together with
171
+ text around it (an image and its caption in Word or Outlook) is read the same
172
+ way: the text is pasted, the picture is not attached; copy the picture alone
173
+ to attach it.
174
+ - Enter while an input method composes a word (Chinese, Japanese, Korean)
175
+ confirms the word instead of sending the half-typed message, renaming a
176
+ conversation or naming a workspace, and Escape then cancels the composition
177
+ without stopping the turn, closing the field or clearing a search.
178
+ - A file dropped on the panel where it attaches none (a `readOnly` panel, one
179
+ with `disableFileManagement` or no upload route, or the header and the
180
+ conversation list of any panel) leaves the page where it is; the browser
181
+ opened it in the tab, away from the chat. A host's own drop zone around the
182
+ panel still receives the drop, and a picture or link dragged from a page
183
+ still goes into a text field as its address.
184
+ - The composer's draft is written when the page goes away (`pagehide`) or is
185
+ hidden, so a reload right after typing keeps it; a keystroke within the
186
+ 300 ms the draft waits before being written was lost. Two panels on one
187
+ conversation write only the draft typed in them, so neither erases the
188
+ other's.
189
+ - A search box hidden as the list shrinks below its threshold clears its
190
+ query: the list stayed filtered by a search the user could neither see nor
191
+ clear.
192
+
11
193
  ## 3.18.1 — unreleased
12
194
 
13
195
  ### Fixed
package/README.md CHANGED
@@ -92,6 +92,10 @@ import { ChatPanel } from '@filigran/chatbot';
92
92
  | `disableFileManagement` | `boolean` | `false` | No attaching or pasting files in the composer |
93
93
  | `maxFileCount` | `number` | `10` | Most files attached to one message |
94
94
  | `maxTotalSize` | `number` | 50 MB | Most bytes the files attached to one message add up to |
95
+ | `maxFileSize` | `number` | — | Most bytes one attached file may hold; none of its own by default, only `maxTotalSize` |
96
+ | `acceptFileTypes` | `string` | — | The file types the composer takes, in the file input's `accept` form (`'.pdf,image/*'`): the picker offers only these, and a pasted or dropped file of another type is refused. A file the browser gives no MIME type (Chrome on Windows for `.md`, `.yaml`) is judged by the type its extension stands for (`.md`, `.markdown`, `.yaml`, `.yml`, `.csv`, `.tsv`, `.txt`, `.log`, `.json`), and refused by a MIME token when it stands for none. Any type by default |
97
+ | `onFileRefused` | `(file: File, reason: ChatFileRefusal) => void` | — | A file the composer refused: `'file-size'` (over `maxFileSize`), `'total-size'` (past `maxTotalSize`), `'file-count'` (past `maxFileCount`) or `'file-type'` (not of `acceptFileTypes`). The composer names the file and says why above the text field either way |
98
+ | `placeholder` | `string \| ((agent: XtmAgent \| null) => string)` | `'Ask a question...'` | The composer's placeholder, or one for the agent selected (`null` until the agents load); used as given, so the host translates it |
95
99
  | `onMessageFeedback` | `(id, feedback, message) => void` | — | Receives each rating of an answer (`null` clears it); `id` is the message's persisted id when the backend reported one (`done.message_id`, a restored entry's `id`), else the panel's own. On its own it enables 👍/👎 and the panel stores nothing; with `apiEndpoints.feedback` set it is called once the backend has stored the rating. With neither, no thumbs. |
96
100
  | `locale` | `string` | browser's | BCP 47 language tag of the platform's language (e.g. `'fr-FR'`): the language of the panel's catalog for what `t` does not translate, of its message times and of read-aloud. Unset, the browser's — except that a host passing `t` gets no catalog then |
97
101
  | `disableImagePreviews` | `boolean` | `false` | Render image attachments as download cards instead of inline previews |
@@ -116,10 +120,14 @@ import { ChatPanel } from '@filigran/chatbot';
116
120
  | `headerActions` / `banner` / `composerHeader` / `transcriptFooter` / `welcome` | `React.ReactNode` | — | The host's content in the header, above the thread, above the composer, after the last message, or in place of the welcome screen |
117
121
  | `renderMessageExtras` / `messageFooterActions` | `(message, { streaming }) => React.ReactNode` | — | The host's content under a message, and controls in a finished answer's footer; `streaming` while the message is the answer still being written. `message.raw` holds the backend's own fields — see [Host extensions](#host-extensions) |
118
122
  | `sidebarRowActions` | `(conversation) => React.ReactNode` | — | The host's controls on a row of the conversation list |
119
- | `sidebarHeaderActions` / `sidebarSections` | `React.ReactNode` | — | The host's controls beside New conversation, and sections under the list |
123
+ | `sidebarHeaderActions` | `React.ReactNode` | — | The host's controls beside New conversation |
124
+ | `sidebarSections` | `React.ReactNode \| ((search: ChatSidebarSearch) => React.ReactNode)` | — | Sections under the list; a function draws them from the list's search (`query`, trimmed), again when the search or the function changes (pass a stable one, and put hooks in a component it returns), and offers the search box whatever the number of conversations |
120
125
  | `statusLabel` | `(status: AgentStatusState) => string \| undefined` | — | The status line's text for a status, over the built-in one |
126
+ | `toolLabels` | `Record<string, string> \| ((toolName: string) => string \| undefined)` | — | Tool names as the status line and the reasoning details show them, by the tool's raw name; a name it does not give keeps the built-in one (`search_iocs` → `Search Iocs`). A new function redraws every message: keep one across renders unless its labels change |
121
127
  | `waitingMessages` | `string[]` | built-in | The waiting panel's rotating messages, already translated |
122
128
  | `renderWaitingPanel` | `() => React.ReactNode` | — | Replaces the waiting panel's content (its messages and the invitation to play) |
129
+ | `renderReasoningAction` | `() => React.ReactNode` | — | An action in the reasoning window's corner (a game to play while the agent works), shown while the window is |
130
+ | `collapseUnnamedFiles` | `boolean \| (attachment, message) => boolean` | `false` | Puts the files a reply does not name behind one collapsed `Working files (N)` line, with a thumbnail for each picture once opened: all of them, or those the predicate picks |
123
131
  | `fontFamily` | `string` | — | Font of the panel's text |
124
132
  | `theme` | `ChatPanelTheme` | — | The host's colours, type and shapes; see [Custom Theme](#custom-theme) |
125
133
  | `agentMentions` | `boolean` | `false` | `@` also mentions agents of the agent list; the message carries their ids as `mentioned_agent_ids` |
@@ -130,6 +138,14 @@ import { ChatPanel } from '@filigran/chatbot';
130
138
  | `onBeforeSend` | `(draft) => false \| { text?, displayText?, extras? } \| void` | — | Sees every non-empty send first and takes it over (`false`), rewords it, shows the user's bubble other words or adds body fields (a message with fields of its own is held with `holdSendsDuringTurn`) — see [Host extensions](#host-extensions) |
131
139
  | `hideConversationList` | `boolean` | `false` | Hides the conversation list of fullscreen and inline modes, for a rail or a task pane (usable from 320 px wide); the header's history menu stands in for it — see [Host extensions](#host-extensions) |
132
140
  | `holdSendsDuringTurn` | `boolean` | `false` | A message that cannot steer is held and sent when the turn ends, instead of waiting in the composer. A turn winding down after a provisional end (until its stored end or its stream's close, at most 5 s) holds sends with or without it |
141
+ | `onBeforeMove` | `(conversation, workspace \| null) => boolean \| Promise<boolean>` | — | Asked before a drag or a row's Move menu files a conversation into a workspace (`null`: out of one); `false` leaves it where it was |
142
+ | `onConversationActionFailed` | `(action, conversation \| null, error \| null) => boolean \| void` | — | A failed `'delete'`, `'rename'`, `'move'` or `'clearAll'`, with the backend's reason; return `true` when the host told the user itself, and the panel says nothing |
143
+ | `sidebarSearchThreshold` | `number` | `7` | The conversation list's search box shows above this many conversations; `0` shows it from the first |
144
+ | `channelLabel` | `(channel: string) => string \| null \| undefined` | — | Names the channel a row's conversation was started in (`Started in {channel}`); `null` shows nothing, `undefined` keeps the panel's names (Slack, Teams, WhatsApp, Telegram, Discord, Google Chat) |
145
+ | `headerTitle` / `headerSubtitle` | `React.ReactNode \| ((context: ChatHeaderContext) => React.ReactNode)` | — | A title beside the header's agent name and a line under it, or functions of the conversation on screen (as the list holds it) and the agent |
146
+ | `agentHref` | `(agent: XtmAgent) => string \| undefined` | — | Links the header's agent name to this address (http(s) or a path of the host's origin), in place; the chevron beside it opens the picker |
147
+ | `onAgentNameClick` | `(agent: XtmAgent) => void` | — | Takes a plain click on the header's agent name (over `agentHref`); the chevron beside it opens the picker |
148
+ | `clearAllAction` | `'delete' \| 'archive'` | `'delete'` | What Clear all does, for its wording (`Archive all conversations`) |
133
149
 
134
150
  Plain-data props (`requestHeaders`, `apiEndpoints`, `pageContext`, `promptSuggestions`) are compared by content and callback props are called
135
151
  through a stable wrapper, so building them inline is fine. `t` and `logoIcon` are used as given: keep them stable (`useCallback` / `useMemo`),
@@ -224,8 +240,9 @@ them stable, and a new one re-renders every message, which is also how to
224
240
  refresh what they render.
225
241
 
226
242
  `onBeforeSend(draft)` sees every non-empty send first, synchronously:
227
- `draft.text`, `draft.source` (`'composer'`, `'host'` or `'voice'`) and
228
- `draft.turnRunning` (a turn is running). It returns `false` to take the send
243
+ `draft.text`, `draft.source` (`'composer'`, `'host'` or `'voice'`),
244
+ `draft.turnRunning` (a turn is running) and `draft.fileCount` (the files the
245
+ message carries), e.g. to refuse a send whose files the host cannot use. It returns `false` to take the send
229
246
  over (the composer keeps its draft), or an object: `text` rewords what the
230
247
  backend receives, `displayText` is what the user's bubble shows, and `extras`
231
248
  adds body fields. The panel's own fields cannot be set this way, and a message
@@ -439,11 +456,15 @@ Returns available AI agents.
439
456
  "name": "General Assistant",
440
457
  "slug": "general",
441
458
  "icon": null,
442
- "description": "A helpful general-purpose assistant"
459
+ "description": "A helpful general-purpose assistant",
460
+ "badges": ["Company"]
443
461
  }
444
462
  ]
445
463
  ```
446
464
 
465
+ `badges` is optional: short labels, already translated, shown after the
466
+ agent's name in the agent picker. Anything but strings is ignored.
467
+
447
468
  ### `POST {apiBaseUrl}/chat/sessions`
448
469
 
449
470
  Restores conversation history (and, implicitly, resolves the session).
@@ -499,8 +520,8 @@ conversation. If the endpoint responds with a non-2xx status, the component
499
520
  silently resets the stored id and starts fresh on the next message, except
500
521
  when it says nothing of the conversation: a `502`, `503` or `504` (or no
501
522
  answer at all) keeps the id and reads it back once the backend answers, and a
502
- `401` or `403` (expired credentials) keeps it, shows the response's `detail`
503
- (or "This conversation could not be loaded.") with a Retry button, and reads
523
+ `401` or `403` (expired credentials) keeps it, shows the response's
524
+ [reason](#a-refusals-reason) (or "This conversation could not be loaded.") with a Retry button, and reads
504
525
  it back as soon as `requestHeaders` change.
505
526
 
506
527
  Assistant history messages **should echo the same `attachments[]` array** that
@@ -535,11 +556,17 @@ Lists the user's past conversations for the history menu in the chat header.
535
556
  "conversation_id": "uuid-here",
536
557
  "title": "What is the weather?",
537
558
  "updated_at": "2026-06-10T08:30:00Z",
538
- "message_count": 12
559
+ "message_count": 12,
560
+ "last_message_preview": "It is sunny in Paris.",
561
+ "channel": "slack"
539
562
  }
540
563
  ]
541
564
  ```
542
565
 
566
+ `last_message_preview` and `channel` are optional: a row shows the preview
567
+ under its title, and names a messaging channel the conversation was started
568
+ in (see `channelLabel`).
569
+
543
570
  Selecting a conversation restores it through the existing
544
571
  `POST /chat/sessions` contract above. The endpoint is fetched lazily when the
545
572
  history menu opens; a backend that doesn't implement it yet (404/405) simply
@@ -556,7 +583,7 @@ the row reverts to its previous title.
556
583
 
557
584
  With [workspaces](#workspaces) on, the same route files a conversation:
558
585
  `{ "workspace_id": "uuid" }`, or `{ "workspace_id": null }` to take it out of
559
- its workspace. A refusal's `detail` is shown under the list.
586
+ its workspace. A refusal's [reason](#a-refusals-reason) is shown under the list.
560
587
 
561
588
  ### Workspaces
562
589
 
@@ -632,6 +659,17 @@ Deletes a conversation from the history menu. Any 2xx response counts as
632
659
  success; deleting the active conversation resets the panel to a fresh chat so
633
660
  the next message never targets a dead conversation id.
634
661
 
662
+ ### A refusal's reason
663
+
664
+ Where the panel shows why the backend refused a request, it reads the JSON body
665
+ of the non-2xx answer: its `detail` when a string, or the `message` (or
666
+ `detail`) of a `detail` object, as XTM One answers; else its `error` when that
667
+ is a sentence, as the products' proxies answer (`{ "error": "..." }`, beside a
668
+ `status` or not). A code (`invalid_token`, `Unauthorized`), a status's reason
669
+ phrase (`Not Found`, `Too Many Requests`), anything else in the body (a
670
+ top-level `message` included), a string over 500 characters or a body that is
671
+ not JSON shows the panel's own, translated message.
672
+
635
673
  ### `POST {apiBaseUrl}/chat/messages`
636
674
 
637
675
  Sends a message and streams the response via SSE.
@@ -659,7 +697,7 @@ It must be JSON-serializable — a non-serializable value (circular reference,
659
697
 
660
698
  A refusal before the stream (a non-2xx answer: a rate limit, a refused
661
699
  option, an unknown conversation) shows the response's `detail` in place of the
662
- answer - a string, or the `message` (or `detail`) of an object - and
700
+ answer ([its reason](#a-refusals-reason)), and
663
701
  "Unable to connect. Please check the configuration." when it has none; the
664
702
  handle's `send` resolves `'failed'`.
665
703
 
@@ -793,7 +831,7 @@ until the next send. On a non-2xx
793
831
  response or a network error nothing was stored: the optimistic user bubble is
794
832
  rolled back, the text is restored into the composer (prepended on its own line
795
833
  if the user already typed something new), the send resolves `'failed'` and the
796
- refusal's `detail`, when there is one, shows above the composer — so a backend
834
+ refusal's [reason](#a-refusals-reason), when there is one, shows above the composer — so a backend
797
835
  without steering support degrades gracefully and the message is never silently
798
836
  lost. A `409` means no turn is running any more, an ordinary race: no notice
799
837
  shows, and with `holdSendsDuringTurn` the message is held and sent as the next
@@ -808,9 +846,9 @@ mentions or body fields of its own (`send`'s `extras`, `onBeforeSend`'s
808
846
  `extras`) never steers, since a steer reaches the agent as text alone: it waits
809
847
  for the turn to end (`holdSendsDuringTurn`), and `onBeforeSend` is asked again,
810
848
  with `turnRunning: false`, before it goes out. A file that failed to upload
811
- neither steers nor waits: while a turn runs, winds down or is let go of with
812
- the event stream, the composer offers no send and says why, and once no turn
813
- holds the conversation the message goes without that file. `send` and a voice
849
+ neither steers nor waits, nor goes without that file: its chip says why (the
850
+ upload's [reason](#a-refusals-reason)), and the composer offers no send, and says why, until the
851
+ user removes it. `send` and a voice
814
852
  call attach no files, so this never holds them. Esc stops generating.
815
853
 
816
854
  #### Multi-segment responses
@@ -1017,7 +1055,7 @@ chat) opts into six more routes, none with a default:
1017
1055
 
1018
1056
  | `apiEndpoints` key | Request | What it does |
1019
1057
  | --- | --- | --- |
1020
- | `cancel` | `POST {cancel}` with `{ conversation_id }` | Stop asks the backend to end the turn; the panel keeps reading until the stream's `cancelled` frame (or lets go after 10 s, or at once on a network error or an answer of `{ "turn": "none" }`, reading the conversation back for an end it had not read yet). A Stop pressed before the panel's own send started its turn (its stream delivered nothing yet) carries `"awaiting_send": true`, and its `none` says nothing of that turn: the Stop stays pending and is sent again, without the flag, as the turn starts (or as the answer comes, the turn started meanwhile); any other answer reached a turn, and nothing goes again. A backend may keep a Stop with that flag for the send's turn, and must then take a second cancel of one turn; a Stop without it says nothing of a later send. A refusal (non-2xx) keeps the turn running and shows its `detail` above the composer |
1058
+ | `cancel` | `POST {cancel}` with `{ conversation_id }` | Stop asks the backend to end the turn; the panel keeps reading until the stream's `cancelled` frame (or lets go after 10 s, or at once on a network error or an answer of `{ "turn": "none" }`, reading the conversation back for an end it had not read yet). A Stop pressed before the panel's own send started its turn (its stream delivered nothing yet) carries `"awaiting_send": true`, and its `none` says nothing of that turn: the Stop stays pending and is sent again, without the flag, as the turn starts (or as the answer comes, the turn started meanwhile); any other answer reached a turn, and nothing goes again. A backend may keep a Stop with that flag for the send's turn, and must then take a second cancel of one turn; a Stop without it says nothing of a later send. A refusal (non-2xx) keeps the turn running and shows its [reason](#a-refusals-reason) above the composer |
1021
1059
  | `skipWait` | `POST {skipWait}` with `{ conversation_id }` | `Continue now` on `status: wait_start` (`{ seconds, reason }`), for as long as the pause lasts (see the status values) |
1022
1060
  | `events` | `GET {events}/{conversation_id}/events?client_id=<id>` → SSE | Turns this panel did not start (another tab, a reload mid-turn), `user_message`, `conversation_title`, `approval_resolved`; every frame of a turn names it (`turn_id`, below) |
1023
1061
  | `messagesPage` | `GET {messagesPage}/{conversation_id}/messages?before=<message id>&limit=50&supports_artifacts=true` → `{ messages, has_more }` | "Load earlier messages" past the restored ones, offered when the restore answers `has_more: true` |
@@ -1263,9 +1301,12 @@ both. The same variables may be set in CSS on an ancestor instead.
1263
1301
 
1264
1302
  ### Internationalization
1265
1303
 
1266
- The panel speaks English, French, Spanish and Japanese on its own: it ships a
1267
- catalog for each and picks one from `locale` (`fr-FR` reads French; English, or
1268
- a language it lacks, is its own keys). A host in another language translates
1304
+ The panel speaks English, French, German, Italian, Spanish, Japanese, Korean,
1305
+ Russian and Simplified Chinese on its own: it ships a catalog for each and
1306
+ picks one from `locale` by its language, whatever the region (`fr-FR` reads
1307
+ French, `zh-cn` Chinese; English, or a language it lacks, is its own keys).
1308
+ Traditional Chinese (`zh-TW`, `zh-HK`, `zh-Hant`) has no catalog: it reads
1309
+ English unless the host translates it through `t`. A host in another language translates
1269
1310
  through `t`. A host with nothing to add only passes `locale`:
1270
1311
 
1271
1312
  ```tsx
package/dist/index.d.ts CHANGED
@@ -473,6 +473,30 @@ interface ChatPanelProps {
473
473
  maxFileCount?: number;
474
474
  /** Maximum total size in bytes for attached files. Default: 50 * 1024 * 1024 (50 MB). */
475
475
  maxTotalSize?: number;
476
+ /** Maximum size in bytes of one attached file. Default: none (only `maxTotalSize`). */
477
+ maxFileSize?: number;
478
+ /**
479
+ * The file types the composer takes, in the file input's `accept` form
480
+ * (`'.pdf,image/*'`): the picker offers only these, and a pasted or dropped
481
+ * file of another type is refused. A file the browser gives no MIME type
482
+ * (Chrome on Windows for `.md`, `.yaml`) is judged by the type its extension
483
+ * stands for (`.md`, `.markdown`, `.yaml`, `.yml`, `.csv`, `.tsv`, `.txt`,
484
+ * `.log`, `.json`), and refused by a MIME token when it stands for none.
485
+ * Default: any type.
486
+ */
487
+ acceptFileTypes?: string;
488
+ /**
489
+ * A file the user attached that the composer refused, and why. The composer
490
+ * names it and says why above the text field, whether or not the host
491
+ * listens.
492
+ */
493
+ onFileRefused?: (file: File, reason: ChatFileRefusal) => void;
494
+ /**
495
+ * The composer's placeholder, over "Ask a question...": a text, or one for
496
+ * the agent selected (`null` until the agents load). Used as given, so the
497
+ * host translates it.
498
+ */
499
+ placeholder?: string | ((agent: XtmAgent | null) => string);
476
500
  /** Additional HTTP headers added to chatbot API requests (messages, sessions, agents, uploads). */
477
501
  requestHeaders?: Record<string, string>;
478
502
  /**
@@ -546,7 +570,7 @@ interface ChatPanelProps {
546
570
  onMessageFeedback?: (messageId: string, feedback: MessageFeedback | null, message: ChatMessage) => void;
547
571
  /**
548
572
  * The user's language as a BCP 47 tag (`'fr'`, `'en-US'`): the language the
549
- * panel speaks (its own catalog: en, es, fr, ja) where `t` does not
573
+ * panel speaks (its own catalog: en, de, es, fr, it, ja, ko, ru, zh-Hans) where `t` does not
550
574
  * translate a key, formats the time under each message and is the language
551
575
  * answers are read aloud in. Default: the browser's own, except that a host
552
576
  * passing `t` and no `locale` gets no catalog.
@@ -664,14 +688,42 @@ interface ChatPanelProps {
664
688
  sidebarRowActions?: (conversation: ChatConversationSummary) => React.ReactNode;
665
689
  /** Controls added beside New conversation, atop the conversation list. */
666
690
  sidebarHeaderActions?: React.ReactNode;
667
- /** Sections added under the conversation list (conversations shared with the user). */
668
- sidebarSections?: React.ReactNode;
691
+ /**
692
+ * Sections added under the conversation list (conversations shared with the
693
+ * user). A function draws them from the list's search, so they filter on
694
+ * what is typed, as the list does. It is drawn again only when the search or
695
+ * the function changes: pass a stable one (`useCallback`). Hooks go in a
696
+ * component it returns (`({ query }) => <Shared query={query} />`): hook
697
+ * calls in the function itself break once it draws another branch. With it,
698
+ * the search box shows whatever the number of conversations, and a search
699
+ * with no match among the user's own says so ("None of your conversations
700
+ * matches").
701
+ */
702
+ sidebarSections?: React.ReactNode | ((search: ChatSidebarSearch) => React.ReactNode);
669
703
  /** The status line's text for a status, over the built-in one; `undefined` keeps it. */
670
704
  statusLabel?: (status: AgentStatusState) => string | undefined;
705
+ /**
706
+ * Tool names as the status line and the reasoning details show them, over
707
+ * the raw name: by name, or a function of it (`undefined` keeps the raw name).
708
+ * A new function redraws every message: keep one across renders unless its
709
+ * labels change.
710
+ */
711
+ toolLabels?: ChatToolLabels;
671
712
  /** The rotating messages of the waiting panel, already translated. */
672
713
  waitingMessages?: string[];
673
714
  /** Replaces the waiting panel's content (its messages and the invitation to play). */
674
715
  renderWaitingPanel?: () => React.ReactNode;
716
+ /**
717
+ * An action in the corner of the reasoning window (a game to play while the
718
+ * agent works), shown while the window is and gone with it.
719
+ */
720
+ renderReasoningAction?: () => React.ReactNode;
721
+ /**
722
+ * Files a reply carries but whose markers it does not name go behind one
723
+ * collapsed "Working files (N)" line instead of one card each: all of them
724
+ * (`true`), or those the predicate picks. Default: one card each.
725
+ */
726
+ collapseUnnamedFiles?: boolean | ((attachment: ChatAttachment, message: ChatMessage) => boolean);
675
727
  /** Font of the panel's text; the package's own otherwise. */
676
728
  fontFamily?: string;
677
729
  /**
@@ -753,6 +805,63 @@ interface ChatPanelProps {
753
805
  * `sidebar*` slots render nothing meanwhile. Default: false.
754
806
  */
755
807
  hideConversationList?: boolean;
808
+ /**
809
+ * Asked before a conversation is filed into a workspace (`null`: taken out
810
+ * of its workspace), by a drag or the row's Move menu: `false` (or a promise
811
+ * of it) leaves it where it is, e.g. once the user declined to share it with
812
+ * a workspace's members. One that throws or rejects refuses the move.
813
+ */
814
+ onBeforeMove?: (conversation: ChatConversationSummary, workspace: ChatWorkspace | null) => boolean | Promise<boolean>;
815
+ /**
816
+ * Told when deleting, renaming, moving or clearing conversations fails, with
817
+ * the backend's reason when it gave one (`conversation` is `null` for
818
+ * `clearAll`). The panel says it under the conversation list (or the
819
+ * header) unless this returns `true`: the host told the user itself.
820
+ */
821
+ onConversationActionFailed?: (action: ChatConversationAction, conversation: ChatConversationSummary | null, error: string | null) => boolean | void;
822
+ /**
823
+ * The conversation list's search box shows once the list holds more than
824
+ * this many conversations; `0` shows it from the first one. Default: 7.
825
+ */
826
+ sidebarSearchThreshold?: number;
827
+ /**
828
+ * Names the channel a conversation was started in, on its row (`Started in
829
+ * {channel}`): a string names it, `null` shows nothing, `undefined` keeps
830
+ * the panel's own name (Slack, Teams, WhatsApp, Telegram, Discord, Google
831
+ * Chat; no other channel is shown).
832
+ */
833
+ channelLabel?: (channel: string) => string | null | undefined;
834
+ /** A title beside the agent's name in the header (the conversation's, say), or a function of what is on screen. */
835
+ headerTitle?: React.ReactNode | ((context: ChatHeaderContext) => React.ReactNode);
836
+ /** A line under the agent's name in the header (`Agent · Slack`, say), or a function of what is on screen. */
837
+ headerSubtitle?: React.ReactNode | ((context: ChatHeaderContext) => React.ReactNode);
838
+ /**
839
+ * Links the header's agent name to this address (an http(s) URL or a path
840
+ * of the host's own origin), opened in place; the chevron beside the name
841
+ * opens the agent picker then. `undefined` links nothing.
842
+ */
843
+ agentHref?: (agent: XtmAgent) => string | undefined;
844
+ /**
845
+ * Called on a click of the header's agent name, over following `agentHref`
846
+ * (a click with a modifier key still follows it): a host routes to the
847
+ * agent's page itself. The chevron beside the name opens the agent picker then.
848
+ */
849
+ onAgentNameClick?: (agent: XtmAgent) => void;
850
+ /**
851
+ * What Clear all does to the conversations, for its wording: `'archive'`
852
+ * says "Archive all conversations". Default: `'delete'`. A host words it
853
+ * otherwise through `t`.
854
+ */
855
+ clearAllAction?: 'delete' | 'archive';
856
+ }
857
+ /** A conversation action of the list, as `onConversationActionFailed` names it. */
858
+ type ChatConversationAction = 'delete' | 'rename' | 'move' | 'clearAll';
859
+ /** What the header's `headerTitle` and `headerSubtitle` functions are given. */
860
+ interface ChatHeaderContext {
861
+ /** The conversation on screen as the conversation list holds it; `null` for a new chat, or before the list is read. */
862
+ conversation: ChatConversationSummary | null;
863
+ /** The agent the header names, when the agent list holds it. */
864
+ agent: XtmAgent | null;
756
865
  }
757
866
  /** What a marker card is drawn from. */
758
867
  interface ChatMarkerContext {
@@ -774,6 +883,8 @@ interface ChatCodeBlockContext {
774
883
  /** The answer is still streaming: the block may not be complete yet. */
775
884
  streaming: boolean;
776
885
  }
886
+ /** Tool names as a person reads them, by the tool's raw name (see `ChatPanelProps.toolLabels`). */
887
+ type ChatToolLabels = Readonly<Record<string, string>> | ((toolName: string) => string | undefined);
777
888
  /** Renders one fenced code block of a language (see `ChatPanelProps.codeBlockRenderers`); *code* is the block's text. `null` keeps the block as code. */
778
889
  type ChatCodeBlockRenderer = (code: string, context: ChatCodeBlockContext) => React.ReactNode;
779
890
  /** What a message slot (`renderMessageExtras`, `messageFooterActions`) is drawn with. */
@@ -844,6 +955,8 @@ interface ChatSendDraft {
844
955
  source: 'composer' | 'host' | 'voice';
845
956
  /** A turn is running. */
846
957
  turnRunning: boolean;
958
+ /** How many files the message carries (only the composer attaches files); always set by the panel. */
959
+ fileCount?: number;
847
960
  /**
848
961
  * A message held while a turn ran, asked about again as it leaves, the turn
849
962
  * over (`turnRunning: false`), each send held with it on its own. `false`
@@ -998,6 +1111,12 @@ interface ChatMessage {
998
1111
  * reasoning-details affordance, mirroring the XTM One web chat.
999
1112
  */
1000
1113
  isTruncated?: boolean;
1114
+ /**
1115
+ * True when the server restarted while the turn ran, so the answer stopped
1116
+ * before it could finish (`interrupted` on the stored message): marked as a
1117
+ * truncated answer is.
1118
+ */
1119
+ interrupted?: boolean;
1001
1120
  /**
1002
1121
  * The backend's own fields for the message, as it sent them: the restored
1003
1122
  * (or paged, or relayed) entry, or the `done` frame - or the stored end -
@@ -1105,6 +1224,17 @@ interface AgentStatusState {
1105
1224
  waitStartMs?: number;
1106
1225
  /** `consulting`: the consult the agent is waiting on. */
1107
1226
  consult?: ConsultStatus;
1227
+ /** `preparing`: the setup step running (`history`, `compaction`, `memory`, `integrations`, `tools`). */
1228
+ phase?: string;
1229
+ /** `analyzing`: the agent's step, from 1. */
1230
+ iteration?: number;
1231
+ /** `transferring`: the agent the conversation goes to; `mention_answering`: the mentioned agent answering. */
1232
+ agentName?: string;
1233
+ /**
1234
+ * The mentioned agent answering now, on every status of its run: a turn
1235
+ * that answers several mentioned agents runs one after the other.
1236
+ */
1237
+ answeringAgentName?: string;
1108
1238
  }
1109
1239
  /**
1110
1240
  * A consult the agent is waiting on, as the status line shows it. A consulted
@@ -1134,7 +1264,20 @@ interface ChatFile {
1134
1264
  fileId?: string;
1135
1265
  /** Upload status: 'pending' while uploading, 'done' when uploaded, 'error' on failure. */
1136
1266
  uploadStatus?: 'pending' | 'done' | 'error';
1267
+ /** Why the upload failed, as the backend said, when it said: its reason (README, "A refusal's reason"). */
1268
+ uploadError?: string;
1269
+ }
1270
+ /** The conversation list's search, as a host's sections read it (`sidebarSections`). */
1271
+ interface ChatSidebarSearch {
1272
+ /** What the search box holds, trimmed; `''` when it holds nothing. */
1273
+ query: string;
1137
1274
  }
1275
+ /**
1276
+ * Why the composer refused a file (see `onFileRefused`): larger than
1277
+ * `maxFileSize`, past `maxTotalSize` with the files attached, past
1278
+ * `maxFileCount`, or not of `acceptFileTypes`.
1279
+ */
1280
+ type ChatFileRefusal = 'file-size' | 'total-size' | 'file-count' | 'file-type';
1138
1281
  /**
1139
1282
  * A past conversation surfaced in the history menu (REST backend only).
1140
1283
  * Listed via `GET {apiBaseUrl}{apiEndpoints.history ?? apiEndpoints.sessions ?? '/chat/sessions'}`
@@ -1159,6 +1302,10 @@ interface ChatConversationSummary {
1159
1302
  * backend reports it. Undefined on backends that do not.
1160
1303
  */
1161
1304
  workspaceId?: string | null;
1305
+ /** The last message's text, shortened by the backend (`last_message_preview`), when it sends one. */
1306
+ lastMessagePreview?: string;
1307
+ /** Where the conversation was started (`channel`: `slack`, `teams`, `web`, …), when the backend reports it. */
1308
+ channel?: string;
1162
1309
  }
1163
1310
  /**
1164
1311
  * A conversation the composer's `@` menu offers (see
@@ -1200,6 +1347,8 @@ interface XtmAgent {
1200
1347
  description: string | null;
1201
1348
  /** The heading the agent picker lists it under; ungrouped agents come first. */
1202
1349
  group?: string;
1350
+ /** Short labels shown after its name in the agent picker (`Company`), already translated. */
1351
+ badges?: string[];
1203
1352
  }
1204
1353
 
1205
1354
  /**
@@ -1220,4 +1369,4 @@ interface TransferredAgent {
1220
1369
  declare const voiceCallSupported: () => boolean;
1221
1370
 
1222
1371
  export { ChatPanel, ChatToggleButton, voiceCallSupported };
1223
- export type { AgentStatusState, ApiEndpoints, BackendType, ChatAnsweredBy, ChatArtifact, ChatArtifactKind, ChatAttachment, ChatCodeBlockContext, ChatCodeBlockRenderer, ChatContextBreakdown, ChatContextUsage, ChatConversationRef, ChatConversationReferenceCandidate, ChatConversationSummary, ChatFile, ChatHeldOutcome, ChatMarkerContext, ChatMarkerRenderer, ChatMessage, ChatMessagePersistedFeedback, ChatMessageSlotContext, ChatMode, ChatPanelHandle, ChatPanelMode, ChatPanelProps, ChatPanelTheme, ChatPromptTemplate, ChatQuotaStatus, ChatSendDecision, ChatSendDraft, ChatSendOptions, ChatSendOutcome, ChatStreamEventInfo, ChatToggleButtonProps, ChatTurnEnd, ChatTurnOutcome, ChatWorkspace, ConsultStatus, MessageFeedback, ToolApprovalDecision, ToolApprovalProposal, ToolApprovalVerdict, TransferredAgent, Translate, XtmAgent };
1372
+ export type { AgentStatusState, ApiEndpoints, BackendType, ChatAnsweredBy, ChatArtifact, ChatArtifactKind, ChatAttachment, ChatCodeBlockContext, ChatCodeBlockRenderer, ChatContextBreakdown, ChatContextUsage, ChatConversationAction, ChatConversationRef, ChatConversationReferenceCandidate, ChatConversationSummary, ChatFile, ChatFileRefusal, ChatHeaderContext, ChatHeldOutcome, ChatMarkerContext, ChatMarkerRenderer, ChatMessage, ChatMessagePersistedFeedback, ChatMessageSlotContext, ChatMode, ChatPanelHandle, ChatPanelMode, ChatPanelProps, ChatPanelTheme, ChatPromptTemplate, ChatQuotaStatus, ChatSendDecision, ChatSendDraft, ChatSendOptions, ChatSendOutcome, ChatSidebarSearch, ChatStreamEventInfo, ChatToggleButtonProps, ChatToolLabels, ChatTurnEnd, ChatTurnOutcome, ChatWorkspace, ConsultStatus, MessageFeedback, ToolApprovalDecision, ToolApprovalProposal, ToolApprovalVerdict, TransferredAgent, Translate, XtmAgent };