@filigran/chatbot 3.19.0 → 3.20.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.
package/CHANGELOG.md CHANGED
@@ -8,7 +8,152 @@ 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.0 — unreleased
11
+ ## 3.20.0 — unreleased
12
+
13
+ ### Added
14
+
15
+ - `ChatMarkerRenderer.standalone`: an answer that is the renderer's marker and
16
+ nothing else shows the card alone, without the agent's name, the time or the
17
+ actions, as a note between messages (a change of agent, for instance). Each
18
+ answer showed its marker's card inside the answer's frame.
19
+ - The panel speaks German, Italian, Korean, Russian and Simplified Chinese on
20
+ its own: a catalog for each, picked from `locale` by its language whatever
21
+ the region (`de-de`, `it-it`, `ko-kr`, `ru-ru`, `zh-cn`). Those languages
22
+ showed in English unless the host translated every key through `t`.
23
+ Traditional Chinese tags (`zh-TW`, `zh-HK`, `zh-Hant`) keep falling back.
24
+ - An agent the backend marks `disable_chat: true` (`XtmAgent.disableChat`) is
25
+ said `Not recommended for chat` in the agent picker, and the composer warns
26
+ while it is the conversation's agent as the header names it. It stays
27
+ selectable.
28
+ - The header names the channel a conversation was started in under the
29
+ agent's name (`Agent · {channel}`), as its row does, when the host draws no
30
+ `headerSubtitle`; `channelLabel` names it there too. The restore's
31
+ `channel` names it before the list is read.
32
+ - The composer's warning also covers an agent the draft `@`-mentions that is
33
+ not meant for chat, until the mention is removed.
34
+ - With workspaces, the list's `Not in a workspace` group has a `New
35
+ conversation outside a workspace` button, beside each workspace's own.
36
+ - In a reply's "Working files" list, a file the same reply wrote again later,
37
+ with the same name and kind of file (MIME type without its parameters, else
38
+ the type label), carries a "Superseded" tag; its last version carries none.
39
+ A file listed again ranks where it first appeared.
40
+ - `ChatPanelHandle.attachFiles(files)` puts files in the composer's tray as if
41
+ the user picked them, uploaded and held to the same limits, for the next
42
+ message: files another app shared to the host. Nothing happens where the
43
+ panel takes no files.
44
+ - `initialConversationListCollapsed` starts the conversation list collapsed to
45
+ its strip, from which the user opens it: for a phone or a narrow window.
46
+ Read once, when the panel mounts.
47
+ - `replaceMessage(message, { streaming })` lets a host draw a message its own
48
+ way: `undefined` or a boolean leaves it to the panel, `null` leaves it out,
49
+ and anything else is drawn in its place without the message's frame. It is
50
+ asked of every message, the reply's empty placeholder included, and a throw
51
+ leaves that message to the panel. For messages a host folds into one of its
52
+ own, such as the replies of a turn cut and resumed shown as one card.
53
+ - `expandedWorkspaceGroups` and `onExpandedWorkspaceGroupsChange` let a host
54
+ keep, per user, which workspace groups of the conversation list are open.
55
+ Given, every other group starts collapsed until the user opens it, and a
56
+ workspace the user creates from the list opens. A host still reading them
57
+ passes `undefined` until it has them. Unset, groups start open and the ones
58
+ the user collapses are remembered per browser, as before.
59
+
60
+ ### Changed
61
+
62
+ - The agent picker follows the conversation on screen: opening a conversation
63
+ of another agent, or reading back one the host continued with another agent,
64
+ picks that agent, so a new chat starts with it, after a reload too, and
65
+ `onAgentChange` is called.
66
+ An agent picked in the menu meanwhile stays picked. The picker kept the agent
67
+ last picked in the menu, while the header named the conversation's.
68
+
69
+ ### Fixed
70
+
71
+ - `maxTotalSize: Infinity` lifts the cap on the total size of a message's
72
+ files, as a host with a per-file limit only passes it (XTM One's web chat and
73
+ Office add-in). The panel read a value that is not finite as invalid and kept
74
+ its 50 MB default total.
75
+ - A message sent from a new chat no longer comes back as the next new chat's
76
+ draft: the draft leaves the conversation it was typed in as it is sent. The
77
+ write of the emptied composer could land on the conversation the send
78
+ created, leaving the sent text under the new chat's draft.
79
+ - An image's lightbox is named after the image (its alt text, or the file's
80
+ name for an uploaded picture) and keeps `Image preview` for one with none; it
81
+ read `Image preview` for every picture.
82
+ - Tab and Shift+Tab stay on the lightbox's own controls (Download when offered,
83
+ Close) while it is open; they reached the page under it.
84
+ - An image's Expand button is visible while it has keyboard focus, so the focus
85
+ given back after the lightbox closes can be seen. It showed on hover only.
86
+ - A refusal whose body has no `detail` shows its `error` when that is a
87
+ sentence, as the products' proxies answer (`{ "error": "..." }`, beside a
88
+ `status` or not), never a code (`invalid_token`, `Unauthorized`) nor a
89
+ status's reason phrase (`Not Found`, `Too Many Requests`). It shows above
90
+ the composer, in place of the answer, on a failed upload's chip, under the
91
+ conversation list and on a conversation that could not be loaded. The panel
92
+ showed its generic message instead.
93
+ - The status line has its own wording when it has no agent's name (`The agent
94
+ is answering…`, `Consulting an agent…`, `Transferring to another agent…`),
95
+ where it filled the name with a lowercase `the agent`: `the agent is
96
+ answering…` started without a capital. A host translating through `t` in a
97
+ language the panel has no catalog for translates these three keys; it
98
+ translated `the agent` before, and shows them in English until it does.
99
+ - A host that can steer (a `steer` route, on by default) gets its conversation
100
+ created before the first message, so the first turn can be steered. Where
101
+ turns end with their stream (no `cancel`, `skipWait` or `events` route) the
102
+ panel learned the conversation's id only at the turn's end, and a message
103
+ typed while that first turn ran could not be sent.
104
+ - A steer the backend refuses because the turn just ended (409) goes out as
105
+ the next message, as a send during a turn winding down does, whatever
106
+ `holdSendsDuringTurn` says. Without it, the text went back to the composer
107
+ and the user had to send it again.
108
+ - Files added from outside the panel's own events (a host's call) upload too:
109
+ the upload launched on a timer that assumed the tray had been updated
110
+ already, which only a pick guaranteed.
111
+ - A wait a resumed turn continued (`wait_start`) ends on `wait_continued`
112
+ with `Analyzing results…`, as the turn moves on to read what it waited for.
113
+ The status line read `Thinking...` until the next status.
114
+
115
+ ## 3.19.1 — 2026-11-02
116
+
117
+ ### Added
118
+
119
+ - The panel speaks German, Italian, Korean, Russian and Simplified Chinese on
120
+ its own: a catalog for each, picked from `locale` by its language whatever
121
+ the region (`de-de`, `it-it`, `ko-kr`, `ru-ru`, `zh-cn`). Those languages
122
+ showed in English unless the host translated every key through `t`.
123
+ Traditional Chinese tags (`zh-TW`, `zh-HK`, `zh-Hant`) keep falling back.
124
+ - Inline, as a sidebar and fullscreen, the composer's footer has a line of
125
+ its own for what the keys do (`filigran-chat-composer-hint`), above the
126
+ disclaimer: an idle composer reads
127
+ `Enter to send · Shift+Enter for new line`, a busy one the status it read
128
+ before, and the line keeps its height when empty. The floating window keeps
129
+ one line, where a status takes the disclaimer's place.
130
+
131
+ ### Fixed
132
+
133
+ - `filigran-chat-disclaimer` holds the disclaimer alone. It held the
134
+ composer's status in its place while a turn ran or a file failed to
135
+ upload; the status is now `filigran-chat-composer-hint`.
136
+ - A refusal whose body has no `detail` shows its `error` when that is a
137
+ sentence, as the products' proxies answer (`{ "error": "..." }`, beside a
138
+ `status` or not), never a code (`invalid_token`, `Unauthorized`) nor a
139
+ status's reason phrase (`Not Found`, `Too Many Requests`). It shows above
140
+ the composer, in place of the answer, on a failed upload's chip, under the
141
+ conversation list and on a conversation that could not be loaded. The panel
142
+ showed its generic message instead.
143
+ - The status line has its own wording when it has no agent's name (`The agent
144
+ is answering…`, `Consulting an agent…`, `Transferring to another agent…`),
145
+ where it filled the name with a lowercase `the agent`: `the agent is
146
+ answering…` started without a capital. A host translating through `t` in a
147
+ language the panel has no catalog for translates these three keys; it
148
+ translated `the agent` before, and shows them in English until it does.
149
+ - A conversation list that could not be read (a refusal, a server or network
150
+ error) says "Could not load your conversations." with a Retry, in the
151
+ fullscreen list and the header's history menu, where it said "No
152
+ conversations yet". A read that fails once the list was read keeps the
153
+ conversations it showed, unless it was refused (401/403), which drops them;
154
+ a backend without the route (404/405) still shows an empty list.
155
+
156
+ ## 3.19.0 — 2026-11-02
12
157
 
13
158
  ### Added
14
159
 
@@ -164,7 +309,7 @@ This file starts at 3.10.0; earlier releases are on the
164
309
  query: the list stayed filtered by a search the user could neither see nor
165
310
  clear.
166
311
 
167
- ## 3.18.1 — unreleased
312
+ ## 3.18.1 — 2026-11-02
168
313
 
169
314
  ### Fixed
170
315
 
package/README.md CHANGED
@@ -107,7 +107,7 @@ import { ChatPanel } from '@filigran/chatbot';
107
107
  | `conversationId` | `string \| null` | — | The conversation on screen when the host owns it (a `?c=` URL); `null` is a new chat. Left `undefined`, the panel remembers its own, and a host-owned one is never written to storage |
108
108
  | `onConversationChange` | `(id: string \| null) => void` | — | Called whenever the conversation on screen changes, whoever changed it (a first answer, New chat, the history) |
109
109
  | `initialAgent` | `string` | — | Agent selected once the list loads, by id or slug, over the remembered one |
110
- | `onAgentChange` | `(agent: XtmAgent) => void` | — | Called when another agent is picked |
110
+ | `onAgentChange` | `(agent: XtmAgent) => void` | — | Called when another agent is picked, in the menu or by opening a conversation of another agent: the picker follows the conversation on screen |
111
111
  | `readOnly` | `boolean` | `false` | Hides the composer: a conversation the user may read but not write to |
112
112
  | `readOnlyNotice` | `React.ReactNode` | — | Shown in place of the composer while `readOnly` |
113
113
  | `getRequestExtras` | `() => Record<string, unknown>` | — | Fields merged into every message body, read at send time (the host's own per-message options). They never override the panel's fields |
@@ -119,6 +119,7 @@ import { ChatPanel } from '@filigran/chatbot';
119
119
  | `onArtifactOpen` | `(artifact: ChatArtifact) => void` | — | Opens a deck, report or video in the host, in place of the `Open in XTM One` link |
120
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 |
121
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) |
122
+ | `replaceMessage` | `(message, { streaming }) => React.ReactNode` | — | The host's own drawing of a message: `undefined` or a boolean leaves it to the panel, `null` leaves it out, anything else is drawn in its place without the message's frame (name, time, actions). Asked of every message, the reply's empty placeholder included. A throw leaves that message to the panel. For messages a host folds into one of its own, such as a turn cut and resumed shown as one card |
122
123
  | `sidebarRowActions` | `(conversation) => React.ReactNode` | — | The host's controls on a row of the conversation list |
123
124
  | `sidebarHeaderActions` | `React.ReactNode` | — | The host's controls beside New conversation |
124
125
  | `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 |
@@ -137,12 +138,14 @@ import { ChatPanel } from '@filigran/chatbot';
137
138
  | `onTurnRunningChange` | `(running: boolean, info?: ChatTurnEnd) => void` | — | A turn started or stopped running on screen, own or relayed: the state of the Stop button and of `draft.turnRunning`; with `false` comes how it ended, `info.ended`: `'answered'`, `'stopped'` or `'failed'` — see [Host extensions](#host-extensions). After a provisional end (see [Turns that outlive their stream](#turns-that-outlive-their-stream)) `false` comes at once, while sends are still held as the turn winds down: until its stored end or its stream's close, at most 5 s |
138
139
  | `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) |
139
140
  | `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) |
141
+ | `expandedWorkspaceGroups` | `string[]` | — | The workspace groups the user has opened (workspace ids), for a host that keeps them per user: every other group starts collapsed, and one the user creates from the list opens. Read once, the first time it is given (`undefined` while the host reads them). Unset, groups start open and the collapsed ones are remembered per browser |
142
+ | `onExpandedWorkspaceGroupsChange` | `(keys: string[]) => void` | — | The groups the user has opened, each time one is opened or collapsed (see `expandedWorkspaceGroups`) |
140
143
  | `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
144
  | `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
145
  | `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
146
  | `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 |
147
+ | `channelLabel` | `(channel: string) => string \| null \| undefined` | — | Names the channel a conversation was started in, on its row (`Started in {channel}`) and in the header (`Agent · {channel}`); `null` shows nothing, `undefined` keeps the panel's names (Slack, Teams, WhatsApp, Telegram, Discord, Google Chat) |
148
+ | `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. Without `headerSubtitle`, the line reads `Agent · {channel}` for a conversation started in a channel its row names |
146
149
  | `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
150
  | `onAgentNameClick` | `(agent: XtmAgent) => void` | — | Takes a plain click on the header's agent name (over `agentHref`); the chevron beside it opens the picker |
148
151
  | `clearAllAction` | `'delete' \| 'archive'` | `'delete'` | What Clear all does, for its wording (`Archive all conversations`) |
@@ -194,13 +197,16 @@ A host adds what is its own without the package learning about it. An answer
194
197
  marker `[[KIND:args]]` renders the host's card where it stands, once the answer
195
198
  is done (`renderWhileStreaming` to show it earlier); a renderer named `FILE`,
196
199
  `PRESENTATION`, `REPORT` or `VIDEO` replaces the built-in card, and a marker no
197
- renderer names stays text:
200
+ renderer names stays text. A `standalone` renderer draws an answer that is its
201
+ marker and nothing else as the card alone, without the agent's name, the time
202
+ or the actions: a note between messages, such as a change of agent:
198
203
 
199
204
  ```tsx
200
205
  const markers = useMemo(
201
206
  () => ({
202
207
  WORK_ITEM: { render: (id) => <WorkItemCard id={id} /> },
203
208
  INVESTIGATION: { render: (id) => <InvestigationBlock id={id} />, fullWidth: true },
209
+ AGENT_SWITCH: { render: (id) => <AgentSwitchDivider id={id} />, fullWidth: true, standalone: true },
204
210
  }),
205
211
  [],
206
212
  );
@@ -465,6 +471,11 @@ Returns available AI agents.
465
471
  `badges` is optional: short labels, already translated, shown after the
466
472
  agent's name in the agent picker. Anything but strings is ignored.
467
473
 
474
+ `disable_chat` is optional: `true` marks an agent meant for assignments rather
475
+ than chat. The picker says `Not recommended for chat` after its name, and the
476
+ composer warns while it is the agent written to; it stays selectable. The
477
+ panel reads it as `XtmAgent.disableChat`.
478
+
468
479
  ### `POST {apiBaseUrl}/chat/sessions`
469
480
 
470
481
  Restores conversation history (and, implicitly, resolves the session).
@@ -508,6 +519,11 @@ without one shows no time. `feedback` is the caller's own rating of an
508
519
  assistant answer: the thumbs are then locked on it ("Feedback already
509
520
  submitted").
510
521
 
522
+ The response may also carry the conversation's `channel` (`slack`, `teams`,
523
+ …), as the list does: the header names it (`Agent · {channel}`) without
524
+ reading the list, which floating and sidebar modes read only once the history
525
+ menu opens.
526
+
511
527
  The response **should echo a `conversation_id`**. When the stored
512
528
  `conversation_id` no longer exists (e.g. the platform was reset but the
513
529
  browser kept an old id in `localStorage`), the backend is expected to
@@ -520,8 +536,8 @@ conversation. If the endpoint responds with a non-2xx status, the component
520
536
  silently resets the stored id and starts fresh on the next message, except
521
537
  when it says nothing of the conversation: a `502`, `503` or `504` (or no
522
538
  answer at all) keeps the id and reads it back once the backend answers, and a
523
- `401` or `403` (expired credentials) keeps it, shows the response's `detail`
524
- (or "This conversation could not be loaded.") with a Retry button, and reads
539
+ `401` or `403` (expired credentials) keeps it, shows the response's
540
+ [reason](#a-refusals-reason) (or "This conversation could not be loaded.") with a Retry button, and reads
525
541
  it back as soon as `requestHeaders` change.
526
542
 
527
543
  Assistant history messages **should echo the same `attachments[]` array** that
@@ -564,14 +580,15 @@ Lists the user's past conversations for the history menu in the chat header.
564
580
  ```
565
581
 
566
582
  `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`).
583
+ under its title, and the search matches it; the row and the header name a
584
+ messaging channel the conversation was started in (see `channelLabel`).
569
585
 
570
586
  Selecting a conversation restores it through the existing
571
587
  `POST /chat/sessions` contract above. The endpoint is fetched lazily when the
572
588
  history menu opens; a backend that doesn't implement it yet (404/405) simply
573
589
  yields an empty list, so the menu shows its empty state instead of breaking
574
- the chat. Set `apiEndpoints.history` to `null` to hide the history menu
590
+ the chat. Any other failed read shows "Could not load your conversations."
591
+ with a Retry, and keeps the conversations a previous read gave. Set `apiEndpoints.history` to `null` to hide the history menu
575
592
  entirely, or point it at a dedicated path if your proxy can't route `GET` on
576
593
  the sessions path.
577
594
 
@@ -583,7 +600,7 @@ the row reverts to its previous title.
583
600
 
584
601
  With [workspaces](#workspaces) on, the same route files a conversation:
585
602
  `{ "workspace_id": "uuid" }`, or `{ "workspace_id": null }` to take it out of
586
- its workspace. A refusal's `detail` is shown under the list.
603
+ its workspace. A refusal's [reason](#a-refusals-reason) is shown under the list.
587
604
 
588
605
  ### Workspaces
589
606
 
@@ -659,6 +676,17 @@ Deletes a conversation from the history menu. Any 2xx response counts as
659
676
  success; deleting the active conversation resets the panel to a fresh chat so
660
677
  the next message never targets a dead conversation id.
661
678
 
679
+ ### A refusal's reason
680
+
681
+ Where the panel shows why the backend refused a request, it reads the JSON body
682
+ of the non-2xx answer: its `detail` when a string, or the `message` (or
683
+ `detail`) of a `detail` object, as XTM One answers; else its `error` when that
684
+ is a sentence, as the products' proxies answer (`{ "error": "..." }`, beside a
685
+ `status` or not). A code (`invalid_token`, `Unauthorized`), a status's reason
686
+ phrase (`Not Found`, `Too Many Requests`), anything else in the body (a
687
+ top-level `message` included), a string over 500 characters or a body that is
688
+ not JSON shows the panel's own, translated message.
689
+
662
690
  ### `POST {apiBaseUrl}/chat/messages`
663
691
 
664
692
  Sends a message and streams the response via SSE.
@@ -686,7 +714,7 @@ It must be JSON-serializable — a non-serializable value (circular reference,
686
714
 
687
715
  A refusal before the stream (a non-2xx answer: a rate limit, a refused
688
716
  option, an unknown conversation) shows the response's `detail` in place of the
689
- answer - a string, or the `message` (or `detail`) of an object - and
717
+ answer ([its reason](#a-refusals-reason)), and
690
718
  "Unable to connect. Please check the configuration." when it has none; the
691
719
  handle's `send` resolves `'failed'`.
692
720
 
@@ -820,23 +848,25 @@ until the next send. On a non-2xx
820
848
  response or a network error nothing was stored: the optimistic user bubble is
821
849
  rolled back, the text is restored into the composer (prepended on its own line
822
850
  if the user already typed something new), the send resolves `'failed'` and the
823
- refusal's `detail`, when there is one, shows above the composer — so a backend
851
+ refusal's [reason](#a-refusals-reason), when there is one, shows above the composer — so a backend
824
852
  without steering support degrades gracefully and the message is never silently
825
853
  lost. A `409` means no turn is running any more, an ordinary race: no notice
826
- shows, and with `holdSendsDuringTurn` the message is held and sent as the next
827
- turn instead of going back to the composer. Set `apiEndpoints.steer` to `null`
828
- to disable the steering affordances entirely.
854
+ shows, and the message is held and sent as the next turn, with or without
855
+ `holdSendsDuringTurn`. Set `apiEndpoints.steer` to `null` to disable the
856
+ steering affordances entirely.
829
857
 
830
858
  Steering only applies to sends on the `rest` backend with a known
831
- `conversation_id` (the first turn of a fresh conversation only receives its id
832
- on `done`). A message with attachments steers only with `steerAttachments`
859
+ `conversation_id`. The stream names a fresh conversation only on `done`, so
860
+ while steering is on a new chat is created (`POST {sessions}`) before its first
861
+ message, and that first turn can be steered too; with `steer: null` it is not.
862
+ A message with attachments steers only with `steerAttachments`
833
863
  (once every file is uploaded); one carrying conversation references, agent
834
864
  mentions or body fields of its own (`send`'s `extras`, `onBeforeSend`'s
835
865
  `extras`) never steers, since a steer reaches the agent as text alone: it waits
836
866
  for the turn to end (`holdSendsDuringTurn`), and `onBeforeSend` is asked again,
837
867
  with `turnRunning: false`, before it goes out. A file that failed to upload
838
868
  neither steers nor waits, nor goes without that file: its chip says why (the
839
- upload's `detail`), and the composer offers no send, and says why, until the
869
+ upload's [reason](#a-refusals-reason)), and the composer offers no send, and says why, until the
840
870
  user removes it. `send` and a voice
841
871
  call attach no files, so this never holds them. Esc stops generating.
842
872
 
@@ -1044,7 +1074,7 @@ chat) opts into six more routes, none with a default:
1044
1074
 
1045
1075
  | `apiEndpoints` key | Request | What it does |
1046
1076
  | --- | --- | --- |
1047
- | `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 |
1077
+ | `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 |
1048
1078
  | `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) |
1049
1079
  | `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) |
1050
1080
  | `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` |
@@ -1290,9 +1320,12 @@ both. The same variables may be set in CSS on an ancestor instead.
1290
1320
 
1291
1321
  ### Internationalization
1292
1322
 
1293
- The panel speaks English, French, Spanish and Japanese on its own: it ships a
1294
- catalog for each and picks one from `locale` (`fr-FR` reads French; English, or
1295
- a language it lacks, is its own keys). A host in another language translates
1323
+ The panel speaks English, French, German, Italian, Spanish, Japanese, Korean,
1324
+ Russian and Simplified Chinese on its own: it ships a catalog for each and
1325
+ picks one from `locale` by its language, whatever the region (`fr-FR` reads
1326
+ French, `zh-cn` Chinese; English, or a language it lacks, is its own keys).
1327
+ Traditional Chinese (`zh-TW`, `zh-HK`, `zh-Hant`) has no catalog: it reads
1328
+ English unless the host translates it through `t`. A host in another language translates
1296
1329
  through `t`. A host with nothing to add only passes `locale`:
1297
1330
 
1298
1331
  ```tsx
package/dist/index.d.ts CHANGED
@@ -36,8 +36,11 @@ interface ApiEndpoints {
36
36
  * message was persisted and will be injected into the running agentic loop
37
37
  * at the next iteration boundary. On a non-2xx response the optimistic
38
38
  * bubble is rolled back and the text is restored into the composer, so a
39
- * backend without steering support degrades gracefully. Set to null to
40
- * disable mid-run steering entirely.
39
+ * backend without steering support degrades gracefully. A new chat is
40
+ * created (`POST {sessions}`) before its first message, so a steer can name
41
+ * the first turn's conversation: the stream names it only when the turn
42
+ * ends. Set to null to disable mid-run steering entirely, and that create
43
+ * with it.
41
44
  */
42
45
  steer?: string | null;
43
46
  /**
@@ -471,7 +474,7 @@ interface ChatPanelProps {
471
474
  onDownloadError?: (error: unknown, attachment: ChatAttachment) => void;
472
475
  /** Maximum number of files attachable in one chat context. Default: 10. */
473
476
  maxFileCount?: number;
474
- /** Maximum total size in bytes for attached files. Default: 50 * 1024 * 1024 (50 MB). */
477
+ /** Maximum total size in bytes for attached files; `Infinity` for none. Default: 50 * 1024 * 1024 (50 MB). */
475
478
  maxTotalSize?: number;
476
479
  /** Maximum size in bytes of one attached file. Default: none (only `maxTotalSize`). */
477
480
  maxFileSize?: number;
@@ -570,7 +573,7 @@ interface ChatPanelProps {
570
573
  onMessageFeedback?: (messageId: string, feedback: MessageFeedback | null, message: ChatMessage) => void;
571
574
  /**
572
575
  * The user's language as a BCP 47 tag (`'fr'`, `'en-US'`): the language the
573
- * panel speaks (its own catalog: en, es, fr, ja) where `t` does not
576
+ * panel speaks (its own catalog: en, de, es, fr, it, ja, ko, ru, zh-Hans) where `t` does not
574
577
  * translate a key, formats the time under each message and is the language
575
578
  * answers are read aloud in. Default: the browser's own, except that a host
576
579
  * passing `t` and no `locale` gets no catalog.
@@ -619,7 +622,11 @@ interface ChatPanelProps {
619
622
  onConversationChange?: (conversationId: string | null) => void;
620
623
  /** Agent selected once the list loads, by id or slug, over the remembered one. */
621
624
  initialAgent?: string;
622
- /** Called when another agent is picked. */
625
+ /**
626
+ * Called when another agent is picked, in the menu or by opening a
627
+ * conversation of another agent (the picker follows the conversation on
628
+ * screen, so a new chat starts with its agent).
629
+ */
623
630
  onAgentChange?: (agent: XtmAgent) => void;
624
631
  /** Hides the composer: a conversation the user may read but not write to. */
625
632
  readOnly?: boolean;
@@ -682,6 +689,18 @@ interface ChatPanelProps {
682
689
  welcome?: React.ReactNode;
683
690
  /** Rendered under a message's content, before its footer; `context.streaming` while the answer is still coming. */
684
691
  renderMessageExtras?: (message: ChatMessage, context: ChatMessageSlotContext) => React.ReactNode;
692
+ /**
693
+ * Draws a message in its own way: `undefined` (or a boolean, as
694
+ * `isCut(m) && <Card />` gives) leaves it to the panel, `null` leaves it
695
+ * out, and anything else is drawn in its place, without the message's frame
696
+ * (its name, time and actions). For a host that folds several messages into
697
+ * one of its own (a turn cut and resumed, drawn as one card on the first of
698
+ * them, the others left out). Asked of every message, streaming ones
699
+ * included, the reply's empty placeholder too (`null` leaves out its status
700
+ * bubble): return `undefined` fast for the others. A throw, while asked or
701
+ * while the replacement is drawn, leaves that message to the panel.
702
+ */
703
+ replaceMessage?: (message: ChatMessage, context: ChatMessageSlotContext) => React.ReactNode;
685
704
  /** Controls added to a finished message's footer. */
686
705
  messageFooterActions?: (message: ChatMessage, context: ChatMessageSlotContext) => React.ReactNode;
687
706
  /** Controls added to a conversation's row in the conversation list. */
@@ -805,6 +824,26 @@ interface ChatPanelProps {
805
824
  * `sidebar*` slots render nothing meanwhile. Default: false.
806
825
  */
807
826
  hideConversationList?: boolean;
827
+ /**
828
+ * The conversation list starts collapsed to its strip, from which the user
829
+ * opens it: where it would squeeze the thread, a phone or a narrow window.
830
+ * Read once, when the panel mounts. Default: false.
831
+ */
832
+ initialConversationListCollapsed?: boolean;
833
+ /**
834
+ * The workspace groups of the conversation list the user has opened (their
835
+ * keys: a workspace's id), for a host that keeps them per user. Given, every
836
+ * other group starts collapsed until the user opens it, so many workspaces
837
+ * do not push the conversations in none below the fold; one the user creates
838
+ * from the list opens. Read once, the first time it is given: a host still
839
+ * reading them passes `undefined` until it has them. The panel keeps what
840
+ * the user opens from then on, while the list is hidden and shown again too.
841
+ * Unset, groups start open and the ones the user collapses are remembered
842
+ * per browser.
843
+ */
844
+ expandedWorkspaceGroups?: string[];
845
+ /** The workspace groups the user has opened, each time they open or collapse one (see `expandedWorkspaceGroups`). */
846
+ onExpandedWorkspaceGroupsChange?: (keys: string[]) => void;
808
847
  /**
809
848
  * Asked before a conversation is filed into a workspace (`null`: taken out
810
849
  * of its workspace), by a drag or the row's Move menu: `false` (or a promise
@@ -826,14 +865,18 @@ interface ChatPanelProps {
826
865
  sidebarSearchThreshold?: number;
827
866
  /**
828
867
  * Names the channel a conversation was started in, on its row (`Started in
829
- * {channel}`): a string names it, `null` shows nothing, `undefined` keeps
868
+ * {channel}`) and in the header (`Agent · {channel}`): a string names it, `null` shows nothing, `undefined` keeps
830
869
  * the panel's own name (Slack, Teams, WhatsApp, Telegram, Discord, Google
831
870
  * Chat; no other channel is shown).
832
871
  */
833
872
  channelLabel?: (channel: string) => string | null | undefined;
834
873
  /** A title beside the agent's name in the header (the conversation's, say), or a function of what is on screen. */
835
874
  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. */
875
+ /**
876
+ * A line under the agent's name in the header, or a function of what is on
877
+ * screen. Undefined: `Agent · {channel}` for a conversation started in a
878
+ * channel its row names (see `channelLabel`), nothing otherwise.
879
+ */
837
880
  headerSubtitle?: React.ReactNode | ((context: ChatHeaderContext) => React.ReactNode);
838
881
  /**
839
882
  * Links the header's agent name to this address (an http(s) URL or a path
@@ -877,6 +920,11 @@ interface ChatMarkerRenderer {
877
920
  renderWhileStreaming?: boolean;
878
921
  /** Give it the thread's full width rather than the prose's. */
879
922
  fullWidth?: boolean;
923
+ /**
924
+ * An answer that is this marker and nothing else shows the card alone, as a
925
+ * note between messages: no agent name, time or actions around it.
926
+ */
927
+ standalone?: boolean;
880
928
  }
881
929
  /** What a code block renderer is drawn from. */
882
930
  interface ChatCodeBlockContext {
@@ -925,6 +973,12 @@ interface ChatPanelHandle {
925
973
  */
926
974
  getDraft: () => string;
927
975
  focusComposer: () => void;
976
+ /**
977
+ * Put *files* in the composer's tray as if the user picked them (uploaded and
978
+ * checked the same way), to go with the next message: files another app
979
+ * shared to the host. Nothing where the panel takes no files.
980
+ */
981
+ attachFiles: (files: readonly File[]) => void;
928
982
  /** Same as the Stop button; also cancels the question a voice call awaits, and stops it speaking. */
929
983
  stop: () => void;
930
984
  /**
@@ -1264,7 +1318,7 @@ interface ChatFile {
1264
1318
  fileId?: string;
1265
1319
  /** Upload status: 'pending' while uploading, 'done' when uploaded, 'error' on failure. */
1266
1320
  uploadStatus?: 'pending' | 'done' | 'error';
1267
- /** Why the upload failed, as the backend said (`detail`), when it said. */
1321
+ /** Why the upload failed, as the backend said, when it said: its reason (README, "A refusal's reason"). */
1268
1322
  uploadError?: string;
1269
1323
  }
1270
1324
  /** The conversation list's search, as a host's sections read it (`sidebarSections`). */
@@ -1349,6 +1403,12 @@ interface XtmAgent {
1349
1403
  group?: string;
1350
1404
  /** Short labels shown after its name in the agent picker (`Company`), already translated. */
1351
1405
  badges?: string[];
1406
+ /**
1407
+ * The agent is meant for assignments rather than chat (`disable_chat`): the
1408
+ * picker says "Not recommended for chat" and the composer warns while it is
1409
+ * the one written to. It stays selectable.
1410
+ */
1411
+ disableChat?: boolean;
1352
1412
  }
1353
1413
 
1354
1414
  /**