inline-chat-kit 0.56.0 → 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,35 @@ 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
+
11
40
  ## 0.56.0 — 2026-09-22
12
41
 
13
42
  Four things that kept the kit out of a real host app: an answer could only
package/README.md CHANGED
@@ -461,6 +461,10 @@ and the kit does not guess its shape.
461
461
  `ChatExperience` calls `useChatTurns` itself, so a host using it passes its own
462
462
  instead — `chat={useChatTurns({ onSend })}` — to hold `updatePart`.
463
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
+
464
468
  #### Citing a source from the prose
465
469
 
466
470
  `[^1]` in the answer is a **citation marker**, and the number is a position in
@@ -1028,6 +1032,7 @@ between products, and the only part.
1028
1032
  | `onOpen` | `(id: string) => void` | Without one the card is a record, not a control |
1029
1033
  | `<ArtifactPane>` `modal` | `boolean` | See below |
1030
1034
  | `<ChatLayout>` `pane` | `({ narrow }) => ReactNode` | |
1035
+ | `<ChatLayout>` `surface` | `"flush" \| "panes"` | One surface, or two. See below |
1031
1036
 
1032
1037
  **`modal` is the one prop that changes behaviour, and it is not about
1033
1038
  position.** Covering the conversation changes what the pane *is*: focus has to
@@ -1041,6 +1046,78 @@ That is the part worth having in a library rather than the box. On open, focus
1041
1046
  moves to the pane's heading — not into its first control, which would skip what
1042
1047
  the thing is — and it is **not** trapped unless the pane is covering the chat.
1043
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
+
1044
1121
  ### `<SystemMessage>`
1045
1122
 
1046
1123
  The conversation saying something about itself — not the reader, not the agent.
@@ -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;
@@ -5,7 +5,7 @@ import { UseChatTurnsOptions, UseChatTurnsResult } from '../useChatTurns/useChat
5
5
  import { FoldMotion } from '../QuestionGroup/QuestionGroup';
6
6
  import { InlineAnimConfig } from '../ChatInput/ChatInput';
7
7
  import { TranscribeHandler } from '../voice/useVoiceInput';
8
- import { CustomPart, TurnPartUpdate } from '../turnParts/turnParts';
8
+ import { CustomPart, CustomPartContext, TurnPartUpdate } from '../turnParts/turnParts';
9
9
  import { ComposerMenuItem } from '../ChatInput/AddCardsOverlay';
10
10
  import { ChatLabels } from '../labels/labels';
11
11
  import { Answer } from '../QuestionCard/types';
@@ -84,9 +84,7 @@ export interface ChatExperienceProps {
84
84
  * it stable — outside the component or in `useCallback` — or every row
85
85
  * re-renders on every frame of an answer arriving.
86
86
  */
87
- renderPart?: (part: CustomPart, context: {
88
- turnId: string;
89
- }) => ReactNode;
87
+ renderPart?: (part: CustomPart, context: CustomPartContext) => ReactNode;
90
88
  /**
91
89
  * Every word the chat says, grouped by the component that says it. Partial:
92
90
  * anything left out stays English. Reaches every piece through context, so
@@ -127,6 +125,31 @@ export interface ChatExperienceProps {
127
125
  contextBase?: number;
128
126
  /** The pane beside the conversation, asked for the artifact that is open. */
129
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";
130
153
  /** The theme, if the host keeps it. Left off, this manages its own and puts
131
154
  a toggle in the header; `data-theme` on the root element either way, and
132
155
  unset until somebody chooses, so the kit follows the system preference —
@@ -154,4 +177,4 @@ export interface ChatExperienceProps {
154
177
  onDecideApproval?: (write: PartWriter, turnId: string, partId: string, decision: Decision) => void;
155
178
  className?: string;
156
179
  }
157
- export declare function ChatExperience({ onSend, chat: hostChat, renderPart, labels, headerActions: builtInActions, composerMenu, onTranscribe, onThreadReply, title: titleProp, 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;