inline-chat-kit 0.55.2 → 0.57.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,6 +8,82 @@ The versions before 1.0 follow the pre-release convention: **a breaking change
8
8
  or new public API bumps the minor**, and the patch is for fixes. Anything that would break an
9
9
  existing install is called out under **Breaking**, with what to do about it.
10
10
 
11
+ ## 0.57.0 — 2026-09-22
12
+
13
+ A pane the host draws itself. `ChatExperience` placed its artifact pane beside
14
+ the conversation and held which one was open, which is right until the host has
15
+ a layout of its own. Nothing changes for a host that passes none of this.
16
+
17
+ ### Added
18
+
19
+ - **`openArtifactId` and `onOpenArtifactChange` on `ChatExperience`.** Which
20
+ artifact is open, held by the host. Left out, the component keeps its own;
21
+ `null` is a held "none open". The change callback fires either way.
22
+ - **`pane="inline" | "none"` on `ChatExperience`.** `"none"` draws no
23
+ `ArtifactPane`, reserves no width for one and never calls `artifact`; a
24
+ pressed card only reports its id, and still shows it is open from
25
+ `openArtifactId`.
26
+ - **`openArtifact(id)` and `closeArtifact()` in `renderPart`'s context**, next
27
+ to `turnId` — so a host's own card can open the pane. `CustomPartContext` is
28
+ exported. `ChatTurnRow` takes `onArtifactChange` for the same when assembled
29
+ by hand.
30
+ - **`ArtifactPane` outside `ChatLayout`**, documented and tested: a region with
31
+ focus on its title when it mounts, no trap and no Escape unless `modal`.
32
+ - **`surface="flush" | "panes"` on `ChatExperience` and `ChatLayout`.**
33
+ `"panes"` makes the conversation a card of its own, so the pane beside it —
34
+ the kit's or the host's — is a second surface rather than a strip cut off the
35
+ same one. Both are drawn from the same tokens, and `--ick-chat-pane-inset` is
36
+ both the ring of space around them and the gap between them. Below the
37
+ layout's breakpoint the corners and the inset go, where the pane is a sheet.
38
+ `"flush"` is the default and is what the kit drew before.
39
+
40
+ ## 0.56.0 — 2026-09-22
41
+
42
+ Four things that kept the kit out of a real host app: an answer could only
43
+ hold the kit's own parts, an approval always offered "forever", an assembled
44
+ chat could not be translated, and it drew controls a host had no use for.
45
+ Nothing here changes what an existing install draws.
46
+
47
+ ### Added
48
+
49
+ - **`{ kind: "custom", id, type, data }` — the host's own card in an answer.**
50
+ Every other part is a component in this package; this one is drawn by the
51
+ new `renderPart(part, { turnId })` prop on `ChatTurnRow` and
52
+ `ChatExperience`, in its place among the other parts and inside the answer's
53
+ entrance. Merged by id like the rest, so `{ kind: "custom", id, data }`
54
+ replaces `data` and keeps `type`. Without a renderer, or when it returns
55
+ `null`, the part draws nothing. `CustomPart` is exported.
56
+ - **`chat` on `ChatExperience`.** It called `useChatTurns` itself, so a host
57
+ had no `updatePart` and a card could not change after the answer finished.
58
+ Pass your own hook's result and it is used instead; left out, nothing
59
+ changes. `onSend` is optional when `chat` is given.
60
+ - **`choices` on `<Approval>` and on the approval part.** `["once", "deny"]`
61
+ leaves "Always allow" out. Allow once stays the primary and Deny stays first.
62
+ `ApprovalChoice` is exported.
63
+ - **`labels` on `ChatExperience` and `ChatTurnRow`, and `LabelsProvider`.** One
64
+ `ChatLabels` object, grouped by component and partial, reaches every piece
65
+ through context — the header, the composer and its menu, the microphone and
66
+ what it announces, the highlighter, the thread, the code block's copy, the
67
+ pane, the jump button, the option letters, and every part. The strings that
68
+ were hard-coded English with no way in now all go through it.
69
+ `defaultLabels` is the English set. The individual `labels` props keep
70
+ working and win over the provider.
71
+ - **`headerActions` and `composerMenu` on `ChatExperience`.**
72
+ `headerActions={false}` (or `["theme"]`, `["share"]`) drops the built-in
73
+ header actions; `composerMenu={false}` removes the "+", and a list of
74
+ `{ id, label, icon?, onSelect? }` replaces its three entries. `ChatInput`
75
+ takes the same list as `menu`. `ComposerMenuItem` is exported.
76
+
77
+ ### Fixed
78
+
79
+ - **Every row in `ChatExperience` re-rendered on every frame of an answer.**
80
+ `onOpenArtifact` was an arrow written inline in the rows' props — a new
81
+ function each render — so `ChatTurnRow`'s memo never held inside the
82
+ assembled experience. Hoisted; the custom-part test fails without it.
83
+ - **The composer's menu opened the file picker by matching the word "Add".**
84
+ Translated, it would have stopped attaching anything. It matches the entry's
85
+ id now.
86
+
11
87
  ## 0.55.2 — 2026-09-16
12
88
 
13
89
  ### Fixed
package/README.md CHANGED
@@ -153,6 +153,14 @@ Which is why the callbacks take the turn's id rather than being closed over per
153
153
  row. Pass the hook's own functions straight through — they are stable. An arrow
154
154
  created during render is not, and hands the memo a new prop every time.
155
155
 
156
+ `renderPart` is a callback like the others and follows the same rule: define
157
+ it outside the component, or wrap it in `useCallback` / `useMemo`. A renderer
158
+ written inline is a new function every render, which is a changed prop on every
159
+ row — so every finished answer re-renders on every frame of the one arriving,
160
+ exactly the cost the memo exists to remove. What a card needs to change (it was
161
+ applied, it was sent) belongs in the part's `data`, written back with
162
+ `updatePart`; then only the row that owns it re-renders.
163
+
156
164
  If you write your own row instead, wrap it in `React.memo` and do the same.
157
165
 
158
166
  ## The four states
@@ -217,6 +225,9 @@ input that morphs into its own bubble rather than a record of what was typed.
217
225
  | `onFeedback` | `(id, verdict) => void` | | Draws the thumbs |
218
226
  | `feedback` | `"up" \| "down" \| null` | `null` | Which one is lit |
219
227
  | `answerActions` | `boolean` | `true` | Leave the row out |
228
+ | `renderPart` | `(part, { turnId }) => ReactNode` | | Draws `custom` parts — see below. Keep it stable |
229
+ | `composerMenu` | `ComposerMenuItem[] \| false` | built-in three | The composer's "+" menu; `false` removes the "+" |
230
+ | `labels` | `ChatLabels` | English | Every word the row says — see [Labels](#labels-every-word-the-kit-says) |
220
231
 
221
232
  The live row carries **`data-active-input`**, so a page can style the composer
222
233
  without knowing which turn it is. The case it exists for: a gradient fading the
@@ -411,15 +422,49 @@ component that owns it.
411
422
  | `tasks` | `<TaskList>` | `title`, `tasks`, `collapsible` |
412
423
  | `chain` | `<ChainOfThought>` | `steps`, `state`, `duration` |
413
424
  | `sources` | `<Sources>` | `sources`, `title`, `collapsible` |
414
- | `approval` | `<Approval>` | `title`, `description`, `tool`, `decision` |
425
+ | `approval` | `<Approval>` | `title`, `description`, `tool`, `decision`, `choices` |
415
426
  | `question` | `<QuestionGroup>` | `title`, `questions`, `answers`, `activeIndex`, `collapsible` |
416
427
  | `notice` | `<SystemMessage>` | `text`, `tone` |
417
428
  | `artifact` | `<ArtifactCard>` | `title`, `meta`, `preview`, `lang`, `content`, `state` |
429
+ | `custom` | whatever `renderPart` returns | `type`, `data` — the host's own card |
418
430
 
419
431
  `reasoning` and `chain` are the same job at two grains — a block of prose, or
420
432
  steps that follow from one another. Sending both for one stretch of thinking
421
433
  says it twice.
422
434
 
435
+ #### Your own cards: `custom`
436
+
437
+ Every other kind is a component in this package. `custom` is the host's: `type`
438
+ says which of your cards it is, `data` is whatever that card needs, and
439
+ `renderPart` draws it — in its place among the other parts, inside the answer
440
+ and its entrance.
441
+
442
+ ```tsx
443
+ import type { CustomPart } from "inline-chat-kit";
444
+
445
+ // Streamed like any part…
446
+ yield { kind: "custom", id: "plan", type: "plan-diff", data: { changes, status: "proposed" } };
447
+
448
+ // …drawn by the host. Outside the component, so it is stable.
449
+ const renderPart = (part: CustomPart, { turnId }: { turnId: string }) =>
450
+ part.type === "plan-diff" ? <PlanDiff turnId={turnId} id={part.id} {...(part.data as PlanDiffData)} /> : null;
451
+
452
+ // …and changed after the answer has finished, by id. Only `data` is replaced.
453
+ updatePart(turnId, { kind: "custom", id: "plan", data: { changes, status: "applied" } });
454
+ ```
455
+
456
+ Without `renderPart`, or when it returns `null`, the part draws nothing — no
457
+ placeholder, no error. A type the host does not recognise is a type it chose
458
+ not to draw. `data` is replaced whole on an update, not merged: it is yours,
459
+ and the kit does not guess its shape.
460
+
461
+ `ChatExperience` calls `useChatTurns` itself, so a host using it passes its own
462
+ instead — `chat={useChatTurns({ onSend })}` — to hold `updatePart`.
463
+
464
+ The second argument to `renderPart` is `{ turnId, openArtifact, closeArtifact }`
465
+ (`CustomPartContext`), so a card can open the pane — see
466
+ [Drawing the pane yourself](#drawing-the-pane-yourself).
467
+
423
468
  #### Citing a source from the prose
424
469
 
425
470
  `[^1]` in the answer is a **citation marker**, and the number is a position in
@@ -567,6 +612,7 @@ can already see.
567
612
  | `decision` | `"once" \| "always" \| "denied" \| null` | `null` while it is still asking |
568
613
  | `onDecide` | `(decision) => void` | |
569
614
  | `readOnly` | `boolean` | A record of a decision made elsewhere |
615
+ | `choices` | `("once" \| "always" \| "deny")[]` | Which answers are offered. All three by default |
570
616
 
571
617
  **Three answers, not two.** "Yes" and "yes forever" are not the same answer,
572
618
  and a UI offering one button for both collects the wrong one. **Allow once is
@@ -578,6 +624,15 @@ too, so a keyboard reaches the safe answer without tabbing past the other two.
578
624
  It turns red only under the pointer, because a permanently red button is the
579
625
  first thing the eye lands on.
580
626
 
627
+ **Two answers, where "forever" means nothing.** Applying one change to a plan
628
+ happens once; `choices={["once", "deny"]}` leaves "Always allow" out. Nothing
629
+ else moves: Allow once is still the primary, Deny is still first in the DOM and
630
+ alone on the left, and the record it settles into says only what was decided.
631
+
632
+ ```tsx
633
+ <Approval title="Apply the change to your plan" choices={["once", "deny"]} onDecide={decide} />
634
+ ```
635
+
581
636
  Give it something to show. An approval with nothing under it is asking for a
582
637
  signature on a blank page.
583
638
 
@@ -586,7 +641,7 @@ decided. Live controls under a decision already made invite a second one that
586
641
  contradicts the first.
587
642
 
588
643
  As a `TurnPart` it is `{ kind: "approval", id, title, description?, tool?,
589
- decision? }` — data, like every part, so the tool it names is drawn for it
644
+ decision?, choices? }` — data, like every part, so the tool it names is drawn for it
590
645
  rather than passed in as an element. `<ChatTurnRow>` reports through
591
646
  `onDecideApproval`.
592
647
 
@@ -977,6 +1032,7 @@ between products, and the only part.
977
1032
  | `onOpen` | `(id: string) => void` | Without one the card is a record, not a control |
978
1033
  | `<ArtifactPane>` `modal` | `boolean` | See below |
979
1034
  | `<ChatLayout>` `pane` | `({ narrow }) => ReactNode` | |
1035
+ | `<ChatLayout>` `surface` | `"flush" \| "panes"` | One surface, or two. See below |
980
1036
 
981
1037
  **`modal` is the one prop that changes behaviour, and it is not about
982
1038
  position.** Covering the conversation changes what the pane *is*: focus has to
@@ -990,6 +1046,78 @@ That is the part worth having in a library rather than the box. On open, focus
990
1046
  moves to the pane's heading — not into its first control, which would skip what
991
1047
  the thing is — and it is **not** trapped unless the pane is covering the chat.
992
1048
 
1049
+ #### Two panes, not one page split in half
1050
+
1051
+ By default the conversation fills the layout and the pane is the only card on
1052
+ it — which is what a chat that owns the whole window wants. `surface="panes"`
1053
+ makes the conversation a card of its own instead:
1054
+
1055
+ ```tsx
1056
+ <ChatExperience onSend={send} surface="panes" artifact={artifact} />
1057
+ ```
1058
+
1059
+ Both surfaces are drawn from the same tokens — `--ick-chat-pane-surface` is
1060
+ `--ick-artifact-pane-surface`, and the radius follows — because two panes side
1061
+ by side have to be the same *kind* of thing, or the chat reads as the
1062
+ background and the pane as a dialog over it. One number,
1063
+ `--ick-chat-pane-inset`, is the ring of space around both and the gap between
1064
+ them, so the frame is even the whole way round. No divider: the gap is what
1065
+ says there are two of them.
1066
+
1067
+ Below `<ChatLayout>`'s breakpoint the corners and the inset go: the pane is a
1068
+ sheet over the conversation there, and a card inside a card with a sheet over
1069
+ both is three surfaces for one screen.
1070
+
1071
+ Inside the card, `--ick-page` is the card — so the header's tint and the fades
1072
+ at both ends end in the colour they are standing on rather than in the ground
1073
+ outside it.
1074
+
1075
+ #### Drawing the pane yourself
1076
+
1077
+ `ChatExperience` places the pane beside the conversation. A host with a layout
1078
+ of its own — its own column, a drawer, a route — holds which artifact is open
1079
+ and tells the kit to keep out of the way:
1080
+
1081
+ ```tsx
1082
+ const [openId, setOpenId] = useState<string | null>(null);
1083
+ const open = openId ? findArtifact(openId) : null;
1084
+
1085
+ <div className="workspace">
1086
+ <ChatExperience
1087
+ chat={chat}
1088
+ pane="none" // no pane here, and no room made for one
1089
+ openArtifactId={openId} // the cards still show which one is open
1090
+ onOpenArtifactChange={setOpenId} // a card pressed, or a custom card's openArtifact
1091
+ renderPart={renderPart}
1092
+ />
1093
+ {open && (
1094
+ <aside className="my-column">
1095
+ <ArtifactPane key={openId} title={open.title} onClose={() => setOpenId(null)}>
1096
+ {open.body}
1097
+ </ArtifactPane>
1098
+ </aside>
1099
+ )}
1100
+ </div>
1101
+ ```
1102
+
1103
+ - **`openArtifactId` / `onOpenArtifactChange`** hold the state outside. Left
1104
+ out, `ChatExperience` keeps its own, as before. `null` is a held "nothing
1105
+ open"; only `undefined` means "not held". `onOpenArtifactChange` is called
1106
+ either way, so a host can listen without holding.
1107
+ - **`pane="none"`** draws no `ArtifactPane`, makes no room for one and never
1108
+ calls `artifact`. Pressing a card reports the id (pressing the open one again
1109
+ reports `null`).
1110
+ - **A custom card opens one too.** `renderPart`'s second argument carries
1111
+ `openArtifact(id)` and `closeArtifact()`, next to `turnId`: a caregiver in
1112
+ your card opens her profile the way an `ArtifactCard` opens a plan.
1113
+ - **`surface="panes"` goes with it.** The conversation becomes a card, your
1114
+ pane is the second one, and the two match without your having to copy the
1115
+ kit's radius and shadow.
1116
+ - **`<ArtifactPane>` stands on its own.** Outside `ChatLayout` leave `modal`
1117
+ off: it is a region, focus moves to its title when it mounts, and it neither
1118
+ traps focus nor takes Escape. Key it by the artifact's id, so opening a
1119
+ different one is a new pane — and focus moves to the new title.
1120
+
993
1121
  ### `<SystemMessage>`
994
1122
 
995
1123
  The conversation saying something about itself — not the reader, not the agent.
@@ -1266,6 +1394,63 @@ existing call sites keep working. Its `s` / `m` / `l` map to `m` / `l` / `xl`.
1266
1394
  Neither takes a dark-mode prop: the theme is a token swap on an ancestor. See
1267
1395
  [theming.md](./theming.md).
1268
1396
 
1397
+ ## Labels: every word the kit says
1398
+
1399
+ Every component takes a `labels` prop, and `ChatExperience` and `ChatTurnRow`
1400
+ take one object for all of them — grouped by the component that says it, and
1401
+ partial: anything left out stays English.
1402
+
1403
+ ```tsx
1404
+ import { ChatExperience, type ChatLabels } from "inline-chat-kit";
1405
+
1406
+ const sr: ChatLabels = {
1407
+ input: { placeholder: "Pitaj bilo šta…", send: "Pošalji poruku", copy: "Kopiraj", edit: "Izmeni" },
1408
+ approval: { once: "Dozvoli jednom", deny: "Odbij", allowedOnce: "Dozvoljeno jednom", wasDenied: "Odbijeno" },
1409
+ reasoning: { thinking: "Razmišljam", thoughtFor: "Razmišljao" },
1410
+ answerActions: { copy: "Kopiraj odgovor", regenerate: "Ponovo" },
1411
+ tasks: { progress: "{done} od {total}" },
1412
+ };
1413
+
1414
+ <ChatExperience onSend={send} labels={sr} />;
1415
+ ```
1416
+
1417
+ What counts as a word: text on screen, an `aria-label`, a `title`, a
1418
+ placeholder, text only a screen reader gets, and what is said through the live
1419
+ region. `{name}` in a value is filled in — keep it, and move it wherever the
1420
+ language wants it. `defaultLabels` is the full English set, which is also the
1421
+ list of what there is to translate.
1422
+
1423
+ It travels by context, not by a prop on every row, so the rows' memo is not
1424
+ disturbed. For pieces assembled by hand, `LabelsProvider` does what
1425
+ `ChatExperience` does:
1426
+
1427
+ ```tsx
1428
+ <LabelsProvider labels={sr}>
1429
+ {turns.map((turn) => <ChatTurnRow key={turn.id} turn={turn} /* … */ />)}
1430
+ </LabelsProvider>
1431
+ ```
1432
+
1433
+ The value is compared by content, so an object written inline costs nothing
1434
+ after the first render. A component's own `labels` prop wins over the provider
1435
+ for that one instance.
1436
+
1437
+ ### Turning off what you do not use
1438
+
1439
+ `ChatExperience` draws a theme toggle and Share in the header and a "+" menu in
1440
+ the composer. A host that has none of those says so:
1441
+
1442
+ ```tsx
1443
+ <ChatExperience
1444
+ onSend={send}
1445
+ headerActions={false} // or ["theme"], or ["share"]
1446
+ composerMenu={false} // or your own: [{ id, label, icon?, onSelect }]
1447
+ />
1448
+ ```
1449
+
1450
+ The saved highlights button is not one of the header's actions — it appears
1451
+ only once there is a highlight, and it is the only way back to them. An entry in
1452
+ your own menu with id `"attach"` and no `onSelect` opens the file picker.
1453
+
1269
1454
  ## Theming
1270
1455
 
1271
1456
  The kit reads CSS custom properties, all prefixed `--ick-`. That is the whole
@@ -1,7 +1,10 @@
1
1
  import { HTMLAttributes, ReactNode } from 'react';
2
+ import { ApprovalLabels } from '../labels/labels';
2
3
  /** Once, every time from now on, or not at all. */
3
4
  export type Decision = "once" | "always" | "denied";
4
- type Labels = Record<"once" | "always" | "deny", string> & Record<"allowedOnce" | "allowedAlways" | "wasDenied" | "pending", string>;
5
+ /** The buttons, by what they answer. `"deny"` is the button; `"denied"` the record. */
6
+ export type ApprovalChoice = "once" | "always" | "deny";
7
+ type Labels = ApprovalLabels;
5
8
  export interface ApprovalProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
6
9
  /** What is being asked. One line: "Run a command in your shell". */
7
10
  title: ReactNode;
@@ -20,6 +23,14 @@ export interface ApprovalProps extends Omit<HTMLAttributes<HTMLElement>, "title"
20
23
  onDecide?: (decision: Decision) => void;
21
24
  /** Nothing can be decided from here — a record of a decision already made. */
22
25
  readOnly?: boolean;
26
+ /**
27
+ * Which answers are offered. All three when left out.
28
+ *
29
+ * Leave out `"always"` where a standing permission means nothing — applying
30
+ * one edit to a plan is a thing that happens once. "Allow once" stays the
31
+ * primary and Deny stays first; only the button that is not offered goes.
32
+ */
33
+ choices?: ApprovalChoice[];
23
34
  labels?: Partial<Labels>;
24
35
  }
25
36
  /**
@@ -38,5 +49,5 @@ export interface ApprovalProps extends Omit<HTMLAttributes<HTMLElement>, "title"
38
49
  * decided. Leaving live controls under a decision already made invites a
39
50
  * second, contradictory one.
40
51
  */
41
- export declare function Approval({ title, description, children, decision, onDecide, readOnly, labels, className, ...rest }: ApprovalProps): import("react").JSX.Element;
52
+ export declare function Approval({ title, description, children, decision, onDecide, readOnly, choices, labels, className, ...rest }: ApprovalProps): import("react").JSX.Element;
42
53
  export {};
@@ -47,4 +47,4 @@ export interface ArtifactPaneProps extends Omit<HTMLAttributes<HTMLDivElement>,
47
47
  * conversation, and it must **not** be trapped unless the pane is covering
48
48
  * that conversation. Every host gets one of those two wrong.
49
49
  */
50
- export declare function ArtifactPane({ title, meta, children, onClose, modal, expanded, onToggleExpanded, closeLabel, expandLabel, collapseLabel, className, ...rest }: ArtifactPaneProps): import("react").JSX.Element;
50
+ export declare function ArtifactPane({ title, meta, children, onClose, modal, expanded, onToggleExpanded, closeLabel: closeProp, expandLabel: expandProp, collapseLabel: collapseProp, className, ...rest }: ArtifactPaneProps): import("react").JSX.Element;
@@ -28,6 +28,19 @@ export interface ChatLayoutProps extends HTMLAttributes<HTMLDivElement> {
28
28
  * it out and the sheet has neither, which is a sheet a thumb cannot dismiss.
29
29
  */
30
30
  onDismiss?: () => void;
31
+ /**
32
+ * Whether the conversation is a surface of its own.
33
+ *
34
+ * `"flush"` (the default): the conversation fills the layout and the pane is
35
+ * the only card — which is what a chat that owns the whole window wants.
36
+ *
37
+ * `"panes"`: two cards side by side on the page, with the same ring of space
38
+ * around both and the same gap between them. A divider would have said the
39
+ * same thing more cheaply and less truthfully: these are two surfaces, not
40
+ * one surface with a line through it. The pane the host draws itself goes
41
+ * beside a `"panes"` conversation and looks like it belongs there.
42
+ */
43
+ surface?: "flush" | "panes";
31
44
  }
32
45
  /**
33
46
  * Where the pane goes, decided once.
@@ -41,4 +54,4 @@ export interface ChatLayoutProps extends HTMLAttributes<HTMLDivElement> {
41
54
  * What is left open is the part that actually differs — what is *in* the pane.
42
55
  * See `ArtifactPane`.
43
56
  */
44
- export declare function ChatLayout({ children, pane, onDismiss, className, ...rest }: ChatLayoutProps): import("react").JSX.Element;
57
+ export declare function ChatLayout({ children, pane, onDismiss, surface, className, ...rest }: ChatLayoutProps): import("react").JSX.Element;
@@ -1,4 +1,5 @@
1
1
  import { HTMLAttributes } from 'react';
2
+ import { LabelsOf } from '../labels/labels';
2
3
  /** Something sent along with a message. */
3
4
  export interface Attachment {
4
5
  id: string;
@@ -16,9 +17,7 @@ export interface Attachment {
16
17
  /** In bytes. Shown beside the name. */
17
18
  size?: number;
18
19
  }
19
- type Labels = {
20
- remove: string;
21
- };
20
+ type Labels = LabelsOf<"attachments">;
22
21
  export interface AttachmentsProps extends Omit<HTMLAttributes<HTMLUListElement>, "onRemove"> {
23
22
  attachments: Attachment[];
24
23
  /**
@@ -1,10 +1,6 @@
1
1
  import { HTMLAttributes } from 'react';
2
- type Labels = {
3
- previous: string;
4
- next: string;
5
- /** `{index}` and `{total}` are filled in. */
6
- position: string;
7
- };
2
+ import { LabelsOf } from '../labels/labels';
3
+ type Labels = LabelsOf<"branch">;
8
4
  export interface BranchProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
9
5
  /** How many answers this turn has had. */
10
6
  total: number;
@@ -1,5 +1,6 @@
1
1
  import { HTMLAttributes, ReactNode } from 'react';
2
2
  import { WorkState } from '../stateGlyph/StateGlyph';
3
+ import { LabelsOf } from '../labels/labels';
3
4
  /** One step of a derivation: what was worked out, and the working. */
4
5
  export interface Thought {
5
6
  id: string;
@@ -10,13 +11,7 @@ export interface Thought {
10
11
  /** Defaults to `done`: a step you can read is a step that happened. */
11
12
  state?: WorkState;
12
13
  }
13
- type Labels = {
14
- /** Followed by the count: "Thought through 4 steps". */
15
- through: string;
16
- step: string;
17
- steps: string;
18
- thinking: string;
19
- };
14
+ type Labels = LabelsOf<"chain">;
20
15
  export interface ChainOfThoughtProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
21
16
  steps: Thought[];
22
17
  /** The chain as a whole. `thinking` holds it open and narrates the last step. */
@@ -1,11 +1,13 @@
1
1
  import { ReactNode } from 'react';
2
2
  import { ChatHeaderAction } from '../ChatHeader/ChatHeader';
3
3
  import { ReplyThreadPopupProps } from '../ReplyThreadPopup/ReplyThreadPopup';
4
- import { UseChatTurnsOptions } from '../useChatTurns/useChatTurns';
4
+ import { UseChatTurnsOptions, UseChatTurnsResult } from '../useChatTurns/useChatTurns';
5
5
  import { FoldMotion } from '../QuestionGroup/QuestionGroup';
6
6
  import { InlineAnimConfig } from '../ChatInput/ChatInput';
7
7
  import { TranscribeHandler } from '../voice/useVoiceInput';
8
- import { TurnPartUpdate } from '../turnParts/turnParts';
8
+ import { CustomPart, CustomPartContext, TurnPartUpdate } from '../turnParts/turnParts';
9
+ import { ComposerMenuItem } from '../ChatInput/AddCardsOverlay';
10
+ import { ChatLabels } from '../labels/labels';
9
11
  import { Answer } from '../QuestionCard/types';
10
12
  import { Decision } from '../Approval/Approval';
11
13
  /**
@@ -60,10 +62,48 @@ export interface ChatExperienceArtifact {
60
62
  }
61
63
  /** A part the host drives, handed the writer for the turn it belongs to. */
62
64
  export type PartWriter = (turnId: string, part: TurnPartUpdate) => void;
65
+ /** The header's built-in actions, by id. */
66
+ export type ChatExperienceHeaderAction = "theme" | "share";
63
67
  export interface ChatExperienceProps {
64
68
  /** Where answers come from. Return a string, a promise of one, or an async
65
- iterable of deltas — see `useChatTurns`. */
66
- onSend: UseChatTurnsOptions["onSend"];
69
+ iterable of deltas — see `useChatTurns`. Required unless `chat` is
70
+ given, which brings its own. */
71
+ onSend?: UseChatTurnsOptions["onSend"];
72
+ /**
73
+ * The conversation, held by the host.
74
+ *
75
+ * Left out, this component calls `useChatTurns` itself and nothing outside
76
+ * can reach the turns. Pass what your own `useChatTurns` returned and the
77
+ * host has the same handle this does — `updatePart` in particular, which is
78
+ * how a card in an answer changes after the answer has finished ("Apply" →
79
+ * "Applied"). `onSend` is ignored then; the hook you called has one.
80
+ */
81
+ chat?: UseChatTurnsResult;
82
+ /**
83
+ * Draws the host's own `{ kind: "custom" }` parts. See `ChatTurnRow`. Keep
84
+ * it stable — outside the component or in `useCallback` — or every row
85
+ * re-renders on every frame of an answer arriving.
86
+ */
87
+ renderPart?: (part: CustomPart, context: CustomPartContext) => ReactNode;
88
+ /**
89
+ * Every word the chat says, grouped by the component that says it. Partial:
90
+ * anything left out stays English. Reaches every piece through context, so
91
+ * an inline object costs nothing — it is compared by content.
92
+ */
93
+ labels?: ChatLabels;
94
+ /**
95
+ * The header's built-in actions. `true` (the default) is the theme toggle
96
+ * and Share; `false` is neither; a list keeps only those named. The saved
97
+ * highlights button is not one of these — it appears only once there is a
98
+ * highlight, and it is the only way back to them. `actions` still adds your
99
+ * own after whatever is left.
100
+ */
101
+ headerActions?: boolean | ChatExperienceHeaderAction[];
102
+ /**
103
+ * The composer's "+" menu. `false` takes the "+" away; a list replaces the
104
+ * built-in three (Add, Design, Connectors). Keep a list stable.
105
+ */
106
+ composerMenu?: ComposerMenuItem[] | false;
67
107
  /** Speech to text. The kit records; the host transcribes. Omit and the
68
108
  microphone does not appear. */
69
109
  onTranscribe?: TranscribeHandler;
@@ -85,6 +125,31 @@ export interface ChatExperienceProps {
85
125
  contextBase?: number;
86
126
  /** The pane beside the conversation, asked for the artifact that is open. */
87
127
  artifact?: (openId: string) => ChatExperienceArtifact | null;
128
+ /**
129
+ * Which artifact is open, held by the host. Left out (`undefined`), this
130
+ * component keeps it. `null` is "none open" — held, just empty.
131
+ */
132
+ openArtifactId?: string | null;
133
+ /** Somebody opened or closed one — a card pressed, the pane's X, a custom
134
+ card calling `openArtifact`. Called whether or not the state is held. */
135
+ onOpenArtifactChange?: (id: string | null) => void;
136
+ /**
137
+ * Who draws the pane. `"inline"` (the default): this component, beside the
138
+ * conversation, from `artifact`. `"none"`: nobody here — no pane, no room
139
+ * made for one. The cards still open and still show which one is open;
140
+ * the host draws the pane wherever its own layout wants it, usually with
141
+ * `openArtifactId` and `<ArtifactPane>`.
142
+ */
143
+ pane?: "inline" | "none";
144
+ /**
145
+ * Whether the conversation is a card of its own.
146
+ *
147
+ * `"flush"` (the default) fills the window, as before. `"panes"` makes it a
148
+ * surface on the page, so the pane beside it — the kit's or the host's own —
149
+ * is a second surface rather than a strip cut off the same one. See
150
+ * `ChatLayout`.
151
+ */
152
+ surface?: "flush" | "panes";
88
153
  /** The theme, if the host keeps it. Left off, this manages its own and puts
89
154
  a toggle in the header; `data-theme` on the root element either way, and
90
155
  unset until somebody chooses, so the kit follows the system preference —
@@ -112,4 +177,4 @@ export interface ChatExperienceProps {
112
177
  onDecideApproval?: (write: PartWriter, turnId: string, partId: string, decision: Decision) => void;
113
178
  className?: string;
114
179
  }
115
- export declare function ChatExperience({ onSend, onTranscribe, onThreadReply, title, backHref, backLabel, actions, placeholder, empty, contextTotal, contextBase, artifact, theme: themeProp, onThemeChange, cursor, selectionToggle, anchorOffset, endOffset, animationConfig, foldMotion, feedDelay, onAnswerQuestion, onEditQuestion, onDecideApproval, className, }: ChatExperienceProps): import("react").JSX.Element;
180
+ export declare function ChatExperience({ onSend, chat: hostChat, renderPart, labels, headerActions: builtInActions, composerMenu, onTranscribe, onThreadReply, title: titleProp, backHref, backLabel, actions, placeholder, empty, contextTotal, contextBase, artifact, openArtifactId: openArtifactProp, onOpenArtifactChange, pane: paneMode, surface, theme: themeProp, onThemeChange, cursor, selectionToggle, anchorOffset, endOffset, animationConfig, foldMotion, feedDelay, onAnswerQuestion, onEditQuestion, onDecideApproval, className, }: ChatExperienceProps): import("react").JSX.Element;