@aleph-alpha/chat-kit 1.11.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-C1Jnn1fK.js → CkInput.vue_vue_type_script_setup_true_lang-DCZTSiBj.js} +371 -135
  2. package/dist/adapters/responses-api.d.ts +54 -5
  3. package/dist/adapters/responses-api.d.ts.map +1 -1
  4. package/dist/adapters/responses-api.js +43 -9
  5. package/dist/chat-kit.css +19 -0
  6. package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.d.ts +30 -0
  7. package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.d.ts.map +1 -0
  8. package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts +13 -0
  9. package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts.map +1 -0
  10. package/dist/components/base/CkFeedbackDialog/index.d.ts +3 -0
  11. package/dist/components/base/CkFeedbackDialog/index.d.ts.map +1 -0
  12. package/dist/components/base/CkFeedbackDialog/types.d.ts +152 -0
  13. package/dist/components/base/CkFeedbackDialog/types.d.ts.map +1 -0
  14. package/dist/components/base/index.d.ts +2 -0
  15. package/dist/components/base/index.d.ts.map +1 -1
  16. package/dist/components/index.d.ts +2 -2
  17. package/dist/components/index.d.ts.map +1 -1
  18. package/dist/components/index.js +16 -15
  19. package/dist/composables/index.js +1 -1
  20. package/dist/handlers/index.js +2 -2
  21. package/dist/handlers/useChatKitLabels.d.ts +18 -0
  22. package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
  23. package/dist/handlers/useMessageHandler.d.ts +6 -2
  24. package/dist/handlers/useMessageHandler.d.ts.map +1 -1
  25. package/dist/helpers/conversationTree.d.ts +10 -1
  26. package/dist/helpers/conversationTree.d.ts.map +1 -1
  27. package/dist/{index-BLq6_Lt2.js → index-Ct5hD214.js} +1 -1
  28. package/dist/index.d.ts +3 -3
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +19 -18
  31. package/dist/types.d.ts +14 -0
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/{useChatKitLabels-ftC52m7f.js → useChatKitLabels-DeHxZG2a.js} +9 -0
  34. package/dist/{useMessageHandler-ZRC-gIOJ.js → useMessageHandler-CMdy-lm0.js} +36 -9
  35. package/package.json +5 -5
  36. package/src/adapters/responses-api.spec.ts +70 -0
  37. package/src/adapters/responses-api.ts +123 -12
  38. package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.spec.ts +177 -0
  39. package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.ts +258 -0
  40. package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.vue +288 -0
  41. package/src/components/base/CkFeedbackDialog/index.ts +2 -0
  42. package/src/components/base/CkFeedbackDialog/types.ts +155 -0
  43. package/src/components/base/index.ts +8 -0
  44. package/src/components/index.ts +6 -0
  45. package/src/handlers/useChatKitLabels.spec.ts +11 -0
  46. package/src/handlers/useChatKitLabels.ts +29 -0
  47. package/src/handlers/useMessageHandler.spec.ts +214 -0
  48. package/src/handlers/useMessageHandler.ts +28 -3
  49. package/src/helpers/conversationTree.spec.ts +134 -0
  50. package/src/helpers/conversationTree.ts +32 -1
  51. package/src/index.ts +7 -0
  52. package/src/types.ts +15 -0
@@ -118,6 +118,34 @@ describe('useMessageHandler', () => {
118
118
  handler.setCurrentUserMessage(' ');
119
119
  expect(() => handler.commitUserMessage()).toThrow('No message');
120
120
  });
121
+
122
+ it('attaches metadata when supplied and exposes it on the message', () => {
123
+ const handler = useMessageHandler();
124
+ handler.setCurrentUserMessage('Hello');
125
+ const msg = handler.commitUserMessage({
126
+ metadata: {
127
+ attachments: [
128
+ { name: 'report.pdf', fileId: 'f1', searchStoreId: 's1' },
129
+ ],
130
+ },
131
+ });
132
+
133
+ expect(msg.metadata).toEqual({
134
+ attachments: [
135
+ { name: 'report.pdf', fileId: 'f1', searchStoreId: 's1' },
136
+ ],
137
+ });
138
+ expect(handler.messages.value[0].metadata).toEqual(msg.metadata);
139
+ });
140
+
141
+ it('omits metadata on the returned message when none is supplied', () => {
142
+ const handler = useMessageHandler();
143
+ handler.setCurrentUserMessage('Hello');
144
+ const msg = handler.commitUserMessage();
145
+
146
+ expect('metadata' in msg).toBe(false);
147
+ expect('metadata' in handler.messages.value[0]).toBe(false);
148
+ });
121
149
  });
122
150
 
123
151
  describe('updateMessage', () => {
@@ -232,6 +260,61 @@ describe('useMessageHandler', () => {
232
260
  expect(getText(handler.messages.value[1].content)).toBe('Regenerated');
233
261
  });
234
262
 
263
+ it('preserves existing metadata when the update omits it', () => {
264
+ const handler = useMessageHandler();
265
+ handler.setCurrentUserMessage('Hello');
266
+ const msg = handler.commitUserMessage({
267
+ metadata: { attachments: [{ name: 'report.pdf' }] },
268
+ });
269
+
270
+ handler.updateMessage({
271
+ id: msg.id,
272
+ content: [text('Edited')],
273
+ role: 'user',
274
+ status: 'completed',
275
+ });
276
+
277
+ expect(handler.getMessageById(msg.id)?.metadata).toEqual({
278
+ attachments: [{ name: 'report.pdf' }],
279
+ });
280
+ });
281
+
282
+ it('overwrites metadata when explicitly supplied on update', () => {
283
+ const handler = useMessageHandler();
284
+ handler.setCurrentUserMessage('Hello');
285
+ const msg = handler.commitUserMessage({
286
+ metadata: { attachments: [{ name: 'old.pdf' }] },
287
+ });
288
+
289
+ handler.updateMessage({
290
+ id: msg.id,
291
+ content: [text('Hello')],
292
+ role: 'user',
293
+ status: 'completed',
294
+ metadata: { attachments: [{ name: 'new.pdf' }] },
295
+ });
296
+
297
+ expect(handler.getMessageById(msg.id)?.metadata).toEqual({
298
+ attachments: [{ name: 'new.pdf' }],
299
+ });
300
+ });
301
+
302
+ it('upserts a new message with metadata when supplied', () => {
303
+ const handler = useMessageHandler();
304
+ handler.updateMessage({
305
+ id: 'u-new',
306
+ content: [text('Hello')],
307
+ role: 'user',
308
+ status: 'completed',
309
+ metadata: { attachments: [{ name: 'fresh.pdf' }] },
310
+ });
311
+
312
+ const stored = handler.getMessageById('u-new');
313
+ expect(stored?.metadata).toEqual({
314
+ attachments: [{ name: 'fresh.pdf' }],
315
+ });
316
+ });
317
+
235
318
  it('does not truncate when updating the last message', () => {
236
319
  const handler = useMessageHandler();
237
320
  handler.setTree(
@@ -551,6 +634,137 @@ describe('useMessageHandler', () => {
551
634
  });
552
635
  });
553
636
 
637
+ describe('cutBranchAfter', () => {
638
+ it('removes every message that comes after the target on the active path', () => {
639
+ const handler = useMessageHandler();
640
+ handler.setTree(
641
+ makeTree(
642
+ {
643
+ u1: makeTreeMessage({ id: 'u1', content: [text('Hello')] }),
644
+ a1: makeTreeMessage({
645
+ id: 'a1',
646
+ parentId: 'u1',
647
+ role: 'assistant',
648
+ content: [text('Hi')],
649
+ }),
650
+ u2: makeTreeMessage({
651
+ id: 'u2',
652
+ parentId: 'a1',
653
+ content: [text('Follow-up')],
654
+ }),
655
+ a2: makeTreeMessage({
656
+ id: 'a2',
657
+ parentId: 'u2',
658
+ role: 'assistant',
659
+ content: [text('Answer')],
660
+ }),
661
+ },
662
+ 'u1',
663
+ ),
664
+ );
665
+
666
+ handler.cutBranchAfter('u2');
667
+
668
+ expect(handler.messages.value.map((m) => m.id)).toEqual([
669
+ 'u1',
670
+ 'a1',
671
+ 'u2',
672
+ ]);
673
+ expect(handler.getMessageById('a2')).toBeUndefined();
674
+ });
675
+
676
+ it('keeps the target message and makes it the new leaf', () => {
677
+ const handler = useMessageHandler();
678
+ handler.setTree(
679
+ makeTree(
680
+ {
681
+ u1: makeTreeMessage({ id: 'u1', content: [text('Hello')] }),
682
+ a1: makeTreeMessage({
683
+ id: 'a1',
684
+ parentId: 'u1',
685
+ role: 'assistant',
686
+ content: [text('Hi')],
687
+ }),
688
+ u2: makeTreeMessage({
689
+ id: 'u2',
690
+ parentId: 'a1',
691
+ content: [text('Follow-up')],
692
+ }),
693
+ },
694
+ 'u1',
695
+ ),
696
+ );
697
+
698
+ handler.cutBranchAfter('a1');
699
+
700
+ expect(handler.getMessageById('a1')).toBeDefined();
701
+ expect(handler.currentLeafId.value).toBe('a1');
702
+ });
703
+
704
+ it('drops sibling branches that share the cut ancestor', () => {
705
+ const handler = useMessageHandler();
706
+ handler.setTree(
707
+ makeTree(
708
+ {
709
+ u1: makeTreeMessage({ id: 'u1', content: [text('Hello')] }),
710
+ a1: makeTreeMessage({
711
+ id: 'a1',
712
+ parentId: 'u1',
713
+ role: 'assistant',
714
+ content: [text('Hi')],
715
+ }),
716
+ a2: makeTreeMessage({
717
+ id: 'a2',
718
+ parentId: 'u1',
719
+ role: 'assistant',
720
+ content: [text('Hey')],
721
+ }),
722
+ u2: makeTreeMessage({
723
+ id: 'u2',
724
+ parentId: 'a2',
725
+ content: [text('Follow-up')],
726
+ }),
727
+ },
728
+ 'u1',
729
+ ),
730
+ );
731
+
732
+ handler.cutBranchAfter('u1');
733
+
734
+ expect(handler.getMessageById('a1')).toBeUndefined();
735
+ expect(handler.getMessageById('a2')).toBeUndefined();
736
+ expect(handler.getMessageById('u2')).toBeUndefined();
737
+ expect(handler.messages.value.map((m) => m.id)).toEqual(['u1']);
738
+ });
739
+
740
+ it('is a no-op when the target has no descendants', () => {
741
+ const handler = useMessageHandler();
742
+ handler.setCurrentUserMessage('Hello');
743
+ const msg = handler.commitUserMessage();
744
+
745
+ handler.cutBranchAfter(msg.id);
746
+
747
+ expect(handler.messages.value).toHaveLength(1);
748
+ expect(handler.messages.value[0].id).toBe(msg.id);
749
+ });
750
+
751
+ it('is a no-op for a nonexistent id', () => {
752
+ const handler = useMessageHandler();
753
+ handler.setCurrentUserMessage('Hello');
754
+ handler.commitUserMessage();
755
+ handler.updateMessage({
756
+ id: 'a1',
757
+ content: [text('Reply')],
758
+ role: 'assistant',
759
+ status: 'completed',
760
+ });
761
+
762
+ handler.cutBranchAfter('nonexistent');
763
+
764
+ expect(handler.messages.value).toHaveLength(2);
765
+ });
766
+ });
767
+
554
768
  describe('currentLeafId', () => {
555
769
  it('is null when there are no messages', () => {
556
770
  const handler = useMessageHandler();
@@ -2,6 +2,7 @@ import {
2
2
  generateId,
3
3
  addNode as treeAddNode,
4
4
  createEmptyTree,
5
+ cutBranchAfter as treeCutBranchAfter,
5
6
  getLinearPath,
6
7
  navigate as treeNavigate,
7
8
  updateNode,
@@ -11,6 +12,7 @@ import type {
11
12
  BaseMessage,
12
13
  ConversationTree,
13
14
  MessageContent,
15
+ MessageMetadata,
14
16
  MessageRole,
15
17
  MessageStatus,
16
18
  TreeMessage,
@@ -36,7 +38,9 @@ export const useMessageHandler = () => {
36
38
  currentUserMessage.value = '';
37
39
  }
38
40
 
39
- function commitUserMessage(): BaseMessage {
41
+ function commitUserMessage(opts?: {
42
+ metadata?: MessageMetadata;
43
+ }): BaseMessage {
40
44
  const id = generateId();
41
45
  const text = currentUserMessage.value.trim();
42
46
  if (!text) throw new Error('No message');
@@ -54,18 +58,21 @@ export const useMessageHandler = () => {
54
58
  content,
55
59
  status: 'completed',
56
60
  createdAt: new Date(),
61
+ ...(opts?.metadata !== undefined ? { metadata: opts.metadata } : {}),
57
62
  };
58
63
 
59
64
  treeAddNode(tree, node);
60
65
  clearCurrentUserMessage();
61
66
 
62
- return {
67
+ const base: BaseMessage = {
63
68
  id: node.id,
64
69
  role: node.role,
65
70
  content: node.content,
66
71
  createdAt: node.createdAt,
67
72
  status: node.status,
68
73
  };
74
+ if (node.metadata !== undefined) base.metadata = node.metadata;
75
+ return base;
69
76
  }
70
77
 
71
78
  function updateMessage(message: {
@@ -73,6 +80,7 @@ export const useMessageHandler = () => {
73
80
  content: MessageContent[];
74
81
  role: MessageRole;
75
82
  status: MessageStatus;
83
+ metadata?: MessageMetadata;
76
84
  }) {
77
85
  const existing = tree.nodes[message.id];
78
86
 
@@ -81,6 +89,9 @@ export const useMessageHandler = () => {
81
89
  content: message.content,
82
90
  role: message.role,
83
91
  status: message.status,
92
+ ...(message.metadata !== undefined
93
+ ? { metadata: message.metadata }
94
+ : {}),
84
95
  });
85
96
  } else {
86
97
  const node: TreeMessage = {
@@ -92,6 +103,9 @@ export const useMessageHandler = () => {
92
103
  content: message.content,
93
104
  status: message.status,
94
105
  createdAt: new Date(),
106
+ ...(message.metadata !== undefined
107
+ ? { metadata: message.metadata }
108
+ : {}),
95
109
  };
96
110
  treeAddNode(tree, node);
97
111
  }
@@ -100,13 +114,15 @@ export const useMessageHandler = () => {
100
114
  function getMessageById(id: string): BaseMessage | undefined {
101
115
  const node = tree.nodes[id];
102
116
  if (!node) return undefined;
103
- return {
117
+ const base: BaseMessage = {
104
118
  id: node.id,
105
119
  role: node.role,
106
120
  content: node.content,
107
121
  createdAt: node.createdAt,
108
122
  status: node.status,
123
+ ...(node.metadata !== undefined ? { metadata: node.metadata } : {}),
109
124
  };
125
+ return base;
110
126
  }
111
127
 
112
128
  function updateMessageId(oldId: string, newId: string) {
@@ -135,6 +151,14 @@ export const useMessageHandler = () => {
135
151
  treeNavigate(tree, targetId);
136
152
  }
137
153
 
154
+ /**
155
+ * Drop every message that comes after `messageId` on every branch beneath
156
+ * it. The target message is kept and becomes the new leaf of its branch.
157
+ */
158
+ function cutBranchAfter(messageId: string) {
159
+ treeCutBranchAfter(tree, messageId);
160
+ }
161
+
138
162
  return {
139
163
  messages: messages as Ref<BaseMessage[]>,
140
164
  currentUserMessage: readonly(currentUserMessage),
@@ -148,5 +172,6 @@ export const useMessageHandler = () => {
148
172
  clearMessages,
149
173
  setTree,
150
174
  navigate,
175
+ cutBranchAfter,
151
176
  };
152
177
  };
@@ -7,6 +7,7 @@ import type {
7
7
  import {
8
8
  addNode,
9
9
  createEmptyTree,
10
+ cutBranchAfter,
10
11
  getLinearPath,
11
12
  navigate,
12
13
  updateNode,
@@ -193,6 +194,35 @@ describe('getLinearPath', () => {
193
194
  expect(u1Msg.status).toBe('completed');
194
195
  expect(u1Msg.createdAt).toEqual(new Date('2025-01-01'));
195
196
  });
197
+
198
+ it('forwards node metadata onto the BaseMessage projection', () => {
199
+ const tree = createEmptyTree();
200
+ addNode(
201
+ tree,
202
+ makeTreeMessage({
203
+ id: 'u1',
204
+ content: [text('Hello')],
205
+ metadata: {
206
+ attachments: [
207
+ { name: 'report.pdf', fileId: 'f1', searchStoreId: 's1' },
208
+ ],
209
+ },
210
+ }),
211
+ );
212
+
213
+ const path = getLinearPath(tree);
214
+ expect(path[0].metadata).toEqual({
215
+ attachments: [{ name: 'report.pdf', fileId: 'f1', searchStoreId: 's1' }],
216
+ });
217
+ });
218
+
219
+ it('omits metadata when the node has none', () => {
220
+ const tree = createEmptyTree();
221
+ addNode(tree, makeTreeMessage({ id: 'u1', content: [text('Hello')] }));
222
+
223
+ const path = getLinearPath(tree);
224
+ expect('metadata' in path[0]).toBe(false);
225
+ });
196
226
  });
197
227
 
198
228
  describe('addNode', () => {
@@ -287,6 +317,38 @@ describe('updateNode', () => {
287
317
  updateNode(tree, 'nonexistent', { content: [text('x')] });
288
318
  expect(Object.keys(tree.nodes).length).toBe(nodeCount);
289
319
  });
320
+
321
+ it('sets metadata when supplied', () => {
322
+ const tree = makeBranchingTree();
323
+ updateNode(tree, 'u1', {
324
+ metadata: {
325
+ attachments: [{ name: 'report.pdf', fileId: 'f1' }],
326
+ },
327
+ });
328
+ expect(tree.nodes.u1.metadata).toEqual({
329
+ attachments: [{ name: 'report.pdf', fileId: 'f1' }],
330
+ });
331
+ });
332
+
333
+ it('clears metadata when explicitly set to undefined', () => {
334
+ const tree = makeBranchingTree();
335
+ tree.nodes.u1.metadata = {
336
+ attachments: [{ name: 'old.pdf' }],
337
+ };
338
+ updateNode(tree, 'u1', { metadata: undefined });
339
+ expect(tree.nodes.u1.metadata).toBeUndefined();
340
+ });
341
+
342
+ it('leaves metadata untouched when the key is absent from updates', () => {
343
+ const tree = makeBranchingTree();
344
+ tree.nodes.u1.metadata = {
345
+ attachments: [{ name: 'keep.pdf' }],
346
+ };
347
+ updateNode(tree, 'u1', { content: [text('Updated')] });
348
+ expect(tree.nodes.u1.metadata).toEqual({
349
+ attachments: [{ name: 'keep.pdf' }],
350
+ });
351
+ });
290
352
  });
291
353
 
292
354
  describe('updateNodeId', () => {
@@ -385,6 +447,78 @@ describe('navigate', () => {
385
447
  });
386
448
  });
387
449
 
450
+ describe('cutBranchAfter', () => {
451
+ it('removes every descendant of the target node', () => {
452
+ const tree = makeBranchingTree();
453
+
454
+ cutBranchAfter(tree, 'u2');
455
+
456
+ expect(tree.nodes.a2).toBeUndefined();
457
+ expect(tree.nodes.a3).toBeUndefined();
458
+ });
459
+
460
+ it('keeps the target node and clears its children', () => {
461
+ const tree = makeBranchingTree();
462
+
463
+ cutBranchAfter(tree, 'u2');
464
+
465
+ expect(tree.nodes.u2).toBeDefined();
466
+ expect(tree.nodes.u2.childrenIds).toEqual([]);
467
+ expect(tree.nodes.u2.activeChildIndex).toBe(0);
468
+ });
469
+
470
+ it('makes the target node the leaf of the active path', () => {
471
+ const tree = makeBranchingTree();
472
+
473
+ cutBranchAfter(tree, 'u2');
474
+
475
+ const path = getLinearPath(tree);
476
+ expect(path.map((m) => m.id)).toEqual(['u1', 'a1', 'u2']);
477
+ });
478
+
479
+ it('removes the entire subtree, including grandchildren', () => {
480
+ const tree = makeBranchingTree();
481
+
482
+ cutBranchAfter(tree, 'u1');
483
+
484
+ expect(Object.keys(tree.nodes)).toEqual(['u1']);
485
+ expect(tree.nodes.u1.childrenIds).toEqual([]);
486
+ });
487
+
488
+ it('only cuts descendants — sibling branches are untouched', () => {
489
+ const tree = makeBranchingTree();
490
+
491
+ cutBranchAfter(tree, 'a1');
492
+
493
+ expect(tree.nodes.u2).toBeUndefined();
494
+ expect(tree.nodes.a2).toBeUndefined();
495
+ expect(tree.nodes.a3).toBeUndefined();
496
+
497
+ expect(tree.nodes.a1b).toBeDefined();
498
+ expect(tree.nodes.u3).toBeDefined();
499
+ expect(tree.nodes.u1.childrenIds).toEqual(['a1', 'a1b']);
500
+ });
501
+
502
+ it('is a no-op when the node has no children', () => {
503
+ const tree = makeBranchingTree();
504
+ const nodeCount = Object.keys(tree.nodes).length;
505
+
506
+ cutBranchAfter(tree, 'a2');
507
+
508
+ expect(Object.keys(tree.nodes).length).toBe(nodeCount);
509
+ expect(tree.nodes.a2.childrenIds).toEqual([]);
510
+ });
511
+
512
+ it('is a no-op for a nonexistent id', () => {
513
+ const tree = makeBranchingTree();
514
+ const before = Object.keys(tree.nodes).sort();
515
+
516
+ cutBranchAfter(tree, 'nonexistent');
517
+
518
+ expect(Object.keys(tree.nodes).sort()).toEqual(before);
519
+ });
520
+ });
521
+
388
522
  describe('integration: full branching scenario', () => {
389
523
  it('simulates send → regenerate → navigate workflow', () => {
390
524
  const tree = createEmptyTree();
@@ -25,6 +25,7 @@ export function getLinearPath(tree: ConversationTree): BaseMessage[] {
25
25
  content: node.content,
26
26
  createdAt: node.createdAt,
27
27
  status: node.status,
28
+ ...(node.metadata !== undefined ? { metadata: node.metadata } : {}),
28
29
  };
29
30
 
30
31
  if (node.parentId !== null) {
@@ -61,7 +62,9 @@ export function addNode(tree: ConversationTree, node: TreeMessage): void {
61
62
  export function updateNode(
62
63
  tree: ConversationTree,
63
64
  id: string,
64
- updates: Partial<Pick<TreeMessage, 'content' | 'status' | 'role'>>,
65
+ updates: Partial<
66
+ Pick<TreeMessage, 'content' | 'status' | 'role' | 'metadata'>
67
+ >,
65
68
  ): void {
66
69
  const node = tree.nodes[id];
67
70
  if (!node) return;
@@ -69,6 +72,9 @@ export function updateNode(
69
72
  if (updates.content !== undefined) node.content = updates.content;
70
73
  if (updates.status !== undefined) node.status = updates.status;
71
74
  if (updates.role !== undefined) node.role = updates.role;
75
+ // `metadata` is intentionally allowed through with an `undefined` value so
76
+ // callers can clear it; the call site checks for `'metadata' in updates`.
77
+ if ('metadata' in updates) node.metadata = updates.metadata;
72
78
  }
73
79
 
74
80
  export function updateNodeId(
@@ -99,6 +105,31 @@ export function updateNodeId(
99
105
  if (tree.rootId === oldId) tree.rootId = newId;
100
106
  }
101
107
 
108
+ /**
109
+ * Remove every descendant of the node with the given id, keeping the node
110
+ * itself intact. The node's `childrenIds` is cleared and `activeChildIndex`
111
+ * is reset to 0, which makes the node the new leaf of its branch.
112
+ *
113
+ * Useful when editing a message and discarding the conversation that was
114
+ * generated after it.
115
+ */
116
+ export function cutBranchAfter(tree: ConversationTree, id: string): void {
117
+ const node = tree.nodes[id];
118
+ if (!node) return;
119
+
120
+ const stack: string[] = [...node.childrenIds];
121
+ while (stack.length > 0) {
122
+ const currentId = stack.pop() as string;
123
+ const current = tree.nodes[currentId];
124
+ if (!current) continue;
125
+ stack.push(...current.childrenIds);
126
+ delete tree.nodes[currentId];
127
+ }
128
+
129
+ node.childrenIds = [];
130
+ node.activeChildIndex = 0;
131
+ }
132
+
102
133
  /**
103
134
  * Navigate to a different branch by updating the parent's activeChildIndex
104
135
  * to point at the target sibling.
package/src/index.ts CHANGED
@@ -67,6 +67,7 @@ export {
67
67
  CkConversationLayout,
68
68
  CkCopyMessageActionButton,
69
69
  CkFeedbackButton,
70
+ CkFeedbackDialog,
70
71
  CkFileItemsList,
71
72
  CkHistory,
72
73
  CkHistoryItem,
@@ -86,6 +87,11 @@ export type {
86
87
  CkConversationLayoutProps,
87
88
  CkConversationProps,
88
89
  CkFeedbackButtonProps,
90
+ CkFeedbackDialogEmits,
91
+ CkFeedbackDialogProps,
92
+ CkFeedbackDialogSubmitPayload,
93
+ CkFeedbackReason,
94
+ CkFeedbackSentiment,
89
95
  CkFeedbackValue,
90
96
  CkFileEntry,
91
97
  CkFileItemsListEmits,
@@ -107,6 +113,7 @@ export type {
107
113
  ConversationTree,
108
114
  MessageContent,
109
115
  MessageContentStatus,
116
+ MessageMetadata,
110
117
  MessageReasoningContent,
111
118
  MessageRole,
112
119
  MessageStatus,
package/src/types.ts CHANGED
@@ -68,6 +68,19 @@ export type MessageContent =
68
68
  | MessageToolContent
69
69
  | MessageReasoningContent;
70
70
 
71
+ /**
72
+ * Open-ended metadata bag carried alongside a message. chat-kit is
73
+ * intentionally agnostic about its contents — adapter authors
74
+ * (`responsesToTree`, …) and message handlers preserve the value verbatim
75
+ * through the tree → linear path projection, but never inspect it.
76
+ *
77
+ * Consumers attach their own well-known keys (attachments, citations,
78
+ * ratings, …) and define their own typed projections at the read site
79
+ * rather than extending this base type. That keeps the handler API stable
80
+ * even as the shape of what gets stored on a message evolves.
81
+ */
82
+ export type MessageMetadata = Record<string, unknown>;
83
+
71
84
  export interface BaseMessage {
72
85
  id: string;
73
86
  role: MessageRole;
@@ -76,6 +89,7 @@ export interface BaseMessage {
76
89
  status: MessageStatus;
77
90
  siblingIds?: string[];
78
91
  siblingIndex?: number;
92
+ metadata?: MessageMetadata;
79
93
  }
80
94
 
81
95
  export interface TreeMessage {
@@ -87,6 +101,7 @@ export interface TreeMessage {
87
101
  content: MessageContent[];
88
102
  status: MessageStatus;
89
103
  createdAt: Date;
104
+ metadata?: MessageMetadata;
90
105
  }
91
106
 
92
107
  export interface ConversationTree {